卫戍协议:盟约 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:
commit
01a44cd288
408 files changed
+308986
No files matched your search
@@ -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>
|
||||
@@ -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));
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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; });
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user