Merge branch 'main' into fix-sam-bug

This commit is contained in:
Abdallah Bahrawi
2026-02-16 21:58:54 +02:00
committed by GitHub
84 changed files with 1626 additions and 688 deletions
+2
View File
@@ -56,6 +56,8 @@ export const UserMeResponseSchema = z.object({
publicId: z.string(),
roles: z.string().array().optional(),
flares: z.string().array().optional(),
flareExpiration: z.record(z.string(), z.number()).optional(),
tempFlaresCooldown: z.boolean(),
achievements: z
.array(
z.object({
+5 -3
View File
@@ -118,7 +118,7 @@ export class GameRunner {
this.turns.push(turn);
}
public executeNextTick(): boolean {
public executeNextTick(pendingTurns?: number): boolean {
if (this.isExecuting) {
return false;
}
@@ -182,6 +182,7 @@ export class GameRunner {
updates: updates,
playerNameViewData: this.playerViewData,
tickExecutionDuration: tickExecutionDuration,
pendingTurns: pendingTurns ?? 0,
});
this.isExecuting = false;
return true;
@@ -195,13 +196,14 @@ export class GameRunner {
playerID: PlayerID,
x?: number,
y?: number,
units?: UnitType[],
): PlayerActions {
const player = this.game.player(playerID);
const tile =
x !== undefined && y !== undefined ? this.game.ref(x, y) : null;
const actions = {
canAttack: tile !== null && player.canAttack(tile),
buildableUnits: player.buildableUnits(tile),
canAttack: tile !== null && units === undefined && player.canAttack(tile),
buildableUnits: player.buildableUnits(tile, units),
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
canEmbargoAll: player.canEmbargoAll(),
} as PlayerActions;
+12 -10
View File
@@ -34,7 +34,7 @@ export type Intent =
| BoatAttackIntent
| CancelBoatIntent
| AllianceRequestIntent
| AllianceRequestReplyIntent
| AllianceRejectIntent
| AllianceExtensionIntent
| BreakAllianceIntent
| TargetPlayerIntent
@@ -60,9 +60,7 @@ export type BoatAttackIntent = z.infer<typeof BoatAttackIntentSchema>;
export type EmbargoAllIntent = z.infer<typeof EmbargoAllIntentSchema>;
export type CancelBoatIntent = z.infer<typeof CancelBoatIntentSchema>;
export type AllianceRequestIntent = z.infer<typeof AllianceRequestIntentSchema>;
export type AllianceRequestReplyIntent = z.infer<
typeof AllianceRequestReplyIntentSchema
>;
export type AllianceRejectIntent = z.infer<typeof AllianceRejectIntentSchema>;
export type BreakAllianceIntent = z.infer<typeof BreakAllianceIntentSchema>;
export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>;
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>;
@@ -139,6 +137,9 @@ export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
export type GameInfo = z.infer<typeof GameInfoSchema>;
export type PublicGames = z.infer<typeof PublicGamesSchema>;
export type PublicGameInfo = z.infer<typeof PublicGameInfoSchema>;
export type PublicGameType = z.infer<typeof PublicGameTypeSchema>;
export const PublicGameTypeSchema = z.enum(["ffa", "team", "special"]);
const ClientInfoSchema = z.object({
clientID: z.string(),
@@ -152,6 +153,7 @@ export const GameInfoSchema = z.object({
startsAt: z.number().optional(),
serverTime: z.number(),
gameConfig: z.lazy(() => GameConfigSchema).optional(),
publicGameType: PublicGameTypeSchema.optional(),
});
export const PublicGameInfoSchema = z.object({
@@ -159,11 +161,12 @@ export const PublicGameInfoSchema = z.object({
numClients: z.number(),
startsAt: z.number(),
gameConfig: z.lazy(() => GameConfigSchema).optional(),
publicGameType: PublicGameTypeSchema,
});
export const PublicGamesSchema = z.object({
serverTime: z.number(),
games: PublicGameInfoSchema.array(),
games: z.record(PublicGameTypeSchema, z.array(PublicGameInfoSchema)),
});
export class LobbyInfoEvent implements GameEvent {
@@ -311,10 +314,9 @@ export const AllianceRequestIntentSchema = z.object({
recipient: ID,
});
export const AllianceRequestReplyIntentSchema = z.object({
type: z.literal("allianceRequestReply"),
requestor: ID, // The one who made the original alliance request
accept: z.boolean(),
export const AllianceRejectIntentSchema = z.object({
type: z.literal("allianceReject"),
requestor: ID,
});
export const BreakAllianceIntentSchema = z.object({
@@ -426,7 +428,7 @@ const IntentSchema = z.discriminatedUnion("type", [
BoatAttackIntentSchema,
CancelBoatIntentSchema,
AllianceRequestIntentSchema,
AllianceRequestReplyIntentSchema,
AllianceRejectIntentSchema,
BreakAllianceIntentSchema,
TargetPlayerIntentSchema,
EmojiIntentSchema,
+3 -7
View File
@@ -3,8 +3,8 @@ import { PseudoRandom } from "../PseudoRandom";
import { ClientID, GameID, StampedIntent, Turn } from "../Schemas";
import { simpleHash } from "../Util";
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
import { AllianceRejectExecution } from "./alliance/AllianceRejectExecution";
import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution";
import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyExecution";
import { BreakAllianceExecution } from "./alliance/BreakAllianceExecution";
import { AttackExecution } from "./AttackExecution";
import { BoatRetreatExecution } from "./BoatRetreatExecution";
@@ -75,12 +75,8 @@ export class Executor {
return new TransportShipExecution(player, intent.dst, intent.troops);
case "allianceRequest":
return new AllianceRequestExecution(player, intent.recipient);
case "allianceRequestReply":
return new AllianceRequestReplyExecution(
intent.requestor,
player,
intent.accept,
);
case "allianceReject":
return new AllianceRejectExecution(intent.requestor, player);
case "breakAlliance":
return new BreakAllianceExecution(player, intent.recipient);
case "targetPlayer":
+1
View File
@@ -63,6 +63,7 @@ export class MirvExecution implements Execution {
}
if (this.targetPlayer !== this.player) {
this.targetPlayer.updateRelation(this.player, -100);
this.player.updateRelation(this.targetPlayer, -100);
}
}
}
@@ -0,0 +1,49 @@
import { Execution, Game, Player, PlayerID } from "../../game/Game";
export class AllianceRejectExecution implements Execution {
private active = true;
constructor(
private requestorID: PlayerID,
private recipient: Player,
) {}
init(mg: Game, ticks: number): void {
if (!mg.hasPlayer(this.requestorID)) {
console.warn(
`[AllianceRejectExecution] Requestor ${this.requestorID} not found`,
);
this.active = false;
return;
}
const requestor = mg.player(this.requestorID);
if (requestor.isFriendly(this.recipient)) {
console.warn(
`[AllianceRejectExecution] Player ${this.requestorID} cannot reject alliance with ${this.recipient.id}, already allied`,
);
} else {
const request = requestor
.outgoingAllianceRequests()
.find((ar) => ar.recipient() === this.recipient);
if (request === undefined) {
console.warn(
`[AllianceRejectExecution] Player ${this.requestorID} cannot reject alliance with ${this.recipient.id}, no alliance request found`,
);
} else {
request.reject();
}
}
this.active = false;
}
tick(ticks: number): void {}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
@@ -2,8 +2,10 @@ import {
AllianceRequest,
Execution,
Game,
MessageType,
Player,
PlayerID,
UnitType,
} from "../../game/Game";
export class AllianceRequestExecution implements Execution {
@@ -39,6 +41,19 @@ export class AllianceRequestExecution implements Execution {
// then accept it instead of creating a new one.
this.active = false;
incoming.accept();
// Update player relations
this.requestor.updateRelation(recipient, 100);
recipient.updateRelation(this.requestor, 100);
// Automatically remove embargoes only if they were automatically created
if (this.requestor.hasEmbargoAgainst(recipient))
this.requestor.endTemporaryEmbargo(recipient);
if (recipient.hasEmbargoAgainst(this.requestor))
recipient.endTemporaryEmbargo(this.requestor);
// Cancel incoming nukes between players
this.cancelNukesBetweenAlliedPlayers(recipient);
} else {
this.req = this.requestor.createAllianceRequest(recipient);
}
@@ -69,4 +84,51 @@ export class AllianceRequestExecution implements Execution {
activeDuringSpawnPhase(): boolean {
return false;
}
cancelNukesBetweenAlliedPlayers(recipient: Player): void {
const neutralized = new Map<Player, number>();
const players = [this.requestor, recipient];
for (const launcher of players) {
for (const unit of launcher.units(
UnitType.AtomBomb,
UnitType.HydrogenBomb,
)) {
if (!unit.isActive() || unit.reachedTarget()) continue;
const targetTile = unit.targetTile();
if (!targetTile) continue;
const targetOwner = this.mg.owner(targetTile);
if (!targetOwner.isPlayer()) continue;
const other = launcher === this.requestor ? recipient : this.requestor;
if (targetOwner !== other) continue;
unit.delete(false);
neutralized.set(launcher, (neutralized.get(launcher) ?? 0) + 1);
}
}
for (const [launcher, count] of neutralized) {
const other = launcher === this.requestor ? recipient : this.requestor;
this.mg.displayMessage(
"events_display.alliance_nukes_destroyed_outgoing",
MessageType.ALLIANCE_ACCEPTED,
launcher.id(),
undefined,
{ name: other.displayName(), count },
);
this.mg.displayMessage(
"events_display.alliance_nukes_destroyed_incoming",
MessageType.ALLIANCE_ACCEPTED,
other.id(),
undefined,
{ name: launcher.displayName(), count },
);
}
}
}
@@ -1,117 +0,0 @@
import {
Execution,
Game,
MessageType,
Player,
PlayerID,
UnitType,
} from "../../game/Game";
export class AllianceRequestReplyExecution implements Execution {
private active = true;
private requestor: Player | null = null;
constructor(
private requestorID: PlayerID,
private recipient: Player,
private accept: boolean,
) {}
private cancelNukesBetweenAlliedPlayers(
mg: Game,
p1: Player,
p2: Player,
): void {
const neutralized = new Map<Player, number>();
const players = [p1, p2];
for (const launcher of players) {
for (const unit of launcher.units(
UnitType.AtomBomb,
UnitType.HydrogenBomb,
)) {
if (!unit.isActive() || unit.reachedTarget()) continue;
const targetTile = unit.targetTile();
if (!targetTile) continue;
const targetOwner = mg.owner(targetTile);
if (!targetOwner.isPlayer()) continue;
const other = launcher === p1 ? p2 : p1;
if (targetOwner !== other) continue;
unit.delete(false);
neutralized.set(launcher, (neutralized.get(launcher) ?? 0) + 1);
}
}
for (const [launcher, count] of neutralized) {
const other = launcher === p1 ? p2 : p1;
mg.displayMessage(
"events_display.alliance_nukes_destroyed_outgoing",
MessageType.ALLIANCE_ACCEPTED,
launcher.id(),
undefined,
{ name: other.displayName(), count },
);
mg.displayMessage(
"events_display.alliance_nukes_destroyed_incoming",
MessageType.ALLIANCE_ACCEPTED,
other.id(),
undefined,
{ name: launcher.displayName(), count },
);
}
}
init(mg: Game, ticks: number): void {
if (!mg.hasPlayer(this.requestorID)) {
console.warn(
`AllianceRequestReplyExecution requester ${this.requestorID} not found`,
);
this.active = false;
return;
}
this.requestor = mg.player(this.requestorID);
if (this.requestor.isFriendly(this.recipient)) {
console.warn("already allied");
} else {
const request = this.requestor
.outgoingAllianceRequests()
.find((ar) => ar.recipient() === this.recipient);
if (request === undefined) {
console.warn("no alliance request found");
} else {
if (this.accept) {
request.accept();
this.requestor.updateRelation(this.recipient, 100);
this.recipient.updateRelation(this.requestor, 100);
this.cancelNukesBetweenAlliedPlayers(
mg,
this.requestor,
this.recipient,
);
} else {
request.reject();
}
}
}
this.active = false;
}
tick(ticks: number): void {}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
@@ -87,7 +87,7 @@ export class NationAllianceBehavior {
}
return false;
}
// Reject if otherPlayer has allied with 50% or more of all players (Hard and Impossible only)
// Reject if otherPlayer has allied with a lot of players (Hard and Impossible only)
// To make sure there are enough non-friendly players in the game to stop the crown with nukes
if (this.hasTooManyAlliances(otherPlayer)) {
return false;
@@ -148,7 +148,7 @@ export class NationAllianceBehavior {
.filter((p) => p.type() !== PlayerType.Bot).length;
const otherPlayerAlliances = otherPlayer.alliances().length;
if (difficulty !== Difficulty.Hard) {
if (difficulty === Difficulty.Hard) {
return otherPlayerAlliances >= totalPlayers * 0.5;
} else {
return otherPlayerAlliances >= totalPlayers * 0.25;
@@ -4,7 +4,9 @@ import {
Game,
Gold,
Player,
PlayerID,
PlayerType,
Tick,
UnitType,
} from "../../game/Game";
import { TileRef } from "../../game/GameMap";
@@ -18,7 +20,15 @@ import {
respondToMIRV,
} from "./NationEmojiBehavior";
// 30 seconds at 10 ticks/second
const MIRV_COOLDOWN_TICKS = 300;
export class NationMIRVBehavior {
// Shared across all NationMIRVBehavior instances.
// Tracks the last tick a MIRV was sent at each player, so multiple nations don't pile-on the same target.
// Especially important for games with very high starting gold settings.
private static recentMirvTargets = new Map<PlayerID, Tick>();
constructor(
private random: PseudoRandom,
private game: Game,
@@ -119,19 +129,19 @@ export class NationMIRVBehavior {
}
const inboundMIRVSender = this.selectCounterMirvTarget();
if (inboundMIRVSender) {
if (inboundMIRVSender && !this.wasRecentlyMirved(inboundMIRVSender)) {
this.maybeSendMIRV(inboundMIRVSender);
return true;
}
const victoryDenialTarget = this.selectVictoryDenialTarget();
if (victoryDenialTarget) {
if (victoryDenialTarget && !this.wasRecentlyMirved(victoryDenialTarget)) {
this.maybeSendMIRV(victoryDenialTarget);
return true;
}
const steamrollStopTarget = this.selectSteamrollStopTarget();
if (steamrollStopTarget) {
if (steamrollStopTarget && !this.wasRecentlyMirved(steamrollStopTarget)) {
this.maybeSendMIRV(steamrollStopTarget);
return true;
}
@@ -223,6 +233,17 @@ export class NationMIRVBehavior {
return null;
}
// MIRV Cooldown Methods
private wasRecentlyMirved(target: Player): boolean {
const lastTick = NationMIRVBehavior.recentMirvTargets.get(target.id());
if (lastTick === undefined) return false;
return this.game.ticks() - lastTick < MIRV_COOLDOWN_TICKS;
}
private recordMirvHit(target: Player): void {
NationMIRVBehavior.recentMirvTargets.set(target.id(), this.game.ticks());
}
// MIRV Helper Methods
private getValidMirvTargetPlayers(): Player[] {
if (this.player === null) throw new Error("not initialized");
@@ -261,6 +282,7 @@ export class NationMIRVBehavior {
const centerTile = this.calculateTerritoryCenter(enemy);
if (centerTile && this.player.canBuild(UnitType.MIRV, centerTile)) {
this.game.addExecution(new MirvExecution(this.player, centerTile));
this.recordMirvHit(enemy);
this.emojiBehavior.sendEmoji(AllPlayers, EMOJI_NUKE);
respondToMIRV(this.game, this.random, enemy);
}
@@ -659,8 +659,8 @@ export class NationNukeBehavior {
this.recentlySentNukes.push([tick, tile, nukeType]);
if (nukeType === UnitType.AtomBomb) {
this.atomBombsLaunched++;
// Increase perceived cost by 35% each time to simulate saving up for a MIRV (higher than hydro to make atom bombs less attractive for the lategame)
this.atomBombPerceivedCost = (this.atomBombPerceivedCost * 135n) / 100n;
// Increase perceived cost by 50% each time to simulate saving up for a MIRV (higher than hydro to make atom bombs less attractive for the lategame)
this.atomBombPerceivedCost = (this.atomBombPerceivedCost * 150n) / 100n;
} else if (nukeType === UnitType.HydrogenBomb) {
this.hydrogenBombsLaunched++;
// Increase perceived cost by 25% each time to simulate saving up for a MIRV
+39 -4
View File
@@ -3,6 +3,7 @@ import {
Game,
GameMode,
HumansVsNations,
isStructureType,
Player,
PlayerID,
PlayerType,
@@ -199,6 +200,13 @@ export class AiAttackBehavior {
borderingFriends: Player[],
borderingEnemies: Player[],
) {
// In games with high starting gold, nations will quickly build a lot of cities
// This causes them to expand slowly (cities increase max troops), and bots will steal their structures
// In this case: Attack bots before ratio checks
if (this.hasNeighboringBotWithStructures()) {
if (this.attackBots()) return;
}
// Save up troops until we reach the reserve ratio
if (!this.hasReserveRatioTroops()) return;
@@ -345,6 +353,18 @@ export class AiAttackBehavior {
}
}
private hasNeighboringBotWithStructures(): boolean {
return this.player
.neighbors()
.some(
(n) =>
n.isPlayer() &&
n.type() === PlayerType.Bot &&
!this.player.isFriendly(n) &&
n.units().some((u) => isStructureType(u.type())),
);
}
private hasReserveRatioTroops(): boolean {
const maxTroops = this.game.config().maxTroops(this.player);
const ratio = this.player.troops() / maxTroops;
@@ -380,6 +400,7 @@ export class AiAttackBehavior {
// Sort neighboring bots by density (troops / tiles) and attempt to attack many of them (Parallel attacks)
// sendAttack will do nothing if we don't have enough reserve troops left
// Bots that own structures are prioritized as targets (they might have stolen our structures and they will delete them!)
private attackBots(): boolean {
const bots = this.player
.neighbors()
@@ -397,7 +418,16 @@ export class AiAttackBehavior {
this.botAttackTroopsSent = 0;
const density = (p: Player) => p.troops() / p.numTilesOwned();
const sortedBots = bots.slice().sort((a, b) => density(a) - density(b));
const ownsStructures = (p: Player) =>
p.units().some((u) => isStructureType(u.type()));
const sortedBots = bots.slice().sort((a, b) => {
const aHasStructures = ownsStructures(a);
const bHasStructures = ownsStructures(b);
if (aHasStructures !== bHasStructures) {
return aHasStructures ? -1 : 1;
}
return density(a) - density(b);
});
const reducedBots = sortedBots.slice(0, this.getBotAttackMaxParallelism());
for (const bot of reducedBots) {
@@ -700,9 +730,14 @@ export class AiAttackBehavior {
private sendLandAttack(target: Player | TerraNullius) {
const maxTroops = this.game.config().maxTroops(this.player);
const reserveRatio = target.isPlayer()
? this.reserveRatio
: this.expandRatio;
const botWithStructures =
target.isPlayer() &&
target.type() === PlayerType.Bot &&
target.units().some((u) => isStructureType(u.type()));
// Use the expand ratio when attacking a bot that owns structures — we need to
// recapture those structures ASAP, even before reaching the normal reserve.
const useReserve = target.isPlayer() && !botWithStructures;
const reserveRatio = useReserve ? this.reserveRatio : this.expandRatio;
const targetTroops = maxTroops * reserveRatio;
let troops;
+3 -1
View File
@@ -120,6 +120,7 @@ export enum GameMapType {
AmazonRiver = "Amazon River",
Yenisei = "Yenisei",
TradersDream = "Traders Dream",
Hawaii = "Hawaii",
}
export type GameMapName = keyof typeof GameMapType;
@@ -168,6 +169,7 @@ export const mapCategories: Record<string, GameMapType[]> = {
GameMapType.StraitOfHormuz,
GameMapType.AmazonRiver,
GameMapType.Yenisei,
GameMapType.Hawaii,
],
fantasy: [
GameMapType.Pangaea,
@@ -625,7 +627,7 @@ export interface Player {
unitCount(type: UnitType): number;
unitsConstructed(type: UnitType): number;
unitsOwned(type: UnitType): number;
buildableUnits(tile: TileRef | null): BuildableUnit[];
buildableUnits(tile: TileRef | null, units?: UnitType[]): BuildableUnit[];
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
buildUnit<T extends UnitType>(
type: T,
-6
View File
@@ -332,12 +332,6 @@ export class GameImpl implements Game {
request,
);
// Automatically remove embargoes only if they were automatically created
if (requestor.hasEmbargoAgainst(recipient))
requestor.endTemporaryEmbargo(recipient);
if (recipient.hasEmbargoAgainst(requestor))
recipient.endTemporaryEmbargo(requestor);
this.addUpdate({
type: GameUpdateType.AllianceRequestReply,
request: request.toUpdate(),
+1
View File
@@ -20,6 +20,7 @@ export interface GameUpdateViewData {
packedTileUpdates: BigUint64Array;
playerNameViewData: Record<string, NameViewData>;
tickExecutionDuration?: number;
pendingTurns?: number;
}
export interface ErrorUpdate {
+6 -1
View File
@@ -403,11 +403,12 @@ export class PlayerView {
return { hasEmbargo, hasFriendly };
}
async actions(tile?: TileRef): Promise<PlayerActions> {
async actions(tile?: TileRef, units?: UnitType[]): Promise<PlayerActions> {
return this.game.worker.playerInteraction(
this.id(),
tile && this.game.x(tile),
tile && this.game.y(tile),
units,
);
}
@@ -636,6 +637,10 @@ export class GameView implements GameMap {
return this.lastUpdate?.updates ?? null;
}
public isCatchingUp(): boolean {
return (this.lastUpdate?.pendingTurns ?? 0) > 1;
}
public update(gu: GameUpdateViewData) {
this.toDelete.forEach((id) => this._units.delete(id));
this.toDelete.clear();
+34 -24
View File
@@ -22,6 +22,7 @@ import {
EmojiMessage,
GameMode,
Gold,
isStructureType,
MessageType,
MutableAlliance,
Player,
@@ -960,31 +961,40 @@ export class PlayerImpl implements Player {
this.recordUnitConstructed(unit.type());
}
public buildableUnits(tile: TileRef | null): BuildableUnit[] {
const validTiles = tile !== null ? this.validStructureSpawnTiles(tile) : [];
return Object.values(UnitType).map((u) => {
let canUpgrade: number | false = false;
let canBuild: TileRef | false = false;
if (!this.mg.inSpawnPhase()) {
const existingUnit = tile !== null && this.findUnitToUpgrade(u, tile);
if (existingUnit !== false) {
canUpgrade = existingUnit.id();
public buildableUnits(
tile: TileRef | null,
units?: UnitType[],
): BuildableUnit[] {
const validTiles =
tile !== null &&
(units === undefined || units.some((u) => isStructureType(u)))
? this.validStructureSpawnTiles(tile)
: [];
return Object.values(UnitType)
.filter((u) => units === undefined || units.includes(u))
.map((u) => {
let canUpgrade: number | false = false;
let canBuild: TileRef | false = false;
if (!this.mg.inSpawnPhase()) {
const existingUnit = tile !== null && this.findUnitToUpgrade(u, tile);
if (existingUnit !== false) {
canUpgrade = existingUnit.id();
}
if (tile !== null) {
canBuild = this.canBuild(u, tile, validTiles);
}
}
if (tile !== null) {
canBuild = this.canBuild(u, tile, validTiles);
}
}
return {
type: u,
canBuild,
canUpgrade,
cost: this.mg.config().unitInfo(u).cost(this.mg, this),
overlappingRailroads:
canBuild !== false
? this.mg.railNetwork().overlappingRailroads(canBuild)
: [],
} as BuildableUnit;
});
return {
type: u,
canBuild,
canUpgrade,
cost: this.mg.config().unitInfo(u).cost(this.mg, this),
overlappingRailroads:
canBuild !== false
? this.mg.railNetwork().overlappingRailroads(canBuild)
: [],
} as BuildableUnit;
});
}
canBuild(
+6
View File
@@ -192,6 +192,12 @@ export class UserSettings {
}
}
getFlag(): string | undefined {
const flag = localStorage.getItem("flag");
if (!flag || flag === "xx") return undefined;
return flag;
}
backgroundMusicVolume(): number {
return this.getFloat("settings.backgroundMusicVolume", 0);
}
+4 -2
View File
@@ -42,9 +42,10 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
if (!gr) {
break;
}
const ticksToRun = Math.min(gr.pendingTurns(), MAX_TICKS_PER_HEARTBEAT);
const pendingTurns = gr.pendingTurns();
const ticksToRun = Math.min(pendingTurns, MAX_TICKS_PER_HEARTBEAT);
for (let i = 0; i < ticksToRun; i++) {
if (!gr.executeNextTick()) {
if (!gr.executeNextTick(gr.pendingTurns())) {
break;
}
}
@@ -94,6 +95,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
message.playerID,
message.x,
message.y,
message.units,
);
sendMessage({
type: "player_actions_result",
+6 -3
View File
@@ -4,6 +4,7 @@ import {
PlayerBorderTiles,
PlayerID,
PlayerProfile,
UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
@@ -164,6 +165,7 @@ export class WorkerClient {
playerID: PlayerID,
x?: number,
y?: number,
units?: UnitType[],
): Promise<PlayerActions> {
return new Promise((resolve, reject) => {
if (!this.isInitialized) {
@@ -185,9 +187,10 @@ export class WorkerClient {
this.worker.postMessage({
type: "player_actions",
id: messageId,
playerID: playerID,
x: x,
y: y,
playerID,
x,
y,
units,
});
});
}
+2
View File
@@ -3,6 +3,7 @@ import {
PlayerBorderTiles,
PlayerID,
PlayerProfile,
UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { GameUpdateViewData } from "../game/GameUpdates";
@@ -62,6 +63,7 @@ export interface PlayerActionsMessage extends BaseWorkerMessage {
playerID: PlayerID;
x?: number;
y?: number;
units?: UnitType[];
}
export interface PlayerActionsResultMessage extends BaseWorkerMessage {