feedback7 R23-TICK: docs for whole-frame attack intervals and SP costs (PR #402 by @Cloudnyco) — SIM.md §2 / §7.1, design/geometry-time.md §4, DESIGN §27.61 and the intro's area map; golden: none in this commit (the previous one moved every family: roster, bonds, fields, matches, standins, diy)

docs/SIM.md §2: the countdown (attackCountdown, 1e-9 like timeLeft / buffs / every()), 1 s = 30 ticks and 3 s = 90, the
双眼皮 turrets and 寒檀's icicles on it, the drones that compared with 1e-9 already, the count-up content clocks not
covered; the official note — PRTS 作战机制/sandbox 「帧对齐机制对攻速的影响」: one frame = 1/30 s, since 2019-12-24 a
non-integer frame count is rounded (四舍五入; before, 进一取整), an exact one stays; here a non-integer count still ends on
the tick that crosses 0 (⌈30 × interval⌉: 1.25 s = 38, 能天使's 0.678 s 21 against the official 20) — an older
difference, unchanged. §7.1: SP within 1e-9 of the cost is the cost (gainSp, _normalize). design/geometry-time.md §4:
the engine's timer tolerance in one place. history/0.2.2.md §27.61 (Seen / Official / Now / Golden with the end-result
flips one line each / Tests / Docs) and the area map's "→ §27.61". docs-consistency + docs-paths: 31 pass.
This commit is contained in:
sganggs committed 2026-10-09 01:37:32 +08:00
1 parent 227ab9e7ca
commit 6de5dec30c
3 files changed
+84 -2

No files matched your search

+13 -1
View File
@@ -565,6 +565,16 @@ Aggregation: `ATK/DEF/maxHp = (base + Σflat) × (1 + Σpct) × Πmul`, for ATK
percentages: `ATK = ((base + ΣatkFlat) × (1 + ΣatkPct) + ΣatkFinal) × ΠatkMul`; `res = clamp((base + ΣresFlat) × ΠresMul, 0, 100)`;
`aspd = clamp(base + Σaspd, 20, 600)` (floor 20: PRTS 数值范围 ATTACK_SPEED 默认下限; user playtest #6); `interval = bat × (1 + ΣbatPct) × 100 / aspd`; `moveSpeed = (base + ΣmoveFlat) × ΠmoveMul`;
tiles/s = `moveSpeed × MOVE_SCALE (0.5)`. A maxHp change keeps the HP ratio. Elite stats (module included) come from data.
The attack cooldown `atkCd` starts at `interval` and counts down by `TICK`; what is left within 1e-9 of 0 is 0
(`ai.js attackCountdown`, the tolerance of skill `timeLeft`, buff intervals and `every()`; PR #402 by @Cloudnyco, 0.2.2),
so an interval of a whole number of ticks takes exactly that many: 1 s = 30 ticks, 3 s = 90 (floating point alone left
2.1e-16 after thirty steps of 1/30, and every such attack came a tick late — 31, 91). The same countdown runs the 双眼皮
turrets (`content/devices.js`) and 寒檀's S2 icicles; the drones of 澄闪 / 荒芜拉普兰德 and 电弧's 赛柯 volleys compared
with 1e-9 already. Content clocks that count up and restart from 0 (the boss self-timers of `content/bosses.js`, kit auras)
are not covered. Official (PRTS 作战机制/sandbox 「帧对齐机制对攻速的影响」): the time unit is one frame, 1/30 s, and since
the 2019-12-24 update a non-integer frame count is rounded (四舍五入; before, 进一取整); an exact one stays. Here a
non-integer count still ends on the tick that crosses 0 — ⌈30 × interval⌉ ticks (1.2 s = 36, 1.25 s = 38; 能天使's
0.678 s is 21 here, 20 frames officially): an older difference from the official rounding, not changed by this rule.
**Which bucket (PRTS 游戏数据基础 属性基本公式 / 作战机制, DESIGN §20.10).** `Σpct` is the official **直接乘算** class — its
values are summed (`A = (A₀ + D_p)(1 + D_t)`, D_t = t₁ + … + tₙ): a skill's or talent's "攻击力+X%" **and** every "+X%"
@@ -918,7 +928,9 @@ instance) and skip `'counter'` / `'reflect'` damage. When the guard trips, the l
PRTS 异常效果 晕眩 names no SP effect — community report #18). Attack-type SP: attacks made
by the skill (the pending "next attack" of an instant/charge skill, every shot of a timed skill including the one that
ends it) recover nothing, so a cost-N skill fires every **N+1** attacks (AK). Charges (`maxChargeTime > 1`):
SP fills to cost → +1 charge (SP restarts) until charges are full (then SP stays full).
SP fills to cost → +1 charge (SP restarts) until charges are full (then SP stays full). SP within 1e-9 of the cost is
the cost (`gainSp`, `_normalize`; a tiny negative remainder is 0): the float sum of the 1/30-SP time gains can stop
2.5e-14 short of it — a 10-SP skill at 1 SP/s took 301 ticks, now 300 (PR #402, 0.2.2).
- Triggers (`skill.trigger.rule` in data — the official 技能策略, PRTS 卫戍协议/帮助 §作战阶段 技能操作, resolved by
`tools/build-data.mjs resolveTrigger`: charId rows by skill index; the class rows (重装 / 执旗手 / 战术家 / 吟游者 / 解放者 /
阵法术师) for **every MANUAL skill** of the class; SKILL_RANGE for a MANUAL skill with a 技能范围 of its own; ACTIVE_RANGE
+7
View File
@@ -22,6 +22,13 @@ Part of [DESIGN.md](../DESIGN.md) (the index; section numbers are global).
## 4. Time model
- Simulation time is **game seconds**. Fixed step `TICK = 1/30 s`.
- Timers count in ticks and take what is within 1e-9 as reached — attack cooldowns and enemy wind-ups (`ai.js
attackCountdown`), time SP against its cost (`skills.js gainSp`), skill durations, buff intervals, `every()` /
`after()`, enemy abilities, the 失衡 end —, so a whole number of ticks takes exactly that many: 1 s = 30 ticks (PRTS
作战机制/sandbox 「帧对齐机制对攻速的影响」: one frame = 1/30 s). Floating point alone held 1 s attacks and 10-SP charges a
tick longer until 0.2.2 (PR #402, §27.61). A non-integer count ends on the tick that crosses the line, ⌈30 × t⌉ (1.25 s
= 38); the official game rounds it (四舍五入 since 2019-12-24: 能天使's 0.678 s is 20 frames there, 21 here) — an older
difference, not changed.
- Combat runs at **2× real time** (forced, like the original): 60 ticks per real second — in the browser's runner (§14) or, for server-run fields, the server's accumulator (2 ticks every real 1/30 s, never more than 8 ticks per interval to avoid spirals).
- Combat time limits come from `data/config.json → modes[m].rounds[r].combatTimeLimit` (= the level's `maxPlayTime`; also `modes[m].combatTimeLimit[r]`) and are **real seconds of the forced-2× battle** (verified by the balance pass: spawn schedules exceed the limit when read as game seconds). The server passes `2 × value` game seconds to the Battle (`server/match/gamedata.js → combatTimeLimit`; `config.combatTimeScale`, default 2); the countdown players see equals the data value. When time runs out, living non-boss enemies count as leaked. 联防 uses the round's limit.
- Final Assault / Hidden Core have **no hard stop** and their clocks are real seconds too: the boss level's `levelMaxPlayTime` (120 real s) is only the HUD countdown (`m.public.deadline`; the battle goes on past it), and the overtime drain (`bossOvertimeAfter` 150 / `bossOvertimeDrainPerSec` 1 = official `bossTurnHpReduceTime`) takes 1 team LP per whole **real** second from 150 real s (= 300 game s on the 2× field clock; first point at 151 s — `gamedata.js bossOvertimeDue`). `m.public.overtimeAt` = the ms epoch when the drain starts; both are placed on the field clock.
+64 -1
View File
@@ -18,7 +18,8 @@ the boss bounty payout and the bots' guards → §27.33–§27.35; the battle vi
pull slides, the champagne's bar, closed eyes, late attack clips, the HP-bar readouts, the renderer abort, held skill
cues, atlas refreshes, 梓兰's frost) → §27.36–§27.46; the interface (device tips, the settings button, the shop fold,
spectators, the top-bar capsule, the empty new shop slot) → §27.47–§27.52; voices → §27.53–§27.54; the statistics page
→ §27.55; potential and training (潜能 / 练度) and the 干员调配 list → §27.56–§27.59; the 自选 picker's Esc → §27.60.
→ §27.55; potential and training (潜能 / 练度) and the 干员调配 list → §27.56–§27.59; the 自选 picker's Esc → §27.60;
whole-frame attack intervals and SP costs (PR #402) → §27.61.
Decisions of the owner, 2026-10-08 (round 22, from community reports, GitHub and research into the official rules):
潜能 and the 自持有 练度 bonus are set per operator in 干员调配, defaults 潜能 6 and 精英2 Lv.60 (official: the account's
@@ -1456,3 +1457,65 @@ Notes for the integrator
- **Tests**: test/ui/diy.e2e.test.js adds the immediate Esc (it fails without the fix). The loadout-battle /
teammate-loadout / loadout browser tests follow §27.57's 干员调配: the row's quick skill / module choices, the default
练度 in the detail card's base ATK (×1.1), the detail back at the top before a click.
### 27.61 An attack interval or a time-SP cost of a whole number of frames takes exactly that many — 1 s is 30 frames, not 31 (PR #402 by @Cloudnyco; the owner's approval of 2026-10-09) — `sim/ai.js attackCountdown / updateAlly / updateEnemy`, `sim/skills.js SkillRuntime.gainSp / _normalize`, `sim/content/devices.js tickTurret`, `sim/content/kits/ops/chess_char_5_21-sntlla.js`
- **Seen**: PR #402: an attack cooldown counted down by TICK = 1/30 s (not a binary fraction) stops at 2.1e-16 after
thirty steps from 1 s, and the fire check `atkCd > 0` waited a 31st frame; 300 time gains of 1/30 SP add up to
9.999999999999975, so `sp >= cost` charged a 10-SP skill a frame late. Measured on 0.2.2 before the fix, over the 265
golden battles: 30.85 % of the consecutive attacks on a whole-frame interval came a frame late (the other intervals,
the control: 0.09 %) — 1 s gaps were 31 frames in 3 545 of 3 801, 3 s 91 in all 6 677, 4 s 121 in all 2 223 —, and
4 157 end-of-tick states sat within 1e-9 under a skill's cost (229 battles). The engine already took a timer within
1e-9 as done (skill `timeLeft`, buff intervals, `every()` / `after()`, enemy abilities, nextCastAt, the 失衡 end);
these two did not.
- **Official**: PRTS 作战机制/sandbox 「帧对齐机制对攻速的影响」: the time unit is one frame, 1/30 s; since the 2019-12-24
update a non-integer frame count is rounded (四舍五入; before, 进一取整) — 能天使's 0.696 s is 21 frames, 0.678 s 20.
An exact 1 s is 30 frames under either rule; PRTS 游戏数据基础 攻速公式 (T = T₀ / (clamp(S, 10, 600) / 100)) adds no frame.
- **Now**: `ai.js attackCountdown(cd, dt)` — what is left within 1e-9 of 0 is 0 — counts down the operators' cooldown
(still only while `canAct`) and the enemies' (not while stunned or hidden; the 失衡 swing cut as it was). An enemy's
strike frame is that countdown's 0, so a whole-frame wind-up is exact too (a 0.4 s clip hit: 12 frames, 13 before). The
“双眼皮” turrets (`content/devices.js`) and 寒檀's S2 icicles (0.5 s: 15 frames, 16 before) use it — the same `cd -= dt;
if (cd > 0)` shape; the drones of 澄闪 / 荒芜拉普兰德 and 电弧's 赛柯 volleys compared with 1e-9 already. `gainSp` takes SP
within 1e-9 of the cost as full, every charge of a multi-charge skill included (a tiny negative remainder is 0), and
`_normalize` (an SP cost changed mid-battle) compares the same way. Attack / hurt SP (+1 on an integer cost) compare as
before: a cost-N attack-SP skill still fires every N + 1 attacks, its attacks now on time. 1e-9 is far above any float
residue and far below a frame (PR #402's bounds: ≤ 7.5e-13 on the ASPD 20–600 grid; an interval off the frame grid is
≥ 1 / (30 × 600) ≈ 5.6e-5 s from a boundary). Unchanged on purpose: a non-integer count still ends on the frame that crosses 0, ⌈30 × interval⌉ (1.25 s =
38; 0.678 s = 21, officially 20) — the official 四舍五入 is an older, separate difference; content clocks that count up
and restart from 0 (the boss self-timers of `content/bosses.js`: 管 / 弦's strikes and summons, “萨米的意志”'s 冰凌,
阿利斯泰尔's 王权号令 SP; kit auras and pulses — PR #402 lists them) are not touched and may still take a frame more.
- **Golden**: 281 of 283 scenarios move — roster 49 / 49, bonds 46 / 46, fields 22 / 22, matches 18 / 18, standins
10 / 10, diy 136 / 138: both sides attack on time (more enemy kinds than operator forms sit on whole-frame intervals —
3 s, 4 s, 1 s …) and time SP charges a frame sooner. After it: 0.04 % of the whole-frame consecutive attacks a frame
late (control 0.13 %), no SP state short of its cost. The moved battles' totals: kills 10 647 → 10 625, leaks 12 398 →
12 445, deaths 2 913 → 2 944. End results that flip (one frame cascades; the causes as measured):
- solo-NORMAL-1: won → lost in round 14 (team LP 8 → 0) — the Final Assault on 卢西恩 (pool 390 000) runs as before for
two minutes (his 0.4 s wind-up a frame shorter), then the team's damage on the pool trails: 884 are left when the
overtime drain (from 300 game s) takes the last of the 17 LP at 334 s; before, the pool emptied at 318 s with 8 left.
- solo-HARD-1: LP 10 → 12 — round 8 lets one more through (26 → 25; rounds 11–12 even it out) and the Final Assault ends
at 300.6 s, before the overtime drain (304.8 s and 2 LP before).
- solo-NORMAL-2: LP 9 → 25 — round 5 lets one more through (26 → 25, a coin less), the bot's shop path parts from round
6, and its later board (突袭, 232 layers at round 13 against 94) clears rounds 12–13 with no leak (7 + 18 before).
- coop2-NORMAL-3: team LP 63 → 68 — ai_1's round-13 battle leaks 1 instead of 6 (its round-12 leaker now goes to 联防).
- coop2-ABYSS-10-boosted: team LP 583 → 630 — the Final Assault ends at 606.6 s (639.6) and the Hidden Core at 335.9 s
(397.8): 47 LP less of overtime drain.
- coop2-NORMAL-14-standins: team LP 41 → 40 — ai_0's round-13 battle lets one 潜伏者 through (23 → 22).
- Battles: roster-016 cleared at 220.3 s → a time-out at 230 s (主角阵营角色 leaks instead of falling; kills 101 → 105,
leaks 26 → 22); roster-041 a time-out → cleared at 189.0 s, perfect (the last 粉碎攻坚组长 falls inside the 190 s);
bond-suntShip-high cleared at 164.2 s → a time-out at 190 s (kills 11 → 13, deaths 4 → 2); standin-04 perfect → 10
leaks (deaths 8 → 10); diy-005 / diy-008 cleared at 225.3 s → a time-out at 230 s (a last 宿主重装士兵 / 深池方阵指挥官
still walking); diy-127 (白铁 / 酒神) a time-out → cleared at 225.8 s (deaths 7 → 13, leaks 57 → 72); diy-131 (死芒)
perfect → 1 leak (a 掠海漂移体).
- **Tests**: `test/sim/tick-tolerance.test.js` (new, 7 — the PR's four adapted, plus three): the residue itself;
attackCountdown over bat 0.1–10 s × 14 ASPD values = ⌈30 × interval⌉; a 1 s operator every 30 frames and a 3 s ranged
enemy every 90; the control 1.2 s = 36, 1.25 s = 38; time-SP costs 4 / 10 / 16 / 20 / 30 / 50 after exactly 30 × N
gains, one and two charges; the “双眼皮” turret at 0 layers (BAT 4 s) every 120; 寒檀's icicles every 15 — the four
engine ones fail on the parent (31 / 91, cost 4 at 121 gains, 121, 16). The PR's seven test edits, re-run here: the same
8 assertions failed on the new timing, and each edit passes on the parent and on the fix — feedback2-surtr-stealth
(counts from when no arrow is in flight: a shot keeps its target), kits_alt_t2 拉普兰德 S1 (the dummy's ATK 300 → 250:
at 15 frames a hit, 120 × 15 = her 1 791 HP by 59.5 s), kits_alt_t6 (the medic smoke starts the team at half HP; 余 S1
compares up to the attacker's first 燃烧 burst, which refuses the next fill), op_jesca2 (the T6 shield, 60 + 30 = the
90 s cost, returns a tick after the cast), op_lmlee (his hits before the burst), op_logos (counts after the clear),
op_narant (the first attack thrown after the new enemies came). node --test: 5 703 tests, 5 701 pass, 0 fail, 2
skipped (the browser suites off); GOLDEN_FULL golden.test 9 / 9; eslint 0 errors (135 warnings, as on the parent).
- **Docs**: docs/SIM.md §2 (the countdown, the turrets / icicles, the official 帧对齐 note) and §7.1 (SP);
docs/design/geometry-time.md §4.