Merge branch 'main' into patterned-territory

This commit is contained in:
Aotumuri
2025-05-22 20:20:46 +09:00
committed by GitHub
12 changed files with 218 additions and 44 deletions
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 611 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

+1 -1
View File
@@ -134,7 +134,7 @@ export class PublicLobby extends LitElement {
</div>
</div>
<div class="flex flex-col items-center">
<div class="text-md font-medium text-blue-100 mb-2">
<div class="text-md font-medium text-blue-100 mb-4">
${lobby.numClients} / ${lobby.gameConfig.maxPlayers}
</div>
<div class="text-md font-medium text-blue-100">
+10
View File
@@ -20,6 +20,7 @@ import { NameLayer } from "./layers/NameLayer";
import { OptionsMenu } from "./layers/OptionsMenu";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { PlayerTeamLabel } from "./layers/PlayerTeamLabel";
import { RadialMenu } from "./layers/RadialMenu";
import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureLayer } from "./layers/StructureLayer";
@@ -162,6 +163,14 @@ export function createRenderer(
}
multiTabModal.game = game;
const playerTeamLabel = document.querySelector(
"player-team-label",
) as PlayerTeamLabel;
if (!(playerTeamLabel instanceof PlayerTeamLabel)) {
console.error("player team label not found");
}
playerTeamLabel.game = game;
const layers: Layer[] = [
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus),
@@ -193,6 +202,7 @@ export function createRenderer(
teamStats,
topBar,
playerPanel,
playerTeamLabel,
multiTabModal,
];
+1 -1
View File
@@ -167,7 +167,7 @@ export class OptionsMenu extends LitElement implements Layer {
children: this.isPaused ? "▶️" : "⏸",
})}
<div
class="w-15 h-8 lg:w-24 lg:h-10 flex items-center justify-center
class="w-15 h-8 lg:w-24 lg:h-10 flex items-center justify-center w-full
bg-opacity-50 bg-gray-700 text-opacity-90 text-white
rounded text-sm lg:text-xl"
>
@@ -0,0 +1,74 @@
import { Colord } from "colord";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { GameMode } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
@customElement("player-team-label")
export class PlayerTeamLabel extends LitElement implements Layer {
public game: GameView;
@state()
private isTeamsGameMode: boolean = false;
private isVisible = false;
private playerTeam: string | null = null;
private playerColor: Colord = new Colord("#FFFFFF");
createRenderRoot() {
return this;
}
init() {
this.isTeamsGameMode =
this.game.config().gameConfig().gameMode === GameMode.Team;
if (this.isTeamsGameMode) {
this.isVisible = true;
this.requestUpdate();
}
}
tick() {
if (
this.isTeamsGameMode &&
!this.playerTeam &&
this.game.myPlayer()?.team()
) {
this.playerTeam = this.game.myPlayer()!.team();
if (this.playerTeam) {
this.playerColor = this.game
.config()
.theme()
.teamColor(this.playerTeam);
this.requestUpdate();
}
}
if (!this.game.inSpawnPhase() && this.isVisible) {
this.isVisible = false;
this.requestUpdate();
}
}
render() {
if (!this.isVisible) {
return html``;
}
return html`
<div
class="flex items-center w-full justify-evenly h-8 lg:h-10 top-0 lg:top-4 left-0 lg:left-4 bg-opacity-60 bg-gray-900 rounded-es-sm lg:rounded-lg backdrop-blur-md text-white py-1 lg:p-2"
@contextmenu=${(e) => e.preventDefault()}
>
Your team:
<span style="color: ${this.playerColor?.toRgbString()}"
>${this.playerTeam} &#10687;</span
>
</div>
`;
}
}
+3
View File
@@ -345,6 +345,9 @@
<options-menu></options-menu>
<player-info-overlay></player-info-overlay>
</div>
<div class="fixed left-[10px] top-[10px] z-50 w-36 lg:w-48 items-center">
<player-team-label></player-team-label>
</div>
<div
class="bottom-0 w-full flex-col-reverse sm:flex-row z-50"
+42 -39
View File
@@ -1,4 +1,3 @@
import { PriorityQueue } from "@datastructures-js/priority-queue";
import { renderNumber, renderTroops } from "../../client/Utils";
import {
Attack,
@@ -13,16 +12,14 @@ import {
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { PseudoRandom } from "../PseudoRandom";
import { FlatBinaryHeap } from "./utils/FlatBinaryHeap"; // adjust path if needed
const malusForRetreat = 25;
export class AttackExecution implements Execution {
private breakAlliance = false;
private active: boolean = true;
private toConquer: PriorityQueue<TileContainer> =
new PriorityQueue<TileContainer>((a: TileContainer, b: TileContainer) => {
return a.priority - b.priority;
});
private toConquer = new FlatBinaryHeap();
private random = new PseudoRandom(123);
private _owner: Player;
@@ -196,9 +193,12 @@ export class AttackExecution implements Execution {
if (this.attack === null) {
throw new Error("Attack not initialized");
}
let troopCount = this.attack.troops(); // cache troop count
const targetIsPlayer = this.target.isPlayer(); // cache target type
const targetPlayer = targetIsPlayer ? (this.target as Player) : null; // cache target player
if (this.attack.retreated()) {
if (this.attack.target().isPlayer()) {
if (targetIsPlayer) {
this.retreat(malusForRetreat);
} else {
this.retreat();
@@ -216,12 +216,14 @@ export class AttackExecution implements Execution {
return;
}
const alliance = this._owner.allianceWith(this.target as Player);
const alliance = targetPlayer
? this._owner.allianceWith(targetPlayer)
: null;
if (this.breakAlliance && alliance !== null) {
this.breakAlliance = false;
this._owner.breakAlliance(alliance);
}
if (this.target.isPlayer() && this._owner.isAlliedWith(this.target)) {
if (targetPlayer && this._owner.isAlliedWith(targetPlayer)) {
// In this case a new alliance was created AFTER the attack started.
this.retreat();
return;
@@ -230,14 +232,14 @@ export class AttackExecution implements Execution {
let numTilesPerTick = this.mg
.config()
.attackTilesPerTick(
this.attack.troops(),
troopCount,
this._owner,
this.target,
this.border.size + this.random.nextInt(0, 5),
);
while (numTilesPerTick > 0) {
if (this.attack.troops() < 1) {
if (troopCount < 1) {
this.attack.delete();
this.active = false;
return;
@@ -249,13 +251,16 @@ export class AttackExecution implements Execution {
return;
}
const tileToConquer = this.toConquer.dequeue().tile;
const [tileToConquer] = this.toConquer.dequeue();
this.border.delete(tileToConquer);
const onBorder =
this.mg
.neighbors(tileToConquer)
.filter((t) => this.mg.owner(t) === this._owner).length > 0;
let onBorder = false;
for (const n of this.mg.neighbors(tileToConquer)) {
if (this.mg.owner(n) === this._owner) {
onBorder = true;
break;
}
}
if (this.mg.owner(tileToConquer) !== this.target || !onBorder) {
continue;
}
@@ -264,15 +269,16 @@ export class AttackExecution implements Execution {
.config()
.attackLogic(
this.mg,
this.attack.troops(),
troopCount,
this._owner,
this.target,
tileToConquer,
);
numTilesPerTick -= tilesPerTickUsed;
this.attack.setTroops(this.attack.troops() - attackerTroopLoss);
if (this.target.isPlayer()) {
this.target.removeTroops(defenderTroopLoss);
troopCount -= attackerTroopLoss;
this.attack.setTroops(troopCount);
if (targetPlayer) {
targetPlayer.removeTroops(defenderTroopLoss);
}
this._owner.conquer(tileToConquer);
this.handleDeadDefender();
@@ -280,6 +286,8 @@ export class AttackExecution implements Execution {
}
private addNeighbors(tile: TileRef) {
const tickNow = this.mg.ticks(); // cache tick
for (const neighbor of this.mg.neighbors(tile)) {
if (
this.mg.isWater(neighbor) ||
@@ -288,11 +296,15 @@ export class AttackExecution implements Execution {
continue;
}
this.border.add(neighbor);
const numOwnedByMe = this.mg
.neighbors(neighbor)
.filter((t) => this.mg.owner(t) === this._owner).length;
let numOwnedByMe = 0;
for (const n of this.mg.neighbors(neighbor)) {
if (this.mg.owner(n) === this._owner) {
numOwnedByMe++;
}
}
let mag = 0;
switch (this.mg.terrainType(tile)) {
switch (this.mg.terrainType(neighbor)) {
case TerrainType.Plains:
mag = 1;
break;
@@ -303,14 +315,12 @@ export class AttackExecution implements Execution {
mag = 2;
break;
}
this.toConquer.enqueue(
new TileContainer(
neighbor,
(this.random.nextInt(0, 7) + 10) *
(1 - numOwnedByMe * 0.5 + mag / 2) +
this.mg.ticks(),
),
);
const priority =
(this.random.nextInt(0, 7) + 10) * (1 - numOwnedByMe * 0.5 + mag / 2) +
tickNow;
this.toConquer.enqueue(neighbor, priority);
}
}
@@ -356,10 +366,3 @@ export class AttackExecution implements Execution {
return this.active;
}
}
class TileContainer {
constructor(
public readonly tile: TileRef,
public readonly priority: number,
) {}
}
+1 -1
View File
@@ -43,7 +43,7 @@ export class QuickChatExecution implements Execution {
this.variables,
this.recipient.id(),
true,
this.recipient.name(),
this.sender.name(),
);
this.mg.displayChat(
@@ -0,0 +1,84 @@
import { TileRef } from "../../game/GameMap";
/**
* Lightweight min-heap specialised for (priority:number, tile:TileRef) pairs.
* - priorities stored in a contiguous Float32Array
* - tiles stored in a parallel object array
*/
export class FlatBinaryHeap {
/** parallel arrays: pri[ i ] is the priority of tiles[ i ] */
private pri: Float32Array;
private tiles: TileRef[];
private len = 0; // current number of elements
constructor(capacity = 1024) {
this.pri = new Float32Array(capacity);
this.tiles = new Array<TileRef>(capacity);
}
/** remove every element without reallocating */
clear(): void {
this.len = 0;
}
/** current heap size */
size(): number {
return this.len;
}
//insert tiles
enqueue(tile: TileRef, priority: number): void {
if (this.len === this.pri.length) this.grow(); // ensure space
let i = this.len++;
/* sift-up */
while (i > 0) {
const parent = (i - 1) >> 1;
if (priority >= this.pri[parent]) break;
this.pri[i] = this.pri[parent];
this.tiles[i] = this.tiles[parent];
i = parent;
}
this.pri[i] = priority;
this.tiles[i] = tile;
}
//remove tiles
dequeue(): [TileRef, number] {
if (this.len === 0) throw new Error("heap empty");
const topTile = this.tiles[0];
const topPri = this.pri[0];
const lastPri = this.pri[--this.len];
const lastTile = this.tiles[this.len];
/* sift-down */
let i = 0;
while (true) {
const left = (i << 1) + 1;
if (left >= this.len) break;
const right = left + 1;
const child =
right < this.len && this.pri[right] < this.pri[left] ? right : left;
if (lastPri <= this.pri[child]) break;
this.pri[i] = this.pri[child];
this.tiles[i] = this.tiles[child];
i = child;
}
this.pri[i] = lastPri;
this.tiles[i] = lastTile;
return [topTile, topPri];
}
/** double the underlying storage */
private grow(): void {
const newCap = this.pri.length << 1;
const newPri = new Float32Array(newCap);
newPri.set(this.pri);
this.pri = newPri;
this.tiles.length = newCap;
}
}