Skip to content

Commit 31e718a

Browse files
authored
Merge branch 'main' into nation-nuking-part-2
2 parents 6f0e9fe + 6d1e2f5 commit 31e718a

21 files changed

Lines changed: 405 additions & 39 deletions

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@
451451
<settings-modal></settings-modal>
452452
<player-panel></player-panel>
453453
<spawn-timer></spawn-timer>
454+
<immunity-timer></immunity-timer>
454455
<help-modal></help-modal>
455456
<game-info-modal></game-info-modal>
456457
<dark-mode-button></dark-mode-button>

resources/lang/debug.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"options_title": "host_modal.options_title",
146146
"bots": "host_modal.bots",
147147
"bots_disabled": "host_modal.bots_disabled",
148+
"player_immunity_duration": "host_modal.player_immunity_duration",
148149
"disable_nations": "host_modal.disable_nations",
149150
"instant_build": "host_modal.instant_build",
150151
"random_spawn": "host_modal.random_spawn",

resources/lang/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@
307307
"options_title": "Options",
308308
"bots": "Bots: ",
309309
"bots_disabled": "Disabled",
310+
"player_immunity_duration": "PVP immunity duration (minutes)",
310311
"nations": "Nations: ",
311312
"disable_nations": "Disable Nations",
312313
"max_timer": "Game length (minutes)",

src/client/HostLobbyModal.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export class HostLobbyModal extends LitElement {
4545
@state() private gameMode: GameMode = GameMode.FFA;
4646
@state() private teamCount: TeamCountConfig = 2;
4747
@state() private bots: number = 400;
48+
@state() private spawnImmunity: boolean = false;
49+
@state() private spawnImmunityDurationMinutes: number | undefined = undefined;
4850
@state() private infiniteGold: boolean = false;
4951
@state() private donateGold: boolean = false;
5052
@state() private infiniteTroops: boolean = false;
@@ -514,7 +516,7 @@ export class HostLobbyModal extends LitElement {
514516
id="end-timer-value"
515517
min="0"
516518
max="120"
517-
.value=${String(this.maxTimerValue ?? "")}
519+
.value=${String(this.maxTimerValue ?? 0)}
518520
style="width: 60px; color: black; text-align: right; border-radius: 8px;"
519521
@input=${this.handleMaxTimerValueChanges}
520522
@keydown=${this.handleMaxTimerValueKeyDown}
@@ -524,6 +526,47 @@ export class HostLobbyModal extends LitElement {
524526
${translateText("host_modal.max_timer")}
525527
</div>
526528
</label>
529+
530+
<label
531+
for="spawn-immunity"
532+
class="option-card ${this.spawnImmunity ? "selected" : ""}"
533+
>
534+
<div class="checkbox-icon"></div>
535+
<input
536+
type="checkbox"
537+
id="spawn-immunity"
538+
@change=${(e: Event) => {
539+
const checked = (e.target as HTMLInputElement).checked;
540+
if (!checked) {
541+
this.spawnImmunityDurationMinutes = undefined;
542+
}
543+
this.spawnImmunity = checked;
544+
this.putGameConfig();
545+
}}
546+
.checked=${this.spawnImmunity}
547+
/>
548+
${
549+
this.spawnImmunity === false
550+
? ""
551+
: html`<input
552+
type="number"
553+
id="spawn-immunity-duration"
554+
min="0"
555+
max="120"
556+
step="1"
557+
.value=${String(
558+
this.spawnImmunityDurationMinutes ?? 0,
559+
)}
560+
style="width: 60px; color: black; text-align: right; border-radius: 8px;"
561+
@input=${this.handleSpawnImmunityDurationInput}
562+
@keydown=${this.handleSpawnImmunityDurationKeyDown}
563+
/>`
564+
}
565+
<div class="option-card-title">
566+
<span>${translateText("host_modal.player_immunity_duration")}</span>
567+
</div>
568+
</label>
569+
527570
<hr style="width: 100%; border-top: 1px solid #444; margin: 16px 0;" />
528571
529572
<!-- Individual disables for structures/weapons -->
@@ -691,6 +734,23 @@ export class HostLobbyModal extends LitElement {
691734
this.putGameConfig();
692735
}
693736

737+
private handleSpawnImmunityDurationKeyDown(e: KeyboardEvent) {
738+
if (["-", "+", "e", "E"].includes(e.key)) {
739+
e.preventDefault();
740+
}
741+
}
742+
743+
private handleSpawnImmunityDurationInput(e: Event) {
744+
const input = e.target as HTMLInputElement;
745+
input.value = input.value.replace(/[eE+-]/g, "");
746+
const value = parseInt(input.value, 10);
747+
if (Number.isNaN(value) || value < 0 || value > 120) {
748+
return;
749+
}
750+
this.spawnImmunityDurationMinutes = value;
751+
this.putGameConfig();
752+
}
753+
694754
private handleRandomSpawnChange(e: Event) {
695755
this.randomSpawn = Boolean((e.target as HTMLInputElement).checked);
696756
this.putGameConfig();
@@ -757,6 +817,9 @@ export class HostLobbyModal extends LitElement {
757817
}
758818

759819
private async putGameConfig() {
820+
const spawnImmunityTicks = this.spawnImmunityDurationMinutes
821+
? this.spawnImmunityDurationMinutes * 60 * 10
822+
: 0;
760823
this.dispatchEvent(
761824
new CustomEvent("update-game-config", {
762825
detail: {
@@ -775,6 +838,9 @@ export class HostLobbyModal extends LitElement {
775838
randomSpawn: this.randomSpawn,
776839
gameMode: this.gameMode,
777840
disabledUnits: this.disabledUnits,
841+
spawnImmunityDuration: this.spawnImmunity
842+
? spawnImmunityTicks
843+
: undefined,
778844
playerTeams: this.teamCount,
779845
...(this.gameMode === GameMode.Team &&
780846
this.teamCount === HumansVsNations

src/client/LocalServer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ export class LocalServer {
132132
if (!this.lobbyConfig.gameRecord) {
133133
if (clientMsg.turnNumber % 100 === 0) {
134134
// In singleplayer, only store hash every 100 turns to reduce size of game record.
135-
this.turns[clientMsg.turnNumber].hash = clientMsg.hash;
135+
const turn = this.turns[clientMsg.turnNumber];
136+
if (turn) {
137+
turn.hash = clientMsg.hash;
138+
}
136139
}
137140
return;
138141
}

src/client/components/FluentSlider.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class FluentSlider extends LitElement {
8484
this.dispatchValueChange();
8585
}
8686

87-
private handleNumberChange(e: Event) {
87+
private handleNumberInput(e: Event) {
8888
const target = e.target as HTMLInputElement;
8989
let val = target.valueAsNumber;
9090
if (isNaN(val)) {
@@ -93,11 +93,19 @@ export class FluentSlider extends LitElement {
9393
if (val < this.min) val = this.min;
9494
if (val > this.max) val = this.max;
9595
this.value = val;
96+
// Don't dispatch value change on every input - only on blur/enter
97+
}
98+
99+
private handleNumberComplete() {
100+
// Dispatch the value change when editing is complete
96101
this.dispatchValueChange();
97102
}
98103

99104
private handleNumberKeyDown(e: KeyboardEvent) {
100-
if (e.key === "Enter") this.isEditing = false;
105+
if (e.key === "Enter") {
106+
this.isEditing = false;
107+
this.handleNumberComplete();
108+
}
101109
}
102110

103111
private enableEditing() {
@@ -125,8 +133,11 @@ export class FluentSlider extends LitElement {
125133
.min=${this.min}
126134
.max=${this.max}
127135
.valueAsNumber=${this.value}
128-
@input=${this.handleNumberChange}
129-
@blur=${() => (this.isEditing = false)}
136+
@input=${this.handleNumberInput}
137+
@blur=${() => {
138+
this.isEditing = false;
139+
this.handleNumberComplete();
140+
}}
130141
@keydown=${this.handleNumberKeyDown}
131142
/>`
132143
: html`<span

src/client/graphics/GameRenderer.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { FxLayer } from "./layers/FxLayer";
1818
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
1919
import { GameRightSidebar } from "./layers/GameRightSidebar";
2020
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
21+
import { ImmunityTimer } from "./layers/ImmunityTimer";
2122
import { Layer } from "./layers/Layer";
2223
import { Leaderboard } from "./layers/Leaderboard";
2324
import { MainRadialMenu } from "./layers/MainRadialMenu";
@@ -234,6 +235,14 @@ export function createRenderer(
234235
spawnTimer.game = game;
235236
spawnTimer.transformHandler = transformHandler;
236237

238+
const immunityTimer = document.querySelector(
239+
"immunity-timer",
240+
) as ImmunityTimer;
241+
if (!(immunityTimer instanceof ImmunityTimer)) {
242+
console.error("immunity timer not found");
243+
}
244+
immunityTimer.game = game;
245+
237246
// When updating these layers please be mindful of the order.
238247
// Try to group layers by the return value of shouldTransform.
239248
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
@@ -262,6 +271,7 @@ export function createRenderer(
262271
playerPanel,
263272
),
264273
spawnTimer,
274+
immunityTimer,
265275
leaderboard,
266276
gameLeftSidebar,
267277
unitDisplay,
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { LitElement, html } from "lit";
2+
import { customElement } from "lit/decorators.js";
3+
import { GameMode } from "../../../core/game/Game";
4+
import { GameView } from "../../../core/game/GameView";
5+
import { Layer } from "./Layer";
6+
7+
@customElement("immunity-timer")
8+
export class ImmunityTimer extends LitElement implements Layer {
9+
public game: GameView;
10+
11+
private isVisible = false;
12+
private isActive = false;
13+
private progressRatio = 0;
14+
15+
createRenderRoot() {
16+
this.style.position = "fixed";
17+
this.style.top = "0";
18+
this.style.left = "0";
19+
this.style.width = "100%";
20+
this.style.height = "7px";
21+
this.style.zIndex = "1000";
22+
this.style.pointerEvents = "none";
23+
return this;
24+
}
25+
26+
init() {
27+
this.isVisible = true;
28+
}
29+
30+
tick() {
31+
if (!this.game || !this.isVisible) {
32+
return;
33+
}
34+
35+
const showTeamOwnershipBar =
36+
this.game.config().gameConfig().gameMode === GameMode.Team &&
37+
!this.game.inSpawnPhase();
38+
39+
this.style.top = showTeamOwnershipBar ? "7px" : "0px";
40+
41+
const immunityDuration = this.game.config().spawnImmunityDuration();
42+
const spawnPhaseTurns = this.game.config().numSpawnPhaseTurns();
43+
44+
if (immunityDuration <= 5 * 10 || this.game.inSpawnPhase()) {
45+
this.setInactive();
46+
return;
47+
}
48+
49+
const immunityEnd = spawnPhaseTurns + immunityDuration;
50+
const ticks = this.game.ticks();
51+
52+
if (ticks >= immunityEnd || ticks < spawnPhaseTurns) {
53+
this.setInactive();
54+
return;
55+
}
56+
57+
const elapsedTicks = Math.max(0, ticks - spawnPhaseTurns);
58+
this.progressRatio = Math.min(
59+
1,
60+
Math.max(0, elapsedTicks / immunityDuration),
61+
);
62+
this.isActive = true;
63+
this.requestUpdate();
64+
}
65+
66+
private setInactive() {
67+
if (this.isActive) {
68+
this.isActive = false;
69+
this.requestUpdate();
70+
}
71+
}
72+
73+
shouldTransform(): boolean {
74+
return false;
75+
}
76+
77+
render() {
78+
if (!this.isVisible || !this.isActive) {
79+
return html``;
80+
}
81+
82+
const widthPercent = this.progressRatio * 100;
83+
84+
return html`
85+
<div class="w-full h-full flex z-[999]">
86+
<div
87+
class="h-full transition-all duration-100 ease-in-out"
88+
style="width: ${widthPercent}%; background-color: rgba(255, 165, 0, 0.9);"
89+
></div>
90+
</div>
91+
`;
92+
}
93+
}

src/core/Schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export const GameConfigSchema = z.object({
179179
randomSpawn: z.boolean(),
180180
maxPlayers: z.number().optional(),
181181
maxTimerValue: z.number().int().min(1).max(120).optional(),
182+
spawnImmunityDuration: z.number().int().min(0).optional(), // In ticks
182183
disabledUnits: z.enum(UnitType).array().optional(),
183184
playerTeams: TeamCountConfigSchema.optional(),
184185
});

src/core/configuration/DefaultConfig.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const numPlayersConfig = {
8787
[GameMapType.Lemnos]: [20, 15, 10],
8888
[GameMapType.TwoLakes]: [60, 50, 40],
8989
[GameMapType.StraitOfHormuz]: [40, 36, 30],
90-
[GameMapType.Surrounded]: [56, 28, 14], // 4, 2, 1 players per island
90+
[GameMapType.Surrounded]: [42, 28, 14], // 3, 2, 1 player(s) per island
9191
} as const satisfies Record<GameMapType, [number, number, number]>;
9292

9393
export abstract class DefaultServerConfig implements ServerConfig {
@@ -246,7 +246,7 @@ export class DefaultConfig implements Config {
246246
return 30 * 10; // 30 seconds
247247
}
248248
spawnImmunityDuration(): Tick {
249-
return 5 * 10;
249+
return this._gameConfig.spawnImmunityDuration ?? 5 * 10; // default to 5 seconds
250250
}
251251

252252
gameConfig(): GameConfig {

0 commit comments

Comments
 (0)