卫戍协议:盟约 web remake — initial import

Online co-op browser remake of Arknights' Stronghold Protocol: Alliance
(client-simulated combat, official waves/bonds/items/strategies, 3D board,
facing wheel, operator loadout, official emotes, deployment scripts).
This commit is contained in:
sganggs committed 2026-09-29 16:21:31 +08:00
commit 01a44cd288
408 files changed
+308986

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
<!doctype html>
<!--
In-match UI mock harness (dev only): drives the real game screens (js/screens/game.js …) with
fabricated m.public / m.private / m.field / b.snap / b.ev streams and a tiny mock server behind
net.request, so every phase can be developed and screenshot without the game server.
URL params: ?phase=PREP|INFO_CHECK|BAND_DRAFT|BATTLE_CHECK|SP_DRAFT|COMBAT|UNITE|SETTLE|FINAL_ASSAULT|HIDDEN_CORE|RESULT
&variant=reward|bounty|supply|tactic|temp|frozen|dead|defeat|solo|popup|detail|drawer|info|pen|emote|settings|collapsed|bond|drag
|overtime|drain (boss rounds: level time over / overtime drain) |paused (solo battles)
&shot=1 (hide the switcher) &render=engine|fallback (default: the Pixi engine, DOM fallback if it fails)
Keyboard: ` toggles the switcher.
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<title>Game Mock · 卫戍协议:盟约</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="/fonts/fonts.css" />
<link rel="stylesheet" href="/css/theme.css" />
<link rel="stylesheet" href="/css/components.css" />
<link rel="stylesheet" href="/css/screens/game.css" />
<link rel="stylesheet" href="/css/screens/game-shop.css" />
<link rel="stylesheet" href="/css/screens/game-panels.css" />
<link rel="stylesheet" href="/css/screens/briefing.css" />
<link rel="stylesheet" href="/css/screens/draft.css" />
<link rel="stylesheet" href="/css/screens/result.css" />
<link rel="stylesheet" href="/css/screens/guide.css" />
<link rel="stylesheet" href="/css/emotes.css" />
<link rel="stylesheet" href="/css/devices.css" />
<script type="importmap">
{ "imports": { "preact": "/vendor/preact.module.js", "preact/hooks": "/vendor/hooks.module.js", "htm": "/vendor/htm.module.js" } }
</script>
<style>
.mockbar { position: fixed; z-index: 150; right: 8px; top: 50%; transform: translateY(-50%); width: 188px; max-height: 92vh; overflow: auto;
display: flex; flex-direction: column; gap: 4px; padding: 8px; background: rgba(6, 9, 8, .92); border: 1px solid #3e4b45; font: 12px/1.3 sans-serif; color: #c3cbc7; }
.mockbar h4 { margin: 6px 0 2px; font-size: 11px; letter-spacing: .12em; color: #4ed8af; }
.mockbar button { text-align: left; padding: 4px 6px; background: #141816; border: 1px solid #2f3a35; color: #f2f2f2; font-size: 12px; cursor: pointer; }
.mockbar button:hover { border-color: #4ed8af; }
.mockbar button.on { background: #145443; border-color: #4ed8af; }
.mockbar.hidden { display: none; }
</style>
</head>
<body>
<div id="app"></div>
<div id="mockbar" class="mockbar"></div>
<script type="module" src="/dev/game-mock.js"></script>
</body>
</html>
+726
View File
@@ -0,0 +1,726 @@
// Mock harness for the in-match UI (see game-mock.html). Fabricates a 4-player co-op match from the real
// data files and answers `g.*` intents with a small in-browser mock server (buy / sell / move / equip /
// refresh / freeze / level / ready / reward / choice / band …), so drag & drop and every button can be
// exercised. Combat phases stream b.snap / b.ev at 20 Hz through net._emit like the real socket.
import '/js/ui/compat.js';
import { render } from '/vendor/preact.module.js';
import { installDeviceSupport } from '/js/ui/device.js';
import { html, UiHosts } from '/js/ui/components.js';
import { ToastHost, toast } from '/js/ui/toasts.js';
import { ConnectionBanner } from '/js/ui/connBanner.js';
import { GuideHost } from '/js/ui/guide.js';
import { GameScreen } from '/js/screens/game.js';
import { store, emptyMatch, selectRoute } from '/js/store.js';
import { net, NetError } from '/js/net.js';
import { data } from '/js/data.js';
import { installAudio } from '/js/audio.js';
import { settingsStore } from '/js/ui/settings.js';
import { awayStore } from '/js/ui/matchChrome.js';
import { GAME_FILES } from '/js/ui/gameComponents.js';
import { PHASE, GEO } from '/shared/constants.js';
const params = new URLSearchParams(location.search);
const SHOT = params.get('shot') === '1';
const VARIANTS = new Set((params.get('variant') || '').split(',').filter(Boolean));
// ---- deterministic rng ----------------------------------------------------------------------------------
let seed = 20260927;
const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 2 ** 32; };
const pick = (arr) => arr[Math.floor(rnd() * arr.length)];
const shuffle = (arr) => { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; };
const ME = 'p1';
const PLAYERS = [
{ playerId: 'p1', seat: 0, name: '凯尔希', isBot: false, bandId: 'band_bldsk' },
{ playerId: 'ai_2', seat: 1, name: 'AI·华法琳', isBot: true, bandId: 'band_sarkazb' },
{ playerId: 'p3', seat: 2, name: 'Doctor·B', isBot: false, bandId: 'band_amiya' },
{ playerId: 'p4', seat: 3, name: '灰烬', isBot: false, bandId: 'band_amedic' },
];
let uidSeq = 100;
const nextUid = () => ++uidSeq;
let S = null; // mock server state
// ---- builders ------------------------------------------------------------------------------------------------
function visibleChess(tier) {
return data.list('chess').filter((c) => c.visible && !c.isGolden && (!tier || c.tier === tier));
}
function shopItems() { return data.list('items').filter((i) => !i.isGolden && i.itemType === 'EQUIP' && !i.hideInShop); }
function makeSlot(level) {
const tier = Math.max(1, Math.min(level, 1 + Math.floor(rnd() * level)));
const c = pick(visibleChess(tier));
const base = c.price;
const r = rnd();
const price = r < 0.15 ? Math.max(1, base - 1) : r > 0.9 ? base + 1 : base;
return { kind: 'chess', id: c.chessId, price, basePrice: base, sold: false };
}
function makeItemSlot() {
const it = pick(shopItems());
return { kind: 'item', id: it.id, price: it.price, basePrice: it.price, sold: false };
}
function chessPiece(c, golden = false) {
const id = golden ? (c.goldenId || c.chessId) : c.chessId;
return { uid: nextUid(), kind: 'chess', id, golden, tier: c.tier, items: [] };
}
function itemPiece(it) { return { uid: nextUid(), kind: 'item', id: it.id, golden: !!it.isGolden, tier: it.tier }; }
function computeBonds(priv) {
const counts = new Map();
const seen = new Set();
for (const p of priv.board) {
if (p.kind !== 'chess') continue;
const c = data.lookup('chess', p.id);
const base = c?.baseId || p.id;
if (seen.has(base)) continue;
seen.add(base);
for (const b of c?.bonds || []) counts.set(b, (counts.get(b) || 0) + 1);
}
const out = [];
for (const [bondId, count] of counts) {
const b = data.lookup('bonds', bondId);
const thresholds = b?.thresholds || [2];
let tier = 0;
for (const t of thresholds) if (count >= t) tier++;
const layers = S?.layers?.[bondId] ?? (tier ? Math.floor(rnd() * 40) + 4 : Math.floor(rnd() * 6));
if (S) S.layers[bondId] = layers;
out.push({ bondId, count, active: tier > 0, tier, layers, thresholds, countsHand: !!b?.countsHand });
}
return out.sort((a, b) => (b.active - a.active) || (b.layers - a.layers));
}
function refreshPrivate() {
const p = S.priv;
p.deployCount = p.board.filter((x) => x.kind === 'chess').length;
p.bonds = computeBonds(p);
p.canReady = !p.temp.some(Boolean);
const upg = [5, 8, 11, 12, 13];
p.shop.maxLevel = 6;
if (p.shop.upgradePrice == null) p.shop.upgradePrice = upg[p.shop.level - 1] ?? 0;
p.shop.refreshPrice = p.shop.freeRefreshes > 0 ? 0 : 1;
store.patch('match', { private: JSON.parse(JSON.stringify(p)) });
const me = S.pub.players.find((x) => x.playerId === ME);
if (me) { me.bonds = p.bonds; me.boardCount = p.deployCount; me.lp = p.lp; me.ready = p.ready; if (S.pub.phase === PHASE.PREP) me.status = p.ready ? 'ready' : 'acting'; }
pushPublic();
}
function pushPublic() {
S.pub.serverNow = Date.now();
store.patch('match', { public: JSON.parse(JSON.stringify(S.pub)) });
}
function buildState() {
uidSeq = 100;
seed = 20260927;
const stage = data.lookup('stages', 'act2autochess_m01');
const melee = stage.deployTiles.normal.melee.slice();
const ranged = stage.deployTiles.normal.rangedOnly.slice();
const bonds = data.list('bonds');
const core = shuffle(bonds.filter((b) => b.isCore && b.weight > 0)).slice(0, 3).map((b) => b.bondId);
const addon = shuffle(bonds.filter((b) => !b.isCore && b.weight > 0)).slice(0, 4).map((b) => b.bondId);
const disabled = new Set([...core, ...addon]);
const banned = visibleChess().filter((c) => c.bonds.length && c.bonds.every((b) => disabled.has(b))).map((c) => c.chessId);
const pool = visibleChess().filter((c) => !banned.includes(c.chessId));
// board: 7 units on legal tiles
const board = [];
const used = new Set();
const preferBond = pool.filter((c) => c.bonds.includes('yanShip') || c.bonds.includes('preciShip'));
const lineup = shuffle(preferBond).slice(0, 5).concat(shuffle(pool).slice(0, 4));
for (const c of lineup) {
if (board.length >= 7) break;
const tiles = c.position === 'MELEE' ? melee : [...ranged, ...melee];
const t = tiles.find((x) => !used.has(`${x[0]},${x[1]}`));
if (!t) continue;
used.add(`${t[0]},${t[1]}`);
const golden = board.length === 1;
const piece = { ...chessPiece(c, golden), row: t[0], col: t[1] };
if (board.length === 0) piece.items = [{ uid: nextUid(), id: shopItems()[3].id }, { uid: nextUid(), id: shopItems()[9].id }];
if (board.length === 2) piece.items = [{ uid: nextUid(), id: shopItems()[5].id }];
board.push(piece);
}
const hand = new Array(GEO.HAND_SIZE).fill(null);
const hc = shuffle(pool).slice(0, 5);
hand[9] = chessPiece(hc[0]);
hand[8] = chessPiece(hc[1]);
hand[7] = chessPiece(board[3] ? data.lookup('chess', board[3].id) : hc[2]); // a merge pair
hand[6] = chessPiece(hc[3], true);
hand[5] = itemPiece(shopItems()[12]);
hand[4] = itemPiece(shopItems()[20]);
const tokenOwner = data.list('tokens').find((t) => t.tokenId === 'token_10028_vigil_wolf');
if (tokenOwner) hand[3] = { uid: nextUid(), kind: 'token', id: tokenOwner.tokenId, golden: false, count: 2 };
const temp = new Array(GEO.TEMP_SIZE).fill(null);
if (VARIANTS.has('temp')) { temp[0] = chessPiece(hc[4]); temp[1] = itemPiece(shopItems()[2]); }
const level = 4;
const slots = [];
for (let i = 0; i < 5; i++) slots.push(makeSlot(level));
slots[2].sold = true;
// make one card complete a merge
if (board[4]) { const c4 = data.lookup('chess', board[4].id); slots[0] = { kind: 'chess', id: c4.chessId, price: c4.price, basePrice: c4.price, sold: false }; hand[2] = chessPiece(c4); }
slots.push(makeItemSlot());
const enemies = data.list('enemies');
const byRank = (r) => enemies.filter((e) => e.rank === r && !e.key.includes('template'));
// m.private.nextEnemies (server/match/waves.js previewOf): one entry per spawn action, in spawn order
const flyOf = (e) => e.stats?.motion === 'FLY' || !!e.isFlyEnemy;
const nextEnemies = [
...shuffle(byRank('NORMAL')).slice(0, 4).map((e, i) => ({ enemyKey: e.key, count: [8, 6, 4, 3][i], gate: i % 2 ? 'upper' : 'lower', t: [3, 5, 12, 15][i], fly: flyOf(e), elite: false, boss: false, source: 'wave', tag: null })),
...shuffle(byRank('ELITE')).slice(0, 2).map((e, i) => ({ enemyKey: e.key, count: 1, gate: i ? 'upper' : 'lower', t: 18 + i * 6, fly: flyOf(e), elite: true, boss: false, source: 'wave', tag: null })),
(() => { const e = pick(byRank('ELITE')); return { enemyKey: e.key, count: 1, gate: 'lower', t: 20, fly: flyOf(e), elite: true, boss: false, source: 'bounty', tag: 'bounty' }; })(),
].sort((a, b) => a.t - b.t);
const priv = {
playerId: ME, seat: 0, alive: !VARIANTS.has('dead'), lp: 24, funds: 13, bandId: 'band_bldsk', ready: false, canReady: true,
shop: { level, maxLevel: 6, upgradePrice: 9, refreshPrice: 1, freeRefreshes: 0, frozen: VARIANTS.has('frozen'), slots, rewardOffer: null },
hand, temp, board, deployCap: 8, deployCount: 0, bonds: [],
effects: [
{ id: 'aceffect_band_21', name: '重点监护', desc: data.lookup('bands', 'band_bldsk')?.descRaw || '', iconKind: 'band', iconId: 'band_bldsk' },
{ id: 'allybuff_select_5', name: '补给', desc: '获得<@ba.vup>2</>次免费刷新机会,若存在其他队友则他们也获得', iconKind: 'team', iconId: 'icon_team_buff', counter: 2 },
{ id: 'enemyeffect_3', name: '悬赏·飞行I', desc: '为自身<@ba.vup>下场作战</>添加1只悬赏敌人,将其击倒者获得<@ba.vup>1</>资金', iconKind: 'choice', iconId: 'icon_player_buff', counter: 1 },
],
nextEnemies,
stats: { dmgDealt: 184230, kills: 96, leaks: 3, gold: 58, refreshes: 11, merges: 3 },
};
if (VARIANTS.has('loadout')) priv.loadout = mockLoadout(board, slots);
if (VARIANTS.has('reward')) {
priv.shop.rewardOffer = { tier: 5, slots: shuffle(visibleChess(5)).slice(0, 3).map((c) => ({ kind: 'chess', id: c.chessId, price: 0, sold: false })) };
}
const players = PLAYERS.map((p, i) => ({
...p, connected: i !== 3 || true, alive: true, lp: [24, 31, 18, 5][i], shopLevel: [4, 5, 3, 4][i], boardCount: [7, 8, 6, 7][i],
ready: [false, true, false, false][i], bonds: [], fieldId: `n:${p.playerId}`, status: ['acting', 'ready', 'acting', 'acting'][i],
}));
players[3].connected = false;
// boss-round prep (最终攻势 prep on the own half of the boss field): `boss` = the left half; `bossR` = seats swapped so
// I am the second of the first pair → the mirrored right half (server pairPlayers: alive players by seat, in pairs)
if (VARIANTS.has('bossR')) { players[0].seat = 1; players[1].seat = 0; }
if (VARIANTS.has('dead')) { players[0].alive = false; players[0].status = 'dead'; players[0].lp = 0; priv.lp = 0; }
const pub = {
phase: PHASE.PREP, round: 6, lastRound: 14, bossRound: 14, hiddenRound: 15, deadline: Date.now() + 74000, serverNow: Date.now(), modeId: 'mode_multi_hard', difficulty: 'HARD',
stageId: 'act2autochess_m01', factions: ['FLY', 'TIMES', 'SPECIAL'], disabledBonds: [...disabled], bannedChess: banned, bossId: 'boss_5',
hiddenBossId: 'boss_8', teamLp: null, bossHp: null, draft: null, sp: null, players, fields: [],
};
if (VARIANTS.has('boss') || VARIANTS.has('bossR')) pub.round = pub.bossRound;
S = { stage, pub, priv, layers: {}, battle: null, pool, requests: [] };
}
/**
* `loadout` variant (DESIGN §16): the first board operator and the first shop card fight with a non-default skill, the
* elite board operator with its module unequipped (m.private.loadout). Data without `skills[]` / `modules[]` (before the
* loadout data build) gets a stand-in second skill / module list on those records — mock only.
*/
function mockLoadout(board, slots) {
const out = {};
const withSkills = (c) => {
if (!c) return null;
const recs = [c, c.goldenId ? data.lookup('chess', c.goldenId) : null, c.baseId && c.baseId !== c.chessId ? data.lookup('chess', c.baseId) : null].filter(Boolean);
for (const r of recs) {
if (Array.isArray(r.skills) && r.skills.length > 1) continue;
const d = r.skill || {};
const alt = { ...d, index: (d.index ?? 0) + 1, skillId: `${d.skillId || 'sk'}_mockalt`, iconId: d.iconId || r.assets?.skillIcon || d.skillId, name: `${d.name || '技能'}·改`, isDefault: false,
desc: '(模拟)调配的第二技能:攻击力+60%,持续 20 秒', descRaw: '(模拟)调配的第二技能:攻击力<@ba.vup>+60%</>,持续 20 秒', spCost: 30, initSp: 10, duration: 20 };
r.skills = [{ ...d, isDefault: true }, alt];
}
const base = c.isGolden ? data.lookup('chess', c.baseId) || c : c;
const alt = (base.skills || []).find((x) => x && !x.isDefault);
return alt ? { base, alt } : null;
};
const b0 = board[0] ? data.lookup('chess', board[0].id) : null;
const s0 = slots.find((x) => x && x.kind === 'chess' && !x.sold);
for (const c of [b0, s0 ? data.lookup('chess', s0.id) : null]) {
const r = withSkills(c);
if (r) out[r.base.chessId] = { skill: r.alt.index, module: r.base.goldenId ? 'none' : null };
}
const gold = board.find((p) => p.golden);
const g = gold ? data.lookup('chess', gold.id) : null;
if (g && g.isGolden) {
const r = withSkills(g);
if (g.module && !Array.isArray(g.modules)) g.modules = [{ uniEquipId: g.module.id, name: g.module.name, typeName: g.module.type, isDefault: true }];
if (r) out[r.base.chessId] = { skill: r.alt.index, module: 'none' };
}
return out;
}
// ---- phases ---------------------------------------------------------------------------------------------------------
function setPhase(phase, variant) {
stopBattle();
buildState();
for (const v of (variant || '').split(',').filter(Boolean)) VARIANTS.add(v);
if (VARIANTS.has('reward') && !S.priv.shop.rewardOffer) S.priv.shop.rewardOffer = { tier: 5, slots: shuffle(visibleChess(5)).slice(0, 3).map((c) => ({ kind: 'chess', id: c.chessId, price: 0, sold: false })) };
if (VARIANTS.has('temp') && !S.priv.temp.some(Boolean)) { S.priv.temp[0] = chessPiece(pick(S.pool)); S.priv.temp[1] = itemPiece(shopItems()[2]); }
const pub = S.pub;
pub.phase = phase;
store.set({ match: emptyMatch(), ticker: [], emotes: [] });
awayStore.set({ away: false });
const solo = VARIANTS.has('solo');
if (solo) {
pub.players = pub.players.slice(0, 1);
pub.modeId = 'mode_single_hard';
store.set({ room: { ...store.get().room, mode: 'solo' } });
} else {
store.set({ room: { ...store.get().room, mode: 'coop' } });
}
switch (phase) {
case PHASE.INFO_CHECK:
pub.round = 0; pub.deadline = Date.now() + 21000;
pub.players.forEach((p, i) => { p.ready = i === 1 || i === 2; p.status = p.ready ? 'ready' : 'deciding'; p.bandId = null; p.lp = 0; });
S.priv.bandId = null;
break;
case PHASE.BAND_DRAFT:
pub.round = 0; pub.deadline = Date.now() + 9000;
pub.players.forEach((p) => { p.bandId = null; p.status = 'deciding'; p.ready = false; });
S.priv.bandId = null;
if (!solo) {
pub.draft = { order: ['p3', 'p1', 'ai_2', 'p4'], turn: 'p1', picks: { p3: 'band_sarkazb' }, skipsLeft: { p1: 1, p3: 1, ai_2: 1, p4: 1 }, turnDeadline: Date.now() + 9000 };
pub.players.find((p) => p.playerId === 'p3').bandId = 'band_sarkazb';
} else pub.deadline = 0;
break;
case PHASE.BATTLE_CHECK:
pub.round = 0; pub.deadline = Date.now() + 3000;
break;
case PHASE.SP_DRAFT: {
pub.round = 9; pub.deadline = Date.now() + 14000;
const fam = VARIANTS.has('supply') ? 'supply' : VARIANTS.has('tactic') ? 'tactic' : 'bounty';
const ch = data.get('choices');
let cards;
if (fam === 'supply') cards = shuffle(shopItems().filter((i) => i.tier >= 3)).slice(0, 6).map((i) => ({ itemId: i.id }));
else if (fam === 'tactic') cards = shuffle(ch.cards.tactic.filter((t) => t.kind !== 'terrain')).slice(0, 6).map((t) => ({ effectId: t.effectId }));
else cards = shuffle(ch.cards.bounty.filter((b) => !b.name.includes('战术特训'))).slice(0, 6).map((b) => ({ effectId: b.effectId }));
if (solo) { cards = cards.slice(0, 3); pub.deadline = 0; }
pub.sp = { family: fam, cards, order: solo ? ['p1'] : ['p4', 'p1', 'ai_2', 'p3'], turn: solo ? 'p1' : 'p1', picks: solo ? {} : { p4: 2 }, untimed: solo };
pub.players.forEach((p) => { p.status = p.playerId === 'p4' ? 'ready' : 'deciding'; });
break;
}
case PHASE.COMBAT: case PHASE.UNITE: case PHASE.SETTLE: case PHASE.FINAL_ASSAULT: case PHASE.HIDDEN_CORE:
startCombat(phase);
break;
case PHASE.RESULT:
pub.round = 14;
store.patch('match', { result: buildResult(!VARIANTS.has('defeat')) });
break;
default:
break;
}
refreshPrivate();
applyUiVariants();
}
// m.result in the exact shape of server/match/results.js buildResult (title = config.titles record, stats.gold = spent)
function buildResult(victory) {
const titles = (data.get('config')?.titles || []).filter((t) => victory || !t.onlyOnWin);
const pickTitle = (i) => { const t = titles[[0, 3, 5, 2][i] % Math.max(1, titles.length)]; return t ? { id: t.id, name: t.name, picId: t.picId, text: t.text } : null; };
return {
victory, roundsPassed: victory ? 14 : 11, hiddenReached: false, hiddenCleared: false, reason: victory ? 'victory' : 'defeat',
bossId: 'boss_5', hiddenBossId: 'boss_8', difficulty: 'HARD', modeId: 'mode_multi_hard', stageId: S.pub.stageId, durationMs: 52 * 60000,
players: S.pub.players.map((p, i) => ({
playerId: p.playerId, seat: p.seat, name: p.name, isBot: p.isBot, left: false, alive: i !== 3, victory, lp: [12, 12, 12, 0][i], bandId: p.bandId,
roundsPassed: i === 3 ? 9 : victory ? 14 : 11, eliminatedRound: i === 3 ? 10 : null, title: pickTitle(i),
lineup: shuffle(S.pool).slice(0, 8 + (i % 2)).map((c, k) => ({ id: k < 3 ? c.goldenId : c.chessId, golden: k < 3, tier: c.tier, row: 9 + (k % 4), col: 2 + k, items: [] })),
bonds: shuffle(data.list('bonds')).slice(0, 4).map((b, k) => ({ bondId: b.bondId, count: 3 - (k % 3), layers: [359, 136, 34, 12][k], active: k < 3, tier: k < 3 ? 1 : 0 })),
stats: {
dmgDealt: [2310000, 1720000, 980000, 402000][i], kills: [412, 388, 301, 150][i], leaks: [3, 6, 9, 31][i], gold: [188, 164, 231, 90][i],
refreshes: [22, 18, 30, 8][i], merges: [7, 5, 9, 2][i], itemsEquipped: [9, 6, 11, 3][i], bossDamage: victory ? [912000, 610000, 240000, 0][i] : 0,
activatedLayers: [541, 402, 377, 120][i], lpLost: [16, 19, 12, 24][i], perfectRounds: [11, 12, 9, 4][i],
},
trophies: i === 3 ? 1 : 5, reward: i === 3 ? 170 : 425,
})),
};
}
// ---- combat simulation ------------------------------------------------------------------------------------------------
function stopBattle() { if (S?.battle?.timer) clearInterval(S.battle.timer); if (S) S.battle = null; }
function startCombat(phase) {
const pub = S.pub;
const boss = phase === PHASE.FINAL_ASSAULT || phase === PHASE.HIDDEN_CORE;
pub.round = boss ? (phase === PHASE.HIDDEN_CORE ? 15 : 14) : 6;
pub.deadline = boss ? Date.now() + 95000 : Date.now() + 40000;
// boss rounds (server Match: deadline = the level's 120 s countdown, overtimeAt = the drain start at 150 s):
// `overtime` = the level time ran out, the drain starts in 18 s; `drain` = draining for 7 s
if (boss) {
const now = Date.now();
if (VARIANTS.has('drain')) { pub.deadline = now - 37000; pub.overtimeAt = now - 7000; }
else if (VARIANTS.has('overtime')) { pub.deadline = now - 12000; pub.overtimeAt = now + 18000; }
else pub.overtimeAt = pub.deadline + 30000;
}
// solo pause (g.pause → m.public.paused)
if (VARIANTS.has('paused')) pub.paused = true;
pub.players.forEach((p, i) => { p.status = phase === PHASE.SETTLE ? 'done' : i === 1 ? 'done' : 'combat'; });
if (phase === PHASE.UNITE) { pub.players[1].status = 'helping'; pub.players[0].status = 'helping'; }
if (VARIANTS.has('done')) pub.players[0].status = 'done';
let fields;
let rect;
let kind;
if (boss) {
kind = phase === PHASE.HIDDEN_CORE ? 'hidden' : 'boss';
fields = pub.players.length === 1 ? [{ fieldId: 'b1', kind, players: ['p1'], live: true }]
: [{ fieldId: 'b1', kind, players: ['p1', 'ai_2'], live: true }, { fieldId: 'b2', kind, players: ['p3', 'p4'], live: true }];
rect = { ...GEO.BOSS_RECT };
pub.teamLp = 61;
pub.bossHp = { hp: 1_240_000, max: 1_990_000 };
} else if (phase === PHASE.UNITE) {
kind = 'unite';
fields = [{ fieldId: 'u', kind: 'unite', players: ['p1', 'ai_2'], live: true }];
rect = { ...GEO.UNITE_RECT };
} else {
kind = 'normal';
fields = pub.players.map((p) => ({ fieldId: `n:${p.playerId}`, kind: 'normal', players: [p.playerId], live: true }));
rect = { ...GEO.NORMAL_RECT };
}
pub.fields = fields;
// like the server's m.public players[].fieldId (Match.fieldOf): the field each player fights on, null without one
for (const p of pub.players) p.fieldId = fields.find((f) => f.players.includes(p.playerId))?.fieldId ?? null;
const fieldRef = { id: fields[0].fieldId };
const fieldId = fieldRef.id;
const units = [];
let id = 1;
const board = S.priv.board;
for (const p of board) {
const c = data.lookup('chess', p.id);
const colOff = 0;
const r = boss ? p.row - 7 : p.row;
units.push({ id: id++, kind: 'op', side: 'ally', ownerId: ME, defId: p.id, name: c?.name, tier: c?.tier, golden: !!p.golden, spine: c?.charId, avatar: c?.assets?.avatar, x: p.col + colOff, y: r, facing: 1, maxHp: c?.stats?.maxHp || 1000, uid: p.uid });
}
if (kind !== 'normal') {
for (const p of board.slice(0, 5)) {
const c = data.lookup('chess', pick(S.pool).chessId);
const r = boss ? p.row - 7 : p.row;
units.push({ id: id++, kind: 'op', side: 'ally', ownerId: 'ai_2', defId: c.chessId, name: c.name, tier: c.tier, golden: false, spine: c.charId, avatar: c.assets.avatar, x: 20 - p.col, y: r, facing: -1, maxHp: c.stats.maxHp });
}
}
const enemyKeys = S.priv.nextEnemies.map((e) => e.enemyKey);
const enemies = [];
const nEnemy = boss ? 10 : 12;
for (let i = 0; i < nEnemy; i++) {
const key = enemyKeys[i % enemyKeys.length];
const e = data.lookup('enemies', key);
const y = boss ? [2, 5, 3][i % 3] : [9, 12][i % 2];
const startX = boss ? (i % 2 ? 17 : 3) : 10;
enemies.push({ id: id++, kind: 'enemy', side: 'enemy', ownerId: ME, defId: key, name: e?.name, spine: key, avatar: key, x: startX + rnd() * 0.3, y, facing: -1, maxHp: e?.stats?.maxHp || 3000, hp: e?.stats?.maxHp || 3000, spawnAt: i * 0.9, dir: boss ? (i % 2 ? -1 : 1) : -1, dead: false });
}
if (boss) {
const bossRec = data.lookup('bosses', phase === PHASE.HIDDEN_CORE ? 'boss_8' : 'boss_5');
const e = data.lookup('enemies', bossRec.enemyKey);
enemies.push({ id: id++, kind: 'enemy', side: 'enemy', ownerId: ME, defId: bossRec.enemyKey, name: bossRec.name, spine: bossRec.enemyKey, avatar: bossRec.enemyKey, x: 10, y: 3, facing: -1, maxHp: e?.stats?.maxHp || 1e5, hp: (e?.stats?.maxHp || 1e5) * 0.62, spawnAt: 0, dir: 0, boss: true, dead: false });
}
const field = { fieldId, kind, rect, stageId: pub.stageId, units: units.map((u) => ({ ...u })) };
store.patch('match', { field });
const allyState = units.map((u) => ({ ...u, hp: u.maxHp * (0.55 + rnd() * 0.45), sp: rnd() * 20, spMax: 20 }));
const t0 = performance.now();
const total = enemies.length;
let killed = phase === PHASE.SETTLE ? total : 0;
if (phase === PHASE.SETTLE) enemies.forEach((e) => { e.dead = true; });
const spawned = new Set();
let pausedFor = 0;
let pausedSince = null;
const tick = () => {
// a paused solo battle stands still (the server's runner stops the clock too)
if (S.pub.paused) { if (pausedSince == null) pausedSince = performance.now(); return; }
if (pausedSince != null) { pausedFor += performance.now() - pausedSince; pausedSince = null; }
const t = (performance.now() - t0 - pausedFor) / 1000 + 4;
const ev = [];
for (const e of enemies) {
if (e.dead || t < e.spawnAt) continue;
if (!spawned.has(e.id)) { spawned.add(e.id); ev.push(['spawn', { ...e }]); }
if (e.dir) e.x = Math.max(rect.c0 + 2.2, Math.min(rect.c1 - 2, e.x + e.dir * 0.035));
}
// attacks
const liveEnemies = enemies.filter((e) => !e.dead && t >= e.spawnAt);
for (const a of allyState) {
if (!liveEnemies.length || rnd() > 0.2) continue;
const tgt = liveEnemies[Math.floor(rnd() * liveEnemies.length)];
const dmg = Math.round(200 + rnd() * 700);
tgt.hp -= e2boss(tgt) ? dmg * 3 : dmg;
ev.push(['atk', a.id, tgt.id, 'arrow'], ['dmg', tgt.id, dmg, rnd() < 0.3 ? 'arts' : 'phys']);
a.sp = (a.sp + 1) % a.spMax;
if (tgt.hp <= 0 && !tgt.boss && !tgt.dead) { tgt.dead = true; killed++; ev.push(['die', tgt.id]); }
}
if (rnd() < 0.15) { const a = pick(allyState); a.hp = Math.min(a.maxHp, a.hp + 180); ev.push(['heal', a.id, 180]); }
const snapUnits = [
...allyState.map((a) => [a.id, a.x, a.y, a.hp, a.maxHp, a.sp, a.spMax, a.sp > 15 ? 16 : 0, 2]),
...enemies.filter((e) => !e.dead && t >= e.spawnAt).map((e) => [e.id, e.x, e.y, Math.max(1, e.hp), e.maxHp, 0, 0, e.y === 5 ? 512 : 0, e.dir ? 1 : 2]),
];
// wire frames exactly like server/match/fields.js: `t` is the frame type, game time travels as `gt`; b.ev first
const snap = { t: 'b.snap', fieldId: fieldRef.id, gt: t, units: snapUnits, dp: Math.min(99, 10 + Math.floor(t)), killed, total };
if (boss) snap.boss = { hp: S.pub.bossHp.hp, max: S.pub.bossHp.max };
if (ev.length) net._emit('b.ev', { t: 'b.ev', fieldId: fieldRef.id, gt: t, ev });
net._emit('b.snap', snap);
};
const e2boss = (e) => !!e.boss;
S.battle = { timer: setInterval(tick, 50), fieldRef };
setTimeout(tick, 60);
}
// ---- mock server --------------------------------------------------------------------------------------------------------
const fail = (code) => { throw new NetError(code); };
function findPiece(uid) {
const p = S.priv;
let i = p.board.findIndex((x) => x.uid === uid);
if (i >= 0) return { area: 'board', i, piece: p.board[i] };
i = p.hand.findIndex((x) => x && x.uid === uid);
if (i >= 0) return { area: 'hand', i, piece: p.hand[i] };
i = p.temp.findIndex((x) => x && x.uid === uid);
if (i >= 0) return { area: 'temp', i, piece: p.temp[i] };
return null;
}
function removeAt(loc) {
const p = S.priv;
if (loc.area === 'board') p.board.splice(loc.i, 1);
else if (loc.area === 'hand') p.hand[loc.i] = null;
else p.temp[loc.i] = null;
}
function freeHandIdx() { for (let i = GEO.HAND_SIZE - 1; i >= 0; i--) if (!S.priv.hand[i]) return i; return -1; }
async function mockRequest(t, f = {}) {
await new Promise((r) => setTimeout(r, 40 + Math.random() * 60));
if (!S) fail('WRONG_PHASE');
S.requests.push([t, JSON.parse(JSON.stringify(f ?? {}))]); // E2E: what the UI sent
const p = S.priv;
const pub = S.pub;
const prepOnly = () => { if (pub.phase !== PHASE.PREP) fail('WRONG_PHASE'); if (p.ready && t !== 'g.ready') fail('ALREADY'); };
switch (t) {
case 'g.infoReady': { const me = pub.players.find((x) => x.playerId === ME); me.ready = true; me.status = 'ready'; pushPublic(); return {}; }
case 'g.band': {
p.bandId = f.bandId; const me = pub.players.find((x) => x.playerId === ME); me.bandId = f.bandId;
if (pub.draft) { pub.draft.picks[ME] = f.bandId; pub.draft.turn = 'ai_2'; pub.draft.turnDeadline = Date.now() + 12000; }
refreshPrivate(); return {};
}
case 'g.bandSkip': {
if (!pub.draft) fail('WRONG_PHASE');
if (pub.draft.skipsLeft?.[ME] === 0) fail('ALREADY');
pub.draft.order = pub.draft.order.filter((x) => x !== ME).concat(ME); pub.draft.skipsLeft = { ...pub.draft.skipsLeft, [ME]: 0 }; pub.draft.turn = 'ai_2'; pushPublic(); return {};
}
case 'g.buy': {
prepOnly();
const s = p.shop.slots[f.slot];
if (!s || s.sold) fail('SOLD_OUT');
if (s.price > p.funds) fail('NO_FUNDS');
const idx = freeHandIdx();
if (idx < 0) fail('HAND_FULL');
p.funds -= s.price; s.sold = true;
p.hand[idx] = s.kind === 'item' ? itemPiece(data.lookup('items', s.id)) : chessPiece(data.lookup('chess', s.id));
refreshPrivate(); return {};
}
case 'g.refresh': {
prepOnly();
if (p.shop.refreshPrice > p.funds) fail('NO_FUNDS');
if (p.shop.freeRefreshes > 0) p.shop.freeRefreshes--; else p.funds -= 1;
const n = p.shop.slots.filter((s) => s.kind !== 'item').length;
p.shop.slots = [...Array.from({ length: n }, () => makeSlot(p.shop.level)), makeItemSlot()];
refreshPrivate(); return {};
}
case 'g.freeze': prepOnly(); p.shop.frozen = !p.shop.frozen; refreshPrivate(); return {};
case 'g.levelUp': {
prepOnly();
if (p.shop.level >= 6) fail('MAX_LEVEL');
if (p.shop.upgradePrice > p.funds) fail('NO_FUNDS');
p.funds -= p.shop.upgradePrice; p.shop.level++; p.shop.upgradePrice = [5, 8, 11, 12, 13][p.shop.level - 1] ?? 0;
const n = p.shop.level >= 4 ? 5 : 4;
while (p.shop.slots.filter((s) => s.kind !== 'item').length < n) p.shop.slots.splice(p.shop.slots.length - 1, 0, makeSlot(p.shop.level));
toast(`调度中心等级提升至 ${p.shop.level}`, 'success');
refreshPrivate(); return {};
}
case 'g.sell': {
prepOnly();
const loc = findPiece(f.uid); if (!loc) fail('BAD_TARGET');
if (loc.piece.kind === 'item') fail('BAD_TARGET');
removeAt(loc); p.funds += 1; refreshPrivate(); return {};
}
case 'g.destroy': { prepOnly(); const loc = findPiece(f.uid); if (!loc) fail('BAD_TARGET'); removeAt(loc); refreshPrivate(); return {}; }
case 'g.move': {
prepOnly();
const loc = findPiece(f.uid); if (!loc) fail('BAD_TARGET');
const to = f.to;
if (to.area === 'hand') {
const occ = p.hand[to.idx];
removeAt(loc);
if (occ) {
if (loc.area === 'board') { p.board.push({ ...occ, row: loc.piece.row, col: loc.piece.col }); }
else if (loc.area === 'hand') p.hand[loc.i] = occ;
else p.temp[loc.i] = occ;
}
const { row, col, dir, ...rest } = loc.piece; // eslint-disable-line no-unused-vars
p.hand[to.idx] = rest;
} else {
// the deploy wheel's facing, like PlayerState.move: top-level dir, else to.dir, else RIGHT (research 09 §1.2)
const d = ['UP', 'RIGHT', 'DOWN', 'LEFT'].includes(f.dir) ? f.dir : ['UP', 'RIGHT', 'DOWN', 'LEFT'].includes(to.dir) ? to.dir : 'RIGHT';
const oi = p.board.findIndex((x) => x.row === to.row && x.col === to.col);
const occ = oi >= 0 ? p.board[oi] : null;
if (occ && occ.uid === f.uid) { occ.dir = d; refreshPrivate(); return {}; } // re-orient in place
if (!occ && loc.area !== 'board' && loc.piece.kind === 'chess' && p.board.filter((x) => x.kind === 'chess').length >= p.deployCap) fail('BOARD_FULL');
if (occ) p.board.splice(oi, 1);
const locNow = findPiece(f.uid);
removeAt(locNow);
if (occ) {
if (loc.area === 'board') p.board.push({ ...occ, row: loc.piece.row, col: loc.piece.col });
else { const { row, col, dir, ...rest } = occ; if (loc.area === 'hand') p.hand[loc.i] = rest; else p.temp[loc.i] = rest; } // eslint-disable-line no-unused-vars
}
p.board.push({ ...loc.piece, row: to.row, col: to.col, dir: d });
}
refreshPrivate(); return {};
}
case 'g.equip': {
// like server PlayerState.equip: a third item replaces the equipped item the dialog picked (replaceUid, which
// must be one of the target's items), else the oldest; the replaced item is destroyed
prepOnly();
const it = findPiece(f.itemUid); const tg = findPiece(f.targetUid);
if (!it || !tg || tg.piece.kind !== 'chess') fail('BAD_TARGET');
if (f.replaceUid != null && !(tg.piece.items || []).some((x) => x.uid === f.replaceUid)) fail('BAD_TARGET');
removeAt(it);
const target = findPiece(f.targetUid).piece;
const items = [...(target.items || [])];
if (items.length >= 2) {
const i = f.replaceUid != null ? items.findIndex((x) => x.uid === f.replaceUid) : 0;
items.splice(i >= 0 ? i : 0, 1);
}
target.items = [...items, { uid: it.piece.uid, id: it.piece.id }];
refreshPrivate(); return {};
}
case 'g.pause': {
// solo battles only (server: g.pause {on} → m.public.paused)
if (S.pub.players.length > 1) fail('WRONG_PHASE');
if (![PHASE.COMBAT, PHASE.FINAL_ASSAULT, PHASE.HIDDEN_CORE].includes(pub.phase)) fail('WRONG_PHASE');
const now = Date.now();
if (f.on && !pub.paused) { pub.paused = true; pub.pausedAt = now; }
else if (!f.on && pub.paused) {
// the clocks resume where they stopped
const d = now - (pub.pausedAt || now);
if (pub.deadline) pub.deadline += d;
if (pub.overtimeAt) pub.overtimeAt += d;
pub.paused = false; delete pub.pausedAt;
}
pushPublic(); return {};
}
case 'g.art': { prepOnly(); const it = findPiece(f.itemUid); if (!it) fail('BAD_TARGET'); removeAt(it); toast('奇术已生效', 'success'); refreshPrivate(); return {}; }
case 'g.reward': {
if (!p.shop.rewardOffer) fail('WRONG_PHASE');
const s = p.shop.rewardOffer.slots[f.idx]; if (!s) fail('BAD_TARGET');
const idx = freeHandIdx(); if (idx < 0) fail('HAND_FULL');
p.hand[idx] = chessPiece(data.lookup('chess', s.id)); p.shop.rewardOffer = null; refreshPrivate(); return {};
}
case 'g.choice': {
if (!pub.sp) fail('WRONG_PHASE');
const taken = Object.values(pub.sp.picks).includes(f.idx);
if (taken) fail('BAD_TARGET');
pub.sp.picks[ME] = f.idx; pub.sp.turn = 'ai_2'; pushPublic(); return {};
}
case 'g.ready': {
if (pub.phase !== PHASE.PREP) fail('WRONG_PHASE');
if (f.ready && !p.canReady) fail('TEMP_NOT_EMPTY');
p.ready = !!f.ready; refreshPrivate(); return {};
}
case 'g.emote': {
store.set((s) => ({ emotes: [...s.emotes.slice(-19), { seq: Date.now(), playerId: ME, id: f.id, at: Date.now() }] }));
return {};
}
case 'g.watch': {
// server Match.watch rules: the other pair's boss field is hidden from a fighting player; no dead targets
const target = (pub.fields || []).find((x) => x.fieldId === f.fieldId);
const mine = (pub.fields || []).find((x) => x.players.includes(ME));
const meAlive = pub.players.find((x) => x.playerId === ME)?.alive !== false;
if (target && (target.kind === 'boss' || target.kind === 'hidden') && meAlive && mine && mine.fieldId !== target.fieldId) fail('BAD_TARGET');
if (!target && String(f.fieldId).startsWith('n:') && pub.players.find((x) => x.playerId === f.fieldId.slice(2))?.alive === false) fail('BAD_TARGET');
if ([PHASE.COMBAT, PHASE.UNITE, PHASE.FINAL_ASSAULT, PHASE.HIDDEN_CORE, PHASE.SETTLE].includes(pub.phase)) {
const cur = store.get().match.field;
if (S.battle?.fieldRef) S.battle.fieldRef.id = f.fieldId;
if (cur) store.patch('match', { field: { ...cur, fieldId: f.fieldId } });
} else {
const who = f.fieldId.replace(/^n:/, '');
if (who !== ME) {
const units = shuffle(S.pool).slice(0, 6).map((c, i) => ({ id: 900 + i, kind: 'op', side: 'ally', ownerId: who, defId: c.chessId, name: c.name, tier: c.tier, golden: i === 0, spine: c.charId, avatar: c.assets.avatar, x: [3, 4, 5, 7, 8, 5][i], y: [9, 9, 10, 11, 12, 12][i], facing: 1, maxHp: c.stats.maxHp }));
// like server/match/Match.js prepFieldMeta: a read-only prep board with THEIR coming enemies (the pen shows them)
const theirs = S.priv.nextEnemies.filter((e) => e.source !== 'bounty').slice(0, 3).map((e) => ({ ...e, gate: 'upper', count: e.count + 1 }));
store.patch('match', { field: { fieldId: f.fieldId, kind: 'normal', rect: { ...GEO.NORMAL_RECT }, stageId: pub.stageId, units, prep: true, nextEnemies: theirs } });
}
}
return {};
}
case 'g.autoplay': case 'g.leave': case 'room.leave': return {};
default: return {};
}
}
// ---- UI variants (open panels for screenshots) ------------------------------------------------------------------------------
function applyUiVariants() {
const clickSel = (sel, delay = 250) => setTimeout(() => document.querySelector(sel)?.click(), delay);
// the enemy list is the 敌方情报 tab of the 本局信息 dialog (left 🔍); the right 🔍▶▶ pans to the pen
if (VARIANTS.has('drawer')) { clickSel('.gtop__iconbtn', 400); clickSel('.edrawer .tabs__tab:nth-child(2)', 700); }
if (VARIANTS.has('info')) clickSel('.gtop__iconbtn', 400);
if (VARIANTS.has('pen')) {
// the pen needs the field view: wait for it to mount
const t0 = Date.now();
const tryPen = () => {
if (globalThis.__SP_VIEW__ && document.querySelector('.enemybtn')) { setTimeout(() => document.querySelector('.enemybtn')?.click(), 300); return; }
if (Date.now() - t0 < 15000) setTimeout(tryPen, 100);
};
tryPen();
}
if (VARIANTS.has('bond')) clickSel('.bslot .bond', 500);
if (VARIANTS.has('emote')) {
clickSel('.ewheel__btn', 400);
setTimeout(() => {
const now = Date.now();
store.set({ emotes: [{ seq: now, playerId: 'p3', id: 'autochess_battle_nice_cooperate', at: now }, { seq: now + 1, playerId: 'ai_2', id: 'slug_autochess_battle_thanks', at: now }] });
}, 300);
}
if (VARIANTS.has('settings')) clickSel('.gm__gear', 400);
if (VARIANTS.has('collapsed')) clickSel('.funds__collapse', 400);
if (VARIANTS.has('detail')) clickSel('.scard .scard__detail', 500);
if (VARIANTS.has('detailpiece')) setTimeout(() => {
const el = document.querySelector('.ff-piece.is-golden') || document.querySelector('.ff-piece');
el?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 10, clientY: 10 }));
}, 600);
if (VARIANTS.has('ticker')) setTimeout(() => store.set((s) => ({ ticker: [...s.ticker, { id: Date.now(), text: '<@ba.vup>Doctor·B博士</>对敌方领袖造成的伤害超过20%!', at: Date.now() }] })), 900);
if (VARIANTS.has('toast')) setTimeout(() => toast('资金不足', 'error'), 500);
}
// ---- switcher --------------------------------------------------------------------------------------------------------------
const SWITCH = [
['INFO_CHECK', PHASE.INFO_CHECK, ''], ['BAND_DRAFT', PHASE.BAND_DRAFT, ''], ['BAND_DRAFT solo', PHASE.BAND_DRAFT, 'solo'],
['BATTLE_CHECK', PHASE.BATTLE_CHECK, ''], ['PREP', PHASE.PREP, ''], ['PREP + reward', PHASE.PREP, 'reward'],
['PREP + temp', PHASE.PREP, 'temp'], ['PREP frozen', PHASE.PREP, 'frozen'], ['PREP dead', PHASE.PREP, 'dead'],
['PREP boss (L)', PHASE.PREP, 'boss'], ['PREP boss (R)', PHASE.PREP, 'bossR'],
['SP bounty', PHASE.SP_DRAFT, 'bounty'], ['SP supply', PHASE.SP_DRAFT, 'supply'], ['SP tactic', PHASE.SP_DRAFT, 'tactic'], ['SP solo', PHASE.SP_DRAFT, 'solo'],
['COMBAT', PHASE.COMBAT, ''], ['COMBAT done', PHASE.COMBAT, 'done'], ['UNITE', PHASE.UNITE, ''], ['SETTLE', PHASE.SETTLE, ''],
['FINAL_ASSAULT', PHASE.FINAL_ASSAULT, ''], ['FA overtime soon', PHASE.FINAL_ASSAULT, 'overtime'], ['FA draining', PHASE.FINAL_ASSAULT, 'drain'],
['HIDDEN_CORE', PHASE.HIDDEN_CORE, ''], ['COMBAT solo (pause)', PHASE.COMBAT, 'solo'], ['FA solo paused', PHASE.FINAL_ASSAULT, 'solo,paused'],
['RESULT win', PHASE.RESULT, ''], ['RESULT lose', PHASE.RESULT, 'defeat'],
];
function Switcher() {
const cur = store.get().match.public?.phase;
return html`<h4>PHASE</h4>${SWITCH.map(([label, ph, v]) => html`<button class=${cur === ph ? 'on' : ''} onClick=${() => { VARIANTS.clear(); setPhase(ph, v); renderBar(); }}>${label}</button>`)}
<h4>EVENTS</h4>
<button onClick=${() => { const now = Date.now(); store.set((s) => ({ emotes: [...s.emotes, { seq: now, playerId: pick(['p3', 'ai_2', 'p4']), id: pick(['autochess_battle_happy', 'slug_autochess_battle_thanks', 'autochess_battle_call', 'autochess_battle_fooldoctor_05', 'autochess_battle_foolamiya_03', 'autochess_battle_foolwisdel_01']), at: now }] })); }}>teammate emote</button>
<button onClick=${() => store.set((s) => ({ ticker: [...s.ticker, { id: Date.now(), text: `<@ba.vup>${pick(['Doctor·B', '灰烬'])}博士</>将调度中心等级提升为5级`, at: Date.now() }] }))}>ticker</button>
<button onClick=${() => toast('资金不足', 'error')}>error toast</button>
<button onClick=${() => { if (S.priv) { S.priv.shop.rewardOffer = { tier: 5, slots: shuffle(visibleChess(5)).slice(0, 3).map((c) => ({ kind: 'chess', id: c.chessId, price: 0, sold: false })) }; refreshPrivate(); } }}>merge reward</button>
<button onClick=${() => { S.priv.funds += 10; refreshPrivate(); }}>+10 funds</button>`;
}
const bar = document.getElementById('mockbar');
function renderBar() { render(html`<${Switcher} />`, bar); }
window.addEventListener('keydown', (e) => { if (e.key === '`') bar.classList.toggle('hidden'); });
if (SHOT) bar.classList.add('hidden');
// ---- boot ---------------------------------------------------------------------------------------------------------------------
async function boot() {
net.request = mockRequest;
store.set({
session: { entered: true },
me: { playerId: ME, name: '凯尔希', token: null },
connection: { status: 'online', ping: 42, attempt: 0, retryAt: 0, lastError: null, everOnline: true },
clock: { offset: 0, rtt: 20, synced: true },
room: { code: 'MOCK', hostId: ME, mode: 'coop', difficulty: 'HARD', inMatch: true, seats: PLAYERS.map((p) => ({ seat: p.seat, playerId: p.playerId, name: p.name, isBot: p.isBot, ready: true, connected: true })) },
});
await data.loadAll(GAME_FILES);
installDeviceSupport();
installAudio({ getManifest: () => data.get('assets'), subscribe: store.subscribe, getState: store.get, selectRoute, settings: settingsStore.get() });
const phase = params.get('phase') || 'PREP';
setPhase(PHASE[phase] || PHASE.PREP, params.get('variant') || '');
render(html`<div class="app-root"><div class="app-bg" aria-hidden="true"></div><${GameScreen} /><${ConnectionBanner} /><${ToastHost} /><${UiHosts} /><${GuideHost} /></div>`, document.getElementById('app'));
renderBar();
store.subscribe(() => renderBar());
globalThis.__MOCK__ = { store, S: () => S, setPhase, mutate: (fn) => { fn(S); refreshPrivate(); }, pushPublic: () => pushPublic() };
}
boot().catch((err) => console.error('[mock] boot failed', err));
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"boss-m02","title":"最终攻势 · 卢西恩,“猩红血钻” · R14","kind":"boss","stageId":"act2autochess_m02","modeId":"mode_multi_normal","round":14,"wave":"act2autochess_h07_05","tick":0.03333333333333333,"snapEvery":3,"duration":75,"recordedAt":"2026-09-27","field":{"fieldId":"b1","kind":"boss","rect":{"r0":0,"r1":5,"c0":0,"c1":20},"stageId":"act2autochess_m02","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":2,"facing":1,"maxHp":3003,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_b","name":"山","tier":5,"golden":true,"spine":"char_264_f12yin","avatar":"char_264_f12yin_2","x":8,"y":2,"facing":1,"maxHp":2738,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_b","name":"史尔特尔","tier":5,"golden":true,"spine":"char_350_surtr","avatar":"char_350_surtr_2","x":7,"y":2,"facing":1,"maxHp":2680,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_b","name":"夕","tier":5,"golden":true,"spine":"char_2015_dusk","avatar":"char_2015_dusk_2","x":4,"y":3,"facing":1,"maxHp":1787,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":4,"facing":1,"maxHp":1208,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_b","name":"烛煌","tier":5,"golden":true,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2_2","x":6,"y":5,"facing":1,"maxHp":1472,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":7,"y":5,"facing":1,"maxHp":1184,"uid":7},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_6_05_a","name":"异客","tier":6,"golden":false,"spine":"char_472_pasngr","avatar":"char_472_pasngr","x":8,"y":5,"facing":1,"maxHp":1215,"uid":8},{"id":9,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_03_b","name":"余","tier":6,"golden":true,"spine":"char_2026_yu","avatar":"char_2026_yu_2","x":11,"y":2,"facing":-1,"maxHp":3430,"uid":101},{"id":10,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_17_a","name":"耀骑士临光","tier":6,"golden":false,"spine":"char_1014_nearl2","avatar":"char_1014_nearl2","x":12,"y":2,"facing":-1,"maxHp":2698,"uid":102},{"id":11,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_19_b","name":"锏","tier":6,"golden":true,"spine":"char_4116_blkkgt","avatar":"char_4116_blkkgt_2","x":13,"y":2,"facing":-1,"maxHp":2767,"uid":103},{"id":12,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_01_b","name":"蕾缪安","tier":6,"golden":true,"spine":"char_4193_lemuen","avatar":"char_4193_lemuen_2","x":16,"y":3,"facing":-1,"maxHp":1470,"uid":104},{"id":13,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_08_a","name":"焰影苇草","tier":6,"golden":false,"spine":"char_1020_reed2","avatar":"char_1020_reed2","x":15,"y":4,"facing":-1,"maxHp":1361,"uid":105},{"id":14,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_12_b","name":"迷迭香","tier":6,"golden":true,"spine":"char_391_rosmon","avatar":"char_391_rosmon_2","x":14,"y":5,"facing":-1,"maxHp":2887,"uid":106},{"id":15,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_14_a","name":"流明","tier":6,"golden":false,"spine":"char_4042_lumen","avatar":"char_4042_lumen","x":13,"y":5,"facing":-1,"maxHp":1569,"uid":107},{"id":16,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_20_a","name":"纯烬艾雅法拉","tier":6,"golden":false,"spine":"char_1016_agoat2","avatar":"char_1016_agoat2","x":12,"y":5,"facing":-1,"maxHp":1237,"uid":108},{"id":17,"kind":"enemy","side":"enemy","ownerId":"P1","defId":"enemy_2016_csphtm","name":"卢西恩,“猩红血钻”","tier":3,"golden":false,"spine":"enemy_2016_csphtm","avatar":"enemy_2016_csphtm","x":9.99,"y":3.99,"facing":1,"maxHp":195000,"boss":true}]},"frames":[{"snap":{"fieldId":"b1","t":0.1,"units":[[1,9,2,3003,3003,0.1,8,0,6],[2,8,2,2738,2738,0.1,8,0,6],[3,7,2,2680,2680,0.1,8,0,6],[4,4,3,1787,1787,0.1,6,0,6],[5,5,4,1208,1208,20.1,45,0,6],[6,6,5,1472,1472,16.1,34,0,6],[7,7,5,1184,1184,43.1,70,0,6],[8,8,5,1215,1215,0.1,35,0,6],[9,11,2,3430,3430,33.1,55,0,6],[10,12,2,2698,2698,15.1,55,0,6],[11,13,2,2767,2767,15.1,33,0,6],[12,16,3,1470,1470,29.1,42,0,6],[13,15,4,1361,1361,15.1,45,0,6],[14,14,5,2887,2887,20.1,34,0,6],[15,13,5,1569,1569,0.1,61,0,6],[16,12,5,1237,1237,30.1,75,0,6],[17,9.98,3.98,195000,195000,0,0,0,1],[18,10.01,3.99,195000,195000,0,0,0,1]],"dp":10,"killed":0,"total":11,"dps":{"P1":10,"P2":10},"boss":{"hp":195000,"max":195000}},"ev":[["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_b","name":"烛煌","tier":5,"gLine truncated
+194
View File
@@ -0,0 +1,194 @@
[
{
"name": "normal-m01",
"title": "常规作战 · 源石流发生装置 · R5",
"kind": "normal",
"stageId": "act2autochess_m01",
"duration": 55,
"frames": 550,
"maxUnits": 30,
"file": "normal-m01.json",
"bytes": 416135,
"result": {
"reason": "timeout",
"killed": 14,
"total": 32
}
},
{
"name": "normal-m02",
"title": "常规作战 · 沼泽控制 · R8",
"kind": "normal",
"stageId": "act2autochess_m02",
"duration": 85,
"frames": 850,
"maxUnits": 24,
"file": "normal-m02.json",
"bytes": 566914,
"result": {
"reason": "timeout",
"killed": 15,
"total": 35
}
},
{
"name": "normal-m03",
"title": "常规作战 · 排气格栅 · R11",
"kind": "normal",
"stageId": "act2autochess_m03",
"duration": 90,
"frames": 900,
"maxUnits": 31,
"file": "normal-m03.json",
"bytes": 719521,
"result": {
"reason": "timeout",
"killed": 11,
"total": 46
}
},
{
"name": "normal-m04",
"title": "常规作战 · 涨潮控制 · R6",
"kind": "normal",
"stageId": "act2autochess_m04",
"duration": 70,
"frames": 700,
"maxUnits": 34,
"file": "normal-m04.json",
"bytes": 557852,
"result": {
"reason": "timeout",
"killed": 19,
"total": 39
}
},
{
"name": "late-m03-r12",
"title": "常规作战 · 排气格栅 · R12(官方波次)",
"kind": "normal",
"stageId": "act2autochess_m03",
"duration": 84.23,
"frames": 843,
"maxUnits": 30,
"file": "late-m03-r12.json",
"bytes": 570239,
"result": {
"reason": "cleared",
"killed": 12,
"total": 57
}
},
{
"name": "late-m01-r13",
"title": "常规作战 · 源石流发生装置 · R13(官方波次)",
"kind": "normal",
"stageId": "act2autochess_m01",
"duration": 110,
"frames": 1100,
"maxUnits": 33,
"file": "late-m01-r13.json",
"bytes": 849162,
"result": {
"reason": "timeout",
"killed": 6,
"total": 55
}
},
{
"name": "unite-m01",
"title": "联防 · 源石流发生装置 · R9",
"kind": "unite",
"stageId": "act2autochess_m01",
"duration": 60,
"frames": 600,
"maxUnits": 36,
"file": "unite-m01.json",
"bytes": 619719,
"result": {
"reason": "timeout",
"killed": 17,
"total": 40
}
},
{
"name": "boss-m02",
"title": "最终攻势 · 卢西恩,“猩红血钻” · R14",
"kind": "boss",
"stageId": "act2autochess_m02",
"duration": 75,
"frames": 750,
"maxUnits": 24,
"file": "boss-m02.json",
"bytes": 582523,
"result": {
"reason": "forced",
"killed": 10,
"total": 17
}
},
{
"name": "normal-a1m01",
"title": "常规作战 · 战场#01 · R4",
"kind": "normal",
"stageId": "act1autochess_m01",
"duration": 55,
"frames": 550,
"maxUnits": 20,
"file": "normal-a1m01.json",
"bytes": 326193,
"result": {
"reason": "timeout",
"killed": 11,
"total": 19
}
},
{
"name": "normal-a1m02",
"title": "常规作战 · 战场#02 · R7",
"kind": "normal",
"stageId": "act1autochess_m02",
"duration": 85,
"frames": 850,
"maxUnits": 28,
"file": "normal-a1m02.json",
"bytes": 617662,
"result": {
"reason": "timeout",
"killed": 26,
"total": 43
}
},
{
"name": "normal-a1m03",
"title": "常规作战 · 战场#03 · R10",
"kind": "normal",
"stageId": "act1autochess_m03",
"duration": 90,
"frames": 900,
"maxUnits": 33,
"file": "normal-a1m03.json",
"bytes": 713331,
"result": {
"reason": "timeout",
"killed": 20,
"total": 41
}
},
{
"name": "normal-a1m04",
"title": "常规作战 · 战场#04 活性源石 · R6",
"kind": "normal",
"stageId": "act1autochess_m04",
"duration": 70,
"frames": 700,
"maxUnits": 35,
"file": "normal-a1m04.json",
"bytes": 593839,
"result": {
"reason": "timeout",
"killed": 17,
"total": 39
}
}
]
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"late-m01-r13","title":"常规作战 · 源石流发生装置 · R13(官方波次)","kind":"normal","stageId":"act2autochess_m01","modeId":"mode_multi_hard","round":13,"wave":"act1autochess_h06","tick":0.03333333333333333,"snapEvery":3,"duration":110,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_b","name":"星熊","tier":4,"golden":true,"spine":"char_136_hsguma","avatar":"char_136_hsguma_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3513,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_b","name":"银灰","tier":4,"golden":true,"spine":"char_172_svrash","avatar":"char_172_svrash_2","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":2569,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_b","name":"斯卡蒂","tier":3,"golden":true,"spine":"char_263_skadi","avatar":"char_263_skadi_2","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":3853,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_b","name":"莫斯提马","tier":4,"golden":true,"spine":"char_213_mostma","avatar":"char_213_mostma_2","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1695,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_b","name":"寒芒克洛丝","tier":4,"golden":true,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1561,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1288,"uid":7},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_b","name":"远牙","tier":4,"golden":true,"spine":"char_430_fartth","avatar":"char_430_fartth_2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1419,"uid":8}]},"preview":[{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":2,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"upper","t":3,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":7,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":1,"gate":"upper","t":7,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":10,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":11,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"upper","t":21,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":32,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":37,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":39,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":55,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":62,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null}],"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,3513,3513,0,0,0,6],[2,8,9,2569,2569,60.1,90,0,6],[3,7,9,3853,3853,60.1,90,0,6],[4,4,10,1695,1695,62.1,110,0,6],[5,4,11,1271,1271,70.1,100,0,6],[6,4,12,1561,1561,20.1,40,0,6],[7,5,12,1288,1288,20.1,55,0,6],[8,6,12,1419,1419,7.1,24,0,6]],"dp":10,"killed":0,"total":55},"ev":[["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_b","name":"寒芒克洛丝","tier":4,"golden":true,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1561,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1288,"uid":7}],["deploy",7],["spawn",{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_b","name":"远牙","tier":4,"golden":true,"spine":"char_430_farttLine truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"late-m03-r12","title":"常规作战 · 排气格栅 · R12(官方波次)","kind":"normal","stageId":"act2autochess_m03","modeId":"mode_multi_hard","round":12,"wave":"act1autochess_h05","tick":0.03333333333333333,"snapEvery":3,"duration":84.23,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m03","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3003,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_b","name":"星熊","tier":4,"golden":true,"spine":"char_136_hsguma","avatar":"char_136_hsguma_2","x":6,"y":9,"facing":1,"dir":"RIGHT","maxHp":3513,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_b","name":"山","tier":5,"golden":true,"spine":"char_264_f12yin","avatar":"char_264_f12yin_2","x":5,"y":9,"facing":1,"dir":"RIGHT","maxHp":2738,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_b","name":"史尔特尔","tier":5,"golden":true,"spine":"char_350_surtr","avatar":"char_350_surtr_2","x":4,"y":9,"facing":1,"dir":"RIGHT","maxHp":2680,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_b","name":"夕","tier":5,"golden":true,"spine":"char_2015_dusk","avatar":"char_2015_dusk_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1787,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_b","name":"莫斯提马","tier":4,"golden":true,"spine":"char_213_mostma","avatar":"char_213_mostma_2","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1695,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_b","name":"铃兰","tier":5,"golden":true,"spine":"char_358_lisa","avatar":"char_358_lisa_2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1480,"uid":7},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_08_b","name":"号角","tier":5,"golden":true,"spine":"char_4039_horn","avatar":"char_4039_horn_2","x":3,"y":9,"facing":1,"dir":"RIGHT","maxHp":2775,"uid":8},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}]},"preview":[{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":2,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":5,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"upper","t":5,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"lower","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":10,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"upper","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":12,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"upper","t":21,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"lower","t":30,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"lower","t":31,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":35,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":37,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":51,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":53,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null}],"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,3003,3003,0.1,8,0,6],[2,6,9,3513,3513,0,0,0,6],[3,5,9,2738,2738,0.1,8,0,6],[4,4,9,2680,2680,0.1,8,0,6],[5,4,12,1787,1787,0.1,6,0,6]Line truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"normal-a1m01","title":"常规作战 · 战场#01 · R4","kind":"normal","stageId":"act1autochess_m01","modeId":"mode_multi_normal","round":4,"wave":"act1autochess_04","tick":0.03333333333333333,"snapEvery":3,"duration":55,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act1autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_02_a","name":"角峰","tier":1,"golden":false,"spine":"char_199_yak","avatar":"char_199_yak","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2216,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1285,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_12_a","name":"艾丝黛尔","tier":1,"golden":false,"spine":"char_127_estell","avatar":"char_127_estell","x":4,"y":9,"facing":1,"dir":"RIGHT","maxHp":1840,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_01_a","name":"隐现","tier":1,"golden":false,"spine":"char_498_inside","avatar":"char_498_inside","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":1123,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1145,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1540,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":7},{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":5,"y":10,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":6,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2216,2216,0.1,47,0,6],[2,8,9,1285,1285,23.1,40,0,6],[3,4,9,1840,1840,10,24,0,6],[4,3,12,1123,1123,8.1,24,0,6],[5,4,12,1145,1145,0.1,60,0,6],[6,5,12,1540,1540,0.1,26,0,6],[7,6,12,1472,1472,20.1,44,0,6],[8,5,11,100,100,0,0,0,6],[9,5,10,100,100,0,0,0,6],[10,6,10,100,100,0,0,0,6]],"dp":11,"killed":0,"total":19},"ev":[["spawn",{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":5,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":6,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_01_a","name":"隐现","tier":1,"golden":false,"spine":"char_498_inside","avatar":"char_498_inside","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":1123,"uid":4}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1145,"uid":5}],["deploy",5],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":7}],["deploy",7],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_12_a","name":"艾丝黛尔","tier":1,"golden":false,"spine":"char_127_estell","avataLine truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"normal-a1m02","title":"常规作战 · 战场#02 · R7","kind":"normal","stageId":"act1autochess_m02","modeId":"mode_multi_normal","round":7,"wave":"act1autochess_07","tick":0.03333333333333333,"snapEvery":3,"duration":85,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act1autochess_m02","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2849,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":6,"y":9,"facing":1,"dir":"RIGHT","maxHp":2022,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_a","name":"斯卡蒂","tier":3,"golden":false,"spine":"char_263_skadi","avatar":"char_263_skadi","x":5,"y":9,"facing":1,"dir":"RIGHT","maxHp":2938,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":1428,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_a","name":"寒芒克洛丝","tier":4,"golden":false,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1246,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":7}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2849,2849,0,0,0,6],[2,6,9,2022,2022,55.1,90,0,6],[3,5,9,2938,2938,55.1,90,0,6],[4,3,12,1428,1428,56.1,110,0,6],[5,4,12,1271,1271,70.1,100,0,6],[6,5,12,1246,1246,15.1,45,0,6],[7,6,12,1217,1217,6.1,27,0,6]],"dp":10,"killed":0,"total":43},"ev":[["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":1428,"uid":4}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5}],["deploy",5],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_a","name":"寒芒克洛丝","tier":4,"golden":false,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1246,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":7}],["deploy",7],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_a","name":"斯卡蒂","tier":3,"golden":false,"spine":"char_263_skadi","avatar":"char_263_skadi","x":5,"y":9,"facing":1,"dir":"RIGHT","maxHp":2938,"uid":3}],["deploy",3],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":6,"y":9,"facing":1,"dir":"RIGHT","maxHp":2022,"uid":2}],["deploy",2],["spawn",{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2849,"uid":1}],["deploy",1],["fx","garrisonGrant",6,12,{"id":7,"src":"gar:garrison_73_a","key":"garrison_96_a","from":6}],["fx","garrisonGrant",6,12,{"id":7,"src":"gar:garrison_148_a","key":"garrison_108_a","from":7}]]},{"snap":{"fieldId":"n:P1","t":0.2,"units":[[1,9,9,2849,2849,0,0,0,6],[2,6,9,2022,2022,55.2,90,0,6],[3,5,9,2938,2938,55.2,90,0,6],[4,3,12,1428,1428,56.2,110,0,6],[5,4,12,1271,1271,70.2,100,0,6],[6,5,12,1246,1246,15.2,45,0,6],[7,6,12,1217,1217,6.2,27,0,6]],"dp":10,"killed":0,"total":43},"ev":[]},{"snap":{"fieldId":"n:P1","t":0.3,"units":[[1,9,9,2849,2849,0,0,0,6],[2,6,9,2022,2022,55.3,90,0,6],[3,5,9,2938,2938,55.3,90,0,6],[4,3,12,1428,1428,56.3,110,0,6],[5,4,12,1271,1271,70.3,100,0,6],[6,5,12,1246,1246,15.3,45,0,6],[7,6,12,1217,1217,6.3,27,0,6]],"dp":10,"killed":0,"total":43},"ev":[]},{"snap":{"fieldId":"n:P1","t":0.4,"units":[[1,9,9,2849,2849,0,0,0,6],[2,6,9,2022,2022,55.5,90,0,6],[3,5,9,2Line truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"normal-a1m03","title":"常规作战 · 战场#03 · R10","kind":"normal","stageId":"act1autochess_m03","modeId":"mode_multi_hard","round":10,"wave":"act1autochess_h03","tick":0.03333333333333333,"snapEvery":3,"duration":90,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act1autochess_m03","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3003,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_a","name":"山","tier":5,"golden":false,"spine":"char_264_f12yin","avatar":"char_264_f12yin","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":2250,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_a","name":"史尔特尔","tier":5,"golden":false,"spine":"char_350_surtr","avatar":"char_350_surtr","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2216,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_a","name":"夕","tier":5,"golden":false,"spine":"char_2015_dusk","avatar":"char_2015_dusk","x":3,"y":10,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_a","name":"烛煌","tier":5,"golden":false,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2","x":7,"y":11,"facing":1,"dir":"RIGHT","maxHp":1206,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":8,"y":11,"facing":1,"dir":"RIGHT","maxHp":1184,"uid":7},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_08_a","name":"号角","tier":5,"golden":false,"spine":"char_4039_horn","avatar":"char_4039_horn","x":6,"y":9,"facing":1,"dir":"RIGHT","maxHp":2200,"uid":8},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,3003,3003,0.1,8,0,6],[2,8,9,2250,2250,0.1,9,0,6],[3,7,9,2216,2216,0.1,9,0,6],[4,3,10,1404,1404,0.1,7,0,6],[5,4,10,1208,1208,20.1,45,0,6],[6,7,11,1206,1206,13.1,37,0,6],[7,8,11,1184,1184,43.1,70,0,6],[8,6,9,2200,2200,15.1,50,0,6],[9,9,10,100,100,0,0,0,6],[10,5,11,100,100,0,0,0,6],[11,9,11,100,100,0,0,0,6]],"dp":10,"killed":0,"total":41},"ev":[["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_a","name":"烛煌","tier":5,"golden":false,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2","x":7,"y":11,"facing":1,"dir":"RIGHT","maxHp":1206,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":8,"y":11,"facing":1,"dir":"RIGHT","maxHp":1184,"uid":7}],["deploy",7],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_a","name":"夕","tier":5,"golden":false,"spine":"char_2015_dusk","avatar":"char_2015_dusk","x":3,"y":10,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi",Line truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"normal-a1m04","title":"常规作战 · 战场#04 活性源石 · R6","kind":"normal","stageId":"act1autochess_m04","modeId":"mode_multi_normal","round":6,"wave":"act1autochess_06","tick":0.03333333333333333,"snapEvery":3,"duration":70,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act1autochess_m04","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_16_a","name":"蛇屠箱","tier":3,"golden":false,"spine":"char_150_snakek","avatar":"char_150_snakek","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2173,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_18_a","name":"忍冬","tier":3,"golden":false,"spine":"char_4026_vulpis","avatar":"char_4026_vulpis","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1481,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_02_a","name":"断崖","tier":3,"golden":false,"spine":"char_294_ayer","avatar":"char_294_ayer","x":4,"y":9,"facing":1,"dir":"RIGHT","maxHp":1911,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_17_a","name":"流星","tier":3,"golden":false,"spine":"char_126_shotst","avatar":"char_126_shotst","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_21_a","name":"空弦","tier":3,"golden":false,"spine":"char_332_archet","avatar":"char_332_archet","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1364,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_14_a","name":"调香师","tier":2,"golden":false,"spine":"char_181_flower","avatar":"char_181_flower","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":1232,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_14_a","name":"初雪","tier":3,"golden":false,"spine":"char_174_slbell","avatar":"char_174_slbell","x":6,"y":11,"facing":1,"dir":"RIGHT","maxHp":1251,"uid":7},{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":8,"y":12,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2173,2173,0.1,40,0,6],[2,8,9,1481,1481,6.1,21,0,6],[3,4,9,1911,1911,18.1,54,0,6],[4,3,11,1404,1404,0,19,0,6],[5,4,11,1364,1364,15,37,32,6],[6,5,11,1232,1232,20.1,60,0,6],[7,6,11,1251,1251,13.1,40,0,6],[8,3,12,100,100,0,0,0,6],[9,4,12,100,100,0,0,0,6],[10,8,12,100,100,0,0,0,6]],"dp":10,"killed":0,"total":39},"ev":[["spawn",{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":8,"y":12,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_17_a","name":"流星","tier":3,"golden":false,"spine":"char_126_shotst","avatar":"char_126_shotst","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_21_a","name":"空弦","tier":3,"golden":false,"spine":"char_332_archet","avatar":"char_332_archet","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1364,"uid":5}],["deploy",5],["status",5,"talent:archet_shield",1],["fx","shield",4,11,{"id":5}],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_14_a","name":"调香师","tier":2,"golden":false,"spine":"char_181_flower","avatar":"char_181_flower","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":1232,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_14_a","name":"初雪","tier":3,"golden":false,"spine":"char_174_slbell","avatar":"char_174_slbell","x":6,"y":11,"facing":1,"dir":"RIGHT","maxHp":1251,"uid":7}],["deploy",7],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_02_aLine truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"normal-m01","title":"常规作战 · 源石流发生装置 · R5","kind":"normal","stageId":"act2autochess_m01","modeId":"mode_multi_normal","round":5,"wave":"act1autochess_05","tick":0.03333333333333333,"snapEvery":3,"duration":55,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_02_a","name":"角峰","tier":1,"golden":false,"spine":"char_199_yak","avatar":"char_199_yak","x":9,"y":9,"facing":1,"maxHp":2216,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"maxHp":1285,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_12_b","name":"艾丝黛尔","tier":1,"golden":true,"spine":"char_127_estell","avatar":"char_127_estell_2","x":7,"y":9,"facing":1,"maxHp":2389,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_01_a","name":"隐现","tier":1,"golden":false,"spine":"char_498_inside","avatar":"char_498_inside","x":4,"y":10,"facing":1,"maxHp":1123,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":11,"facing":1,"maxHp":1260,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":12,"facing":1,"maxHp":1400,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":5,"y":12,"facing":1,"maxHp":1472,"uid":7}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2216,2216,0.1,47,0,6],[2,8,9,1285,1285,23.1,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.1,24,0,6],[5,4,11,1260,1260,0.1,60,0,6],[6,4,12,1400,1400,0.1,26,0,6],[7,5,12,1472,1472,20.1,44,0,6]],"dp":11,"killed":0,"total":32},"ev":[["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":12,"facing":1,"maxHp":1400,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":5,"y":12,"facing":1,"maxHp":1472,"uid":7}],["deploy",7],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":11,"facing":1,"maxHp":1145,"uid":5}],["deploy",5],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_01_a","name":"隐现","tier":1,"golden":false,"spine":"char_498_inside","avatar":"char_498_inside","x":4,"y":10,"facing":1,"maxHp":1123,"uid":4}],["deploy",4],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_12_b","name":"艾丝黛尔","tier":1,"golden":true,"spine":"char_127_estell","avatar":"char_127_estell_2","x":7,"y":9,"facing":1,"maxHp":2389,"uid":3}],["deploy",3],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"maxHp":1285,"uid":2}],["deploy",2],["spawn",{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_02_a","name":"角峰","tier":1,"golden":false,"spine":"char_199_yak","avatar":"char_199_yak","x":9,"y":9,"facing":1,"maxHp":2216,"uid":1}],["deploy",1],["status",5,"talent:angel_bless_ally",1],["fx","buff",4,11,{"id":5,"src":7,"talent":"angel_bless"}]]},{"snap":{"fieldId":"n:P1","t":0.2,"units":[[1,9,9,2216,2216,0.2,47,0,6],[2,8,9,1285,1285,23.2,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.2,24,0,6],[5,4,11,1260,1260,0.2,60,0,6],[6,4,12,1400,1400,0.2,26,0,6],[7,5,12,1472,1472,20.2,44,0,6]],"dp":11,"killed":0,"total":32},"ev":[]},{"snap":{"fieldId":"n:P1","t":0.3,"units":[[1,9,9,2216,2216,0.3,47,0,6],[2,8,9,1285,1285,23.3,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.3,24,0,6],[5,4,11,1260,1260,0.3,60,0,6],[6,4,12,1400,1400,0.3,26,0,6],[7,5,12,1472,1472,20.3,44,0,6]],"dp":11,"killed":0,"total":32},"ev":[]},{"snap":{"fieldId":"n:P1","t":0.4,"units":[[1,9,9,2216,2216,0.4,47,0,6],[2,8,9,1285,1285,23.4,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.4,24,0,6],[5,4,11,1260,1260,0.4,60,0,6],[6,4,12,1400,1400,0.4,26,0,6],[7,5,12,1472,1472,20.4,44,0,6]],"dp":11,"killed":0,"total":32},"ev":[]},{"snap":{"fieldId":"n:P1","t":0.5,"units":[[1,9,9,2216,2216,0.5,47,0,0],[2,8,9,1285,1285,23.5,40,0,0],[3,7,9,2389,2389,10,23,0,0Line truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"normal-m02","title":"常规作战 · 沼泽控制 · R8","kind":"normal","stageId":"act2autochess_m02","modeId":"mode_multi_hard","round":8,"wave":"act1autochess_h01","tick":0.03333333333333333,"snapEvery":3,"duration":85,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m02","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"maxHp":2849,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":8,"y":9,"facing":1,"maxHp":2022,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_a","name":"斯卡蒂","tier":3,"golden":false,"spine":"char_263_skadi","avatar":"char_263_skadi","x":7,"y":9,"facing":1,"maxHp":2938,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":4,"y":10,"facing":1,"maxHp":1428,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":5,"y":11,"facing":1,"maxHp":1271,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_a","name":"寒芒克洛丝","tier":4,"golden":false,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2","x":6,"y":12,"facing":1,"maxHp":1246,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":7,"y":12,"facing":1,"maxHp":1288,"uid":7},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":8,"y":12,"facing":1,"maxHp":1217,"uid":8},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2849,2849,0,0,0,6],[2,8,9,2022,2022,55.1,90,0,6],[3,7,9,2938,2938,55.1,90,0,6],[4,4,10,1428,1428,56.1,110,0,6],[5,5,11,1271,1271,70.1,100,0,6],[6,6,12,1246,1246,15.1,45,0,6],[7,7,12,1288,1288,20.1,55,0,6],[8,8,12,1217,1217,6.1,27,0,6],[9,9,11,100,100,0,0,0,6],[10,9,10,100,100,0,0,0,6]],"dp":10,"killed":0,"total":35},"ev":[["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"maxHp":100}],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_a","name":"寒芒克洛丝","tier":4,"golden":false,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2","x":6,"y":12,"facing":1,"maxHp":1246,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":7,"y":12,"facing":1,"maxHp":1288,"uid":7}],["deploy",7],["spawn",{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":8,"y":12,"facing":1,"maxHp":1217,"uid":8}],["deploy",8],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":5,"y":11,"facing":1,"maxHp":1271,"uid":5}],["deploy",5],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":4,"y":10,"facing":1,"maxHp":1428,"uid":4}],["deploy",4],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_a","name":"斯卡蒂","tier":3,"golden":false,"spine":"char_263_skadi","avatar":"char_263_skadi","x":7,"y":9,"facing":1,"maxHp":2938,"uid":3}],["deploy",3],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"goldLine truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"normal-m03","title":"常规作战 · 排气格栅 · R11","kind":"normal","stageId":"act2autochess_m03","modeId":"mode_multi_hard","round":11,"wave":"act1autochess_h04","tick":0.03333333333333333,"snapEvery":3,"duration":90,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m03","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":9,"facing":1,"maxHp":3003,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_a","name":"山","tier":5,"golden":false,"spine":"char_264_f12yin","avatar":"char_264_f12yin","x":6,"y":9,"facing":1,"maxHp":2250,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_a","name":"史尔特尔","tier":5,"golden":false,"spine":"char_350_surtr","avatar":"char_350_surtr","x":5,"y":9,"facing":1,"maxHp":2216,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_a","name":"夕","tier":5,"golden":false,"spine":"char_2015_dusk","avatar":"char_2015_dusk","x":4,"y":12,"facing":1,"maxHp":1404,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":12,"facing":1,"maxHp":1208,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_a","name":"烛煌","tier":5,"golden":false,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2","x":6,"y":12,"facing":1,"maxHp":1206,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":7,"y":12,"facing":1,"maxHp":1184,"uid":7},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_08_b","name":"号角","tier":5,"golden":true,"spine":"char_4039_horn","avatar":"char_4039_horn_2","x":4,"y":9,"facing":1,"maxHp":2775,"uid":8},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"maxHp":100},{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,3003,3003,0.1,8,0,6],[2,6,9,2250,2250,0.1,9,0,6],[3,5,9,2216,2216,0.1,9,0,6],[4,4,12,1404,1404,0.1,7,0,6],[5,5,12,1208,1208,20.1,45,0,6],[6,6,12,1206,1206,13.1,37,8,6],[7,7,12,1184,1184,43.1,70,0,6],[8,4,9,2775,2775,25.1,40,0,6],[9,3,12,100,100,0,0,0,6],[10,3,11,100,100,0,0,0,6],[11,9,11,100,100,0,0,0,6]],"dp":10,"killed":0,"total":46},"ev":[["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"maxHp":100}],["spawn",{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"maxHp":100}],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_a","name":"夕","tier":5,"golden":false,"spine":"char_2015_dusk","avatar":"char_2015_dusk","x":4,"y":12,"facing":1,"maxHp":1404,"uid":4}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":12,"facing":1,"maxHp":1208,"uid":5}],["deploy",5],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_a","name":"烛煌","tier":5,"golden":false,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2","x":6,"y":12,"facing":1,"maxHp":1206,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":7,"y":12,"facing":1,"maxHp":1184,"uid":7}],["deploy",7],["spawn",{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_08_b","name":"号角","tier":5,"golden":true,"spine":"char_4039_horn","avatar":"char_4039_horn_2Line truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"normal-m04","title":"常规作战 · 涨潮控制 · R6","kind":"normal","stageId":"act2autochess_m04","modeId":"mode_multi_normal","round":6,"wave":"act1autochess_06","tick":0.03333333333333333,"snapEvery":3,"duration":70,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m04","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_16_a","name":"蛇屠箱","tier":3,"golden":false,"spine":"char_150_snakek","avatar":"char_150_snakek","x":9,"y":9,"facing":1,"maxHp":2173,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_18_a","name":"忍冬","tier":3,"golden":false,"spine":"char_4026_vulpis","avatar":"char_4026_vulpis","x":8,"y":9,"facing":1,"maxHp":1481,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_02_a","name":"断崖","tier":3,"golden":false,"spine":"char_294_ayer","avatar":"char_294_ayer","x":7,"y":9,"facing":1,"maxHp":1911,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_17_a","name":"流星","tier":3,"golden":false,"spine":"char_126_shotst","avatar":"char_126_shotst","x":3,"y":12,"facing":1,"maxHp":1404,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_21_a","name":"空弦","tier":3,"golden":false,"spine":"char_332_archet","avatar":"char_332_archet","x":4,"y":12,"facing":1,"maxHp":1364,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_14_a","name":"调香师","tier":2,"golden":false,"spine":"char_181_flower","avatar":"char_181_flower","x":5,"y":12,"facing":1,"maxHp":1232,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_14_a","name":"初雪","tier":3,"golden":false,"spine":"char_174_slbell","avatar":"char_174_slbell","x":6,"y":12,"facing":1,"maxHp":1251,"uid":7},{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"maxHp":100},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2173,2173,0.1,40,0,6],[2,8,9,1481,1481,6.1,21,0,6],[3,7,9,1911,1911,18.1,54,0,6],[4,3,12,1404,1404,0,19,0,6],[5,4,12,1364,1364,15,37,32,6],[6,5,12,1232,1232,20.1,60,0,6],[7,6,12,1251,1251,13.1,40,0,6],[8,9,11,100,100,0,0,0,6],[9,9,10,100,100,0,0,0,6]],"dp":10,"killed":0,"total":39},"ev":[["spawn",{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"maxHp":100}],["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"maxHp":100}],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_17_a","name":"流星","tier":3,"golden":false,"spine":"char_126_shotst","avatar":"char_126_shotst","x":3,"y":12,"facing":1,"maxHp":1404,"uid":4}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_21_a","name":"空弦","tier":3,"golden":false,"spine":"char_332_archet","avatar":"char_332_archet","x":4,"y":12,"facing":1,"maxHp":1364,"uid":5}],["deploy",5],["status",5,"talent:archet_shield",1],["fx","shield",4,12,{"id":5}],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_14_a","name":"调香师","tier":2,"golden":false,"spine":"char_181_flower","avatar":"char_181_flower","x":5,"y":12,"facing":1,"maxHp":1232,"uid":6}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_14_a","name":"初雪","tier":3,"golden":false,"spine":"char_174_slbell","avatar":"char_174_slbell","x":6,"y":12,"facing":1,"maxHp":1251,"uid":7}],["deploy",7],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_02_a","name":"断崖","tier":3,"golden":false,"spine":"char_294_ayer","avatar":"char_294_ayer","x":7,"y":9,"facing":1,"maxHp":1911,"uid":3}],["deploy",3],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_18_a","name":"忍冬","tier":3,"golden":false,"spine":"char_4026_vulpis","avatar":"char_4026_vulpis","x":8,"y":9,"facing":1,"maxHp":1481,"uid":2}],["deploy",2],["spawn",{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_16_a","name":"蛇屠箱","tier":3,"golden":false,"spine":"char_150_snakek","avatar":"char_150_snakek","x":9,"y":9,"facing":1,"maxHp":2173,"uid":1}],["deploy",1]]},{"snap":{"fieldId":"n:P1","t":0.2,"units":[[1,9,9,2173,2173,0.2,40,0,6],[2,8,9,1481,1481,6.2,21,0Line truncated
+1
View File
@@ -0,0 +1 @@
{"version":1,"name":"unite-m01","title":"联防 · 源石流发生装置 · R9","kind":"unite","stageId":"act2autochess_m01","modeId":"mode_multi_normal","round":9,"wave":"act1autochess_escaped_multi","tick":0.03333333333333333,"snapEvery":3,"duration":60,"recordedAt":"2026-09-27","field":{"fieldId":"u","kind":"unite","rect":{"r0":9,"r1":12,"c0":0,"c1":20},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"maxHp":2849,"uid":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"maxHp":1414,"uid":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":4,"y":10,"facing":1,"maxHp":1472,"uid":3},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":11,"facing":1,"maxHp":1400,"uid":4},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":12,"facing":1,"maxHp":1145,"uid":5},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":7,"y":9,"facing":1,"maxHp":2022,"uid":6},{"id":7,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_5_11_a","name":"塞雷娅","tier":5,"golden":false,"spine":"char_202_demkni","avatar":"char_202_demkni","x":17,"y":9,"facing":1,"maxHp":2268,"uid":101},{"id":8,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_2_16_a","name":"拉普兰德","tier":2,"golden":false,"spine":"char_140_whitew","avatar":"char_140_whitew","x":16,"y":9,"facing":1,"maxHp":1791,"uid":102},{"id":9,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":12,"y":10,"facing":1,"maxHp":1428,"uid":103},{"id":10,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":12,"y":11,"facing":1,"maxHp":1271,"uid":104},{"id":11,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":12,"y":12,"facing":1,"maxHp":1217,"uid":105},{"id":12,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":13,"y":12,"facing":1,"maxHp":1208,"uid":106}]},"frames":[{"snap":{"fieldId":"u","t":0.1,"units":[[1,9,9,2849,2849,0,0,0,6],[2,8,9,1414,1414,23.1,40,0,6],[3,4,10,1472,1472,20.1,44,0,6],[4,4,11,1400,1400,0.1,26,0,6],[5,4,12,1145,1145,0.1,60,0,6],[6,7,9,2022,2022,55.1,90,0,6],[7,17,9,2268,2268,0.1,9,0,6],[8,16,9,1791,1791,0,27,0,6],[9,12,10,1428,1428,56.1,110,0,6],[10,12,11,1271,1271,70.1,100,0,6],[11,12,12,1217,1217,6.1,27,0,6],[12,13,12,1208,1208,20.1,45,0,6]],"dp":11,"killed":0,"total":40,"dps":{"P1":11,"P2":10}},"ev":[["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":12,"facing":1,"maxHp":1145,"uid":5}],["deploy",5],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":11,"facing":1,"maxHp":1400,"uid":4}],["deploy",4],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":4,"y":10,"facing":1,"maxHp":1472,"uid":3}],["deploy",3],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":7,"y":9,"facing":1,"maxHp":2022,"uid":6}],["deploy",6],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"maxHp":1285,"uid":2}],["deploy",2],["spawn",{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"maxHp":2849,"uid":1}],["deploy",1],["spawn",{"id":11,"kind":"op","side":"ally","ownerId":Line truncated
+68
View File
@@ -0,0 +1,68 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<title>Render demo · 卫戍协议:盟约</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' fill='%230c0f0e'/%3E%3Cpath fill='%234ed8af' d='M5 3h3v2h2V3h4v2h2V3h3v5l-2 2v7l2 2v2H5v-2l2-2v-7L5 8z'/%3E%3C/svg%3E" />
<link rel="stylesheet" href="/fonts/fonts.css" />
<link rel="stylesheet" href="/css/theme.css" />
<script type="importmap">
{ "imports": { "preact": "/vendor/preact.module.js", "preact/hooks": "/vendor/hooks.module.js", "htm": "/vendor/htm.module.js" } }
</script>
<!-- PixiJS 7 + pixi-spine as classic scripts (global PIXI / PIXI.spine), like the game page -->
<script src="/vendor/pixi.min.js"></script>
<script src="/vendor/pixi-spine.js"></script>
<style>
html, body { margin: 0; height: 100%; background: #0a0e0d; overflow: hidden; }
#field { position: fixed; inset: 0; }
.rd-panel {
position: fixed; right: 12px; bottom: 12px; z-index: 5; display: flex; flex-direction: column; gap: 8px;
padding: 10px 12px; min-width: 320px; background: rgba(12, 16, 15, .86); border: 1px solid #2f3a35;
color: #c3cbc7; font: 13px/1.35 "Noto Sans SC", system-ui, sans-serif; backdrop-filter: blur(6px);
}
.rd-panel.is-hidden { display: none; }
.rd-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rd-panel select, .rd-panel button {
background: #1e2421; color: #f2f2f2; border: 1px solid #3e4b45; padding: 4px 8px; font: inherit; cursor: pointer;
}
.rd-panel button.is-on { border-color: #4ed8af; color: #4ed8af; }
.rd-panel input[type=range] { flex: 1; accent-color: #4ed8af; }
.rd-title { font-weight: 700; color: #f2f2f2; }
.rd-micro { font: 11px/1 Bender, Oxanium, monospace; letter-spacing: .08em; color: #4ed8af; text-transform: uppercase; }
.rd-stats { font: 12px/1.4 Bender, Oxanium, ui-monospace, monospace; color: #8a948f; white-space: pre; }
.rd-log { position: fixed; left: 12px; bottom: 12px; z-index: 5; max-width: 42vw; font: 12px/1.4 ui-monospace, monospace; color: #8a948f; pointer-events: none; white-space: pre-wrap; }
</style>
</head>
<body>
<div id="field"></div>
<div class="rd-panel" id="panel">
<div class="rd-row"><span class="rd-micro">RENDER DEMO</span><span class="rd-title" id="title">—</span></div>
<div class="rd-row">
<select id="scene" aria-label="scene"></select>
<select id="stage" aria-label="stage"></select>
<button id="play" type="button">❚❚</button>
<select id="speed" aria-label="speed">
<option value="0.25">0.25×</option><option value="0.5">0.5×</option><option value="1" selected>1×</option><option value="2">2×</option><option value="4">4×</option>
</select>
<button id="editable" type="button" class="is-on">编辑</button>
</div>
<div class="rd-row">
<span class="rd-micro">BOARD</span>
<button id="board3d" type="button" class="is-on" title="official 3D board (three.js) / 2D atlas board">3D</button>
<select id="cam" aria-label="camera">
<option value="">scene camera</option>
<option value="prep">prep · shop</option><option value="prep-noshop">prep · shop collapsed</option>
<option value="normal">battle</option><option value="unite">联防 全景</option><option value="boss">最终攻势 全景</option>
<option value="boss-L">最终攻势 左</option><option value="boss-R">最终攻势 右</option><option value="pen">enemy pen</option>
<option value="fa-L">最终攻势 整备 左</option><option value="fa-R">最终攻势 整备 右</option>
</select>
</div>
<div class="rd-row"><input id="scrub" type="range" min="0" max="1000" value="0" aria-label="scrub" /><span id="time" class="rd-stats">0.0 s</span></div>
<div class="rd-stats" id="stats"></div>
</div>
<div class="rd-log" id="log"></div>
<script type="module" src="/dev/render-demo.js"></script>
</body>
</html>
+552
View File
@@ -0,0 +1,552 @@
// public/dev/render-demo.js — dev harness for the battlefield renderer (render/app.js):
// * prep scene: fake private state (hand / temp / board pieces, items, token stack), draggable; drops are
// applied locally as if the server accepted them (swap when occupied, equip items onto operators)
// * recordings (public/dev/recordings/*.json, tools/record-battle.mjs): real battles replayed at real speed
// through the same pushSnapshot/pushEvents path as the game, with play/pause, speed, scrub and scene select
// * stress scene: 120 units + projectiles / damage numbers every frame (FPS check)
// * fx scene: a small normal field (incl. art-less / unknown units, a crate and a turret device) cycling through
// every sim fx kind (render/fx.js FX_KINDS), every projectile kind and every status icon
// Query: ?scene=prep|<recording>|stress|numbers &stage=<stageId> &t=<seconds> &speed=<x> &paused=1 &panel=0 &quality=high|medium|low
// &pen=<recording> (prep: that recording's round preview — m.private.nextEnemies — in the enemy preview pen)
// &fa=L|R (prep on that half of the Final Assault boss field, view.setCamera('bossPrep', { side }))
// &board=2d|3d (the official 3D board scene, DESIGN §15; default 3D when the local art + WebGL2 are available)
// &cam=prep|normal|unite|boss|pen|bossPrep [&side=L|R &half=1 &shop=0] (official configBlackBoard framings)
// Puppeteer hook: window.__demo = { ready, view, seek(t), play(), pause(), setScene(name), stats(), log,
// refuseDrops (true ⇒ prep drops are silently ignored, like a server that rejects the move) }
import { createFieldView } from '../js/render/app.js';
import { data } from '../js/data.js';
import { assets } from '../js/assets.js';
import { GEO } from '../../shared/constants.js';
import { FX_KINDS } from '../js/render/fx.js';
const $ = (id) => document.getElementById(id);
const q = new URLSearchParams(location.search);
const logLines = [];
function log(...a) {
const s = a.map((x) => (typeof x === 'string' ? x : JSON.stringify(x))).join(' ');
logLines.push(s);
if (logLines.length > 12) logLines.shift();
$('log').textContent = logLines.join('\n');
}
window.addEventListener('error', (e) => log('ERROR', e.message));
window.addEventListener('unhandledrejection', (e) => log('REJECT', String(e.reason?.message || e.reason)));
const demo = { ready: false, errors: [], log: logLines };
window.__demo = demo;
let demoHover = null;
const STAGE_IDS = ['act2autochess_m01', 'act2autochess_m02', 'act2autochess_m03', 'act2autochess_m04', 'act1autochess_m01', 'act1autochess_m02', 'act1autochess_m03', 'act1autochess_m04'];
async function main() {
if (q.get('panel') === '0') $('panel').classList.add('is-hidden');
await data.loadAll('chess', 'tokens', 'items', 'enemies', 'stages', 'bonds', 'config');
await assets.ready();
const index = await fetch('/dev/recordings/index.json').then((r) => (r.ok ? r.json() : []), () => []);
const view = await createFieldView($('field'), {
data, assets, settings: { quality: q.get('quality') || 'high', damageNumbers: true },
...(q.has('aa') ? { antialias: q.get('aa') !== '0' } : {}),
});
demo.view = view;
window.__view = view;
// ---- UI --------------------------------------------------------------------------------------------------
const sceneSel = $('scene');
const scenes = [{ name: 'prep', title: '休整期 · 拖拽演示' }, ...index.map((r) => ({ name: r.name, title: r.title, rec: r })), { name: 'stress', title: '压力测试 · 120 单位' }, { name: 'fx', title: '特效图鉴 · 全部 fx' }, { name: 'numbers', title: '伤害数字 · 重叠测试' }];
for (const s of scenes) sceneSel.append(new Option(s.title, s.name));
const stageSel = $('stage');
for (const id of STAGE_IDS) { const st = data.lookup('stages', id); if (st) stageSel.append(new Option(st.name || id, id)); }
let current = null; // active scene controller
let playing = q.get('paused') !== '1';
let speed = Number(q.get('speed')) || 1;
$('speed').value = String(speed);
const setPlay = (p) => { playing = p; $('play').textContent = p ? '❚❚' : '▶'; };
setPlay(playing);
$('play').onclick = () => setPlay(!playing);
$('speed').onchange = () => { speed = Number($('speed').value) || 1; };
sceneSel.onchange = () => setScene(sceneSel.value);
stageSel.onchange = () => setScene(sceneSel.value, stageSel.value);
$('scrub').oninput = () => current?.seek?.((Number($('scrub').value) / 1000) * (current.duration || 0));
$('editable').onclick = () => { const on = !$('editable').classList.contains('is-on'); $('editable').classList.toggle('is-on', on); current?.setEditable?.(on); };
// board layer toggle (3D official scene ⇄ 2D atlas board) and the official camera framings
const syncBoardBtn = () => { const on = !!view.stats().board3d?.on; $('board3d').classList.toggle('is-on', on); $('board3d').textContent = on ? '3D' : '2D'; };
$('board3d').onclick = async () => { await view.setBoardMode?.(view.stats().board3d?.on ? '2d' : '3d'); syncBoardBtn(); };
setTimeout(syncBoardBtn, 0);
$('cam').onchange = () => {
const v = $('cam').value;
if (!v) return;
const [kind, side] = v.split('-');
if (kind === 'prep') view.setCamera('prep', { shop: side !== 'noshop' });
else if (kind === 'fa') view.setCamera('bossPrep', { side: side || 'L' });
else view.setCamera(kind, { side: side || 'L', half: !!side });
};
async function setScene(name, stageId) {
try { current?.stop?.(); } catch { /* ignore */ }
const s = scenes.find((x) => x.name === name) || scenes[0];
sceneSel.value = s.name;
$('title').textContent = s.title;
if (s.name === 'prep') current = await prepScene(view, stageId || q.get('stage') || 'act2autochess_m01', index);
else if (s.name === 'numbers') current = numbersScene(view, stageId || q.get('stage') || 'act2autochess_m02');
else if (s.name === 'stress') current = stressScene(view, stageId || q.get('stage') || 'act2autochess_m01');
else if (s.name === 'fx') current = fxScene(view, stageId || q.get('stage') || 'act2autochess_m03');
else current = await recordingScene(view, s.rec);
if (current.stageId) stageSel.value = current.stageId;
demo.scene = current;
return current;
}
demo.setScene = setScene;
demo.showRange = (uid) => (demoHover ? demoHover(uid) : false);
demo.seek = (t) => current?.seek?.(t);
demo.play = () => setPlay(true);
demo.pause = () => setPlay(false);
demo.stats = () => ({ ...view.stats(), scene: sceneSel.value, t: current?.time ?? 0, duration: current?.duration ?? 0 });
await setScene(q.get('scene') || 'prep');
const t0 = Number(q.get('t'));
if (Number.isFinite(t0) && t0 > 0) current?.seek?.(t0);
// ?cam=prep|normal|unite|boss|pen|bossPrep[&side=L|R&half=1&shop=0]: override the scene's camera (official framings)
if (q.get('cam')) {
view.setCamera(q.get('cam'), { side: q.get('side') || 'L', half: q.get('half') === '1', shop: q.get('shop') === '0' ? false : undefined, instant: true });
}
let last = performance.now();
const loop = (now) => {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
if (current?.tick) {
try { current.tick(playing ? dt * speed : 0); } catch (err) { log('tick', err.message); }
if (current.duration) {
$('scrub').value = String(Math.round((current.time / current.duration) * 1000));
$('time').textContent = `${current.time.toFixed(1)} / ${current.duration.toFixed(1)} s`;
}
}
const st = view.stats();
$('stats').textContent = `fps ${st.fps.toFixed(0)} frame ${st.frameMs.toFixed(1)} ms units ${st.units} particles ${st.particles} proj ${st.projectiles} nums ${st.numbers}\nspine ${st.spine ? `${st.spine.ready} ready / ${st.spine.loading} loading / ${st.spine.failed} failed` : '-'} rate ${st.rate?.toFixed?.(2) ?? '-'} buffered ${st.buffered}\nboard ${st.board3d?.on ? `3D · ${st.board3d.calls} calls · ${st.board3d.triangles} tris · ${st.board3d.cpuMs} ms` : '2D atlas'}`;
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
demo.ready = true;
}
// =============================================================================================================
// prep scene
async function prepScene(view, stageId, index = []) {
const stage = data.lookup('stages', stageId);
view.setStage(stage);
const fa = q.get('fa');
if (fa === 'L' || fa === 'R') view.setCamera('bossPrep', { side: fa });
else view.setCamera('prep', { rect: { ...GEO.NORMAL_RECT }, side: 'L' });
// the enemy preview pen: a recording's round preview (m.private.nextEnemies shape), else a small R4-like wave
let nextEnemies = [
{ enemyKey: 'enemy_1007_slime', count: 2, gate: 'lower', t: 3, fly: false, elite: false, source: 'wave' },
{ enemyKey: 'enemy_1000_gopro_2', count: 3, gate: 'upper', t: 3, fly: false, elite: false, source: 'wave' },
{ enemyKey: 'enemy_1019_jshoot', count: 3, gate: 'lower', t: 12, fly: false, elite: false, source: 'wave' },
{ enemyKey: 'enemy_1006_shield', count: 1, gate: 'lower', t: 13, fly: false, elite: true, source: 'wave' },
{ enemyKey: 'enemy_1005_yokai', count: 2, gate: 'upper', t: 7, fly: true, elite: false, source: 'wave' },
];
const penRec = q.get('pen') ? index.find((r) => r.name === q.get('pen')) : null;
if (penRec) {
const rec = await fetch(`/dev/recordings/${penRec.file}`).then((r) => r.json(), () => null);
if (Array.isArray(rec?.preview)) nextEnemies = rec.preview;
} else if (q.get('pen') === '0') nextEnemies = [];
const chess = data.list('chess').filter((c) => c.visible && !c.isGolden);
const pick = (name) => chess.find((c) => c.name === name);
const golden = (c) => data.lookup('chess', c.goldenId) || c;
const items = data.list('items').filter((i) => i.itemType === 'EQUIP' && !i.isGolden);
let uid = 1;
const P = (c, extra = {}) => ({ uid: uid++, kind: 'chess', id: c.chessId, golden: !!c.isGolden, tier: c.tier, items: [], ...extra });
const deploy = stage?.deployTiles?.normal || { melee: [], rangedOnly: [] };
const meleeTiles = deploy.melee.slice().sort((a, b) => a[0] - b[0] || b[1] - a[1]);
const rangedTiles = [...(deploy.rangedOnly || []), ...meleeTiles.slice().reverse()];
const state = { hand: Array(10).fill(null), temp: Array(5).fill(null), board: [], nextEnemies };
const taken = new Set();
const place = (c) => {
const list = c.position === 'MELEE' ? meleeTiles : rangedTiles;
const t = list.find(([r, cc]) => !taken.has(`${r},${cc}`));
if (!t) return;
taken.add(`${t[0]},${t[1]}`);
state.board.push({ ...P(c), row: t[0], col: t[1] });
};
for (const n of ['塞雷娅', '银灰', '德克萨斯', '能天使', '莫斯提马', '白面鸮', '伊内丝']) { const c = pick(n); if (c) place(n === '银灰' || n === '能天使' ? golden(c) : c); }
const it0 = items[3];
if (state.board[0] && it0) state.board[0].items = [{ uid: uid++, id: it0.id }];
const handNames = ['斯卡蒂', '夕', '史尔特尔', '缇缇', '蕾缪安', '异客'];
handNames.forEach((n, i) => { const c = pick(n); if (c) state.hand[i] = P(c); });
if (items[0]) state.hand[6] = { uid: uid++, kind: 'item', id: items[0].id, golden: false, tier: items[0].tier };
if (items[12]) state.hand[7] = { uid: uid++, kind: 'item', id: items[12].id, golden: false, tier: items[12].tier };
const tok = data.list('tokens').find((t) => t.kind === 'summon' && t.displayType === 'DEFAULT');
if (tok) state.hand[8] = { uid: uid++, kind: 'token', id: tok.tokenId, golden: false, tier: 1, count: 3 };
const c1 = pick('角峰'), c2 = pick('格雷伊');
if (c1) state.temp[0] = P(c1);
if (c2) state.temp[1] = P(c2);
let editable = true;
const find = (u) => {
for (let i = 0; i < 10; i++) if (state.hand[i]?.uid === u) return { area: 'hand', idx: i, piece: state.hand[i] };
for (let i = 0; i < 5; i++) if (state.temp[i]?.uid === u) return { area: 'temp', idx: i, piece: state.temp[i] };
const b = state.board.find((p) => p.uid === u);
return b ? { area: 'board', piece: b } : null;
};
const at = (t) => (t.area === 'hand' ? state.hand[t.idx] : state.board.find((p) => p.row === t.row && p.col === t.col) || null);
const tileOk = (piece, r, c) => {
const rec = data.lookup('chess', piece.id);
const inMelee = deploy.melee.some(([a, b]) => a === r && b === c);
const inRanged = (deploy.rangedOnly || []).some(([a, b]) => a === r && b === c);
return rec?.position === 'MELEE' ? inMelee : inMelee || inRanged;
};
const canPlace = (u, target) => {
const src = find(u);
if (!src || !editable) return false;
const occ = at(target);
if (src.piece.kind === 'item') return !!occ && occ.kind === 'chess' && (occ.items || []).length < 2;
if (target.area === 'hand') return !occ || src.area !== 'board' || occ.kind !== 'item';
if (src.piece.kind !== 'chess' && src.piece.kind !== 'token') return false;
if (!tileOk(src.piece, target.row, target.col) && src.piece.kind === 'chess') return false;
if (occ && src.area === 'board' && !tileOk(occ, src.piece.row, src.piece.col)) return false;
return true;
};
const apply = (u, target) => {
const src = find(u);
if (!src) return;
const occ = at(target);
if (src.piece.kind === 'item') {
if (occ && occ.kind === 'chess') { occ.items = [...(occ.items || []), { uid: src.piece.uid, id: src.piece.id }]; remove(src); }
return;
}
const moved = { ...src.piece };
remove(src);
if (occ) {
const o = { ...occ };
removeAt(target);
putAt(o, src.area === 'board' ? { area: 'board', row: src.piece.row, col: src.piece.col } : { area: src.area, idx: src.idx });
}
putAt(moved, target);
};
const remove = (loc) => {
if (loc.area === 'hand') state.hand[loc.idx] = null;
else if (loc.area === 'temp') state.temp[loc.idx] = null;
else state.board = state.board.filter((p) => p.uid !== loc.piece.uid);
};
const removeAt = (t) => {
if (t.area === 'hand') state.hand[t.idx] = null;
else state.board = state.board.filter((p) => !(p.row === t.row && p.col === t.col));
};
const putAt = (p, t) => {
const { row, col, ...rest } = p; // eslint-disable-line no-unused-vars
if (t.area === 'hand') state.hand[t.idx] = rest;
else if (t.area === 'temp') state.temp[t.idx] = rest;
else state.board.push({ ...rest, row: t.row, col: t.col });
};
const push = () => view.setPrep(JSON.parse(JSON.stringify(state)), { editable, canPlace });
push();
const offs = [
view.on('pieceDrop', (e) => {
log('pieceDrop', { uid: e.uid, target: e.target });
if (e.target.area === 'outside' || demo.refuseDrops) return;
// simulate the server round trip
setTimeout(() => { apply(e.uid, e.target); push(); }, 90);
}),
view.on('pieceClick', (e) => log('pieceClick', { uid: e.uid, detail: e.detail })),
view.on('pieceDragStart', (e) => { view.highlightTiles(null); log('pieceDragStart', { uid: e.uid, from: e.from }); }),
// hover a board operator ⇒ its attack range (orange) + its tile (mint), like the original
view.on('pieceHover', (e) => {
view.highlightTiles(null);
const p = e.uid != null ? state.board.find((x) => x.uid === e.uid) : null;
const rec = p ? data.lookup('chess', p.id) : null;
if (!rec || !Array.isArray(rec.rangeGrid)) return;
const tiles = rec.rangeGrid.map(([dr, dc]) => [p.row + dr, p.col + dc]).filter(([r, c]) => !(r === p.row && c === p.col));
view.highlightTiles(tiles, { color: 0xff9c33, fill: 0.28, line: 0.85, group: 'range' });
view.highlightTiles([[p.row, p.col]], { color: 0x4ed8af, fill: 0.3, line: 0.9, group: 'rangeStand' });
}),
];
demoHover = (uid) => {
const p = state.board.find((x) => x.uid === uid);
const rec = p ? data.lookup('chess', p.id) : null;
if (!rec) return false;
view.highlightTiles(rec.rangeGrid.map(([dr, dc]) => [p.row + dr, p.col + dc]).filter(([r, c]) => !(r === p.row && c === p.col)), 'range');
view.highlightTiles([[p.row, p.col]], 'rangeStand');
return true;
};
let hl = 0;
return {
stageId,
duration: 0,
time: 0,
setEditable(on) { editable = on; push(); },
tick(dt) {
hl += dt;
// periodic range preview of the first ranged board piece (demo of highlightTiles 'range')
void hl;
},
stop() { for (const off of offs) off(); view.highlightTiles(null); },
state,
};
}
// =============================================================================================================
// recording scene
async function recordingScene(view, meta) {
const rec = await fetch(`/dev/recordings/${meta.file}`).then((r) => r.json());
const stage = data.lookup('stages', rec.stageId);
const frames = rec.frames;
const t0 = frames[0].snap.t;
const duration = frames[frames.length - 1].snap.t - t0;
const kind = rec.kind === 'hidden' ? 'boss' : rec.kind;
const fieldId = rec.field.fieldId;
let idx = 0, gameT = t0, endWait = 0;
const start = () => {
view.setStage(stage);
view.enterBattle(rec.field);
view.setCamera(kind, { rect: rec.field.rect, side: 'L' });
idx = 0; gameT = t0 - 0.001; endWait = 0;
};
start();
const feed = () => {
while (idx < frames.length && frames[idx].snap.t <= gameT) {
const f = frames[idx++];
// like the match server: b.ev first, then the b.snap of the same drain, both with the game time `gt`
// (the recorded snapshot keeps its numeric `t`, which the engine also accepts)
if (f.ev.length) view.pushEvents({ t: 'b.ev', fieldId, gt: f.snap.t, ev: f.ev });
view.pushSnapshot(f.snap);
}
};
const seek = (t) => {
const target = t0 + Math.max(0, Math.min(duration, t));
view.enterBattle(rec.field);
view.setCamera(kind, { rect: rec.field.rect, side: 'L', instant: true });
let k = 0;
while (k + 1 < frames.length && frames[k + 1].snap.t <= target) k++;
const infos = new Map();
for (let i = 0; i <= k; i++) for (const e of frames[i].ev) if (e[0] === 'spawn') infos.set(e[1].id, e[1]);
const present = new Set(frames[k].snap.units.map((u) => u[0]));
const spawns = [...infos.values()].filter((u) => present.has(u.id)).map((u) => ['spawn', u]);
if (spawns.length) view.pushEvents({ fieldId, ev: spawns });
// replay the last few snapshots so the interpolation buffer has a history
const from = Math.max(0, k - 3);
for (let i = from; i <= k; i++) view.pushSnapshot(frames[i].snap);
view.debug.interp.snapToNewest();
idx = k + 1;
gameT = frames[k].snap.t;
};
return {
stageId: rec.stageId,
duration,
get time() { return Math.max(0, gameT - t0); },
seek,
tick(dt) {
if (dt <= 0) return;
if (idx >= frames.length) {
endWait += dt;
if (endWait > 3) start();
return;
}
gameT += dt * 2; // combat runs at 2× real time
feed();
},
stop() {},
rec,
};
}
// =============================================================================================================
// stress scene: 120 units, constant projectiles / numbers
function stressScene(view, stageId) {
const stage = data.lookup('stages', stageId);
view.setStage(stage);
const rect = { r0: 9, r1: 12, c0: 0, c1: 20 };
const chess = data.list('chess').filter((c) => c.visible && !c.isGolden);
const enemies = data.list('enemies').filter((e) => assets.spineEntry(e.key) && e.rank !== 'BOSS');
let seed = 7;
const rnd = () => ((seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff);
const units = [];
let id = 1;
for (let i = 0; i < 40; i++) {
const c = chess[(i * 7) % chess.length];
units.push({ id: id++, kind: 'op', side: 'ally', defId: c.chessId, name: c.name, tier: c.tier, golden: i % 6 === 0, spine: c.assets?.spine, avatar: c.assets?.avatar, x: 2 + (i % 17), y: 9 + ((i / 17) | 0) % 4, facing: 1, maxHp: 3000 });
}
for (let i = 0; i < 80; i++) {
const e = enemies[(i * 5) % enemies.length];
units.push({ id: id++, kind: 'enemy', side: 'enemy', defId: e.key, name: e.name, tier: 1, golden: false, spine: e.spine || e.key, avatar: e.iconId || e.key, x: 20 - (i % 20), y: 9 + (i % 4) + 0.3, facing: -1, maxHp: 5000 });
}
const field = { fieldId: 'stress', kind: 'unite', rect, stageId, units };
view.enterBattle(field);
view.setCamera('unite', { rect });
let t = 0, acc = 0;
const hp = new Map(units.map((u) => [u.id, u.maxHp]));
const kinds = ['arrow', 'bolt', 'orb', 'none', 'bomb', 'chain'];
return {
stageId,
duration: 0,
time: 0,
tick(dt) {
if (dt <= 0) return;
acc += dt * 2;
while (acc >= 0.1) {
acc -= 0.1;
t += 0.1;
const tuples = units.map((u) => {
let x = u.x, y = u.y;
if (u.side === 'enemy') { x = 1 + ((u.x - t * 0.35 + 40) % 19); y = u.y + Math.sin(t + u.id) * 0.25; }
let h = hp.get(u.id);
if (h <= 0) h = u.maxHp;
hp.set(u.id, h);
const flags = (u.id % 11 === 0 ? 2 : 0) | (u.id % 13 === 0 ? 16 : 0);
return [u.id, Math.round(x * 100) / 100, Math.round(y * 100) / 100, h, u.maxHp, (t * 3 + u.id) % 20, 20, flags, u.side === 'enemy' ? 1 : 0];
});
const ev = [];
for (let k = 0; k < 14; k++) {
const a = units[(rnd() * 40) | 0], b = units[40 + ((rnd() * 80) | 0)];
ev.push(['atk', a.id, b.id, kinds[(rnd() * kinds.length) | 0]]);
const d = 100 + ((rnd() * 900) | 0);
hp.set(b.id, hp.get(b.id) - d);
ev.push(['dmg', b.id, d, rnd() < 0.6 ? 'phys' : 'arts']);
}
for (let k = 0; k < 4; k++) { const b = units[(rnd() * 40) | 0]; ev.push(['heal', b.id, 150]); }
if (rnd() < 0.2) ev.push(['skill', units[(rnd() * 40) | 0].id, 1]);
view.pushEvents({ t: 'b.ev', fieldId: 'stress', gt: t, ev });
view.pushSnapshot({ t: 'b.snap', fieldId: 'stress', gt: t, units: tuples, dp: 10, killed: 0, total: 80 });
}
},
stop() {},
};
}
// =============================================================================================================
// fx gallery: every fx kind, projectile kind and status on a small static field
function fxScene(view, stageId) {
const stage = data.lookup('stages', stageId);
view.setStage(stage);
const rect = { ...GEO.NORMAL_RECT };
const pick = (name) => data.list('chess').find((c) => c.name === name && !c.isGolden);
const op = (id, name, x, y) => { const c = pick(name); return c ? { id, kind: 'op', side: 'ally', defId: c.chessId, name, tier: c.tier, golden: false, spine: c.assets?.spine, avatar: c.assets?.avatar, x, y, facing: 1, maxHp: 3000 } : null; };
const en = (id, key, x, y, extra = {}) => { const e = data.lookup('enemies', key); return { id, kind: 'enemy', side: 'enemy', defId: key, name: e?.name || key, tier: 1, golden: false, spine: e?.spine || key, avatar: e?.iconId || key, x, y, facing: -1, maxHp: 4000, ...extra }; };
const units = [
op(1, '银灰', 4, 9), op(2, '能天使', 3, 11), op(3, '塞雷娅', 5, 10), op(4, '莫斯提马', 6, 12),
en(20, 'enemy_1007_slime', 7, 9), en(21, 'enemy_5601_entlec', 8, 10), en(22, 'enemy_nonexistent_x', 7.4, 11),
en(23, 'enemy_1042_frostd', 9, 9.2), en(24, 'enemy_2016_csphtm', 8.6, 12, { boss: true, maxHp: 90000 }),
{ id: 30, kind: 'device', side: 'ally', defId: 'trap_1105_accrate', name: 'crate', x: 6, y: 9, facing: 1, maxHp: 100 },
{ id: 31, kind: 'device', side: 'ally', defId: 'trap_1104_aclasert', name: 'turret', x: 5, y: 12, facing: 1, maxHp: 3000 },
].filter(Boolean);
view.enterBattle({ fieldId: 'fx', kind: 'normal', rect, stageId, units });
view.setCamera('normal', { rect });
const kinds = Object.keys(FX_KINDS);
const projs = ['arrow', 'bolt', 'orb', 'bomb', 'lob', 'drone', 'enemy', 'chain', 'chainHeal', 'beam', 'none'];
const statuses = ['stun', 'ab:frost', 'reed2:scorch', 'fragile', 'sleep', 'silence', 'levitate', 'skill:shotst_shred', 'lumen:resist'];
let t = 0, acc = 0, i = 0;
const hp = new Map(units.map((u) => [u.id, u.maxHp]));
const extraFor = (k) => {
const base = { id: [1, 20, 21, 3, 23][i % 5] };
switch (FX_KINDS[k].a) {
case 'beam': case 'bolt': return { ...base, from: 1, to: 20, src: 2 };
case 'volley': return { ...base, id: 2, src: 2, targets: [20, 21, 23] };
case 'blink': return { ...base, id: 21, fx: 9, fy: 11 };
case 'move': return { ...base, id: 20, fromX: 9, fromY: 9 };
case 'zone': case 'telegraph': return { ...base, r: 1.5, dur: 2, tiles: k === 'telegraph' ? 'box' : undefined };
case 'dp': return { ...base, id: 3, n: 10 };
case 'element': return { ...base, id: 21, element: ['burn', 'neural', 'apoptosis'][i % 3] };
default: return base;
}
};
return {
stageId, duration: 0, time: 0,
get kind() { return kinds[(i - 1 + kinds.length) % kinds.length]; },
tick(dt) {
if (dt <= 0) return;
acc += dt * 2;
while (acc >= 0.1) {
acc -= 0.1;
t += 0.1;
const ev = [];
if (Math.round(t * 10) % 5 === 0) {
const k = kinds[i % kinds.length];
const ex = extraFor(k);
const at = units.find((u) => u.id === ex.id) || units[0];
ev.push(['fx', k, at.x, at.y, ex]);
$('title').textContent = `特效图鉴 · ${k} (${FX_KINDS[k].a})`;
const a = units[i % 4], b = units[4 + (i % 5)];
const pk = projs[i % projs.length];
ev.push(['atk', pk === 'chain' ? 20 : a.id, b.id, pk]);
ev.push(['dmg', b.id, 120 + (i * 37) % 900, ['phys', 'arts', 'true', 'burn'][i % 4]]);
if (i % 3 === 0) ev.push(['heal', units[i % 4].id, 240]);
const st = statuses[i % statuses.length];
ev.push(['status', b.id, st, 1], ['status', units[4 + ((i + 2) % 5)].id, statuses[(i + 4) % statuses.length], 0]);
if (i % 6 === 0) ev.push(['skill', units[i % 4].id, 1]);
if (i % 6 === 3) ev.push(['skill', units[(i - 3) % 4].id, 0]);
hp.set(b.id, Math.max(1, hp.get(b.id) - 300));
i++;
}
const tuples = units.map((u) => {
const h = hp.get(u.id);
const flags = u.id === 21 ? 1 : u.id === 23 ? 128 : u.id === 2 ? 32 : 0;
return [u.id, u.x, u.y, h, u.maxHp, (t * 2 + u.id) % 20, u.kind === 'op' ? 20 : 0, flags, u.side === 'enemy' ? 2 : 0];
});
view.pushEvents({ t: 'b.ev', fieldId: 'fx', gt: t, ev });
view.pushSnapshot({ t: 'b.snap', fieldId: 'fx', gt: t, units: tuples, dp: 10, killed: 0, total: 5 });
}
},
stop() {},
};
}
// =============================================================================================================
// damage numbers: a boss and a knot of minions standing on top of each other take rapid mixed hits (the '41509' /
// '201625' overlap cases: numbers of different units side by side must never read as one)
function numbersScene(view, stageId) {
const stage = data.lookup('stages', stageId);
view.setStage(stage);
const rect = { ...GEO.BOSS_RECT };
const pick = (name) => data.list('chess').find((c) => c.name === name && !c.isGolden);
const op = (id, name, x, y) => { const c = pick(name); return c ? { id, kind: 'op', side: 'ally', defId: c.chessId, name, tier: c.tier, golden: false, spine: c.assets?.spine, avatar: c.assets?.avatar, x, y, facing: 1, dir: 'RIGHT', maxHp: 4000 } : null; };
const en = (id, key, x, y, extra = {}) => { const e = data.lookup('enemies', key); return { id, kind: 'enemy', side: 'enemy', defId: key, name: e?.name || key, tier: 1, golden: false, spine: e?.spine || key, avatar: e?.iconId || key, x, y, facing: -1, maxHp: 6000, ...extra }; };
const units = [
op(1, '史尔特尔', 6, 3), op(2, '能天使', 5, 4), op(3, '莫斯提马', 5, 2), op(4, '白面鸮', 4, 3),
en(20, 'enemy_1001_bigbo', 7.2, 3.1, { boss: true, maxHp: 400000 }),
en(21, 'enemy_1007_slime', 7.35, 3.0), en(22, 'enemy_1007_slime', 7.1, 2.9), en(23, 'enemy_1000_gopro_2', 7.5, 3.2),
en(24, 'enemy_1006_shield', 8.1, 3.05, { maxHp: 12000 }), en(25, 'enemy_1019_jshoot', 7.6, 4.1),
].filter(Boolean);
view.enterBattle({ fieldId: 'nums', kind: 'boss', rect, stageId, units });
view.setCamera('boss', { rect, side: 'L', half: true });
let seed = 11;
const rnd = () => ((seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff);
const hp = new Map(units.map((u) => [u.id, u.maxHp]));
const enemies = units.filter((u) => u.side === 'enemy');
let t = 0, acc = 0;
return {
stageId, duration: 0, time: 0,
tick(dt) {
if (dt <= 0) return;
acc += dt * 2;
while (acc >= 0.1) {
acc -= 0.1;
t += 0.1;
const ev = [];
for (let k = 0; k < 3; k++) {
const b = enemies[(rnd() * enemies.length) | 0];
const boss = !!b.boss;
const style = ['phys', 'arts', 'phys', 'true', 'burn'][(rnd() * 5) | 0];
const n = boss ? 2000 + ((rnd() * 200000) | 0) : 40 + ((rnd() * 900) | 0);
ev.push(['atk', units[(rnd() * 4) | 0].id, b.id, 'none'], ['dmg', b.id, n, style]);
hp.set(b.id, Math.max(1, hp.get(b.id) - n * 0.02));
}
if (rnd() < 0.5) ev.push(['heal', units[(rnd() * 4) | 0].id, 60 + ((rnd() * 400) | 0)]);
const tuples = units.map((u) => [u.id, u.x, u.y, hp.get(u.id), u.maxHp, 5, 20, u.side === 'enemy' && u.x < 7.4 ? 1 : 0, 0]);
view.pushEvents({ t: 'b.ev', fieldId: 'nums', gt: t, ev });
view.pushSnapshot({ t: 'b.snap', fieldId: 'nums', gt: t, units: tuples, dp: 10, killed: 0, total: 6 });
}
},
stop() {},
};
}
main().catch((err) => { log('FATAL', err.message); console.error(err); demo.error = err.message; });
+348
View File
@@ -0,0 +1,348 @@
<!doctype html>
<!--
UI kit gallery (visual QA for js/ui/components.js + css). Standalone page: /dev/uikit.html
Renders every design-system component in its variants/states. Uses real bond icons from
data/assets.json + names from data/bonds.json when those files exist.
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>UI Kit · 卫戍协议:盟约</title>
<link rel="icon" href="data:," />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" media="print" onload="this.media='all'"
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&family=Oxanium:wght@400;500;600;700&family=Rajdhani:wght@500;600;700&display=swap" />
<link rel="stylesheet" href="/fonts/fonts.css" />
<link rel="stylesheet" href="/css/theme.css" />
<link rel="stylesheet" href="/css/components.css" />
<link rel="stylesheet" href="/css/emotes.css" />
<link rel="stylesheet" href="/css/devices.css" />
<script type="importmap">
{ "imports": { "preact": "/vendor/preact.module.js", "preact/hooks": "/vendor/hooks.module.js", "htm": "/vendor/htm.module.js" } }
</script>
<style>
/* Gallery page only: a scrollable document instead of the fixed game viewport. */
html { font-size: clamp(40px, calc(100vw / 19.2), 140px); }
body { overflow: auto; height: auto; min-height: 100%; }
.kit { position: relative; z-index: 1; max-width: 18rem; margin: 0 auto; padding: .5rem .5rem 1.2rem; }
.kit-bg { position: fixed; inset: 0; z-index: 0; }
.kit-head { display: flex; align-items: flex-end; justify-content: space-between; gap: .4rem; padding-bottom: .24rem; border-bottom: 1px solid var(--line); }
.kit-head h1 { font-size: .5rem; font-weight: 900; letter-spacing: .06em; }
.kit-head h1 em { font-style: normal; color: var(--mint-500); }
.kit-sec { margin-top: .5rem; }
.kit-sec > h2 { display: flex; align-items: baseline; gap: .14rem; font-size: .24rem; margin-bottom: .2rem; }
.kit-sec > h2 .num { color: var(--mint-500); font-size: .18rem; }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: .24rem; margin-bottom: .22rem; }
.row--top { align-items: flex-start; }
.cap { font-size: .12rem; color: var(--text-dim); font-family: var(--font-display); letter-spacing: .14em; text-transform: uppercase; width: 100%; margin-bottom: -.1rem; }
.grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .24rem; }
.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .24rem; }
.swatch { display: flex; flex-direction: column; gap: .06rem; width: 1.3rem; font-size: .12rem; color: var(--text-lo); }
.swatch i { display: block; height: .56rem; border: 1px solid var(--line); }
.swatch b { color: var(--text-hi); font-family: var(--font-num); font-weight: 700; font-size: .14rem; }
.type-demo > * { display: block; margin-bottom: .08rem; }
.stack { display: flex; flex-direction: column; gap: .14rem; min-width: 3rem; }
/* 13 emotes */
.emo-stage { position: relative; height: 3.3rem; padding: .2rem; background: rgba(0, 0, 0, .35); border: 1px solid var(--line); }
.emo-stage .ewheel { position: absolute; left: .2rem; bottom: .2rem; }
.emo-team { position: absolute; left: 3.7rem; top: .3rem; display: flex; flex-direction: column; gap: .14rem; }
.emo-row { position: relative; display: flex; align-items: center; gap: .1rem; }
.emo-row .pav { width: .8rem; height: .8rem; display: grid; place-items: center; background: linear-gradient(135deg, #2c4a40, #10201b); border: 1px solid var(--line-2); font-size: .3rem; font-weight: 900; color: var(--mint-400); }
.emo-row .ebubble { position: absolute; left: calc(100% + .06rem); top: 50%; transform: translateY(-50%); }
.emo-bubbles { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
.emo-bubbles figure { margin: 0; display: flex; flex-direction: column; align-items: flex-start; gap: .06rem; font-size: .11rem; color: var(--text-dim); }
.emo-cat { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: .12rem .2rem; }
.emo-cat figure { margin: 0; display: flex; flex-direction: column; align-items: center; gap: .04rem; font-size: .1rem; color: var(--text-dim); font-family: var(--font-mono); text-align: center; }
.emo-cat .cell { width: .85rem; height: .85rem; padding: .07rem; box-sizing: border-box; background: rgba(255, 255, 255, .12); border-radius: .08rem; }
.emo-cat h3 { grid-column: 1 / -1; font-size: .14rem; color: var(--text-md); margin: .06rem 0 0; }
</style>
</head>
<body>
<div class="app-bg kit-bg" aria-hidden="true"></div>
<div id="kit"></div>
<script type="module">
import '/js/ui/compat.js';
import { render } from '/vendor/preact.module.js';
import { useState } from '/vendor/hooks.module.js';
import {
html, Button, Panel, MicroLabel, Chevrons, HexBadge, TierChip, BondDisc, SevenSeg, Countdown, Modal, confirmDialog,
alertDialog, Tooltip, ProgressBar, Tabs, Spinner, AvatarFrame, PhaseBanner, PingPill, DifficultyTag, TextField,
UiHosts, Icon, ICONS,
} from '/js/ui/components.js';
import { ToastHost, toast } from '/js/ui/toasts.js';
import { data, useData, getBond } from '/js/data.js';
import { DIFFICULTIES, EMOTE_THEMES, EMOTES } from '/shared/constants.js';
import { EmoteWheel, EmoteBubble, EmoteArt } from '/js/ui/emotes.js';
const SWATCHES = [
['bg-0', '#0c0f0e'], ['bg-1', '#141816'], ['bg-2', '#1e2421'], ['line', '#2f3a35'],
['mint-500', '#4ed8af'], ['mint-400', '#59f4ca'], ['mint-600', '#35d8b4'], ['mint-glow', '#17f9b7'],
['gold', '#ffc600'], ['amber', '#f6a329'], ['orange', '#e85a1a'], ['red', '#e73118'], ['abyss', '#ff0024'],
['premium', '#ff5454'], ['ice', '#9fd4ff'], ['text-lo', '#8a948f'],
];
const BONDS = ['yanShip', 'sargonShip', 'victoriaShip', 'kjeragShip', 'preciShip', 'swiftShip', 'deputShip', 'investShip'];
function Sec({ n, title, micro, children }) {
return html`<section class="kit-sec">
<h2><span class="num">${n}</span>${title}<${MicroLabel}>${micro}<//></h2>
${children}
</section>`;
}
/** 13 · official emotes: wheel (pager per theme) → bubble beside the sender's avatar; bubbles per theme; catalog. */
function EmoteDemo() {
useData('local');
const [open, setOpen] = useState(true);
const [offline, setOffline] = useState(false);
const [bubbles, setBubbles] = useState(() => ({ me: null, p2: { id: 'slug_autochess_battle_nice_work', seq: 1 } }));
const [replay, setReplay] = useState(0);
const say = (who, id) => setBubbles((b) => ({ ...b, [who]: { id, seq: Date.now() + Math.random(), at: Date.now() } }));
const rows = [['me', '我'], ['p2', '2P']];
return html`
<div class="grid2">
<div class="emo-stage" id="emo-stage">
<div class="emo-team">
${rows.map(([who, t]) => html`<div class="emo-row" key=${who}><span class="pav">${t}</span>
${bubbles[who] ? html`<${EmoteBubble} key=${bubbles[who].seq} id=${bubbles[who].id} at=${bubbles[who].at} class="team__bubble" />` : null}</div>`)}
</div>
<${EmoteWheel} open=${open} onToggle=${setOpen} disabled=${offline} onSend=${(id) => say('me', id)} />
</div>
<div class="stack">
<div class="row">
<${Button} size="sm" onClick=${() => setOpen((o) => !o)}>${open ? '收起面板' : '展开面板'}<//>
<${Button} size="sm" variant="amber" onClick=${() => say('p2', EMOTES[Math.floor(Math.random() * EMOTES.length)])}>队友发送表情<//>
<${Button} size="sm" variant="ghost" onClick=${() => say('p2', 'not_an_emote')}>缺图回退<//>
<${Button} size="sm" variant=${offline ? 'danger' : 'ghost'} onClick=${() => setOffline((v) => !v)}>${offline ? '已断线' : '断线'}<//>
</div>
<p class="t-lo" style="font-size:.14rem">左右滑动 / 拖动 / ← → / 点击圆点切换主题;发送后按钮冷却 1 秒;气泡 3 秒后淡出,新表情替换旧表情。</p>
<div class="emo-bubbles" key=${replay}>
${EMOTE_THEMES.map((t) => html`<figure key=${t.themeId}><${EmoteBubble} id=${t.emotes[0].id} ttl=${600000} /><figcaption>${t.dir}</figcaption></figure>`)}
<figure><${EmoteBubble} id="missing_art" ttl=${600000} /><figcaption>fallback</figcaption></figure>
</div>
<div class="row"><${Button} size="sm" icon="refresh" onClick=${() => setReplay((r) => r + 1)}>重播气泡<//></div>
</div>
</div>
<div class="emo-cat" style="margin-top:.3rem">
${EMOTE_THEMES.map((t, ti) => [
html`<h3 key=${`h${t.themeId}`}>${ti + 1}. ${t.name} <span class="t-lo">(${t.themeId})</span></h3>`,
...t.emotes.map((e) => html`<figure key=${e.id}><div class="cell"><${EmoteArt} id=${e.id} /></div><figcaption>${e.sortId} · ${e.picId}</figcaption></figure>`),
])}
</div>`;
}
function Kit() {
useData('assets', 'bonds');
const assets = data.get('assets');
const [tab, setTab] = useState('shop');
const [modal, setModal] = useState(false);
const [banner, setBanner] = useState(0);
const [name, setName] = useState('凯尔希');
const [code, setCode] = useState('');
const [deadline] = useState(() => Date.now() + 25000);
const [progress, setProgress] = useState(62);
const bond = (id) => ({ id, name: getBond(id)?.name || id.replace('Ship', ''), icon: assets?.bonds?.[id] || null });
return html`<div class="kit">
<header class="kit-head">
<div>
<${MicroLabel} tone="mint">STRONGHOLD PROTOCOL // DESIGN SYSTEM<//>
<h1>UI Kit · 卫戍协议<em>:盟约</em></h1>
</div>
<div class="row" style="margin:0">
<${PingPill} ms=${38} /><${PingPill} ms=${120} /><${PingPill} ms=${260} /><${PingPill} online=${false} />
</div>
</header>
<${Sec} n="01" title="色板" micro="PALETTE">
<div class="row">${SWATCHES.map(([n, c]) => html`<div class="swatch" key=${n}><i style=${`background:${c}`}></i><b>${n}</b>${c}</div>`)}</div>
<//>
<${Sec} n="02" title="字体" micro="TYPOGRAPHY">
<div class="grid2">
<div class="type-demo">
<span style="font-size:.56rem;font-weight:900">卫戍协议:盟约</span>
<span style="font-size:.26rem;font-weight:700">休整期 · 调度中心 · 联防阶段</span>
<span style="font-size:.18rem">正文 Noto Sans SC — 调配资金与干员,抵御多波次进攻。</span>
<span class="t-lo" style="font-size:.15rem">次要文本 text-lo · 说明与提示</span>
</div>
<div class="type-demo">
<span class="display" style="font-size:.34rem">STRONGHOLD PROTOCOL</span>
<span class="num" style="font-size:.5rem;font-weight:700">0123456789 <span class="t-gold">+12</span></span>
<${MicroLabel} tone="mint">RHODES ISLAND // SIMULATION SERVICE<//>
<${MicroLabel}>COUNTDOWN · LEVEL · STAGE 04<//>
</div>
</div>
<//>
<${Sec} n="03" title="按钮" micro="BUTTON">
${['primary', 'secondary', 'amber', 'ice', 'danger', 'ghost'].map((v) => html`<div class="row" key=${v}>
<span class="cap">${v}</span>
<${Button} variant=${v} size="sm">小按钮<//>
<${Button} variant=${v} icon="check">准备就绪<//>
<${Button} variant=${v} size="lg" iconRight="chevrons">开始模拟<//>
<${Button} variant=${v} square=${true} icon=${v === 'danger' ? 'exit' : v === 'ice' ? 'snow' : v === 'amber' ? 'refresh' : 'plus'} aria-label="icon" />
<${Button} variant=${v} disabled=${true}>禁用<//>
<${Button} variant=${v} loading=${true}>加载中<//>
</div>`)}
<div class="row"><span class="cap">xl / active / block</span>
<${Button} variant="primary" size="xl" iconRight="chevrons">开始<//>
<${Button} variant="secondary" active=${true} icon="check">已选定<//>
<div style="width:3rem"><${Button} variant="amber" block=${true} icon="users">加入同盟<//></div>
</div>
<//>
<${Sec} n="04" title="面板" micro="PANEL · BRACKET FRAME">
<div class="grid3">
<${Panel} title="调度中心" micro="SHOP · LEVEL 3" actions=${html`<${HexBadge} value=${5} size="sm" />`}>
<p class="t-lo">带括号角框的面板,标题栏含微型技术标签与操作区。</p>
<//>
<${Panel} title="联防阶段" micro="UNITE" tone="orange"><p class="t-lo">tone = orange</p><//>
<${Panel} title="模拟结束" micro="RESULT" tone="gold" glow=${true}><p class="t-lo">tone = gold, glow</p><//>
</div>
<//>
<${Sec} n="05" title="徽记" micro="HEX BADGE · TIER CHIP · DIFFICULTY">
<div class="row"><span class="cap">hex badge</span>
<${HexBadge} value=${2} /><${HexBadge} value=${4} tone="discount" /><${HexBadge} value=${5} tone="premium" />
<${HexBadge} value=${0} tone="mint" /><${HexBadge} value=${1} tone="amber" icon="refresh" /><${HexBadge} value=${'VI'} tone="dark" />
<${HexBadge} value=${13} size="lg" icon="rook" tone="gold" /><${HexBadge} value=${3} size="sm" tone="ice" />
</div>
<div class="row"><span class="cap">tier chip</span>
${[1, 2, 3, 4, 5, 6].map((t) => html`<${TierChip} key=${t} tier=${t} />`)}
${[1, 3, 6].map((t) => html`<${TierChip} key=${`g${t}`} tier=${t} golden=${true} />`)}
<${TierChip} tier=${2} size="sm" /><${TierChip} tier=${5} size="lg" /><${TierChip} tier=${6} size="lg" golden=${true} />
</div>
<div class="row"><span class="cap">difficulty</span>
${DIFFICULTIES.map((d) => html`<${DifficultyTag} key=${d} difficulty=${d} code=${`AC-${DIFFICULTIES.indexOf(d) + 1}`} />`)}
<${DifficultyTag} difficulty="ABYSS" size="lg" />
</div>
<//>
<${Sec} n="06" title="盟约" micro="BOND DISC">
<div class="row">
${BONDS.map((id, i) => {
const b = bond(id);
return html`<${BondDisc} key=${id} name=${b.name} icon=${b.icon} layers=${[358, 155, 144, 34, 14, 10, 3, 0][i]}
tier=${[3, 2, 2, 1, 1, 1, 0, 0][i]} maxTier=${3} active=${i < 6} disabled=${i === 7} onClick=${() => toast(`${b.name}:查看盟约详情`)} />`;
})}
</div>
<div class="row"><span class="cap">sizes · layers disabled · glyph fallback</span>
<${BondDisc} name="炎" size="sm" layers=${4} tier=${1} active=${true} />
<${BondDisc} name="萨尔贡" size="lg" layers=${12} tier=${2} maxTier=${4} active=${true} icon=${bond('sargonShip').icon} />
<${BondDisc} name="助力" layers=${34} tier=${1} active=${true} layersDisabled=${true} />
<${BondDisc} name="谢拉格" layers=${0} maxTier=${2} />
</div>
<//>
<${Sec} n="07" title="倒计时" micro="SEVEN SEGMENT · COUNTDOWN">
<div class="row row--top">
<${Countdown} seconds=${59} total=${65} />
<${Countdown} seconds=${8} total=${65} />
<${Countdown} deadline=${deadline} total=${25} size="lg" />
<${Countdown} seconds=${195} total=${195} size="sm" />
<${Countdown} />
</div>
<div class="row">
<${SevenSeg} text="12:45" size=".5rem" /><${SevenSeg} text="-99" tone="orange" size=".5rem" />
<${SevenSeg} text="0123456789" tone="gold" size=".4rem" /><${SevenSeg} text="HELP" tone="ice" size=".4rem" />
<${SevenSeg} text="Err" tone="red" size=".4rem" />
</div>
<//>
<${Sec} n="08" title="横幅" micro="PHASE BANNER">
<div class="stack">
<${PhaseBanner} key=${`a${banner}`} title="休整期" micro="PHASE // PREP" sub="第 3 回合" />
<${PhaseBanner} key=${`b${banner}`} title="联防阶段" micro="PHASE // UNITE" sub="层数叠加已禁用" tone="orange" />
<${PhaseBanner} key=${`c${banner}`} title="最终攻势" micro="PHASE // FINAL ASSAULT" tone="red" />
</div>
<div class="row" style="margin-top:.2rem">
<${Button} icon="refresh" onClick=${() => setBanner((b) => b + 1)}>重播动画<//>
<${Button} variant="amber" onClick=${() => setBanner((b) => -(Math.abs(b) + 1))}>全屏横幅 (2.2s)<//>
<${Chevrons} /><${Chevrons} tone="orange" count=${5} /><${Chevrons} tone="gold" dir="left" />
</div>
${banner < 0 ? html`<${PhaseBanner} key=${`o${banner}`} mode="overlay" duration=${2200} title="机变阶段" micro="PHASE // SP DRAFT"
sub="当前轮到你决策" tone="gold" onDone=${() => setBanner(0)} />` : null}
<//>
<${Sec} n="09" title="进度 · 标签页 · 加载" micro="PROGRESS · TABS · SPINNER">
<div class="grid2">
<div class="stack">
<${ProgressBar} value=${progress} label="首领生命值" showValue=${true} tone="orange" />
<${ProgressBar} value=${7} max=${9} segments=${9} label="已部署" showValue=${true} />
<${ProgressBar} value=${35} tone="ice" size="sm" />
<${ProgressBar} value=${90} tone="gold" size="lg" label="经验" />
<div class="row"><${Button} size="sm" onClick=${() => setProgress((p) => (p + 17) % 101)}>改变进度<//></div>
</div>
<div class="stack">
<${Tabs} value=${tab} onChange=${setTab} items=${[
{ id: 'shop', label: '调度中心' }, { id: 'bonds', label: '盟约', badge: 6 }, { id: 'enemy', label: '敌方情报' },
{ id: 'locked', label: '未解锁', disabled: true },
]} />
<${Tabs} size="sm" value=${tab} onChange=${setTab} items=${[{ id: 'shop', label: '自己' }, { id: 'bonds', label: '队友' }]} />
<div class="row"><${Spinner} size="sm" /><${Spinner} label="LOADING" /><${Spinner} size="lg" tone="gold" label="SYNCING" /></div>
</div>
</div>
<//>
<${Sec} n="10" title="头像框" micro="AVATAR FRAME">
<div class="row">
<${AvatarFrame} name="凯尔希" size="lg" host=${true} self=${true} ready=${true} />
<${AvatarFrame} name="Doctor" size="lg" seat=${1} />
<${AvatarFrame} name="AI·华法琳" size="lg" seat=${2} bot=${true} />
<${AvatarFrame} name="阿米娅" size="lg" seat=${3} offline=${true} />
<${AvatarFrame} size="lg" empty=${true} />
<${AvatarFrame} name="W" size="md" seat=${2} dead=${true} />
<${AvatarFrame} name="惊蛰" size="sm" seat=${1} self=${true} />
<${AvatarFrame} name="艾雅法拉" size="xl" seat=${0} host=${true} />
</div>
<//>
<${Sec} n="11" title="输入 · 提示 · 对话框" micro="FIELD · TOOLTIP · MODAL · TOAST">
<div class="grid2">
<div class="stack">
<${TextField} label="博士代号" micro="CALLSIGN" icon="user" value=${name} onInput=${setName} maxLength=${12} />
<${TextField} size="code" icon="key" value=${code} placeholder="输入同盟密钥" onInput=${(v) => setCode(v.toUpperCase().slice(0, 4))} />
<${TextField} label="无效输入" value="!!" onInput=${() => {}} invalid=${true} hint="仅限字母与数字" />
</div>
<div class="stack">
<div class="row">
<${Tooltip} text="刷新调度中心(消耗 1 资金)"><${Button} variant="amber" icon="refresh">刷新<//><//>
<${Tooltip} text=${'冻结后,调度中心在下一回合开始时保留\n快捷键 F'} placement="bottom"><${Button} variant="ice" icon="snow">冻结<//><//>
<${Tooltip} text="只有房主可以操作"><${TierChip} tier=${4} /><//>
</div>
<div class="row">
<${Button} onClick=${() => setModal(true)}>打开模态框<//>
<${Button} variant="danger" onClick=${async () => toast((await confirmDialog({ title: '放弃模拟', text: '确定要离开吗?', danger: true, okText: '离开' })) ? '已确认' : '已取消')}>确认对话框<//>
<${Button} variant="ghost" onClick=${() => alertDialog({ title: '提示', text: '同盟人数达标,准许进入模拟。' })}>提示对话框<//>
</div>
<div class="row">
<${Button} size="sm" onClick=${() => toast('已复制同盟密钥 ABCD', 'success')}>成功<//>
<${Button} size="sm" onClick=${() => toast('新的回合即将开始')}>信息<//>
<${Button} size="sm" onClick=${() => toast('尚未连接到服务器,请稍候', 'warn')}>警告<//>
<${Button} size="sm" onClick=${() => toast('资金不足', 'error')}>错误<//>
</div>
</div>
</div>
<${Modal} open=${modal} title="装备替换" micro="EQUIPMENT" onClose=${() => setModal(false)}
actions=${html`<${Button} onClick=${() => setModal(false)}>取消<//><${Button} variant="primary" icon="check" onClick=${() => setModal(false)}>替换<//>`}>
<p class="modal__text">该干员已装备 2 件装备。继续配发将<b class="t-orange">销毁</b>较早的装备,是否继续?</p>
<//>
<//>
<${Sec} n="12" title="图标" micro="ICONS">
<div class="row">${Object.keys(ICONS).map((n) => html`<${Tooltip} key=${n} text=${n}><span style="font-size:.3rem;color:var(--text-md)"><${Icon} name=${n} /></span><//>`)}</div>
<//>
<${Sec} n="13" title="表情" micro="EMOTES · 6 THEMES × 6 · PICTURE ONLY">
<${EmoteDemo} />
<//>
</div>
<${ToastHost} />
<${UiHosts} />`;
}
render(html`<${Kit} />`, document.getElementById('kit'));
</script>
</body>
</html>