feedback7: merge fb7-ui-backlog — #229 device tooltips, #238 settings button in lobby / room, #138 准备 folds the shop, #120 in-match spectator roster (cap stays 2), PR #308 capsule counts (+ the shared-field progress fix), PR #333 empty new shop slot on 调度中心 upgrade
- Conflicts: the stats entry (merged fb7-stats) and the settings button both sit in the lobby / room top bars — both kept;
public/js/ui/settings.js (#321 diagnostics, voiceLang, SettingsButton); docs/PLAYING.md table rows resolved line by
line against the merge base (#368, #354, #333 edits kept); docs/SIM.md / design/network.md snapshot field lists
(resolved + ammo / wolves / neg / stand / standCut); design/client.md Screens line (#368's AI switch kept).
- Grok review: MERGE AFTER FIXES — the shared-field capsule finding fixed on the branch (1a880699).
- Golden: roster 23, bonds 2, fields 11, matches 18, diy 11 — #308 totals / snapshots and #333's shop RNG (union OK);
broad node --test 5473 (5472 pass, 0 fail, 1 skipped); lint 0 errors; typecheck; i18n strict clean.
This commit is contained in:
commit
dc609ea13f
71 files changed
+3016
-806
No files matched your search
+40
-7
@@ -23,6 +23,9 @@ 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));
|
||||
// the match's stage (data/stages.json id): ?stage=act1autochess_m01 boards a stage whose own devices (crates / turrets)
|
||||
// stand on the prep field — the default act2autochess_m01 keeps its blowers and turrets off it (rows 6 / 8 / 13)
|
||||
const STAGE_ID = params.get('stage') || 'act2autochess_m01';
|
||||
|
||||
// ---- deterministic rng ----------------------------------------------------------------------------------
|
||||
let seed = 20260927;
|
||||
@@ -119,7 +122,7 @@ function pushPublic() {
|
||||
function buildState() {
|
||||
uidSeq = 100;
|
||||
seed = 20260927;
|
||||
const stage = data.lookup('stages', 'act2autochess_m01');
|
||||
const stage = data.lookup('stages', STAGE_ID) || data.lookup('stages', 'act2autochess_m01');
|
||||
const melee = stage.deployTiles.normal.melee.slice();
|
||||
const ranged = stage.deployTiles.normal.rangedOnly.slice();
|
||||
const bonds = data.list('bonds');
|
||||
@@ -206,7 +209,7 @@ function buildState() {
|
||||
|
||||
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',
|
||||
stageId: STAGE_ID, 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;
|
||||
@@ -472,6 +475,13 @@ function startCombat(phase) {
|
||||
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 });
|
||||
}
|
||||
// ?variant=devices — the stage's crates inside the rect are device UNITS (kind 'device', defId = the device key), as in the
|
||||
// real sim: absent from the field meta (a battle shown from its start), they come in with 'spawn' events once it steps;
|
||||
// __MOCK__.breakDevice(i) destroys one (hp 0 for a second in the snapshots, then gone)
|
||||
const deviceUnits = !VARIANTS.has('devices') ? [] : (S.stage.devices || [])
|
||||
.filter((d) => d.role === 'crate' && (typeof d.active === 'boolean' ? d.active : !d.hidden)
|
||||
&& d.pos[0] >= rect.r0 && d.pos[0] <= rect.r1 && d.pos[1] >= rect.c0 && d.pos[1] <= rect.c1)
|
||||
.map((d) => ({ id: id++, kind: 'device', side: 'ally', ownerId: null, defId: d.key, name: d.name, x: d.pos[1], y: d.pos[0], facing: 1, maxHp: 100, hp: 100, deadAt: null }));
|
||||
// every field — 联防 too — is fought on the round's battlefield (server unite.js; 0.2.0's escaped-level map withdrawn)
|
||||
const field = { fieldId, kind, rect, stageId: pub.stageId, units: units.map((u) => ({ ...u })) };
|
||||
store.patch('match', { field });
|
||||
@@ -489,6 +499,7 @@ function startCombat(phase) {
|
||||
if (pausedSince != null) { pausedFor += performance.now() - pausedSince; pausedSince = null; }
|
||||
const t = (performance.now() - t0 - pausedFor) / 1000 + 4;
|
||||
const ev = [];
|
||||
for (const d of deviceUnits) if (!spawned.has(d.id)) { spawned.add(d.id); ev.push(['spawn', { ...d }]); }
|
||||
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 }]); }
|
||||
@@ -508,6 +519,7 @@ function startCombat(phase) {
|
||||
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]),
|
||||
...deviceUnits.filter((d) => d.deadAt == null || t - d.deadAt < 1).map((d) => [d.id, d.x, d.y, Math.max(0, d.hp), d.maxHp, 0, 0, 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
|
||||
@@ -517,7 +529,13 @@ function startCombat(phase) {
|
||||
net._emit('b.snap', snap);
|
||||
};
|
||||
const e2boss = (e) => !!e.boss;
|
||||
S.battle = { timer: setInterval(tick, 50), fieldRef };
|
||||
S.battle = { timer: setInterval(tick, 50), fieldRef, devices: deviceUnits, hurtDevice: (i = 0, dmg = 100) => {
|
||||
const d = deviceUnits[i];
|
||||
if (!d || d.deadAt != null) return false;
|
||||
d.hp = Math.max(0, d.hp - dmg);
|
||||
if (d.hp <= 0) { d.deadAt = (performance.now() - t0 - pausedFor) / 1000 + 4; net._emit('b.ev', { t: 'b.ev', fieldId: fieldRef.id, gt: d.deadAt, ev: [['die', d.id]] }); }
|
||||
return true;
|
||||
} };
|
||||
setTimeout(tick, 60);
|
||||
}
|
||||
|
||||
@@ -575,7 +593,7 @@ async function mockRequest(t, f = {}) {
|
||||
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;
|
||||
const n = p.shop.slots.filter((s) => !s || s.kind !== 'item').length;
|
||||
p.shop.slots = [...Array.from({ length: n }, () => makeSlot(p.shop.level)), makeItemSlot()];
|
||||
refreshPrivate(); return {};
|
||||
}
|
||||
@@ -586,7 +604,8 @@ async function mockRequest(t, f = {}) {
|
||||
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));
|
||||
// the upgrade opens the new slot EMPTY (server/match/player/economy.js _openLevelSlots: a null slot; a refresh fills it)
|
||||
while (p.shop.slots.filter((s) => !s || s.kind !== 'item').length < n) p.shop.slots.splice(p.shop.slots.length - 1, 0, null);
|
||||
toast(`调度中心等级提升至 ${p.shop.level}`, 'success');
|
||||
refreshPrivate(); return {};
|
||||
}
|
||||
@@ -706,6 +725,15 @@ async function mockRequest(t, f = {}) {
|
||||
return {};
|
||||
}
|
||||
case 'g.autoplay': case 'g.leave': case 'room.leave': return {};
|
||||
// the host frees a spectator seat (server/lobby.js removeSpectator; ?variant=spectators seats two, ?variant=notHost makes
|
||||
// another player the host)
|
||||
case 'room.removeSpectator': {
|
||||
const room = store.get().room;
|
||||
if (room?.hostId !== ME) fail('NOT_HOST');
|
||||
if (!(room.spectators || []).some((x) => x.playerId === f.playerId)) fail('BAD_TARGET');
|
||||
store.set({ room: { ...room, spectators: room.spectators.filter((x) => x.playerId !== f.playerId) } });
|
||||
return {};
|
||||
}
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
@@ -780,7 +808,11 @@ async function boot() {
|
||||
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 })) },
|
||||
room: {
|
||||
code: 'MOCK', hostId: VARIANTS.has('notHost') ? 'ai_2' : 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 })),
|
||||
...(VARIANTS.has('spectators') ? { spectators: [{ playerId: 'sp_1', name: '观战者甲', connected: true }, { playerId: 'sp_2', name: '观战者乙', connected: false }] } : {}),
|
||||
},
|
||||
});
|
||||
await data.loadAll(GAME_FILES);
|
||||
installDeviceSupport();
|
||||
@@ -790,6 +822,7 @@ async function boot() {
|
||||
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() };
|
||||
globalThis.__MOCK__ = { store, S: () => S, setPhase, mutate: (fn) => { fn(S); refreshPrivate(); }, pushPublic: () => pushPublic(),
|
||||
hurtDevice: (i, dmg) => S?.battle?.hurtDevice?.(i, dmg) ?? false };
|
||||
}
|
||||
boot().catch((err) => console.error('[mock] boot failed', err));
|
||||
Reference in new issue
Block a user