feedback5: merge CB2 — community reports (healers / summons): 刺玫's S2 heals the most injured ally again (the counter's heal alone goes to her protégé), 琳琅诗怀雅 heals herself and her 破财消灾 cost restarts at 5 DP with every deployment, 赫默's S2 casts at full SP so the drone takes the field at once, the countdown summons (医疗探机, 诅咒娃娃, 海嗣 and four 自选 ones) take no damage, are never healed and show the life left on their bar, stacked ground areas stay readable; §25.16 after CB5's §25.19 (renumbered with the later merges); golden: vs feedback5 CB2's own rules — roster-001/002/003/005/008/016/018/027/035/037/045/046/049, bond-deputShip-low/high, bond-suntShip-high, bond-victoriaShip-low/high, bond-visiShip-low/high, boss-boss_1/3/5/7-pair, hidden-boss_9-pair, unite-1, matches solo-FUNNY-2, coop2-HARD-4, coop3-ABYSS-5, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-ABYSS-10-boosted, coop2-NORMAL-14-standins; vs CB2 exactly CB5's 18 bot matches

Merge fixes (the three tests the trial merge failed):
- test/content/summon_loadout_conflicts.test.js (赫默's drone lifetime per module variant): it asserted "no cast yet"
  after the drone's redeploy time and then cast S2 by hand — with nobody hurt the old DEFAULT rule never cast. S2 now
  casts by itself the tick its SP is full (22 s), which is the official rule (client skill skchr_silent_2: no
  `_trigger`, `_allowNoTarget` 1, `_checkHasTargetBeforeDoCast` 0; CB2 64f8bf9), so the test waits for that cast and
  checks both drones' lifetimes from it. Failed on fb5-cb2 itself (the stream did not run the file).
- test/sim/feedback1f-downed-tile.test.js (F5 rule 3): its "killed" case killed 浊心斯卡蒂's 海嗣 with 1e9 true damage;
  the 海嗣 is a countdown summon now — 无敌 + 禁疗, the official state (PRTS 海嗣 备注 「持有禁疗、无敌」) — so it only
  expires. The killed case takes 缪尔赛思's 流形 (a placed board summon that is attacked and killed and also comes back on
  its tile 25 s later); both cases now also check the summon's return. Failed on fb5-cb2 itself.
- test/golden-standins.test.js (the 补位 match fields at least 3 different stand-ins): already failing on feedback5
  37558f0 — CB5's 调度中心 upgrade card changed the human seat's shop in coop2-NORMAL-14-standins, which then fielded
  only 2 of its 7 not-owned chess (忍冬 → Sharp, 安洁莉娜 → Raidian). tools/golden.mjs MATCH_NOT_OWNED adds 缄默德克萨斯
  and 铃兰 (chess its AI fields now): 4 stand-ins (Sharp from round 4, Misery from 7, 预备干员-辅助 from 9, Raidian in 12);
  the test is unchanged, test/golden/README.md says 9 chess. This moves coop2-NORMAL-14-standins only.
- docs/history/0.2.0.md: the §25 intro's two stream lines joined into one sentence (the merge had cut it).

Golden moves outside CB2's commit list are CB2's rules on the paths CB5 gave the bots: coop2-HARD-4 fields 琳琅诗怀雅
from round 6, coop2-NORMAL-8-serverrun 刺玫 from round 7, coop2-ABYSS-10-boosted 刺玫 from round 2,
coop2-NORMAL-14-standins 刺玫 from round 3 and 琳琅诗怀雅 in rounds 6-7 (instrumented); coop2-NORMAL-3 and coop4-FUNNY-6
(in CB2's list) still field 刺玫 and her S2 heals now reach other allies (36 / 205 heals), but no kill, leak, LP or
end stat changes, so their digests stay.

Gate: non-browser suites 4949 / 4949, npm run golden ok (283 scenarios), test/golden.test.js 9 / 9, lint 0 errors,
check:imports ok, typecheck ok.
This commit is contained in:
sganggs committed 2026-10-07 03:10:22 +08:00
commit 47079456be
34 files changed
+964 -368

No files matched your search

+2 -1
View File
@@ -84,7 +84,8 @@ export class FxSim {
this.mortar(this._viewOf(ex.id ?? ex.src), at.x, at.y, r, flight);
break;
}
case 'zone': this.zone(at.x, at.y, at.z, r, col, Math.max(0.6, dur || 1.5), spec.tex); break;
// `key`: one zone the sim re-sends as it moves / grows (an update in place, never a second layer — zones.js)
case 'zone': this.zone(at.x, at.y, at.z, r, col, Math.max(0.6, dur || 1.5), spec.tex, false, ex.key != null ? String(ex.key) : null); break;
case 'wall': {
// a line of burning tiles through the anchor tile along `axis` ('col' | 'row', from the sim event)
const rect = this.ctx.fieldRect ? this.ctx.fieldRect() : null;
+47 -7
View File
@@ -3,6 +3,29 @@
import { NO_OPTS, clamp, easeOut } from './limits.js';
/** A zone lasting longer than this (real s) is a persistent area (炼金单元, fields): drawn dimmer than a skill's short burst. */
export const ZONE_PERSIST = 3;
/** Disc / edge alpha: a short zone, a persistent one (before the overlap damping), a telegraph. */
export const ZONE_ALPHA = Object.freeze({ short: [0.28, 0.75], persist: [0.16, 0.5], warn: [0.35, 0.75] });
/**
* [disc, edge] alpha of zone `zn` among `zones` (additive blending). A persistent area is dimmer and shares its light with
* the persistent areas overlapping it — ÷ √n for n of them, centres closer than ¾ of their radii summed —, so stacked
* alchemy units read as one brighter area, not a white blot (community report of 2026-10-06 「炼金单元等范围持续性技能特效太亮
* (尤其是多层叠加以后)」). Short bursts and telegraphs keep their full strength.
*/
export function zoneAlpha(zn, zones) {
if (zn.warn) return ZONE_ALPHA.warn;
if (!(zn.dur > ZONE_PERSIST)) return ZONE_ALPHA.short;
let n = 1;
for (const o of zones) {
if (o === zn || o.warn || !(o.dur > ZONE_PERSIST)) continue;
if (Math.hypot(o.x - zn.x, o.y - zn.y) < 0.75 * (o.r + zn.r)) n++;
}
const k = 1 / Math.sqrt(n);
return [ZONE_ALPHA.persist[0] * k, ZONE_ALPHA.persist[1] * k];
}
export class FxZones {
/**
* An explosion on the ground at (x, y, z) of radius r tiles: fireball in `col`, a white flash, a shockwave and a
@@ -65,15 +88,28 @@ export class FxZones {
this.burst(g.x, g.y - g.s * 0.2, g.s, 6, tint, { speed: 2.4, life: 0.35 });
}
/** Persistent ground area: soft disc + pulsing edge ring for `dur` real seconds (telegraphs pulse faster). */
zone(x, y, z, r, tint, dur, tex = 'soft', warn = false) {
/**
* Persistent ground area: soft disc + pulsing edge ring for `dur` real seconds (telegraphs pulse faster). `key` (the
* sim fx's `key`): a zone already up under that key is updated in place — moved, resized, its end `dur` from now —
* instead of a second one stacked on it (引星棘刺 S2's drifting alchemy unit is re-sent every second: twelve layers of the
* same zone used to pile up, community report of 2026-10-06 「炼金单元等范围持续性技能特效太亮(尤其是多层叠加以后)」).
*/
zone(x, y, z, r, tint, dur, tex = 'soft', warn = false, key = null) {
if (key != null) {
const zn = this.zones.find((q) => q.key === key && q.t < q.dur);
if (zn) {
zn.x = x; zn.y = y; zn.z = z; zn.r = r; zn.tint = tint; zn.dur = zn.t + dur;
this._onGround(zn.disc, y, z); this._onGround(zn.edge, y, z);
return;
}
}
const P = this.P;
const disc = new P.Sprite(this.tex[tex === 'ring' ? 'soft' : tex] || this.tex.soft);
disc.anchor.set(0.5); disc.blendMode = P.BLEND_MODES.ADD; disc.tint = tint;
const edge = new P.Sprite(this.tex.ring);
edge.anchor.set(0.5); edge.blendMode = P.BLEND_MODES.ADD; edge.tint = tint;
this._onGround(disc, y, z); this._onGround(edge, y, z);
this.zones.push({ disc, edge, x, y, z, r, t: 0, dur, warn });
this.zones.push({ disc, edge, x, y, z, r, t: 0, dur, warn, key, tint });
if (this.zones.length > 24) this._freeZone(this.zones.shift());
}
@@ -86,6 +122,10 @@ export class FxZones {
for (const zn of this.zones) {
zn.t += dt;
if (zn.t >= zn.dur) { this._freeZone(zn); continue; }
this.zones[w++] = zn;
}
this.zones.length = w;
for (const zn of this.zones) {
const k = zn.t / zn.dur;
const grow = Math.min(1, zn.t / 0.25);
const rad = zn.r * (0.35 + 0.65 * easeOut(grow));
@@ -94,11 +134,11 @@ export class FxZones {
const rx = p.s * rad, ry = Math.max(1, p.y - q.y);
const fade = k > 0.8 ? (1 - k) / 0.2 : 1;
const pulse = zn.warn ? 0.55 + 0.45 * Math.abs(Math.sin(zn.t * 7)) : 0.8 + 0.2 * Math.sin(zn.t * 3);
zn.disc.position.set(p.x, p.y); zn.disc.scale.set((rx * 2) / 128, (ry * 2) / 128); zn.disc.alpha = (zn.warn ? 0.35 : 0.28) * fade * pulse;
zn.edge.position.set(p.x, p.y); zn.edge.scale.set((rx * 2.1) / 128, (ry * 2.1) / 128); zn.edge.alpha = 0.75 * fade * pulse;
this.zones[w++] = zn;
const [discA, edgeA] = zoneAlpha(zn, this.zones);
zn.disc.tint = zn.edge.tint = zn.tint ?? zn.disc.tint;
zn.disc.position.set(p.x, p.y); zn.disc.scale.set((rx * 2) / 128, (ry * 2) / 128); zn.disc.alpha = discA * fade * pulse;
zn.edge.position.set(p.x, p.y); zn.edge.scale.set((rx * 2.1) / 128, (ry * 2.1) / 128); zn.edge.alpha = edgeA * fade * pulse;
}
this.zones.length = w;
}
/** Flash a set of tiles ([[r,c]]) on the ground (telegraphed boxes, blast tiles). */