Files
ark-stronghold-protocol/public/js/ui/gameLogic/terrain.js
T
sganggs 7059528c47 feedback5: merge feedback4 (3af5b84) — PRs #186 / #134 / #185 / #24 / #183 ported to the split layout (terrainInfo / TERRAIN_TIPS into a new ui/gameLogic/terrain.js re-exported by the gameLogic.js barrel, closesOnFieldPress into ui/gameLogic/shortcuts.js, the rest in place; 42 strings through t(), #183's 设置 too); golden unchanged
Ported by hand:
- public/js/ui/gameLogic.js is a re-export shim on feedback5: #185's TERRAIN_TIPS / terrainInfo went into the new
  public/js/ui/gameLogic/terrain.js (the barrel gains one re-export line); closesOnFieldPress 'terrain' into
  ui/gameLogic/shortcuts.js.
- i18n (docs/I18N.md): every new user-visible string goes through t() with an English entry in public/i18n/en.json
  (42, after the 补位 strings): the terrain names / tags / fact table are N_() and translated where terrainInfo hands
  them out, the mechanism lines are msgids with their numbers as params (a line with an optional half is two whole
  msgids, the list joins use an array param), the card's 地形机制 / 这一格 titles in ui/detailPanel.js. Official EN
  terms where the EN client has them (stage_table tileInfo: Active Originium, Mire, Exhaust Grille, Deep Water Zone,
  Incursion Point, Protection Objective, Tunnel Entry / Exit, High Ground; Camouflage). The Chinese output is
  byte-identical to feedback4's terrainInfo on all 5187 tiles of the 13 stages and on synthetic stages.
- public/js/screens/game.js (MatchScreen still lives there): the import keeps feedback5's names plus terrainInfo /
  fieldTile; the terrain stage is the one view.setStage gets, so it includes feedback5's fieldStage — a 联防 field is
  played on its own map (GitHub #41), and a tap there explains that map's tile, not the round stage's. The tileClick
  handler and render/app.js emitTileClick / ui/fallbackField.js auto-merged in place. terrainInfo reads the stage only,
  so a 补位 / 自选 board explains its tiles the same way (their pieces only change a piece's art and name).
- public/js/ui/detailPanel.js: the terrain branch of resolveDetail before feedback5's stand-in / 自选 helpers.
- public/js/screens/title.js (#183): feedback5's LangToggle / t() imports plus GIcon / SettingsModal; the gear
  button's aria-label / title are t('设置') (en.json already has "Settings").
- tools/fetch-assets.mjs / tools/assets/downloader.mjs: --add-only (keepExisting, the font skip) beside #24's
  --asset-source / SP_ASSET_SOURCE and the shared MirrorPolicy; feedback5's extraOperators / moduleTypes and the
  voice plan unchanged.
- data/assets.json: feedback5's manifest plus #134's audio.sfx.battle.leak (after heal, the plan order); stats files
  6993 → 6994 and bytes +23 240 as measured on feedback4; the hash recomputed with the generator's contentHash
  (c01aa739a2d3 → 6a547fc5c937). A field-wise JSON diff against feedback5 shows only those four fields.
- docs: DESIGN §24 intro and §24.5 / §24.6 as on feedback4 (the rest of DESIGN.md is feedback5's); ASSETS.md,
  DEPLOY.md, PLAYING.md auto-merged.
- Auto-merged and checked: audio.js (the sim still emits ['leak', id], server/sim/battle/deploy.js), render/style.js,
  textures.js, tiles.js, board3d/materials.js, boardArt.js, tools/assets/*, tools/setup.mjs, extract.py, the tests.

Checks: the PRs' unit tests (audio 22, gameLogic 44, terrain-detail 1, originium-art 4, boardart 15,
board3d-extract 9, asset-network 21, fetch-assets-shrink 4), i18n 9 / i18n-data 8 / i18n-server 4, client-static 313,
docs-consistency 24, kits_layout 6 pass; lint 0 errors (no new warnings), check:imports, typecheck; golden: all 283
scenarios match, nothing regenerated.
2026-10-06 14:41:40 +08:00

126 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ui/gameLogic/terrain.js — special terrain tips (GitHub issue #184: 特殊地形的单击信息提示). 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.
import { isObj } from './shared.js';
import { t, N_ } from '../../../../shared/i18n.js';
// ---- special terrain tips (GitHub issue #184: 特殊地形的单击信息提示) ----------------------------------------
/**
* What tapping a special tile says. `lines` are functions of the stage's own terrain parameters (`stage.special[<terrain>]`
* and the tile's `bb`, the very numbers the sim runs on — server/sim/content/devices.js), so a tip can never disagree with
* the battle; the prose is ours (docs/PLAYING.md wording, PRTS 特殊地形 / 沼泽控制 / 深水区 地形信息).
* `tag` is the chip above the name; `fact` needs the tile's own legend entry (see terrainInfo).
*/
const TERRAIN_TIPS = Object.freeze({
infection: {
name: N_('活性源石'), tag: N_('特殊地形'),
lines: (st) => {
const b = isObj(st?.infection?.bb) ? st.infection.bb : {};
const dmg = param(b.damage, 0);
const mods = [];
if (param(b.atk, 0)) mods.push(t('攻击力 +{atk}%', { atk: Math.round(param(b.atk, 0) * 100) }));
if (param(b.attack_speed, 0)) mods.push(t('攻击速度 +{aspd}', { aspd: param(b.attack_speed, 0) }));
return [
dmg ? t('部署于其上的我方单位、经过的敌方单位,每秒受到 {dmg} 点真实伤害(无来源)', { dmg }) : t('在其上的我方单位与经过的敌方单位持续受到伤害'),
mods.length ? t('同时获得:{mods}', { mods }) : null,
param(b.duration, 0) ? t('效果持续 {sec} 秒;离开地块后仍然保留,再次接触会重新计时', { sec: param(b.duration, 0) }) : null,
].filter(Boolean);
},
},
mire: {
name: N_('沼泽'), tag: N_('特殊地形'),
lines: (st) => {
const m = isObj(st?.mire) ? st.mire : {};
const per = param(m.aspdPerStack, -0.05);
const move = param(m.moveMulPerStack, -0.05);
const max = param(m.maxStacks, 10);
const heavy = param(m.heavyWeight, 3);
const sec = param(m.intervalSec, 1);
return [
move
? t('留在沼泽里的单位每 {sec} 秒获得 1 层「陷入沼泽」:攻击速度 {aspd},敌方单位还有移动速度 {move}', { sec, aspd: pctText(per), move: pctText(move) })
: t('留在沼泽里的单位每 {sec} 秒获得 1 层「陷入沼泽」:攻击速度 {aspd}', { sec, aspd: pctText(per) }),
heavy ? t('重量 ≥ {heavy} 的敌人一次获得 2 层', { heavy }) : null,
t('最多 {max} 层;离开沼泽后解除', { max }),
].filter(Boolean);
},
},
smog: {
name: N_('排气格栅'), tag: N_('特殊地形'),
// the sim gives the tile's buff `flags: { stealth: true }` (devices.js enterTerrain): enemy ranged targeting
// skips it like 隐匿 — and, like 隐匿, it does NOT stop the enemy it blocks from attacking it (PRTS 隐匿).
lines: () => [
t('站在排气格栅上的干员不会被敌方的远程攻击选中(效果相当于隐匿)'),
t('但挡住敌人的干员仍会被它攻击到'),
],
},
deepsea: {
name: N_('深水区'), tag: N_('特殊地形'),
lines: (st) => {
const b = isObj(st?.deepsea?.bb) ? st.deepsea.bb : {};
const dmg = param(b['sea_drown[enemy].damage'], 0);
const aspd = param(b['sea_drown[enemy].attack_speed'], 0);
const move = param(b['sea_drown[enemy].move_speed'], 0);
const out = [];
if (dmg) out.push(t('敌人每秒受到 {dmg} 点伤害', { dmg }));
const slowed = move && move !== 1;
if (aspd && slowed) out.push(t('攻击速度 {aspd}、移动速度 ×{move}', { aspd: pctText(aspd), move }));
else if (aspd) out.push(t('攻击速度 {aspd}', { aspd: pctText(aspd) }));
else if (slowed) out.push(t('移动速度 ×{move}', { move }));
// devices.js tickDeepsea: sourceless true damage tagged 'dot' / 'periodic' / 'deepsea' — deliberately NOT 'terrain'
// (环境伤害, which is what 活性源石's tick is): it is nobody's damage, so no 干员's 增伤 / 穿透 / 装备 applies.
out.push(t('溺水伤害属于无来源伤害(不吃干员的增伤、穿透与装备加成),也不归类为环境伤害'));
out.push(t('拒绝部署(特制水上平台可以让这一格变得可部署)'));
return out;
},
},
start: { name: N_('红门'), tag: N_('敌方入口'), lines: () => [t('敌方单位从这里出场')] },
end: { name: N_('蓝门'), tag: N_('保护目标'), lines: () => [t('敌人走进这里会扣你的目标生命值(LP),一回合至多 10 点')] },
telin: { name: N_('传送入口'), tag: N_('特殊地形'), lines: () => [t('敌人走到这里会从场上消失')] },
telout: { name: N_('传送出口'), tag: N_('特殊地形'), lines: () => [t('消失的敌人会从这里重新出现')] },
});
/** Tile keys that carry a tip of their own although the legend gives them no `special` tag (gates, teleports). */
const TIP_BY_TILEKEY = Object.freeze({ tile_start: 'start', tile_end: 'end', tile_telin: 'telin', tile_telout: 'telout' });
/** 深水区's own legend entry is the one tile whose mechanism overrides the level's buildableType (grid.js DEPLOY_REFUSED_TILES). */
const BUILDABILITY = Object.freeze({ ALL: N_('可部署'), MELEE: N_('仅近战位可部署'), RANGED: N_('仅远程位可部署'), NONE: N_('不可部署') });
const param = (v, d) => (Number.isFinite(Number(v)) ? Number(v) : d);
const pctText = (v) => `${v > 0 ? '+' : '−'}${Math.abs(Math.round(param(v, 0) * 100))}%`;
/**
* The tip a tap on board tile (row, col) opens (GitHub issue #184 — "建议加入对于特殊地形的单击信息提示"), or null for an
* ordinary tile (road / floor / wall / fence …): those say nothing, so a tap on them still just closes what is open.
* The tile comes from the stage the board on screen is built from (`stage.rows` + `stage.tiles`, data/stages.json), and
* the numbers from that stage's own terrain parameters — the same values the sim runs. It reads the stage only, so a
* 补位 / 自选 board (whose pieces are other records) explains its tiles the same way. The text is in the current
* language (t()).
* @param {{ rows?: string[], tiles?: Record<string, any>, special?: any } | null | undefined} stage the shown field's stage
* @param {number} row board row (row 0 = the bottom row, DESIGN §1)
* @param {number} col
* @returns {{ key:string, name:string, tag:string, row:number, col:number, lines:string[], facts:string[] } | null}
*/
export function terrainInfo(stage, row, col) {
const rows = Array.isArray(stage?.rows) ? stage.rows : null;
const line = rows && Number.isInteger(row) && row >= 0 ? rows[row] : null;
if (typeof line !== 'string' || !Number.isInteger(col) || col < 0 || col >= line.length) return null;
const tiles = isObj(stage.tiles) ? stage.tiles : null;
const tile = tiles ? tiles[line[col]] : null;
if (!isObj(tile)) return null;
const key = tile.special || TIP_BY_TILEKEY[tile.tileKey] || null;
const tip = key ? TERRAIN_TIPS[key] : null;
if (!tip) return null;
const facts = [];
const build = BUILDABILITY[tile.buildable];
if (build) facts.push(t(build));
if (tile.height === 'HIGH') facts.push(t('高台'));
if (tile.groundPassable === false) facts.push(t('只有空中单位能通过'));
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 };
}