mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-28 12:34:16 +00:00
Merge branch 'main' into patterned-territory
This commit is contained in:
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 |
@@ -134,7 +134,7 @@ export class PublicLobby extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center">
|
<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}
|
${lobby.numClients} / ${lobby.gameConfig.maxPlayers}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-md font-medium text-blue-100">
|
<div class="text-md font-medium text-blue-100">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { NameLayer } from "./layers/NameLayer";
|
|||||||
import { OptionsMenu } from "./layers/OptionsMenu";
|
import { OptionsMenu } from "./layers/OptionsMenu";
|
||||||
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
||||||
import { PlayerPanel } from "./layers/PlayerPanel";
|
import { PlayerPanel } from "./layers/PlayerPanel";
|
||||||
|
import { PlayerTeamLabel } from "./layers/PlayerTeamLabel";
|
||||||
import { RadialMenu } from "./layers/RadialMenu";
|
import { RadialMenu } from "./layers/RadialMenu";
|
||||||
import { SpawnTimer } from "./layers/SpawnTimer";
|
import { SpawnTimer } from "./layers/SpawnTimer";
|
||||||
import { StructureLayer } from "./layers/StructureLayer";
|
import { StructureLayer } from "./layers/StructureLayer";
|
||||||
@@ -162,6 +163,14 @@ export function createRenderer(
|
|||||||
}
|
}
|
||||||
multiTabModal.game = game;
|
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[] = [
|
const layers: Layer[] = [
|
||||||
new TerrainLayer(game, transformHandler),
|
new TerrainLayer(game, transformHandler),
|
||||||
new TerritoryLayer(game, eventBus),
|
new TerritoryLayer(game, eventBus),
|
||||||
@@ -193,6 +202,7 @@ export function createRenderer(
|
|||||||
teamStats,
|
teamStats,
|
||||||
topBar,
|
topBar,
|
||||||
playerPanel,
|
playerPanel,
|
||||||
|
playerTeamLabel,
|
||||||
multiTabModal,
|
multiTabModal,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export class OptionsMenu extends LitElement implements Layer {
|
|||||||
children: this.isPaused ? "▶️" : "⏸",
|
children: this.isPaused ? "▶️" : "⏸",
|
||||||
})}
|
})}
|
||||||
<div
|
<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
|
bg-opacity-50 bg-gray-700 text-opacity-90 text-white
|
||||||
rounded text-sm lg:text-xl"
|
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} ⦿</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -345,6 +345,9 @@
|
|||||||
<options-menu></options-menu>
|
<options-menu></options-menu>
|
||||||
<player-info-overlay></player-info-overlay>
|
<player-info-overlay></player-info-overlay>
|
||||||
</div>
|
</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
|
<div
|
||||||
class="bottom-0 w-full flex-col-reverse sm:flex-row z-50"
|
class="bottom-0 w-full flex-col-reverse sm:flex-row z-50"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
|
||||||
import { renderNumber, renderTroops } from "../../client/Utils";
|
import { renderNumber, renderTroops } from "../../client/Utils";
|
||||||
import {
|
import {
|
||||||
Attack,
|
Attack,
|
||||||
@@ -13,16 +12,14 @@ import {
|
|||||||
} from "../game/Game";
|
} from "../game/Game";
|
||||||
import { TileRef } from "../game/GameMap";
|
import { TileRef } from "../game/GameMap";
|
||||||
import { PseudoRandom } from "../PseudoRandom";
|
import { PseudoRandom } from "../PseudoRandom";
|
||||||
|
import { FlatBinaryHeap } from "./utils/FlatBinaryHeap"; // adjust path if needed
|
||||||
|
|
||||||
const malusForRetreat = 25;
|
const malusForRetreat = 25;
|
||||||
|
|
||||||
export class AttackExecution implements Execution {
|
export class AttackExecution implements Execution {
|
||||||
private breakAlliance = false;
|
private breakAlliance = false;
|
||||||
private active: boolean = true;
|
private active: boolean = true;
|
||||||
private toConquer: PriorityQueue<TileContainer> =
|
private toConquer = new FlatBinaryHeap();
|
||||||
new PriorityQueue<TileContainer>((a: TileContainer, b: TileContainer) => {
|
|
||||||
return a.priority - b.priority;
|
|
||||||
});
|
|
||||||
private random = new PseudoRandom(123);
|
private random = new PseudoRandom(123);
|
||||||
|
|
||||||
private _owner: Player;
|
private _owner: Player;
|
||||||
@@ -196,9 +193,12 @@ export class AttackExecution implements Execution {
|
|||||||
if (this.attack === null) {
|
if (this.attack === null) {
|
||||||
throw new Error("Attack not initialized");
|
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.retreated()) {
|
||||||
if (this.attack.target().isPlayer()) {
|
if (targetIsPlayer) {
|
||||||
this.retreat(malusForRetreat);
|
this.retreat(malusForRetreat);
|
||||||
} else {
|
} else {
|
||||||
this.retreat();
|
this.retreat();
|
||||||
@@ -216,12 +216,14 @@ export class AttackExecution implements Execution {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alliance = this._owner.allianceWith(this.target as Player);
|
const alliance = targetPlayer
|
||||||
|
? this._owner.allianceWith(targetPlayer)
|
||||||
|
: null;
|
||||||
if (this.breakAlliance && alliance !== null) {
|
if (this.breakAlliance && alliance !== null) {
|
||||||
this.breakAlliance = false;
|
this.breakAlliance = false;
|
||||||
this._owner.breakAlliance(alliance);
|
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.
|
// In this case a new alliance was created AFTER the attack started.
|
||||||
this.retreat();
|
this.retreat();
|
||||||
return;
|
return;
|
||||||
@@ -230,14 +232,14 @@ export class AttackExecution implements Execution {
|
|||||||
let numTilesPerTick = this.mg
|
let numTilesPerTick = this.mg
|
||||||
.config()
|
.config()
|
||||||
.attackTilesPerTick(
|
.attackTilesPerTick(
|
||||||
this.attack.troops(),
|
troopCount,
|
||||||
this._owner,
|
this._owner,
|
||||||
this.target,
|
this.target,
|
||||||
this.border.size + this.random.nextInt(0, 5),
|
this.border.size + this.random.nextInt(0, 5),
|
||||||
);
|
);
|
||||||
|
|
||||||
while (numTilesPerTick > 0) {
|
while (numTilesPerTick > 0) {
|
||||||
if (this.attack.troops() < 1) {
|
if (troopCount < 1) {
|
||||||
this.attack.delete();
|
this.attack.delete();
|
||||||
this.active = false;
|
this.active = false;
|
||||||
return;
|
return;
|
||||||
@@ -249,13 +251,16 @@ export class AttackExecution implements Execution {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tileToConquer = this.toConquer.dequeue().tile;
|
const [tileToConquer] = this.toConquer.dequeue();
|
||||||
this.border.delete(tileToConquer);
|
this.border.delete(tileToConquer);
|
||||||
|
|
||||||
const onBorder =
|
let onBorder = false;
|
||||||
this.mg
|
for (const n of this.mg.neighbors(tileToConquer)) {
|
||||||
.neighbors(tileToConquer)
|
if (this.mg.owner(n) === this._owner) {
|
||||||
.filter((t) => this.mg.owner(t) === this._owner).length > 0;
|
onBorder = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (this.mg.owner(tileToConquer) !== this.target || !onBorder) {
|
if (this.mg.owner(tileToConquer) !== this.target || !onBorder) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -264,15 +269,16 @@ export class AttackExecution implements Execution {
|
|||||||
.config()
|
.config()
|
||||||
.attackLogic(
|
.attackLogic(
|
||||||
this.mg,
|
this.mg,
|
||||||
this.attack.troops(),
|
troopCount,
|
||||||
this._owner,
|
this._owner,
|
||||||
this.target,
|
this.target,
|
||||||
tileToConquer,
|
tileToConquer,
|
||||||
);
|
);
|
||||||
numTilesPerTick -= tilesPerTickUsed;
|
numTilesPerTick -= tilesPerTickUsed;
|
||||||
this.attack.setTroops(this.attack.troops() - attackerTroopLoss);
|
troopCount -= attackerTroopLoss;
|
||||||
if (this.target.isPlayer()) {
|
this.attack.setTroops(troopCount);
|
||||||
this.target.removeTroops(defenderTroopLoss);
|
if (targetPlayer) {
|
||||||
|
targetPlayer.removeTroops(defenderTroopLoss);
|
||||||
}
|
}
|
||||||
this._owner.conquer(tileToConquer);
|
this._owner.conquer(tileToConquer);
|
||||||
this.handleDeadDefender();
|
this.handleDeadDefender();
|
||||||
@@ -280,6 +286,8 @@ export class AttackExecution implements Execution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addNeighbors(tile: TileRef) {
|
private addNeighbors(tile: TileRef) {
|
||||||
|
const tickNow = this.mg.ticks(); // cache tick
|
||||||
|
|
||||||
for (const neighbor of this.mg.neighbors(tile)) {
|
for (const neighbor of this.mg.neighbors(tile)) {
|
||||||
if (
|
if (
|
||||||
this.mg.isWater(neighbor) ||
|
this.mg.isWater(neighbor) ||
|
||||||
@@ -288,11 +296,15 @@ export class AttackExecution implements Execution {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.border.add(neighbor);
|
this.border.add(neighbor);
|
||||||
const numOwnedByMe = this.mg
|
let numOwnedByMe = 0;
|
||||||
.neighbors(neighbor)
|
for (const n of this.mg.neighbors(neighbor)) {
|
||||||
.filter((t) => this.mg.owner(t) === this._owner).length;
|
if (this.mg.owner(n) === this._owner) {
|
||||||
|
numOwnedByMe++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mag = 0;
|
let mag = 0;
|
||||||
switch (this.mg.terrainType(tile)) {
|
switch (this.mg.terrainType(neighbor)) {
|
||||||
case TerrainType.Plains:
|
case TerrainType.Plains:
|
||||||
mag = 1;
|
mag = 1;
|
||||||
break;
|
break;
|
||||||
@@ -303,14 +315,12 @@ export class AttackExecution implements Execution {
|
|||||||
mag = 2;
|
mag = 2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.toConquer.enqueue(
|
|
||||||
new TileContainer(
|
const priority =
|
||||||
neighbor,
|
(this.random.nextInt(0, 7) + 10) * (1 - numOwnedByMe * 0.5 + mag / 2) +
|
||||||
(this.random.nextInt(0, 7) + 10) *
|
tickNow;
|
||||||
(1 - numOwnedByMe * 0.5 + mag / 2) +
|
|
||||||
this.mg.ticks(),
|
this.toConquer.enqueue(neighbor, priority);
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,10 +366,3 @@ export class AttackExecution implements Execution {
|
|||||||
return this.active;
|
return this.active;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TileContainer {
|
|
||||||
constructor(
|
|
||||||
public readonly tile: TileRef,
|
|
||||||
public readonly priority: number,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class QuickChatExecution implements Execution {
|
|||||||
this.variables,
|
this.variables,
|
||||||
this.recipient.id(),
|
this.recipient.id(),
|
||||||
true,
|
true,
|
||||||
this.recipient.name(),
|
this.sender.name(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.mg.displayChat(
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user