Merge branch 'main' into configure-cors
@@ -24,7 +24,7 @@ FROM base
|
||||
COPY --from=dependencies / /
|
||||
|
||||
ARG GIT_COMMIT=unknown
|
||||
ENV GIT_COMMIT=$GIT_COMMIT
|
||||
ENV GIT_COMMIT="$GIT_COMMIT"
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /usr/src/app
|
||||
@@ -35,7 +35,7 @@ COPY package*.json ./
|
||||
# Install dependencies while bypassing Husky hooks
|
||||
ENV HUSKY=0
|
||||
ENV NPM_CONFIG_IGNORE_SCRIPTS=1
|
||||
RUN mkdir -p .git && npm install
|
||||
RUN mkdir -p .git && npm ci
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
@@ -45,7 +45,7 @@ RUN npm run build-prod
|
||||
|
||||
# So we can see which commit was used to build the container
|
||||
# https://openfront.io/commit.txt
|
||||
RUN echo $GIT_COMMIT > static/commit.txt
|
||||
RUN echo "$GIT_COMMIT" > static/commit.txt
|
||||
|
||||
# Copy Nginx configuration and ensure it's used instead of the default
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-jest": "^29.7.0",
|
||||
"binary-base64-loader": "^1.0.0",
|
||||
"canvas": "^3.1.0",
|
||||
"chai": "^5.1.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -58,6 +59,7 @@
|
||||
"html-loader": "^5.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^30.0.0-beta.3",
|
||||
"lint-staged": "^15.4.3",
|
||||
"mrmime": "^2.0.0",
|
||||
"postcss": "^8.5.1",
|
||||
@@ -123,6 +125,7 @@
|
||||
"jimp": "^0.22.12",
|
||||
"jose": "^6.0.10",
|
||||
"lit": "^3.2.1",
|
||||
"lit-markdown": "^1.3.2",
|
||||
"msgpack5": "^6.0.2",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-addon-api": "^8.1.0",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# header
|
||||
|
||||
changelog here
|
||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -400,12 +400,22 @@
|
||||
"sams": "SAMs",
|
||||
"warships": "Warships",
|
||||
"health": "Health",
|
||||
"attitude": "Attitude"
|
||||
"attitude": "Attitude",
|
||||
"levels": "Levels"
|
||||
},
|
||||
"events_display": {
|
||||
"retreating": "retreating",
|
||||
"boat": "Boat"
|
||||
},
|
||||
"unit_info_modal": {
|
||||
"structure_info": "Structure Info",
|
||||
"unit_type_unknown": "Unknown",
|
||||
"close": "Close",
|
||||
"cooldown": "Cooldown",
|
||||
"type": "Type",
|
||||
"upgrade": "Upgrade",
|
||||
"level": "Level"
|
||||
},
|
||||
"relation": {
|
||||
"hostile": "Hostile",
|
||||
"distrustful": "Distrustful",
|
||||
@@ -435,6 +445,10 @@
|
||||
"none": "None",
|
||||
"alliances": "Alliances"
|
||||
},
|
||||
"replay_panel": {
|
||||
"replay_speed": "Replay speed",
|
||||
"game_speed": "Game speed"
|
||||
},
|
||||
"error_modal": {
|
||||
"crashed": "Game crashed!",
|
||||
"paste_discord": "Please paste the following in your bug report in Discord:",
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
"coordinates": [564, 845],
|
||||
"name": "Southern Uí Néill",
|
||||
"strength": 3,
|
||||
"flag": "1_Southern Uí Néill"
|
||||
"flag": "1_Southern Ui Neill"
|
||||
},
|
||||
{
|
||||
"coordinates": [639, 680],
|
||||
@@ -133,7 +133,7 @@
|
||||
"coordinates": [416, 678],
|
||||
"name": "Northern Uí Néill",
|
||||
"strength": 3,
|
||||
"flag": "1_Northern Uí Néill"
|
||||
"flag": "1_Northern Ui Neill"
|
||||
},
|
||||
{
|
||||
"coordinates": [1869, 1308],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
v0.24.0-dev
|
||||
@@ -268,8 +268,10 @@ export class ClientGameRunner {
|
||||
});
|
||||
const worker = this.worker;
|
||||
const keepWorkerAlive = () => {
|
||||
worker.sendHeartbeat();
|
||||
requestAnimationFrame(keepWorkerAlive);
|
||||
if (this.isActive) {
|
||||
worker.sendHeartbeat();
|
||||
requestAnimationFrame(keepWorkerAlive);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(keepWorkerAlive);
|
||||
|
||||
@@ -329,8 +331,10 @@ export class ClientGameRunner {
|
||||
}
|
||||
|
||||
public stop(saveFullGame: boolean = false) {
|
||||
this.worker.cleanup();
|
||||
if (!this.isActive) return;
|
||||
|
||||
this.isActive = false;
|
||||
this.worker.cleanup();
|
||||
this.transport.leaveGame(saveFullGame);
|
||||
if (this.connectionCheckInterval) {
|
||||
clearInterval(this.connectionCheckInterval);
|
||||
@@ -516,12 +520,13 @@ export class ClientGameRunner {
|
||||
if (this.transport.isLocal) {
|
||||
return;
|
||||
}
|
||||
const timeSinceLastMessage = Date.now() - this.lastMessageTime;
|
||||
const now = Date.now();
|
||||
const timeSinceLastMessage = now - this.lastMessageTime;
|
||||
if (timeSinceLastMessage > 5000) {
|
||||
console.log(
|
||||
`No message from server for ${timeSinceLastMessage} ms, reconnecting`,
|
||||
);
|
||||
this.lastMessageTime = Date.now();
|
||||
this.lastMessageTime = now;
|
||||
this.transport.reconnect();
|
||||
}
|
||||
}
|
||||
@@ -554,26 +559,25 @@ function showErrorModal(
|
||||
const button = document.createElement("button");
|
||||
button.textContent = translateText("error_modal.copy_clipboard");
|
||||
button.className = "copy-btn";
|
||||
button.addEventListener("click", () => {
|
||||
navigator.clipboard
|
||||
.writeText(content)
|
||||
.then(() => (button.textContent = translateText("error_modal.copied")))
|
||||
.catch(
|
||||
() => (button.textContent = translateText("error_modal.failed_copy")),
|
||||
);
|
||||
});
|
||||
|
||||
const closeButton = document.createElement("button");
|
||||
closeButton.textContent = "X";
|
||||
closeButton.className = "close-btn";
|
||||
closeButton.addEventListener("click", () => {
|
||||
modal.style.display = "none";
|
||||
button.addEventListener("click", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
button.textContent = translateText("error_modal.copied");
|
||||
} catch {
|
||||
button.textContent = translateText("error_modal.failed_copy");
|
||||
}
|
||||
});
|
||||
|
||||
// Add to modal
|
||||
modal.appendChild(pre);
|
||||
modal.appendChild(button);
|
||||
if (closable) {
|
||||
const closeButton = document.createElement("button");
|
||||
closeButton.textContent = "X";
|
||||
closeButton.className = "close-btn";
|
||||
closeButton.addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
modal.appendChild(closeButton);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { getAltKey, getModifierKey, translateText } from "../client/Utils";
|
||||
import "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
|
||||
@@ -41,7 +41,7 @@ export class HelpModal extends LitElement {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="scroll-combo-horizontal">
|
||||
<span class="key">Shift</span>
|
||||
<span class="key">⇧ Shift</span>
|
||||
<span class="plus">+</span>
|
||||
<div class="mouse-shell alt-left-click">
|
||||
<div class="mouse-left-corner"></div>
|
||||
@@ -54,7 +54,7 @@ export class HelpModal extends LitElement {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="scroll-combo-horizontal">
|
||||
<span class="key">Ctrl</span>
|
||||
<span class="key">${getModifierKey()}</span>
|
||||
<span class="plus">+</span>
|
||||
<div class="mouse-shell alt-left-click">
|
||||
<div class="mouse-left-corner"></div>
|
||||
@@ -67,7 +67,7 @@ export class HelpModal extends LitElement {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="scroll-combo-horizontal">
|
||||
<span class="key">Alt</span>
|
||||
<span class="key">${getAltKey()}</span>
|
||||
<span class="plus">+</span>
|
||||
<div class="mouse-shell alt-left-click">
|
||||
<div class="mouse-left-corner"></div>
|
||||
@@ -99,7 +99,7 @@ export class HelpModal extends LitElement {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="scroll-combo-horizontal">
|
||||
<span class="key">Shift</span>
|
||||
<span class="key">⇧ Shift</span>
|
||||
<span class="plus">+</span>
|
||||
<div class="mouse-with-arrows">
|
||||
<div class="mouse-shell">
|
||||
@@ -116,7 +116,8 @@ export class HelpModal extends LitElement {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key">ALT</span> + <span class="key">R</span>
|
||||
<span class="key">${getAltKey()}</span> +
|
||||
<span class="key">R</span>
|
||||
</td>
|
||||
<td>${translateText("help_modal.action_reset_gfx")}</td>
|
||||
</tr>
|
||||
@@ -139,6 +140,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Leaderboard"
|
||||
title="Leaderboard"
|
||||
class="default-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -158,6 +160,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Control panel"
|
||||
title="Control panel"
|
||||
class="default-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -188,12 +191,14 @@ export class HelpModal extends LitElement {
|
||||
alt="Event panel"
|
||||
title="Event panel"
|
||||
class="default-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
<img
|
||||
src="/images/helpModal/eventsPanelAttack.webp"
|
||||
alt="Event panel"
|
||||
title="Event panel"
|
||||
class="default-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,6 +230,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Options"
|
||||
title="Options"
|
||||
class="default-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -252,6 +258,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Player info overlay"
|
||||
title="Player info overlay"
|
||||
class="default-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -274,12 +281,14 @@ export class HelpModal extends LitElement {
|
||||
alt="Radial menu"
|
||||
title="Radial menu"
|
||||
class="default-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
<img
|
||||
src="/images/helpModal/radialMenuAlly.webp"
|
||||
alt="Radial menu ally"
|
||||
title="Radial menu ally"
|
||||
class="default-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -294,6 +303,7 @@ export class HelpModal extends LitElement {
|
||||
src="/images/InfoIcon.svg"
|
||||
class="inline-block icon"
|
||||
style="fill: white; background: transparent;"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span>${translateText("help_modal.radial_info")}</span>
|
||||
</li>
|
||||
@@ -330,6 +340,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Enemy info panel"
|
||||
title="Enemy info panel"
|
||||
class="info-panel-img"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
@@ -373,6 +384,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Ally info panel"
|
||||
title="Ally info panel"
|
||||
class="info-panel-img"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
@@ -482,6 +494,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Number 1 player"
|
||||
title="Number 1 player"
|
||||
class="player-icon-img w-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -498,6 +511,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Traitor"
|
||||
title="Traitor"
|
||||
class="player-icon-img w-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -514,6 +528,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Ally"
|
||||
title="Ally"
|
||||
class="player-icon-img w-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -532,6 +547,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Stopped trading"
|
||||
title="Stopped trading"
|
||||
class="player-icon-img w-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -548,6 +564,7 @@ export class HelpModal extends LitElement {
|
||||
alt="Alliance Request"
|
||||
title="Alliance Request"
|
||||
class="player-icon-img w-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventBus, GameEvent } from "../core/EventBus";
|
||||
import { UnitView } from "../core/game/GameView";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier";
|
||||
|
||||
export class MouseUpEvent implements GameEvent {
|
||||
constructor(
|
||||
@@ -82,6 +83,10 @@ export class AttackRatioEvent implements GameEvent {
|
||||
constructor(public readonly attackRatio: number) {}
|
||||
}
|
||||
|
||||
export class ReplaySpeedChangeEvent implements GameEvent {
|
||||
constructor(public readonly replaySpeedMultiplier: ReplaySpeedMultiplier) {}
|
||||
}
|
||||
|
||||
export class CenterCameraEvent implements GameEvent {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -103,6 +108,7 @@ export class InputHandler {
|
||||
|
||||
private moveInterval: NodeJS.Timeout | null = null;
|
||||
private activeKeys = new Set<string>();
|
||||
private keybinds: Record<string, string> = {};
|
||||
|
||||
private readonly PAN_SPEED = 5;
|
||||
private readonly ZOOM_SPEED = 10;
|
||||
@@ -115,7 +121,7 @@ export class InputHandler {
|
||||
) {}
|
||||
|
||||
initialize() {
|
||||
const keybinds = {
|
||||
this.keybinds = {
|
||||
toggleView: "Space",
|
||||
centerCamera: "KeyC",
|
||||
moveUp: "KeyW",
|
||||
@@ -127,8 +133,17 @@ export class InputHandler {
|
||||
attackRatioDown: "Digit1",
|
||||
attackRatioUp: "Digit2",
|
||||
boatAttack: "KeyB",
|
||||
modifierKey: "ControlLeft",
|
||||
altKey: "AltLeft",
|
||||
...JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}"),
|
||||
};
|
||||
|
||||
// Mac users might have different keybinds
|
||||
const isMac = /Mac/.test(navigator.userAgent);
|
||||
if (isMac) {
|
||||
this.keybinds.modifierKey = "MetaLeft"; // Use Command key on Mac
|
||||
}
|
||||
|
||||
this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e));
|
||||
window.addEventListener("pointerup", (e) => this.onPointerUp(e));
|
||||
this.canvas.addEventListener(
|
||||
@@ -154,22 +169,22 @@ export class InputHandler {
|
||||
let deltaY = 0;
|
||||
|
||||
if (
|
||||
this.activeKeys.has(keybinds.moveUp) ||
|
||||
this.activeKeys.has(this.keybinds.moveUp) ||
|
||||
this.activeKeys.has("ArrowUp")
|
||||
)
|
||||
deltaY += this.PAN_SPEED;
|
||||
if (
|
||||
this.activeKeys.has(keybinds.moveDown) ||
|
||||
this.activeKeys.has(this.keybinds.moveDown) ||
|
||||
this.activeKeys.has("ArrowDown")
|
||||
)
|
||||
deltaY -= this.PAN_SPEED;
|
||||
if (
|
||||
this.activeKeys.has(keybinds.moveLeft) ||
|
||||
this.activeKeys.has(this.keybinds.moveLeft) ||
|
||||
this.activeKeys.has("ArrowLeft")
|
||||
)
|
||||
deltaX += this.PAN_SPEED;
|
||||
if (
|
||||
this.activeKeys.has(keybinds.moveRight) ||
|
||||
this.activeKeys.has(this.keybinds.moveRight) ||
|
||||
this.activeKeys.has("ArrowRight")
|
||||
)
|
||||
deltaX -= this.PAN_SPEED;
|
||||
@@ -182,13 +197,13 @@ export class InputHandler {
|
||||
const cy = window.innerHeight / 2;
|
||||
|
||||
if (
|
||||
this.activeKeys.has(keybinds.zoomOut) ||
|
||||
this.activeKeys.has(this.keybinds.zoomOut) ||
|
||||
this.activeKeys.has("Minus")
|
||||
) {
|
||||
this.eventBus.emit(new ZoomEvent(cx, cy, this.ZOOM_SPEED));
|
||||
}
|
||||
if (
|
||||
this.activeKeys.has(keybinds.zoomIn) ||
|
||||
this.activeKeys.has(this.keybinds.zoomIn) ||
|
||||
this.activeKeys.has("Equal")
|
||||
) {
|
||||
this.eventBus.emit(new ZoomEvent(cx, cy, -this.ZOOM_SPEED));
|
||||
@@ -196,7 +211,7 @@ export class InputHandler {
|
||||
}, 1);
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.code === keybinds.toggleView) {
|
||||
if (e.code === this.keybinds.toggleView) {
|
||||
e.preventDefault();
|
||||
if (!this.alternateView) {
|
||||
this.alternateView = true;
|
||||
@@ -211,21 +226,21 @@ export class InputHandler {
|
||||
|
||||
if (
|
||||
[
|
||||
keybinds.moveUp,
|
||||
keybinds.moveDown,
|
||||
keybinds.moveLeft,
|
||||
keybinds.moveRight,
|
||||
keybinds.zoomOut,
|
||||
keybinds.zoomIn,
|
||||
this.keybinds.moveUp,
|
||||
this.keybinds.moveDown,
|
||||
this.keybinds.moveLeft,
|
||||
this.keybinds.moveRight,
|
||||
this.keybinds.zoomOut,
|
||||
this.keybinds.zoomIn,
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"ArrowRight",
|
||||
"Minus",
|
||||
"Equal",
|
||||
keybinds.attackRatioDown,
|
||||
keybinds.attackRatioUp,
|
||||
keybinds.centerCamera,
|
||||
this.keybinds.attackRatioDown,
|
||||
this.keybinds.attackRatioUp,
|
||||
this.keybinds.centerCamera,
|
||||
"ControlLeft",
|
||||
"ControlRight",
|
||||
].includes(e.code)
|
||||
@@ -234,7 +249,7 @@ export class InputHandler {
|
||||
}
|
||||
});
|
||||
window.addEventListener("keyup", (e) => {
|
||||
if (e.code === keybinds.toggleView) {
|
||||
if (e.code === this.keybinds.toggleView) {
|
||||
e.preventDefault();
|
||||
this.alternateView = false;
|
||||
this.eventBus.emit(new AlternateViewEvent(false));
|
||||
@@ -245,22 +260,22 @@ export class InputHandler {
|
||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||||
}
|
||||
|
||||
if (e.code === keybinds.boatAttack) {
|
||||
if (e.code === this.keybinds.boatAttack) {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new DoBoatAttackEvent());
|
||||
}
|
||||
|
||||
if (e.code === keybinds.attackRatioDown) {
|
||||
if (e.code === this.keybinds.attackRatioDown) {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new AttackRatioEvent(-10));
|
||||
}
|
||||
|
||||
if (e.code === keybinds.attackRatioUp) {
|
||||
if (e.code === this.keybinds.attackRatioUp) {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new AttackRatioEvent(10));
|
||||
}
|
||||
|
||||
if (e.code === keybinds.centerCamera) {
|
||||
if (e.code === this.keybinds.centerCamera) {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new CenterCameraEvent());
|
||||
}
|
||||
@@ -297,11 +312,11 @@ export class InputHandler {
|
||||
this.pointerDown = false;
|
||||
this.pointers.clear();
|
||||
|
||||
if (event.ctrlKey) {
|
||||
if (this.isModifierKeyPressed(event)) {
|
||||
this.eventBus.emit(new ShowBuildMenuEvent(event.clientX, event.clientY));
|
||||
return;
|
||||
}
|
||||
if (event.altKey) {
|
||||
if (this.isAltKeyPressed(event)) {
|
||||
this.eventBus.emit(new ShowEmojiMenuEvent(event.clientX, event.clientY));
|
||||
return;
|
||||
}
|
||||
@@ -400,4 +415,22 @@ export class InputHandler {
|
||||
}
|
||||
this.activeKeys.clear();
|
||||
}
|
||||
|
||||
isModifierKeyPressed(event: PointerEvent): boolean {
|
||||
return (
|
||||
(this.keybinds.modifierKey === "AltLeft" && event.altKey) ||
|
||||
(this.keybinds.modifierKey === "ControlLeft" && event.ctrlKey) ||
|
||||
(this.keybinds.modifierKey === "ShiftLeft" && event.shiftKey) ||
|
||||
(this.keybinds.modifierKey === "MetaLeft" && event.metaKey)
|
||||
);
|
||||
}
|
||||
|
||||
isAltKeyPressed(event: PointerEvent): boolean {
|
||||
return (
|
||||
(this.keybinds.altKey === "AltLeft" && event.altKey) ||
|
||||
(this.keybinds.altKey === "ControlLeft" && event.ctrlKey) ||
|
||||
(this.keybinds.altKey === "ShiftLeft" && event.shiftKey) ||
|
||||
(this.keybinds.altKey === "MetaLeft" && event.metaKey)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ export class LangSelector extends LitElement {
|
||||
"game-starting-modal",
|
||||
"top-bar",
|
||||
"player-panel",
|
||||
"replay-panel",
|
||||
"help-modal",
|
||||
"username-input",
|
||||
"public-lobby",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import {
|
||||
AllPlayersStats,
|
||||
ClientMessage,
|
||||
@@ -12,7 +13,9 @@ import {
|
||||
} from "../core/Schemas";
|
||||
import { createGameRecord, decompressGameRecord, replacer } from "../core/Util";
|
||||
import { LobbyConfig } from "./ClientGameRunner";
|
||||
import { ReplaySpeedChangeEvent } from "./InputHandler";
|
||||
import { getPersistentID } from "./Main";
|
||||
import { defaultReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier";
|
||||
|
||||
export class LocalServer {
|
||||
// All turns from the game record on replay.
|
||||
@@ -24,6 +27,7 @@ export class LocalServer {
|
||||
private startedAt: number;
|
||||
|
||||
private paused = false;
|
||||
private replaySpeedMultiplier = defaultReplaySpeedMultiplier;
|
||||
|
||||
private winner: ClientSendWinnerMessage | null = null;
|
||||
private allPlayersStats: AllPlayersStats = {};
|
||||
@@ -38,23 +42,29 @@ export class LocalServer {
|
||||
private clientConnect: () => void,
|
||||
private clientMessage: (message: ServerMessage) => void,
|
||||
private isReplay: boolean,
|
||||
private eventBus: EventBus,
|
||||
) {}
|
||||
|
||||
start() {
|
||||
this.turnCheckInterval = setInterval(() => {
|
||||
if (this.turnsExecuted === this.turns.length) {
|
||||
if (
|
||||
this.isReplay ||
|
||||
Date.now() >
|
||||
this.turnStartTime + this.lobbyConfig.serverConfig.turnIntervalMs()
|
||||
) {
|
||||
this.turnStartTime = Date.now();
|
||||
// End turn on the server means the client will start processing the turn.
|
||||
this.endTurn();
|
||||
}
|
||||
const turnIntervalMs =
|
||||
this.lobbyConfig.serverConfig.turnIntervalMs() *
|
||||
this.replaySpeedMultiplier;
|
||||
|
||||
if (
|
||||
this.turnsExecuted === this.turns.length &&
|
||||
Date.now() > this.turnStartTime + turnIntervalMs
|
||||
) {
|
||||
this.turnStartTime = Date.now();
|
||||
// End turn on the server means the client will start processing the turn.
|
||||
this.endTurn();
|
||||
}
|
||||
}, 5);
|
||||
|
||||
this.eventBus.on(ReplaySpeedChangeEvent, (event) => {
|
||||
this.replaySpeedMultiplier = event.replaySpeedMultiplier;
|
||||
});
|
||||
|
||||
this.startedAt = Date.now();
|
||||
this.clientConnect();
|
||||
if (this.lobbyConfig.gameRecord) {
|
||||
|
||||
@@ -59,11 +59,19 @@ class Client {
|
||||
constructor() {}
|
||||
|
||||
initialize(): void {
|
||||
const gameVersion = document.getElementById(
|
||||
"game-version",
|
||||
) as HTMLDivElement;
|
||||
if (!gameVersion) {
|
||||
console.warn("Game version element not found");
|
||||
}
|
||||
fetch("/version.txt")
|
||||
.then((response) => (response.ok ? response.text() : "Failed to load"))
|
||||
.then((version) => (gameVersion.innerText = version));
|
||||
|
||||
const newsModal = document.querySelector("news-modal") as NewsModal;
|
||||
if (!newsModal) {
|
||||
console.warn("News modal element not found");
|
||||
} else {
|
||||
console.log("News modal element found");
|
||||
}
|
||||
newsModal instanceof NewsModal;
|
||||
const newsButton = document.querySelector("news-button") as NewsButton;
|
||||
@@ -72,6 +80,9 @@ class Client {
|
||||
} else {
|
||||
console.log("News button element found");
|
||||
}
|
||||
fetch("/changelog.md")
|
||||
.then((response) => (response.ok ? response.text() : "Failed to load"))
|
||||
.then((changelog) => (newsModal.markdown = changelog));
|
||||
|
||||
// Comment out to show news button.
|
||||
// newsButton.hidden = true;
|
||||
@@ -79,13 +90,13 @@ class Client {
|
||||
const langSelector = document.querySelector(
|
||||
"lang-selector",
|
||||
) as LangSelector;
|
||||
const LanguageModal = document.querySelector(
|
||||
"lang-selector",
|
||||
const languageModal = document.querySelector(
|
||||
"language-modal",
|
||||
) as LanguageModal;
|
||||
if (!langSelector) {
|
||||
console.warn("Lang selector element not found");
|
||||
}
|
||||
if (!LanguageModal) {
|
||||
if (!languageModal) {
|
||||
console.warn("Language modal element not found");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, query } from "lit/decorators.js";
|
||||
import { resolveMarkdown } from "lit-markdown";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import "./components/baseComponents/Button";
|
||||
import "./components/baseComponents/Modal";
|
||||
@@ -11,6 +12,8 @@ export class NewsModal extends LitElement {
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
@property({ type: String }) markdown = "Loading...";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -51,22 +54,10 @@ export class NewsModal extends LitElement {
|
||||
<div class="options-section">
|
||||
<div class="news-container">
|
||||
<div class="news-content">
|
||||
<h3>Main things to note:</h3>
|
||||
<br />
|
||||
<ul>
|
||||
<li>Workers reproduce faster than troops.</li>
|
||||
<li>Defense = troops divided how much land you have.</li>
|
||||
<li>Attacking troops count toward your population limit.</li>
|
||||
</ul>
|
||||
<br />
|
||||
<br />
|
||||
See full changelog
|
||||
<a
|
||||
href="https://discord.com/channels/1284581928254701718/1286745902320713780"
|
||||
target="_blank"
|
||||
style="color: #4a9eff; font-weight: bold;"
|
||||
>here</a
|
||||
>.
|
||||
${resolveMarkdown(this.markdown, {
|
||||
includeImages: true,
|
||||
includeCodeBlockClassNames: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,13 @@ export class SendBreakAllianceIntentEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendUpgradeStructureIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly unitId: number,
|
||||
public readonly unitType: UnitType,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendAllianceReplyIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
// The original alliance requestor
|
||||
@@ -187,6 +194,9 @@ export class Transport {
|
||||
this.onSendSpawnIntentEvent(e),
|
||||
);
|
||||
this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e));
|
||||
this.eventBus.on(SendUpgradeStructureIntentEvent, (e) =>
|
||||
this.onSendUpgradeStructureIntent(e),
|
||||
);
|
||||
this.eventBus.on(SendBoatAttackIntentEvent, (e) =>
|
||||
this.onSendBoatAttackIntent(e),
|
||||
);
|
||||
@@ -266,6 +276,7 @@ export class Transport {
|
||||
onconnect,
|
||||
onmessage,
|
||||
this.lobbyConfig.gameRecord !== undefined,
|
||||
this.eventBus,
|
||||
);
|
||||
this.localServer.start();
|
||||
}
|
||||
@@ -426,6 +437,15 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onSendUpgradeStructureIntent(event: SendUpgradeStructureIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "upgrade_structure",
|
||||
unit: event.unitType,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unitId: event.unitId,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "targetPlayer",
|
||||
|
||||
@@ -150,3 +150,21 @@ export function getMessageTypeClasses(type: MessageType): string {
|
||||
return severityColors["white"];
|
||||
}
|
||||
}
|
||||
|
||||
export function getModifierKey(): string {
|
||||
const isMac = /Mac/.test(navigator.userAgent);
|
||||
if (isMac) {
|
||||
return "⌘"; // Command key
|
||||
} else {
|
||||
return "Ctrl";
|
||||
}
|
||||
}
|
||||
|
||||
export function getAltKey(): string {
|
||||
const isMac = /Mac/.test(navigator.userAgent);
|
||||
if (isMac) {
|
||||
return "⌥"; // Option key
|
||||
} else {
|
||||
return "Alt";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@
|
||||
"name": "Cayman Islands"
|
||||
},
|
||||
{
|
||||
"code": "Ceará",
|
||||
"code": "Ceara",
|
||||
"continent": "South America",
|
||||
"name": "Ceará"
|
||||
},
|
||||
@@ -1238,7 +1238,7 @@
|
||||
"name": "Malta"
|
||||
},
|
||||
{
|
||||
"code": "Māori flag",
|
||||
"code": "Maori flag",
|
||||
"continent": "Oceania",
|
||||
"name": "Māori Flag"
|
||||
},
|
||||
@@ -1501,7 +1501,7 @@
|
||||
"name": "Northern Mariana Islands"
|
||||
},
|
||||
{
|
||||
"code": "1_Northern Uí Néill",
|
||||
"code": "1_Northern Ui Neill",
|
||||
"continent": "Europe",
|
||||
"name": "Northern Uí Néill"
|
||||
},
|
||||
@@ -1566,7 +1566,7 @@
|
||||
"name": "Papua New Guinea"
|
||||
},
|
||||
{
|
||||
"code": "Pará",
|
||||
"code": "Para",
|
||||
"continent": "South America",
|
||||
"name": "Pará"
|
||||
},
|
||||
@@ -1803,7 +1803,7 @@
|
||||
"name": "Sao Tome and Principe"
|
||||
},
|
||||
{
|
||||
"code": "São Paulo",
|
||||
"code": "Sao Paulo",
|
||||
"continent": "South America",
|
||||
"name": "São Paulo"
|
||||
},
|
||||
@@ -1941,7 +1941,7 @@
|
||||
"name": "South Sudan"
|
||||
},
|
||||
{
|
||||
"code": "1_Southern Uí Néill",
|
||||
"code": "1_Southern Ui Neill",
|
||||
"continent": "Europe",
|
||||
"name": "Southern Uí Néill"
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ import { OptionsMenu } from "./layers/OptionsMenu";
|
||||
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
||||
import { PlayerPanel } from "./layers/PlayerPanel";
|
||||
import { PlayerTeamLabel } from "./layers/PlayerTeamLabel";
|
||||
import { ReplayPanel } from "./layers/ReplayPanel";
|
||||
import { SpawnTimer } from "./layers/SpawnTimer";
|
||||
import { StructureLayer } from "./layers/StructureLayer";
|
||||
import { TeamStats } from "./layers/TeamStats";
|
||||
@@ -126,6 +127,13 @@ export function createRenderer(
|
||||
optionsMenu.eventBus = eventBus;
|
||||
optionsMenu.game = game;
|
||||
|
||||
const replayPanel = document.querySelector("replay-panel") as ReplayPanel;
|
||||
if (!(replayPanel instanceof ReplayPanel)) {
|
||||
console.error("ReplayPanel element not found in the DOM");
|
||||
}
|
||||
replayPanel.eventBus = eventBus;
|
||||
replayPanel.game = game;
|
||||
|
||||
const topBar = document.querySelector("top-bar") as TopBar;
|
||||
if (!(topBar instanceof TopBar)) {
|
||||
console.error("top bar not found");
|
||||
@@ -215,6 +223,7 @@ export function createRenderer(
|
||||
playerInfo,
|
||||
winModel,
|
||||
optionsMenu,
|
||||
replayPanel,
|
||||
teamStats,
|
||||
topBar,
|
||||
playerPanel,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
export class ProgressBar {
|
||||
private static readonly CLEAR_PADDING = 2;
|
||||
constructor(
|
||||
private colors: string[] = [],
|
||||
private ctx: CanvasRenderingContext2D,
|
||||
private x: number,
|
||||
private y: number,
|
||||
private w: number,
|
||||
private h: number,
|
||||
private progress: number = 0, // Progress from 0 to 1
|
||||
) {
|
||||
this.setProgress(progress);
|
||||
}
|
||||
|
||||
setProgress(progress: number): void {
|
||||
progress = Math.max(0, Math.min(1, progress));
|
||||
this.clear();
|
||||
// Draw the loading bar background
|
||||
this.ctx.fillStyle = "rgba(0, 0, 0, 1)";
|
||||
this.ctx.fillRect(this.x - 1, this.y - 1, this.w, this.h);
|
||||
|
||||
// Draw the loading progress
|
||||
if (this.colors.length === 0) {
|
||||
this.ctx.fillStyle = "#808080"; // default gray
|
||||
} else {
|
||||
const idx = Math.min(
|
||||
this.colors.length - 1,
|
||||
Math.floor(progress * this.colors.length),
|
||||
);
|
||||
this.ctx.fillStyle = this.colors[idx];
|
||||
}
|
||||
this.ctx.fillRect(
|
||||
this.x,
|
||||
this.y,
|
||||
Math.max(1, Math.floor(progress * (this.w - 2))),
|
||||
this.h - 2,
|
||||
);
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.ctx.clearRect(
|
||||
this.x - ProgressBar.CLEAR_PADDING,
|
||||
this.y - ProgressBar.CLEAR_PADDING,
|
||||
this.w + ProgressBar.CLEAR_PADDING,
|
||||
this.h + ProgressBar.CLEAR_PADDING,
|
||||
);
|
||||
}
|
||||
|
||||
getX(): number {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
getY(): number {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
getProgress(): number {
|
||||
return this.progress;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import targetIcon from "../../../../resources/images/TargetIcon.svg";
|
||||
import traitorIcon from "../../../../resources/images/TraitorIcon.svg";
|
||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { AllPlayers, Cell, nukeTypes, UnitType } from "../../../core/game/Game";
|
||||
import { AllPlayers, Cell, nukeTypes } from "../../../core/game/Game";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { createCanvas, renderNumber, renderTroops } from "../../Utils";
|
||||
@@ -516,7 +516,7 @@ export class NameLayer implements Layer {
|
||||
const isSendingNuke = render.player.id() === unit.owner().id();
|
||||
const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id();
|
||||
return (
|
||||
(nukeTypes as UnitType[]).includes(unit.type()) &&
|
||||
nukeTypes.includes(unit.type()) &&
|
||||
isSendingNuke &&
|
||||
notMyPlayer &&
|
||||
unit.isActive()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameType } from "../../../core/game/Game";
|
||||
|
||||
@@ -240,18 +240,58 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.ports")}:
|
||||
${player.units(UnitType.Port).length}
|
||||
${player
|
||||
.units(UnitType.Port)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0) > 1
|
||||
? html`(${translateText("player_info_overlay.levels")}:
|
||||
${player
|
||||
.units(UnitType.Port)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0)})`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.cities")}:
|
||||
${player.units(UnitType.City).length}
|
||||
${player
|
||||
.units(UnitType.City)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0) > 1
|
||||
? html`(${translateText("player_info_overlay.levels")}:
|
||||
${player
|
||||
.units(UnitType.City)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0)})`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.missile_launchers")}:
|
||||
${player.units(UnitType.MissileSilo).length}
|
||||
${player
|
||||
.units(UnitType.MissileSilo)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0) > 1
|
||||
? html`(${translateText("player_info_overlay.levels")}:
|
||||
${player
|
||||
.units(UnitType.MissileSilo)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0)})`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.sams")}:
|
||||
${player.units(UnitType.SAMLauncher).length}
|
||||
${player
|
||||
.units(UnitType.SAMLauncher)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0) > 1
|
||||
? html`(${translateText("player_info_overlay.levels")}:
|
||||
${player
|
||||
.units(UnitType.SAMLauncher)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0)})`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.warships")}:
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameType } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { ReplaySpeedChangeEvent } from "../../InputHandler";
|
||||
import {
|
||||
defaultReplaySpeedMultiplier,
|
||||
ReplaySpeedMultiplier,
|
||||
} from "../../utilities/ReplaySpeedMultiplier";
|
||||
import { translateText } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("replay-panel")
|
||||
export class ReplayPanel extends LitElement implements Layer {
|
||||
public game: GameView | undefined;
|
||||
public eventBus: EventBus | undefined;
|
||||
|
||||
@state()
|
||||
private _replaySpeedMultiplier: number = defaultReplaySpeedMultiplier;
|
||||
private _isSinglePlayer: boolean = false;
|
||||
|
||||
@state()
|
||||
private _isVisible = false;
|
||||
|
||||
init() {
|
||||
this._isSinglePlayer =
|
||||
this.game?.config().gameConfig().gameType === GameType.Singleplayer;
|
||||
if (this._isSinglePlayer) {
|
||||
this.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this._isVisible && this.game?.config().isReplay()) {
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
onReplaySpeedChange(value: ReplaySpeedMultiplier) {
|
||||
this._replaySpeedMultiplier = value;
|
||||
this.eventBus?.emit(new ReplaySpeedChangeEvent(value));
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
// Render any necessary canvas elements
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisible(visible: boolean) {
|
||||
this._isVisible = visible;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-es-sm lg:rounded-lg backdrop-blur-md"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<label class="block mb-1 text-white" translate="no">
|
||||
${this._isSinglePlayer
|
||||
? translateText("replay_panel.game_speed")
|
||||
: translateText("replay_panel.replay_speed")}
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.slow
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.slow);
|
||||
}}
|
||||
>
|
||||
×0.5
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.normal
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.normal);
|
||||
}}
|
||||
>
|
||||
×1
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.fast
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.fast);
|
||||
}}
|
||||
>
|
||||
×2
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.fastest
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.fastest);
|
||||
}}
|
||||
>
|
||||
max
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this; // Disable shadow DOM to allow Tailwind styles
|
||||
}
|
||||
}
|
||||
@@ -242,13 +242,13 @@ export class StructureLayer implements Layer {
|
||||
const config = this.unitConfigs[unitType];
|
||||
let icon: ImageData | undefined;
|
||||
|
||||
if (unitType === UnitType.SAMLauncher && unit.isCooldown()) {
|
||||
if (unitType === UnitType.SAMLauncher && unit.isInCooldown()) {
|
||||
icon = this.unitIcons.get("reloadingSam");
|
||||
} else {
|
||||
icon = this.unitIcons.get(iconType);
|
||||
}
|
||||
|
||||
if (unitType === UnitType.MissileSilo && unit.isCooldown()) {
|
||||
if (unitType === UnitType.MissileSilo && unit.isInCooldown()) {
|
||||
icon = this.unitIcons.get("reloadingSilo");
|
||||
} else {
|
||||
icon = this.unitIcons.get(iconType);
|
||||
@@ -268,13 +268,13 @@ export class StructureLayer implements Layer {
|
||||
if (!unit.isActive()) return;
|
||||
|
||||
let borderColor = this.theme.borderColor(unit.owner());
|
||||
if (unitType === UnitType.SAMLauncher && unit.isCooldown()) {
|
||||
if (unitType === UnitType.SAMLauncher && unit.isInCooldown()) {
|
||||
borderColor = reloadingColor;
|
||||
} else if (unit.type() === UnitType.Construction) {
|
||||
borderColor = underConstructionColor;
|
||||
}
|
||||
|
||||
if (unitType === UnitType.MissileSilo && unit.isCooldown()) {
|
||||
if (unitType === UnitType.MissileSilo && unit.isInCooldown()) {
|
||||
borderColor = reloadingColor;
|
||||
} else if (unit.type() === UnitType.Construction) {
|
||||
borderColor = underConstructionColor;
|
||||
@@ -391,6 +391,7 @@ export class StructureLayer implements Layer {
|
||||
const screenPos = this.transformHandler.worldToScreenCoordinates(cell);
|
||||
const unitTile = clickedUnit.tile();
|
||||
this.unitInfoModal?.onOpenStructureModal({
|
||||
eventBus: this.eventBus,
|
||||
unit: clickedUnit,
|
||||
x: screenPos.x,
|
||||
y: screenPos.y,
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { Colord } from "colord";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import { Tick, UnitType } from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { UnitSelectionEvent } from "../../InputHandler";
|
||||
import { ProgressBar } from "../ProgressBar";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
const COLOR_PROGRESSION = [
|
||||
"rgb(232, 25, 25)",
|
||||
"rgb(240, 122, 25)",
|
||||
"rgb(202, 231, 15)",
|
||||
"rgb(44, 239, 18)",
|
||||
];
|
||||
const HEALTHBAR_WIDTH = 11; // Width of the health bar
|
||||
const LOADINGBAR_WIDTH = 18; // Width of the loading bar
|
||||
const PROGRESSBAR_HEIGHT = 3; // Height of a bar
|
||||
|
||||
/**
|
||||
* Layer responsible for drawing UI elements that overlay the game
|
||||
* such as selection boxes, health bars, etc.
|
||||
@@ -17,7 +29,11 @@ export class UILayer implements Layer {
|
||||
|
||||
private theme: Theme | null = null;
|
||||
private selectionAnimTime = 0;
|
||||
|
||||
private allProgressBars: Map<
|
||||
number,
|
||||
{ unit: UnitView; startTick: Tick; endTick: Tick; progressBar: ProgressBar }
|
||||
> = new Map();
|
||||
private allHealthBars: Map<number, ProgressBar> = new Map();
|
||||
// Keep track of currently selected unit
|
||||
private selectedUnit: UnitView | null = null;
|
||||
|
||||
@@ -51,6 +67,16 @@ export class UILayer implements Layer {
|
||||
if (this.selectedUnit && this.selectedUnit.type() === UnitType.Warship) {
|
||||
this.drawSelectionBox(this.selectedUnit);
|
||||
}
|
||||
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
|
||||
?.forEach((unitView) => {
|
||||
if (unitView === undefined) return;
|
||||
this.onUnitEvent(unitView);
|
||||
});
|
||||
|
||||
this.updateProgressBars();
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -76,6 +102,42 @@ export class UILayer implements Layer {
|
||||
this.canvas.height = this.game.height();
|
||||
}
|
||||
|
||||
onUnitEvent(unit: UnitView) {
|
||||
switch (unit.type()) {
|
||||
case UnitType.Construction: {
|
||||
const playerId = this.game.myPlayer()?.id();
|
||||
if (
|
||||
unit.isActive() &&
|
||||
playerId !== undefined &&
|
||||
unit.owner().id() === playerId
|
||||
) {
|
||||
const constructionType = unit.constructionType();
|
||||
if (constructionType === undefined) {
|
||||
// Skip units without construction type
|
||||
return;
|
||||
}
|
||||
const endTick =
|
||||
this.game.unitInfo(constructionType).constructionDuration || 0;
|
||||
this.drawLoadingBar(unit, endTick);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UnitType.Warship: {
|
||||
this.drawHealthBar(unit);
|
||||
break;
|
||||
}
|
||||
case UnitType.SAMLauncher:
|
||||
case UnitType.MissileSilo:
|
||||
if (unit.isActive() && unit.isInCooldown()) {
|
||||
const endTick = unit.ticksLeftInCooldown() || 0;
|
||||
this.drawLoadingBar(unit, endTick);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the unit selection event
|
||||
*/
|
||||
@@ -187,11 +249,71 @@ export class UILayer implements Layer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw health bar for a unit (placeholder for future implementation)
|
||||
* Draw health bar for a unit
|
||||
*/
|
||||
public drawHealthBar(unit: UnitView) {
|
||||
// This is a placeholder for future health bar implementation
|
||||
// It would draw a health bar above units that have health
|
||||
const maxHealth = this.game.unitInfo(unit.type()).maxHealth;
|
||||
if (maxHealth === undefined || this.context === null) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.allHealthBars.has(unit.id()) &&
|
||||
(unit.health() >= maxHealth || unit.health() <= 0 || !unit.isActive())
|
||||
) {
|
||||
// full hp/dead warships dont need a hp bar
|
||||
this.allHealthBars.get(unit.id())?.clear();
|
||||
this.allHealthBars.delete(unit.id());
|
||||
} else if (unit.health() < maxHealth && unit.health() > 0) {
|
||||
this.allHealthBars.get(unit.id())?.clear();
|
||||
const healthBar = new ProgressBar(
|
||||
COLOR_PROGRESSION,
|
||||
this.context,
|
||||
this.game.x(unit.tile()) - 4,
|
||||
this.game.y(unit.tile()) - 6,
|
||||
HEALTHBAR_WIDTH,
|
||||
PROGRESSBAR_HEIGHT,
|
||||
unit.health() / maxHealth,
|
||||
);
|
||||
// keep track of units that have health bars for clearing purposes
|
||||
this.allHealthBars.set(unit.id(), healthBar);
|
||||
}
|
||||
}
|
||||
|
||||
private updateProgressBars() {
|
||||
const currentTick = this.game.ticks();
|
||||
this.allProgressBars.forEach((progressBarInfo, unitId) => {
|
||||
const progress =
|
||||
(currentTick - progressBarInfo.startTick) / progressBarInfo.endTick;
|
||||
if (progress >= 1 || !progressBarInfo.unit.isActive()) {
|
||||
this.allProgressBars.get(unitId)?.progressBar.clear();
|
||||
this.allProgressBars.delete(unitId);
|
||||
return;
|
||||
}
|
||||
progressBarInfo.progressBar.setProgress(progress);
|
||||
});
|
||||
}
|
||||
|
||||
public drawLoadingBar(unit: UnitView, endTick: Tick) {
|
||||
if (!this.context) {
|
||||
return;
|
||||
}
|
||||
if (!this.allProgressBars.has(unit.id())) {
|
||||
const progressBar = new ProgressBar(
|
||||
COLOR_PROGRESSION,
|
||||
this.context,
|
||||
this.game.x(unit.tile()) - 8,
|
||||
this.game.y(unit.tile()) - 10,
|
||||
LOADINGBAR_WIDTH,
|
||||
PROGRESSBAR_HEIGHT,
|
||||
0,
|
||||
);
|
||||
this.allProgressBars.set(unit.id(), {
|
||||
unit,
|
||||
startTick: this.game.ticks(),
|
||||
endTick,
|
||||
progressBar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
paintCell(x: number, y: number, color: Colord, alpha: number) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { SendUpgradeStructureIntentEvent } from "../../Transport";
|
||||
import { Layer } from "./Layer";
|
||||
import { StructureLayer } from "./StructureLayer";
|
||||
|
||||
@@ -14,6 +17,7 @@ export class UnitInfoModal extends LitElement implements Layer {
|
||||
|
||||
public game: GameView;
|
||||
public structureLayer: StructureLayer | null = null;
|
||||
private eventBus: EventBus;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -28,12 +32,14 @@ export class UnitInfoModal extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
public onOpenStructureModal = ({
|
||||
eventBus,
|
||||
unit,
|
||||
x,
|
||||
y,
|
||||
tileX,
|
||||
tileY,
|
||||
}: {
|
||||
eventBus: EventBus;
|
||||
unit: UnitView;
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -43,6 +49,7 @@ export class UnitInfoModal extends LitElement implements Layer {
|
||||
if (!this.game) return;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.eventBus = eventBus;
|
||||
const targetRef = this.game.ref(tileX, tileY);
|
||||
|
||||
const allUnitTypes = Object.values(UnitType);
|
||||
@@ -118,12 +125,44 @@ export class UnitInfoModal extends LitElement implements Layer {
|
||||
.close-button:hover {
|
||||
background: #a00;
|
||||
}
|
||||
|
||||
.upgrade-button {
|
||||
background: #3a0;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.upgrade-button:hover {
|
||||
background: #0a0;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
if (!this.unit) return null;
|
||||
|
||||
const cooldown = this.unit.ticksLeftInCooldown() ?? 0;
|
||||
const ticksLeftInCooldown = this.unit.ticksLeftInCooldown();
|
||||
let configTimer;
|
||||
switch (this.unit.type()) {
|
||||
case UnitType.MissileSilo:
|
||||
configTimer = this.game.config().SiloCooldown();
|
||||
break;
|
||||
case UnitType.SAMLauncher:
|
||||
configTimer = this.game.config().SAMCooldown();
|
||||
break;
|
||||
}
|
||||
let cooldown = 0;
|
||||
if (ticksLeftInCooldown !== undefined && configTimer !== undefined) {
|
||||
cooldown = configTimer - (this.game.ticks() - ticksLeftInCooldown);
|
||||
}
|
||||
const secondsLeft = Math.ceil(cooldown / 10);
|
||||
|
||||
return html`
|
||||
@@ -133,17 +172,53 @@ export class UnitInfoModal extends LitElement implements Layer {
|
||||
.x}px; top: ${this.y}px; position: absolute;"
|
||||
>
|
||||
<div style="margin-bottom: 8px; font-size: 16px; font-weight: bold;">
|
||||
Structure Info
|
||||
${translateText("unit_info_modal.structure_info")}
|
||||
</div>
|
||||
<div style="margin-bottom: 4px;">
|
||||
<strong>Type:</strong> ${this.unit.type?.() ?? "Unknown"}
|
||||
<strong>${translateText("unit_info_modal.type")}:</strong>
|
||||
${translateText(+"unit_type." + this.unit.type?.().toLowerCase()) ??
|
||||
translateText("unit_info_modal.unit_type_unknown")}
|
||||
<strong
|
||||
style="display: ${this.game.unitInfo(this.unit.type()).upgradable
|
||||
? "inline"
|
||||
: "none"};"
|
||||
>${translateText("unit_info_modal.level")}:</strong
|
||||
>
|
||||
${this.game.unitInfo(this.unit.type()).upgradable &&
|
||||
this.unit.level?.()
|
||||
? this.unit.level?.()
|
||||
: ""}
|
||||
</div>
|
||||
${secondsLeft > 0
|
||||
? html`<div style="margin-bottom: 4px;">
|
||||
<strong>Cooldown:</strong> ${secondsLeft}s
|
||||
<strong>${translateText("unit_info_modal.cooldown")}</strong>
|
||||
${secondsLeft}s
|
||||
</div>`
|
||||
: ""}
|
||||
<div style="margin-top: 14px; display: flex; justify-content: center;">
|
||||
<div
|
||||
style="margin-top: 14px; display: flex; justify-content: space-between;"
|
||||
>
|
||||
<button
|
||||
@click=${() => {
|
||||
if (this.unit) {
|
||||
this.eventBus.emit(
|
||||
new SendUpgradeStructureIntentEvent(
|
||||
this.unit.id(),
|
||||
this.unit.type(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
class="upgrade-button"
|
||||
title="${translateText("unit_info_modal.upgrade")}"
|
||||
style="width: 100px; height: 32px; display: ${this.game.unitInfo(
|
||||
this.unit.type(),
|
||||
).upgradable
|
||||
? "block"
|
||||
: "none"};"
|
||||
>
|
||||
${translateText("unit_info_modal.upgrade")}
|
||||
</button>
|
||||
<button
|
||||
@click=${() => {
|
||||
this.onCloseStructureModal();
|
||||
@@ -152,10 +227,10 @@ export class UnitInfoModal extends LitElement implements Layer {
|
||||
}
|
||||
}}
|
||||
class="close-button"
|
||||
title="Close"
|
||||
title="${translateText("unit_info_modal.close")}"
|
||||
style="width: 100px; height: 32px;"
|
||||
>
|
||||
CLOSE
|
||||
${translateText("unit_info_modal.close")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +203,9 @@
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="l-header__highlightText">v23.0</div>
|
||||
<div id="game-version" class="l-header__highlightText">
|
||||
Loading version...
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="bg-image"></div>
|
||||
@@ -304,6 +306,7 @@
|
||||
class="flex flex-column gap-2 fixed right-[10px] top-[10px] z-50 flex flex-col w-32 sm:w-32 lg:w-48"
|
||||
>
|
||||
<options-menu></options-menu>
|
||||
<replay-panel></replay-panel>
|
||||
<player-info-overlay></player-info-overlay>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum ReplaySpeedMultiplier {
|
||||
slow = 2,
|
||||
normal = 1,
|
||||
fast = 0.5,
|
||||
fastest = 0,
|
||||
}
|
||||
|
||||
export const defaultReplaySpeedMultiplier = ReplaySpeedMultiplier.normal;
|
||||
@@ -34,7 +34,8 @@ export type Intent =
|
||||
| EmbargoIntent
|
||||
| QuickChatIntent
|
||||
| MoveWarshipIntent
|
||||
| MarkDisconnectedIntent;
|
||||
| MarkDisconnectedIntent
|
||||
| UpgradeStructureIntent;
|
||||
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
||||
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
|
||||
@@ -55,6 +56,9 @@ export type TargetTroopRatioIntent = z.infer<
|
||||
typeof TargetTroopRatioIntentSchema
|
||||
>;
|
||||
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
|
||||
export type UpgradeStructureIntent = z.infer<
|
||||
typeof UpgradeStructureIntentSchema
|
||||
>;
|
||||
export type MoveWarshipIntent = z.infer<typeof MoveWarshipIntentSchema>;
|
||||
export type QuickChatIntent = z.infer<typeof QuickChatIntentSchema>;
|
||||
export type MarkDisconnectedIntent = z.infer<
|
||||
@@ -178,6 +182,7 @@ const BaseIntentSchema = z.object({
|
||||
"emoji",
|
||||
"troop_ratio",
|
||||
"build_unit",
|
||||
"upgrade_structure",
|
||||
"embargo",
|
||||
"move_warship",
|
||||
]),
|
||||
@@ -266,6 +271,12 @@ export const BuildUnitIntentSchema = BaseIntentSchema.extend({
|
||||
y: z.number(),
|
||||
});
|
||||
|
||||
export const UpgradeStructureIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("upgrade_structure"),
|
||||
unit: z.nativeEnum(UnitType),
|
||||
unitId: z.number(),
|
||||
});
|
||||
|
||||
export const CancelAttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("cancel_attack"),
|
||||
attackID: z.string(),
|
||||
@@ -316,6 +327,7 @@ const IntentSchema = z.union([
|
||||
DonateTroopIntentSchema,
|
||||
TargetTroopRatioIntentSchema,
|
||||
BuildUnitIntentSchema,
|
||||
UpgradeStructureIntentSchema,
|
||||
EmbargoIntentSchema,
|
||||
MoveWarshipIntentSchema,
|
||||
QuickChatIntentSchema,
|
||||
|
||||
@@ -83,6 +83,7 @@ export const OTHER_INDEX_BUILT = 0; // Structures and warships built
|
||||
export const OTHER_INDEX_DESTROY = 1; // Structures and warships destroyed
|
||||
export const OTHER_INDEX_CAPTURE = 2; // Structures captured
|
||||
export const OTHER_INDEX_LOST = 3; // Structures/warships destroyed/captured by others
|
||||
export const OTHER_INDEX_UPGRADE = 4; // Structures upgraded
|
||||
|
||||
const BigIntStringSchema = z.preprocess((val) => {
|
||||
if (typeof val === "string" && /^\d+$/.test(val)) return BigInt(val);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { colord, Colord } from "colord";
|
||||
import { ColoredTeams, Team } from "../game/Game";
|
||||
import { simpleHash } from "../Util";
|
||||
|
||||
export const red: Colord = colord({ r: 235, g: 53, b: 53 }); // Bright Red
|
||||
export const blue: Colord = colord({ r: 41, g: 98, b: 255 }); // Royal Blue
|
||||
@@ -9,7 +11,7 @@ export const orange = colord({ h: 25, s: 95, l: 53 });
|
||||
export const green = colord({ h: 128, s: 49, l: 50 });
|
||||
export const botColor: Colord = colord({ r: 210, g: 206, b: 200 }); // Muted Beige Gray
|
||||
|
||||
export const territoryColors: Colord[] = [
|
||||
export const nationColors: Colord[] = [
|
||||
colord({ r: 230, g: 100, b: 100 }), // Bright Red
|
||||
colord({ r: 100, g: 180, b: 230 }), // Sky Blue
|
||||
colord({ r: 230, g: 180, b: 80 }), // Golden Yellow
|
||||
@@ -109,110 +111,71 @@ export const territoryColors: Colord[] = [
|
||||
colord({ r: 170, g: 150, b: 170 }), // Dusty Rose
|
||||
];
|
||||
|
||||
// Bright pastel theme with 64 colors
|
||||
export const humanColors: Colord[] = [
|
||||
// Original set
|
||||
colord({ r: 235, g: 75, b: 75 }), // Bright Red
|
||||
colord({ r: 67, g: 190, b: 84 }), // Fresh Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Royal Blue
|
||||
colord({ r: 245, g: 158, b: 11 }), // Amber
|
||||
colord({ r: 236, g: 72, b: 153 }), // Deep Pink
|
||||
colord({ r: 48, g: 178, b: 180 }), // Teal
|
||||
colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple
|
||||
colord({ r: 251, g: 191, b: 36 }), // Marigold
|
||||
colord({ r: 74, g: 222, b: 128 }), // Mint
|
||||
colord({ r: 239, g: 68, b: 68 }), // Crimson
|
||||
colord({ r: 34, g: 197, b: 94 }), // Emerald
|
||||
colord({ r: 96, g: 165, b: 250 }), // Sky Blue
|
||||
colord({ r: 249, g: 115, b: 22 }), // Tangerine
|
||||
colord({ r: 192, g: 132, b: 252 }), // Lavender
|
||||
colord({ r: 45, g: 212, b: 191 }), // Turquoise
|
||||
colord({ r: 244, g: 114, b: 182 }), // Rose
|
||||
colord({ r: 132, g: 204, b: 22 }), // Lime
|
||||
colord({ r: 56, g: 189, b: 248 }), // Light Blue
|
||||
colord({ r: 234, g: 179, b: 8 }), // Sunflower
|
||||
colord({ r: 217, g: 70, b: 239 }), // Fuchsia
|
||||
colord({ r: 16, g: 185, b: 129 }), // Sea Green
|
||||
colord({ r: 251, g: 146, b: 60 }), // Light Orange
|
||||
colord({ r: 147, g: 51, b: 234 }), // Bright Purple
|
||||
colord({ r: 79, g: 70, b: 229 }), // Indigo
|
||||
colord({ r: 245, g: 101, b: 101 }), // Coral
|
||||
colord({ r: 134, g: 239, b: 172 }), // Light Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Cerulean
|
||||
colord({ r: 253, g: 164, b: 175 }), // Salmon Pink
|
||||
colord({ r: 147, g: 197, b: 253 }), // Powder Blue
|
||||
colord({ r: 252, g: 211, b: 77 }), // Golden
|
||||
colord({ r: 190, g: 92, b: 251 }), // Amethyst
|
||||
colord({ r: 82, g: 183, b: 136 }), // Jade
|
||||
colord({ r: 248, g: 113, b: 113 }), // Warm Red
|
||||
colord({ r: 99, g: 202, b: 253 }), // Azure
|
||||
colord({ r: 240, g: 171, b: 252 }), // Orchid
|
||||
colord({ r: 163, g: 230, b: 53 }), // Yellow Green
|
||||
colord({ r: 234, g: 88, b: 12 }), // Burnt Orange
|
||||
colord({ r: 125, g: 211, b: 252 }), // Crystal Blue
|
||||
colord({ r: 251, g: 113, b: 133 }), // Watermelon
|
||||
colord({ r: 34, g: 197, b: 94 }), // Emerald
|
||||
colord({ r: 45, g: 212, b: 191 }), // Turquoise
|
||||
colord({ r: 48, g: 178, b: 180 }), // Teal
|
||||
colord({ r: 52, g: 211, b: 153 }), // Spearmint
|
||||
colord({ r: 167, g: 139, b: 250 }), // Periwinkle
|
||||
colord({ r: 245, g: 158, b: 11 }), // Honey
|
||||
colord({ r: 56, g: 189, b: 248 }), // Light Blue
|
||||
colord({ r: 59, g: 130, b: 246 }), // Royal Blue
|
||||
colord({ r: 67, g: 190, b: 84 }), // Fresh Green
|
||||
colord({ r: 74, g: 222, b: 128 }), // Mint
|
||||
colord({ r: 79, g: 70, b: 229 }), // Indigo
|
||||
colord({ r: 82, g: 183, b: 136 }), // Jade
|
||||
colord({ r: 96, g: 165, b: 250 }), // Sky Blue
|
||||
colord({ r: 99, g: 202, b: 253 }), // Azure
|
||||
colord({ r: 110, g: 231, b: 183 }), // Seafoam
|
||||
colord({ r: 233, g: 213, b: 255 }), // Light Lilac
|
||||
colord({ r: 202, g: 138, b: 4 }), // Rich Gold
|
||||
colord({ r: 151, g: 255, b: 187 }), // Fresh Mint
|
||||
colord({ r: 220, g: 38, b: 38 }), // Ruby
|
||||
colord({ r: 124, g: 58, b: 237 }), // Royal Purple
|
||||
colord({ r: 45, g: 212, b: 191 }), // Ocean
|
||||
colord({ r: 252, g: 165, b: 165 }), // Peach
|
||||
|
||||
// Additional 50 colors
|
||||
colord({ r: 179, g: 136, b: 255 }), // Light Purple
|
||||
colord({ r: 125, g: 211, b: 252 }), // Crystal Blue
|
||||
colord({ r: 132, g: 204, b: 22 }), // Lime
|
||||
colord({ r: 133, g: 77, b: 14 }), // Chocolate
|
||||
colord({ r: 52, g: 211, b: 153 }), // Aquamarine
|
||||
colord({ r: 234, g: 179, b: 8 }), // Mustard
|
||||
colord({ r: 236, g: 72, b: 153 }), // Hot Pink
|
||||
colord({ r: 147, g: 197, b: 253 }), // Sky
|
||||
colord({ r: 249, g: 115, b: 22 }), // Pumpkin
|
||||
colord({ r: 167, g: 139, b: 250 }), // Iris
|
||||
colord({ r: 16, g: 185, b: 129 }), // Pine
|
||||
colord({ r: 251, g: 146, b: 60 }), // Mango
|
||||
colord({ r: 192, g: 132, b: 252 }), // Wisteria
|
||||
colord({ r: 79, g: 70, b: 229 }), // Sapphire
|
||||
colord({ r: 245, g: 101, b: 101 }), // Salmon
|
||||
colord({ r: 134, g: 239, b: 172 }), // Spring Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Ocean Blue
|
||||
colord({ r: 253, g: 164, b: 175 }), // Rose Gold
|
||||
colord({ r: 16, g: 185, b: 129 }), // Forest
|
||||
colord({ r: 252, g: 211, b: 77 }), // Sunshine
|
||||
colord({ r: 190, g: 92, b: 251 }), // Grape
|
||||
colord({ r: 82, g: 183, b: 136 }), // Eucalyptus
|
||||
colord({ r: 248, g: 113, b: 113 }), // Cherry
|
||||
colord({ r: 99, g: 202, b: 253 }), // Arctic
|
||||
colord({ r: 240, g: 171, b: 252 }), // Lilac
|
||||
colord({ r: 163, g: 230, b: 53 }), // Chartreuse
|
||||
colord({ r: 234, g: 88, b: 12 }), // Rust
|
||||
colord({ r: 125, g: 211, b: 252 }), // Ice Blue
|
||||
colord({ r: 251, g: 113, b: 133 }), // Strawberry
|
||||
colord({ r: 52, g: 211, b: 153 }), // Sage
|
||||
colord({ r: 167, g: 139, b: 250 }), // Violet
|
||||
colord({ r: 245, g: 158, b: 11 }), // Apricot
|
||||
colord({ r: 110, g: 231, b: 183 }), // Mint Green
|
||||
colord({ r: 233, g: 213, b: 255 }), // Thistle
|
||||
colord({ r: 202, g: 138, b: 4 }), // Bronze
|
||||
colord({ r: 151, g: 255, b: 187 }), // Pistachio
|
||||
colord({ r: 220, g: 38, b: 38 }), // Fire Engine
|
||||
colord({ r: 124, g: 58, b: 237 }), // Electric Purple
|
||||
colord({ r: 45, g: 212, b: 191 }), // Caribbean
|
||||
colord({ r: 252, g: 165, b: 165 }), // Melon
|
||||
colord({ r: 168, g: 85, b: 247 }), // Byzantium
|
||||
colord({ r: 74, g: 222, b: 128 }), // Kelly Green
|
||||
colord({ r: 239, g: 68, b: 68 }), // Cardinal
|
||||
colord({ r: 34, g: 197, b: 94 }), // Shamrock
|
||||
colord({ r: 96, g: 165, b: 250 }), // Marina
|
||||
colord({ r: 249, g: 115, b: 22 }), // Carrot
|
||||
colord({ r: 192, g: 132, b: 252 }), // Heliotrope
|
||||
colord({ r: 45, g: 212, b: 191 }), // Lagoon
|
||||
colord({ r: 244, g: 114, b: 182 }), // Bubble Gum
|
||||
colord({ r: 132, g: 204, b: 22 }), // Apple
|
||||
colord({ r: 56, g: 189, b: 248 }), // Electric Blue
|
||||
colord({ r: 234, g: 179, b: 8 }), // Daffodil
|
||||
colord({ r: 134, g: 239, b: 172 }), // Light Green
|
||||
colord({ r: 147, g: 51, b: 234 }), // Bright Purple
|
||||
colord({ r: 147, g: 197, b: 253 }), // Powder Blue
|
||||
colord({ r: 151, g: 255, b: 187 }), // Fresh Mint
|
||||
colord({ r: 163, g: 230, b: 53 }), // Yellow Green
|
||||
colord({ r: 167, g: 139, b: 250 }), // Periwinkle
|
||||
colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple
|
||||
colord({ r: 179, g: 136, b: 255 }), // Light Purple
|
||||
colord({ r: 186, g: 255, b: 201 }), // Pale Emerald
|
||||
colord({ r: 190, g: 92, b: 251 }), // Amethyst
|
||||
colord({ r: 192, g: 132, b: 252 }), // Lavender
|
||||
colord({ r: 202, g: 138, b: 4 }), // Rich Gold
|
||||
colord({ r: 202, g: 225, b: 255 }), // Baby Blue
|
||||
colord({ r: 204, g: 204, b: 255 }), // Soft Lavender Blue
|
||||
colord({ r: 217, g: 70, b: 239 }), // Fuchsia
|
||||
colord({ r: 220, g: 38, b: 38 }), // Ruby
|
||||
colord({ r: 220, g: 220, b: 255 }), // Meringue Blue
|
||||
colord({ r: 220, g: 240, b: 250 }), // Ice Blue
|
||||
colord({ r: 230, g: 250, b: 210 }), // Pastel Lime
|
||||
colord({ r: 230, g: 255, b: 250 }), // Mint Whisper
|
||||
colord({ r: 233, g: 213, b: 255 }), // Light Lilac
|
||||
colord({ r: 234, g: 88, b: 12 }), // Burnt Orange
|
||||
colord({ r: 234, g: 179, b: 8 }), // Sunflower
|
||||
colord({ r: 235, g: 75, b: 75 }), // Bright Red
|
||||
colord({ r: 236, g: 72, b: 153 }), // Deep Pink
|
||||
colord({ r: 239, g: 68, b: 68 }), // Crimson
|
||||
colord({ r: 240, g: 171, b: 252 }), // Orchid
|
||||
colord({ r: 240, g: 240, b: 200 }), // Light Khaki
|
||||
colord({ r: 244, g: 114, b: 182 }), // Rose
|
||||
colord({ r: 245, g: 101, b: 101 }), // Coral
|
||||
colord({ r: 245, g: 158, b: 11 }), // Amber
|
||||
colord({ r: 248, g: 113, b: 113 }), // Warm Red
|
||||
colord({ r: 249, g: 115, b: 22 }), // Tangerine
|
||||
colord({ r: 250, g: 215, b: 225 }), // Cotton Candy
|
||||
colord({ r: 250, g: 250, b: 210 }), // Pastel Lemon
|
||||
colord({ r: 251, g: 113, b: 133 }), // Watermelon
|
||||
colord({ r: 251, g: 146, b: 60 }), // Light Orange
|
||||
colord({ r: 251, g: 191, b: 36 }), // Marigold
|
||||
colord({ r: 251, g: 235, b: 245 }), // Rose Powder
|
||||
colord({ r: 252, g: 165, b: 165 }), // Peach
|
||||
colord({ r: 252, g: 211, b: 77 }), // Golden
|
||||
colord({ r: 253, g: 164, b: 175 }), // Salmon Pink
|
||||
colord({ r: 255, g: 204, b: 229 }), // Blush Pink
|
||||
colord({ r: 255, g: 223, b: 186 }), // Apricot Cream
|
||||
colord({ r: 255, g: 240, b: 200 }), // Vanilla
|
||||
];
|
||||
|
||||
export const botColors: Colord[] = [
|
||||
@@ -266,3 +229,156 @@ export const botColors: Colord[] = [
|
||||
colord({ r: 150, g: 160, b: 140 }), // Muted Dark Olive Green
|
||||
colord({ r: 150, g: 140, b: 150 }), // Muted Dusty Rose
|
||||
];
|
||||
|
||||
// Fallback colors for when the color palette is exhausted. Currently 100 colors.
|
||||
export const fallbackColors: Colord[] = [
|
||||
colord({ r: 0, g: 5, b: 0 }), // Black Mint
|
||||
colord({ r: 0, g: 15, b: 0 }), // Deep Forest
|
||||
colord({ r: 0, g: 25, b: 0 }), // Jungle
|
||||
colord({ r: 0, g: 35, b: 0 }), // Dark Emerald
|
||||
colord({ r: 0, g: 45, b: 0 }), // Green Moss
|
||||
colord({ r: 0, g: 55, b: 0 }), // Moss Shadow
|
||||
colord({ r: 0, g: 65, b: 0 }), // Dark Meadow
|
||||
colord({ r: 0, g: 75, b: 0 }), // Forest Fern
|
||||
colord({ r: 0, g: 85, b: 0 }), // Pine Leaf
|
||||
colord({ r: 0, g: 95, b: 0 }), // Shadow Grass
|
||||
colord({ r: 0, g: 105, b: 0 }), // Classic Green
|
||||
colord({ r: 0, g: 115, b: 0 }), // Deep Lime
|
||||
colord({ r: 0, g: 125, b: 0 }), // Dense Leaf
|
||||
colord({ r: 0, g: 135, b: 0 }), // Basil Green
|
||||
colord({ r: 0, g: 145, b: 0 }), // Organic Green
|
||||
colord({ r: 0, g: 155, b: 0 }), // Bitter Herb
|
||||
colord({ r: 0, g: 165, b: 0 }), // Raw Spinach
|
||||
colord({ r: 0, g: 175, b: 0 }), // Woodland
|
||||
colord({ r: 0, g: 185, b: 0 }), // Spring Weed
|
||||
colord({ r: 0, g: 195, b: 5 }), // Apple Stem
|
||||
colord({ r: 0, g: 205, b: 10 }), // Crisp Lettuce
|
||||
colord({ r: 0, g: 215, b: 15 }), // Vibrant Green
|
||||
colord({ r: 0, g: 225, b: 20 }), // Bright Herb
|
||||
colord({ r: 0, g: 235, b: 25 }), // Green Splash
|
||||
colord({ r: 0, g: 245, b: 30 }), // Mint Leaf
|
||||
colord({ r: 0, g: 255, b: 35 }), // Fresh Mint
|
||||
colord({ r: 10, g: 255, b: 45 }), // Neon Grass
|
||||
colord({ r: 20, g: 255, b: 55 }), // Lemon Balm
|
||||
colord({ r: 30, g: 255, b: 65 }), // Juicy Green
|
||||
colord({ r: 40, g: 255, b: 75 }), // Pear Tint
|
||||
colord({ r: 50, g: 255, b: 85 }), // Avocado Pastel
|
||||
colord({ r: 60, g: 255, b: 95 }), // Lime Glow
|
||||
colord({ r: 70, g: 255, b: 105 }), // Light Leaf
|
||||
colord({ r: 80, g: 255, b: 115 }), // Soft Fern
|
||||
colord({ r: 90, g: 255, b: 125 }), // Pastel Green
|
||||
colord({ r: 100, g: 255, b: 135 }), // Green Melon
|
||||
colord({ r: 110, g: 255, b: 145 }), // Herbal Mist
|
||||
colord({ r: 120, g: 255, b: 155 }), // Kiwi Foam
|
||||
colord({ r: 130, g: 255, b: 165 }), // Aloe Fresh
|
||||
colord({ r: 140, g: 255, b: 175 }), // Light Mint
|
||||
colord({ r: 150, g: 200, b: 255 }), // Cornflower Mist
|
||||
colord({ r: 150, g: 255, b: 185 }), // Green Sorbet
|
||||
colord({ r: 160, g: 215, b: 255 }), // Powder Blue
|
||||
colord({ r: 160, g: 255, b: 195 }), // Pastel Apple
|
||||
colord({ r: 170, g: 190, b: 255 }), // Periwinkle Ice
|
||||
colord({ r: 170, g: 225, b: 255 }), // Baby Sky
|
||||
colord({ r: 170, g: 255, b: 205 }), // Aloe Breeze
|
||||
colord({ r: 180, g: 180, b: 255 }), // Pale Indigo
|
||||
colord({ r: 180, g: 235, b: 250 }), // Aqua Pastel
|
||||
colord({ r: 180, g: 255, b: 215 }), // Pale Mint
|
||||
colord({ r: 190, g: 140, b: 195 }), // Fuchsia Tint
|
||||
colord({ r: 190, g: 245, b: 240 }), // Ice Mint
|
||||
colord({ r: 190, g: 255, b: 225 }), // Mint Water
|
||||
colord({ r: 195, g: 145, b: 200 }), // Dusky Rose
|
||||
colord({ r: 200, g: 150, b: 205 }), // Plum Frost
|
||||
colord({ r: 200, g: 170, b: 255 }), // Lilac Bloom
|
||||
colord({ r: 200, g: 255, b: 215 }), // Cool Aloe
|
||||
colord({ r: 200, g: 255, b: 235 }), // Cool Mist
|
||||
colord({ r: 205, g: 155, b: 210 }), // Berry Foam
|
||||
colord({ r: 210, g: 160, b: 215 }), // Grape Cloud
|
||||
colord({ r: 210, g: 255, b: 245 }), // Sea Mist
|
||||
colord({ r: 215, g: 165, b: 220 }), // Light Bloom
|
||||
colord({ r: 215, g: 255, b: 200 }), // Fresh Mint
|
||||
colord({ r: 220, g: 160, b: 255 }), // Violet Mist
|
||||
colord({ r: 220, g: 170, b: 225 }), // Cherry Blossom
|
||||
colord({ r: 220, g: 255, b: 255 }), // Pale Aqua
|
||||
colord({ r: 225, g: 175, b: 230 }), // Faded Rose
|
||||
colord({ r: 225, g: 255, b: 175 }), // Soft Lime
|
||||
colord({ r: 230, g: 180, b: 235 }), // Dreamy Mauve
|
||||
colord({ r: 230, g: 250, b: 255 }), // Sky Haze
|
||||
colord({ r: 235, g: 150, b: 255 }), // Orchid Glow
|
||||
colord({ r: 235, g: 185, b: 240 }), // Powder Violet
|
||||
colord({ r: 240, g: 190, b: 245 }), // Pastel Violet
|
||||
colord({ r: 240, g: 240, b: 255 }), // Frosted Lilac
|
||||
colord({ r: 240, g: 250, b: 160 }), // Citrus Wash
|
||||
colord({ r: 245, g: 160, b: 240 }), // Rose Lilac
|
||||
colord({ r: 245, g: 195, b: 250 }), // Soft Magenta
|
||||
colord({ r: 245, g: 245, b: 175 }), // Lemon Mist
|
||||
colord({ r: 250, g: 200, b: 255 }), // Lilac Cream
|
||||
colord({ r: 250, g: 230, b: 255 }), // Misty Mauve
|
||||
colord({ r: 255, g: 170, b: 225 }), // Bubblegum Pink
|
||||
colord({ r: 255, g: 185, b: 215 }), // Blush Mist
|
||||
colord({ r: 255, g: 195, b: 235 }), // Faded Fuchsia
|
||||
colord({ r: 255, g: 200, b: 220 }), // Cotton Rose
|
||||
colord({ r: 255, g: 205, b: 245 }), // Pastel Orchid
|
||||
colord({ r: 255, g: 205, b: 255 }), // Violet Bloom
|
||||
colord({ r: 255, g: 210, b: 230 }), // Pastel Blush
|
||||
colord({ r: 255, g: 210, b: 250 }), // Lavender Mist
|
||||
colord({ r: 255, g: 210, b: 255 }), // Orchid Mist
|
||||
colord({ r: 255, g: 215, b: 195 }), // Apricot Glow
|
||||
colord({ r: 255, g: 215, b: 245 }), // Rose Whisper
|
||||
colord({ r: 255, g: 220, b: 235 }), // Pink Mist
|
||||
colord({ r: 255, g: 220, b: 250 }), // Powder Petal
|
||||
colord({ r: 255, g: 225, b: 180 }), // Butter Peach
|
||||
colord({ r: 255, g: 225, b: 255 }), // Petal Mist
|
||||
colord({ r: 255, g: 230, b: 245 }), // Light Rose
|
||||
colord({ r: 255, g: 235, b: 200 }), // Cream Peach
|
||||
colord({ r: 255, g: 235, b: 235 }), // Blushed Petal
|
||||
colord({ r: 255, g: 240, b: 220 }), // Pastel Sand
|
||||
colord({ r: 255, g: 245, b: 210 }), // Soft Banana
|
||||
];
|
||||
|
||||
export class ColorAllocator {
|
||||
private availableColors: Colord[];
|
||||
private fallbackColors: Colord[];
|
||||
private assigned = new Map<string, Colord>();
|
||||
|
||||
constructor(colors: Colord[], fallback: Colord[]) {
|
||||
this.availableColors = [...colors];
|
||||
this.fallbackColors = [...fallback];
|
||||
}
|
||||
|
||||
assignColor(id: string): Colord {
|
||||
if (this.assigned.has(id)) {
|
||||
return this.assigned.get(id)!;
|
||||
}
|
||||
if (this.availableColors.length === 0) {
|
||||
this.availableColors = [...this.fallbackColors];
|
||||
}
|
||||
const index = 0;
|
||||
const color = this.availableColors.splice(index, 1)[0];
|
||||
this.assigned.set(id, color);
|
||||
return color;
|
||||
}
|
||||
|
||||
assignTeamColor(team: Team): Colord {
|
||||
switch (team) {
|
||||
case ColoredTeams.Blue:
|
||||
return blue;
|
||||
case ColoredTeams.Red:
|
||||
return red;
|
||||
case ColoredTeams.Teal:
|
||||
return teal;
|
||||
case ColoredTeams.Purple:
|
||||
return purple;
|
||||
case ColoredTeams.Yellow:
|
||||
return yellow;
|
||||
case ColoredTeams.Orange:
|
||||
return orange;
|
||||
case ColoredTeams.Green:
|
||||
return green;
|
||||
case ColoredTeams.Bot:
|
||||
return botColor;
|
||||
default:
|
||||
return this.availableColors[
|
||||
simpleHash(team) % this.availableColors.length
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +359,7 @@ export class DefaultConfig implements Config {
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
@@ -402,6 +403,7 @@ export class DefaultConfig implements Config {
|
||||
: 1_000_000n,
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 10 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
@@ -418,6 +420,7 @@ export class DefaultConfig implements Config {
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 5 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
case UnitType.SAMLauncher:
|
||||
return {
|
||||
@@ -434,6 +437,7 @@ export class DefaultConfig implements Config {
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 30 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
case UnitType.City:
|
||||
return {
|
||||
@@ -451,6 +455,7 @@ export class DefaultConfig implements Config {
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
case UnitType.Construction:
|
||||
return {
|
||||
@@ -678,7 +683,11 @@ export class DefaultConfig implements Config {
|
||||
player.type() === PlayerType.Human && this.infiniteTroops()
|
||||
? 1_000_000_000
|
||||
: 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
|
||||
player.units(UnitType.City).length * this.cityPopulationIncrease();
|
||||
player
|
||||
.units(UnitType.City)
|
||||
.map((city) => city.level())
|
||||
.reduce((a, b) => a + b, 0) *
|
||||
this.cityPopulationIncrease();
|
||||
|
||||
if (player.type() === PlayerType.Bot) {
|
||||
return maxPop / 2;
|
||||
@@ -764,7 +773,7 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
defaultNukeSpeed(): number {
|
||||
return 4;
|
||||
return 6;
|
||||
}
|
||||
|
||||
// Humans can be population, soldiers attacking, soldiers in boat etc.
|
||||
@@ -773,8 +782,7 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
structureMinDist(): number {
|
||||
// TODO: Increase this to ~15 once upgradable structures are implemented.
|
||||
return 1;
|
||||
return 15;
|
||||
}
|
||||
|
||||
shellLifetime(): number {
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { Colord, colord } from "colord";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
import { ColoredTeams, PlayerType, Team, TerrainType } from "../game/Game";
|
||||
import { PlayerType, Team, TerrainType } from "../game/Game";
|
||||
import { GameMap, TileRef } from "../game/GameMap";
|
||||
import { PlayerView } from "../game/GameView";
|
||||
import {
|
||||
blue,
|
||||
botColor,
|
||||
botColors,
|
||||
green,
|
||||
ColorAllocator,
|
||||
fallbackColors,
|
||||
humanColors,
|
||||
orange,
|
||||
purple,
|
||||
red,
|
||||
teal,
|
||||
territoryColors,
|
||||
yellow,
|
||||
nationColors,
|
||||
} from "./Colors";
|
||||
import { Theme } from "./Config";
|
||||
|
||||
@@ -24,9 +17,12 @@ type ColorCache = Map<string, Colord>;
|
||||
export class PastelTheme implements Theme {
|
||||
private borderColorCache: ColorCache = new Map<string, Colord>();
|
||||
private rand = new PseudoRandom(123);
|
||||
private humanColorAllocator = new ColorAllocator(humanColors, fallbackColors);
|
||||
private botColorAllocator = new ColorAllocator(botColors, botColors);
|
||||
private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors);
|
||||
private nationColorAllocator = new ColorAllocator(nationColors, nationColors);
|
||||
|
||||
private background = colord({ r: 60, g: 60, b: 60 });
|
||||
private land = colord({ r: 194, g: 193, b: 148 });
|
||||
private shore = colord({ r: 204, g: 203, b: 158 });
|
||||
private falloutColors = [
|
||||
colord({ r: 120, g: 255, b: 71 }), // Original color
|
||||
@@ -45,26 +41,7 @@ export class PastelTheme implements Theme {
|
||||
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
|
||||
|
||||
teamColor(team: Team): Colord {
|
||||
switch (team) {
|
||||
case ColoredTeams.Blue:
|
||||
return blue;
|
||||
case ColoredTeams.Red:
|
||||
return red;
|
||||
case ColoredTeams.Teal:
|
||||
return teal;
|
||||
case ColoredTeams.Purple:
|
||||
return purple;
|
||||
case ColoredTeams.Yellow:
|
||||
return yellow;
|
||||
case ColoredTeams.Orange:
|
||||
return orange;
|
||||
case ColoredTeams.Green:
|
||||
return green;
|
||||
case ColoredTeams.Bot:
|
||||
return botColor;
|
||||
default:
|
||||
return humanColors[simpleHash(team) % humanColors.length];
|
||||
}
|
||||
return this.teamColorAllocator.assignTeamColor(team);
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
@@ -73,12 +50,12 @@ export class PastelTheme implements Theme {
|
||||
return this.teamColor(team);
|
||||
}
|
||||
if (player.type() === PlayerType.Human) {
|
||||
return humanColors[simpleHash(player.id()) % humanColors.length];
|
||||
return this.humanColorAllocator.assignColor(player.id());
|
||||
}
|
||||
if (player.type() === PlayerType.Bot) {
|
||||
return botColors[simpleHash(player.id()) % botColors.length];
|
||||
return this.botColorAllocator.assignColor(player.id());
|
||||
}
|
||||
return territoryColors[simpleHash(player.id()) % territoryColors.length];
|
||||
return this.nationColorAllocator.assignColor(player.id());
|
||||
}
|
||||
|
||||
textColor(player: PlayerView): string {
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { Colord, colord } from "colord";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
import { ColoredTeams, PlayerType, Team, TerrainType } from "../game/Game";
|
||||
import { PlayerType, Team, TerrainType } from "../game/Game";
|
||||
import { GameMap, TileRef } from "../game/GameMap";
|
||||
import { PlayerView } from "../game/GameView";
|
||||
import {
|
||||
blue,
|
||||
botColor,
|
||||
botColors,
|
||||
green,
|
||||
ColorAllocator,
|
||||
fallbackColors,
|
||||
humanColors,
|
||||
orange,
|
||||
purple,
|
||||
red,
|
||||
teal,
|
||||
territoryColors,
|
||||
yellow,
|
||||
nationColors,
|
||||
} from "./Colors";
|
||||
import { Theme } from "./Config";
|
||||
|
||||
@@ -24,9 +17,12 @@ type ColorCache = Map<string, Colord>;
|
||||
export class PastelThemeDark implements Theme {
|
||||
private borderColorCache: ColorCache = new Map<string, Colord>();
|
||||
private rand = new PseudoRandom(123);
|
||||
private humanColorAllocator = new ColorAllocator(humanColors, fallbackColors);
|
||||
private botColorAllocator = new ColorAllocator(botColors, botColors);
|
||||
private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors);
|
||||
private nationColorAllocator = new ColorAllocator(nationColors, nationColors);
|
||||
|
||||
private background = colord({ r: 0, g: 0, b: 0 });
|
||||
private land = colord({ r: 194, g: 193, b: 148 });
|
||||
private shore = colord({ r: 134, g: 133, b: 88 });
|
||||
private falloutColors = [
|
||||
colord({ r: 120, g: 255, b: 71 }), // Original color
|
||||
@@ -45,26 +41,7 @@ export class PastelThemeDark implements Theme {
|
||||
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
|
||||
|
||||
teamColor(team: Team): Colord {
|
||||
switch (team) {
|
||||
case ColoredTeams.Blue:
|
||||
return blue;
|
||||
case ColoredTeams.Red:
|
||||
return red;
|
||||
case ColoredTeams.Teal:
|
||||
return teal;
|
||||
case ColoredTeams.Purple:
|
||||
return purple;
|
||||
case ColoredTeams.Yellow:
|
||||
return yellow;
|
||||
case ColoredTeams.Orange:
|
||||
return orange;
|
||||
case ColoredTeams.Green:
|
||||
return green;
|
||||
case ColoredTeams.Bot:
|
||||
return botColor;
|
||||
default:
|
||||
return humanColors[simpleHash(team) % humanColors.length];
|
||||
}
|
||||
return this.teamColorAllocator.assignTeamColor(team);
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
@@ -73,12 +50,12 @@ export class PastelThemeDark implements Theme {
|
||||
return this.teamColor(team);
|
||||
}
|
||||
if (player.type() === PlayerType.Human) {
|
||||
return humanColors[simpleHash(player.id()) % humanColors.length];
|
||||
return this.humanColorAllocator.assignColor(player.id());
|
||||
}
|
||||
if (player.type() === PlayerType.Bot) {
|
||||
return botColors[simpleHash(player.id()) % botColors.length];
|
||||
return this.botColorAllocator.assignColor(player.id());
|
||||
}
|
||||
return territoryColors[simpleHash(player.id()) % territoryColors.length];
|
||||
return this.nationColorAllocator.assignColor(player.id());
|
||||
}
|
||||
|
||||
textColor(player: PlayerView): string {
|
||||
|
||||
@@ -122,31 +122,15 @@ export class AttackExecution implements Execution {
|
||||
// Record stats
|
||||
this.mg.stats().attack(this._owner, this.target, this.startTroops);
|
||||
|
||||
for (const incoming of this._owner.incomingAttacks()) {
|
||||
if (incoming.attacker() === this.target) {
|
||||
// Target has opposing attack, cancel them out
|
||||
if (incoming.troops() > this.attack.troops()) {
|
||||
incoming.setTroops(incoming.troops() - this.attack.troops());
|
||||
this.attack.delete();
|
||||
this.active = false;
|
||||
return;
|
||||
} else {
|
||||
this.attack.setTroops(this.attack.troops() - incoming.troops());
|
||||
incoming.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const outgoing of this._owner.outgoingAttacks()) {
|
||||
if (
|
||||
outgoing !== this.attack &&
|
||||
outgoing.target() === this.attack.target() &&
|
||||
outgoing.sourceTile() === this.attack.sourceTile()
|
||||
// Boat attacks (sourceTile is not null) are not combined with other attacks
|
||||
this.attack.sourceTile() === null
|
||||
) {
|
||||
// Existing attack on same target, add troops
|
||||
outgoing.setTroops(outgoing.troops() + this.attack.troops());
|
||||
this.active = false;
|
||||
this.attack.delete();
|
||||
return;
|
||||
this.attack.setTroops(this.attack.troops() + outgoing.troops());
|
||||
outgoing.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,11 +58,8 @@ export class BotExecution implements Execution {
|
||||
if (this.behavior === null) {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
const traitors = this.bot
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
|
||||
if (traitors.length > 0) {
|
||||
const toAttack = this.random.randElement(traitors);
|
||||
const toAttack = this.behavior.getNeighborTraitorToAttack();
|
||||
if (toAttack !== null) {
|
||||
const odds = this.bot.isFriendly(toAttack) ? 6 : 3;
|
||||
if (this.random.chance(odds)) {
|
||||
this.behavior.sendAttack(toAttack);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
import { TargetPlayerExecution } from "./TargetPlayerExecution";
|
||||
import { TransportShipExecution } from "./TransportShipExecution";
|
||||
import { UpgradeStructureExecution } from "./UpgradeStructureExecution";
|
||||
|
||||
export class Executor {
|
||||
// private random = new PseudoRandom(999)
|
||||
@@ -114,6 +115,8 @@ export class Executor {
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.unit,
|
||||
);
|
||||
case "upgrade_structure":
|
||||
return new UpgradeStructureExecution(player, intent.unitId);
|
||||
case "quick_chat":
|
||||
return new QuickChatExecution(
|
||||
player,
|
||||
|
||||
@@ -34,10 +34,20 @@ export class MissileSiloExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
const cooldown = this.silo.ticksLeftInCooldown();
|
||||
const frontTime = this.silo.ticksLeftInCooldown();
|
||||
if (frontTime === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cooldown =
|
||||
this.mg.config().SiloCooldown() - (this.mg.ticks() - frontTime);
|
||||
if (typeof cooldown === "number" && cooldown >= 0) {
|
||||
this.silo.touch();
|
||||
}
|
||||
|
||||
if (cooldown <= 0) {
|
||||
this.silo.reloadMissile();
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
|
||||
@@ -155,11 +155,6 @@ export class SAMLauncherExecution implements Execution {
|
||||
target = this.getSingleTarget();
|
||||
}
|
||||
|
||||
const cooldown = this.sam.ticksLeftInCooldown();
|
||||
if (typeof cooldown === "number" && cooldown >= 0) {
|
||||
this.sam.touch();
|
||||
}
|
||||
|
||||
const isSingleTarget = target && !target.targetedBySAM();
|
||||
if (
|
||||
(isSingleTarget || mirvWarheadTargets.length > 0) &&
|
||||
@@ -204,6 +199,21 @@ export class SAMLauncherExecution implements Execution {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frontTime = this.sam.ticksLeftInCooldown();
|
||||
if (frontTime === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cooldown =
|
||||
this.mg.config().SAMCooldown() - (this.mg.ticks() - frontTime);
|
||||
if (typeof cooldown === "number" && cooldown >= 0) {
|
||||
this.sam.touch();
|
||||
}
|
||||
|
||||
if (cooldown <= 0) {
|
||||
this.sam.reloadMissile();
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Execution, Game, Player, Unit } from "../game/Game";
|
||||
|
||||
export class UpgradeStructureExecution implements Execution {
|
||||
private structure: Unit | undefined;
|
||||
private cost: bigint;
|
||||
|
||||
constructor(
|
||||
private player: Player,
|
||||
private unitId: number,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.structure = this.player
|
||||
.units()
|
||||
.find((unit) => unit.id() === this.unitId);
|
||||
|
||||
if (this.structure === undefined) {
|
||||
console.warn(`structure is undefined`);
|
||||
return;
|
||||
}
|
||||
if (!this.structure.info().upgradable) {
|
||||
console.warn(`unit type ${this.structure} cannot be upgraded`);
|
||||
return;
|
||||
}
|
||||
this.cost = this.structure.info().cost(this.player);
|
||||
if (this.player.gold() < this.cost) {
|
||||
return;
|
||||
}
|
||||
this.player.upgradeUnit(this.structure);
|
||||
return;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
return;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -43,22 +43,48 @@ export class BotBehavior {
|
||||
this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji));
|
||||
}
|
||||
|
||||
private setNewEnemy(newEnemy: Player | null) {
|
||||
this.enemy = newEnemy;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
|
||||
private clearEnemy() {
|
||||
this.enemy = null;
|
||||
}
|
||||
|
||||
forgetOldEnemies() {
|
||||
// Forget old enemies
|
||||
if (this.game.ticks() - this.enemyUpdated > 100) {
|
||||
this.enemy = null;
|
||||
this.clearEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
private hasSufficientTroops(): boolean {
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const ratio = this.player.population() / maxPop;
|
||||
return ratio >= this.triggerRatio;
|
||||
}
|
||||
|
||||
private checkIncomingAttacks() {
|
||||
// Switch enemies if we're under attack
|
||||
const incomingAttacks = this.player.incomingAttacks();
|
||||
if (incomingAttacks.length > 0) {
|
||||
this.enemy = incomingAttacks
|
||||
.sort((a, b) => b.troops() - a.troops())[0]
|
||||
.attacker();
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
let largestAttack = 0;
|
||||
let largestAttacker: Player | undefined;
|
||||
for (const attack of incomingAttacks) {
|
||||
if (attack.troops() <= largestAttack) continue;
|
||||
largestAttack = attack.troops();
|
||||
largestAttacker = attack.attacker();
|
||||
}
|
||||
if (largestAttacker !== undefined) {
|
||||
this.setNewEnemy(largestAttacker);
|
||||
}
|
||||
}
|
||||
|
||||
getNeighborTraitorToAttack(): Player | null {
|
||||
const traitors = this.player
|
||||
.neighbors()
|
||||
.filter((n): n is Player => n.isPlayer() && n.isTraitor());
|
||||
return traitors.length > 0 ? this.random.randElement(traitors) : null;
|
||||
}
|
||||
|
||||
assistAllies() {
|
||||
@@ -79,8 +105,7 @@ export class BotBehavior {
|
||||
}
|
||||
// All checks passed, assist them
|
||||
this.player.updateRelation(ally, -20);
|
||||
this.enemy = target;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
this.setNewEnemy(target);
|
||||
this.emoji(ally, this.assistAcceptEmoji);
|
||||
break outer;
|
||||
}
|
||||
@@ -90,50 +115,57 @@ export class BotBehavior {
|
||||
selectEnemy(): Player | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const ratio = this.player.population() / maxPop;
|
||||
if (ratio < this.triggerRatio) return null;
|
||||
}
|
||||
if (!this.hasSufficientTroops()) return null;
|
||||
|
||||
// Prefer neighboring bots
|
||||
if (this.enemy === null) {
|
||||
// Prefer neighboring bots
|
||||
const bots = this.player
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.type() === PlayerType.Bot) as Player[];
|
||||
.filter(
|
||||
(n): n is Player => n.isPlayer() && n.type() === PlayerType.Bot,
|
||||
);
|
||||
if (bots.length > 0) {
|
||||
const density = (p: Player) => p.troops() / p.numTilesOwned();
|
||||
this.enemy = bots.sort((a, b) => density(a) - density(b))[0];
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
let lowestDensityBot: Player | undefined;
|
||||
let lowestDensity = Infinity;
|
||||
|
||||
for (const bot of bots) {
|
||||
const currentDensity = density(bot);
|
||||
if (currentDensity < lowestDensity) {
|
||||
lowestDensity = currentDensity;
|
||||
lowestDensityBot = bot;
|
||||
}
|
||||
}
|
||||
|
||||
if (lowestDensityBot !== undefined) {
|
||||
this.setNewEnemy(lowestDensityBot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
|
||||
// Select the most hated player
|
||||
if (this.enemy === null) {
|
||||
const mostHated = this.player.allRelationsSorted()[0];
|
||||
if (mostHated !== undefined && mostHated.relation === Relation.Hostile) {
|
||||
this.enemy = mostHated.player;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
// Select the most hated player
|
||||
if (this.enemy === null) {
|
||||
const mostHated = this.player.allRelationsSorted()[0];
|
||||
if (
|
||||
mostHated !== undefined &&
|
||||
mostHated.relation === Relation.Hostile
|
||||
) {
|
||||
this.setNewEnemy(mostHated.player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check, don't attack our allies or teammates
|
||||
if (this.enemy && this.player.isFriendly(this.enemy)) {
|
||||
this.enemy = null;
|
||||
}
|
||||
return this.enemy;
|
||||
return this.enemySanityCheck();
|
||||
}
|
||||
|
||||
selectRandomEnemy(): Player | TerraNullius | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
const maxPop = this.game.config().maxPopulation(this.player);
|
||||
const ratio = this.player.population() / maxPop;
|
||||
if (ratio < this.triggerRatio) return null;
|
||||
if (!this.hasSufficientTroops()) return null;
|
||||
|
||||
// Choose a new enemy randomly
|
||||
const neighbors = this.player.neighbors();
|
||||
@@ -145,34 +177,32 @@ export class BotBehavior {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.enemy = neighbor;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
this.setNewEnemy(neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
|
||||
// Select a traitor as an enemy
|
||||
if (this.enemy === null) {
|
||||
const traitors = this.player
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
|
||||
if (traitors.length > 0) {
|
||||
const toAttack = this.random.randElement(traitors);
|
||||
const odds = this.player.isFriendly(toAttack) ? 6 : 3;
|
||||
if (this.random.chance(odds)) {
|
||||
this.enemy = toAttack;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
// Select a traitor as an enemy
|
||||
if (this.enemy === null) {
|
||||
const toAttack = this.getNeighborTraitorToAttack();
|
||||
if (toAttack !== null) {
|
||||
if (!this.player.isFriendly(toAttack) && this.random.chance(3)) {
|
||||
this.setNewEnemy(toAttack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check, don't attack our allies or teammates
|
||||
return this.enemySanityCheck();
|
||||
}
|
||||
|
||||
private enemySanityCheck(): Player | null {
|
||||
if (this.enemy && this.player.isFriendly(this.enemy)) {
|
||||
this.enemy = null;
|
||||
this.clearEnemy();
|
||||
}
|
||||
return this.enemy;
|
||||
}
|
||||
@@ -200,12 +230,17 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
function shouldAcceptAllianceRequest(player: Player, request: AllianceRequest) {
|
||||
const isTraitor = request.requestor().isTraitor();
|
||||
const hasMalice = player.relation(request.requestor()) < Relation.Neutral;
|
||||
const requestorIsMuchLarger =
|
||||
request.requestor().numTilesOwned() > player.numTilesOwned() * 3;
|
||||
const tooManyAlliances = request.requestor().alliances().length >= 3;
|
||||
return (
|
||||
!isTraitor && !hasMalice && (requestorIsMuchLarger || !tooManyAlliances)
|
||||
);
|
||||
if (player.relation(request.requestor()) < Relation.Neutral) {
|
||||
return false; // Reject if hasMalice
|
||||
}
|
||||
if (request.requestor().isTraitor()) {
|
||||
return false; // Reject if isTraitor
|
||||
}
|
||||
if (request.requestor().numTilesOwned() > player.numTilesOwned() * 3) {
|
||||
return true; // Accept if requestorIsMuchLarger
|
||||
}
|
||||
if (request.requestor().alliances().length >= 3) {
|
||||
return false; // Reject if tooManyAlliances
|
||||
}
|
||||
return true; // Accept otherwise
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ export interface UnitInfo {
|
||||
maxHealth?: number;
|
||||
damage?: number;
|
||||
constructionDuration?: number;
|
||||
upgradable?: boolean;
|
||||
}
|
||||
|
||||
export enum UnitType {
|
||||
@@ -385,8 +386,9 @@ export interface Unit {
|
||||
|
||||
// SAMs & Missile Silos
|
||||
launch(): void;
|
||||
ticksLeftInCooldown(): Tick | undefined;
|
||||
reloadMissile(): void;
|
||||
isInCooldown(): boolean;
|
||||
ticksLeftInCooldown(): Tick | undefined;
|
||||
|
||||
// Trade Ships
|
||||
setSafeFromPirates(): void; // Only for trade ships
|
||||
@@ -396,6 +398,10 @@ export interface Unit {
|
||||
constructionType(): UnitType | null;
|
||||
setConstructionType(type: UnitType): void;
|
||||
|
||||
// Upgradable Structures
|
||||
level(): number;
|
||||
increaseLevel(): void;
|
||||
|
||||
// Warships
|
||||
setPatrolTile(tile: TileRef): void;
|
||||
patrolTile(): TileRef | undefined;
|
||||
@@ -471,6 +477,7 @@ export interface Player {
|
||||
spawnTile: TileRef,
|
||||
params: UnitParams<T>,
|
||||
): Unit;
|
||||
upgradeUnit(unit: Unit): void;
|
||||
|
||||
captureUnit(unit: Unit): void;
|
||||
|
||||
|
||||
@@ -80,7 +80,9 @@ export interface UnitUpdate {
|
||||
targetTile?: TileRef; // Only for nukes
|
||||
health?: number;
|
||||
constructionType?: UnitType;
|
||||
ticksLeftInCooldown?: Tick;
|
||||
missileTimerQueue: number[];
|
||||
readyMissileCount: number;
|
||||
level: number;
|
||||
}
|
||||
|
||||
export interface AttackUpdate {
|
||||
|
||||
@@ -112,11 +112,13 @@ export class UnitView {
|
||||
return this.data.targetTile;
|
||||
}
|
||||
ticksLeftInCooldown(): Tick | undefined {
|
||||
return this.data.ticksLeftInCooldown;
|
||||
return this.data.missileTimerQueue?.[0];
|
||||
}
|
||||
isCooldown(): boolean {
|
||||
if (this.data.ticksLeftInCooldown === undefined) return false;
|
||||
return this.data.ticksLeftInCooldown > 0;
|
||||
isInCooldown(): boolean {
|
||||
return this.data.readyMissileCount === 0;
|
||||
}
|
||||
level(): number {
|
||||
return this.data.level;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ export class PlayerImpl implements Player {
|
||||
troops: a.troops(),
|
||||
id: a.id(),
|
||||
retreating: a.retreating(),
|
||||
} as AttackUpdate;
|
||||
} satisfies AttackUpdate;
|
||||
}),
|
||||
incomingAttacks: this._incomingAttacks.map((a) => {
|
||||
return {
|
||||
@@ -166,7 +166,7 @@ export class PlayerImpl implements Player {
|
||||
troops: a.troops(),
|
||||
id: a.id(),
|
||||
retreating: a.retreating(),
|
||||
} as AttackUpdate;
|
||||
} satisfies AttackUpdate;
|
||||
}),
|
||||
outgoingAllianceRequests: outgoingAllianceRequests,
|
||||
hasSpawned: this.hasSpawned(),
|
||||
@@ -252,7 +252,9 @@ export class PlayerImpl implements Player {
|
||||
if (this.mg.map().isLand(neighbor)) {
|
||||
const owner = this.mg.map().ownerID(neighbor);
|
||||
if (owner !== this.smallID()) {
|
||||
ns.add(this.mg.playerBySmallID(owner) as Player | TerraNullius);
|
||||
ns.add(
|
||||
this.mg.playerBySmallID(owner) satisfies Player | TerraNullius,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,7 +396,7 @@ export class PlayerImpl implements Player {
|
||||
if (this.isAlliedWith(recipient)) {
|
||||
throw new Error(`cannot create alliance request, already allies`);
|
||||
}
|
||||
return this.mg.createAllianceRequest(this, recipient as Player);
|
||||
return this.mg.createAllianceRequest(this, recipient satisfies Player);
|
||||
}
|
||||
|
||||
relation(other: Player): Relation {
|
||||
@@ -481,7 +483,7 @@ export class PlayerImpl implements Player {
|
||||
.map((a) => a.other(this))
|
||||
.flatMap((ally) => ally.targets());
|
||||
ts.push(...this.targets());
|
||||
return [...new Set(ts)] as Player[];
|
||||
return [...new Set(ts)] satisfies Player[];
|
||||
}
|
||||
|
||||
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void {
|
||||
@@ -752,6 +754,12 @@ export class PlayerImpl implements Player {
|
||||
return b;
|
||||
}
|
||||
|
||||
upgradeUnit(unit: Unit) {
|
||||
const cost = this.mg.unitInfo(unit.type()).cost(this);
|
||||
this.removeGold(cost);
|
||||
unit.increaseLevel();
|
||||
}
|
||||
|
||||
public buildableUnits(tile: TileRef): BuildableUnit[] {
|
||||
const validTiles = this.validStructureSpawnTiles(tile);
|
||||
return Object.values(UnitType).map((u) => {
|
||||
@@ -1007,8 +1015,8 @@ export class PlayerImpl implements Player {
|
||||
if (this.mg.owner(tile) === this) {
|
||||
return false;
|
||||
}
|
||||
if (this.mg.hasOwner(tile)) {
|
||||
const other = this.mg.owner(tile) as Player;
|
||||
const other = this.mg.owner(tile);
|
||||
if (other.isPlayer()) {
|
||||
if (this.isFriendly(other)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1018,7 +1026,7 @@ export class PlayerImpl implements Player {
|
||||
return false;
|
||||
}
|
||||
if (this.mg.hasOwner(tile)) {
|
||||
return this.sharesBorderWith(this.mg.owner(tile));
|
||||
return this.sharesBorderWith(other);
|
||||
} else {
|
||||
for (const t of this.mg.bfs(
|
||||
tile,
|
||||
|
||||
@@ -85,6 +85,9 @@ export interface Stats {
|
||||
// Player captures a unit of type
|
||||
unitCapture(player: Player, type: OtherUnitType): void;
|
||||
|
||||
// Player upgrades a unit of type
|
||||
unitUpgrade(player: Player, type: OtherUnitType): void;
|
||||
|
||||
// Player destroys a unit of type
|
||||
unitDestroy(player: Player, type: OtherUnitType): void;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
OTHER_INDEX_CAPTURE,
|
||||
OTHER_INDEX_DESTROY,
|
||||
OTHER_INDEX_LOST,
|
||||
OTHER_INDEX_UPGRADE,
|
||||
OtherUnitType,
|
||||
PlayerStats,
|
||||
unitTypeToBombUnit,
|
||||
@@ -234,6 +235,10 @@ export class StatsImpl implements Stats {
|
||||
this._addOtherUnit(player, type, OTHER_INDEX_CAPTURE, 1);
|
||||
}
|
||||
|
||||
unitUpgrade(player: Player, type: OtherUnitType): void {
|
||||
this._addOtherUnit(player, type, OTHER_INDEX_UPGRADE, 1);
|
||||
}
|
||||
|
||||
unitDestroy(player: Player, type: OtherUnitType): void {
|
||||
this._addOtherUnit(player, type, OTHER_INDEX_DESTROY, 1);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ export class UnitImpl implements Unit {
|
||||
private _constructionType: UnitType | undefined;
|
||||
private _lastOwner: PlayerImpl | null = null;
|
||||
private _troops: number;
|
||||
private _cooldownStartTick: Tick | null = null;
|
||||
private _missileTimerQueue: number[] = [];
|
||||
private _readyMissileCount: number = 1;
|
||||
private _patrolTile: TileRef | undefined;
|
||||
private _level: number = 1;
|
||||
constructor(
|
||||
private _type: UnitType,
|
||||
private mg: GameImpl,
|
||||
@@ -104,7 +106,9 @@ export class UnitImpl implements Unit {
|
||||
constructionType: this._constructionType,
|
||||
targetUnitId: this._targetUnit?.id() ?? undefined,
|
||||
targetTile: this.targetTile() ?? undefined,
|
||||
ticksLeftInCooldown: this.ticksLeftInCooldown() ?? undefined,
|
||||
missileTimerQueue: this._missileTimerQueue,
|
||||
readyMissileCount: this._readyMissileCount,
|
||||
level: this.level(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,30 +271,23 @@ export class UnitImpl implements Unit {
|
||||
}
|
||||
|
||||
launch(): void {
|
||||
this._cooldownStartTick = this.mg.ticks();
|
||||
this._missileTimerQueue.push(this.mg.ticks());
|
||||
this._readyMissileCount--;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
|
||||
ticksLeftInCooldown(): Tick | undefined {
|
||||
let cooldownDuration = 0;
|
||||
if (this.type() === UnitType.SAMLauncher) {
|
||||
cooldownDuration = this.mg.config().SAMCooldown();
|
||||
} else if (this.type() === UnitType.MissileSilo) {
|
||||
cooldownDuration = this.mg.config().SiloCooldown();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this._cooldownStartTick) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cooldownDuration - (this.mg.ticks() - this._cooldownStartTick);
|
||||
return this._missileTimerQueue[0];
|
||||
}
|
||||
|
||||
isInCooldown(): boolean {
|
||||
const ticksLeft = this.ticksLeftInCooldown();
|
||||
return ticksLeft !== undefined && ticksLeft > 0;
|
||||
return this._readyMissileCount === 0;
|
||||
}
|
||||
|
||||
reloadMissile(): void {
|
||||
this._missileTimerQueue.shift();
|
||||
this._readyMissileCount++;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
|
||||
setTargetTile(targetTile: TileRef | undefined) {
|
||||
@@ -335,4 +332,16 @@ export class UnitImpl implements Unit {
|
||||
this.mg.config().safeFromPiratesCooldownMax()
|
||||
);
|
||||
}
|
||||
|
||||
level(): number {
|
||||
return this._level;
|
||||
}
|
||||
|
||||
increaseLevel(): void {
|
||||
this._level++;
|
||||
if ([UnitType.MissileSilo, UnitType.SAMLauncher].includes(this.type())) {
|
||||
this._readyMissileCount++;
|
||||
}
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ export class GameServer {
|
||||
|
||||
this.allClients.set(client.clientID, client);
|
||||
|
||||
client.ws.removeAllListeners("message");
|
||||
client.ws.on(
|
||||
"message",
|
||||
gatekeeper.wsHandler(client.ip, async (message: string) => {
|
||||
@@ -239,6 +240,7 @@ export class GameServer {
|
||||
}
|
||||
}),
|
||||
);
|
||||
client.ws.removeAllListeners("close");
|
||||
client.ws.on("close", () => {
|
||||
this.log.info("client disconnected", {
|
||||
clientID: client.clientID,
|
||||
@@ -248,6 +250,7 @@ export class GameServer {
|
||||
(c) => c.clientID !== client.clientID,
|
||||
);
|
||||
});
|
||||
client.ws.removeAllListeners("error");
|
||||
client.ws.on("error", (error: Error) => {
|
||||
if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") {
|
||||
client.ws.close(1002);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { colord, Colord } from "colord";
|
||||
import {
|
||||
blue,
|
||||
botColor,
|
||||
ColorAllocator,
|
||||
red,
|
||||
teal,
|
||||
} from "../src/core/configuration/Colors";
|
||||
import { ColoredTeams } from "../src/core/game/Game";
|
||||
|
||||
const mockColors: Colord[] = [
|
||||
colord({ r: 255, g: 0, b: 0 }),
|
||||
colord({ r: 0, g: 255, b: 0 }),
|
||||
colord({ r: 0, g: 0, b: 255 }),
|
||||
];
|
||||
|
||||
const fallbackMockColors: Colord[] = [
|
||||
colord({ r: 0, g: 0, b: 0 }),
|
||||
colord({ r: 255, g: 255, b: 255 }),
|
||||
];
|
||||
|
||||
describe("ColorAllocator", () => {
|
||||
let allocator: ColorAllocator;
|
||||
|
||||
beforeEach(() => {
|
||||
allocator = new ColorAllocator(mockColors, fallbackMockColors);
|
||||
});
|
||||
|
||||
test("returns a unique color for each new ID", () => {
|
||||
const c1 = allocator.assignColor("a");
|
||||
const c2 = allocator.assignColor("b");
|
||||
const c3 = allocator.assignColor("c");
|
||||
|
||||
expect(c1.isEqual(c2)).toBe(false);
|
||||
expect(c1.isEqual(c3)).toBe(false);
|
||||
expect(c2.isEqual(c3)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns the same color for the same ID", () => {
|
||||
const c1 = allocator.assignColor("a");
|
||||
const c2 = allocator.assignColor("a");
|
||||
|
||||
expect(c1.isEqual(c2)).toBe(true);
|
||||
});
|
||||
|
||||
test("falls back when colors are exhausted", () => {
|
||||
allocator.assignColor("1");
|
||||
allocator.assignColor("2");
|
||||
allocator.assignColor("3");
|
||||
const fallback = allocator.assignColor("4");
|
||||
const fallback2 = allocator.assignColor("5");
|
||||
|
||||
const match = fallbackMockColors.some((color) => color.isEqual(fallback));
|
||||
expect(match).toBe(true);
|
||||
|
||||
const match2 = fallback.isEqual(fallback2);
|
||||
expect(match2).toBe(false);
|
||||
});
|
||||
|
||||
test("assignBotColor returns deterministic color from botColors", () => {
|
||||
const allocator = new ColorAllocator(mockColors, mockColors);
|
||||
|
||||
const id1 = "bot123";
|
||||
const id2 = "bot456";
|
||||
|
||||
const c1 = allocator.assignColor(id1);
|
||||
const c2 = allocator.assignColor(id2);
|
||||
const c1Again = allocator.assignColor(id1);
|
||||
const c2Again = allocator.assignColor(id2);
|
||||
|
||||
expect(c1.isEqual(c1Again)).toBe(true);
|
||||
expect(c2.isEqual(c2Again)).toBe(true);
|
||||
});
|
||||
|
||||
test("assignTeamColor returns the expected static color for known teams", () => {
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Blue)).toEqual(blue);
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Red)).toEqual(red);
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Teal)).toEqual(teal);
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Bot)).toEqual(botColor);
|
||||
});
|
||||
});
|
||||
@@ -15,8 +15,8 @@ describe("getMessageTypeClasses", () => {
|
||||
|
||||
it("should return a valid CSS class for every MessageType", () => {
|
||||
const messageTypes = Object.values(MessageType).filter(
|
||||
(value) => typeof value === "number",
|
||||
) as MessageType[];
|
||||
(value): value is MessageType => typeof value === "number",
|
||||
);
|
||||
|
||||
messageTypes.forEach((messageType) => {
|
||||
const result = getMessageTypeClasses(messageType);
|
||||
@@ -30,8 +30,8 @@ describe("getMessageTypeClasses", () => {
|
||||
|
||||
it("should not trigger console.warn for any MessageType", () => {
|
||||
const messageTypes = Object.values(MessageType).filter(
|
||||
(value) => typeof value === "number",
|
||||
) as MessageType[];
|
||||
(value): value is MessageType => typeof value === "number",
|
||||
);
|
||||
|
||||
messageTypes.forEach((messageType) => {
|
||||
getMessageTypeClasses(messageType);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NukeExecution } from "../src/core/execution/NukeExecution";
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import { UpgradeStructureExecution } from "../src/core/execution/UpgradeStructureExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from "../src/core/game/Game";
|
||||
import { TileRef } from "../src/core/game/GameMap";
|
||||
import { setup } from "./util/Setup";
|
||||
import { constructionExecution } from "./util/utils";
|
||||
import { constructionExecution, executeTicks } from "./util/utils";
|
||||
|
||||
let game: Game;
|
||||
let attacker: Player;
|
||||
@@ -85,7 +86,21 @@ describe("MissileSilo", () => {
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
game.executeNextTick();
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(attacker.units(UnitType.MissileSilo)[0].isInCooldown()).toBeFalsy();
|
||||
});
|
||||
|
||||
test("missilesilo should have increased level after upgrade", async () => {
|
||||
expect(attacker.units(UnitType.MissileSilo)[0].level()).toEqual(1);
|
||||
|
||||
const upgradeStructureExecution = new UpgradeStructureExecution(
|
||||
attacker,
|
||||
attacker.units(UnitType.MissileSilo)[0].id(),
|
||||
);
|
||||
game.addExecution(upgradeStructureExecution);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(attacker.units(UnitType.MissileSilo)[0].level()).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NukeExecution } from "../src/core/execution/NukeExecution";
|
||||
import { SAMLauncherExecution } from "../src/core/execution/SAMLauncherExecution";
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import { UpgradeStructureExecution } from "../src/core/execution/UpgradeStructureExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
@@ -94,6 +95,7 @@ describe("SAM", () => {
|
||||
|
||||
test("sam should cooldown as long as configured", async () => {
|
||||
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
|
||||
|
||||
game.addExecution(new SAMLauncherExecution(defender, null, sam));
|
||||
expect(sam.isInCooldown()).toBeFalsy();
|
||||
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
|
||||
@@ -103,6 +105,7 @@ describe("SAM", () => {
|
||||
executeTicks(game, 3);
|
||||
|
||||
expect(nuke.isActive()).toBeFalsy();
|
||||
|
||||
for (let i = 0; i < game.config().SAMCooldown() - 3; i++) {
|
||||
game.executeNextTick();
|
||||
expect(sam.isInCooldown()).toBeTruthy();
|
||||
@@ -161,4 +164,18 @@ describe("SAM", () => {
|
||||
expect(sam1.isInCooldown()).toBeFalsy();
|
||||
expect(sam2.isInCooldown()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("SAM should have increased level after upgrade", async () => {
|
||||
defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].level()).toEqual(1);
|
||||
|
||||
const upgradeStructureExecution = new UpgradeStructureExecution(
|
||||
defender,
|
||||
defender.units(UnitType.SAMLauncher)[0].id(),
|
||||
);
|
||||
game.addExecution(upgradeStructureExecution);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].level()).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { ProgressBar } from "../../../src/client/graphics/ProgressBar";
|
||||
|
||||
describe("ProgressBar", () => {
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
let canvas: HTMLCanvasElement;
|
||||
|
||||
beforeEach(() => {
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.width = 100;
|
||||
canvas.height = 20;
|
||||
ctx = canvas.getContext("2d")!;
|
||||
});
|
||||
|
||||
it("should initialize and draw the background", () => {
|
||||
const spyClearRect = jest.spyOn(ctx, "clearRect");
|
||||
const spyFillRect = jest.spyOn(ctx, "fillRect");
|
||||
const spyFillStyle = jest.spyOn(ctx, "fillStyle", "set");
|
||||
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 0.5);
|
||||
expect(spyClearRect).toHaveBeenCalledWith(0, 0, 82, 12);
|
||||
expect(spyFillRect).toHaveBeenCalledWith(1, 1, 80, 10);
|
||||
expect(spyFillStyle).toHaveBeenCalledWith("#00ff00");
|
||||
expect(bar.getX()).toBe(2);
|
||||
expect(bar.getY()).toBe(2);
|
||||
});
|
||||
|
||||
it("should set progress and draw the progress bar", () => {
|
||||
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10);
|
||||
const spyFillRect = jest.spyOn(ctx, "fillRect");
|
||||
bar.setProgress(0.5);
|
||||
expect(bar.getProgress()).toBe(0.5);
|
||||
expect(spyFillRect).toHaveBeenCalledWith(
|
||||
2,
|
||||
2,
|
||||
Math.floor(0.5 * (80 - 2)),
|
||||
8,
|
||||
);
|
||||
expect(ctx.fillStyle).toBe("#00ff00");
|
||||
|
||||
bar.setProgress(0.1);
|
||||
expect(ctx.fillStyle).toBe("#ff0000");
|
||||
});
|
||||
|
||||
it("should clamp progress between 0 and 1 on init", () => {
|
||||
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, -1);
|
||||
expect(bar.getProgress()).toBe(0);
|
||||
const bar2 = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 2);
|
||||
expect(bar2.getProgress()).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle empty colors array gracefully", () => {
|
||||
const bar = new ProgressBar([], ctx, 2, 2, 80, 10, 0.5);
|
||||
expect(() => bar.setProgress(0.5)).not.toThrow();
|
||||
expect(ctx.fillStyle).toBe("#808080");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { UILayer } from "../../../src/client/graphics/layers/UILayer";
|
||||
import { UnitSelectionEvent } from "../../../src/client/InputHandler";
|
||||
import { UnitView } from "../../../src/core/game/GameView";
|
||||
|
||||
describe("UILayer", () => {
|
||||
let game: any;
|
||||
let eventBus: any;
|
||||
let transformHandler: any;
|
||||
|
||||
beforeEach(() => {
|
||||
game = {
|
||||
width: () => 100,
|
||||
height: () => 100,
|
||||
config: () => ({
|
||||
theme: () => ({
|
||||
territoryColor: () => ({
|
||||
lighten: () => ({ alpha: () => ({ toRgbString: () => "#fff" }) }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
x: () => 10,
|
||||
y: () => 10,
|
||||
unitInfo: () => ({ maxHealth: 10, constructionDuration: 5 }),
|
||||
myPlayer: () => ({ id: () => 1 }),
|
||||
ticks: () => 1,
|
||||
updatesSinceLastTick: () => undefined,
|
||||
};
|
||||
eventBus = { on: jest.fn() };
|
||||
transformHandler = {};
|
||||
});
|
||||
|
||||
it("should initialize and redraw canvas", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
ui.redraw();
|
||||
expect(ui["canvas"].width).toBe(100);
|
||||
expect(ui["canvas"].height).toBe(100);
|
||||
expect(ui["context"]).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should handle unit selection event", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
ui.redraw();
|
||||
const unit = {
|
||||
type: () => "Warship",
|
||||
isActive: () => true,
|
||||
tile: () => ({}),
|
||||
owner: () => ({}),
|
||||
};
|
||||
const event = { isSelected: true, unit };
|
||||
ui.drawSelectionBox = jest.fn();
|
||||
ui["onUnitSelection"](event as UnitSelectionEvent);
|
||||
expect(ui.drawSelectionBox).toHaveBeenCalledWith(unit);
|
||||
});
|
||||
|
||||
it("should add and clear health bars", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
ui.redraw();
|
||||
const unit = {
|
||||
id: () => 1,
|
||||
type: () => "Warship",
|
||||
health: () => 5,
|
||||
tile: () => ({}),
|
||||
owner: () => ({}),
|
||||
isActive: () => true,
|
||||
} as unknown as UnitView;
|
||||
ui.drawHealthBar(unit);
|
||||
expect(ui["allHealthBars"].has(1)).toBe(true);
|
||||
|
||||
// a full hp unit doesnt have a health bar
|
||||
unit.health = () => 10;
|
||||
ui.drawHealthBar(unit);
|
||||
expect(ui["allHealthBars"].has(1)).toBe(false);
|
||||
|
||||
// a dead unit doesnt have a health bar
|
||||
unit.health = () => 5;
|
||||
ui.drawHealthBar(unit);
|
||||
expect(ui["allHealthBars"].has(1)).toBe(true);
|
||||
unit.health = () => 0;
|
||||
ui.drawHealthBar(unit);
|
||||
expect(ui["allHealthBars"].has(1)).toBe(false);
|
||||
});
|
||||
|
||||
it("should add loading bar for unit", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
ui.redraw();
|
||||
const unit = {
|
||||
id: () => 2,
|
||||
tile: () => ({}),
|
||||
isActive: () => true,
|
||||
} as unknown as UnitView;
|
||||
ui.drawLoadingBar(unit, 5);
|
||||
expect(ui["allProgressBars"].has(2)).toBe(true);
|
||||
});
|
||||
|
||||
it("should remove loading bar for inactive unit", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
ui.redraw();
|
||||
const unit = {
|
||||
id: () => 2,
|
||||
type: () => "Construction",
|
||||
constructionType: () => "City",
|
||||
owner: () => ({ id: () => 1 }),
|
||||
tile: () => ({}),
|
||||
isActive: () => true,
|
||||
} as unknown as UnitView;
|
||||
ui.onUnitEvent(unit);
|
||||
expect(ui["allProgressBars"].has(2)).toBe(true);
|
||||
|
||||
// an inactive unit should not have a loading bar
|
||||
unit.isActive = () => false;
|
||||
ui.tick();
|
||||
expect(ui["allProgressBars"].has(2)).toBe(false);
|
||||
});
|
||||
|
||||
it("should remove loading bar for a finished progress bar", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
ui.redraw();
|
||||
const unit = {
|
||||
id: () => 2,
|
||||
type: () => "Construction",
|
||||
constructionType: () => "City",
|
||||
owner: () => ({ id: () => 1 }),
|
||||
tile: () => ({}),
|
||||
isActive: () => true,
|
||||
} as unknown as UnitView;
|
||||
ui.onUnitEvent(unit);
|
||||
expect(ui["allProgressBars"].has(2)).toBe(true);
|
||||
|
||||
game.ticks = () => 6; // simulate enough ticks for completion
|
||||
ui.tick();
|
||||
expect(ui["allProgressBars"].has(2)).toBe(false);
|
||||
});
|
||||
});
|
||||