From a8c87e3bf10f93dec77cba5eb5f844e59f3dc397 Mon Sep 17 00:00:00 2001 From: Readixyee <49241765+Readixyee@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:41:06 +0200 Subject: [PATCH] Sprite system (#512) ## Description: This adds the spriteLoader that loads and caches sprites to be used in the unitlayer for rendering units as well as sprites for all units and an alternative mirv sprite to show what can be done using sprites color codes for grayscaled sprites: b4b4b4 = territory color 464646 = border color 828282 = spawnhighlight color The sprites are grayscaled and recolored to have a single base png for all units Fixes #505 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: Readix --- resources/sprites/atombomb.png | Bin 0 -> 133 bytes resources/sprites/hydrogenbomb.png | Bin 0 -> 161 bytes resources/sprites/mirv.png | Bin 0 -> 188 bytes resources/sprites/mirv2.png | Bin 0 -> 248 bytes resources/sprites/samMissile.png | Bin 0 -> 102 bytes resources/sprites/tradeship.png | Bin 0 -> 104 bytes resources/sprites/transportship.png | Bin 0 -> 104 bytes resources/sprites/warship.png | Bin 0 -> 154 bytes src/client/graphics/SpriteLoader.ts | 136 ++++++++++++++++++ src/client/graphics/layers/UnitLayer.ts | 177 ++++-------------------- 10 files changed, 166 insertions(+), 147 deletions(-) create mode 100644 resources/sprites/atombomb.png create mode 100644 resources/sprites/hydrogenbomb.png create mode 100644 resources/sprites/mirv.png create mode 100644 resources/sprites/mirv2.png create mode 100644 resources/sprites/samMissile.png create mode 100644 resources/sprites/tradeship.png create mode 100644 resources/sprites/transportship.png create mode 100644 resources/sprites/warship.png create mode 100644 src/client/graphics/SpriteLoader.ts diff --git a/resources/sprites/atombomb.png b/resources/sprites/atombomb.png new file mode 100644 index 0000000000000000000000000000000000000000..bc91d698a05a728bac5370c8b5d140d1c1239500 GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VKAtX)Arj%e7Y_0=7_hKjh&tK8 z$F?O%dXl~e|D9F*p;pQvU%NQ2mdO1n5@TRr)iASv=F;*T6PQ^N6iQYm9Ex53b++1( g9ou4K|9RV-@PA2ACjCxNJ+Wi_hoH{zXR{gsZwN*8x z!JW-|%34>3#{8)l6xLNc?cVf9;>gPz%WZdl`s$kEz5DiWhSlpGCr2-=djhnR!PC{x JWt~$(69DlbJTm|Q literal 0 HcmV?d00001 diff --git a/resources/sprites/mirv.png b/resources/sprites/mirv.png new file mode 100644 index 0000000000000000000000000000000000000000..f29cf7cda5b96aedb16c62b5d883e31fa5132bdc GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3I*W=|K#5DWjkR}S(XP~cziuOsJkIQbnAn%F$Ec+1s00sQXlx_=rKC&ZaFndh)=I>24c+?#NC zpM&iWdpDPrQyz)!5L511?g|yY<_2!tyG?tp0Z|efL%_ n+j?iEA+yuxGpBg1GX0y^Mb4am?bkk_a~M2b{an^LB{Ts5@$^W> literal 0 HcmV?d00001 diff --git a/resources/sprites/mirv2.png b/resources/sprites/mirv2.png new file mode 100644 index 0000000000000000000000000000000000000000..7a403c57fc675fe65751b3021f33884a3cced4d8 GIT binary patch literal 248 zcmVZ#r-P=8WX)vLHp2H@VEW$xf)&G(;9J3{%3$nV@n<)f{gV y6>hH4r9J5YHhk8bZ-9?eqXcN0XDCgTe~DWM4f^zR*C literal 0 HcmV?d00001 diff --git a/resources/sprites/tradeship.png b/resources/sprites/tradeship.png new file mode 100644 index 0000000000000000000000000000000000000000..bb61aa4207991a6f86055d36d5fa80c04354cfc7 GIT binary patch literal 104 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$61|)m))t&+=eNPw15RT~Nl!hkV=xu+#fBcxp zQh1QXBjM5#PoB!kN*)DHw;2pR>>fil+7_R@2QGBvn4m%UA5 zDLlyHp`fqdpLl7BCyxTB+YAODb`PfAUO8z@4B^Y9t}nGsT?5q0;OXk;vd$@?2>{rB B8>s*Q literal 0 HcmV?d00001 diff --git a/resources/sprites/warship.png b/resources/sprites/warship.png new file mode 100644 index 0000000000000000000000000000000000000000..489034625f5972765668c21ac21e5428581aa0ac GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhiJmTwArj$JCtl=bP~>nfwNFcw zNK3TmRa3p;QFMI%7e?0S6)n5<1!wA{Dlv4VnT41}ipb4y42UT>cyS^#>qXm#TxU)s>(4uP$={h_<*z>Bzxf@YWelFKelF{r5}E)< C{5F>W literal 0 HcmV?d00001 diff --git a/src/client/graphics/SpriteLoader.ts b/src/client/graphics/SpriteLoader.ts new file mode 100644 index 000000000..d7836efb8 --- /dev/null +++ b/src/client/graphics/SpriteLoader.ts @@ -0,0 +1,136 @@ +import { Colord } from "colord"; +import atomBombSprite from "../../../resources/sprites/atombomb.png"; +import hydrogenBombSprite from "../../../resources/sprites/hydrogenbomb.png"; +import mirvSprite from "../../../resources/sprites/mirv2.png"; +import samMissileSprite from "../../../resources/sprites/samMissile.png"; +import tradeShipSprite from "../../../resources/sprites/tradeship.png"; +import transportShipSprite from "../../../resources/sprites/transportship.png"; +import warshipSprite from "../../../resources/sprites/warship.png"; +import { Theme } from "../../core/configuration/Config"; +import { UnitType } from "../../core/game/Game"; +import { UnitView } from "../../core/game/GameView"; + +const SPRITE_CONFIG: Partial> = { + [UnitType.TransportShip]: transportShipSprite, + [UnitType.Warship]: warshipSprite, + [UnitType.SAMMissile]: samMissileSprite, + [UnitType.AtomBomb]: atomBombSprite, + [UnitType.HydrogenBomb]: hydrogenBombSprite, + [UnitType.TradeShip]: tradeShipSprite, + [UnitType.MIRV]: mirvSprite, +}; + +const spriteMap: Map = new Map(); + +// preload all images +export const loadAllSprites = async (): Promise => { + const entries = Object.entries(SPRITE_CONFIG); + const totalSprites = entries.length; + let loadedCount = 0; + + await Promise.all( + entries.map(async ([unitType, url]) => { + const typedUnitType = unitType as UnitType; + + if (!url || url === "") { + console.warn(`No sprite URL for ${typedUnitType}, skipping...`); + return; + } + + try { + const img = new Image(); + img.crossOrigin = "anonymous"; + img.src = url; + + await new Promise((resolve, reject) => { + img.onload = () => resolve(); + img.onerror = (err) => reject(err); + }); + + const bitmap = await createImageBitmap(img); + spriteMap.set(typedUnitType, bitmap); + loadedCount++; + + if (loadedCount === totalSprites) { + console.log("All sprites loaded."); + } + } catch (err) { + console.error(`Failed to load sprite for ${typedUnitType}:`, err); + } + }), + ); +}; + +const getSpriteForUnit = (unitType: UnitType): ImageBitmap | null => { + return spriteMap.get(unitType) ?? null; +}; + +export const isSpriteReady = (unitType: UnitType): boolean => { + return spriteMap.has(unitType); +}; + +const coloredSpriteCache: Map = new Map(); + +// puts the sprite in an canvas colors it and caches the colored canvas +export const getColoredSprite = ( + unit: UnitView, + theme: Theme, + customTerritoryColor?: Colord, +): HTMLCanvasElement => { + const owner = unit.owner(); + const territoryColor = customTerritoryColor ?? theme.territoryColor(owner); + const borderColor = theme.borderColor(owner); + const spawnHighlightColor = theme.spawnHighlightColor(); + const colorKey = customTerritoryColor + ? customTerritoryColor.toRgbString() + : ""; + const key = owner.id() + unit.type() + colorKey; + + if (coloredSpriteCache.has(key)) { + return coloredSpriteCache.get(key)!; + } + + const sprite = getSpriteForUnit(unit.type()); + + const territoryRgb = territoryColor.toRgb(); + const borderRgb = borderColor.toRgb(); + const spawnHighlightRgb = spawnHighlightColor.toRgb(); + + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d")!; + canvas.width = sprite.width; + canvas.height = sprite.height; + + ctx.drawImage(sprite, 0, 0); + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + + if (r === 180 && g === 180 && b === 180) { + data[i] = territoryRgb.r; + data[i + 1] = territoryRgb.g; + data[i + 2] = territoryRgb.b; + } + + if (r === 70 && g === 70 && b === 70) { + data[i] = borderRgb.r; + data[i + 1] = borderRgb.g; + data[i + 2] = borderRgb.b; + } + + if (r === 130 && g === 130 && b === 130) { + data[i] = spawnHighlightRgb.r; + data[i + 1] = spawnHighlightRgb.g; + data[i + 2] = spawnHighlightRgb.b; + } + } + + ctx.putImageData(imageData, 0.5, 0.5); + coloredSpriteCache.set(key, canvas); + return canvas; +}; diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 44b645988..d03957e04 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -19,6 +19,8 @@ import { MoveWarshipIntentEvent } from "../../Transport"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; +import { getColoredSprite, loadAllSprites } from "../SpriteLoader"; + enum Relationship { Self, Ally, @@ -77,6 +79,8 @@ export class UnitLayer implements Layer { this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e)); this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelectionChange(e)); this.redraw(); + + loadAllSprites(); } /** @@ -258,56 +262,13 @@ export class UnitLayer implements Layer { this.clearCell(this.game.x(t), this.game.y(t)); } - if (!unit.isActive()) { - return; - } - - let outerColor = this.theme.territoryColor(unit.owner()); - if (unit.warshipTargetId()) { - const targetOwner = this.game - .units() - .find((u) => u.id() == unit.warshipTargetId()) - ?.owner(); - if (targetOwner == this.myPlayer) { - outerColor = colord({ r: 200, b: 0, g: 0 }); + if (unit.isActive()) { + if (unit.warshipTargetId()) { + this.drawSprite(unit, colord({ r: 200, b: 0, g: 0 })); + } else { + this.drawSprite(unit); } } - - // Paint outer territory - for (const t of this.game.bfs( - unit.tile(), - euclDistFN(unit.tile(), 5, false), - )) { - this.paintCell(this.game.x(t), this.game.y(t), rel, outerColor, 255); - } - - // Paint border - for (const t of this.game.bfs( - unit.tile(), - manhattanDistFN(unit.tile(), 4), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.borderColor(unit.owner()), - 255, - ); - } - - // Paint inner territory - for (const t of this.game.bfs( - unit.tile(), - euclDistFN(unit.tile(), 1, false), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.territoryColor(unit.owner()), - 255, - ); - } } private handleShellEvent(unit: UnitView) { @@ -355,32 +316,13 @@ export class UnitLayer implements Layer { } if (unit.isActive()) { - for (const t of this.game.bfs( - unit.tile(), - euclDistFN(unit.tile(), range, false), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.spawnHighlightColor(), - 255, - ); - } - - this.paintCell( - this.game.x(unit.tile()), - this.game.y(unit.tile()), - rel, - this.theme.borderColor(unit.owner()), - 255, - ); + this.drawSprite(unit); } } private handleNuke(unit: UnitView) { - const rel = this.relationship(unit); let range = 0; + switch (unit.type()) { case UnitType.AtomBomb: range = 4; @@ -393,7 +335,6 @@ export class UnitLayer implements Layer { break; } - // Clear previous area for (const t of this.game.bfs( unit.lastTile(), euclDistFN(unit.lastTile(), range, false), @@ -402,30 +343,7 @@ export class UnitLayer implements Layer { } if (unit.isActive()) { - for (const t of this.game.bfs( - unit.tile(), - euclDistFN(unit.tile(), range, false), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.spawnHighlightColor(), - 255, - ); - } - for (const t of this.game.bfs( - unit.tile(), - euclDistFN(unit.tile(), 2, false), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.borderColor(unit.owner()), - 255, - ); - } + this.drawSprite(unit); } } @@ -458,33 +376,7 @@ export class UnitLayer implements Layer { } if (unit.isActive()) { - // Paint territory - for (const t of this.game.bfs( - unit.tile(), - manhattanDistFN(unit.tile(), 2), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.territoryColor(unit.owner()), - 255, - ); - } - - // Paint border - for (const t of this.game.bfs( - unit.tile(), - manhattanDistFN(unit.tile(), 1), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.borderColor(unit.owner()), - 255, - ); - } + this.drawSprite(unit); } } @@ -500,7 +392,7 @@ export class UnitLayer implements Layer { // Clear previous area for (const t of this.game.bfs( unit.lastTile(), - manhattanDistFN(unit.lastTile(), 2), + manhattanDistFN(unit.lastTile(), 4), )) { this.clearCell(this.game.x(t), this.game.y(t)); } @@ -518,31 +410,7 @@ export class UnitLayer implements Layer { ); } - for (const t of this.game.bfs( - unit.tile(), - manhattanDistFN(unit.tile(), 2), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.borderColor(unit.owner()), - 255, - ); - } - - for (const t of this.game.bfs( - unit.tile(), - manhattanDistFN(unit.tile(), 1), - )) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.territoryColor(unit.owner()), - 255, - ); - } + this.drawSprite(unit); } else { for (const t of trail) { this.clearCell( @@ -606,4 +474,19 @@ export class UnitLayer implements Layer { ) { context.clearRect(x, y, 1, 1); } + + drawSprite(unit: UnitView, customTerritoryColor?: Colord) { + const x = this.game.x(unit.tile()); + const y = this.game.y(unit.tile()); + + const sprite = getColoredSprite(unit, this.theme, customTerritoryColor); + + this.context.drawImage( + sprite, + Math.round(x - sprite.width / 2), + Math.round(y - sprite.height / 2), + sprite.width, + sprite.width, + ); + } }