feedback5 WG: 补位 in the browser — the 干员持有 tab next to 干员调配 (the 53 NORMAL chess by tier, operator → stand-in, a switch each, 全部持有, export / import, localStorage + room.ownership through the shared sync engine), 「替补:X」 on shop / reward cards and own prep pieces, the stand-in's model on the board, its body on the detail card (own pieces, cards, any unit with standInFor), placement and ranges by the stand-in

This commit is contained in:
sganggs committed 2026-10-05 19:34:52 +08:00
1 parent 24205581e1
commit 4b57f8120c
23 files changed
+1227 -126

No files matched your search

+15 -5
View File
@@ -275,6 +275,7 @@ export async function createFieldView(host, options = {}) {
let penHidden = true; // the pen's figures are shown only by the pen camera (setPenHidden)
let penList = null;
let ownPen = null; // the own m.private.nextEnemies (fallback composition of a scouted teammate's pen)
let standInList = []; // the own m.private.standIns (0.2.0 补位): board pieces of these chess draw the stand-in
let camBeforePen = null; // { kind, opts } the camera the pen returns to
let leader = null; // { key, view, stand, area } the round leader standing on the boss field in the prep (setLeader)
let leaderHidden = true; // shown only by the boss-field prep camera (leaderShown)
@@ -285,7 +286,9 @@ export async function createFieldView(host, options = {}) {
cam: () => cam, heightAt,
animRate: () => (mode === 'battle' ? interp.rate : 1),
timeScale: () => (mode === 'battle' ? interp.rate : 1),
lookupDef: (info) => (info.side === 'enemy' ? data.enemy(info.defId) : data.chess(info.defId) || data.token(info.defId)),
// (a chess fighting as its 补位 stand-in — `standInFor` — reads the stand-in's record: its attack interval)
lookupDef: (info) => (info.side === 'enemy' ? data.enemy(info.defId)
: (info.standInFor && data.standIn(info.defId)) || data.chess(info.defId) || data.token(info.defId)),
crowded: () => views.size > 90,
renderer: app.renderer,
frameNo: () => frameNo,
@@ -301,7 +304,7 @@ export async function createFieldView(host, options = {}) {
timeScale: () => ctx.timeScale(),
loadLevel: () => loadLevel,
fieldRect: () => (mode === 'battle' && battleMeta ? battleMeta.rect : null),
subProfOf: (defId) => data.chess(defId)?.subProfessionId || null,
subProfOf: (defId, info = null) => ((info && info.standInFor && data.standIn(defId)) || data.chess(defId))?.subProfessionId || null,
view: (id) => views.get(id) || null,
screenSize: size,
fieldTop: () => {
@@ -626,11 +629,16 @@ export async function createFieldView(host, options = {}) {
const rec = data.token(piece.id);
return { kind: 'token', side: 'ally', defId: piece.id, spine: rec?.assets?.spine || piece.id, avatar: rec?.assets?.avatar || piece.id, tier: piece.tier || 1, golden: false, dir };
}
const rec = data.chess(piece.id);
const chess = data.chess(piece.id);
// 0.2.0 补位: a board piece of a chess the player does not own (m.private.standIns) is deployed as its stand-in —
// the stand-in's model; on the bench it keeps the chess's own (the hand shows the original operator)
const si = area === 'board' && chess && standInList.includes(chess.baseId || chess.chessId) ? data.standIn(piece.id) : null;
const rec = si || chess;
return {
kind: 'op', side: 'ally', defId: piece.id,
spine: rec?.assets?.spine || rec?.charId || null, avatar: rec?.assets?.avatar || rec?.charId || null,
tier: rec?.tier || piece.tier || 1, golden: !!(piece.golden || rec?.isGolden), dir,
tier: chess?.tier || piece.tier || 1, golden: !!(piece.golden || chess?.isGolden), dir,
...(si ? { standInFor: si.standInFor } : null),
};
}
@@ -678,6 +686,7 @@ export async function createFieldView(host, options = {}) {
});
};
const src = ps && typeof ps === 'object' ? ps : {};
standInList = Array.isArray(src.standIns) ? src.standIns.filter((x) => typeof x === 'string') : [];
ownPen = Array.isArray(src.nextEnemies) ? src.nextEnemies : null;
setPenList(ownPen);
addList(src.hand, 'hand');
@@ -705,7 +714,8 @@ export async function createFieldView(host, options = {}) {
const key = 'p:' + e.uid;
e.key = key;
const info = pieceInfo(e.piece, e.area);
const sig = `${info.kind}|${info.defId}|${info.golden ? 1 : 0}`;
// (the model is part of it: a 补位 chess changes body between the bench and the board — pieceInfo)
const sig = `${info.kind}|${info.defId}|${info.golden ? 1 : 0}|${info.spine || ''}`;
let v = views.get(key);
if (v && v._sig !== sig) { dropView(key); v = null; }
const w = slotWorld(e);
+17
View File
@@ -1,6 +1,7 @@
// public/js/render/app/host.js — asset lookup, data lookup and GL teardown for one field view.
import { createAssets, assets as defaultAssets } from '../../assets.js';
import { standInRecord } from '../../../../shared/standIn.js';
function withTimeout(p, ms) {
return Promise.race([p, new Promise((resolve) => setTimeout(resolve, ms))]);
@@ -24,9 +25,25 @@ function makeData(src) {
} catch { /* ignore */ }
return null;
};
// 0.2.0 补位: the composed stand-in record of a chess (shared/standIn.js over data/backups.json), cached per chess
const standIns = new Map();
const backups = () => {
try { return (src && typeof src.get === 'function' ? src.get('backups') : src && src.backups) || null; } catch { return null; }
};
const standIn = (id) => {
const c = look('chess', id);
const b = backups();
if (!c || !b) return null;
if (standIns.has(c)) return standIns.get(c);
let rec;
try { rec = standInRecord(c, b); } catch { rec = null; }
standIns.set(c, rec);
return rec;
};
return {
chess: (id) => look('chess', id), token: (id) => look('tokens', id), item: (id) => look('items', id),
enemy: (id) => look('enemies', id), stage: (id) => look('stages', id), bond: (id) => look('bonds', id),
standIn,
};
}
+3
View File
@@ -42,5 +42,8 @@ export function renderInfo(u) {
// unit (resolveDetail `unitItems` → the read-only 装备 section and the 变形同构体 pairing chips); the owner's own
// unit takes its items from the piece instead, so only other players' boards ever read this field
items: Array.isArray(u.items) ? u.items.filter((x) => typeof x === 'string') : undefined,
// 0.2.0 补位: the replaced operator's charId of a chess fighting as its stand-in (UnitInfo.standInFor; spine / avatar /
// name are already the stand-in's) — the detail card and the view's data lookups (attack interval, splash FX) follow it
standInFor: typeof u.standInFor === 'string' && u.standInFor ? u.standInFor : undefined,
};
}
+1 -1
View File
@@ -59,7 +59,7 @@ export class FxNumbers {
this.particle('glow', px, py, { tint, life: 0.18, s0: (s / 128) * (big ? 1.0 : 0.6), s1: (s / 128) * (big ? 1.5 : 0.9), a0: 0.9, a1: 0 });
if (melee) { this._slashAt = null; this._slash(view, srcView, px, py, s, style, big); }
this.burst(px, py, s, big ? 8 : melee ? 6 : 4, tint, { speed: melee ? 2.8 : 2.4, size: big ? 0.6 : 0.46 });
if (srcView && this.ctx.subProfOf && SPLASH_SUBS.has(this.ctx.subProfOf(srcView.info?.defId))) {
if (srcView && this.ctx.subProfOf && SPLASH_SUBS.has(this.ctx.subProfOf(srcView.info?.defId, srcView.info))) {
if (!this._lastRing || this.time - this._lastRing > 0.08) {
this._lastRing = this.time;
this.ring(view.x, view.y, view.z || 0, 0.1, 1.1, tint, 0.3);