0.2.3: prepare follow-up fixes and client features (#407, #408, #410, #413, #431, #435, #436, #440, #444)
Add special-target activation without bypassing attack cadence, cooperative card preferences and emotes, skill sound banks, per-operator voice choices, text sizing, install prompts, closed-tab recovery and unanimous setup rerolls. Retain late deployment elapsed time and resolve echo/counter pulses without recursive hook overflow. Independent final review remains pending. Golden: roster-016, roster-035, roster-036, roster-045; boss-boss_1-pair, boss-boss_3-pair, boss-boss_5-pair, boss-boss_7-pair, hidden-boss_9-pair; solo-HARD-9-boosted: Lemuen attack cadence. diy-132: Necras extra activation targets. coop2-NORMAL-14-standins: cooperative purchase preference and simulation fixes. hidden-boss_10-solo: echo pulse FIFO resolution. See DESIGN 28.13-28.24 for assumptions, sources and contributor credit.
This commit is contained in:
1 parent
314564c139
commit
f3658a0dd4
104 files changed
+3674
-741
No files matched your search
@@ -141,6 +141,74 @@ function firstMatching(banks, event, abilities, ok = () => true) {
|
||||
const SKILL_MODE_FILE = /_(d|h|s)\d*\.mp3$/i;
|
||||
/** A bank of an operator's normal attack: none of its files belongs to a skill mode. */
|
||||
export const normalModeBank = (paths) => Array.isArray(paths) && paths.length > 0 && !paths.some((p) => SKILL_MODE_FILE.test(p));
|
||||
/**
|
||||
* The skill mode of a sound file: 'd' / 'h' / 's' for 技能1 / 2 / 3, null for a normal-mode (`_n`) file. The mode letter
|
||||
* is the official naming of a skill's own sounds — it holds for the whole chain of a skill (原声: `p_skill_lzxqlkl_s`
|
||||
* 技能3发动, `p_atk_lzxqlkl_s` 技能3的攻击, `p_imp_…_s` 技能3命中): 司霆惊蛰's three skills have the activation sounds
|
||||
* `_d` / `_h` / `_s` and the matching attack banks `p_atk_lzxqlkl_d1` / `_h` / `_s`, and every operator whose activation
|
||||
* sounds name a mode agrees with its slot (§ pickModeAttacks).
|
||||
*/
|
||||
export function skillModeLetter(path) {
|
||||
const m = SKILL_MODE_FILE.exec(typeof path === 'string' ? path : '');
|
||||
return m ? m[1].toLowerCase() : null;
|
||||
}
|
||||
|
||||
/** [ASSUMED] Slot fallback for operators without a named activation sound; DESIGN §28.17. */
|
||||
export const SLOT_MODE_LETTER = Object.freeze({ 0: 'd', 1: 'h', 2: 's' });
|
||||
|
||||
/**
|
||||
* The per-skill-mode bank of one unit, by mode letter: `{ d?: string[], h?: string[], s?: string[] }`.
|
||||
*
|
||||
* Why a second table: `pickUnitSfx` builds the operator's NORMAL attack / impact, and for that it refuses every
|
||||
* skill-mode file (`_d` / `_h` / `_s`) — 银灰's S3 swing is not his normal attack. An operator whose library is all
|
||||
* skill modes has therefore no normal bank at all: 司霆惊蛰's attack banks are `ON_ABILITY_ON…attack.1.2.1`
|
||||
* (`p_atk_lzxqlkl_d1`, 技能1), `…attack.4` (`_h2` / `_h`, 技能2) and `…attack.7` (`_s`, 技能3) — she never has a plain
|
||||
* bank, because (解放者) she only attacks while a skill runs. The official client plays that mode's own files during the
|
||||
* skill, so the manifest needs them per skill index (plan.mjs writes `sfx.units[id].attacks` / `hits` = the mode's
|
||||
* attack / impact sound, public/js/audio.js plays them while that skill is active).
|
||||
*
|
||||
* `events` picks which banks count: the attack is cast / held (`ON_ABILITY_START` preferred over `ON_ABILITY_ON`, the
|
||||
* order pickUnitSfx prefers), the impact is the hit (`ON_ABILITY_HIT`). Among equals the plain ability wins over a
|
||||
* numbered variant, then the numeric order. 赤刃明霄陈 S3 (the official banks, `attack.3`):
|
||||
* `ON_ABILITY_START` → `p_atk_hljdswd_s` (the swing: different from her normal `p_atk_hljdswd_n`),
|
||||
* `ON_ABILITY_HIT` → `p_imp_hljdswd_s` (the slash's impact: different from `p_imp_hljdswd_n`).
|
||||
* @param {Map<string,string[]>|undefined} banks unit bank table (from indexAudio().unitBanks)
|
||||
* @param {string[]} events official event prefixes to read
|
||||
* @returns {{ d?: string[], h?: string[], s?: string[] }}
|
||||
*/
|
||||
export function pickModeBanks(banks, events) {
|
||||
const cands = new Map(); // letter → [{ rank, rest, paths }]
|
||||
if (!banks || !banks.size) return {};
|
||||
for (const [name, paths] of banks) {
|
||||
const ev = events.find((e) => name.startsWith(e + '.'));
|
||||
if (!ev || !Array.isArray(paths) || !paths.length) continue;
|
||||
const ability = name.slice(ev.length + 1).split('.')[0];
|
||||
if (!/attack|combat/i.test(ability)) continue;
|
||||
const letter = skillModeLetter(paths[0]);
|
||||
if (!letter || paths.some((p) => skillModeLetter(p) !== letter)) continue;
|
||||
const rest = name.slice(ev.length + 1 + ability.length);
|
||||
const numbered = rest ? 1 : 0; // 'attack.1.2.1' / 'attack.4' vs the plain 'attack'
|
||||
const rank = `${events.indexOf(ev)}${numbered}`;
|
||||
if (!cands.has(letter)) cands.set(letter, []);
|
||||
cands.get(letter).push({ rank, rest, paths });
|
||||
}
|
||||
const out = {};
|
||||
for (const [letter, list] of cands) {
|
||||
list.sort((a, b) => (a.rank !== b.rank ? (a.rank < b.rank ? -1 : 1) : abilityOrder(a.rest, b.rest)));
|
||||
out[letter] = list[0].paths;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The per-skill-mode ATTACK bank of one unit (the swing / cast while that skill runs) — see pickModeBanks. */
|
||||
export const pickModeAttacks = (banks) => pickModeBanks(banks, ['ON_ABILITY_START', 'ON_ABILITY_ON']);
|
||||
|
||||
/**
|
||||
* The per-skill-mode IMPACT bank of one unit (the hit sound while that skill runs): 赤刃明霄陈's S3 slashes hit with
|
||||
* `p_imp_hljdswd_s`, not her normal `p_imp_hljdswd_n`. A mode without an `ON_ABILITY_HIT` bank keeps the unit's normal
|
||||
* impact (司霆惊蛰's S3 has none: only its 技能2 swings `_h` and impacts `_h`).
|
||||
*/
|
||||
export const pickModeHits = (banks) => pickModeBanks(banks, ['ON_ABILITY_HIT']);
|
||||
|
||||
/**
|
||||
* Pick role → candidate sound paths for one unit.
|
||||
|
||||
+30
-1
@@ -23,7 +23,7 @@
|
||||
|
||||
import { RAW, joinUrl, safeName, urlBase, urlDir } from './sources.mjs';
|
||||
import { kindOf } from './formats.mjs';
|
||||
import { pickUnitSfx, UI_SFX, BATTLE_SFX, resolveSpec, indexVoice, VOICE_DIRS, VOICE_BATTLE_SLOTS } from './audio.mjs';
|
||||
import { pickUnitSfx, pickModeAttacks, pickModeHits, skillModeLetter, SLOT_MODE_LETTER, UI_SFX, BATTLE_SFX, resolveSpec, indexVoice, VOICE_DIRS, VOICE_BATTLE_SLOTS } from './audio.mjs';
|
||||
import { literal } from './manifest.mjs';
|
||||
import { EMOTE_CATALOG } from '../../shared/constants.js';
|
||||
|
||||
@@ -387,6 +387,19 @@ export function buildPlan({ assets07, ops03, enemies05, maps05, audio, modelsDat
|
||||
born: audio.bank(`battle.ON_PROJECTILE_BORN.projectile_chr_${short}`), hit: audio.bank(`battle.ON_PROJECTILE_HIT.projectile_chr_${short}`) } });
|
||||
const { roles: u, mix } = unitSounds(audio, sfx);
|
||||
const skillSfx = {};
|
||||
// Per-skill ATTACK / IMPACT sound (audio.mjs pickModeAttacks / pickModeHits): a skill mode has its own swing and its
|
||||
// own hit sound, and neither may play as the unit's normal attack / impact (银灰, 纯烬艾雅法拉). An operator whose
|
||||
// every attack belongs to a skill mode has no normal bank at all (司霆惊蛰 — 解放者, she only attacks while a skill
|
||||
// runs — the report 「三技能攻击没有音效」); 赤刃明霄陈's S3 slashes swing `p_atk_hljdswd_s` and land `p_imp_hljdswd_s`
|
||||
// where her normal attack uses the `_n` files (the report 「斩击音效应该和普通攻击不一样」). The mode letter of a skill
|
||||
// index is read from that skill's own activation sound (`p_skill_lzxqlkl_s` → s = 技能3); without one (most operators
|
||||
// carry no activation sound) we infer 技能1 / 2 / 3 = d / h / s ([ASSUMED], below). The client plays
|
||||
// them while that skill is active (public/js/audio.js unit()).
|
||||
const modeAttacks = pickModeAttacks(audio.unitBanks.get(id));
|
||||
const modeHits = pickModeHits(audio.unitBanks.get(id));
|
||||
const attacks = {};
|
||||
const hits = {};
|
||||
const attackMix = {}, hitMix = {};
|
||||
for (const i of idx) {
|
||||
const s = (o.skills || []).find((k) => k.index === i);
|
||||
if (!s) { notes.push(`${id}: skill index ${i} missing in research data`); continue; }
|
||||
@@ -395,10 +408,26 @@ export function buildPlan({ assets07, ops03, enemies05, maps05, audio, modelsDat
|
||||
if (s.skillId) skillsById[s.skillId] = iconId;
|
||||
const ss = audio.skillBanks.get(s.skillId)?.get('ON_SKILL_START');
|
||||
if (ss?.length) skillSfx[String(i)] = soundLeaf(ss);
|
||||
// [ASSUMED] Without a named activation mode, infer d/h/s from slot 1/2/3; see DESIGN §28.17.
|
||||
const letter = skillModeLetter(ss?.[0]) ?? SLOT_MODE_LETTER[i];
|
||||
if (letter && modeAttacks[letter]) {
|
||||
attacks[String(i)] = soundLeaf(modeAttacks[letter]);
|
||||
const m = audio.mixOf?.(modeAttacks[letter]);
|
||||
if (m) attackMix[String(i)] = m;
|
||||
}
|
||||
if (letter && modeHits[letter]) {
|
||||
hits[String(i)] = soundLeaf(modeHits[letter]);
|
||||
const m = audio.mixOf?.(modeHits[letter]);
|
||||
if (m) hitMix[String(i)] = m;
|
||||
}
|
||||
}
|
||||
const primarySkill = skillSfx[String(idx[0])];
|
||||
if (primarySkill) u.skill = primarySkill;
|
||||
if (Object.keys(skillSfx).length > 1) u.skills = skillSfx;
|
||||
if (Object.keys(attacks).length) u.attacks = attacks;
|
||||
if (Object.keys(hits).length) u.hits = hits;
|
||||
if (Object.keys(attackMix).length) u.attackMix = attackMix;
|
||||
if (Object.keys(hitMix).length) u.hitMix = hitMix;
|
||||
if (mix) u.mix = mix;
|
||||
if (Object.keys(u).length) unitsSfx[id] = u;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,9 @@ function runOne(cfg, seed) {
|
||||
}, { maxSteps: 5e6 });
|
||||
rec.ms = performance.now() - t0;
|
||||
rec.errors = errors.length + m.errorCount;
|
||||
// Keep diagnostics in the JSON so a nonzero benchmark count is actionable.
|
||||
if (errors.length) rec.errorMessages = errors;
|
||||
if (m.errors.length) rec.matchErrors = m.errors;
|
||||
rec.victory = !!(summary && summary.victory);
|
||||
rec.roundsPassed = summary ? summary.roundsPassed : 0;
|
||||
rec.hidden = !!(summary && summary.hiddenReached);
|
||||
|
||||
Reference in new issue
Block a user