thread_split: convert all tile to tileref

This commit is contained in:
Evan
2025-01-17 20:13:26 -08:00
parent c42cc2a9b4
commit f0f5bae79f
53 changed files with 1104 additions and 1405 deletions
+7 -8
View File
@@ -4,8 +4,7 @@ import { EventBus } from "../core/EventBus";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler"
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientMessageSchema, GameConfig, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn } from "../core/Schemas";
import { loadTerrainFromFile, loadTerrainMap, TerrainMapImpl } from "../core/game/TerrainMapLoader";
import { and, bfs, dist, generateID, manhattanDist } from "../core/Util";
import { loadTerrainFromFile, loadTerrainMap } from "../core/game/TerrainMapLoader";
import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport";
import { createCanvas } from "./Utils";
import { MessageType } from '../core/game/Game';
@@ -72,10 +71,10 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: GameConfig, eventBus: EventBus, transport: Transport): Promise<ClientGameRunner> {
const config = getConfig(gameConfig)
const terrainMap = await loadTerrainMap(gameConfig.gameMap);
const gameMap = await loadTerrainMap(gameConfig.gameMap);
const worker = new WorkerClient(lobbyConfig.gameID, gameConfig)
await worker.initialize()
const gameView = new GameView(worker, config, terrainMap.terrain)
const gameView = new GameView(worker, config, gameMap.gameMap)
consolex.log('going to init path finder')
@@ -177,12 +176,12 @@ export class ClientGameRunner {
return
}
const cell = this.renderer.transformHandler.screenToWorldCoordinates(event.x, event.y)
if (!this.gameView.isOnMap(cell)) {
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
return
}
consolex.log(`clicked cell ${cell}`)
const tile = this.gameView.tile(cell)
if (tile.terrain().isLand() && !tile.hasOwner() && this.gameView.inSpawnPhase()) {
const tile = this.gameView.ref(cell.x, cell.y)
if (this.gameView.isLand(tile) && !this.gameView.hasOwner(tile) && this.gameView.inSpawnPhase()) {
this.eventBus.emit(new SendSpawnIntentEvent(cell))
return
}
@@ -200,7 +199,7 @@ export class ClientGameRunner {
if (actions.canAttack) {
this.eventBus.emit(
new SendAttackIntentEvent(
tile.owner().id(),
this.gameView.owner(tile).id(),
this.myPlayer.troops() * this.renderer.uiState.attackRatio
)
)
+1 -5
View File
@@ -1,7 +1,7 @@
import { Config, ServerConfig } from "../core/configuration/Config"
import { SendLogEvent } from "../core/Consolex"
import { EventBus, GameEvent } from "../core/EventBus"
import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game"
import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerType, UnitType } from "../core/game/Game"
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig, ClientLogMessageSchema } from "../core/Schemas"
import { LobbyConfig } from "./ClientGameRunner"
import { LocalServer } from "./LocalServer"
@@ -298,10 +298,6 @@ export class Transport {
attackerID: this.lobbyConfig.playerID,
targetID: event.targetID,
troops: event.troops,
sourceX: null,
sourceY: null,
targetX: null,
targetY: null,
})
}
+4 -10
View File
@@ -1,5 +1,4 @@
import { Game, Player, Tile, Cell, NameViewData } from '../../core/game/Game';
import { GameView } from '../../core/GameView';
import { Game, Player, Cell, NameViewData } from '../../core/game/Game';
import { calculateBoundingBox, within } from '../../core/Util';
export interface Point {
@@ -17,12 +16,7 @@ export interface Rectangle {
export function placeName(game: Game, player: Player): NameViewData {
return {
x: 0,
y: 0,
size: 0
}
const boundingBox = calculateBoundingBox(player.borderTiles());
const boundingBox = calculateBoundingBox(game, player.borderTiles());
const rawScalingFactor = (boundingBox.max.x - boundingBox.min.x) / 100
@@ -72,8 +66,8 @@ export function createGrid(game: Game, player: Player, boundingBox: { min: Point
for (let y = scaledBoundingBox.min.y; y <= scaledBoundingBox.max.y; y++) {
const cell = new Cell(x * scalingFactor, y * scalingFactor);
if (game.isOnMap(cell)) {
const tile = game.tile(cell);
grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = tile.terrain().isLake() || tile.owner() === player; // TODO: okay if lake
const tile = game.ref(cell.x, cell.y);
grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = this.game.isLake(tile) || this.game.owner(tile) === player; // TODO: okay if lake
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { colord } from "colord";
import { EventBus } from "../../core/EventBus"
import { Cell, Game, Player } from "../../core/game/Game";
import { calculateBoundingBox, calculateBoundingBoxCenter, manhattanDist } from "../../core/Util";
import { calculateBoundingBox, calculateBoundingBoxCenter } from "../../core/Util";
import { ZoomEvent, DragEvent } from "../InputHandler";
import { GoToPlayerEvent } from "./layers/Leaderboard";
import { placeName } from "./NameBoxCalculator";
@@ -131,7 +131,7 @@ export class TransformHandler {
const { screenX, screenY } = this.screenCenter()
const screenMapCenter = new Cell(screenX, screenY)
if (manhattanDist(screenMapCenter, this.target) < 2) {
if (this.game.manhattanDist(this.game.ref(screenX, screenY), this.game.ref(this.target.x, this.target.y)) < 2) {
this.clearTarget()
return
}
+2 -3
View File
@@ -1,7 +1,6 @@
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Layer } from './Layer';
import { Game, Player } from '../../../core/game/Game';
import { ClientID } from '../../../core/Schemas';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { EventBus, GameEvent } from '../../../core/EventBus';
@@ -59,7 +58,7 @@ export class Leaderboard extends LitElement implements Layer {
.map((player, index) => ({
name: player.displayName(),
position: index + 1,
score: formatPercentage(player.numTilesOwned() / this.game.terrainMap().numLandTiles()),
score: formatPercentage(player.numTilesOwned() / this.game.numLandTiles()),
gold: renderNumber(player.gold()),
isMyPlayer: player == myPlayer,
player: player
@@ -78,7 +77,7 @@ export class Leaderboard extends LitElement implements Layer {
this.players.push({
name: myPlayer.displayName(),
position: place,
score: formatPercentage(myPlayer.numTilesOwned() / this.game.terrainMap().numLandTiles()),
score: formatPercentage(myPlayer.numTilesOwned() / this.game.numLandTiles()),
gold: renderNumber(myPlayer.gold()),
isMyPlayer: true,
player: myPlayer
+67 -51
View File
@@ -6,10 +6,25 @@ import { ClientID } from '../../../core/Schemas';
import { EventBus } from '../../../core/EventBus';
import { TransformHandler } from '../TransformHandler';
import { MouseMoveEvent } from '../../InputHandler';
import { euclideanDist, distSortUnit } from '../../../core/Util';
import { renderNumber, renderTroops } from '../../Utils';
import { PauseGameEvent } from '../../Transport';
import { GameView, PlayerView } from '../../../core/GameView';
import { TileRef } from '../../../core/game/GameMap';
import { PauseGameEvent } from '../../Transport';
function euclideanDistWorld(coord: { x: number, y: number }, tileRef: TileRef, game: GameView): number {
const x = game.x(tileRef);
const y = game.y(tileRef);
const dx = coord.x - x;
const dy = coord.y - y;
return Math.sqrt(dx * dx + dy * dy);
}
function distSortUnitWorld(coord: { x: number, y: number }, game: GameView) {
return (a: Unit, b: Unit) => {
const distA = euclideanDistWorld(coord, a.tile(), game);
const distB = euclideanDistWorld(coord, b.tile(), game);
return distA - distB;
};
}
@customElement('player-info-overlay')
export class PlayerInfoOverlay extends LitElement implements Layer {
@@ -29,13 +44,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private player: Player | null = null;
@state()
private playerProfile: PlayerProfile | null = null
private playerProfile: PlayerProfile | null = null;
@state()
private unit: Unit | null = null;
@state()
private showPauseButton: boolean = true
private showPauseButton: boolean = true;
@state()
private _isInfoVisible: boolean = false;
@@ -43,39 +58,40 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
@state()
private _isPaused: boolean = false;
private _isActive = false
private _isActive = false;
init() {
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) => this.onMouseEvent(e));
this._isActive = true
this.showPauseButton = this.game.config().gameConfig().gameType == GameType.Singleplayer
this._isActive = true;
this.showPauseButton = this.game.config().gameConfig().gameType == GameType.Singleplayer;
}
private onMouseEvent(event: MouseMoveEvent) {
this.setVisible(false);
this.unit = null;
const lastPlayer = this.player
this.player = null;
const worldCoord = this.transform.screenToWorldCoordinates(event.x, event.y);
if (!this.game.isOnMap(worldCoord)) {
if (!this.game.isValidCoord(worldCoord.x, worldCoord.y)) {
return;
}
const tile = this.game.tile(worldCoord);
const owner = tile.owner();
const tile = this.game.ref(worldCoord.x, worldCoord.y);
if (!tile) return;
if (owner.isPlayer()) {
const owner = this.game.owner(tile);
if (owner && owner.isPlayer()) {
this.player = owner;
(this.player as PlayerView).profile().then(p => {
console.log(`got profile ${JSON.stringify(p)}`)
this.playerProfile = p
})
console.log(`got profile ${JSON.stringify(p)}`);
this.playerProfile = p;
});
this.setVisible(true);
} else if (!tile.terrain().isLand()) {
} else if (!this.game.isLand(tile)) {
const units = this.game.units(UnitType.Destroyer, UnitType.Battleship, UnitType.TradeShip)
.filter(u => euclideanDist(worldCoord, u.tile().cell()) < 50)
.sort(distSortUnit(tile));
.filter(u => euclideanDistWorld(worldCoord, u.tile(), this.game) < 50)
.sort(distSortUnitWorld(worldCoord, this.game));
if (units.length > 0) {
this.unit = units[0];
@@ -120,28 +136,28 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private renderPlayerInfo(player: Player) {
const myPlayer = this.myPlayer();
const isAlly = (myPlayer?.isAlliedWith(player) || player == this.myPlayer()) ?? false;
let relationHtml = null
let relationHtml = null;
if (player.type() == PlayerType.FakeHuman && myPlayer != null) {
let classType = ''
let relationName = ''
const relation = this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral
let classType = '';
let relationName = '';
const relation = this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
switch (relation) {
case Relation.Hostile:
classType = 'hostile'
relationName = 'Hostile'
break
classType = 'hostile';
relationName = 'Hostile';
break;
case Relation.Distrustful:
classType = 'distrustful'
relationName = 'Distrustful'
break
classType = 'distrustful';
relationName = 'Distrustful';
break;
case Relation.Neutral:
classType = 'neutral'
relationName = 'Neutral'
break
classType = 'neutral';
relationName = 'Neutral';
break;
case Relation.Friendly:
classType = 'friendly'
relationName = 'Friendly'
break
classType = 'friendly';
relationName = 'Friendly';
break;
}
relationHtml = html`<div class="type-label">Attitude: <span class="${classType}">${relationName}</span></div>`;
@@ -149,8 +165,8 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return html`
<div class="info-content">
<div class="player-name ${isAlly ? 'ally' : ''}">${player.name()}</div>
<div class="type-label">Troops: ${renderTroops(player.troops())}</div>
<div class="type-label">Gold: ${renderNumber(player.gold())}</div>
<div class="type-label">Troops: ${player.troops()}</div>
<div class="type-label">Gold: ${player.gold()}</div>
${relationHtml == null ? '' : relationHtml}
</div>
`;
@@ -173,7 +189,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
render() {
if (!this._isActive) {
return html``
return html``;
}
return html`
<div class="container">
@@ -277,6 +293,19 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
margin-top: 4px;
}
.hostile {
color: #ff4444;
}
.distrustful {
color: #ff8888;
}
.neutral {
color: #ffffff;
}
.friendly {
color: #4CAF50;
}
@media (max-width: 768px) {
.container {
top: 5px;
@@ -302,19 +331,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
.type-label {
font-size: 12px;
}
}
.hostile {
color: #ff4444;
}
.distrustful {
color: #ff8888;
}
.neutral {
color: #ffffff;
}
.friendly {
color: #4CAF50;
}
`;
}
+31 -22
View File
@@ -1,7 +1,5 @@
import { colord, Colord } from "colord";
import { Theme } from "../../../core/configuration/Config";
import { Unit, Cell, Game, Tile, UnitType } from "../../../core/game/Game";
import { bfs, dist, euclDist } from "../../../core/Util";
import { Layer } from "./Layer";
import { EventBus } from "../../../core/EventBus";
@@ -10,6 +8,8 @@ import missileSiloIcon from '../../../../resources/images/MissileSiloUnit.png';
import shieldIcon from '../../../../resources/images/ShieldIcon.png';
import cityIcon from '../../../../resources/images/CityIcon.png';
import { GameView } from "../../../core/GameView";
import { Cell, Unit, UnitType } from "../../../core/game/Game";
import { euclDistFN } from "../../../core/game/GameMap";
interface UnitRenderConfig {
icon: string;
@@ -17,14 +17,13 @@ interface UnitRenderConfig {
territoryRadius: number;
}
export class StructureLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private unitImages: Map<string, HTMLImageElement> = new Map();
private theme: Theme = null;
private seenUnits = new Set<Unit>()
private seenUnits = new Set<Unit>();
// Configuration for supported unit types only
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
@@ -70,20 +69,20 @@ export class StructureLayer implements Layer {
}
tick() {
this.game.units().forEach(u => this.handleUnitRendering(u))
this.game.units().forEach(u => this.handleUnitRendering(u));
}
init() {
this.redraw()
this.redraw();
}
redraw() {
console.log('structure layer redrawing')
console.log('structure layer redrawing');
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext("2d", { alpha: true });
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.game.units().forEach(u => this.handleUnitRendering(u))
this.game.units().forEach(u => this.handleUnitRendering(u));
}
renderLayer(context: CanvasRenderingContext2D) {
@@ -106,11 +105,11 @@ export class StructureLayer implements Layer {
if (unit.isActive() && this.seenUnits.has(unit)) {
// Already rendered, so don't do anything.
return
return;
}
if (!unit.isActive() && !this.seenUnits.has(unit)) {
// Has been deleted and render is cleared so don't do anything.
return
return;
}
const config = this.unitConfigs[unitType];
@@ -119,14 +118,15 @@ export class StructureLayer implements Layer {
if (!config || !unitImage) return;
// Clear previous rendering
bfs(unit.tile(), euclDist(unit.tile(), config.borderRadius))
.forEach(t => this.clearCell(t.cell()));
for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.borderRadius))) {
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
}
if (!unit.isActive()) {
this.seenUnits.delete(unit)
this.seenUnits.delete(unit);
return;
}
this.seenUnits.add(unit)
this.seenUnits.add(unit);
// Create temporary canvas for icon processing
const tempCanvas = document.createElement('canvas');
@@ -138,16 +138,25 @@ export class StructureLayer implements Layer {
tempContext.drawImage(unitImage, 0, 0);
const iconData = tempContext.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const cell = unit.tile().cell();
const startX = cell.x - Math.floor(tempCanvas.width / 2);
const startY = cell.y - Math.floor(tempCanvas.height / 2);
const startX = this.game.x(unit.tile()) - Math.floor(tempCanvas.width / 2);
const startY = this.game.y(unit.tile()) - Math.floor(tempCanvas.height / 2);
// Draw border and territory
bfs(unit.tile(), euclDist(unit.tile(), config.borderRadius))
.forEach(t => this.paintCell(t.cell(), this.theme.borderColor(unit.owner().info()), 255));
for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.borderRadius))) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
this.theme.borderColor(unit.owner().info()),
255
);
}
bfs(unit.tile(), euclDist(unit.tile(), config.territoryRadius))
.forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(unit.owner().info()), 130));
for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.territoryRadius))) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
this.theme.territoryColor(unit.owner().info()),
130
);
}
// Draw the icon
this.renderIcon(iconData, startX, startY, tempCanvas.width, tempCanvas.height, unit);
@@ -184,7 +193,7 @@ export class StructureLayer implements Layer {
}
paintCell(cell: Cell, color: Colord, alpha: number) {
this.clearCell(cell)
this.clearCell(cell);
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
this.context.fillRect(cell.x, cell.y, 1, 1);
}
+3 -6
View File
@@ -1,8 +1,4 @@
import { inherits } from "util"
import { Game } from "../../../core/game/Game";
import { throws } from "assert";
import { Layer } from "./Layer";
import { TransformHandler } from "../TransformHandler";
import { GameView } from "../../../core/GameView";
export class TerrainLayer implements Layer {
@@ -37,8 +33,9 @@ export class TerrainLayer implements Layer {
initImageData() {
const theme = this.game.config().theme()
this.game.forEachTile((tile) => {
let terrainColor = theme.terrainColor(tile)
const index = (tile.cell().y * this.game.width()) + tile.cell().x
let terrainColor = theme.terrainColor(this.game, tile)
// TODO: isn'te tileref and index the same?
const index = (this.game.y(tile) * this.game.width()) + this.game.x(tile)
const offset = index * 4
this.imageData.data[offset] = terrainColor.rgba.r;
this.imageData.data[offset + 1] = terrainColor.rgba.g;
+89 -86
View File
@@ -1,90 +1,91 @@
import { PriorityQueue } from "@datastructures-js/priority-queue";
import { Cell, Game, Player, PlayerType, Tile, Unit, UnitType, UnitUpdate } from "../../../core/game/Game";
import { Cell, Game, Player, PlayerType, Unit, UnitType, UnitUpdate } from "../../../core/game/Game";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { colord, Colord } from "colord";
import { bfs, dist, euclDist, euclideanDist } from "../../../core/Util";
import { Theme } from "../../../core/configuration/Config";
import { Layer } from "./Layer";
import { EventBus } from "../../../core/EventBus";
import { AlternateViewEvent, DragEvent, MouseDownEvent } from "../../InputHandler";
import { GameView, PlayerView } from "../../../core/GameView";
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
export class TerritoryLayer implements Layer {
private canvas: HTMLCanvasElement
private context: CanvasRenderingContext2D
private imageData: ImageData
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private imageData: ImageData;
private tileToRenderQueue: PriorityQueue<{ tile: Tile, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate })
private random = new PseudoRandom(123)
private theme: Theme = null
private tileToRenderQueue: PriorityQueue<{ tile: TileRef, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate });
private random = new PseudoRandom(123);
private theme: Theme = null;
// Used for spawn highlighting
private highlightCanvas: HTMLCanvasElement
private highlightContext: CanvasRenderingContext2D
private highlightCanvas: HTMLCanvasElement;
private highlightContext: CanvasRenderingContext2D;
private alternativeView = false
private lastDragTime = 0
private nodrawDragDuration = 200
private refreshRate = 50
private lastRefresh = 0
private alternativeView = false;
private lastDragTime = 0;
private nodrawDragDuration = 200;
private refreshRate = 50;
private lastRefresh = 0;
constructor(private game: GameView, private eventBus: EventBus) {
this.theme = game.config().theme()
this.theme = game.config().theme();
}
shouldTransform(): boolean {
return true
return true;
}
tick() {
this.game.recentlyUpdatedTiles()
.forEach(t => this.enqueueTile(t))
.forEach(t => this.enqueueTile(t));
if (!this.game.inSpawnPhase()) {
return
return;
}
if (this.game.ticks() % 5 == 0) {
return
return;
}
this.highlightContext.clearRect(0, 0, this.game.width(), this.game.height());
const humans = this.game.playerViews()
.filter(p => p.type() == PlayerType.Human)
.filter(p => p.type() == PlayerType.Human);
for (const human of humans) {
const center = human.nameLocation()
const center = human.nameLocation();
if (!center) {
continue
continue;
}
const centerTile = this.game.tile(new Cell(center.x, center.y))
const centerTile = this.game.ref(center.x, center.y)
if (!centerTile) {
continue
continue;
}
for (const tile of bfs(centerTile, euclDist(centerTile, 9))) {
if (!tile.hasOwner()) {
this.paintHighlightCell(tile.cell(), this.theme.spawnHighlightColor(), 255)
for (const tile of this.game.bfs(centerTile, euclDistFN(centerTile, 9))) {
if (!this.game.hasOwner(tile)) {
this.paintHighlightCell(
new Cell(this.game.x(tile), this.game.y(tile)),
this.theme.spawnHighlightColor(),
255
);
}
}
}
}
init() {
this.eventBus.on(AlternateViewEvent, e => { this.alternativeView = e.alternateView })
this.eventBus.on(DragEvent, e => { this.lastDragTime = Date.now() })
this.redraw()
this.eventBus.on(AlternateViewEvent, e => { this.alternativeView = e.alternateView; });
this.eventBus.on(DragEvent, e => { this.lastDragTime = Date.now(); });
this.redraw();
}
redraw() {
console.log('redrew territory layer')
console.log('redrew territory layer');
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext("2d")
this.context = this.canvas.getContext("2d");
this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height())
this.initImageData()
this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height());
this.initImageData();
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.context.putImageData(this.imageData, 0, 0);
@@ -96,26 +97,27 @@ export class TerritoryLayer implements Layer {
this.highlightCanvas.height = this.game.height();
this.game.forEachTile(t => {
this.paintTerritory(t)
})
this.paintTerritory(t);
});
}
initImageData() {
this.game.forEachTile((tile) => {
const index = (tile.cell().y * this.game.width()) + tile.cell().x
const offset = index * 4
this.imageData.data[offset + 3] = 0
})
const cell = new Cell(this.game.x(tile), this.game.y(tile));
const index = (cell.y * this.game.width()) + cell.x;
const offset = index * 4;
this.imageData.data[offset + 3] = 0;
});
}
renderLayer(context: CanvasRenderingContext2D) {
if (Date.now() > this.lastDragTime + this.nodrawDragDuration && Date.now() > this.lastRefresh + this.refreshRate) {
this.lastRefresh = Date.now()
this.renderTerritory()
this.lastRefresh = Date.now();
this.renderTerritory();
this.context.putImageData(this.imageData, 0, 0);
}
if (this.alternativeView) {
return
return;
}
context.drawImage(
@@ -124,7 +126,7 @@ export class TerritoryLayer implements Layer {
-this.game.height() / 2,
this.game.width(),
this.game.height()
)
);
if (this.game.inSpawnPhase()) {
context.drawImage(
this.highlightCanvas,
@@ -137,62 +139,60 @@ export class TerritoryLayer implements Layer {
}
renderTerritory() {
let numToRender = Math.floor(this.tileToRenderQueue.size() / 5)
let numToRender = Math.floor(this.tileToRenderQueue.size() / 5);
if (numToRender == 0 || this.game.inSpawnPhase()) {
numToRender = this.tileToRenderQueue.size()
numToRender = this.tileToRenderQueue.size();
}
while (numToRender > 0) {
numToRender--
const tile = this.tileToRenderQueue.pop().tile
this.paintTerritory(tile)
tile.neighbors().forEach(t => this.paintTerritory(t, true))
numToRender--;
const tile = this.tileToRenderQueue.pop().tile;
this.paintTerritory(tile);
for (const neighbor of this.game.neighbors(tile)) {
this.paintTerritory(neighbor, true);
}
}
}
paintTerritory(tile: Tile, isBorder: boolean = false) {
if (isBorder && !tile.hasOwner()) {
return
paintTerritory(tile: TileRef, isBorder: boolean = false) {
if (isBorder && !this.game.hasOwner(tile)) {
return;
}
if (!tile.hasOwner()) {
if (tile.hasFallout()) {
this.paintCell(tile.cell(), this.theme.falloutColor(), 150)
return
if (!this.game.hasOwner(tile)) {
if (this.game.hasFallout(tile)) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
this.theme.falloutColor(),
150
);
return;
}
this.clearCell(tile.cell())
return
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
return;
}
const owner = tile.owner() as Player
if (tile.isBorder()) {
if (tile.hasDefenseBonus()) {
this.paintCell(
tile.cell(),
this.theme.defendedBorderColor(owner.info()),
255
)
} else {
this.paintCell(
tile.cell(),
this.theme.borderColor(owner.info()),
255
)
}
const owner = this.game.owner(tile) as Player;
if (this.game.isBorder(tile)) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
this.theme.borderColor(owner.info()),
255
);
} else {
this.paintCell(
tile.cell(),
new Cell(this.game.x(tile), this.game.y(tile)),
this.theme.territoryColor(owner.info()),
150
)
);
}
}
paintCell(cell: Cell, color: Colord, alpha: number) {
const index = (cell.y * this.game.width()) + cell.x
const offset = index * 4
const index = (cell.y * this.game.width()) + cell.x;
const offset = index * 4;
this.imageData.data[offset] = color.rgba.r;
this.imageData.data[offset + 1] = color.rgba.g;
this.imageData.data[offset + 2] = color.rgba.b;
this.imageData.data[offset + 3] = alpha
this.imageData.data[offset + 3] = alpha;
}
clearCell(cell: Cell) {
@@ -201,12 +201,15 @@ export class TerritoryLayer implements Layer {
this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
enqueueTile(tile: Tile) {
this.tileToRenderQueue.push({ tile: tile, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5) })
enqueueTile(tile: TileRef) {
this.tileToRenderQueue.push({
tile: tile,
lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5)
});
}
paintHighlightCell(cell: Cell, color: Colord, alpha: number) {
this.clearCell(cell)
this.clearCell(cell);
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
this.highlightContext.fillRect(cell.x, cell.y, 1, 1);
}
+213 -89
View File
@@ -1,12 +1,12 @@
import { Colord } from "colord";
import { Theme } from "../../../core/configuration/Config";
import { Unit, Cell, Game, Tile, UnitType, Player, UnitUpdate } from "../../../core/game/Game";
import { bfs, dist, euclDist } from "../../../core/Util";
import { Unit, UnitType, Player, UnitUpdate } from "../../../core/game/Game";
import { Layer } from "./Layer";
import { EventBus } from "../../../core/EventBus";
import { AlternateViewEvent } from "../../InputHandler";
import { ClientID } from "../../../core/Schemas";
import { GameView } from "../../../core/GameView";
import { euclDistFN, manhattanDistFN, TileRef } from "../../../core/game/GameMap";
enum Relationship {
Self,
@@ -18,15 +18,15 @@ export class UnitLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private boatToTrail = new Map<Unit, Set<Tile>>();
private boatToTrail = new Map<Unit, Set<TileRef>>();
private theme: Theme = null;
private alternateView = false
private alternateView = false;
private myPlayer: Player | null = null
private myPlayer: Player | null = null;
private oldShellTile = new Map<Unit, Tile>()
private oldShellTile = new Map<Unit, TileRef>();
constructor(private game: GameView, private eventBus: EventBus, private clientID: ClientID) {
this.theme = game.config().theme();
@@ -38,17 +38,17 @@ export class UnitLayer implements Layer {
tick() {
if (this.myPlayer == null) {
this.myPlayer = this.game.playerByClientID(this.clientID)
this.myPlayer = this.game.playerByClientID(this.clientID);
}
for (const unit of this.game.units()) {
if (unit.wasUpdated())
this.onUnitEvent(unit)
this.onUnitEvent(unit);
}
}
init() {
this.eventBus.on(AlternateViewEvent, e => this.onAlternativeViewEvent(e))
this.redraw()
this.eventBus.on(AlternateViewEvent, e => this.onAlternativeViewEvent(e));
this.redraw();
}
renderLayer(context: CanvasRenderingContext2D) {
@@ -62,11 +62,10 @@ export class UnitLayer implements Layer {
}
onAlternativeViewEvent(event: AlternateViewEvent) {
this.alternateView = event.alternateView
this.redraw()
this.alternateView = event.alternateView;
this.redraw();
}
redraw() {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext("2d");
@@ -80,15 +79,15 @@ export class UnitLayer implements Layer {
private relationship(unit: Unit): Relationship {
if (this.myPlayer == null) {
return Relationship.Enemy
return Relationship.Enemy;
}
if (this.myPlayer == unit.owner()) {
return Relationship.Self
return Relationship.Self;
}
if (this.myPlayer.isAlliedWith(unit.owner())) {
return Relationship.Ally
return Relationship.Ally;
}
return Relationship.Enemy
return Relationship.Enemy;
}
onUnitEvent(unit: Unit) {
@@ -103,137 +102,262 @@ export class UnitLayer implements Layer {
this.handleBattleshipEvent(unit);
break;
case UnitType.Shell:
this.handleShellEvent(unit)
this.handleShellEvent(unit);
break;
case UnitType.TradeShip:
this.handleTradeShipEvent(unit)
this.handleTradeShipEvent(unit);
break;
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
this.handleNuke(unit)
break
this.handleNuke(unit);
break;
}
}
private handleDestroyerEvent(unit: Unit) {
const rel = this.relationship(unit)
bfs(unit.lastTile(), euclDist(unit.lastTile(), 4)).forEach(t => {
this.clearCell(t.cell());
});
if (!unit.isActive()) {
return
const rel = this.relationship(unit);
// Clear previous area
for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 4))) {
this.clearCell(this.game.x(t), this.game.y(t));
}
if (!unit.isActive()) {
return;
}
// Paint border
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 4))) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
255
);
}
// Paint territory
for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 3))) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
255
);
}
bfs(unit.tile(), euclDist(unit.tile(), 4))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255));
bfs(unit.tile(), dist(unit.tile(), 3))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255));
}
private handleBattleshipEvent(unit: Unit) {
const rel = this.relationship(unit)
bfs(unit.lastTile(), euclDist(unit.lastTile(), 6)).forEach(t => {
this.clearCell(t.cell());
});
if (!unit.isActive()) {
return
const rel = this.relationship(unit);
// Clear previous area
for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 6))) {
this.clearCell(this.game.x(t), this.game.y(t));
}
if (!unit.isActive()) {
return;
}
// Paint outer territory
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 5))) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
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().info()),
255
);
}
// Paint inner territory
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 1))) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
255
);
}
bfs(unit.tile(), euclDist(unit.tile(), 5))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255));
bfs(unit.tile(), dist(unit.tile(), 4))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255));
bfs(unit.tile(), euclDist(unit.tile(), 1))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255));
}
private handleShellEvent(unit: Unit) {
const rel = this.relationship(unit)
const rel = this.relationship(unit);
this.clearCell(unit.lastTile().cell())
// Clear current and previous positions
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
if (this.oldShellTile.has(unit)) {
this.clearCell(this.oldShellTile.get(unit).cell())
const oldTile = this.oldShellTile.get(unit);
this.clearCell(this.game.x(oldTile), this.game.y(oldTile));
}
this.oldShellTile.set(unit, unit.lastTile())
this.oldShellTile.set(unit, unit.lastTile());
if (!unit.isActive()) {
return
return;
}
this.paintCell(unit.tile().cell(), rel, this.theme.borderColor(unit.owner().info()), 255)
this.paintCell(unit.lastTile().cell(), rel, this.theme.borderColor(unit.owner().info()), 255)
// Paint current and previous positions
this.paintCell(
this.game.x(unit.tile()),
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner().info()),
255
);
this.paintCell(
this.game.x(unit.lastTile()),
this.game.y(unit.lastTile()),
rel,
this.theme.borderColor(unit.owner().info()),
255
);
}
private handleNuke(unit: Unit) {
const rel = this.relationship(unit)
bfs(unit.lastTile(), euclDist(unit.lastTile(), 2)).forEach(t => {
this.clearCell(t.cell());
});
if (unit.isActive()) {
bfs(unit.tile(), euclDist(unit.tile(), 2))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255));
const rel = this.relationship(unit);
// Clear previous area
for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 2))) {
this.clearCell(this.game.x(t), this.game.y(t));
}
if (unit.isActive()) {
// Paint area
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 2))) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
255
);
}
}
}
private handleTradeShipEvent(unit: Unit) {
const rel = this.relationship(unit)
bfs(unit.lastTile(), euclDist(unit.lastTile(), 3)).forEach(t => {
this.clearCell(t.cell());
});
if (unit.isActive()) {
bfs(unit.tile(), dist(unit.tile(), 2))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255));
const rel = this.relationship(unit);
// Clear previous area
for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 3))) {
this.clearCell(this.game.x(t), this.game.y(t));
}
if (unit.isActive()) {
bfs(unit.tile(), dist(unit.tile(), 1))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255));
// 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().info()),
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().info()),
255
);
}
}
}
private handleBoatEvent(unit: Unit) {
const rel = this.relationship(unit)
const rel = this.relationship(unit);
if (!this.boatToTrail.has(unit)) {
this.boatToTrail.set(unit, new Set<Tile>());
this.boatToTrail.set(unit, new Set<TileRef>());
}
const trail = this.boatToTrail.get(unit);
trail.add(unit.lastTile());
bfs(unit.lastTile(), dist(unit.lastTile(), 3)).forEach(t => {
this.clearCell(t.cell());
});
// Clear previous area
for (const t of this.game.bfs(unit.lastTile(), manhattanDistFN(unit.lastTile(), 3))) {
this.clearCell(this.game.x(t), this.game.y(t));
}
if (unit.isActive()) {
// Paint trail
for (const t of trail) {
this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 150);
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
150
);
}
// Paint border
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().info()),
255
);
}
// Paint territory
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().info()),
255
);
}
bfs(unit.tile(), dist(unit.tile(), 2))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.borderColor(unit.owner().info()), 255));
bfs(unit.tile(), dist(unit.tile(), 1))
.forEach(t => this.paintCell(t.cell(), rel, this.theme.territoryColor(unit.owner().info()), 255));
} else {
trail.forEach(t => this.clearCell(t.cell()));
for (const t of trail) {
this.clearCell(this.game.x(t), this.game.y(t));
}
this.boatToTrail.delete(unit);
}
}
paintCell(cell: Cell, relationship: Relationship, color: Colord, alpha: number) {
this.clearCell(cell)
paintCell(x: number, y: number, relationship: Relationship, color: Colord, alpha: number) {
this.clearCell(x, y);
if (this.alternateView) {
switch (relationship) {
case Relationship.Self:
this.context.fillStyle = this.theme.selfColor().toRgbString()
break
this.context.fillStyle = this.theme.selfColor().toRgbString();
break;
case Relationship.Ally:
this.context.fillStyle = this.theme.allyColor().toRgbString()
break
this.context.fillStyle = this.theme.allyColor().toRgbString();
break;
case Relationship.Enemy:
this.context.fillStyle = this.theme.enemyColor().toRgbString()
break
this.context.fillStyle = this.theme.enemyColor().toRgbString();
break;
}
} else {
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
}
this.context.fillRect(cell.x, cell.y, 1, 1);
this.context.fillRect(x, y, 1, 1);
}
clearCell(cell: Cell) {
this.context.clearRect(cell.x, cell.y, 1, 1);
clearCell(x: number, y: number) {
this.context.clearRect(x, y, 1, 1);
}
}
@@ -230,7 +230,7 @@ export class BuildMenu extends LitElement {
}
showMenu(player: PlayerView, clickedCell: Cell) {
player.actions(this.game.tile(clickedCell)).then(actions => {
player.actions(this.game.ref(clickedCell.x, clickedCell.y)).then(actions => {
console.log(`got actions: ${JSON.stringify(actions)}`)
this.playerActions = actions
this.myPlayer = player;
+20 -20
View File
@@ -1,7 +1,6 @@
import { EventBus } from "../../../../core/EventBus";
import { AllPlayers, Cell, Game, Player, PlayerActions, Tile, UnitType } from "../../../../core/game/Game";
import { AllPlayers, Cell, Game, Player, PlayerActions, } from "../../../../core/game/Game";
import { ClientID } from "../../../../core/Schemas";
import { and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore, targetTransportTile } from "../../../../core/Util";
import { ContextMenuEvent, MouseUpEvent, ShowBuildMenuEvent } from "../../../InputHandler";
import { SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendDonateIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent } from "../../../Transport";
import { TransformHandler } from "../../TransformHandler";
@@ -21,6 +20,7 @@ import { UIState } from "../../UIState";
import { BuildMenu } from "./BuildMenu";
import { consolex } from "../../../../core/Consolex";
import { GameView, PlayerView } from "../../../../core/GameView";
import { TileRef } from "../../../../core/game/GameMap";
enum Slot {
@@ -54,7 +54,7 @@ export class RadialMenu implements Layer {
constructor(
private eventBus: EventBus,
private game: GameView,
private g: GameView,
private transformHandler: TransformHandler,
private clientID: ClientID,
private emojiTable: EmojiTable,
@@ -70,10 +70,10 @@ export class RadialMenu implements Layer {
if (clickedCell == null) {
return
}
if (!this.game.isOnMap(clickedCell)) {
if (!this.g.isValidCoord(clickedCell.x, clickedCell.y)) {
return
}
const p = this.game.playerByClientID(this.clientID)
const p = this.g.playerByClientID(this.clientID)
if (p == null) {
return
}
@@ -233,19 +233,19 @@ export class RadialMenu implements Layer {
}
this.clickedCell = this.transformHandler.screenToWorldCoordinates(event.x, event.y)
if (!this.game.isOnMap(this.clickedCell)) {
if (!this.g.isValidCoord(this.clickedCell.x, this.clickedCell.y)) {
return
}
const tile = this.game.tile(this.clickedCell)
const tile = this.g.ref(this.clickedCell.x, this.clickedCell.y)
if (this.game.inSpawnPhase()) {
if (tile.terrain().isLand() && !tile.hasOwner()) {
if (this.g.inSpawnPhase()) {
if (this.g.isLand(tile) && !this.g.hasOwner(tile)) {
this.enableCenterButton(true)
}
return
}
const myPlayer = this.game.playerViews().find(p => p.clientID() == this.clientID)
const myPlayer = this.g.playerViews().find(p => p.clientID() == this.clientID)
if (!myPlayer) {
consolex.warn('my player not found')
return
@@ -255,13 +255,13 @@ export class RadialMenu implements Layer {
})
}
private handlePlayerActions(myPlayer: PlayerView, actions: PlayerActions, tile: Tile) {
private handlePlayerActions(myPlayer: PlayerView, actions: PlayerActions, tile: TileRef) {
this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => {
this.buildMenu.showMenu(myPlayer, this.clickedCell)
})
if (actions.interaction?.canSendEmoji) {
this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => {
const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player)
const target = this.g.owner(tile) == myPlayer ? AllPlayers : (this.g.owner(tile) as Player)
this.emojiTable.onEmojiClicked = (emoji: string) => {
this.emojiTable.hideTable()
this.eventBus.emit(new SendEmojiIntentEvent(target, emoji))
@@ -274,7 +274,7 @@ export class RadialMenu implements Layer {
this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => {
this.eventBus.emit(
new SendBoatAttackIntentEvent(
tile.owner().id(),
this.g.owner(tile).id(),
this.clickedCell,
this.uiState.attackRatio * myPlayer.troops()
)
@@ -285,10 +285,10 @@ export class RadialMenu implements Layer {
this.enableCenterButton(true)
}
if (!tile.hasOwner()) {
if (!this.g.hasOwner(tile)) {
return
}
const other = tile.owner() as Player
const other = this.g.owner(tile) as Player
if (actions?.interaction.canDonate) {
@@ -351,13 +351,13 @@ export class RadialMenu implements Layer {
return
}
consolex.log('Center button clicked');
const clicked = this.game.tile(this.clickedCell)
if (this.game.inSpawnPhase()) {
const clicked = this.g.ref(this.clickedCell.x, this.clickedCell.y)
if (this.g.inSpawnPhase()) {
this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell))
} else {
const myPlayer = this.game.players().find(p => p.clientID() == this.clientID)
if (myPlayer != null && clicked.owner() != myPlayer) {
this.eventBus.emit(new SendAttackIntentEvent(clicked.owner().id(), this.uiState.attackRatio * myPlayer.troops()))
const myPlayer = this.g.players().find(p => p.clientID() == this.clientID)
if (myPlayer != null && this.g.owner(clicked) != myPlayer) {
this.eventBus.emit(new SendAttackIntentEvent(this.g.owner(clicked).id(), this.uiState.attackRatio * myPlayer.troops()))
}
}
this.hideRadialMenu();