diff --git a/resources/sprites/atombomb.png b/resources/sprites/atombomb.png new file mode 100644 index 000000000..bc91d698a Binary files /dev/null and b/resources/sprites/atombomb.png differ diff --git a/resources/sprites/hydrogenbomb.png b/resources/sprites/hydrogenbomb.png new file mode 100644 index 000000000..1a01b694f Binary files /dev/null and b/resources/sprites/hydrogenbomb.png differ diff --git a/resources/sprites/mirv.png b/resources/sprites/mirv.png new file mode 100644 index 000000000..f29cf7cda Binary files /dev/null and b/resources/sprites/mirv.png differ diff --git a/resources/sprites/mirv2.png b/resources/sprites/mirv2.png new file mode 100644 index 000000000..7a403c57f Binary files /dev/null and b/resources/sprites/mirv2.png differ diff --git a/resources/sprites/samMissile.png b/resources/sprites/samMissile.png new file mode 100644 index 000000000..353d870ea Binary files /dev/null and b/resources/sprites/samMissile.png differ diff --git a/resources/sprites/tradeship.png b/resources/sprites/tradeship.png new file mode 100644 index 000000000..bb61aa420 Binary files /dev/null and b/resources/sprites/tradeship.png differ diff --git a/resources/sprites/transportship.png b/resources/sprites/transportship.png new file mode 100644 index 000000000..d0e25722e Binary files /dev/null and b/resources/sprites/transportship.png differ diff --git a/resources/sprites/warship.png b/resources/sprites/warship.png new file mode 100644 index 000000000..489034625 Binary files /dev/null and b/resources/sprites/warship.png differ 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, + ); + } }