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:
1 parent
24205581e1
commit
4b57f8120c
23 files changed
+1227
-126
No files matched your search
+15
-5
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user