mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 21:16:21 +00:00
Rebalance Doomsday Clock: late-game stalemate-breaker (10min grace + wave squeeze), slower troop drain, gentler-but-steeper warship attrition (#4518)
Resolves #<add your approved & assigned issue number> ## Description: Rebalances the Doomsday Clock so it acts as a **late-game stalemate-breaker** rather than an early-game culler, softens how fast it removes troops and warships, and makes the HUD countdown clearer. **Clock schedule (all presets):** - A flat **10-minute grace** at 0% required share — the early game is decided by combat, not the clock. - Then a 6-wave squeeze at accelerating levels (4 / 9 / 16 / 26 / 40 / 55%) with short pauses, reaching the final 55% at each preset's cap: **45 / 35 / 25 / 15 min** for slow / normal / fast / veryfast. - `WaveSchedule` `rampSeconds`/`pauseSeconds` are now **per-wave arrays**, so the curve can be shaped (gentle early, steeper late) instead of one uniform ramp. `requiredBasisPoints` and the HUD companion `doomsdayClockWaveState` walk the per-wave segments in lockstep. **Troop drain:** warn window `10s → 30s`, drain eased (`2%→5%` over `90s`), so a caught side takes ~2 minutes to wipe instead of ~1. **Warship attrition:** warships get their own gentler start plus a **convex** decay curve — a ship caught when its side is first doomed lasts about as long as troops, but the rate ramps up steeply so a side at full attrition still loses its fleet in ~2s. Adds a `curveExponent` argument to `doomsdayClockDrain` (1 = linear, used for troops; higher = convex, used for ships). **Determinism:** the drain curve is **integer-only** (fixed-point power, no floats), so the floored per-tick loss is bit-identical on every client in the lockstep sim. The linear troop path keeps its exact existing integer form; only the convex warship path is reshaped. **HUD countdown clarity:** the clock readout now shows a live countdown in both states — `Will reach 16% in M:SS` while the bar is actively climbing, and `Starts rising to 26% in M:SS` during a pause (previously `Next 26% in …`, which read as if it jumped there instantly). Backed by a new `secondsToTarget` field on the shared wave state so the sim and HUD stay in agreement. All display text goes through `translateText()` / `en.json`. <img width="278" height="116" alt="image" src="https://github.com/user-attachments/assets/e0be3d2c-bb88-46be-b344-34d63e4859bd" /> <img width="304" height="171" alt="image" src="https://github.com/user-attachments/assets/da74bd05-0b9a-4aec-bbf5-e7380fbda88e" /> **Danger skull:** while a side is below the bar in the warn window, its on-map skull now blinks progressively faster as the countdown runs out (accelerating to the moment the drain begins), then holds steady once it is actually draining — a clearer "you are about to be hit" cue. Rationale: the previous schedule removed players heavily in the first half of a match and could leave a drawn-out endgame. Holding the clock at 0% early keeps the opening about fighting, and concentrating its pressure in the back half reserves it for actually breaking stalemates. Values are tuning starting points and easy to adjust in `DoomsdayClock.ts` / the config defaults. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: <add your Discord username>
This commit is contained in:
@@ -124,6 +124,7 @@ export class DoomsdayClockPanel extends LitElement {
|
||||
: wave.growing
|
||||
? translateText("doomsday_clock.growing", {
|
||||
pct: scalePct(wave.targetPercent),
|
||||
time: this.secondsToHms(wave.secondsToTarget),
|
||||
})
|
||||
: translateText("doomsday_clock.next_wave", {
|
||||
pct: scalePct(wave.targetPercent),
|
||||
|
||||
@@ -107,10 +107,20 @@ export function computePlayerStatus(
|
||||
const inDoomsdayClock = ps.inDoomsdayClock;
|
||||
// Past the warn grace the side is actively bleeding troops; the skull holds
|
||||
// steady then (vs blinking while merely in danger).
|
||||
const doomsdayClockUnderTicks =
|
||||
(opts.tick ?? 0) - ps.markedDoomsdayClockTick;
|
||||
const doomsdayClockWarnTicks = opts.doomsdayClockWarnTicks ?? 0;
|
||||
const doomsdayClockDraining =
|
||||
inDoomsdayClock &&
|
||||
(opts.tick ?? 0) - ps.markedDoomsdayClockTick >=
|
||||
(opts.doomsdayClockWarnTicks ?? 0);
|
||||
inDoomsdayClock && doomsdayClockUnderTicks >= doomsdayClockWarnTicks;
|
||||
// How far through the warn countdown (0->1); the danger skull blinks faster
|
||||
// as this nears 1, i.e. as the side approaches draining.
|
||||
const doomsdayClockWarnProgress =
|
||||
inDoomsdayClock && doomsdayClockWarnTicks > 0
|
||||
? Math.max(
|
||||
0,
|
||||
Math.min(1, doomsdayClockUnderTicks / doomsdayClockWarnTicks),
|
||||
)
|
||||
: 0;
|
||||
const traitorRemainingTicks = ps.traitorRemainingTicks;
|
||||
|
||||
// Relative flags
|
||||
@@ -179,6 +189,7 @@ export function computePlayerStatus(
|
||||
disconnected,
|
||||
inDoomsdayClock,
|
||||
doomsdayClockDraining,
|
||||
doomsdayClockWarnProgress,
|
||||
alliance,
|
||||
allianceReq,
|
||||
target,
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface PlayerSlot {
|
||||
nukeTargetsMe: boolean;
|
||||
inDoomsdayClock: boolean;
|
||||
doomsdayClockDraining: boolean;
|
||||
doomsdayClockWarnProgress: number;
|
||||
traitorRemainingTicks: number;
|
||||
allianceFraction: number;
|
||||
allianceRemainingTicks: number;
|
||||
|
||||
@@ -327,6 +327,7 @@ export class NamePass {
|
||||
nukeTargetsMe: false,
|
||||
inDoomsdayClock: false,
|
||||
doomsdayClockDraining: false,
|
||||
doomsdayClockWarnProgress: 0,
|
||||
traitorRemainingTicks: 0,
|
||||
allianceFraction: 0,
|
||||
allianceRemainingTicks: 0,
|
||||
@@ -459,6 +460,7 @@ export class NamePass {
|
||||
const disconnected = sd?.disconnected ?? false;
|
||||
const inDoomsdayClock = sd?.inDoomsdayClock ?? false;
|
||||
const doomsdayClockDraining = sd?.doomsdayClockDraining ?? false;
|
||||
const doomsdayClockWarnProgress = sd?.doomsdayClockWarnProgress ?? 0;
|
||||
const alliance = sd?.alliance ?? false;
|
||||
const allianceReq = sd?.allianceReq ?? false;
|
||||
const target = sd?.target ?? false;
|
||||
@@ -475,6 +477,7 @@ export class NamePass {
|
||||
disconnected !== slot.disconnected ||
|
||||
inDoomsdayClock !== slot.inDoomsdayClock ||
|
||||
doomsdayClockDraining !== slot.doomsdayClockDraining ||
|
||||
doomsdayClockWarnProgress !== slot.doomsdayClockWarnProgress ||
|
||||
alliance !== slot.alliance ||
|
||||
allianceReq !== slot.allianceReq ||
|
||||
target !== slot.target ||
|
||||
@@ -490,6 +493,7 @@ export class NamePass {
|
||||
slot.disconnected = disconnected;
|
||||
slot.inDoomsdayClock = inDoomsdayClock;
|
||||
slot.doomsdayClockDraining = doomsdayClockDraining;
|
||||
slot.doomsdayClockWarnProgress = doomsdayClockWarnProgress;
|
||||
slot.alliance = alliance;
|
||||
slot.allianceReq = allianceReq;
|
||||
slot.target = target;
|
||||
@@ -576,14 +580,17 @@ export class NamePass {
|
||||
d[off + 15] = slot.nameHalfWidth;
|
||||
|
||||
// Column 4: flagLayerIdx, emojiAtlasIdx, smallID, doomsdayClock state
|
||||
// (0 none, 1 danger -> blinking skull, 2 draining -> steady skull).
|
||||
// (0 none, 1.0-1.49 danger -> blinking skull, 2 draining -> steady skull).
|
||||
// The warn-countdown progress (0->1) is packed into the danger value's
|
||||
// fraction so the shader can blink faster as drain nears; it stays < 1.5 so
|
||||
// the draining threshold is untouched.
|
||||
d[off + 16] = slot.flagLayerIdx;
|
||||
d[off + 17] = slot.emojiAtlasIdx;
|
||||
d[off + 18] = slot.static.smallID;
|
||||
d[off + 19] = slot.doomsdayClockDraining
|
||||
? 2.0
|
||||
: slot.inDoomsdayClock
|
||||
? 1.0
|
||||
? 1.0 + slot.doomsdayClockWarnProgress * 0.49
|
||||
: 0.0;
|
||||
|
||||
// Column 5: crown, traitor, disconnected, alliance
|
||||
|
||||
@@ -268,12 +268,19 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Doomsday Clock skull: slot 8. Blinks ~2 Hz while in danger (flag 1.0); holds
|
||||
// steady once the side is draining (flag 2.0).
|
||||
// Doomsday Clock skull: slot 8. While in danger (flag 1.0-1.49) the skull
|
||||
// blinks and speeds up as the warn countdown runs out — the progress 0->1 is
|
||||
// packed into the flag's fraction. Base ~2 Hz plus a p^2 phase term so it
|
||||
// accelerates toward the drain (same trick as the alliance-renewal flash
|
||||
// above, which keeps the phase continuous). Holds steady once draining (>=1.5).
|
||||
if (iconSlot == 8) {
|
||||
vFlashAlpha = (statusFlag[8] < 1.5)
|
||||
? 0.35 + 0.65 * (0.5 + 0.5 * cos(uTime * 2.0 * 6.2832))
|
||||
: 1.0;
|
||||
if (statusFlag[8] < 1.5) {
|
||||
float p = clamp((statusFlag[8] - 1.0) / 0.49, 0.0, 1.0);
|
||||
float phase = uTime * 2.0 + p * p * 40.0;
|
||||
vFlashAlpha = 0.35 + 0.65 * (0.5 + 0.5 * cos(phase * 6.2832));
|
||||
} else {
|
||||
vFlashAlpha = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
vDiscard = 0;
|
||||
|
||||
@@ -193,6 +193,7 @@ export interface PlayerStatusData {
|
||||
nukeTargetsMe: boolean;
|
||||
inDoomsdayClock: boolean;
|
||||
doomsdayClockDraining: boolean;
|
||||
doomsdayClockWarnProgress: number;
|
||||
traitorRemainingTicks: number;
|
||||
allianceFraction: number;
|
||||
allianceRemainingTicks: number;
|
||||
|
||||
Reference in New Issue
Block a user