feedback7 UIBACK: a stage device explains itself on a tap — 阻隔工事 / “双眼皮” / 射击台 / 源石流发生装置 get the terrain tip's card (GitHub #228, idea and first version in PR #229 by @rickylxw, ported onto the 0.2.1 modules with t() and the four packs): numbers from the stage's own device entry, the 「场地装置」 chip, the turret's stat grid, the tile fact; in a battle the crate and the turret are found by their device UNIT (the PR matched the stage pos: a turret outside the rect stands elsewhere, the units are not in the field meta of a battle shown from its start, a destroyed crate left the snapshots and still answered), the card carries the live HP and closes with the device; the dev mock boards ?stage= and streams crate units on ?variant=devices; golden: unchanged (client only)
This commit is contained in:
1 parent
ae22335cfe
commit
df540b6210
13 files changed
+802
-18
No files matched your search
@@ -711,9 +711,36 @@ function TerrainDetail({ terrain }) {
|
||||
${Array.isArray(terrain.facts) && terrain.facts.length ? html`<${Section} title=${t('这一格')}><p class="dtext">${terrain.facts.join(' · ')}</p><//>` : null}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A stage device's tip (GitHub #228, PR #229: 阻隔工事 / “双眼皮” / 射击台 / 源石流发生装置 — the terrain tip's sibling). Opened
|
||||
* by a tap on the device itself: the game screen resolves it with `gameLogic.deviceInfo` (prep) or `deviceTipAt` (a battle)
|
||||
* from the stage the board on screen is built from, so the lines carry that stage's own numbers. In a battle the
|
||||
* crate / turret is a device unit: its live HP (`live` from the battle's own sim, else `snapHp` — the screen reads the latest
|
||||
* snapshot tuple through `unitId`) draws a HP bar like a unit card's.
|
||||
* @param {{ name:string, tag:string, lines:string[], facts?:string[], stats?:{k:string,v:any}[] }} device
|
||||
* @param {{ hp:number, max:number }|null} snapHp
|
||||
*/
|
||||
function DeviceDetail({ device, snapHp = null, live = null }) {
|
||||
const hp = hpOf(live, snapHp);
|
||||
return html`
|
||||
<div class="dhead">
|
||||
<div class="dhead__icon"><${Icon} name="info" /></div>
|
||||
<div class="dhead__info">
|
||||
<div class="dhead__chips"><span class="dtag-kind">${device.tag}</span></div>
|
||||
<h3 class="dhead__name">${device.name}</h3>
|
||||
${hp ? html`<div class="dhp"><i style=${`width:${Math.max(0, Math.min(100, (hp.hp / Math.max(1, hp.max)) * 100))}%`}></i><span class="num">${fmtNum(hp.hp)} / ${fmtNum(hp.max)}</span></div>` : null}
|
||||
</div>
|
||||
</div>
|
||||
<${Section} title=${t('装置机制')} micro="DEVICE">
|
||||
${device.lines.map((line, i) => html`<p class="dtext" key=${i}>${line}</p>`)}
|
||||
<//>
|
||||
${Array.isArray(device.stats) && device.stats.length ? html`<div class="dstats">${device.stats.map((x) => html`<${Stat} key=${x.k} k=${x.k} v=${x.v} />`)}</div>` : null}
|
||||
${Array.isArray(device.facts) && device.facts.length ? html`<${Section} title=${t('这一格')}><p class="dtext">${device.facts.join(' · ')}</p><//>` : null}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve what a detail target shows.
|
||||
* @param {{ kind:'piece'|'chess'|'item'|'enemy'|'unit'|'token'|'terrain', id?:string, uid?:number, unit?:any, count?:number }} target
|
||||
* @param {{ kind:'piece'|'chess'|'item'|'enemy'|'unit'|'token'|'terrain'|'device', id?:string, uid?:number, unit?:any, count?:number }} target
|
||||
* @param {Map<number, any>} pieces indexPieces(priv)
|
||||
* @param {{ priv?: any, backups?: any }} [opts] 0.2.0 补位: the player's own pieces and cards of a chess in
|
||||
* m.private.standIns — a unit carrying `standInFor`, and a teammate's bond popup row that says it (`target.standInFor`)
|
||||
@@ -726,6 +753,12 @@ export function resolveDetail(target, pieces, { priv = null, backups = data.get(
|
||||
if (!target) return null;
|
||||
// a special terrain tile (issue #184): the screen resolved the stage's own numbers already (gameLogic.terrainInfo)
|
||||
if (target.kind === 'terrain') return target.terrain && typeof target.terrain === 'object' ? { type: 'terrain', terrain: target.terrain } : null;
|
||||
// a stage device (#228): the screen resolved the device's stage entry (gameLogic.deviceInfo / deviceTipAt); in a battle
|
||||
// `unitId` is the crate / turret unit whose snapshot tuple gives the live HP
|
||||
if (target.kind === 'device') {
|
||||
const device = target.device && typeof target.device === 'object' ? target.device : null;
|
||||
return device ? { type: 'device', device, ...(Number.isInteger(device.unitId) ? { unitId: device.unitId } : {}) } : null;
|
||||
}
|
||||
const ownSi = (c) => ownStandIn(c, priv, backups);
|
||||
const dd = { chess: data.get('chess'), backups };
|
||||
/** the own card of chess `c`: its 自选 record and pick when the player filled that DIY slot */
|
||||
@@ -839,6 +872,7 @@ export function DetailPanel({ detail, editable, snapHp, onClose, onSell, onDestr
|
||||
${detail.type === 'enemy' ? html`<${EnemyDetail} enemy=${detail.enemy} snapHp=${snapHp} count=${detail.count} live=${liveNow} />` : null}
|
||||
${detail.type === 'token' ? html`<${TokenDetail} token=${detail.token} piece=${detail.piece} ownerId=${detail.ownerId ?? null} snapHp=${snapHp} live=${liveNow} />` : null}
|
||||
${detail.type === 'terrain' ? html`<${TerrainDetail} terrain=${detail.terrain} />` : null}
|
||||
${detail.type === 'device' ? html`<${DeviceDetail} device=${detail.device} snapHp=${snapHp} live=${liveNow} />` : null}
|
||||
</div>
|
||||
</aside>`;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export { ownFieldId, homeFieldId, cycleField, watchTarget, switcherLabel, fieldL
|
||||
export { sortBonds, bondTier, nextThreshold, grantedBonds, pieceBondIds, morphPairings, HARMONY_BOND, harmonyMembers, bondMembers, memberHeadCount, bannedPerBond, disabledBondSets, briefingBondTip, modeOffBonds, bandOffBonds, bandOffLine } from './gameLogic/bonds.js';
|
||||
export { priceTone, mergeProgress, mergeTarget, handFull, completesMerge, offerHeader, shopBlockReason, readyFundsPrompt } from './gameLogic/shop.js';
|
||||
export { deploySets, stageOverrides, deployMap, effectiveStage, indexPieces, placementContext, piecePosition, tileAllows, summonRange, summonExcluded, canPlace, equipReplaces, equipMerges, boardTargets, dropIntent, dropFailureReason } from './gameLogic/placement.js';
|
||||
export { terrainInfo } from './gameLogic/terrain.js';
|
||||
export { terrainInfo, deviceInfo, deviceTipAt, noteDeviceUnits } from './gameLogic/terrain.js';
|
||||
export { normalizeDraft, normalizeSp } from './gameLogic/draft.js';
|
||||
export { groupEnemies, PEN, penZoneTiles, penPlacement, previewEnemyKey, factionTypes } from './gameLogic/enemies.js';
|
||||
export { snapHud, bossFrac, bossPctText, hasFlag, UF, attackInterval, fmtNum, rangeGridBox } from './gameLogic/format.js';
|
||||
|
||||
@@ -169,9 +169,10 @@ export const facingSwallows = (e, keys = DEFAULT_HOTKEYS) => e?.key === ' ' || a
|
||||
* right-click or long press — or a battle / teammate unit). Shop, reward, bond-member and intel (enemy) cards stay.
|
||||
* @param {{ kind?: string }|null|undefined} detail
|
||||
*/
|
||||
// a card opened BY a field press (a piece, a unit, a special terrain tile: issue #184) closes on the next press of
|
||||
// the field; the ones opened from the shop / hand / HUD stay until their own close button (or the flow that opened them)
|
||||
export const closesOnFieldPress = (detail) => detail?.kind === 'piece' || detail?.kind === 'unit' || detail?.kind === 'terrain';
|
||||
// a card opened BY a field press (a piece, a unit, a special terrain tile: issue #184, a stage device: #228) closes on the
|
||||
// next press of the field; the ones opened from the shop / hand / HUD stay until their own close button (or the flow that
|
||||
// opened them)
|
||||
export const closesOnFieldPress = (detail) => detail?.kind === 'piece' || detail?.kind === 'unit' || detail?.kind === 'terrain' || detail?.kind === 'device';
|
||||
|
||||
/**
|
||||
* Whether an open overlay swallows a game shortcut: a modal / the guide own the keyboard (Esc included — they close
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// ui/gameLogic/terrain.js — special terrain tips (GitHub issue #184: 特殊地形的单击信息提示). Re-exported from
|
||||
// ../gameLogic.js.
|
||||
// ui/gameLogic/terrain.js — the tips of a tap on the ground: special terrain tiles (GitHub issue #184: 特殊地形的单击信息
|
||||
// 提示) and, in the same place, stage devices (阻隔工事 / “双眼皮” / 射击台 / 气流: GitHub #228, PR #229 by @rickylxw —
|
||||
// deviceInfo / deviceTipAt below). Re-exported from ../gameLogic.js.
|
||||
//
|
||||
// The words go through t() (docs/I18N.md: the Chinese text is the msgid, public/i18n/en.json the English): the name /
|
||||
// tag / fact tables are marked N_() and translated where terrainInfo hands them out, the mechanism lines are built
|
||||
// with their numbers as params.
|
||||
// with their numbers as params. A device's NAME is a data text (data/stages.json `devices[].name`, localized by the
|
||||
// data overlay: "Barricade", "'Double Lids'" …), not a msgid.
|
||||
|
||||
import { isObj } from './shared.js';
|
||||
import { t, N_ } from '../../../../shared/i18n.js';
|
||||
@@ -123,3 +125,174 @@ export function terrainInfo(stage, row, col) {
|
||||
else if (tile.groundPassable === true) facts.push(t('地面单位可通过'));
|
||||
return { key, name: t(tip.name), tag: t(tip.tag), row, col, lines: tip.lines(stage.special).filter((s) => typeof s === 'string' && s), facts };
|
||||
}
|
||||
|
||||
// ---- stage device tips (GitHub #228, PR #229 by @rickylxw: the stage DEVICES, not only the tiles) ---------------------
|
||||
|
||||
/** The device roles a tap explains; the rest is invisible (盟约寒风, the 沼泽 / 涨潮 controllers) or not fielded this season. */
|
||||
const DEVICE_TIP_ROLES = Object.freeze(['crate', 'turret', 'platform', 'blower']);
|
||||
/** Roles the battle spawns as device UNITS (Battle._spawnStageDevices, devices.js spawnTurret): in a battle a tap finds them by unit. */
|
||||
const DEVICE_UNIT_ROLES = Object.freeze(['crate', 'turret']);
|
||||
const TIP_TAG = N_('场地装置');
|
||||
const DIR_NAME = Object.freeze({ UP: N_('上'), DOWN: N_('下'), LEFT: N_('左'), RIGHT: N_('右') });
|
||||
|
||||
const round1 = (v) => Math.round(v * 10) / 10;
|
||||
|
||||
/**
|
||||
* What tapping a stage device says — the same contract as TERRAIN_TIPS, but `lines` read the device's own stage entry
|
||||
* (data/stages.json `stage.devices[]`: stats / skill blackboard / dir / rangeTiles), the very numbers the sim runs
|
||||
* (`sim/content/devices.js`, `Battle._spawnStageDevices`), so a tip can never disagree with the battle. The prose is ours
|
||||
* (docs/PLAYING.md wording; PRTS 阻隔工事 / “双眼皮” / 射击台 / 源石流发生装置 and the sim's behaviour). `stats` is the
|
||||
* stat grid under the lines (the turret's), `facts` what the tile allows.
|
||||
*/
|
||||
const DEVICE_TIPS = Object.freeze({
|
||||
crate: {
|
||||
lines: (st, d) => [
|
||||
t('挡在地面上的工事:地面敌人会绕开它走;只有无路可走时才会撞上来,把它摧毁后继续前进'),
|
||||
t('生命 {hp} 点,被摧毁后从场上消失', { hp: param(d.stats?.maxHp, 100) }),
|
||||
],
|
||||
facts: () => [t(BUILDABILITY.NONE)], // board.js buildDeployMap: an active crate takes its tile off the deploy map
|
||||
},
|
||||
turret: {
|
||||
lines: (st, d) => {
|
||||
const bb = isObj(d.skill?.bb) ? d.skill.bb : {};
|
||||
// devices.js spawnTurret: the same blackboard keys and the same `持续N秒` read from the skill text
|
||||
const dur = /持续(\d+(?:\.\d+)?)秒/.exec(String(d.skill?.desc ?? ''));
|
||||
const aspdMax = param(bb.max_attack_speed, Infinity);
|
||||
const fragMax = param(bb.max_damage_scale, Infinity);
|
||||
return [
|
||||
t('自动攻击射程内的一名敌人,造成法术伤害,命中的敌人附加脆弱'),
|
||||
t('按当前层数最高的盟约计算:每 1 层,攻击速度 +{aspd}(至多 +{aspdMax}),附加的脆弱 +{frag}%(至多 +{fragMax}%)', {
|
||||
aspd: param(bb.attack_speed_per_stack, 1),
|
||||
aspdMax: Number.isFinite(aspdMax) ? aspdMax : '∞',
|
||||
frag: round1(param(bb.damage_scale_per_stack, 0.001) * 100),
|
||||
fragMax: Number.isFinite(fragMax) ? round1((fragMax - 1) * 100) : '∞',
|
||||
}),
|
||||
dur ? t('脆弱持续 {sec} 秒', { sec: Number(dur[1]) }) : null,
|
||||
];
|
||||
},
|
||||
stats: (st, d) => {
|
||||
const s = isObj(d.stats) ? d.stats : {};
|
||||
const out = [{ k: t('生命上限'), v: param(s.maxHp, 100) }];
|
||||
if (param(s.atk, 0)) out.push({ k: t('攻击'), v: param(s.atk, 0) });
|
||||
if (param(s.def, 0)) out.push({ k: t('防御'), v: param(s.def, 0) });
|
||||
const sec = param(s.bat, 0) > 0 ? param(s.bat, 0) * 100 / (param(s.aspd, 100) > 0 ? param(s.aspd, 100) : 100) : 0;
|
||||
if (sec) out.push({ k: t('攻击间隔'), v: `${round1(sec)}s` });
|
||||
return out;
|
||||
},
|
||||
facts: () => [t(BUILDABILITY.NONE)],
|
||||
},
|
||||
platform: {
|
||||
lines: () => [
|
||||
t('高台位装置:地面敌人不能走上这一格'),
|
||||
t('远程位干员可以部署在其上;站上去的干员在高位,不阻挡敌人'),
|
||||
],
|
||||
facts: () => [t(BUILDABILITY.RANGED)], // board.js: an active platform makes its tile a 'ranged' deploy tile
|
||||
},
|
||||
blower: {
|
||||
lines: (st, d) => {
|
||||
// devices.js buildTerrain: the device's own skill blackboard, else stage.special.blower.bb; rangeTiles = its own tile + the lane
|
||||
const bb = isObj(d.skill?.bb) ? d.skill.bb : (isObj(st?.blower?.bb) ? st.blower.bb : {});
|
||||
const dir = DIR_NAME[String(d.dir || 'UP').toUpperCase()] || DIR_NAME.UP;
|
||||
const lane = Array.isArray(d.rangeTiles) && d.rangeTiles.length > 1 ? d.rangeTiles.length - 1 : 3;
|
||||
const out = [t('向前方 {n} 格吹出气流(这一台朝{dir})', { n: lane, dir: t(dir) })];
|
||||
const eq = param(bb['blower_s_character[equal].atk'], 0);
|
||||
const op = param(bb['blower_s_character[opposite].atk'], 0);
|
||||
const mods = [];
|
||||
if (eq) mods.push(t('朝向与风向相同的攻击力 {v}', { v: pctText(eq) }));
|
||||
if (op) mods.push(t('相反的 {v}', { v: pctText(op) }));
|
||||
if (mods.length) out.push(t('部署在气流里的干员:{mods}', { mods }));
|
||||
const eqM = param(bb['blower_s_enemy[equal].move_speed'], 0);
|
||||
const opM = param(bb['blower_s_enemy[opposite].move_speed'], 0);
|
||||
const em = [];
|
||||
if (eqM) em.push(t('顺风移动速度 ×{v}', { v: round2(1 + eqM) }));
|
||||
if (opM) em.push(t('逆风 ×{v}', { v: round2(1 + opM) }));
|
||||
if (em.length) out.push(t('在气流里移动的敌人:{mods}', { mods: em }));
|
||||
return out;
|
||||
},
|
||||
facts: () => [t(BUILDABILITY.NONE)],
|
||||
},
|
||||
});
|
||||
const round2 = (v) => Math.round(v * 100) / 100;
|
||||
|
||||
/** A stage device entry is drawn: `active` when it carries it, else not `hidden` (render/tiles.js _stageDevices, board3d/layout.js stageDevices). */
|
||||
const deviceDrawn = (d) => (typeof d.active === 'boolean' ? d.active : !d.hidden);
|
||||
|
||||
/** The card of stage device `d` standing at (row, col) — `tip` is DEVICE_TIPS[d.role]. */
|
||||
function deviceCard(stage, d, row, col) {
|
||||
const tip = DEVICE_TIPS[d.role];
|
||||
const lines = tip.lines(stage.special, d).filter((s) => typeof s === 'string' && s);
|
||||
const stats = typeof tip.stats === 'function' ? tip.stats(stage.special, d) : null;
|
||||
const name = typeof d.name === 'string' && d.name ? d.name : String(d.role);
|
||||
return {
|
||||
key: d.role, name, tag: t(TIP_TAG), row, col, lines, facts: tip.facts(stage.special, d).filter(Boolean),
|
||||
...(stats && stats.length ? { stats } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The tip a tap on the tile (row, col) opens when a stage DEVICE stands on it (GitHub #228, PR #229), or null: an ordinary
|
||||
* tile — or a device this client does not draw (`active` false: a turret that stays dormant until 机械援助 / a 机变 card
|
||||
* switches it on, a crate such a card removed) — falls through to `terrainInfo`. Visibility mirrors what the renderers draw
|
||||
* (`render/tiles.js _stageDevices`, `board3d/layout.js stageDevices`): `active` when set, else not `hidden`. The stage is
|
||||
* the shown field's (already the player's own `effectiveStage`, so a card-removed crate is not in the way). The device's
|
||||
* name is its data text (localized by the data overlay); the lines are in the current language (t()).
|
||||
* @param {{ devices?: any[], special?: any, rows?: string[], tiles?: Record<string, any> } | null | undefined} stage
|
||||
* @param {number} row board row (row 0 = the bottom row, DESIGN §1)
|
||||
* @param {number} col
|
||||
* @param {{ roles?: readonly string[] }} [opts] `roles`: only these roles answer (default: the four of DEVICE_TIP_ROLES)
|
||||
* @returns {{ key:string, name:string, tag:string, row:number, col:number, lines:string[], facts:string[], stats?:{k:string,v:string|number}[] } | null}
|
||||
*/
|
||||
export function deviceInfo(stage, row, col, { roles = DEVICE_TIP_ROLES } = {}) {
|
||||
const list = Array.isArray(stage?.devices) ? stage.devices : null;
|
||||
if (!list || !Number.isInteger(row) || !Number.isInteger(col)) return null;
|
||||
for (const d of list) {
|
||||
if (!isObj(d) || !Array.isArray(d.pos) || d.pos[0] !== row || d.pos[1] !== col) continue;
|
||||
if (!roles.includes(d.role) || !DEVICE_TIPS[d.role] || !deviceDrawn(d)) continue;
|
||||
return deviceCard(stage, d, row, col);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tip of a tap on (row, col) while a BATTLE shows the field — the live counterpart of `deviceInfo`: crates and “双眼皮”
|
||||
* turrets are device UNITS there (kind 'device', one per drawn stage device, `defId` = the device key), so they are
|
||||
* found by the unit standing on the tile — a turret outside the battle's rect stands on another tile than its stage `pos`
|
||||
* (devices.js deviceTile) — and what the card says comes from the stage entry of that key. The platforms and blowers are no
|
||||
* units: they answer by their stage `pos`, as in the prep.
|
||||
* A destroyed device (HP gone, or no longer in the snapshots once its DIE animation is over) is not there any more: the
|
||||
* tap belongs to the tile under the wreck (null here, so it falls through to `terrainInfo`). Before the first snapshot of the
|
||||
* battle has arrived (`snap` empty) a unit counts as standing.
|
||||
* The result is the same card as `deviceInfo`'s, with `unitId` (the unit, for its live HP: the screen reads `b.snap`
|
||||
* through it, like a unit card's `snapHp`).
|
||||
* @param {object|null|undefined} stage the shown field's stage
|
||||
* @param {number} row @param {number} col
|
||||
* @param {{ units?: any[]|null, snap?: Map<number, any[]>|null }} battle the field's unit infos (m.field.units) and the
|
||||
* latest snapshot tuples by unit id ([id, x, y, hp, maxHp, …])
|
||||
*/
|
||||
export function deviceTipAt(stage, row, col, { units = null, snap = null } = {}) {
|
||||
if (!Number.isInteger(row) || !Number.isInteger(col)) return null;
|
||||
const u = (Array.isArray(units) ? units : []).find((x) => isObj(x) && x.kind === 'device' && Math.round(Number(x.x)) === col && Math.round(Number(x.y)) === row);
|
||||
if (!u) return deviceInfo(stage, row, col, { roles: DEVICE_TIP_ROLES.filter((r) => !DEVICE_UNIT_ROLES.includes(r)) });
|
||||
if (snap && snap.size > 0) {
|
||||
const tuple = snap.get(u.id);
|
||||
if (!Array.isArray(tuple) || !(tuple[3] > 0)) return null;
|
||||
}
|
||||
const list = Array.isArray(stage?.devices) ? stage.devices : [];
|
||||
const same = list.filter((d) => isObj(d) && d.key === u.defId && DEVICE_UNIT_ROLES.includes(d.role));
|
||||
const d = same.find((x) => Array.isArray(x.pos) && x.pos[0] === row && x.pos[1] === col) || same[0];
|
||||
return d ? { ...deviceCard(stage, d, row, col), unitId: u.id } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold the device units a battle shows into `map` (UnitInfo by unit id): the field meta's `units` (what stands when the field
|
||||
* is entered) and the 'spawn' event tuples `['spawn', UnitInfo]` after it — the stage crates and the “双眼皮” turrets come
|
||||
* into being with the battle's first step (`battleStart`), after the meta of a battle shown from its beginning was taken.
|
||||
* Only kind 'device' is kept; `deviceTipAt` reads the map's values.
|
||||
* @param {Map<number, any>} map
|
||||
* @param {{ units?: any[]|null, events?: any[]|null }} [from]
|
||||
*/
|
||||
export function noteDeviceUnits(map, { units = null, events = null } = {}) {
|
||||
for (const u of Array.isArray(units) ? units : []) if (isObj(u) && u.kind === 'device' && u.id != null) map.set(u.id, u);
|
||||
for (const e of Array.isArray(events) ? events : []) if (Array.isArray(e) && e[0] === 'spawn' && isObj(e[1]) && e[1].kind === 'device' && e[1].id != null) map.set(e[1].id, e[1]);
|
||||
return map;
|
||||
}
|
||||
Reference in new issue
Block a user