diff --git a/src/Loader.ts b/src/Loader.ts index 0b17d88..50ce101 100644 --- a/src/Loader.ts +++ b/src/Loader.ts @@ -1,7 +1,7 @@ import {handlePath} from "./util/PathHandler"; +import {gameRenderer} from "./renderer/GameRenderer"; window.addEventListener("load", () => { handlePath(); -}); - -import("./renderer/GameRenderer"); \ No newline at end of file + gameRenderer.startRendering(); +}); \ No newline at end of file diff --git a/src/game/BorderManager.ts b/src/game/BorderManager.ts new file mode 100644 index 0000000..83f0c8b --- /dev/null +++ b/src/game/BorderManager.ts @@ -0,0 +1,87 @@ +import {gameMap} from "./GameData"; +import {onNeighbors} from "../util/MathUtil"; +import {territoryManager} from "./TerritoryManager"; +import {playerNameRenderingManager, PlayerNameUpdate} from "../renderer/manager/PlayerNameRenderingManager"; + +class BorderManager { + private tileGrades: Uint8Array; + private borderTiles: Set[]; + + /** + * Resets the border manager. + * Should only be called when a new game is started. + * @param playerCount The number of players in the game + * @internal + */ + reset(playerCount: number): void { + this.tileGrades = new Uint8Array(gameMap.width * gameMap.height); + this.borderTiles = new Array(playerCount).fill(null).map(() => new Set()); + } + + /** + * Checks for updated borders when claiming tiles. + * @param tiles The tiles that were claimed. + * @param attacker The player that claimed the tiles. + * @param defender The player that lost the tiles. + */ + transitionTiles(tiles: Set, attacker: number, defender: number): BorderTransitionResult { + const attackerBorder = this.borderTiles[attacker]; + const defenderBorder = this.borderTiles[defender] || new Set(); + const attackerName = new PlayerNameUpdate(attacker, false); + const defenderName = new PlayerNameUpdate(defender, true); + const result: BorderTransitionResult = {territory: [], attacker: [], defender: []}; + for (const tile of tiles) { + let grade = 0; + onNeighbors(tile, (neighbor) => { + const owner = territoryManager.getOwner(neighbor); + if (owner === defender) { + //We don't need to fear handling conquered tiles here, as they should already have the attacker as owner + if (this.tileGrades[neighbor]-- === 4) { + defenderBorder.add(neighbor); + playerNameRenderingManager.removeTile(neighbor, defenderName); + result.defender.push(neighbor); + } + } else if (owner === attacker) { + grade++; + if (!tiles.has(neighbor) && ++this.tileGrades[neighbor] === 4) { + attackerBorder.delete(neighbor); + playerNameRenderingManager.addTile(neighbor, attackerName); + result.territory.push(neighbor); + } + } + }); + + defenderBorder.delete(tile); + this.tileGrades[tile] = grade; + if (grade < 4) { + attackerBorder.add(tile); + playerNameRenderingManager.removeTile(tile, defenderName); + result.attacker.push(tile); + } else { + playerNameRenderingManager.addTile(tile, attackerName); + result.territory.push(tile); + } + } + + attackerName.update(); + defenderName.update(); + + return result; + } + + /** + * Gets the border tiles of a player. + * @param player The player to get the border tiles of + * @returns The border tiles of the player + */ + getBorderTiles(player: number): Set { + return this.borderTiles[player]; + } +} + +export const borderManager = new BorderManager(); +export type BorderTransitionResult = { + territory: number[]; // Tiles that changed to attacker territory + attacker: number[]; // Tiles that changed to attacker border + defender: number[]; // Tiles that changed to defender border +}; \ No newline at end of file diff --git a/src/game/Game.ts b/src/game/Game.ts index c768583..3df3c05 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -21,6 +21,7 @@ import {GameTickPacket} from "../network/protocol/packet/game/GameTickPacket"; import {hideAllUIElements, showUIElement} from "../ui/UIManager"; import {ClientPlayer} from "./player/ClientPlayer"; import {EventHandlerRegistry} from "../event/EventHandlerRegistry"; +import {borderManager} from "./BorderManager"; /** * Start a new game with the given map. @@ -33,9 +34,11 @@ import {EventHandlerRegistry} from "../event/EventHandlerRegistry"; */ export function startGame(map: GameMap, mode: GameMode, seed: number, players: { name: string }[], clientId: number, isLocal: boolean) { initGameData(map, mode, isLocal); + gameLoadRegistry.broadcast(); mapNavigationHandler.enable(); mapActionHandler.enable(); territoryManager.reset(); + borderManager.reset(500); boatManager.reset(); playerNameRenderingManager.reset(500); attackActionHandler.init(500); @@ -73,4 +76,5 @@ packetRegistry.handle(GameTickPacket, function () { } }); +export const gameLoadRegistry = new EventHandlerRegistry<[]>(); export const gameStartRegistry = new EventHandlerRegistry<[]>(); \ No newline at end of file diff --git a/src/game/GameTicker.ts b/src/game/GameTicker.ts index 911e36f..2e2f457 100644 --- a/src/game/GameTicker.ts +++ b/src/game/GameTicker.ts @@ -2,6 +2,7 @@ import {EventHandlerRegistry} from "../event/EventHandlerRegistry"; import {GameTickPacket} from "../network/protocol/packet/game/GameTickPacket"; import {packetRegistry} from "../network/NetworkManager"; import {isLocalGame} from "./GameData"; +import {doPacketValidation} from "../network/PacketValidator"; class GameTicker { private readonly TICK_INTERVAL = 1000 / 20; // 50ms @@ -69,7 +70,9 @@ class GameTicker { return; } for (const action of packet.packets) { - packetRegistry.getPacketHandler(action.id).call(action); + if (doPacketValidation(action)) { + packetRegistry.getPacketHandler(action.id).call(action); + } } } this.registry.broadcast(); diff --git a/src/game/TerritoryManager.ts b/src/game/TerritoryManager.ts index 6c5a70a..4ea0ebd 100644 --- a/src/game/TerritoryManager.ts +++ b/src/game/TerritoryManager.ts @@ -95,9 +95,10 @@ class TerritoryManager { const previousOwner = this.tileOwners[tile]; this.tileOwners[tile] = owner; if (previousOwner !== this.OWNER_NONE) { - playerManager.getPlayer(previousOwner).removeTile(tile, transaction); + playerManager.getPlayer(previousOwner).removeTile(tile); } - playerManager.getPlayer(owner).addTile(tile, transaction); + playerManager.getPlayer(owner).addTile(tile); + transaction.addTile(tile); } /** @@ -110,8 +111,8 @@ class TerritoryManager { const owner = this.tileOwners[tile]; if (owner !== this.OWNER_NONE) { this.tileOwners[tile] = this.OWNER_NONE; - playerManager.getPlayer(owner).removeTile(tile, transaction); - transaction.setTerritory(tile); + playerManager.getPlayer(owner).removeTile(tile); + transaction.addTile(tile); } } } diff --git a/src/game/attack/AttackActionValidator.ts b/src/game/attack/AttackActionValidator.ts index 4dce3c9..50328c7 100644 --- a/src/game/attack/AttackActionValidator.ts +++ b/src/game/attack/AttackActionValidator.ts @@ -5,6 +5,8 @@ import {AttackActionPacket} from "../../network/protocol/packet/game/AttackActio import {attackActionHandler} from "./AttackActionHandler"; import {packetRegistry, submitGameAction} from "../../network/NetworkManager"; import {gameMap, gameMode, isLocalGame} from "../GameData"; +import {borderManager} from "../BorderManager"; +import {validatePacket} from "../../network/PacketValidator"; /** * Filters out invalid attacks and submits the attack action. @@ -21,7 +23,7 @@ export function preprocessAttack(attacker: number, target: number, power: number } export function hasBorderWith(player: Player, target: number): boolean { - for (const tile of player.borderTiles) { + for (const tile of borderManager.getBorderTiles(player.id)) { const x = tile % gameMap.width; const y = Math.floor(tile / gameMap.width); if (x > 0 && territoryManager.isOwner(tile - 1, target)) { @@ -40,23 +42,16 @@ export function hasBorderWith(player: Player, target: number): boolean { return false; } +validatePacket(AttackActionPacket, packet => { + //TODO: This should be used by all attack related methods + const realTarget = packet.attacker === packet.target ? territoryManager.OWNER_NONE : packet.target; + return playerManager.validatePlayer(packet.attacker) && playerManager.validatePlayer(packet.target) + && gameMode.canAttack(packet.attacker, realTarget); +}); + //TODO: clean this up, starting pixels should be calculated here and not in the handler itself packetRegistry.handle(AttackActionPacket, function (): void { - const target = this.target === this.attacker ? territoryManager.OWNER_NONE : this.target; - if (!gameMode.canAttack(this.attacker, target)) { - return; - } - - //TODO: Move these into a general validation function - const attacker = playerManager.getPlayer(this.attacker); - if (!attacker || !attacker.isAlive()) { - return; // invalid origin player - } - if (target !== territoryManager.OWNER_NONE && !(playerManager.getPlayer(target) && playerManager.getPlayer(target).isAlive())) { - return; // invalid target player - } - - actuallyHandleAttack(attacker, target, this.power); + actuallyHandleAttack(playerManager.getPlayer(this.attacker), this.attacker === this.target ? territoryManager.OWNER_NONE : this.target, this.power); }); /** diff --git a/src/game/attack/AttackExecutor.ts b/src/game/attack/AttackExecutor.ts index 2d43214..fd34f35 100644 --- a/src/game/attack/AttackExecutor.ts +++ b/src/game/attack/AttackExecutor.ts @@ -5,6 +5,7 @@ import {random} from "../Random"; import {attackActionHandler} from "./AttackActionHandler"; import {gameMap} from "../GameData"; import {TerritoryTransaction} from "../transaction/TerritoryTransaction"; +import {borderManager} from "../BorderManager"; /** * This is the max amount of ticks it can take to conquer a tile. @@ -148,7 +149,7 @@ export class AttackExecutor { const result = []; const amountCache = attackActionHandler.amountCache; - for (const tile of borderTiles ?? this.player.borderTiles) { + for (const tile of borderTiles ?? borderManager.getBorderTiles(this.player.id)) { const x = tile % gameMap.width; const y = Math.floor(tile / gameMap.width); if (x > 0 && tileOwners[tile - 1] === target) { diff --git a/src/game/boat/BoatManager.ts b/src/game/boat/BoatManager.ts index 19b492a..7f5fcae 100644 --- a/src/game/boat/BoatManager.ts +++ b/src/game/boat/BoatManager.ts @@ -5,9 +5,9 @@ import {gameTicker} from "../GameTicker"; import {BoatActionPacket} from "../../network/protocol/packet/game/BoatActionPacket"; import {packetRegistry, submitGameAction} from "../../network/NetworkManager"; import {Player} from "../player/Player"; -import {onNeighbors} from "../../util/MathUtil"; -import {territoryManager} from "../TerritoryManager"; +import {bordersTile} from "../../util/MathUtil"; import {gameMap} from "../GameData"; +import {validatePacket} from "../../network/PacketValidator"; class BoatManager { private readonly boats: Boat[] = []; @@ -122,27 +122,12 @@ export const boatManager = new BoatManager(); gameTicker.registry.register(boatManager.tick); -packetRegistry.handle(BoatActionPacket, function (this: BoatActionPacket): void { - const player = playerManager.getPlayer(this.player); - if (!player || !player.isAlive()) { - return; - } - if (this.start >= gameMap.width * gameMap.height || this.end >= gameMap.width * gameMap.height) { - return; - } - if (gameMap.getDistance(this.start) !== -1 || gameMap.getDistance(this.end) !== 0) { - return; - } +validatePacket(BoatActionPacket, packet => { + return playerManager.validatePlayer(packet.player) + && gameMap.getDistance(packet.start) === -1 && gameMap.getDistance(packet.end) === 0 + && bordersTile(packet.start, packet.player); +}); - let hasBorder = false; - onNeighbors(this.start, neighbor => { - if (territoryManager.isOwner(neighbor, player.id)) { - hasBorder = true; - } - }); - if (!hasBorder) { - return; - } - - boatManager.addBoat(player, this.start, this.end, this.power); +packetRegistry.handle(BoatActionPacket, function (this: BoatActionPacket): void { + boatManager.addBoat(playerManager.getPlayer(this.player), this.start, this.end, this.power); }); \ No newline at end of file diff --git a/src/game/bot/BotPlayer.ts b/src/game/bot/BotPlayer.ts index 876393e..a7f6eaa 100644 --- a/src/game/bot/BotPlayer.ts +++ b/src/game/bot/BotPlayer.ts @@ -10,6 +10,7 @@ import {onNeighbors} from "../../util/MathUtil"; import {rayTraceWater} from "../../util/VoxelRayTrace"; import {gameMap, gameMode} from "../GameData"; import {boatManager} from "../boat/BoatManager"; +import {borderManager} from "../BorderManager"; export class BotPlayer extends Player { protected readonly triggers: BotTrigger[]; @@ -32,7 +33,7 @@ export class BotPlayer extends Player { //TODO: Attack percentage should be configurable actuallyHandleAttack(this, target, 100); } else if (this.waterTiles > 0) { - const borderTiles = Array.from(this.borderTiles); //TODO: Check the performance hit this causes + const borderTiles = Array.from(borderManager.getBorderTiles(this.id)); //TODO: Check the performance hit this causes const startTile = borderTiles[random.nextInt(borderTiles.length)]; const possibleStarts: number[] = []; onNeighbors(startTile, tile => { diff --git a/src/game/bot/BotStrategy.ts b/src/game/bot/BotStrategy.ts index de8bdfa..2f0307a 100644 --- a/src/game/bot/BotStrategy.ts +++ b/src/game/bot/BotStrategy.ts @@ -4,6 +4,7 @@ import {territoryManager} from "../TerritoryManager"; import {gameMode} from "../GameData"; import {BotPlayer} from "./BotPlayer"; import {playerManager} from "../player/PlayerManager"; +import {borderManager} from "../BorderManager"; export class BotStrategy { constructor( @@ -19,7 +20,7 @@ export class BotStrategy { getTarget(player: BotPlayer): number | null { //TODO: Neighbor logic should probably be done using tile updates instead of every tick let targets: number[] = []; - for (const border of player.borderTiles) { + for (const border of borderManager.getBorderTiles(player.id)) { onNeighbors(border, neighbor => { const owner = territoryManager.getOwner(neighbor); if (owner !== player.id && !targets.includes(owner)) { diff --git a/src/game/player/ClientPlayer.ts b/src/game/player/ClientPlayer.ts index 8bcdc2b..0298d89 100644 --- a/src/game/player/ClientPlayer.ts +++ b/src/game/player/ClientPlayer.ts @@ -1,5 +1,4 @@ import {Player} from "./Player"; -import {TerritoryTransaction} from "../transaction/TerritoryTransaction"; import {onNeighbors} from "../../util/MathUtil"; import {HSLColor} from "../../util/HSLColor"; import {areaCalculator} from "../../map/area/AreaCalculator"; @@ -14,8 +13,8 @@ export class ClientPlayer extends Player { areaIndex = new Uint16Array(areaCalculator.preprocessMap()); } - addTile(tile: number, transaction: TerritoryTransaction) { - super.addTile(tile, transaction); + addTile(tile: number) { + super.addTile(tile); onNeighbors(tile, neighbor => { if (territoryManager.isWater(neighbor)) { @@ -24,8 +23,8 @@ export class ClientPlayer extends Player { }); } - removeTile(tile: number, transaction: TerritoryTransaction) { - super.removeTile(tile, transaction); + removeTile(tile: number) { + super.removeTile(tile); onNeighbors(tile, neighbor => { if (territoryManager.isWater(neighbor)) { diff --git a/src/game/player/Player.ts b/src/game/player/Player.ts index 10a5a66..8d0f5e8 100644 --- a/src/game/player/Player.ts +++ b/src/game/player/Player.ts @@ -1,18 +1,15 @@ import {territoryManager} from "../TerritoryManager"; import {onNeighbors} from "../../util/MathUtil"; -import {playerNameRenderingManager} from "../../renderer/manager/PlayerNameRenderingManager"; import {attackActionHandler} from "../attack/AttackActionHandler"; import {HSLColor} from "../../util/HSLColor"; import {gameMode} from "../GameData"; import {spawnManager} from "./SpawnManager"; -import {TerritoryTransaction} from "../transaction/TerritoryTransaction"; export class Player { readonly id: number; readonly name: string; readonly baseColor: HSLColor; private troops: number = 1000; - readonly borderTiles: Set = new Set(); private territorySize: number = 0; private alive: boolean = true; protected waterTiles = 0; @@ -27,24 +24,13 @@ export class Player { * Add a tile to the player's territory. * WARNING: Make sure to call this method AFTER updating the territory manager. * @param tile The tile to add - * @param transaction The transaction to update the tile in * @internal */ - addTile(tile: number, transaction: TerritoryTransaction): void { + addTile(tile: number): void { this.territorySize++; - if (territoryManager.isBorder(tile)) { - this.borderTiles.add(tile); - transaction.setBorder(tile); - } else { - transaction.setTerritory(tile); - playerNameRenderingManager.addTile(tile, transaction); - } onNeighbors(tile, neighbor => { if (territoryManager.isWater(neighbor)) { this.waterTiles++; - } else if (territoryManager.isOwner(neighbor, this.id) && !territoryManager.isBorder(neighbor) && this.borderTiles.delete(neighbor)) { - transaction.setTerritory(neighbor); - playerNameRenderingManager.addTile(neighbor, transaction); } }); @@ -55,21 +41,13 @@ export class Player { * Remove a tile from the player's territory. * WARNING: Make sure to call this method AFTER updating the territory manager. * @param tile The tile to remove - * @param transaction The transaction to update the tile in * @internal */ - removeTile(tile: number, transaction: TerritoryTransaction): void { + removeTile(tile: number): void { this.territorySize--; - if (!this.borderTiles.delete(tile)) { - playerNameRenderingManager.removeTile(tile, transaction); - } onNeighbors(tile, neighbor => { if (territoryManager.isWater(neighbor)) { this.waterTiles--; - } else if (territoryManager.isOwner(neighbor, this.id) && !this.borderTiles.has(neighbor)) { - this.borderTiles.add(neighbor); - transaction.setDefendantBorder(neighbor); - playerNameRenderingManager.removeTile(neighbor, transaction); } }); diff --git a/src/game/player/PlayerManager.ts b/src/game/player/PlayerManager.ts index 69ff478..e1d0f42 100644 --- a/src/game/player/PlayerManager.ts +++ b/src/game/player/PlayerManager.ts @@ -77,6 +77,15 @@ class PlayerManager { this.players.forEach(player => player.income()); } } + + /** + * Validate a player id. + * @param player The player to validate + * @returns Whether the player is valid + */ + validatePlayer(player: number) { + return this.players[player] && this.players[player].isAlive(); + } } export const playerManager = new PlayerManager(); diff --git a/src/game/transaction/TerritoryTransaction.ts b/src/game/transaction/TerritoryTransaction.ts index 3383042..65d2b80 100644 --- a/src/game/transaction/TerritoryTransaction.ts +++ b/src/game/transaction/TerritoryTransaction.ts @@ -3,17 +3,10 @@ import {Player} from "../player/Player"; import {getTransactionExecutors, registerTransactionType} from "./TransactionExecutors"; import {InvalidArgumentException} from "../../util/Exceptions"; -//TODO: Do border calculations when transaction is applied (we currently paint some border tiles multiple times) export class TerritoryTransaction extends Transaction { protected readonly attacker: Player | null; protected readonly defendant: Player | null; - protected readonly territoryQueue: Array = []; - protected readonly borderQueue: Array = []; - protected readonly defendantBorderQueue: Array = []; - protected namePos: number = 0; - protected namePosSize: number = 0; - protected defendantNamePos: number = 0; - protected defendantNamePosSize: number = -1; + protected readonly tiles: Set = new Set(); /** * Create a new territory transaction. @@ -35,59 +28,17 @@ export class TerritoryTransaction extends Transaction { * Add a tile to the territory update queue. * @param tile index of the tile */ - setTerritory(tile: number): void { - this.territoryQueue.push(tile); + addTile(tile: number): void { + this.tiles.add(tile); } - /** - * Add a border to the territory update queue. - * @param tile index of the tile - */ - setBorder(tile: number): void { - this.borderQueue.push(tile); - } - - /** - * Set the border of the defendant player. - * @param tile index of the tile - */ - setDefendantBorder(tile: number): void { - this.defendantBorderQueue.push(tile); - } - - /** - * Set the name position of the player. - * This will use the greatest size this transaction has seen. - * @param pos The position of the name - * @param size The size of the name - */ - setNamePos(pos: number, size: number) { - if (size > this.namePosSize) { - this.namePos = pos; - this.namePosSize = size; - } - } - - /** - * Set the defendant name position. - * @param pos The position of the name - * @param size The size of the name - */ - setDefendantNamePos(pos: number, size: number) { - if (size > this.defendantNamePosSize) { - this.defendantNamePos = pos; - this.defendantNamePosSize = size; - } + apply() { + if (this.tiles.size === 0) return; + super.apply(); } cleanup() { - this.namePos = 0; - this.namePosSize = 0; - this.defendantNamePos = 0; - this.defendantNamePosSize = -1; - this.territoryQueue.length = 0; - this.borderQueue.length = 0; - this.defendantBorderQueue.length = 0; + this.tiles.clear(); } } diff --git a/src/network/NetworkManager.ts b/src/network/NetworkManager.ts index 160b081..349c346 100644 --- a/src/network/NetworkManager.ts +++ b/src/network/NetworkManager.ts @@ -11,6 +11,7 @@ import {BasePacket} from "./protocol/packet/BasePacket"; import {isLocalGame} from "../game/GameData"; import {GameActionPacket} from "./protocol/packet/game/GameActionPacket"; import {NetworkException} from "../util/Exceptions"; +import {doPacketValidation} from "./PacketValidator"; export const packetRegistry = new PacketRegistry(); @@ -147,7 +148,7 @@ export function sendPacket>(packet: T, force: boolean = export function submitGameAction>(action: T): void { if (isLocalGame) { packetRegistry.getPacketHandler(action.id).call(action); - } else { + } else if (doPacketValidation(action)) { sendPacket(action); } } \ No newline at end of file diff --git a/src/network/PacketValidator.ts b/src/network/PacketValidator.ts new file mode 100644 index 0000000..04cc01c --- /dev/null +++ b/src/network/PacketValidator.ts @@ -0,0 +1,28 @@ +import {BasePacket} from "./protocol/packet/BasePacket"; + +const validators: Record boolean> = {}; + +/** + * Register a packet validator. + * @param packet The packet to validate + * @param validator The validator function, should return true if the packet is valid + */ +export function validatePacket(packet: {prototype: T}, validator: (packet: T) => boolean) { + validators[packet.prototype.id] = validator as (packet: IncomingPacket) => boolean; +} + +/** + * Validate a packet. + * @param packet The packet to validate + * @returns Whether the packet is valid, if not, the packet should be dropped + */ +export function doPacketValidation(packet: IncomingPacket): boolean { + const validator = validators[packet.id]; + if (!validator) { + console.warn(`No validator for packet ${packet.id} (${packet.constructor.name})`); + return false; + } + return validator(packet); +} + +type IncomingPacket = Omit, "transferContext" | "buildTransferContext">; \ No newline at end of file diff --git a/src/renderer/GameRenderer.ts b/src/renderer/GameRenderer.ts index 65996f9..9132da0 100644 --- a/src/renderer/GameRenderer.ts +++ b/src/renderer/GameRenderer.ts @@ -7,6 +7,7 @@ import {nameRenderer} from "./layer/NameRenderer"; import {boatRenderer} from "./layer/BoatRenderer"; import {debugRenderer} from "./layer/debug/DebugRenderer"; import {gameStartRegistry} from "../game/Game"; +import {InvalidArgumentException} from "../util/Exceptions"; /** * Main renderer for anything canvas related in the game. @@ -26,10 +27,6 @@ export class GameRenderer { this.canvas.style.top = "0"; this.canvas.style.zIndex = "-1"; this.context = this.canvas.getContext("2d") as CanvasRenderingContext2D; - - this.doRenderTick(); - - document.body.appendChild(this.canvas); } /** @@ -69,12 +66,27 @@ export class GameRenderer { requestAnimationFrame(() => this.doRenderTick()); } + /** + * Start rendering the game. + * @throws {InvalidArgumentException} if a game renderer is already running + */ + startRendering(): void { + if (isRendering) { + throw new InvalidArgumentException("Game renderer is already running"); + } + isRendering = true; + + this.doRenderTick(); + document.body.appendChild(this.canvas); + } + resize(this: void, width: number, height: number): void { gameRenderer.canvas.width = Math.ceil(width / window.devicePixelRatio); gameRenderer.canvas.height = Math.ceil(height / window.devicePixelRatio); } } +let isRendering = false; export const gameRenderer = new GameRenderer(); windowResizeHandler.register(gameRenderer.resize); diff --git a/src/renderer/layer/TerritoryRenderer.ts b/src/renderer/layer/TerritoryRenderer.ts index 65a5f13..e41fd8b 100644 --- a/src/renderer/layer/TerritoryRenderer.ts +++ b/src/renderer/layer/TerritoryRenderer.ts @@ -1,7 +1,12 @@ import {CachedLayer} from "./CachedLayer"; import {mapTransformHandler} from "../../event/MapTransformHandler"; import {gameMap} from "../../game/GameData"; -import {gameStartRegistry} from "../../game/Game"; +import {gameLoadRegistry} from "../../game/Game"; +import {TerritoryRenderingManager} from "../manager/TerritoryRenderingManager"; +import {getSetting, registerSettingListener} from "../../util/UserSettingManager"; +import {registerTransactionExecutor} from "../../game/transaction/TransactionExecutors"; +import {TerritoryTransaction} from "../../game/transaction/TerritoryTransaction"; +import {borderManager} from "../../game/BorderManager"; /** * Territory renderer. @@ -9,6 +14,8 @@ import {gameStartRegistry} from "../../game/Game"; * @internal */ class TerritoryRenderer extends CachedLayer { + readonly manager: TerritoryRenderingManager = new TerritoryRenderingManager(this.context); + init(): void { this.resizeCanvas(gameMap.width, gameMap.height); } @@ -27,6 +34,21 @@ export const territoryRenderer = new TerritoryRenderer(); mapTransformHandler.scale.register(territoryRenderer.onMapScale); mapTransformHandler.move.register(territoryRenderer.onMapMove); -gameStartRegistry.register(territoryRenderer.init.bind(territoryRenderer)); +gameLoadRegistry.register(territoryRenderer.init.bind(territoryRenderer)); + +registerSettingListener("theme", territoryRenderer.manager.forceRepaint.bind(this)); -import("../manager/TerritoryRenderingManager"); \ No newline at end of file +registerTransactionExecutor(TerritoryTransaction, function (this: TerritoryTransaction) { + //TODO: this needs to be less magical for clearing + const borders = borderManager.transitionTiles(this.tiles, this.attacker?.id ?? -1, this.defendant?.id ?? -1); + if (this.attacker) { + territoryRenderer.manager.paintTiles(borders.territory, getSetting("theme").getTerritoryColor(this.attacker.baseColor)); + territoryRenderer.manager.paintTiles(borders.attacker, getSetting("theme").getBorderColor(this.attacker.baseColor)); + } else { + territoryRenderer.manager.clearTiles(this.tiles); + } + + if (this.defendant) { + territoryRenderer.manager.paintTiles(borders.defender, getSetting("theme").getBorderColor(this.defendant.baseColor)); + } +}); \ No newline at end of file diff --git a/src/renderer/layer/debug/DebugRendererRegistry.ts b/src/renderer/layer/debug/DebugRendererRegistry.ts index d1182ab..bbfceb4 100644 --- a/src/renderer/layer/debug/DebugRendererRegistry.ts +++ b/src/renderer/layer/debug/DebugRendererRegistry.ts @@ -1,9 +1,11 @@ import {MultiSelectSetting} from "../../../util/MultiSelectSetting"; import {BoatMeshDebugRenderer} from "./BoatMeshDebugRenderer"; import {RendererLayer} from "../RendererLayer"; +import {NameDepthDebugRenderer} from "./NameDepthDebugRenderer"; export const debugRendererLayers = MultiSelectSetting.init() - .option("boat-navigation-mesh", new BoatMeshDebugRenderer(), "Boat Navigation Mesh", false); + .option("boat-navigation-mesh", new BoatMeshDebugRenderer(), "Boat Navigation Mesh", false) + .option("name-depth", new NameDepthDebugRenderer(), "Name Depth", false) export interface DebugRendererLayer extends Omit { useCache: boolean; diff --git a/src/renderer/layer/debug/NameDepthDebugRenderer.ts b/src/renderer/layer/debug/NameDepthDebugRenderer.ts new file mode 100644 index 0000000..89dd95e --- /dev/null +++ b/src/renderer/layer/debug/NameDepthDebugRenderer.ts @@ -0,0 +1,26 @@ +import {DebugRendererLayer} from "./DebugRendererRegistry"; +import {playerNameRenderingManager} from "../../manager/PlayerNameRenderingManager"; +import {gameMap} from "../../../game/GameData"; +import {mapNavigationHandler} from "../../../game/action/MapNavigationHandler"; + +export class NameDepthDebugRenderer implements DebugRendererLayer { + readonly useCache = false; + + render(context: CanvasRenderingContext2D): void { + const map = playerNameRenderingManager.getNameDepth(); + const xMin = mapNavigationHandler.getMapX(0); + const xMax = mapNavigationHandler.getMapX(context.canvas.width); + const yMin = mapNavigationHandler.getMapY(0); + const yMax = mapNavigationHandler.getMapY(context.canvas.height); + for (let i = 0; i < gameMap.width * gameMap.height; i++) { + if (mapNavigationHandler.zoom < 1 || i % gameMap.width + 1 < xMin || i % gameMap.width > xMax || Math.floor(i / gameMap.width) + 1 < yMin || Math.floor(i / gameMap.width) > yMax) { + continue; + } + const depth = map[i]; + if (depth !== 0) { + context.fillStyle = `rgba(${255 * depth / 50}, 0, 0, 0.5)`; + context.fillRect((i % gameMap.width) * mapNavigationHandler.zoom + mapNavigationHandler.x, Math.floor(i / gameMap.width) * mapNavigationHandler.zoom + mapNavigationHandler.y, mapNavigationHandler.zoom, mapNavigationHandler.zoom); + } + } + } +} \ No newline at end of file diff --git a/src/renderer/manager/PlayerNameRenderingManager.ts b/src/renderer/manager/PlayerNameRenderingManager.ts index f952669..c050efe 100644 --- a/src/renderer/manager/PlayerNameRenderingManager.ts +++ b/src/renderer/manager/PlayerNameRenderingManager.ts @@ -7,8 +7,8 @@ import {random} from "../../game/Random"; import {gameTicker} from "../../game/GameTicker"; import {mapNavigationHandler} from "../../game/action/MapNavigationHandler"; import {gameMap} from "../../game/GameData"; -import {TerritoryTransaction} from "../../game/transaction/TerritoryTransaction"; -import {registerTransactionExecutor} from "../../game/transaction/TransactionExecutors"; +import {playerManager} from "../../game/player/PlayerManager"; +import {borderManager} from "../../game/BorderManager"; class PlayerNameRenderingManager { playerData: PlayerNameRenderingData[] = []; @@ -37,7 +37,7 @@ class PlayerNameRenderingManager { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d") as CanvasRenderingContext2D; const troopLength = context.measureText("123.").width / 10; - this.playerData[player.id] = new PlayerNameRenderingData(player.name, troopLength, player.borderTiles, player.id); + this.playerData[player.id] = new PlayerNameRenderingData(player.name, troopLength, borderManager.getBorderTiles(player.id), player.id); } /** @@ -86,7 +86,7 @@ class PlayerNameRenderingManager { * @param transaction The transaction that added the tile * @internal */ - addTile(tile: number, transaction: TerritoryTransaction): void { + addTile(tile: number, transaction: PlayerNameUpdate): void { this.nameDepth[tile] = 65535; // force recalculation this.recalculateFrom(tile, transaction); } @@ -95,34 +95,33 @@ class PlayerNameRenderingManager { * Update the player name rendering data. * @internal */ - removeTile(tile: number, transaction: TerritoryTransaction): void { + removeTile(tile: number, transaction: PlayerNameUpdate): void { let offset = 0; let rowMax = Infinity; let columnMax = Infinity; - transaction.setDefendantNamePos(tile - gameMap.width - 1, this.nameDepth[tile - gameMap.width - 1]); - let changed: boolean; - do { - changed = false; - for (let i = 0; i < rowMax; i++) { + transaction.setNamePos(tile - gameMap.width - 1, this.nameDepth[tile - gameMap.width - 1]); + while (true) { + if (this.nameDepth[tile] <= offset) { + break; + } + this.nameDepth[tile] = offset; + for (let i = 1; i < rowMax; i++) { if (this.nameDepth[tile + i] <= offset + i) { rowMax = i; break; } this.nameDepth[tile + i] = offset + i; - changed = true; } - tile += gameMap.width; - for (let i = 0; i < columnMax; i++) { + for (let i = 1; i < columnMax; i++) { if (this.nameDepth[tile + i * gameMap.width] <= offset + i) { columnMax = i; break; } this.nameDepth[tile + i * gameMap.width] = offset + i; - changed = true; } - tile++; + tile += gameMap.width + 1; offset++; - } while (changed); + } } /** @@ -132,7 +131,7 @@ class PlayerNameRenderingManager { * @param transaction The transaction to update the tiles in * @private */ - private recalculateFrom(tile: number, transaction: TerritoryTransaction): void { + private recalculateFrom(tile: number, transaction: PlayerNameUpdate): void { let currentOrigin = tile; let isColumn = false; let changed = true; @@ -231,7 +230,7 @@ export class PlayerNameRenderingData { */ validatePosition(): void { const nameDepth = playerNameRenderingManager.getNameDepth(); - if (nameDepth[this.index] === this.size) return; + if (nameDepth[this.index] === this.size && territoryManager.tileOwners[this.index] === this.id) return; if (nameDepth[this.index] !== 0) { this.queue.push([nameDepth[this.index], this.index]); } @@ -298,13 +297,40 @@ export class PlayerNameRenderingData { export const playerNameRenderingManager = new PlayerNameRenderingManager(); -registerTransactionExecutor(TerritoryTransaction, function (this: TerritoryTransaction) { - if (this.attacker && this.namePosSize > 0) { - playerNameRenderingManager.getPlayerData(this.attacker).addPosition(this.namePosSize, this.namePos); +export class PlayerNameUpdate { + private readonly player: number; + private readonly validate: boolean; + private namePos: number = 0; + private namePosSize: number = 0; + + constructor(player: number, validate: boolean) { + this.player = player; + this.validate = validate; + } + + /** + * Set the name position. + * @param pos The position of the name. + * @param size The size of the name. + */ + setNamePos(pos: number, size: number): void { + if (size > this.namePosSize) { + this.namePos = pos; + this.namePosSize = size; + } } - if (this.defendant && this.defendantNamePosSize > -1) { - playerNameRenderingManager.getPlayerData(this.defendant).addPosition(this.defendantNamePosSize, this.defendantNamePos); - playerNameRenderingManager.getPlayerData(this.defendant).validatePosition(); + /** + * Update the player name rendering data. + */ + update(): void { + const player = playerManager.getPlayer(this.player); + if (!player) return; + if (this.namePosSize > 0) { + playerNameRenderingManager.getPlayerData(player).addPosition(this.namePosSize, this.namePos); + } + if (this.validate) { + playerNameRenderingManager.getPlayerData(player).validatePosition(); + } } -}); \ No newline at end of file +} \ No newline at end of file diff --git a/src/renderer/manager/TerritoryRenderingManager.ts b/src/renderer/manager/TerritoryRenderingManager.ts index 9e1a697..15ebd2b 100644 --- a/src/renderer/manager/TerritoryRenderingManager.ts +++ b/src/renderer/manager/TerritoryRenderingManager.ts @@ -1,12 +1,8 @@ -import {getSetting, registerSettingListener} from "../../util/UserSettingManager"; import {HSLColor} from "../../util/HSLColor"; -import {territoryRenderer} from "../layer/TerritoryRenderer"; import {playerManager} from "../../game/player/PlayerManager"; import {territoryManager} from "../../game/TerritoryManager"; import {GameTheme} from "../GameTheme"; import {gameMap, isPlaying} from "../../game/GameData"; -import {registerTransactionExecutor} from "../../game/transaction/TransactionExecutors"; -import {TerritoryTransaction} from "../../game/transaction/TerritoryTransaction"; /** * When a player claims a tile, three types of updates are required: @@ -15,13 +11,19 @@ import {TerritoryTransaction} from "../../game/transaction/TerritoryTransaction" * 3. A neighboring tile can become a border tile of the player's territory. */ export class TerritoryRenderingManager { + private context: CanvasRenderingContext2D; + + constructor(context: CanvasRenderingContext2D) { + this.context = context; + } + /** * Clear the tiles at the given indices. * @param tiles the tiles to clear */ - clearTiles(tiles: number[]): void { + clearTiles(tiles: number[] | Set): void { for (const tile of tiles) { - territoryRenderer.context.clearRect(tile % gameMap.width, Math.floor(tile / gameMap.width), 1, 1); + this.context.clearRect(tile % gameMap.width, Math.floor(tile / gameMap.width), 1, 1); } } @@ -32,16 +34,15 @@ export class TerritoryRenderingManager { * @internal */ paintTiles(tiles: number[], color: HSLColor): void { - const context = territoryRenderer.context; - context.fillStyle = color.toString(); + this.context.fillStyle = color.toString(); if (color.a < 1) { for (const tile of tiles) { - context.clearRect(tile % gameMap.width, Math.floor(tile / gameMap.width), 1, 1); - context.fillRect(tile % gameMap.width, Math.floor(tile / gameMap.width), 1, 1); + this.context.clearRect(tile % gameMap.width, Math.floor(tile / gameMap.width), 1, 1); + this.context.fillRect(tile % gameMap.width, Math.floor(tile / gameMap.width), 1, 1); } } else { for (const tile of tiles) { - context.fillRect(tile % gameMap.width, Math.floor(tile / gameMap.width), 1, 1); + this.context.fillRect(tile % gameMap.width, Math.floor(tile / gameMap.width), 1, 1); } } } @@ -49,9 +50,9 @@ export class TerritoryRenderingManager { /** * Force a repaint of the territory layer. */ - forceRepaint(this: void, theme: GameTheme): void { + forceRepaint(theme: GameTheme): void { if (!isPlaying) return; - territoryRenderer.context.clearRect(0, 0, gameMap.width, gameMap.height); + this.context.clearRect(0, 0, gameMap.width, gameMap.height); const colorCache: string[] = []; for (let i = 0; i < gameMap.width * gameMap.height; i++) { const owner = territoryManager.getOwner(i); @@ -62,26 +63,9 @@ export class TerritoryRenderingManager { if (!colorCache[index]) { colorCache[index] = isTerritory ? theme.getTerritoryColor(player.baseColor).toString() : theme.getBorderColor(player.baseColor).toString(); } - territoryRenderer.context.fillStyle = colorCache[index]; - territoryRenderer.context.fillRect(i % gameMap.width, Math.floor(i / gameMap.width), 1, 1); + this.context.fillStyle = colorCache[index]; + this.context.fillRect(i % gameMap.width, Math.floor(i / gameMap.width), 1, 1); } } } -} - -export const territoryRenderingManager = new TerritoryRenderingManager(); - -registerSettingListener("theme", territoryRenderingManager.forceRepaint); - -registerTransactionExecutor(TerritoryTransaction, function (this: TerritoryTransaction) { - if (this.defendant) { - territoryRenderingManager.paintTiles(this.defendantBorderQueue, getSetting("theme").getBorderColor(this.defendant.baseColor)); - } - - if (this.attacker) { - territoryRenderingManager.paintTiles(this.borderQueue, getSetting("theme").getBorderColor(this.attacker.baseColor)); - territoryRenderingManager.paintTiles(this.territoryQueue, getSetting("theme").getTerritoryColor(this.attacker.baseColor)); - } else { - territoryRenderingManager.clearTiles(this.territoryQueue); - } -}); \ No newline at end of file +} \ No newline at end of file