+
+ {iconSrc ? (
+
+
+
+ ) : null}
{title}
{subtitle}
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index 56e6ab6..098e834 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -1,26 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
-html,
-body,
-#root {
- height: 100%;
-}
-
html {
font-family: 'Chakra Petch', sans-serif;
}
-
-body {
- min-width: 320px;
-}
-
-code,
-pre,
-button,
-input {
- font-family: inherit;
-}
-
-#root {
- min-height: 100vh;
-}
diff --git a/docs/07-UX与界面信息架构.md b/docs/07-UX与界面信息架构.md
index 42b49f3..d0acb0f 100644
--- a/docs/07-UX与界面信息架构.md
+++ b/docs/07-UX与界面信息架构.md
@@ -9,7 +9,7 @@
- 我现在状态如何(生存状态与主要威胁)
- 我下一步能做什么(行动入口与成本)
- 避免“信息黑箱”:所有关键数值变化必须可追溯原因
-- 支持碎片化:移动端单手可操作,任何时候可暂停/保存
+- 支持 PC / 平板横屏的单屏 HUD 体验,任何时候可暂停/保存
## 7.2 信息架构(IA)
@@ -138,10 +138,13 @@ flowchart TB
- PC:
- 三栏布局(状态/背包/主操作+信息)
- 鼠标悬浮展示细节
-- Mobile:
- - 单栏纵向布局
- - 背包与详情用抽屉/底部弹层
- - 长按替代悬浮
+- Tablet(横屏):
+ - 保持单屏 HUD,不切成长网页
+ - 缩窄三栏宽度,优先保住顶部状态带与底部模式坞站
+ - 背包、任务、蓝图通过弹层进入
+- 当前阶段不适配小屏手机:
+ - 不为 860px 以下宽度继续补专门的游戏体验设计
+ - 若后续启动移动版,再单独设计纵向信息架构
## 7.8 界面清单(MVP)
@@ -153,4 +156,3 @@ flowchart TB
- 战斗
- 事件详情(可选)
- 设置(音量/文本速度/难度/存档管理)
-
diff --git a/docs/12-前端HUD架构与资产管线.md b/docs/12-前端HUD架构与资产管线.md
index 43cd5a0..53bc42d 100644
--- a/docs/12-前端HUD架构与资产管线.md
+++ b/docs/12-前端HUD架构与资产管线.md
@@ -91,13 +91,17 @@ UI 组件 <- Query Cache <- 最新 GameView
所有当前接入的 GPT-image-2 资产位于:
- `apps/web/public/generated/`:地点背景
-- `apps/web/public/generated/ui/character-preview.png`:角色立绘
- `apps/web/public/generated/ui/item-atlas.png`:物资图集原图
- `apps/web/public/generated/ui/equipment-atlas.png`:装备图集原图
- `apps/web/public/generated/ui/status-atlas.png`:生存状态图集原图
- `apps/web/public/generated/ui/items/*`:切分后的物资图标
- `apps/web/public/generated/ui/equipment/*`:切分后的装备槽素材
- `apps/web/public/generated/ui/status/*`:切分后的生存状态图标
+- `apps/web/public/generated/hud/reference/tinywaste-hud-reference.png`:当前唯一 HUD 参考图(无右侧人物)
+- `apps/web/public/generated/hud/atlases/*`:HUD chrome / icon atlas 原图与透明版
+- `apps/web/public/generated/hud/chrome/*`:切分后的 HUD 框体素材
+- `apps/web/public/generated/hud/icons/*`:切分后的 HUD 功能图标
+- `apps/web/public/generated/hud/manifest.json`:HUD 资产清单
对应映射入口:
@@ -108,7 +112,9 @@ UI 组件 <- Query Cache <- 最新 GameView
- 地点图使用整图背景,服务于中央场景和左侧路线卡。
- 物资与装备图使用 atlas + 裁切结果,服务于快捷栏、库存卡、角色槽位。
- 生存状态图使用 atlas + 裁切结果,服务于状态效果卡和后续中断提示。
+- HUD chrome 与 HUD icon 统一走 `apps/web/public/generated/hud/`,并通过 `uiAssets.ts` 暴露。
- 所有引用都走 `uiAssets.ts`,避免组件里散落硬编码路径。
+- HUD atlas 的透明化与切分流程由 `scripts/slice_hud_atlas.py` 和 `remove_chroma_key.py` 组合完成。
## 12.6 HUD 信息职责
@@ -137,12 +143,12 @@ UI 组件 <- Query Cache <- 最新 GameView
### 12.6.4 LoadoutPanel
-- 角色 / 背包 / 任务三态切换
-- 角色立绘与槽位矩阵
-- 属性与状态效果
-- 快捷栏
-- 完整库存与避难所仓储管理
-- 任务与休整
+- Equipment terminal 头部与负载状态
+- 2x3 装备槽矩阵
+- 生存能力指标(防护 / 潜行 / 机动)
+- 生存告警条
+- Ready pack 资源终端(快速补给 + 仓储摘要)
+- 打开背包 / 仓储弹层入口
### 12.6.5 BottomDock
@@ -156,17 +162,17 @@ UI 组件 <- Query Cache <- 最新 GameView
- 维持单屏阅读,整页不滚动,滚动只发生在内部面板。
- 中央区优先展示场景与决策,避免全文字堆叠。
-- 右侧区优先视觉化角色与装备,而不是继续做普通列表。
+- 右侧区不再展示人物立绘,改为装备、警报与资源终端。
- 底部区承担模式切换和系统导航,强化“游戏 HUD”心智。
+- HUD chrome 资产只重点压在小模块和导航件上,大面板只保留轻量纹理,避免再次出现网页卡片感。
## 12.8 下一步扩展建议
下一轮优先项:
1. 继续补 GPT-image-2 资产:
- - 头部、侧武器、弹药、医疗、食物第二批图集
+ - 第二批 HUD chrome(战斗态、事件态、模态终端)
- 天气、辐射、事件状态专用小图标
-2. 将 `LoadoutPanel` 的 tab 与底部导航进一步联动为统一模式系统。
-3. 为 `CommandCenter` 增加战斗专属视图和事件专属视图,而不是完全依赖弹层。
-4. 为物资卡补充更精细的分类过滤和排序逻辑。
-5. 引入可配置的 HUD 主题参数,支撑不同章节或区域切换皮肤。
+2. 为 `CommandCenter` 增加战斗专属视图和事件专属视图,而不是完全依赖弹层。
+3. 为物资卡补充更精细的分类过滤和排序逻辑。
+4. 引入可配置的 HUD 主题参数,支撑不同章节或区域切换皮肤。
diff --git a/docs/generated/tinywaste-hud-reference.png b/docs/generated/tinywaste-hud-reference.png
index 056d5f6..20def0a 100644
Binary files a/docs/generated/tinywaste-hud-reference.png and b/docs/generated/tinywaste-hud-reference.png differ
diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts
index 5dd15c7..28094a4 100644
--- a/packages/content/src/index.ts
+++ b/packages/content/src/index.ts
@@ -1070,6 +1070,30 @@ export const gameContent: GameContent = {
},
],
},
+ road_wanderer: {
+ id: 'road_wanderer',
+ title: '路边的求助声',
+ text: '一辆翻覆的车旁边,有人虚弱地问你有没有水。',
+ trigger: 'travel',
+ options: [
+ {
+ id: 'wanderer_share',
+ text: '给他一瓶浑水',
+ conditions: [{ kind: 'has-item', itemId: 'water_dirty', count: 1 }],
+ successEffects: [
+ { type: 'remove-item', itemId: 'water_dirty', count: 1 },
+ { type: 'add-item', itemId: 'herb_bundle', count: 1 },
+ ],
+ successText: '那人把最后一束干草药塞给了你。',
+ },
+ {
+ id: 'wanderer_leave',
+ text: '继续赶路',
+ successEffects: [{ type: 'change-stat', stat: 'sanity', amount: -2 }],
+ successText: '你没有回头,但那声"算了"跟了你一路。',
+ },
+ ],
+ },
},
enemies: {
rat_swarm: {
@@ -1217,29 +1241,4 @@ export const gameContent: GameContent = {
travelEventPool: ['road_wanderer'],
};
-gameContent.events.road_wanderer = {
- id: 'road_wanderer',
- title: '路边的求助声',
- text: '一辆翻覆的车旁边,有人虚弱地问你有没有水。',
- trigger: 'travel',
- options: [
- {
- id: 'wanderer_share',
- text: '给他一瓶浑水',
- conditions: [{ kind: 'has-item', itemId: 'water_dirty', count: 1 }],
- successEffects: [
- { type: 'remove-item', itemId: 'water_dirty', count: 1 },
- { type: 'add-item', itemId: 'herb_bundle', count: 1 },
- ],
- successText: '那人把最后一束干草药塞给了你。',
- },
- {
- id: 'wanderer_leave',
- text: '继续赶路',
- successEffects: [{ type: 'change-stat', stat: 'sanity', amount: -2 }],
- successText: '你没有回头,但那声“算了”跟了你一路。',
- },
- ],
-};
-
export default gameContent;
diff --git a/scripts/slice_hud_atlas.py b/scripts/slice_hud_atlas.py
new file mode 100644
index 0000000..575dda0
--- /dev/null
+++ b/scripts/slice_hud_atlas.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+
+from __future__ import annotations
+
+import argparse
+from pathlib import Path
+
+from PIL import Image
+
+
+WHITE_THRESHOLD = 245
+GREEN_KEY = (0, 255, 0)
+
+
+def is_gutter(sample: list[tuple[int, int, int, int]]) -> bool:
+ white_pixels = 0
+ opaque_pixels = 0
+
+ for red, green, blue, alpha in sample:
+ if alpha == 0:
+ continue
+ opaque_pixels += 1
+ if red >= WHITE_THRESHOLD and green >= WHITE_THRESHOLD and blue >= WHITE_THRESHOLD:
+ white_pixels += 1
+
+ if opaque_pixels == 0:
+ return False
+
+ return white_pixels / opaque_pixels >= 0.92
+
+
+def find_segments(mask: list[bool]) -> list[tuple[int, int]]:
+ segments: list[tuple[int, int]] = []
+ start: int | None = None
+
+ for index, is_gap in enumerate(mask):
+ if not is_gap and start is None:
+ start = index
+ if is_gap and start is not None:
+ segments.append((start, index))
+ start = None
+
+ if start is not None:
+ segments.append((start, len(mask)))
+
+ return segments
+
+
+def detect_grid(image: Image.Image) -> tuple[list[tuple[int, int]], list[tuple[int, int]]]:
+ rgba = image.convert('RGBA')
+ width, height = rgba.size
+ pixels = rgba.load()
+
+ column_mask = []
+ for x in range(width):
+ sample = [pixels[x, y] for y in range(height)]
+ column_mask.append(is_gutter(sample))
+
+ row_mask = []
+ for y in range(height):
+ sample = [pixels[x, y] for x in range(width)]
+ row_mask.append(is_gutter(sample))
+
+ columns = [segment for segment in find_segments(column_mask) if segment[1] - segment[0] > 48]
+ rows = [segment for segment in find_segments(row_mask) if segment[1] - segment[0] > 48]
+
+ return columns, rows
+
+
+def remove_chroma(image: Image.Image) -> Image.Image:
+ rgba = image.convert('RGBA')
+ cleaned = Image.new('RGBA', rgba.size)
+
+ for x in range(rgba.width):
+ for y in range(rgba.height):
+ red, green, blue, alpha = rgba.getpixel((x, y))
+ if alpha == 0:
+ cleaned.putpixel((x, y), (0, 0, 0, 0))
+ continue
+
+ dominant_green = green - max(red, blue)
+ color_distance = abs(red - GREEN_KEY[0]) + abs(green - GREEN_KEY[1]) + abs(blue - GREEN_KEY[2])
+
+ if color_distance <= 60:
+ cleaned.putpixel((x, y), (0, 0, 0, 0))
+ elif green > 120 and dominant_green > 26:
+ despilled_green = min(green, max(red, blue) + 10)
+ new_alpha = max(0, min(alpha, 255 - dominant_green * 2))
+ if new_alpha <= 12:
+ cleaned.putpixel((x, y), (0, 0, 0, 0))
+ else:
+ cleaned.putpixel((x, y), (red, despilled_green, blue, new_alpha))
+ else:
+ cleaned.putpixel((x, y), (red, green, blue, alpha))
+
+ return cleaned
+
+
+def trim_alpha(image: Image.Image, padding: int = 8) -> Image.Image:
+ bbox = image.getbbox()
+ if bbox is None:
+ return image
+
+ left = max(0, bbox[0] - padding)
+ top = max(0, bbox[1] - padding)
+ right = min(image.width, bbox[2] + padding)
+ bottom = min(image.height, bbox[3] + padding)
+ return image.crop((left, top, right, bottom))
+
+
+def save_cells(image: Image.Image, names: list[str], output_dir: Path) -> list[str]:
+ columns, rows = detect_grid(image)
+ expected = len(columns) * len(rows)
+ if expected != len(names):
+ raise ValueError(
+ f'Grid detection found {len(columns)} columns x {len(rows)} rows = {expected} cells, '
+ f'but {len(names)} names were provided.'
+ )
+
+ written: list[str] = []
+ index = 0
+ for row_start, row_end in rows:
+ for col_start, col_end in columns:
+ cell = image.crop((col_start, row_start, col_end, row_end))
+ cleaned = trim_alpha(remove_chroma(cell))
+ target = output_dir / f'{names[index]}.png'
+ cleaned.save(target)
+ written.append(target.name)
+ index += 1
+
+ return written
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description='Slice a HUD atlas with white gutters and green chroma background.')
+ parser.add_argument('--input', required=True, type=Path)
+ parser.add_argument('--output-dir', required=True, type=Path)
+ parser.add_argument('--names', required=True, help='Comma-separated output filenames without extension.')
+ args = parser.parse_args()
+
+ names = [name.strip() for name in args.names.split(',') if name.strip()]
+ args.output_dir.mkdir(parents=True, exist_ok=True)
+
+ atlas = Image.open(args.input)
+ written = save_cells(atlas, names, args.output_dir)
+ print('\n'.join(written))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tmp/hud-audit/current-hud.png b/tmp/hud-audit/current-hud.png
new file mode 100644
index 0000000..605bef5
Binary files /dev/null and b/tmp/hud-audit/current-hud.png differ
diff --git a/tmp/hud-audit/hud-compare-triptych.png b/tmp/hud-audit/hud-compare-triptych.png
new file mode 100644
index 0000000..143217c
Binary files /dev/null and b/tmp/hud-audit/hud-compare-triptych.png differ
diff --git a/tmp/hud-audit/hud-diff.png b/tmp/hud-audit/hud-diff.png
new file mode 100644
index 0000000..2ce6341
Binary files /dev/null and b/tmp/hud-audit/hud-diff.png differ