mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 08:10:45 +00:00
Merge main into multi-lobby
This commit is contained in:
@@ -12,6 +12,21 @@ export const CosmeticsSchema = z.object({
|
||||
role_group: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
flag: z.object({
|
||||
layers: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
color: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
color: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
export type Cosmetics = z.infer<typeof CosmeticsSchema>;
|
||||
export const COSMETICS: Cosmetics = CosmeticsSchema.parse(cosmetics_json);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { COSMETICS } from "./CosmeticSchemas";
|
||||
|
||||
const ANIMATION_DURATIONS: Record<string, number> = {
|
||||
rainbow: 4000,
|
||||
"bright-rainbow": 4000,
|
||||
"copper-glow": 3000,
|
||||
"silver-glow": 3000,
|
||||
"gold-glow": 3000,
|
||||
neon: 3000,
|
||||
lava: 6000,
|
||||
water: 6200,
|
||||
};
|
||||
|
||||
export function renderPlayerFlag(flag: string, target: HTMLElement) {
|
||||
if (!flag.startsWith("!")) return;
|
||||
|
||||
const code = flag.slice("!".length);
|
||||
const layers = code.split("_").map((segment) => {
|
||||
const [layerKey, colorKey] = segment.split("-");
|
||||
return { layerKey, colorKey };
|
||||
});
|
||||
|
||||
target.innerHTML = "";
|
||||
target.style.overflow = "hidden";
|
||||
target.style.position = "relative";
|
||||
target.style.aspectRatio = "3/4";
|
||||
|
||||
for (const { layerKey, colorKey } of layers) {
|
||||
const layerName = COSMETICS.flag.layers[layerKey]?.name ?? layerKey;
|
||||
|
||||
const mask = `/flags/custom/${layerName}.svg`;
|
||||
if (!mask) continue;
|
||||
|
||||
const layer = document.createElement("div");
|
||||
layer.style.position = "absolute";
|
||||
layer.style.top = "0";
|
||||
layer.style.left = "0";
|
||||
layer.style.width = "100%";
|
||||
layer.style.height = "100%";
|
||||
|
||||
const colorValue = COSMETICS.flag.color[colorKey]?.color ?? colorKey;
|
||||
const isSpecial =
|
||||
!colorValue.startsWith("#") &&
|
||||
!/^([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(colorValue);
|
||||
|
||||
if (isSpecial) {
|
||||
const duration = ANIMATION_DURATIONS[colorValue] ?? 5000;
|
||||
const now = performance.now();
|
||||
const offset = now % duration;
|
||||
if (!duration) console.warn(`No animation duration for: ${colorValue}`);
|
||||
layer.classList.add(`flag-color-${colorValue}`);
|
||||
layer.style.animationDelay = `-${offset}ms`;
|
||||
} else {
|
||||
layer.style.backgroundColor = colorValue;
|
||||
}
|
||||
|
||||
layer.style.maskImage = `url(${mask})`;
|
||||
layer.style.maskRepeat = "no-repeat";
|
||||
layer.style.maskPosition = "center";
|
||||
layer.style.maskSize = "contain";
|
||||
|
||||
layer.style.webkitMaskImage = `url(${mask})`;
|
||||
layer.style.webkitMaskRepeat = "no-repeat";
|
||||
layer.style.webkitMaskPosition = "center";
|
||||
layer.style.webkitMaskSize = "contain";
|
||||
|
||||
target.appendChild(layer);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -171,7 +171,7 @@ export const FlagSchema = z.string().max(128).optional();
|
||||
export const RequiredPatternSchema = z
|
||||
.string()
|
||||
.max(128)
|
||||
.base64()
|
||||
.base64url()
|
||||
.refine(
|
||||
(val) => {
|
||||
try {
|
||||
|
||||
@@ -98,10 +98,10 @@ export const PlayerStatsSchema = z
|
||||
.object({
|
||||
attacks: AtLeastOneNumberSchema.optional(),
|
||||
betrayals: BigIntStringSchema.optional(),
|
||||
boats: z.record(BoatUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
bombs: z.record(BombUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
boats: z.partialRecord(BoatUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
bombs: z.partialRecord(BombUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
gold: AtLeastOneNumberSchema.optional(),
|
||||
units: z.record(OtherUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
units: z.partialRecord(OtherUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
})
|
||||
.optional();
|
||||
export type PlayerStats = z.infer<typeof PlayerStatsSchema>;
|
||||
|
||||
@@ -153,6 +153,8 @@ export interface Config {
|
||||
traitorDuration(): number;
|
||||
nukeMagnitudes(unitType: UnitType): NukeMagnitude;
|
||||
defaultNukeSpeed(): number;
|
||||
defaultNukeTargetableRange(): number;
|
||||
defaultSamRange(): number;
|
||||
nukeDeathFactor(humans: number, tilesOwned: number): number;
|
||||
structureMinDist(): number;
|
||||
isReplay(): boolean;
|
||||
|
||||
@@ -302,7 +302,7 @@ export class DefaultConfig implements Config {
|
||||
return Math.min(50, Math.round(10 * Math.pow(numberOfPorts, 0.6)));
|
||||
}
|
||||
trainSpawnRate(numberOfStations: number): number {
|
||||
return Math.round(50 * Math.pow(numberOfStations, 0.8));
|
||||
return Math.min(1400, Math.round(60 * Math.pow(numberOfStations, 0.8)));
|
||||
}
|
||||
trainGold(): Gold {
|
||||
return BigInt(10_000);
|
||||
@@ -363,6 +363,7 @@ export class DefaultConfig implements Config {
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
|
||||
upgradable: true,
|
||||
canBuildTrainStation: true,
|
||||
};
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
@@ -452,6 +453,7 @@ export class DefaultConfig implements Config {
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
|
||||
upgradable: true,
|
||||
canBuildTrainStation: true,
|
||||
};
|
||||
case UnitType.Factory:
|
||||
return {
|
||||
@@ -466,6 +468,7 @@ export class DefaultConfig implements Config {
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
|
||||
canBuildTrainStation: true,
|
||||
};
|
||||
case UnitType.Construction:
|
||||
return {
|
||||
@@ -790,6 +793,14 @@ export class DefaultConfig implements Config {
|
||||
return 6;
|
||||
}
|
||||
|
||||
defaultNukeTargetableRange(): number {
|
||||
return 120;
|
||||
}
|
||||
|
||||
defaultSamRange(): number {
|
||||
return 80;
|
||||
}
|
||||
|
||||
// Humans can be population, soldiers attacking, soldiers in boat etc.
|
||||
nukeDeathFactor(humans: number, tilesOwned: number): number {
|
||||
return (5 * humans) / Math.max(1, tilesOwned);
|
||||
|
||||
@@ -13,8 +13,6 @@ import { ParabolaPathFinder } from "../pathfinding/PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { NukeType } from "../StatsSchemas";
|
||||
|
||||
const NUKE_TARGETABLE_RADIUS = 120;
|
||||
|
||||
const SPRITE_RADIUS = 16;
|
||||
|
||||
export class NukeExecution implements Execution {
|
||||
@@ -179,7 +177,9 @@ export class NukeExecution implements Execution {
|
||||
if (this.nuke === null || this.nuke.targetTile() === undefined) {
|
||||
return;
|
||||
}
|
||||
const targetRangeSquared = NUKE_TARGETABLE_RADIUS * NUKE_TARGETABLE_RADIUS;
|
||||
const targetRangeSquared =
|
||||
this.mg.config().defaultNukeTargetableRange() *
|
||||
this.mg.config().defaultNukeTargetableRange();
|
||||
const targetTile = this.nuke.targetTile();
|
||||
this.nuke.setTargetable(
|
||||
this.mg.euclideanDistSquared(this.nuke.tile(), targetTile!) <
|
||||
|
||||
@@ -14,8 +14,6 @@ export class SAMLauncherExecution implements Execution {
|
||||
private mg: Game;
|
||||
private active: boolean = true;
|
||||
|
||||
private searchRangeRadius = 80;
|
||||
private targetRangeRadius = 120; // Nuke's target should be in this range to be focusable
|
||||
// As MIRV go very fast we have to detect them very early but we only
|
||||
// shoot the one targeting very close (MIRVWarheadProtectionRadius)
|
||||
private MIRVWarheadSearchRadius = 400;
|
||||
@@ -41,7 +39,7 @@ export class SAMLauncherExecution implements Execution {
|
||||
if (this.sam === null) return null;
|
||||
const nukes = this.mg.nearbyUnits(
|
||||
this.sam.tile(),
|
||||
this.searchRangeRadius,
|
||||
this.mg.config().defaultSamRange(),
|
||||
[UnitType.AtomBomb, UnitType.HydrogenBomb],
|
||||
({ unit }) =>
|
||||
unit.owner() !== this.player &&
|
||||
|
||||
@@ -132,6 +132,7 @@ export interface UnitInfo {
|
||||
damage?: number;
|
||||
constructionDuration?: number;
|
||||
upgradable?: boolean;
|
||||
canBuildTrainStation?: boolean;
|
||||
}
|
||||
|
||||
export enum UnitType {
|
||||
|
||||
@@ -684,8 +684,14 @@ export class GameImpl implements Game {
|
||||
tile: TileRef,
|
||||
searchRange: number,
|
||||
types: UnitType | UnitType[],
|
||||
predicate?: (value: { unit: Unit; distSquared: number }) => boolean,
|
||||
): Array<{ unit: Unit; distSquared: number }> {
|
||||
return this.unitGrid.nearbyUnits(tile, searchRange, types) as Array<{
|
||||
return this.unitGrid.nearbyUnits(
|
||||
tile,
|
||||
searchRange,
|
||||
types,
|
||||
predicate,
|
||||
) as Array<{
|
||||
unit: Unit;
|
||||
distSquared: number;
|
||||
}>;
|
||||
|
||||
@@ -401,6 +401,39 @@ export function rectDistFN(
|
||||
}
|
||||
}
|
||||
|
||||
function isInIsometricTile(
|
||||
center: { x: number; y: number },
|
||||
tile: { x: number; y: number },
|
||||
yOffset: number,
|
||||
distance: number,
|
||||
): boolean {
|
||||
const dx = Math.abs(tile.x - center.x);
|
||||
const dy = Math.abs(tile.y - (center.y + yOffset));
|
||||
return dx + dy * 2 <= distance + 1;
|
||||
}
|
||||
|
||||
export function isometricDistFN(
|
||||
root: TileRef,
|
||||
dist: number,
|
||||
center: boolean = false,
|
||||
): (gm: GameMap, tile: TileRef) => boolean {
|
||||
if (!center) {
|
||||
return (gm: GameMap, n: TileRef) => gm.manhattanDist(root, n) <= dist;
|
||||
} else {
|
||||
return (gm: GameMap, n: TileRef) => {
|
||||
const rootX = gm.x(root) - 0.5;
|
||||
const rootY = gm.y(root) - 0.5;
|
||||
|
||||
return isInIsometricTile(
|
||||
{ x: rootX, y: rootY },
|
||||
{ x: gm.x(n), y: gm.y(n) },
|
||||
0,
|
||||
dist,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function hexDistFN(
|
||||
root: TileRef,
|
||||
dist: number,
|
||||
|
||||
@@ -88,6 +88,8 @@ export function createRailNetwork(game: Game): RailNetwork {
|
||||
}
|
||||
|
||||
export class RailNetworkImpl implements RailNetwork {
|
||||
private maxConnectionDistance: number = 4;
|
||||
|
||||
constructor(
|
||||
private game: Game,
|
||||
private stationManager: StationManager,
|
||||
@@ -142,12 +144,20 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
const neighborStation = this.stationManager.findStation(neighbor.unit);
|
||||
if (!neighborStation) continue;
|
||||
|
||||
const neighborCluster = neighborStation.getCluster();
|
||||
if (!neighborCluster || neighborCluster.has(station)) continue;
|
||||
const distanceToStation = this.distanceFrom(
|
||||
neighborStation,
|
||||
station,
|
||||
this.maxConnectionDistance,
|
||||
);
|
||||
|
||||
const neighborCluster = neighborStation.getCluster();
|
||||
if (neighborCluster === null) continue;
|
||||
const connectionAvailable =
|
||||
distanceToStation > this.maxConnectionDistance ||
|
||||
distanceToStation === -1;
|
||||
if (
|
||||
neighbor.distSquared >
|
||||
this.game.config().trainStationMinRange() ** 2
|
||||
connectionAvailable &&
|
||||
neighbor.distSquared > this.game.config().trainStationMinRange() ** 2
|
||||
) {
|
||||
if (this.connect(station, neighborStation)) {
|
||||
neighborCluster.addStation(station);
|
||||
@@ -196,6 +206,37 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
return false;
|
||||
}
|
||||
|
||||
private distanceFrom(
|
||||
start: TrainStation,
|
||||
dest: TrainStation,
|
||||
maxDistance: number,
|
||||
): number {
|
||||
if (start === dest) return 0;
|
||||
|
||||
const visited = new Set<TrainStation>();
|
||||
const queue: Array<{ station: TrainStation; distance: number }> = [
|
||||
{ station: start, distance: 0 },
|
||||
];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { station, distance } = queue.shift()!;
|
||||
if (visited.has(station)) continue;
|
||||
visited.add(station);
|
||||
|
||||
if (distance >= maxDistance) continue;
|
||||
|
||||
for (const neighbor of station.neighbors()) {
|
||||
if (neighbor === dest) return distance + 1;
|
||||
if (!visited.has(neighbor)) {
|
||||
queue.push({ station: neighbor, distance: distance + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If destination not found within maxDistance
|
||||
return -1;
|
||||
}
|
||||
|
||||
private computeCluster(start: TrainStation): Set<TrainStation> {
|
||||
const visited = new Set<TrainStation>();
|
||||
const queue = [start];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import FastPriorityQueue from "fastpriorityqueue";
|
||||
import { AStar, PathFindResultType } from "./AStar";
|
||||
|
||||
/**
|
||||
@@ -12,11 +12,11 @@ export interface GraphAdapter<NodeType> {
|
||||
}
|
||||
|
||||
export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
private fwdOpenSet: PriorityQueue<{
|
||||
private fwdOpenSet: FastPriorityQueue<{
|
||||
tile: NodeType;
|
||||
fScore: number;
|
||||
}>;
|
||||
private bwdOpenSet: PriorityQueue<{
|
||||
private bwdOpenSet: FastPriorityQueue<{
|
||||
tile: NodeType;
|
||||
fScore: number;
|
||||
}>;
|
||||
@@ -39,15 +39,15 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
private graph: GraphAdapter<NodeType>,
|
||||
private directionChangePenalty: number = 0,
|
||||
) {
|
||||
this.fwdOpenSet = new PriorityQueue((a, b) => a.fScore - b.fScore);
|
||||
this.bwdOpenSet = new PriorityQueue((a, b) => a.fScore - b.fScore);
|
||||
this.fwdOpenSet = new FastPriorityQueue((a, b) => a.fScore < b.fScore);
|
||||
this.bwdOpenSet = new FastPriorityQueue((a, b) => a.fScore < b.fScore);
|
||||
this.sources = Array.isArray(src) ? src : [src];
|
||||
this.closestSource = this.findClosestSource(dst);
|
||||
|
||||
// Initialize forward search with source point(s)
|
||||
this.sources.forEach((startPoint) => {
|
||||
this.fwdGScore.set(startPoint, 0);
|
||||
this.fwdOpenSet.enqueue({
|
||||
this.fwdOpenSet.add({
|
||||
tile: startPoint,
|
||||
fScore: this.heuristic(startPoint, dst),
|
||||
});
|
||||
@@ -55,7 +55,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
|
||||
// Initialize backward search from destination
|
||||
this.bwdGScore.set(dst, 0);
|
||||
this.bwdOpenSet.enqueue({
|
||||
this.bwdOpenSet.add({
|
||||
tile: dst,
|
||||
fScore: this.heuristic(dst, this.findClosestSource(dst)),
|
||||
});
|
||||
@@ -85,7 +85,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
}
|
||||
|
||||
// Process forward search
|
||||
const fwdCurrent = this.fwdOpenSet.dequeue()!.tile;
|
||||
const fwdCurrent = this.fwdOpenSet.poll()!.tile;
|
||||
|
||||
// Check if we've found a meeting point
|
||||
if (this.bwdGScore.has(fwdCurrent)) {
|
||||
@@ -96,7 +96,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
this.expandNode(fwdCurrent, true);
|
||||
|
||||
// Process backward search
|
||||
const bwdCurrent = this.bwdOpenSet.dequeue()!.tile;
|
||||
const bwdCurrent = this.bwdOpenSet.poll()!.tile;
|
||||
|
||||
// Check if we've found a meeting point
|
||||
if (this.fwdGScore.has(bwdCurrent)) {
|
||||
@@ -145,7 +145,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
const fScore =
|
||||
totalG +
|
||||
this.heuristic(neighbor, isForward ? this.dst : this.closestSource);
|
||||
openSet.enqueue({ tile: neighbor, fScore: fScore });
|
||||
openSet.add({ tile: neighbor, fScore: fScore });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
private heuristic(a: NodeType, b: NodeType): number {
|
||||
const posA = this.graph.position(a);
|
||||
const posB = this.graph.position(b);
|
||||
return 1.1 * (Math.abs(posA.x - posB.x) + Math.abs(posA.y - posB.y));
|
||||
return 2 * (Math.abs(posA.x - posB.x) + Math.abs(posA.y - posB.y));
|
||||
}
|
||||
|
||||
private getDirection(from: NodeType, to: NodeType): string {
|
||||
|
||||
Reference in New Issue
Block a user