diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd48167..c72dcdfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ - 选择策略时,围绕本局禁用盟约的策略(标准模拟下的潘格尼尼、克莱门莎、玛恩纳)标出「本局禁用」并在详情里说明,仍可选择。 - 准备阶段收起商店后,棋盘按官方收起商店时的视角放大,整备区留在收起的商店栏和左下角按钮上方,展开后恢复原视角;拖动干员或选择朝向时会等放下后再切换(GitHub #5)。 - 浏览器在后台回收页面、切回时重新加载后,干员不再一直显示为方块加水滴的占位图标:素材清单直接沿用已载入的游戏数据,下载失败会重试,模型下载失败或超时也会重试;在后台开始的战斗切回时干员直接显示模型(GitHub #8)。 +- 选择策略时可以点「查看禁用盟约与干员」,再看一遍确认本局信息里的盟约和禁用干员(只读,倒计时照常进行,轮到下一位时自动关闭;GitHub issue #8 提出);对局中左上角 🔍 的本局信息也按阶位列出禁用干员。 ### 人机 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index a403fc69..fa04f10b 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -486,7 +486,7 @@ Visuals: oblique perspective board (tilted ~30°, trapezoid tiles), raised highl Look & feel: research 06 §11–§12 (near-black green-grey panels, mint `#4ed8af` accents, gold prices, bracket frames, hexagon badges, bond discs, 7-segment countdown). Fonts: Noto Sans SC (Google Fonts with local fallback), Bender/Novecento self-hosted for numerals and Latin micro-labels. -Screens: **Title** (season art backdrop, 开始 → nickname), **Lobby** (独立模拟 / 同盟模拟 → create or join; recent rooms), **Room** (4 seats, invite code + copy link, difficulty picker (host), add/remove AI, ready, start), **Briefing** (INFO_CHECK: stage, factions, disabled bonds, banned chess, boss silhouette, ready x/4), **Band draft** (grid of 40 bands with icon/name/LP/effect; turn order list; skip), **Game** (Pixi field + DOM HUD: top bar with round/phase capsule/LP (live during the own battle, §17.5)/timer/ready; bond strip + popups; left team panel with LP/status/emote bubbles, click to watch; bottom shop bar (level card, 3–5 chess cards, item card, funds, refresh, freeze, collapse); detail panel on click/hover (header with bonds, then 特质 — the operator's own effect — right under it, 特性, stats (live: the battle's own numbers / the start-of-battle ones in prep, §18.5), skill, module, equipped items, talents: §17.5); enemy preview drawer (next round enemies with counts, factions); 机变 overlay (the official card: framed icon + name, the effect text — data `descRaw` — under it, the taker's round avatar at the top-right; two taps, §18.2, §20.7); merge reward overlay (the promotion reward, or a special offer — 凯瑟琳's 定向投放 … — as item / chess cards under its source's label, with the count of offers queued behind it, §21.2); phase banners; ticker (by broadcast priority, highest first; a leader-damage line plays only in its round and a player's newer one replaces the older, §21.10); toasts; emote wheel; settings (volume, quality, show damage numbers)), **Result** (victory/defeat, rounds, per-player lineup, stats, titles, back to room). +Screens: **Title** (season art backdrop, 开始 → nickname), **Lobby** (独立模拟 / 同盟模拟 → create or join; recent rooms), **Room** (4 seats, invite code + copy link, difficulty picker (host), add/remove AI, ready, start), **Briefing** (INFO_CHECK: stage, factions, disabled bonds, banned chess, boss silhouette, ready x/4), **Band draft** (grid of 40 bands with icon/name/LP/effect; turn order list; skip; 查看禁用盟约与干员 under the order list opens the briefing's bond rows, legend and banned operators again in a read-only dialog — one component, `ui/matchInfo.js`, §21.28), **Game** (Pixi field + DOM HUD: top bar with round/phase capsule/LP (live during the own battle, §17.5)/timer/ready; bond strip + popups; left team panel with LP/status/emote bubbles, click to watch; bottom shop bar (level card, 3–5 chess cards, item card, funds, refresh, freeze, collapse); detail panel on click/hover (header with bonds, then 特质 — the operator's own effect — right under it, 特性, stats (live: the battle's own numbers / the start-of-battle ones in prep, §18.5), skill, module, equipped items, talents: §17.5); enemy preview drawer (next round enemies with counts, factions; its 本局信息 tab lists the briefing's disabled bonds and banned operators, §21.28); 机变 overlay (the official card: framed icon + name, the effect text — data `descRaw` — under it, the taker's round avatar at the top-right; two taps, §18.2, §20.7); merge reward overlay (the promotion reward, or a special offer — 凯瑟琳's 定向投放 … — as item / chess cards under its source's label, with the count of offers queued behind it, §21.2); phase banners; ticker (by broadcast priority, highest first; a leader-damage line plays only in its round and a player's newer one replaces the older, §21.10); toasts; emote wheel; settings (volume, quality, show damage numbers)), **Result** (victory/defeat, rounds, per-player lineup, stats, titles, back to room). A bond the mode never activates (标准: official `inactiveBondIdList`) reads 本局禁用 on shop / reward cards (struck through), detail chips and the popup — no 在场 count, no 当前效果 (§21.7). @@ -1433,3 +1433,11 @@ Two public GitHub issues after 0.1.0, both client-only. Normative lines rewritte - **Now**: the field host seeds the asset store with the game data's copies (`seedAssets` → `assets.seed` / `seedLocal`; the match screen already waits for them, so no second download). Without one, `ready()` retries a transient failure after 600 ms and 2 s (like data.js), then resolves `{}` without keeping it: the next `ready()` and a bounded background backoff (5 / 15 / 30 / 60 s) fetch again, and `onChange` tells the field view when a manifest arrives late — every view re-resolves what it could not draw (`UnitView.retryAssets`; prep item icons through `setPrep` / `ItemView.setIcon`). A view whose model failed or timed out loads it again after 2 / 6 / 15 / 30 s (`SPINE_RETRY_MS`; frames only, so never while hidden), `acquire(entry, { retry: true })` dropping the remembered failure; showing the tab again retries at once (also a missing manifest). Battle mode holds the Spine cache (`spine.hold()`, from `enterBattle` to the next `setPrep` or destroy): no quiet budget meanwhile, the 48 MB idle budget still applies, and the quiet budget keeps freeing everything in the lobby, room and result screen (the policy is in the assets.js header). - Verified (headless Chrome, no console errors): a reload with every assets.json request delayed 6 s, with only a second download delayed (the 0.1.0 race: none is made now) and with the first 4 requests failing — operators drawn as Spine models at once; the first 6 failing — placeholders until the store's first background try 5 s later, then Spine models; a battle started in a hidden tab, back after 25–30 s (still running, ended meanwhile, or paused with Spine downloads slowed to 4 s) — every unit a model within 0.3 s of the return, no diamonds (before: the cache was empty from ≈ 18 s); six solo rounds with two battles started hidden — the cache follows the roster (4 → 24 skeletons, 6.9 → 25.2 MB estimated, idle ≤ 11 MB, under the 48 MB budget), never a diamond or a placeholder. - Tests: `test/ui/issue5-fold-camera.test.js`, `test/render/issue8-placeholders.test.js`, `test/ui/playtest5-ui.test.js` (wiring), `test/render/assets.test.js`; opt-in `SP_E2E=1 node --test test/ui/mock.e2e.test.js` (fold / unfold at 1920×1080 and 844×390). + +### 21.28 Match info in the strategy draft (GitHub issue #8 item 1, v0.1.1) — `ui/matchInfo.js`, `screens/briefing.js`, `screens/bandDraft.js draftInfoStatus`, `ui/enemyDrawer.js InfoTab`, `css/screens/draft.css .minfo-dlg` + +- **Reported** (public issue #8, item 1, "选策略时没法返回查看禁用的干员和盟约"): the disabled bonds and the banned operators were shown only in the INFO_CHECK briefing, but a strategy is chosen partly by them. The owner took it into 0.1.1 as a remake addition (no official screen is copied; the look is the briefing's). +- **One component**: `ui/matchInfo.js` — `matchInfoModel(pub, { bonds, chess, mode })` (pure: `gameLogic disabledBondSets` over `config.modes[modeId].inactiveBondIds` — the drawn set D "阵容不完整" and the mode's static inactive bonds "本局禁用", e.g. 标准's 10; every bond by `bondOrder`, then `identifier`, split 核心 / 附加; the banned operators of m.public `bannedChess`, known chess only, by tier; banned members per bond) and the hookless blocks `MatchBondRow` / `MatchLegend` / `BannedOperators` / `MatchInfo` (greyed disc with ✕, the red badge, the `briefingBondTip` tooltip, the legend, the 本局禁用干员 grid). The briefing's right column is `MatchInfo`; the in-game 本局信息 tab (left 🔍) reads the same model for its compact chips and avatars (its banned list is now by tier, like the briefing's). +- **The draft**: 查看禁用盟约与干员 under the order list opens `MatchInfoDialog` — `MatchInfo` in the components.js `Modal`, read-only, closed by 关闭, a tap outside or Esc (a press on the backdrop closes it on `mousedown`, so the click never reaches what lies under it — e.g. 确认选择). The draft runs on underneath: the dialog's footer repeats the turn and its seconds (`draftInfoStatus`: 轮到你决策 / X 决策中 + the step header's number, orange at ≤ 10 s; 已选择「…」after the own pick); a turn change (a pick, a skip, a turn that runs out, an AI pick) or the own pick closes it; the end of the draft unmounts it with the screen; it never changes the highlighted band (`g.bandFocus`) or the buttons. +- **Sizes**: on desktop the briefing's own sizes (1920×1080: no scrolling); every size in the dialog is max(design rem, px floor), so on phones (1rem = 40 px; 756×366, 844×390) the text stays ≥ 9–15 px, the dialog fits the safe area and its body scrolls under a fixed header and footer. +- Tests: `test/ui/match-info.test.js` (the model, greying by state, badges, tips, the legend, banned by tier, real 标准 / 绝境 matches, `draftInfoStatus`, briefing / draft / drawer reading the one module); opt-in `test/ui/match-info.e2e.test.js` (`SP_E2E=1`: solo 标准 on desktop — dialog = briefing, the three ways to close, a confirm afterwards, the in-game tab; co-op 绝境 with a desktop and a phone — the live countdown, a turn change closing it, 844×390 / 756×366 layout and scrolling, the draft's end unmounting it). diff --git a/docs/PLAYING.md b/docs/PLAYING.md index 7efdaba9..7c6e0f9b 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -15,7 +15,7 @@ | 阶段 | 说明 | |---|---| | 确认本局信息 | 本局地图(标准模拟固定为「战场#01」;险境从 8 张地图中随机,绝境 / 终极从不含战场#01 的 7 张中随机)、敌方派系、**被禁用的盟约及其干员**、领袖预告。所有人确认后继续;同盟模拟最多 25 秒;只有你一名玩家时(独立模拟,或同盟模拟只带 AI 队友)不限时。 | -| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。 | +| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。选的时候可以点左侧决策顺序下方的「查看禁用盟约与干员」,再看一遍「确认本局信息」里的盟约(置灰、禁用人数)和本局禁用干员:只能查看,倒计时照常走,底部也显示轮到谁和剩余秒数;点「关闭」、点窗口外面或按 Esc 关闭,轮到下一位时会自动关闭。 | | 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(首位 30 秒,其余 16 秒;只有你一名玩家时不限时),独立 3 选 1。卡片包括装备、资金、干员、盟约层数、悬赏等;**悬赏决策**为你的作战添加悬赏敌人,卡上用蓝字标出持续多久:「下场作战」/「接下来两场作战」的只出现一场或两场,之后不再出现;官方的「多轮悬赏」(原文写「之后的每场作战」)在这里也按「接下来两场作战」处理,只出现两场(如教鞭的法术大师A2·多轮战术特训)。各回合的悬赏决策按官方对局截图来出:第 3 回合是 6 张「接下来两场作战」的悬赏(3 张 1 资金、2 张 2 资金、1 张 3 资金);第 9 回合是各首领的「下场作战」悬赏和「源石虫·特训」;绝境 / 终极的第 11 回合多半是悬赏决策(也可能是机密商店或战术决策),其中的悬赏通常有 1 只特异巨兽,其余是各阵营的「下场战斗」悬赏。击倒悬赏敌人的人(联防中的队友也算)获得资金;右侧效果栏会写「还剩 N 场作战」。**机密商店**的 6 件装备都免费,和官方一样可能出现两件相同的装备(各算一张卡,可以分别被选走);第 11 回合(绝境 / 终极)的机密商店按官方:2 件 VI 阶、至少 1 件 V 阶和 1 枚盟约之币,标准 / 险境较早回合的机密商店各阶装备都可能出现。**战术决策**也和官方一样可能出现两张相同的卡(官方截图里出现过两张「补给」;各算一张卡,可以分别被选走,每张只生效一次,两人各选一张全队卡则效果叠加);第 11 回合(绝境 / 终极)的战术决策按官方截图只出盟誓、驰援、列装 / 财富 / 补给 / 整备 / 升华等增益卡,不出削弱敌人的卡和战场演变卡。每张卡左上是图标和名称,下面写着完整效果(与详情卡相同的官方描述,手机上也显示);已被选走的卡变暗,右上角显示选走它的玩家头像(悬停可看名字)。选卡要**点两次**:第一次只选中(金框 +「确认选择 · 再次点击」),再点同一张或点右上的「确认选择」才生效;点别的卡换选,按 Esc 或点空白处取消。 | | 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消。 | | 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**从上到下、从左到右**的顺序自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样);倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 | @@ -76,7 +76,7 @@ - **元素损伤条**在单位自己的血条正下方:左边是元素图标,白条是剩余的元素值,只有真正爆发时才会耗尽;爆发后条变成元素的颜色、图标闪烁,在冷却期间慢慢回满(这段时间不会再积累元素损伤)。 - **敌人**:远程敌人的射程按官方从干员的碰撞体积(半径 0.25 格)算起,比数据上的攻击范围半径远 0.25 格——例如萨卡兹枯朽战车(射程 2.2)能打到中心 2.45 格以内、站在低地的干员。它的第一下攻击就是「秽蚀轰击」(在目标处留下半径 1.7、持续 10 秒的污染区:低地每秒 50、高台 25 点真实伤害,同名不叠加),被阻挡时普通攻击伤害翻倍。“萨科塔之翼”等血量第一次低于一半时会在原地小范围乱飞 5 秒(官方叫「恐惧」);叙拉古、妮芙等干员造成的恐惧会让敌人朝远离干员的方向逃跑。隐匿的敌人只有被阻挡(或被揭示)时才能被选为攻击目标,溅射、技能的圆形范围伤害也打不到没被挡住的隐匿敌人(和官方一样):深池逐火战士每次被击倒后先重生 1 秒,然后变成继续往前走的隐匿「余烬」(需要 5 次伤害击倒,护卫的「火灰」10 次),一般要用干员挡住它才能选中(挡住时它不再半透明),10 秒内没打掉就满血站起来;敌人重生时身上的减速、恐惧等状态会被清空、技能冷却从头开始,被恐惧的逐火战士变成余烬后也能被挡住;转译基底·α 原始形态受到的伤害全部无效,受到第 4 次物理伤害、第 4 次法术伤害或被阻挡时花 2 秒变成寻仇者、特战术师或幽灵,之后才能被击倒。 - **策略「鸭爵」(“神秘顾客”)**:从第 5 回合起,你和队友的每场作战(**最终攻势和隐秘核心也算**,联防不另外替换)有 0–2 名地面敌人(不含领袖及其部件、悬赏敌人)被替换为特殊版本的鸭爵、高普尼克、流泪小子或圆仔,出现在被替换敌人的时间和路线上(两人同场的领袖战里,各自只替换走向自己保护目标点的敌人);击倒者获得 1 资金(联防中的队友也算),漏进保护目标点照常扣 1 点目标生命值。鸭爵和圆仔**不攻击、无法被阻挡**:鸭爵受到伤害后移动速度 +300%;圆仔始终面向我方干员较多的一侧,来自正面的物理 / 法术伤害 −80%。流泪小子隐匿(被阻挡时才能被选中),攻击使目标晕眩 3.5 秒;高普尼克免疫晕眩、冻结和浮空。 -- 右上角 🔍▶▶ 可以查看本回合即将来袭的敌人(预览区),左上角 🔍 查看本局信息(禁用盟约、策略)。 +- 右上角 🔍▶▶ 可以查看本回合即将来袭的敌人(预览区),左上角 🔍 查看本局信息(禁用盟约、禁用干员、策略)。 ## 5. 联防(同盟) @@ -100,7 +100,7 @@ - **炎佑**:场上 6 名以上「炎」干员时召唤炎佑(9 名时两只)。它追踪全场仇恨最高的敌人并悬停在其上方,技能「祛恶之焰」锁定 1 个目标**持续喷火**(最长 20 秒,每秒对目标周围的敌人造成法术伤害并附带灼燃,喷火时不普攻,目标死亡或被沉默时结束);场上暂时没有敌人时停在原地等待。它持有官方的「孤立」:医疗干员不会治疗它,友方的增益、光环和天赋也不会选中它(敌人照常可以攻击它)。 - **层数**:盟约通过各种效果叠加层数,层数越高效果越强(例如「炎」攻击力随层数提升、「远见」每 10 层给资金)。**层数整局保留,但只有盟约激活时才生效**,多数情况下也只有激活的盟约能叠层。每个盟约最多 **999 层**(与官方相同,取自官方客户端的常量):到 999 后再获得的层数直接作废,按层数发放的奖励(如「远见」的资金)也随之停止。作战中获得的层数在盟约栏里**实时**显示,作战结束后一直保留到结算(联防期间也不会退回去)。 - **加成怎么叠**:盟约、策略、装备、机变卡和特质给的「攻击力 / 防御力 / 生命值 +X%」与技能的「攻击力 +X%」**相加**后再乘到属性上(官方:直接乘算),例如精准 +130%、独行 +60%、一件 +40% 的装备合计 +230%,而不是 ×2.3×1.6×1.4。「伤害提升至 X%」「受到的伤害 +X%」(维多利亚、谢拉格、奥术、脆弱等)这类伤害倍率之间仍然相乘;但同名效果只算一个——同一战场两名玩家都开了「奥术」时,敌人身上只挂一层奥术易伤,取两人中较高的那一个(不再相乘);两名「灵知」的坚冰、两名「莱恩哈特」/「缄默德克萨斯」的法抗削减也一样只算最强的一个。「坚守」的反伤计入被打的那名成员(个人对领袖伤害统计里能看到)。 -- 每局会随机**禁用**若干盟约(标准模拟较少),它们的干员不会出现在商店;开局的「确认本局信息」会列出。 +- 每局会随机**禁用**若干盟约(标准模拟较少),它们的干员不会出现在商店;开局的「确认本局信息」会列出,选择策略时(「查看禁用盟约与干员」)和对局中(左上角 🔍)也能再查看。 - 两件同名装备会合成一件更强的装备;特定的装备组合可以为干员赋予额外的盟约效果:「变形同构体」与另一件盟约装备(如维式重锤)一同携带时,携带者视为该装备对应盟约的成员。对应关系见变形同构体详情卡的「天赋」一栏(本局禁用的盟约会标出;装在干员身上时,正在生效的那一组会高亮)。 ## 7. 最终攻势与隐秘核心 diff --git a/public/css/screens/briefing.css b/public/css/screens/briefing.css index 90a00a4e..b1ec678a 100644 --- a/public/css/screens/briefing.css +++ b/public/css/screens/briefing.css @@ -1,6 +1,8 @@ /* ========================================================================================== screens/briefing.css — INFO_CHECK briefing (js/screens/briefing.js) + the pre-game step header shared with the band draft (js/ui/matchChrome.js StepHeader). Research 06 §4.1 / §11.6. + The bond rows, the legend and the banned operators (.brief-bonds / .brief-legend / .brief-banned) are + js/ui/matchInfo.js MatchInfo — also shown by the band draft's 本局信息 dialog (css/screens/draft.css .minfo-dlg). ========================================================================================== */ /* ---- shared pre-game backdrop + header ---- */ diff --git a/public/css/screens/draft.css b/public/css/screens/draft.css index 57751172..17fb8e6b 100644 --- a/public/css/screens/draft.css +++ b/public/css/screens/draft.css @@ -1,6 +1,7 @@ /* ========================================================================================== screens/draft.css — BAND_DRAFT strategy pick (js/screens/bandDraft.js). Research 06 §4.2: - draft order on the left, portrait grid, detail pane with 初始生命值 and 确认选择 / 跳过. + draft order on the left, portrait grid, detail pane with 初始生命值 and 确认选择 / 跳过; the 本局信息 entry under + the order list and its dialog (js/ui/matchInfo.js MatchInfoDialog, GitHub issue #8 item 1). ========================================================================================== */ .draft__main { position: relative; z-index: 1; flex: 1; min-height: 0; display: grid; grid-template-columns: 3.9rem 1fr 5rem; gap: .3rem; padding: .3rem .42rem .36rem; } @@ -83,3 +84,50 @@ font-size: max(.14rem, 9px); line-height: 1.5; color: var(--text-lo); background: rgba(255, 255, 255, .03); border-left: 2px solid rgba(255, 255, 255, .28); } .draft-detail__off .icon { width: max(.16rem, 10px); height: max(.16rem, 10px); flex: none; margin-top: .02rem; } .draft-detail__offname { color: var(--text-dim); text-decoration: line-through; text-decoration-color: rgba(255, 84, 84, .6); } + +/* ---- 本局信息 (GitHub issue #8 item 1, DESIGN §21.28): the entry under the order list and the read-only dialog ------- + The dialog shows the briefing's own blocks (.brief-bonds / .brief-legend / .brief-banned, css/screens/briefing.css) + at the briefing's size on desktop; every size is max(design rem, px floor), so on a phone (1rem = 40 px) the text + stays at ≥ 9–15 px and the dialog's body scrolls inside the screen (756×366, 844×390) while the header and the + footer (the turn's countdown, 关闭) stay in view. */ +.draft-order__info { + margin-top: .04rem; justify-content: flex-start; padding: 0 .16rem; + height: max(.5rem, 28px); font-size: max(.18rem, 10px); + --btn-bd: var(--mint-800); --btn-bg: rgba(78, 216, 175, .06); --btn-bg-hover: rgba(78, 216, 175, .14); +} +.draft-order__info .btn__icon { color: var(--mint-400); } +.modal__box.minfo-dlg { + width: min(max(12rem, 600px), calc(100vw - .6rem - var(--sa-l, 0px) - var(--sa-r, 0px))); + max-height: calc(100vh - .6rem); + max-height: calc(100svh - .6rem); +} +.minfo-dlg .modal__head { padding: .16rem .3rem .02rem; } +.minfo-dlg .modal__head .micro { font-size: max(.11rem, 8px); } +.minfo-dlg .modal__title { font-size: max(.26rem, 15px); } +.minfo-dlg .modal__body { display: flex; flex-direction: column; gap: max(.16rem, 6px); padding: .1rem .3rem .2rem; } +.minfo-dlg .modal__actions { align-items: center; padding: .12rem .3rem; } +.minfo-dlg .modal__actions .btn { height: max(.5rem, 30px); font-size: max(.18rem, 12px); } +.minfo-dlg__status { margin-right: auto; min-width: 0; } +.minfo-dlg__turn { display: inline-flex; align-items: center; gap: .06rem; font-size: max(.17rem, 11px); font-weight: 700; color: var(--gold); white-space: nowrap; } +.minfo-dlg__turn .icon { width: max(.18rem, 12px); height: max(.18rem, 12px); } +.minfo-dlg__turn b { margin-left: .04rem; font-size: max(.22rem, 14px); } +.minfo-dlg__turn.is-warn { color: var(--orange-hi); } +.minfo-dlg__turn.is-mint { color: var(--mint-400); } +.minfo-dlg__turn.is-dim { color: var(--text-lo); } +.minfo-dlg .brief-bonds, .minfo-dlg .brief-banned { padding: max(.16rem, 6px) max(.2rem, 8px) max(.2rem, 8px); } +.minfo-dlg .brief-h { font-size: max(.21rem, 12px); margin-bottom: max(.12rem, 5px); } +.minfo-dlg .brief-h::before { width: max(.06rem, 3px); height: max(.2rem, 11px); } +.minfo-dlg .brief-bonds__row { gap: max(.16rem, 6px) max(.1rem, 4px); } +.minfo-dlg .brief-bond { width: max(1rem, 50px); } +.minfo-dlg .brief-bond .bond { --disc: max(.66rem, 32px); } +.minfo-dlg .brief-bond .bond__name { font-size: max(.16rem, 10px); } +.minfo-dlg .brief-bond__ban { font-size: max(.12rem, 9px); padding: 0 max(.05rem, 2px); } +.minfo-dlg .brief-bond__ban .icon { width: max(.12rem, 9px); height: max(.12rem, 9px); } +.minfo-dlg .brief-legend { margin-top: 0; font-size: max(.14rem, 10px); } +.minfo-dlg .brief-legend__off { width: max(.16rem, 9px); height: max(.16rem, 9px); } +.minfo-dlg .brief-legend__ban { width: max(.2rem, 12px); height: max(.18rem, 11px); } +.minfo-dlg .brief-legend__ban .icon { width: max(.12rem, 8px); height: max(.12rem, 8px); } +.minfo-dlg .brief-banned__n { font-size: max(.22rem, 13px); } +.minfo-dlg .brief-banned__grid { grid-template-columns: repeat(auto-fill, minmax(max(.6rem, 34px), 1fr)); gap: max(.08rem, 3px); } +.minfo-dlg .brief-banned__grid .uthumb { --ut: max(.6rem, 34px); } +.minfo-dlg .t-dim { font-size: max(.16rem, 10px); } diff --git a/public/js/screens/bandDraft.js b/public/js/screens/bandDraft.js index 4f2c24b8..8a2003d0 100644 --- a/public/js/screens/bandDraft.js +++ b/public/js/screens/bandDraft.js @@ -12,11 +12,17 @@ // A strategy built around a bond the mode switches off (bands.json bondIds ∩ the mode's inactive bonds — 标准: 潘格尼尼 // 拉特兰, 克莱门莎 阿戈尔, 玛恩纳 卡西米尔; the bot never picks one) reads 本局禁用 on its card and in the detail pane // (BandOffTag / BandOffNote, DESIGN §21.26, §21.7's look); it stays selectable — information only. +// 本局信息 (GitHub issue #8 item 1, "选策略时没法返回查看禁用的干员和盟约"): 查看禁用盟约与干员 under the order list opens the +// briefing's bond rows, legend and 本局禁用干员 again, read-only (ui/matchInfo.js MatchInfoDialog — the very blocks of the +// briefing). The draft runs on underneath: its status line repeats the current turn and the countdown (draftInfoStatus), +// a turn change (a pick, a skip, a turn that runs out, an AI pick) closes it, the end of the draft unmounts it, and it +// never touches the highlighted band or the buttons. import { useEffect, useMemo, useRef, useState } from '../../vendor/hooks.module.js'; import { html, Button, Icon, MicroLabel, useTicker, secondsLeft } from '../ui/components.js'; import { useGameData, BandIcon, RichText, PlayerAvatar, LpTower, Sprite } from '../ui/gameComponents.js'; import { StepHeader, ExitModal } from '../ui/matchChrome.js'; +import { MatchInfoDialog, matchInfoModel } from '../ui/matchInfo.js'; import { actions, act } from '../ui/gameActions.js'; import { normalizeDraft, sortedPlayers } from '../ui/gameLogic.js'; import { useStore } from '../store.js'; @@ -139,6 +145,21 @@ export function draftClock(pub) { return { deadline, total: Number(d.turnSeconds) > 0 ? Number(d.turnSeconds) : null }; } +/** + * The status line of the 本局信息 dialog: what the draft does while the dialog covers it — my pick, else whose turn it is + * with the turn's seconds left (the step header's number; none when untimed), warning at ≤ 10 s like the countdown. + * @param {{ myPick?: string|null, pickName?: string|null, myTurn: boolean, turnName?: string|null, secs?: number|null, + * waiting?: boolean }} o waiting: teammates still have to pick after my pick + * @returns {{ text: string, secs: number|null, tone: 'mint'|'gold'|'warn'|'dim' }} + */ +export function draftInfoStatus({ myPick = null, pickName = null, myTurn, turnName = null, secs = null, waiting = false }) { + if (myPick) return { text: `已选择「${pickName || ''}」${waiting ? ',等待其他博士' : ''}`, secs: null, tone: 'mint' }; + const s = Number.isFinite(secs) ? Math.max(0, Math.round(secs)) : null; + const tone = s != null && s <= 10 ? 'warn' : 'gold'; + if (myTurn) return { text: '轮到你决策', secs: s, tone }; + return { text: turnName ? `${turnName} 决策中` : '等待轮到你', secs: s, tone: s != null ? tone : 'dim' }; +} + /** BAND_DRAFT screen. */ export function BandDraftScreen() { const pub = useStore((s) => s.match.public); @@ -150,6 +171,7 @@ export function BandDraftScreen() { const [busy, setBusy] = useState(null); const [exit, setExit] = useState(false); const [skipped, setSkipped] = useState(false); + const [infoOpen, setInfoOpen] = useState(false); const mode = gd.config?.modes?.[pub?.modeId]; const offBonds = modeOffBonds(mode); // the bonds this mode never activates (标准: 10 of 23) @@ -179,6 +201,10 @@ export function BandDraftScreen() { }, [bands.length, myPick, myTurn, takenKey]); // "your turn" cue useEffect(() => { if (myTurn && !solo) audio.sfx('yourTurn'); }, [myTurn]); + // the 本局信息 dialog never outlives the turn it was opened in: a turn change (a pick, a skip, a turn that ran out, an + // AI pick) or my pick closes it, so whoever's turn begins sees the draft + const turnKey = `${draft.turnPid || ''}|${myPick || ''}`; + useEffect(() => { setInfoOpen(false); }, [turnKey]); // one countdown (user playtest #4 item 4): the current turn's — m.public.deadline, the same clock as the picker's row const clock = solo ? null : draftClock(pub); @@ -214,6 +240,10 @@ export function BandDraftScreen() { // the picker's row shows the step header's number (both read the one turn deadline) const turnSecs = clock ? secondsLeft(clock.deadline) : null; const turnLen = Number(pub?.draft?.turnSeconds) > 0 ? Math.round(pub.draft.turnSeconds) : null; + // 本局信息: the briefing's blocks (built only while the dialog is open) and the draft's state under it + const info = infoOpen ? matchInfoModel(pub, { bonds: gd.list('bonds'), chess: gd.chess, mode }) : null; + const infoStatus = infoOpen ? draftInfoStatus({ myPick, pickName: myPick ? gd.band(myPick)?.name : null, myTurn, turnName, secs: turnSecs, + waiting: !solo && !draft.done }) : null; return html`
${draftTip({ timed, turnSeconds: turnLen, autoName: myPick ? null : autoName, selected: autoId === sel })}
` : null} @@ -289,5 +321,9 @@ export function BandDraftScreen() { <${ExitModal} open=${exit} onClose=${() => setExit(false)} solo=${solo} /> + <${MatchInfoDialog} open=${infoOpen} onClose=${() => setInfoOpen(false)} model=${info} + status=${infoStatus ? html` + <${Icon} name=${infoStatus.tone === 'mint' ? 'check' : 'hourglass'} />${infoStatus.text}${infoStatus.secs != null ? html`${infoStatus.secs}s` : null} + ` : null} /> `; } diff --git a/public/js/screens/briefing.js b/public/js/screens/briefing.js index 8c5af7ba..1819692a 100644 --- a/public/js/screens/briefing.js +++ b/public/js/screens/briefing.js @@ -1,44 +1,22 @@ // Briefing — INFO_CHECK "1/2 确认本局信息" (research 06 §4.1, D1): enemy leader (silhouette, name, -// abilities), stage (+ its pool: 战场固定 / 战场随机(共N张)), 特训敌人 factions (icon, name, description), 核心盟约 / 附加盟约 rows of bond discs -// (disabled bonds greyed with the banned-member badge), 本局禁用干员 avatars, difficulty tag, the ready -// count x/N with person pips, the 准备就绪 button (g.infoReady) and the countdown. +// abilities), stage (+ its pool: 战场固定 / 战场随机(共N张)), 特训敌人 factions (icon, name, description), the difficulty +// tag, the ready count x/N with person pips, the 准备就绪 button (g.infoReady) and the countdown. The right column — 核心盟约 +// / 附加盟约 rows of bond discs (disabled bonds greyed with the banned-member badge), the legend and the 本局禁用干员 +// avatars — is ui/matchInfo.js MatchInfo, the same blocks the strategy draft's 本局信息 dialog shows (screens/bandDraft.js). import { useState } from '../../vendor/hooks.module.js'; -import { html, Button, Icon, MicroLabel, BondDisc, Tooltip } from '../ui/components.js'; -import { useGameData, Img, UnitThumb, RichText } from '../ui/gameComponents.js'; +import { html, Button, Icon, MicroLabel } from '../ui/components.js'; +import { useGameData, Img, RichText } from '../ui/gameComponents.js'; import { StepHeader, ExitModal } from '../ui/matchChrome.js'; +import { MatchInfo, matchInfoModel } from '../ui/matchInfo.js'; import { LoadoutButton } from './loadout.js'; import { actions } from '../ui/gameActions.js'; -import { factionTypes, bannedPerBond, sortedPlayers, phaseTotalSeconds, disabledBondSets, briefingBondTip } from '../ui/gameLogic.js'; -import { bondIconUrl, enemyIconUrl, factionIconUrl } from '../ui/assetUrls.js'; +import { factionTypes, sortedPlayers, phaseTotalSeconds } from '../ui/gameLogic.js'; +import { enemyIconUrl, factionIconUrl } from '../ui/assetUrls.js'; import { useStore } from '../store.js'; -import { data } from '../data.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); -function BondRow({ title, micro, bonds, sets, perBond }) { - const m = data.get('assets'); - return html`灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)${sets.off.size ? ',或本模式禁用' : ''} · <${Icon} name="user" />该盟约中无法出现的干员数
-本局没有禁用干员
`} -