feedback3 WD: the ammo bar drains out of the activation's real bullet total, 拉特兰 / 逃犯引渡手续 extras included (community report #35)
This commit is contained in:
1 parent
5a8c271d19
commit
d76c5da7dd
4 files changed
+82
-6
No files matched your search
+5
-3
@@ -715,7 +715,7 @@ registration order. `battle.off(handle)` / `battle.off(name, fn)` / `battle.offO
|
||||
| `dollSwitch` | `{ unit, reason, done }` | content switches a 傀儡师 to its <替身> now (归溟幽灵鲨 S2 "技能结束后立刻切换为<替身>": no lethal HP loss); its trait does it unless it already is one or is not on the field, and sets `done` |
|
||||
| `kill` | `{ killer, victim }` | victim HP reached 0 (a handler may revive by restoring HP) |
|
||||
| `death` | `{ unit, reason:'killed'|'leak'|'retreat'|'merchant'|'expired'|'forcedExit', killer }` | unit removed (`'forcedExit'`: an operator entering 联防 knocked out, §1.1) |
|
||||
| `skillStart` / `skillEnd` | `{ unit, skill, reason }` | mutate `skill.ammoLeft` / `skill.timeLeft` in skillStart |
|
||||
| `skillStart` / `skillEnd` | `{ unit, skill, reason }` | mutate `skill.ammoLeft` / `skill.timeLeft` in skillStart (bullets added there raise `skill.ammoMax`, the ammo bar's full mark) |
|
||||
| `ammoUsed` | `{ unit, left, skill }` | per ammo consumed |
|
||||
| `spGain` | `{ unit, amount, reason:'time'|'attack'|'hurt'|'init'|…, skill }` | mutable `amount` (time gains fire every tick) |
|
||||
| `beforeStatus` | `{ source, target, status, duration, value, cancel }` | set `cancel` (e.g. 浓缩嗅盐) |
|
||||
@@ -825,7 +825,8 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
|
||||
free heals).
|
||||
- Runtime helpers on `unit.skill`: `activate(reason, {free})`, `end(reason)`, `stop()`, `addAmmo(n)`, `extend(s)`,
|
||||
`addCharge(n)`, `gainSp(n, reason)`, `addTriggerRange(fn)`; fields `sp, spCost (= floor(base×spCostMul + spCostFlat)), spCostMul, charges,
|
||||
maxCharges, active, timeLeft, ammoLeft, activations, kind, rule, bb`.
|
||||
maxCharges, active, timeLeft, ammoLeft, ammoMax, activations, kind, rule, bb` (`ammoMax`: the most bullets the running ammo skill
|
||||
has held — set at activation, raised by skillStart additions and `addAmmo` above it; 0 when inactive).
|
||||
|
||||
### 7.2 Kits
|
||||
|
||||
@@ -1147,7 +1148,8 @@ Unknown subprofessions fall back to the profession default (test `professions.te
|
||||
## 9. Wire format (snapshot.js, DESIGN §8.2)
|
||||
|
||||
- `snapshot()` → `{ fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total, dps?, boss?, down?, elem? }`.
|
||||
`sp/spMax` show remaining duration/ammo as a draining bar while a timed skill is active. Units in DIE state stay 0.8 s.
|
||||
`sp/spMax` show remaining duration/ammo as a draining bar while a timed skill is active (ammo: `ammoLeft / ammoMax`, the
|
||||
activation's real total — 拉特兰's and 逃犯引渡手续's extra bullets included, community report #35). Units in DIE state stay 0.8 s.
|
||||
`down: [[id, respawnAt, respawnTime, state, row, col]]` (only when non-empty) = operators lying down waiting to redeploy
|
||||
(`Battle.isDown(u)`: reason `'killed'` or `FORCED_EXIT` (§1.1 carryState `down`), or a forced exit `'retreat'` /
|
||||
`'merchant'` (GitHub #60) — every removal but the 突袭 `'raid'` —, not removed for good, deployed at least once, a finite
|
||||
|
||||
+13
-1
@@ -97,6 +97,7 @@ export class SkillRuntime {
|
||||
this.active = false;
|
||||
this.timeLeft = 0;
|
||||
this.ammoLeft = 0;
|
||||
this.ammoMax = 0; // ammo kind: the most bullets this activation held (snapshot's draining bar)
|
||||
this.pending = false; // instant/charges: next attack uses spec.attack
|
||||
this.activations = 0;
|
||||
this.lastStart = -Infinity;
|
||||
@@ -167,6 +168,7 @@ export class SkillRuntime {
|
||||
this.pending = false;
|
||||
this.timeLeft = 0;
|
||||
this.ammoLeft = 0;
|
||||
this.ammoMax = 0;
|
||||
this.charges = 0;
|
||||
this.sp = 0;
|
||||
this._trigKeys = null;
|
||||
@@ -401,6 +403,7 @@ export class SkillRuntime {
|
||||
this.active = true;
|
||||
this.timeLeft = this.kind === 'duration' ? Math.max(0.01, this.duration) : (this.kind === 'ammo' && this.duration > 0 ? this.duration : Infinity);
|
||||
this.ammoLeft = this.kind === 'ammo' ? Math.max(1, this.ammo) : 0;
|
||||
this.ammoMax = this.ammoLeft;
|
||||
this._applyMods();
|
||||
} else {
|
||||
// instant / charges
|
||||
@@ -413,6 +416,9 @@ export class SkillRuntime {
|
||||
u.skillAnimUntil = b.time + 0.5;
|
||||
this._call('onStart', { reason });
|
||||
if (b._hooks.skillStart) b.emit('skillStart', { unit: u, skill: this, reason });
|
||||
// bullets added in skillStart (拉特兰's ×(1.05 + 0.015 × layers), 逃犯引渡手续, talents): the bar's full mark
|
||||
// (community report #35: the extra bullets sat above a full bar until fewer than the base count were left)
|
||||
if (this.active && this.ammoLeft > this.ammoMax) this.ammoMax = this.ammoLeft;
|
||||
if (!this.isTimed && !this.pending) this.end('instant');
|
||||
return true;
|
||||
}
|
||||
@@ -459,6 +465,7 @@ export class SkillRuntime {
|
||||
this.pending = false;
|
||||
this.timeLeft = 0;
|
||||
this.ammoLeft = 0;
|
||||
this.ammoMax = 0;
|
||||
const n = this.activations;
|
||||
this._call('onEnd', { reason });
|
||||
if (!this.active && this.activations === n) this._removeMods();
|
||||
@@ -481,7 +488,12 @@ export class SkillRuntime {
|
||||
|
||||
// ---- helpers for content -------------------------------------------------------------------------------
|
||||
// (non-finite arguments are ignored: a NaN timer/ammo count would keep the skill active forever)
|
||||
addAmmo(n) { if (this.active && this.kind === 'ammo' && Number.isFinite(n)) this.ammoLeft += n; }
|
||||
// (bullets added above the activation's most so far raise the bar's full mark: the bar drains one bullet at a time)
|
||||
addAmmo(n) {
|
||||
if (!this.active || this.kind !== 'ammo' || !Number.isFinite(n)) return;
|
||||
this.ammoLeft += n;
|
||||
if (this.ammoLeft > this.ammoMax) this.ammoMax = this.ammoLeft;
|
||||
}
|
||||
extend(seconds) { if (this.active && Number.isFinite(this.timeLeft) && Number.isFinite(seconds)) this.timeLeft += seconds; }
|
||||
addCharge(n = 1) { if (!Number.isFinite(n)) return; this.charges = Math.max(0, Math.min(this.maxCharges, this.charges + n)); if (this.charges >= this.maxCharges) this.sp = this.spCost; }
|
||||
stop() { this.end('stopped'); }
|
||||
|
||||
@@ -92,8 +92,9 @@ export function unitTuple(u, t) {
|
||||
const spMax = sk && !sk.noSkill ? sk.spCost : 0;
|
||||
let sp = sk && !sk.noSkill ? sk.sp : 0;
|
||||
if (sk && sk.active && sk.isTimed) {
|
||||
// show remaining duration/ammo as a draining bar
|
||||
if (sk.kind === 'ammo') sp = spMax * (sk.ammoLeft / Math.max(1, sk.ammo));
|
||||
// show remaining duration/ammo as a draining bar — ammo out of the activation's real total (base + bullets added:
|
||||
// 拉特兰, 逃犯引渡手续, refills; community report #35), so every bullet shortens it
|
||||
if (sk.kind === 'ammo') sp = spMax * (sk.ammoLeft / Math.max(1, sk.ammoMax || sk.ammo || 0, sk.ammoLeft));
|
||||
else if (Number.isFinite(sk.timeLeft) && sk.duration > 0) sp = spMax * (sk.timeLeft / sk.duration);
|
||||
}
|
||||
// hp is rounded up (a living unit never shows 0) but never above the rounded max HP
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// test/sim/feedback3-ammo-bar.test.js — community report #35 (0.1.3): "拉特兰盟约叠层之后蕾缪安3技能锁定敌人消耗的子弹不会实时让技力条一格
|
||||
// 一格减少,而是最后突然消耗". 拉特兰's skillStart multiplies the bullets (×(1.05 + 0.015 × layers), floored) and 逃犯引渡手续 adds
|
||||
// one, but the snapshot drew ammoLeft / skill.ammo (the base 5) and the client clamps at 1: the bar stayed full until
|
||||
// fewer than 5 bullets were left. Now the draining bar is ammoLeft / ammoMax — the most bullets this activation held
|
||||
// (set at activation, raised by skillStart additions and addAmmo) — so it starts full and every bullet shortens it.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { unitTuple } from '../../server/sim/snapshot.js';
|
||||
|
||||
const DEFS = { enemies: { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e7, speed: 0 }) } };
|
||||
const bar = (u, t) => { const [, , , , , sp, spMax] = unitTuple(u, t); return sp / spMax; };
|
||||
|
||||
test('蕾缪安 S3 with 30 拉特兰 layers: the bar starts full and loses one bullet\'s share per lock (base 5 + extras)', () => {
|
||||
const seen = [];
|
||||
const h = makeBattle({
|
||||
defs: DEFS, timeLimit: 200, seed: 7,
|
||||
units: [{ chessId: 'chess_char_6_01_a', row: 10, col: 3 }],
|
||||
enemies: [{ key: 'enemy_dummy', pos: [10, 5] }],
|
||||
bonds: { lateranoShip: { count: 3, active: true, tier: 1, layers: 30 } },
|
||||
setup(b) {
|
||||
b.on('ammoUsed', ({ unit, left, skill }) => seen.push({ left, max: skill.ammoMax, bar: bar(unit, b.time) }), { priority: -999 });
|
||||
},
|
||||
});
|
||||
const u = h.unit('chess_char_6_01_a');
|
||||
assert.equal(u.skill.ammo, 5, 'S3: 5 bullets (attack@trigger_time)');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 80));
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 30));
|
||||
assert.ok(seen.length >= 7, `${seen.length} bullets: 5 × 1.5 floored, + 逃犯引渡手续`);
|
||||
const full = seen[0].left + 1;
|
||||
for (const s of seen) assert.equal(s.max, full, 'the full mark is the activation\'s real total');
|
||||
seen.forEach((s, i) => assert.ok(Math.abs(s.bar - (full - 1 - i) / full) < 0.01, `bullet ${i + 1}: bar ${s.bar.toFixed(3)} = ${full - 1 - i}/${full}`));
|
||||
assert.ok(seen[0].bar < 1 - 0.1, 'the first lock already shortens the bar (it used to stay full until < 5 were left)');
|
||||
assert.equal(u.skill.ammoMax, 0, 'reset when the skill ends');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('the full mark follows bullets added mid-skill only above it; without extras the bar is unchanged (n / base)', () => {
|
||||
const h = makeBattle({
|
||||
defs: DEFS, timeLimit: 200, seed: 7,
|
||||
units: [{ chessId: 'chess_char_6_01_a', row: 10, col: 3 }],
|
||||
enemies: [{ key: 'enemy_dummy', pos: [10, 5] }],
|
||||
});
|
||||
const u = h.unit('chess_char_6_01_a');
|
||||
h.step(1);
|
||||
u.skill.activate('test', { free: true });
|
||||
const sk = u.skill;
|
||||
assert.equal(sk.ammoMax, sk.ammoLeft);
|
||||
const base = sk.ammoMax;
|
||||
assert.ok(Math.abs(bar(u, h.b.time) - 1) < 1e-9, 'full at the start');
|
||||
sk.ammoLeft -= 2; // two locks
|
||||
assert.ok(Math.abs(bar(u, h.b.time) - (base - 2) / base) < 0.01);
|
||||
sk.addAmmo(1); // a refill under the mark: the mark stays
|
||||
assert.equal(sk.ammoMax, base);
|
||||
sk.addAmmo(3); // above it: the mark rises, the bar is full again
|
||||
assert.equal(sk.ammoMax, base + 2);
|
||||
assert.ok(Math.abs(bar(u, h.b.time) - 1) < 1e-9);
|
||||
sk.end('test');
|
||||
assert.equal(sk.ammoMax, 0);
|
||||
});
|
||||
Reference in new issue
Block a user