mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:45 +00:00
bugfix: port construction bar completes early; renderer now reads durations from Config
The renderer kept a parallel CONSTRUCTION_DURATIONS table in src/client/render/GameConstants.ts that had drifted from Config: port showed as 20 ticks but the simulation builds it in 50, so the bar hit 100% and idled for 30 ticks. SAM/silo cooldown constants were also stale (120/75 vs Config's 90/90), making the missile-readiness bar slightly wrong too. Delete GameConstants.ts entirely. Thread the Config instance through WebGLGameView → GPURenderer → BarPass / FxPass / FxSpritePass / WorldTextPass; passes call config.unitInfo(...).constructionDuration, config.SAMCooldown(), config.deletionMarkDuration(), config.msPerTick() directly. Add Config.msPerTick() since no method existed for it. Move the visual-only NUKE_EXPLOSION_RADII (not a game-logic value) into FxSpritePass where it's used.
This commit is contained in:
@@ -241,7 +241,10 @@ export function joinLobby(
|
||||
|
||||
// Build the WebGL view + its glCanvas. Must run before createRenderer so the
|
||||
// controllers can be wired directly to the view.
|
||||
function createWebGLView(terrainMap: TerrainMapData): {
|
||||
function createWebGLView(
|
||||
terrainMap: TerrainMapData,
|
||||
config: Config,
|
||||
): {
|
||||
view: WebGLGameView;
|
||||
glCanvas: HTMLCanvasElement;
|
||||
cachedWebGLFrameCallback: { current: FrameRequestCallback | null };
|
||||
@@ -295,6 +298,7 @@ function createWebGLView(terrainMap: TerrainMapData): {
|
||||
},
|
||||
terrainBytes,
|
||||
palette,
|
||||
config,
|
||||
captureRaf,
|
||||
captureCaf,
|
||||
);
|
||||
@@ -463,8 +467,10 @@ async function createClientGame(
|
||||
|
||||
const soundManager = new SoundManager(eventBus, userSettings);
|
||||
try {
|
||||
const { view, glCanvas, cachedWebGLFrameCallback } =
|
||||
createWebGLView(gameMap);
|
||||
const { view, glCanvas, cachedWebGLFrameCallback } = createWebGLView(
|
||||
gameMap,
|
||||
config,
|
||||
);
|
||||
|
||||
// Bind the WebGL renderer's day/night mode to the existing darkMode
|
||||
// UserSetting so the in-game map matches the rest of the UI. Initial
|
||||
|
||||
@@ -37,7 +37,6 @@ each frame (and animate from local time, e.g. the spawn-overlay breath).
|
||||
|
||||
| Path | Purpose |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GameConstants.ts` | Top-level constants shared across passes (`MS_PER_TICK`, nuke radii, etc.) |
|
||||
| `types/` | Shared TS interfaces: `FrameData`, `UnitState`, `PlayerState`, `RendererConfig`, pass-input shapes (`GhostPreviewData`, `NukeTrajectoryData`, `SpawnCenter`, …) |
|
||||
| `frame/` | Frame-data accumulators + per-tick derivations (CPU-side, no GL) |
|
||||
| `frame/derive/` | Pure derivations that turn raw simulation state into renderer-ready shapes (attack rings, alliance clusters, relation matrix, player status, nuke telegraphs) |
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
/**
|
||||
* game-constants.ts — Upstream game facts replicated in the renderer/shim.
|
||||
*
|
||||
* All values here are sourced from upstream game code. When upstream changes,
|
||||
* audit this file first.
|
||||
*
|
||||
* Primary sources:
|
||||
* - vendor/openfront/src/core/configuration/DefaultConfig.ts (DefaultConfig, DefaultServerConfig)
|
||||
* - vendor/openfront/src/client/hud/layers/FxLayer.ts (visual-only constants)
|
||||
*/
|
||||
|
||||
import {
|
||||
UT_ATOM_BOMB,
|
||||
UT_CITY,
|
||||
UT_DEFENSE_POST,
|
||||
UT_FACTORY,
|
||||
UT_HYDROGEN_BOMB,
|
||||
UT_MIRV_WARHEAD,
|
||||
UT_MISSILE_SILO,
|
||||
UT_PORT,
|
||||
UT_SAM_LAUNCHER,
|
||||
} from "./types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tick timing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Milliseconds per game tick.
|
||||
* Source: DefaultServerConfig.turnIntervalMs() → return 100
|
||||
*/
|
||||
export const MS_PER_TICK = 100;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unit health
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Maximum health for a Warship unit.
|
||||
* Source: DefaultConfig.unitInfo(UnitType.Warship) → { maxHealth: 1000 }
|
||||
*/
|
||||
export const WARSHIP_MAX_HEALTH = 1000;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Construction durations (ticks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* How many ticks each structure type takes to finish construction.
|
||||
* Source: DefaultConfig.unitInfo(type).constructionDuration (non-instantBuild path):
|
||||
* case UnitType.City: constructionDuration: 2 * 10
|
||||
* case UnitType.Port: constructionDuration: 2 * 10
|
||||
* case UnitType.Factory: constructionDuration: 2 * 10
|
||||
* case UnitType.DefensePost: constructionDuration: 5 * 10
|
||||
* case UnitType.MissileSilo: constructionDuration: 10 * 10
|
||||
* case UnitType.SAMLauncher: constructionDuration: 30 * 10
|
||||
*/
|
||||
export const CONSTRUCTION_DURATIONS: Readonly<Record<string, number>> = {
|
||||
[UT_CITY]: 2 * 10,
|
||||
[UT_PORT]: 2 * 10,
|
||||
[UT_FACTORY]: 2 * 10,
|
||||
[UT_DEFENSE_POST]: 5 * 10,
|
||||
[UT_MISSILE_SILO]: 10 * 10,
|
||||
[UT_SAM_LAUNCHER]: 30 * 10,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Missile cooldowns (ticks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ticks for a SAM Launcher to reload one missile.
|
||||
* Source: DefaultConfig.SAMCooldown() → return 120
|
||||
* NOTE: different from SiloCooldown — do not conflate.
|
||||
*/
|
||||
export const SAM_COOLDOWN_TICKS = 120;
|
||||
|
||||
/**
|
||||
* Ticks for a Missile Silo to reload one missile.
|
||||
* Source: DefaultConfig.SiloCooldown() → return 75
|
||||
*/
|
||||
export const SILO_COOLDOWN_TICKS = 75;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deletion mark duration (ticks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* How many ticks a structure remains in the "marked for deletion" state.
|
||||
* Source: DefaultConfig.deletionMarkDuration() → return 30 * 10
|
||||
*/
|
||||
export const DELETION_MARK_DURATION = 30 * 10;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nuke explosion visual radii (tiles)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Visual explosion radius (tiles) for each nuke type, used for shockwave and
|
||||
* debris scatter sizing.
|
||||
*
|
||||
* Source: FxLayer.ts, inside the unit-death event handler:
|
||||
* case UnitType.AtomBomb: this.onNukeEvent(unit, 70)
|
||||
* case UnitType.MIRVWarhead: this.onNukeEvent(unit, 70)
|
||||
* case UnitType.HydrogenBomb: this.onNukeEvent(unit, 160)
|
||||
*
|
||||
* Note: these are visual-only radii. The gameplay damage radii are separate
|
||||
* and come from DefaultConfig.nukeMagnitudes() → { inner, outer }.
|
||||
*/
|
||||
export const NUKE_EXPLOSION_RADII: Readonly<Record<string, number>> = {
|
||||
[UT_ATOM_BOMB]: 70,
|
||||
[UT_HYDROGEN_BOMB]: 160,
|
||||
[UT_MIRV_WARHEAD]: 70,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SAM range formula
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* SAM Launcher coverage radius in tiles at a given upgrade level.
|
||||
* Source: DefaultConfig.samRange(level):
|
||||
* return this.maxSamRange() - 480 / (level + 5)
|
||||
* where maxSamRange() → return 150
|
||||
*/
|
||||
export function samRange(level: number): number {
|
||||
return 150 - 480 / (level + 5);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Missile readiness formula
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fractional missile readiness [0, 1] for a Silo or SAM Launcher.
|
||||
* Returns 1.0 when fully loaded, 0.0 when completely empty with no partial reload.
|
||||
*
|
||||
* Source: adapted from upstream readiness display logic (UILayer / FxLayer).
|
||||
* Uses per-type cooldown: SAMCooldown() = 120, SiloCooldown() = 75.
|
||||
*/
|
||||
export function missileReadiness(
|
||||
unitType: string,
|
||||
level: number,
|
||||
missileTimerQueue: number[],
|
||||
gameTick: number,
|
||||
): number {
|
||||
const cooldown =
|
||||
unitType === UT_SAM_LAUNCHER ? SAM_COOLDOWN_TICKS : SILO_COOLDOWN_TICKS;
|
||||
const maxMissiles = level;
|
||||
const reloading = missileTimerQueue.length;
|
||||
if (reloading === 0) return 1;
|
||||
|
||||
const ready = maxMissiles - reloading;
|
||||
if (ready === 0 && maxMissiles > 1) return 0;
|
||||
|
||||
let readiness = ready / maxMissiles;
|
||||
for (const timer of missileTimerQueue) {
|
||||
const progress = gameTick - timer;
|
||||
const ratio = progress / cooldown;
|
||||
readiness += ratio / maxMissiles;
|
||||
}
|
||||
return Math.max(0, Math.min(1, readiness));
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
* Consumers only touch GameView — they never import GPURenderer or Camera.
|
||||
*/
|
||||
|
||||
import type { Config } from "../../../core/configuration/Config";
|
||||
import type {
|
||||
AttackRingInput,
|
||||
BonusEvent,
|
||||
@@ -51,6 +52,7 @@ export class GameView {
|
||||
private header: RendererConfig,
|
||||
private terrainBytes: Uint8Array,
|
||||
private paletteData: Float32Array,
|
||||
private config: Config,
|
||||
private raf?: typeof requestAnimationFrame,
|
||||
private caf?: typeof cancelAnimationFrame,
|
||||
) {
|
||||
@@ -78,6 +80,7 @@ export class GameView {
|
||||
this.header,
|
||||
this.terrainBytes,
|
||||
this.paletteData,
|
||||
this.config,
|
||||
this.raf,
|
||||
this.caf,
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* structure levels → bars → bloom → trails → missiles → fx → conquest → names
|
||||
*/
|
||||
|
||||
import type { Config } from "../../../core/configuration/Config";
|
||||
import type {
|
||||
AttackRingInput,
|
||||
BonusEvent,
|
||||
@@ -185,6 +186,7 @@ export class GPURenderer {
|
||||
header: RendererConfig,
|
||||
terrainBytes: Uint8Array,
|
||||
paletteData: Float32Array,
|
||||
config: Config,
|
||||
raf: typeof requestAnimationFrame = requestAnimationFrame.bind(window),
|
||||
caf: typeof cancelAnimationFrame = cancelAnimationFrame.bind(window),
|
||||
) {
|
||||
@@ -435,9 +437,9 @@ export class GPURenderer {
|
||||
this.structureLevelPass = new StructureLevelPass(gl, header, this.settings);
|
||||
this.unitPass = new UnitPass(gl, header, this.paletteTex, this.settings);
|
||||
this.namePass = new NamePass(gl, header, paletteData, this.settings);
|
||||
this.fxPass = new FxPass(gl, header, this.settings);
|
||||
this.barPass = new BarPass(gl, header, this.settings);
|
||||
this.worldTextPass = new WorldTextPass(gl, this.settings);
|
||||
this.fxPass = new FxPass(gl, header, this.settings, config);
|
||||
this.barPass = new BarPass(gl, header, this.settings, config);
|
||||
this.worldTextPass = new WorldTextPass(gl, this.settings, config);
|
||||
this.worldTextPass.setMapWidth(this.mapW);
|
||||
this.radialMenuPass = new RadialMenuPass(gl);
|
||||
this.selectionBoxPass = new SelectionBoxPass(gl);
|
||||
|
||||
@@ -10,12 +10,8 @@
|
||||
* → instance VBO (x, y, progress) → GPU colored rectangle
|
||||
*/
|
||||
|
||||
import {
|
||||
CONSTRUCTION_DURATIONS,
|
||||
DELETION_MARK_DURATION,
|
||||
missileReadiness,
|
||||
WARSHIP_MAX_HEALTH,
|
||||
} from "../../GameConstants";
|
||||
import type { Config } from "../../../../core/configuration/Config";
|
||||
import { UnitType } from "../../../../core/game/Game";
|
||||
import type { RendererConfig, UnitState } from "../../types";
|
||||
import { UT_MISSILE_SILO, UT_SAM_LAUNCHER } from "../../types";
|
||||
import type { RenderSettings } from "../RenderSettings";
|
||||
@@ -60,15 +56,18 @@ export class BarPass {
|
||||
private progressCount = 0;
|
||||
|
||||
private mapW: number;
|
||||
private warshipMaxHealth: number;
|
||||
|
||||
constructor(
|
||||
gl: WebGL2RenderingContext,
|
||||
header: RendererConfig,
|
||||
settings: RenderSettings,
|
||||
private config: Config,
|
||||
) {
|
||||
this.gl = gl;
|
||||
this.settings = settings;
|
||||
this.mapW = header.mapWidth;
|
||||
this.warshipMaxHealth = config.unitInfo(UnitType.Warship).maxHealth ?? 0;
|
||||
|
||||
// --- Shader program ---
|
||||
this.program = createProgram(gl, barVertSrc, barFragSrc);
|
||||
@@ -130,10 +129,10 @@ export class BarPass {
|
||||
if (
|
||||
unit.health === null ||
|
||||
unit.health <= 0 ||
|
||||
unit.health >= WARSHIP_MAX_HEALTH
|
||||
unit.health >= this.warshipMaxHealth
|
||||
)
|
||||
continue;
|
||||
this.pushHealth(unit, unit.health / WARSHIP_MAX_HEALTH);
|
||||
this.pushHealth(unit, unit.health / this.warshipMaxHealth);
|
||||
}
|
||||
|
||||
// --- Progress bars (structures) ---
|
||||
@@ -234,12 +233,17 @@ export class BarPass {
|
||||
// Deletion progress (reverse countdown — takes priority over other bars)
|
||||
if (unit.markedForDeletion !== false) {
|
||||
const remaining = unit.markedForDeletion - gameTick;
|
||||
return Math.max(0, Math.min(1, remaining / DELETION_MARK_DURATION));
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(1, remaining / this.config.deletionMarkDuration()),
|
||||
);
|
||||
}
|
||||
|
||||
// Construction progress
|
||||
if (unit.underConstruction && unit.constructionStartTick !== null) {
|
||||
const duration = CONSTRUCTION_DURATIONS[unit.unitType] ?? 50;
|
||||
const duration =
|
||||
this.config.unitInfo(unit.unitType as UnitType).constructionDuration ??
|
||||
50;
|
||||
const elapsed = gameTick - unit.constructionStartTick;
|
||||
return Math.min(1, Math.max(0, elapsed / duration));
|
||||
}
|
||||
@@ -249,15 +253,31 @@ export class BarPass {
|
||||
unit.unitType === UT_MISSILE_SILO ||
|
||||
unit.unitType === UT_SAM_LAUNCHER
|
||||
) {
|
||||
const readiness = missileReadiness(
|
||||
unit.unitType,
|
||||
unit.level,
|
||||
unit.missileTimerQueue,
|
||||
gameTick,
|
||||
);
|
||||
const readiness = this.missileReadiness(unit, gameTick);
|
||||
if (readiness < 1) return readiness;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private missileReadiness(unit: UnitState, gameTick: number): number {
|
||||
const maxMissiles = unit.level;
|
||||
const reloading = unit.missileTimerQueue.length;
|
||||
if (reloading === 0) return 1;
|
||||
|
||||
const ready = maxMissiles - reloading;
|
||||
if (ready === 0 && maxMissiles > 1) return 0;
|
||||
|
||||
const cooldown =
|
||||
unit.unitType === UT_SAM_LAUNCHER
|
||||
? this.config.SAMCooldown()
|
||||
: this.config.SiloCooldown();
|
||||
|
||||
let readiness = ready / maxMissiles;
|
||||
for (const timer of unit.missileTimerQueue) {
|
||||
const progress = gameTick - timer;
|
||||
readiness += progress / cooldown / maxMissiles;
|
||||
}
|
||||
return Math.max(0, Math.min(1, readiness));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* - Ghost cost label: persistent build-cost number under the ghost cursor
|
||||
*/
|
||||
|
||||
import type { Config } from "../../../../core/configuration/Config";
|
||||
import type { BonusEvent, ConquestFx } from "../../types";
|
||||
import type { RenderSettings } from "../RenderSettings";
|
||||
import { createProgram } from "../utils/GlUtils";
|
||||
@@ -31,8 +32,6 @@ const atlasUrl = assetUrl("atlases/msdf-atlas.png");
|
||||
const FLOATS_PER_INSTANCE = 10;
|
||||
const BYTES_PER_INSTANCE = FLOATS_PER_INSTANCE * 4;
|
||||
const CONQUEST_LIFETIME_MS = 2500;
|
||||
/** Nominal game tick rate — 100ms per tick. */
|
||||
const MS_PER_TICK = 100;
|
||||
/** Tiles below conquered name location (matches upstream DynamicUILayer). */
|
||||
const CONQUEST_Y_OFFSET = 8;
|
||||
/** World-space font size for conquest popups. */
|
||||
@@ -152,7 +151,11 @@ export class WorldTextPass {
|
||||
return this.timeFn();
|
||||
}
|
||||
|
||||
constructor(gl: WebGL2RenderingContext, settings: RenderSettings) {
|
||||
constructor(
|
||||
gl: WebGL2RenderingContext,
|
||||
settings: RenderSettings,
|
||||
private config: Config,
|
||||
) {
|
||||
this.gl = gl;
|
||||
this.settings = settings;
|
||||
|
||||
@@ -273,7 +276,7 @@ export class WorldTextPass {
|
||||
applyConquestEvents(events: ConquestFx[]): void {
|
||||
const now = this.now();
|
||||
for (const evt of events) {
|
||||
const startMs = now - (evt.tickAge ?? 0) * MS_PER_TICK;
|
||||
const startMs = now - (evt.tickAge ?? 0) * this.config.msPerTick();
|
||||
if (now - startMs >= CONQUEST_LIFETIME_MS) continue;
|
||||
this.active.push({
|
||||
x: evt.x,
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
* Pre-built by generate-sprite-atlases.mjs.
|
||||
*/
|
||||
|
||||
import { MS_PER_TICK, NUKE_EXPLOSION_RADII } from "../../../GameConstants";
|
||||
import type { Config } from "../../../../../core/configuration/Config";
|
||||
import type { ConquestFx, DeadUnitFx, RendererConfig } from "../../../types";
|
||||
import {
|
||||
STRUCTURE_TYPES,
|
||||
UT_ATOM_BOMB,
|
||||
UT_HYDROGEN_BOMB,
|
||||
UT_MIRV_WARHEAD,
|
||||
UT_SHELL,
|
||||
UT_TRAIN,
|
||||
UT_WARSHIP,
|
||||
@@ -130,6 +133,17 @@ const FX_CONFIG: FxTypeConfig[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nuke explosion radii — visual-only (FxLayer source, not Config). These are
|
||||
// the shockwave/debris scatter sizes, not the gameplay damage radii.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const NUKE_EXPLOSION_RADII: Readonly<Record<string, number>> = {
|
||||
[UT_ATOM_BOMB]: 70,
|
||||
[UT_HYDROGEN_BOMB]: 160,
|
||||
[UT_MIRV_WARHEAD]: 70,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nuke debris plan
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -196,6 +210,7 @@ export class FxSpritePass {
|
||||
gl: WebGL2RenderingContext,
|
||||
header: RendererConfig,
|
||||
settings: RenderSettings,
|
||||
private config: Config,
|
||||
) {
|
||||
this.gl = gl;
|
||||
this.mapW = header.mapWidth;
|
||||
@@ -319,7 +334,7 @@ export class FxSpritePass {
|
||||
const now = this.timeFn();
|
||||
const fx = this.settings.fx;
|
||||
for (const evt of events) {
|
||||
const startMs = now - (evt.tickAge ?? 0) * MS_PER_TICK;
|
||||
const startMs = now - (evt.tickAge ?? 0) * this.config.msPerTick();
|
||||
if (now - startMs >= fx.conquestLifetimeMs) continue;
|
||||
this.activeFx.push({
|
||||
x: evt.x,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* interceptions) are coordinated here so each sub-pass stays self-contained.
|
||||
*/
|
||||
|
||||
import { MS_PER_TICK, NUKE_EXPLOSION_RADII } from "../../../GameConstants";
|
||||
import type { Config } from "../../../../../core/configuration/Config";
|
||||
import type {
|
||||
AttackRingInput,
|
||||
ConquestFx,
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
import type { RenderSettings } from "../../RenderSettings";
|
||||
import { FxAttackRingPass } from "./FxAttackRingPass";
|
||||
import { FxShockwavePass } from "./FxShockwavePass";
|
||||
import { FxSpritePass } from "./FxSpritePass";
|
||||
import { FxSpritePass, NUKE_EXPLOSION_RADII } from "./FxSpritePass";
|
||||
|
||||
export type { AttackRingInput } from "../../../types";
|
||||
|
||||
@@ -33,9 +33,10 @@ export class FxPass {
|
||||
gl: WebGL2RenderingContext,
|
||||
header: RendererConfig,
|
||||
settings: RenderSettings,
|
||||
private config: Config,
|
||||
) {
|
||||
this.mapW = header.mapWidth;
|
||||
this.spritePass = new FxSpritePass(gl, header, settings);
|
||||
this.spritePass = new FxSpritePass(gl, header, settings, config);
|
||||
this.shockwavePass = new FxShockwavePass(gl, settings);
|
||||
this.attackRingPass = new FxAttackRingPass(gl, settings);
|
||||
}
|
||||
@@ -47,7 +48,7 @@ export class FxPass {
|
||||
applyDeadUnits(deadUnits: DeadUnitFx[]): void {
|
||||
const now = this.timeFn();
|
||||
for (const unit of deadUnits) {
|
||||
const startMs = now - (unit.tickAge ?? 0) * MS_PER_TICK;
|
||||
const startMs = now - (unit.tickAge ?? 0) * this.config.msPerTick();
|
||||
this.spawnUnit(unit, startMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,9 @@ export class Config {
|
||||
// So defense modifier is between [5, 2.5]
|
||||
return 5 - falloutRatio * 2;
|
||||
}
|
||||
msPerTick(): number {
|
||||
return 100;
|
||||
}
|
||||
SAMCooldown(): number {
|
||||
return 90;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user