mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-09 21:48:07 +00:00
Merge main into nations-ai
This commit is contained in:
+6
-7
@@ -6,7 +6,7 @@ import {
|
||||
GameMode,
|
||||
GameType,
|
||||
PlayerType,
|
||||
TeamName,
|
||||
Team,
|
||||
UnitType,
|
||||
} from "./game/Game";
|
||||
|
||||
@@ -121,16 +121,14 @@ const GameConfigSchema = z.object({
|
||||
infiniteTroops: z.boolean(),
|
||||
instantBuild: z.boolean(),
|
||||
maxPlayers: z.number().optional(),
|
||||
numPlayerTeams: z.number().optional(),
|
||||
});
|
||||
|
||||
const SafeString = z
|
||||
.string()
|
||||
// Remove common dangerous characters and patterns
|
||||
// The weird \u stuff is to allow emojis
|
||||
.regex(
|
||||
/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=\[\]{}|;:"'\/\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|üÜ]+$/,
|
||||
/^([a-zA-Z0-9\s.,!?@#$%&*()-_+=\[\]{}|;:"'\/\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|üÜ])*$/,
|
||||
)
|
||||
// Reasonable max length to prevent DOS
|
||||
.max(1000);
|
||||
|
||||
const EmojiSchema = z.string().refine(
|
||||
@@ -364,7 +362,7 @@ const ClientBaseMessageSchema = z.object({
|
||||
|
||||
export const ClientSendWinnerSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal("winner"),
|
||||
winner: ID.or(z.nativeEnum(TeamName)).nullable(),
|
||||
winner: ID.or(z.nativeEnum(Team)).nullable(),
|
||||
allPlayersStats: AllPlayersStatsSchema,
|
||||
winnerType: z.enum(["player", "team"]),
|
||||
});
|
||||
@@ -396,6 +394,7 @@ export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal("join"),
|
||||
lastTurn: z.number(), // The last turn the client saw.
|
||||
username: SafeString,
|
||||
flag: SafeString.nullable().optional(),
|
||||
});
|
||||
|
||||
export const ClientMessageSchema = z.union([
|
||||
@@ -425,7 +424,7 @@ export const GameRecordSchema = z.object({
|
||||
num_turns: z.number(),
|
||||
turns: z.array(TurnSchema),
|
||||
winner: z
|
||||
.union([ID, z.nativeEnum(TeamName)])
|
||||
.union([ID, z.nativeEnum(Team)])
|
||||
.nullable()
|
||||
.optional(),
|
||||
winnerType: z.enum(["player", "team"]).nullable().optional(),
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import twemoji from "twemoji";
|
||||
import { Cell, Game, Player, TeamName, Unit } from "./game/Game";
|
||||
import { Cell, Game, Player, Team, Unit } from "./game/Game";
|
||||
import { andFN, GameMap, manhattanDistFN, TileRef } from "./game/GameMap";
|
||||
import {
|
||||
AllPlayersStats,
|
||||
@@ -253,7 +253,7 @@ export function createGameRecord(
|
||||
turns: Turn[],
|
||||
start: number,
|
||||
end: number,
|
||||
winner: ClientID | TeamName | null,
|
||||
winner: ClientID | Team | null,
|
||||
winnerType: "player" | "team" | null,
|
||||
allPlayersStats: AllPlayersStats,
|
||||
): GameRecord {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { colord, Colord } from "colord";
|
||||
|
||||
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
|
||||
export const teal = colord({ h: 172, s: 66, l: 50 });
|
||||
export const purple = colord({ h: 271, s: 81, l: 56 });
|
||||
export const yellow = colord({ h: 45, s: 93, l: 47 });
|
||||
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[] = [
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Gold,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
Team,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
UnitInfo,
|
||||
@@ -65,6 +66,7 @@ export interface Config {
|
||||
instantBuild(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
userSettings(): UserSettings;
|
||||
numPlayerTeams(): number;
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number;
|
||||
populationIncreaseRate(player: Player | PlayerView): number;
|
||||
@@ -122,6 +124,7 @@ export interface Config {
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
teamColor(team: Team): Colord;
|
||||
territoryColor(playerInfo: PlayerView): Colord;
|
||||
specialBuildingColor(playerInfo: PlayerView): Colord;
|
||||
borderColor(playerInfo: PlayerView): Colord;
|
||||
|
||||
@@ -189,6 +189,9 @@ export class DefaultConfig implements Config {
|
||||
defensePostDefenseBonus(): number {
|
||||
return 5;
|
||||
}
|
||||
numPlayerTeams(): number {
|
||||
return this._gameConfig.numPlayerTeams ?? 0;
|
||||
}
|
||||
spawnNPCs(): boolean {
|
||||
return !this._gameConfig.disableNPCs;
|
||||
}
|
||||
@@ -211,12 +214,12 @@ export class DefaultConfig implements Config {
|
||||
return 10000 + 150 * Math.pow(dist, 1.1);
|
||||
}
|
||||
tradeShipSpawnRate(numberOfPorts: number): number {
|
||||
if (numberOfPorts <= 3) return 180;
|
||||
if (numberOfPorts <= 5) return 250;
|
||||
if (numberOfPorts <= 8) return 350;
|
||||
if (numberOfPorts <= 10) return 400;
|
||||
if (numberOfPorts <= 12) return 450;
|
||||
return 500;
|
||||
if (numberOfPorts <= 3) return 18;
|
||||
if (numberOfPorts <= 5) return 25;
|
||||
if (numberOfPorts <= 8) return 35;
|
||||
if (numberOfPorts <= 10) return 40;
|
||||
if (numberOfPorts <= 12) return 45;
|
||||
return 50;
|
||||
}
|
||||
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
@@ -268,7 +271,7 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 500_000,
|
||||
p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 750_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.HydrogenBomb:
|
||||
@@ -336,7 +339,7 @@ export class DefaultConfig implements Config {
|
||||
p.type() == PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: Math.min(
|
||||
2_000_000,
|
||||
1_000_000,
|
||||
Math.pow(
|
||||
2,
|
||||
p.unitsIncludingConstruction(UnitType.City).length,
|
||||
@@ -473,18 +476,25 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
if (defender.isPlayer()) {
|
||||
const ratio = within(
|
||||
Math.pow(defender.troops() / attackTroops, 0.4),
|
||||
0.1,
|
||||
10,
|
||||
);
|
||||
const speedRatio = within(
|
||||
defender.troops() / (5 * attackTroops),
|
||||
0.1,
|
||||
10,
|
||||
);
|
||||
|
||||
return {
|
||||
attackerTroopLoss:
|
||||
within(defender.troops() / attackTroops, 0.6, 2) *
|
||||
ratio *
|
||||
mag *
|
||||
0.8 *
|
||||
largeLossModifier *
|
||||
(defender.isTraitor() ? this.traitorDefenseDebuff() : 1),
|
||||
defenderTroopLoss: defender.troops() / defender.numTilesOwned(),
|
||||
tilesPerTickUsed:
|
||||
within(defender.troops() / (5 * attackTroops), 0.2, 1.5) *
|
||||
speed *
|
||||
largeSpeedMalus,
|
||||
defenderTroopLoss: defender.population() / defender.numTilesOwned(),
|
||||
tilesPerTickUsed: Math.floor(speedRatio * speed * largeSpeedMalus),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -620,7 +630,8 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
goldAdditionRate(player: Player): number {
|
||||
return Math.sqrt(player.workers() * player.numTilesOwned()) / 200;
|
||||
const ratio = Math.pow(player.workers() / player.population(), 1.3);
|
||||
return Math.floor(Math.sqrt(player.workers()) * ratio * 5);
|
||||
}
|
||||
|
||||
troopAdjustmentRate(player: Player): number {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GameType, UnitInfo, UnitType } from "../game/Game";
|
||||
import { UnitInfo, UnitType } from "../game/Game";
|
||||
import { UserSettings } from "../game/UserSettings";
|
||||
import { GameConfig } from "../Schemas";
|
||||
import { GameEnv, ServerConfig } from "./Config";
|
||||
@@ -40,10 +40,10 @@ export class DevConfig extends DefaultConfig {
|
||||
super(sc, gc, us);
|
||||
}
|
||||
|
||||
numSpawnPhaseTurns(): number {
|
||||
return this.gameConfig().gameType == GameType.Singleplayer ? 40 : 100;
|
||||
// return 100
|
||||
}
|
||||
// numSpawnPhaseTurns(): number {
|
||||
// return this.gameConfig().gameType == GameType.Singleplayer ? 70 : 100;
|
||||
// // return 100
|
||||
// }
|
||||
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
const info = super.unitInfo(type);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Colord, colord } from "colord";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
import { PlayerType, TeamName, 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,
|
||||
humanColors,
|
||||
orange,
|
||||
purple,
|
||||
red,
|
||||
teal,
|
||||
territoryColors,
|
||||
yellow,
|
||||
} from "./Colors";
|
||||
import { Theme } from "./Config";
|
||||
|
||||
@@ -36,15 +41,31 @@ export const pastelTheme = new (class implements Theme {
|
||||
|
||||
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
|
||||
|
||||
teamColor(team: Team): Colord {
|
||||
switch (team) {
|
||||
case Team.Blue:
|
||||
return blue;
|
||||
case Team.Red:
|
||||
return red;
|
||||
case Team.Teal:
|
||||
return teal;
|
||||
case Team.Purple:
|
||||
return purple;
|
||||
case Team.Yellow:
|
||||
return yellow;
|
||||
case Team.Orange:
|
||||
return orange;
|
||||
case Team.Green:
|
||||
return green;
|
||||
case Team.Bot:
|
||||
return botColor;
|
||||
}
|
||||
throw new Error(`Missing color for ${team}`);
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
if (player.teamName() == TeamName.Bot) {
|
||||
return botColor;
|
||||
}
|
||||
if (player.teamName() == TeamName.Red) {
|
||||
return red;
|
||||
}
|
||||
if (player.teamName() == TeamName.Blue) {
|
||||
return blue;
|
||||
if (player.team() !== null) {
|
||||
return this.teamColor(player.team());
|
||||
}
|
||||
if (player.info().playerType == PlayerType.Human) {
|
||||
return humanColors[simpleHash(player.id()) % humanColors.length];
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Colord, colord } from "colord";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
import { PlayerType, TeamName, 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,
|
||||
humanColors,
|
||||
orange,
|
||||
purple,
|
||||
red,
|
||||
teal,
|
||||
territoryColors,
|
||||
yellow,
|
||||
} from "./Colors";
|
||||
import { Theme } from "./Config";
|
||||
|
||||
@@ -36,15 +41,31 @@ export const pastelThemeDark = new (class implements Theme {
|
||||
|
||||
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
|
||||
|
||||
teamColor(team: Team): Colord {
|
||||
switch (team) {
|
||||
case Team.Blue:
|
||||
return blue;
|
||||
case Team.Red:
|
||||
return red;
|
||||
case Team.Teal:
|
||||
return teal;
|
||||
case Team.Purple:
|
||||
return purple;
|
||||
case Team.Yellow:
|
||||
return yellow;
|
||||
case Team.Orange:
|
||||
return orange;
|
||||
case Team.Green:
|
||||
return green;
|
||||
case Team.Bot:
|
||||
return botColor;
|
||||
}
|
||||
throw new Error(`Missing color for ${team}`);
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
if (player.teamName() == TeamName.Bot) {
|
||||
return botColor;
|
||||
}
|
||||
if (player.teamName() == TeamName.Red) {
|
||||
return red;
|
||||
}
|
||||
if (player.teamName() == TeamName.Blue) {
|
||||
return blue;
|
||||
if (player.team() !== null) {
|
||||
return this.teamColor(player.team());
|
||||
}
|
||||
if (player.info().playerType == PlayerType.Human) {
|
||||
return humanColors[simpleHash(player.id()) % humanColors.length];
|
||||
|
||||
@@ -168,12 +168,12 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
if (enemyborder.length == 0) {
|
||||
if (this.random.chance(5)) {
|
||||
if (this.random.chance(10)) {
|
||||
this.sendBoatRandomly();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.random.chance(10)) {
|
||||
if (this.random.chance(20)) {
|
||||
this.sendBoatRandomly();
|
||||
return;
|
||||
}
|
||||
@@ -598,7 +598,7 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
const src = this.random.randElement(oceanShore);
|
||||
|
||||
const dst = this.randOceanShoreTile(src, 250);
|
||||
const dst = this.randOceanShoreTile(src, 150);
|
||||
if (dst == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ export class MirvExecution implements Execution {
|
||||
|
||||
randomLand(ref: TileRef, taken: TileRef[]): TileRef | null {
|
||||
let tries = 0;
|
||||
const mirvRange2 = this.mirvRange * this.mirvRange;
|
||||
while (tries < 100) {
|
||||
tries++;
|
||||
const x = this.random.nextInt(
|
||||
@@ -174,7 +175,7 @@ export class MirvExecution implements Execution {
|
||||
if (!this.mg.isLand(tile)) {
|
||||
continue;
|
||||
}
|
||||
if (this.mg.euclideanDist(tile, ref) > this.mirvRange) {
|
||||
if (this.mg.euclideanDistSquared(tile, ref) > mirvRange2) {
|
||||
continue;
|
||||
}
|
||||
if (this.mg.owner(tile) != this.targetPlayer) {
|
||||
|
||||
@@ -52,30 +52,25 @@ export class NukeExecution implements Execution {
|
||||
private tilesToDestroy(): Set<TileRef> {
|
||||
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
|
||||
const rand = new PseudoRandom(this.mg.ticks());
|
||||
const inner2 = magnitude.inner * magnitude.inner;
|
||||
const outer2 = magnitude.outer * magnitude.outer;
|
||||
return this.mg.bfs(this.dst, (_, n: TileRef) => {
|
||||
const d = this.mg.euclideanDist(this.dst, n);
|
||||
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer;
|
||||
const d2 = this.mg.euclideanDistSquared(this.dst, n);
|
||||
return d2 <= outer2 && (d2 <= inner2 || rand.chance(2));
|
||||
});
|
||||
}
|
||||
|
||||
private getAttackedTiles() {
|
||||
const toDestroy = this.tilesToDestroy();
|
||||
|
||||
private breakAlliances(toDestroy: Set<TileRef>) {
|
||||
const attacked = new Map<Player, number>();
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
const mp = this.mg.player(owner.id());
|
||||
const prev = attacked.get(mp) ?? 0;
|
||||
attacked.set(mp, prev + 1);
|
||||
const prev = attacked.get(owner) ?? 0;
|
||||
attacked.set(owner, prev + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return attacked;
|
||||
}
|
||||
|
||||
private breakAlliances() {
|
||||
for (const [other, tilesDestroyed] of this.getAttackedTiles()) {
|
||||
for (const [other, tilesDestroyed] of attacked) {
|
||||
if (tilesDestroyed > 100 && this.nuke.type() != UnitType.MIRVWarhead) {
|
||||
// Mirv warheads shouldn't break alliances
|
||||
const alliance = this.player.allianceWith(other);
|
||||
@@ -143,8 +138,6 @@ export class NukeExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
this.breakAlliances();
|
||||
|
||||
if (this.waitTicks > 0) {
|
||||
this.waitTicks--;
|
||||
return;
|
||||
@@ -196,28 +189,32 @@ export class NukeExecution implements Execution {
|
||||
private detonate() {
|
||||
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
|
||||
const toDestroy = this.tilesToDestroy();
|
||||
this.breakAlliances(toDestroy);
|
||||
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
const mp = this.mg.player(owner.id());
|
||||
mp.relinquish(tile);
|
||||
mp.removeTroops(
|
||||
this.mg.config().nukeDeathFactor(mp.troops(), mp.numTilesOwned()),
|
||||
owner.relinquish(tile);
|
||||
owner.removeTroops(
|
||||
this.mg
|
||||
.config()
|
||||
.nukeDeathFactor(owner.troops(), owner.numTilesOwned()),
|
||||
);
|
||||
mp.removeWorkers(
|
||||
this.mg.config().nukeDeathFactor(mp.workers(), mp.numTilesOwned()),
|
||||
owner.removeWorkers(
|
||||
this.mg
|
||||
.config()
|
||||
.nukeDeathFactor(owner.workers(), owner.numTilesOwned()),
|
||||
);
|
||||
mp.outgoingAttacks().forEach((attack) => {
|
||||
owner.outgoingAttacks().forEach((attack) => {
|
||||
const deaths = this.mg
|
||||
.config()
|
||||
.nukeDeathFactor(attack.troops(), mp.numTilesOwned());
|
||||
.nukeDeathFactor(attack.troops(), owner.numTilesOwned());
|
||||
attack.setTroops(attack.troops() - deaths);
|
||||
});
|
||||
mp.units(UnitType.TransportShip).forEach((attack) => {
|
||||
owner.units(UnitType.TransportShip).forEach((attack) => {
|
||||
const deaths = this.mg
|
||||
.config()
|
||||
.nukeDeathFactor(attack.troops(), mp.numTilesOwned());
|
||||
.nukeDeathFactor(attack.troops(), owner.numTilesOwned());
|
||||
attack.setTroops(attack.troops() - deaths);
|
||||
});
|
||||
}
|
||||
@@ -227,6 +224,7 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
const outer2 = magnitude.outer * magnitude.outer;
|
||||
for (const unit of this.mg.units()) {
|
||||
if (
|
||||
unit.type() != UnitType.AtomBomb &&
|
||||
@@ -234,7 +232,7 @@ export class NukeExecution implements Execution {
|
||||
unit.type() != UnitType.MIRVWarhead &&
|
||||
unit.type() != UnitType.MIRV
|
||||
) {
|
||||
if (this.mg.euclideanDist(this.dst, unit.tile()) < magnitude.outer) {
|
||||
if (this.mg.euclideanDistSquared(this.dst, unit.tile()) < outer2) {
|
||||
unit.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export class PortExecution implements Execution {
|
||||
private mg: Game;
|
||||
private port: Unit;
|
||||
private random: PseudoRandom;
|
||||
private checkOffset: number;
|
||||
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
@@ -31,6 +32,7 @@ export class PortExecution implements Execution {
|
||||
}
|
||||
this.mg = mg;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
this.checkOffset = mg.ticks() % 10;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
@@ -55,6 +57,11 @@ export class PortExecution implements Execution {
|
||||
this._owner = this.port.owner().id();
|
||||
}
|
||||
|
||||
// Only check every 10 ticks for performance.
|
||||
if ((this.mg.ticks() + this.checkOffset) % 10 != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const totalNbOfPorts = this.mg.units(UnitType.Port).length;
|
||||
if (
|
||||
!this.random.chance(this.mg.config().tradeShipSpawnRate(totalNbOfPorts))
|
||||
|
||||
@@ -105,7 +105,10 @@ export class SAMLauncherExecution implements Execution {
|
||||
if (this.target && !this.sam.isCooldown() && !this.target.targetedBySAM()) {
|
||||
this.sam.setCooldown(true);
|
||||
const random = this.pseudoRandom.next();
|
||||
const hit = random < this.mg.config().samHittingChance();
|
||||
let hit = true;
|
||||
if (this.target.type() != UnitType.AtomBomb) {
|
||||
hit = random < this.mg.config().samHittingChance();
|
||||
}
|
||||
if (!hit) {
|
||||
this.mg.displayMessage(
|
||||
`Missile failed to intercept ${this.target.type()}`,
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { GameEvent } from "../EventBus";
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
GameMode,
|
||||
Player,
|
||||
Team,
|
||||
TeamName,
|
||||
} from "../game/Game";
|
||||
import { Execution, Game, GameMode, Player, Team } from "../game/Game";
|
||||
|
||||
export class WinEvent implements GameEvent {
|
||||
constructor(public readonly winner: Player) {}
|
||||
@@ -73,9 +66,9 @@ export class WinCheckExecution implements Execution {
|
||||
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
|
||||
const percentage = (max[1] / numTilesWithoutFallout) * 100;
|
||||
if (percentage > this.mg.config().percentageTilesOwnedToWin()) {
|
||||
if (max[0].name == TeamName.Bot) return;
|
||||
this.mg.setWinner(max[0].name, this.mg.stats().stats());
|
||||
console.log(`${max[0].name} has won the game`);
|
||||
if (max[0] == Team.Bot) return;
|
||||
this.mg.setWinner(max[0], this.mg.stats().stats());
|
||||
console.log(`${max[0]} has won the game`);
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,14 @@ export enum Difficulty {
|
||||
Impossible = "Impossible",
|
||||
}
|
||||
|
||||
export enum TeamName {
|
||||
export enum Team {
|
||||
Red = "Red",
|
||||
Blue = "Blue",
|
||||
Teal = "Teal",
|
||||
Purple = "Purple",
|
||||
Yellow = "Yellow",
|
||||
Orange = "Orange",
|
||||
Green = "Green",
|
||||
Bot = "Bot",
|
||||
}
|
||||
|
||||
@@ -75,10 +80,6 @@ export enum GameMode {
|
||||
Team = "Team",
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
name: TeamName;
|
||||
}
|
||||
|
||||
export interface UnitInfo {
|
||||
cost: (player: Player | PlayerView) => Gold;
|
||||
// Determines if its owner changes when its tile is conquered.
|
||||
@@ -443,7 +444,7 @@ export interface Game extends GameMap {
|
||||
ticks(): Tick;
|
||||
inSpawnPhase(): boolean;
|
||||
executeNextTick(): GameUpdates;
|
||||
setWinner(winner: Player | TeamName, allPlayersStats: AllPlayersStats): void;
|
||||
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void;
|
||||
config(): Config;
|
||||
|
||||
// Units
|
||||
|
||||
+17
-10
@@ -20,7 +20,6 @@ import {
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
Team,
|
||||
TeamName,
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
Unit,
|
||||
@@ -76,11 +75,8 @@ export class GameImpl implements Game {
|
||||
|
||||
private _stats: StatsImpl = new StatsImpl();
|
||||
|
||||
private playerTeams: Team[] = [
|
||||
{ name: TeamName.Red },
|
||||
{ name: TeamName.Blue },
|
||||
];
|
||||
private botTeam: Team = { name: TeamName.Bot };
|
||||
private playerTeams: Team[] = [Team.Red, Team.Blue];
|
||||
private botTeam: Team = Team.Bot;
|
||||
|
||||
constructor(
|
||||
private _humans: PlayerInfo[],
|
||||
@@ -103,6 +99,17 @@ export class GameImpl implements Game {
|
||||
),
|
||||
);
|
||||
this.unitGrid = new UnitGrid(this._map);
|
||||
|
||||
if (_config.gameConfig().gameMode === GameMode.Team) {
|
||||
const numPlayerTeams = _config.numPlayerTeams();
|
||||
if (numPlayerTeams < 2) throw new Error("Too few teams!");
|
||||
if (numPlayerTeams >= 3) this.playerTeams.push(Team.Teal);
|
||||
if (numPlayerTeams >= 4) this.playerTeams.push(Team.Purple);
|
||||
if (numPlayerTeams >= 5) this.playerTeams.push(Team.Yellow);
|
||||
if (numPlayerTeams >= 6) this.playerTeams.push(Team.Orange);
|
||||
if (numPlayerTeams >= 7) this.playerTeams.push(Team.Green);
|
||||
if (numPlayerTeams >= 8) throw new Error("Too many teams!");
|
||||
}
|
||||
}
|
||||
|
||||
private addHumans() {
|
||||
@@ -110,7 +117,7 @@ export class GameImpl implements Game {
|
||||
this._humans.forEach((p) => this.addPlayer(p));
|
||||
return;
|
||||
}
|
||||
const playerToTeam = assignTeams(this._humans);
|
||||
const playerToTeam = assignTeams(this._humans, this.playerTeams);
|
||||
for (const [playerInfo, team] of playerToTeam.entries()) {
|
||||
if (team == "kicked") {
|
||||
console.warn(`Player ${playerInfo.name} was kicked from team`);
|
||||
@@ -555,7 +562,7 @@ export class GameImpl implements Game {
|
||||
});
|
||||
}
|
||||
|
||||
setWinner(winner: Player | TeamName, allPlayersStats: AllPlayersStats): void {
|
||||
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void {
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.Win,
|
||||
winner: typeof winner === "string" ? winner : winner.smallID(),
|
||||
@@ -684,8 +691,8 @@ export class GameImpl implements Game {
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number {
|
||||
return this._map.manhattanDist(c1, c2);
|
||||
}
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number {
|
||||
return this._map.euclideanDist(c1, c2);
|
||||
euclideanDistSquared(c1: TileRef, c2: TileRef): number {
|
||||
return this._map.euclideanDistSquared(c1, c2);
|
||||
}
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface GameMap {
|
||||
forEachTile(fn: (tile: TileRef) => void): void;
|
||||
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number;
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number;
|
||||
euclideanDistSquared(c1: TileRef, c2: TileRef): number;
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean,
|
||||
@@ -266,11 +266,10 @@ export class GameMapImpl implements GameMap {
|
||||
Math.abs(this.x(c1) - this.x(c2)) + Math.abs(this.y(c1) - this.y(c2))
|
||||
);
|
||||
}
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number {
|
||||
return Math.sqrt(
|
||||
Math.pow(this.x(c1) - this.x(c2), 2) +
|
||||
Math.pow(this.y(c1) - this.y(c2), 2),
|
||||
);
|
||||
euclideanDistSquared(c1: TileRef, c2: TileRef): number {
|
||||
const x = this.x(c1) - this.x(c2);
|
||||
const y = this.y(c1) - this.y(c2);
|
||||
return x * x + y * y;
|
||||
}
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
@@ -322,8 +321,10 @@ export function euclDistFN(
|
||||
dist: number,
|
||||
center: boolean = false,
|
||||
): (gm: GameMap, tile: TileRef) => boolean {
|
||||
const dist2 = dist * dist;
|
||||
if (!center) {
|
||||
return (gm: GameMap, n: TileRef) => gm.euclideanDist(root, n) <= dist;
|
||||
return (gm: GameMap, n: TileRef) =>
|
||||
gm.euclideanDistSquared(root, n) <= dist2;
|
||||
} else {
|
||||
return (gm: GameMap, n: TileRef) => {
|
||||
// shifts the root tile’s coordinates by -0.5 so that its “center”
|
||||
@@ -333,7 +334,7 @@ export function euclDistFN(
|
||||
const rootY = gm.y(root) - 0.5;
|
||||
const dx = gm.x(n) - rootX;
|
||||
const dy = gm.y(n) - rootY;
|
||||
return Math.sqrt(dx * dx + dy * dy) <= dist;
|
||||
return dx * dx + dy * dy <= dist2;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
NameViewData,
|
||||
PlayerID,
|
||||
PlayerType,
|
||||
TeamName,
|
||||
Team,
|
||||
Tick,
|
||||
UnitType,
|
||||
} from "./Game";
|
||||
@@ -92,7 +92,7 @@ export interface PlayerUpdate {
|
||||
name: string;
|
||||
displayName: string;
|
||||
id: PlayerID;
|
||||
teamName?: TeamName;
|
||||
team?: Team;
|
||||
smallID: number;
|
||||
playerType: PlayerType;
|
||||
isAlive: boolean;
|
||||
@@ -161,7 +161,7 @@ export interface WinUpdate {
|
||||
type: GameUpdateType.Win;
|
||||
allPlayersStats: AllPlayersStats;
|
||||
// Player id or team name.
|
||||
winner: number | TeamName;
|
||||
winner: number | Team;
|
||||
winnerType: "player" | "team";
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
PlayerInfo,
|
||||
PlayerProfile,
|
||||
PlayerType,
|
||||
TeamName,
|
||||
Team,
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
@@ -177,8 +177,8 @@ export class PlayerView {
|
||||
id(): PlayerID {
|
||||
return this.data.id;
|
||||
}
|
||||
teamName(): TeamName {
|
||||
return this.data.teamName;
|
||||
team(): Team | null {
|
||||
return this.data.team ?? null;
|
||||
}
|
||||
type(): PlayerType {
|
||||
return this.data.playerType;
|
||||
@@ -223,9 +223,7 @@ export class PlayerView {
|
||||
}
|
||||
|
||||
isOnSameTeam(other: PlayerView): boolean {
|
||||
return (
|
||||
this.data.teamName != null && this.data.teamName == other.data.teamName
|
||||
);
|
||||
return this.data.team != null && this.data.team == other.data.team;
|
||||
}
|
||||
|
||||
isFriendly(other: PlayerView): boolean {
|
||||
@@ -529,8 +527,8 @@ export class GameView implements GameMap {
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number {
|
||||
return this._map.manhattanDist(c1, c2);
|
||||
}
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number {
|
||||
return this._map.euclideanDist(c1, c2);
|
||||
euclideanDistSquared(c1: TileRef, c2: TileRef): number {
|
||||
return this._map.euclideanDistSquared(c1, c2);
|
||||
}
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
@@ -552,6 +550,8 @@ export class GameView implements GameMap {
|
||||
}
|
||||
|
||||
focusedPlayer(): PlayerView | null {
|
||||
// TODO: renable when performance issues are fixed.
|
||||
return this.myPlayer();
|
||||
if (userSettings.focusLocked()) return this.myPlayer();
|
||||
return this._focusedPlayer;
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export class PlayerImpl implements Player {
|
||||
name: this.name(),
|
||||
displayName: this.displayName(),
|
||||
id: this.id(),
|
||||
teamName: this.team()?.name,
|
||||
team: this.team(),
|
||||
smallID: this.smallID(),
|
||||
playerType: this.type(),
|
||||
isAlive: this.isAlive(),
|
||||
@@ -596,7 +596,7 @@ export class PlayerImpl implements Player {
|
||||
if (this.team() == null || other.team() == null) {
|
||||
return false;
|
||||
}
|
||||
return this._team.name == other.team().name;
|
||||
return this._team == other.team();
|
||||
}
|
||||
|
||||
isFriendly(other: Player): boolean {
|
||||
@@ -768,6 +768,12 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
nukeSpawn(tile: TileRef): TileRef | false {
|
||||
const owner = this.mg.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
if (this.isOnSameTeam(owner)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// only get missilesilos that are not on cooldown
|
||||
const spawns = this.units(UnitType.MissileSilo)
|
||||
.filter((silo) => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { PlayerInfo, Team, TeamName } from "./Game";
|
||||
import { PlayerInfo, Team } from "./Game";
|
||||
|
||||
export function assignTeams(
|
||||
players: PlayerInfo[],
|
||||
teams: Team[],
|
||||
): Map<PlayerInfo, Team | "kicked"> {
|
||||
const result = new Map<PlayerInfo, Team | "kicked">();
|
||||
let redTeamCount = 0;
|
||||
let blueTeamCount = 0;
|
||||
const teamPlayerCount = new Map<Team, number>();
|
||||
|
||||
// Group players by clan
|
||||
const clanGroups = new Map<string, PlayerInfo[]>();
|
||||
@@ -23,7 +23,7 @@ export function assignTeams(
|
||||
}
|
||||
}
|
||||
|
||||
const maxTeamSize = Math.ceil(players.length / 2);
|
||||
const maxTeamSize = Math.ceil(players.length / teams.length);
|
||||
|
||||
// Sort clans by size (largest first)
|
||||
const sortedClans = Array.from(clanGroups.entries()).sort(
|
||||
@@ -33,38 +33,38 @@ export function assignTeams(
|
||||
// First, assign clan players
|
||||
for (const [_, clanPlayers] of sortedClans) {
|
||||
// Try to keep the clan together on the team with fewer players
|
||||
if (redTeamCount <= blueTeamCount) {
|
||||
// Assign to red team
|
||||
for (const player of clanPlayers) {
|
||||
if (redTeamCount < maxTeamSize) {
|
||||
redTeamCount++;
|
||||
result.set(player, { name: TeamName.Red });
|
||||
} else {
|
||||
result.set(player, "kicked");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Assign to blue team
|
||||
for (const player of clanPlayers) {
|
||||
if (blueTeamCount < maxTeamSize) {
|
||||
blueTeamCount++;
|
||||
result.set(player, { name: TeamName.Blue });
|
||||
} else {
|
||||
result.set(player, "kicked");
|
||||
}
|
||||
let team: Team | null = null;
|
||||
let teamSize = 0;
|
||||
for (const t of teams) {
|
||||
const p = teamPlayerCount.get(t) ?? 0;
|
||||
if (team !== null && teamSize <= p) continue;
|
||||
teamSize = p;
|
||||
team = t;
|
||||
}
|
||||
|
||||
for (const player of clanPlayers) {
|
||||
if (teamSize < maxTeamSize) {
|
||||
teamSize++;
|
||||
result.set(player, team);
|
||||
} else {
|
||||
result.set(player, "kicked");
|
||||
}
|
||||
}
|
||||
teamPlayerCount.set(team, teamSize);
|
||||
}
|
||||
|
||||
// Then, assign non-clan players to balance teams
|
||||
for (const player of noClanPlayers) {
|
||||
if (redTeamCount <= blueTeamCount) {
|
||||
redTeamCount++;
|
||||
result.set(player, { name: TeamName.Red });
|
||||
} else {
|
||||
blueTeamCount++;
|
||||
result.set(player, { name: TeamName.Blue });
|
||||
let team: Team | null = null;
|
||||
let teamSize = 0;
|
||||
for (const t of teams) {
|
||||
const p = teamPlayerCount.get(t) ?? 0;
|
||||
if (team !== null && teamSize <= p) continue;
|
||||
teamSize = p;
|
||||
team = t;
|
||||
}
|
||||
teamPlayerCount.set(team, teamSize + 1);
|
||||
result.set(player, team);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -86,6 +86,8 @@ export class UnitImpl implements Unit {
|
||||
}
|
||||
this._lastTile = this._tile;
|
||||
this._tile = tile;
|
||||
this.mg.removeUnit(this);
|
||||
this.mg.addUnit(this);
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
setTroops(troops: number): void {
|
||||
|
||||
@@ -25,7 +25,9 @@ export class UserSettings {
|
||||
}
|
||||
|
||||
focusLocked() {
|
||||
return this.get("settings.focusLocked", false);
|
||||
return false;
|
||||
// TODO: renable when performance issues are fixed.
|
||||
this.get("settings.focusLocked", true);
|
||||
}
|
||||
|
||||
toggleLeftClickOpenMenu() {
|
||||
|
||||
Reference in New Issue
Block a user