Compare commits

..
30 Commits
Author SHA1 Message Date
shenlei 1db9e9a129 no message 2026-09-16 22:15:52 +09:00
shenleiandClaude Opus 5 400e3e8ea5 Merge classic enter-game framing fixes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-12 21:29:22 +09:00
shenleiandClaude Opus 5 85ce75e707 fix(net): 修复 40250 进入游戏时的收包错位
- GC_ITEM_DEL(20) 按 40250 旧结构为 42 字节,GC_REFINE_INFORMATION(95) 为 59 字节
- 公会标记连接接受 GC_MARK_DIFF_DATA(101),按 1 字节消费
- GUILD_SUBHEADER_GC_SKILL_INFO 服务器声明 22 字节但实际只写 21 字节,
  按实际长度分帧,修复 "unknown GC header 200 (last: 75,20)" 断线
- 会话测试的 quest info 包改为 40250 按 flag 定长的格式,新增对应分帧测试

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-12 21:29:21 +09:00
shenlei 474a709f69 Merge combat 40250 parity fixes 2026-09-12 13:15:28 +09:00
shenleiandClaude Opus 5 1933a5ceda fix(combat): 战斗逐项对齐 40250,修复移动 / 技能释放与武器挂点
- 连击类型由 combo_changed 驱动(SetComboSkillFlag,技能 122 等级)
- 命中判定改为 .msa 刀尖 Z 圆柱 vs .msm 防御球(hit_collision.gd 逐行移植)
- _register_hit 改为 map::insert 语义,修正命中上限
- 攻速同时缩放 GetAttackingElapsedTime 与攻击动作速率;按武器动作模式目录绑定攻击段
- 去掉本地连击超时,motion_bound 清命中表 / 连击段号
- __ProcessDataAttackSuccess:InsertDelay 硬直、physics_push.gd 击退 + GetBlendingPosition 同步、
  命中特效、__HitGood / __HitGreate / __HitStone 受击动作链与抖动(ui/hit_reaction.gd)
- ClassicSession::send_use_skill 不再夹带 CG_FLY_TARGETING
- mac 前进 / 后退方向与技能释放修复;武器按职业骨骼挂到手上
- 新增 hit_collision / physics_push / hit_view / net_world_push / weapon_attach 测试;
  gpu_pose_bounds / race_motion_assembly 适配 damage 随机变体
- 文档:CLIENT-GAP.md、CLIENT-GAP-FIX.md §3.5 / §3.7 与 C.5 增量 130

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-12 13:15:28 +09:00
shenandshen 96c29d2bb7 Merge playable STB-01 validation fixes 2026-09-11 21:23:52 +08:00
shenandshen cc5573ddc1 fix(playable): close release validation lifecycle gaps 2026-09-11 21:23:44 +08:00
shenleiandClaude Opus 5 f39a55fdd5 feat(playable): 首个 Mac 联网内测版 STB-01 soak 工具与发布状态文档
- 新增 script/playable_soak.sh:先做 soak 配置/资源校验,无 --allow-gameplay
  只校验不启动客户端;N(>=10) 次独立正常退出运行 + 墙钟 soak,失败即停止后续
  运行并记 BLOCKED;写本批次 release-manifest.json 后聚合
- run_client_gate.sh:确认的故障代理对所有 suite 启动(共享场景的退出运行也经代理);
  soak 超时下限只约束 soak 客户端(validate_soak 增加 soak_client 参数)
- 新增本地 127.0.0.1 故障代理、RSS 采样/内存判定、窗口/指标/流程模块及其测试
- forest_mob_render_test 输出 PASS/FAIL 标记,供 rendering_batch_test.sh 识别
- 新增 docs/FIRST-MAC-PLAYABLE-STATUS.md:如实记录 PASS/BLOCKED、已知问题与环境需求
- .gitignore 排除本地场景配置、凭据文件与运行输出

离线回归:rendering_batch_test.sh failures=0,playable_gate_test.sh PASS,
node 夹具测试 PASS。未联网运行,未重建候选包。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-11 22:06:48 +09:00
shenandshen 374d4165d8 Implement playable Mac client and rendering validation 2026-09-11 14:58:22 +08:00
shenandshen 1ab68bd06e Merge branch 'main' of http://192.168.21.200:8418/4v5u09Z5a4Yuc/metin2-client
# Conflicts:
#	docs/RENDERING-REPAIR-PLAN.md
2026-09-08 16:25:54 +08:00
shenleiandClaude Opus 5 e0c1853909 fix(net): 主角 VID 归参考端所有,GC_CHARACTER_DEL 不再把玩家「去主角化」
实机故障:进入游戏后整屏灰紫,主角还是蓝色胶囊占位体,2D UI 与小地图正常。

根因(已实证,非推测)。`EntityStore::mut_despawn()` 在 despawn 的 vid 等于
主角 vid 时把 `m_main_vid` 清零。40250 服务器在 spawn-in 头几秒会对玩家自己的
vid 反复下发 GC_CHARACTER_DEL + GC_CHARACTER_ADD(视野重算,实测约 10 次)。
第一次 DEL 就把玩家永久去主角化:`get_main_vid()` 返回 0 →
`_sync_main_character()` 直接 return → 建模代码从未跑过(main_attempts=0)→
占位胶囊留在 y=0 → 相机跟着它钻到 C1 地形底下 → 满屏灰紫。地图其实一直是好的
(chunks_built 20/20、objects_placed 565)。

参考端怎么做(40250 ClientVS22)。主角身份属于 `CNetworkActorManager::m_dwMainVID`,
只由 `SetMainActorVID()`(GC_MAIN_CHARACTER)写,只在管理器析构时清;
`RemoveActor()` 删 actor 行和角色实例,但绝不碰它;重新加入时
`__AppendCharacterManagerActor()` 用 `kCreateData.m_isMain = __IsMainActorVID(dwVID)`
重新推导,并再跑一次 `SetMainCharacterIndex(dwVID)`。

修法(entity_store.cpp):
- `mut_despawn()` 只删行、只发 Despawn,不动 m_main_vid;m2dev 分支的
  GC_CHARACTER_DEL 同规则。
- `mut_spawn_full()` 在 `e.vid == m_main_vid` 时把 `is_main` 置回 true,
  并重新广播 MainSet,对齐参考端的重新推导。

顺带落地的渲染修复(同一次实机排查中定位):
- libgr2 `sample_pose()` 增加 `root_offset` 形参;`Metin2AnimPlayer::apply_pose()`
  显式传单位阵而不是模型的 InitialPlacement。Metin2 动画 gr2 的根骨轨道自带骨盆
  高度,再乘一次 IP 会把整个人抬高一个骨盆高(武 99.4cm / 刺 92.6 / 巫 103.7 /
  萨 95.5,正好各自的 IP.z)——即此前的「人物悬空」。`bind_pose()` 语义不变。
- `Metin2Model::_load_hair()` 改用与身体/武器同一条 `make_material()`
  (specular_disabled)而不是 roughness=1.0 的 StandardMaterial3D,头发不再被
  PBR 高光洗成奶白色块。

取证与回归工具:
- `project/net_trace.gd`(新):主角 vid 生命周期取证。挂在 AppFlow 之后逐帧读
  `get_main_vid()`,归零时打印 `NETTRACE: MAIN_LOST was=.. now=0 reason=..` 加最近
  24 条环形事件;`reason` 区分 despawn 与 silent(reset_for_map_change 清空
  m_changes,是静默清零)。`MT_NETTRACE=1` 打开逐事件日志。
- `game_scene.gd`:诊断 tick 支持重复采样(分辨主角装配是「没跑」还是「没跑完」),
  新增 main_sync_ready / main_loading / main_view_key / main_map_vid /
  main_retry_in_ms 字段;`MT_DIAG_SHOT=<png>` 让游戏自己存一帧实机画面
  (screencapture 拍到的是终端窗口,没用)。
- `project/model_render_test.gd` + `test/rendering/model_baseline.json`(新):
  八个种族的模型渲染基线(--bless 重新落盘)。
- `project/char_bench_test.gd`(新):无网络无地图的单角色实验台,五变体 ×
  六机位出证据图,只产图不做阈值判定。
- `project/package_render_test.gd`(新):导出包内的渲染自检入口
  (导出模板不接受 --script,只能走 MT_TEST_MODE)。
- `/project/build/` 加入 .gitignore(渲染用例的落图输出)。

实机复验(真实服务器,非模拟):
- 修前 `LIVE_SMOKE FAIL: 主角色 VID 有效`;
  `NETTRACE: MAIN_LOST was=25910 now=0 reason=despawn t=+7737ms`,
  环里紧跟 `spawn vid=25910 is_main=false`。
- 修后 `LIVE_SMOKE RESULT: PASS`,main_vid=25933、entities=19;
  40s 实机跑 16 次诊断采样全部 model_built=True / main_attempts=1 /
  main_sync_ready=True,player_pos 落在 C1 地形上而非 y=0;
  MT_DIAG_SHOT 存下的帧里地形、建筑、NPC、树木、真实角色模型、HUD、小地图俱全。

回归:gamescene_test / equip_model_test / gamescene_visual_test(feet_gap
0.001373)/ model_render_test(8 races)/ char_select_visual_test / 包内
MT_TEST_MODE=render 全绿;ctest 22/23,唯一失败 net.classic_session 已用
「还原本次改动 → 重跑 → diff 失败集合」确认与本次修改无关(失败集合完全相同)。

注:画面整体偏暗属于光照/色调差距,受 PARITY-GAP.md §0 门禁约束(对照帧未落盘前
不做相机/光照/色调/材质调参),本次不碰。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJugvEJwz3FK4hw9ti3SRb
2026-09-08 17:15:55 +09:00
shenleiandClaude Opus 5 fb4d2d222b docs: 文档审计——修正失效链接与过期结论
按 docs/ 目录逐篇核对,只改与事实/路径不符的部分,不动尚未验证的计划条目:

- `docs/steps/**` 在早前整理时已迁到 `docs/reference/steps/**`,全仓的旧路径
  引用(PLAN.md / README.md / M0-gr2-reader.md 及 libgr2 的头注释)一并改正。
- CLIENT-GAP.md / CLIENT-GAP-FIX.md / BACKLOG.md / MIDREVIEW.md:把已经落地的
  条目从「待办」改为已完成,删掉与代码现状矛盾的描述。
- ANDROID-TESTING.md / CLIENT-PORT.md / CLIENT-ROADMAP.md / GODOT-POC-PLAN.md /
  SHINSOO-WORLD-RENDERING.md / PARITY-GAP.md:同上,另补当前实际的构建/测试入口。
- THIRD-PARTY.md:补齐实际在用的第三方来源与许可说明。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJugvEJwz3FK4hw9ti3SRb
2026-09-08 17:15:17 +09:00
shenandshen cea79fe17c fix(rendering): complete character visual regression fixes 2026-09-08 13:26:42 +08:00
shenandshen c3a6fb973e fix(rendering): checkpoint main-character recovery and visual regression work
Handle late main-character data, guarded model construction, bounded retries and map correction. Validate player model sources and add lifecycle and real-asset visual regressions.

Include pending material and character-selection changes, updated A1 screenshot, and the detailed rendering repair plan. Hair occlusion, full UI parity and final macOS package acceptance remain unfinished.
2026-09-08 08:52:30 +08:00
shenleiandClaude Code 8092ff44e2 装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排,
  新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS),
  帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案
- 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、
  物品槽失配置灰(服务器回收/换武器后刷新)
- 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、
  被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1)
- 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/
  near/far/divorce 命令事件→在线/在身边状态与离婚隐藏
- 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示;
  仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close;
  仓库金钱经核实 40250 无 wire 协议,UI 定界只读
- app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫
- 相机 look_at 零向量守卫(首帧/传送落点)
- 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为
  HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路)
- docs/CLIENT-GAP.md 同步增量 129

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
2026-09-07 21:43:30 +09:00
shenandshen 61272b75e9 客户端问题修改 2026-09-07 14:53:36 +08:00
shenlei b19d5b5dd3 feat: complete mobile UI implementation 2026-09-04 19:54:33 +09:00
shenlei 9a4a044da5 Complete remaining client gap features 2026-09-04 13:25:05 +09:00
shenlei 13ccb8d02d 完善客户端功能并同步差距文档 2026-09-03 18:40:01 +09:00
shenleiandClaude Sonnet 5 f93a172296 40250 classic: G1 integrate W4 (§9.1 map BGM selection, §9.2 background volume save/restore)
Doc conflicts resolved by W0: all four 增量 50 change-log bullets kept, W4 C.5
batch record appended.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 16:13:17 +09:00
shenleiandClaude Sonnet 5 804744c28f 40250 classic: G1 integrate W3 (§8.3 in-game help window)
Doc conflicts resolved by W0: kept all four 增量 50 change-log bullets and
appended the W3 C.5 batch record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 16:10:48 +09:00
shenleiandClaude Sonnet 5 61ddf791fa 40250 classic: G1 integrate W2 (§2.1 data-layer EntityStore + net_world visibility, §2.3/§2.4/§2.5)
Doc conflicts in CLIENT-GAP.md / CLIENT-GAP-FIX.md resolved by W0: keep both
增量 50 W1 + W2 change-log bullets and C.5 batch records.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 16:08:59 +09:00
shenleiandClaude Sonnet 5 ce715c7c38 40250 classic: G1 integrate W1 (§1.5 client version, §1.10 text codec, §2.2 two-packet merge)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 16:07:17 +09:00
shenleiandClaude Sonnet 5 3487df2a16 40250 classic W2: EntityStore data-layer only + net_world visibility (增量 50)
CLIENT-GAP-FIX §2.1/§2.3/§2.4/§2.5 (+ §2.2 steps 4-5):

- §2.1 two-layer model: EntityStore is data-only. Entries are created only by
  mut_spawn / mut_spawn_full / mut_spawn_main and removed only by mut_despawn /
  main-vid table clear. Every other mut_* now does find-or-return with a
  MT_NET_TRACE-gated debug log; no pending / replay buffer. Visibility moves to
  net_world.gd::_update_visibility() — per frame it walks client.get_entities()
  and mirrors is_main / AFFECT_SHOW_ALWAYS(bit 7) / IsWall(14201-14204) /
  planar-distance < CHAR_STAGE_VIEW_BOUND (200*100 cm), building a node on entry
  and fading + freeing it past +10 while keeping the data row (_fading guard).
  Gate is fail-open when the data layer is not queryable (no get_entities /
  _main_vid == 0 / no pos_cm) so existing fake-client tests are unaffected.
- §2.3 one overwrite rule: mut_spawn_full() does a clean whole-row value replace
  (m_ents[vid] = e), no preservation of prior move state on VID reuse; mut_spawn()
  now delegates to it. net_world._on_spawn destroys + recreates a node when the
  vid already has one; mount/dismount rebuild keeps the old global_position.
- §2.4 points bounds: mut_set_points() clamps counts > POINT_MAX_NUM (255) and
  zeroes the tail of a short snapshot; mut_set_point() rejects type >= 255.
- §2.5 field apply order + affect flags: new EntityStore::mut_affect_flags(vid,
  lo, hi) assembles v = lo | (hi << 32) and queues a dirty tick; mut_char_update
  guild/alignment/pk changes now also push Info. net_world._apply_field_updates()
  applies fields in the reference call order (NetworkActorManager.cpp:470).
- §2.2 steps 4-5: mut_char_info() / mut_shop_sign() drop the touch()-creates-
  entity path. Steps 1-3 (m_pending_actor two-packet merge) remain W1-owned.

Tests: ./build.sh Debug exit 0; ctest 16/16 (net.entity_store gains a §2.1
data-layer assertion block). New project/net_world_vis_test.gd for scene-node
visibility. GDScript regressions green: netbridge_test, gamescene_test,
netplay_test, p9_test, combat_fx_test, player_motion_test, net_world_vis_test.

Follow-up for W1: call mut_affect_flags(vid, affect_flag[0], affect_flag[1])
from classic_parser.cpp after GC_CHARACTER_ADD[2] / GC_CHARACTER_UPDATE
(mut_char_update has no affect param). wire_classic.h already has the wanted
static_assert on GCPoints.points.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 16:06:06 +09:00
shenleiandClaude Sonnet 5 b296457e3c 40250 classic: increment 50 (Phase 1 W1) — §1.5 client version, §1.10 text codec, §2.2 two-packet merge
§1.5 CG_CLIENT_VERSION: send_client_version() now branches on locale. EUROPE
(this project's locale) sends CGClientVersion2 (header 0xf1, 67B, timestamp
"1215955205"); non-EUROPE sends CGClientVersion (0xfd) + CMake-injected
MT_BUILD_TIMESTAMP in C __TIMESTAMP__ form. New ClassicSession::set_executable_name()
(default "metin2.bin"; TODO(W0): M2Client passes OS::get_executable_path().get_file()).
m_version_sent set only after send_fixed() succeeds. wire_classic.h gains
CGClientVersion2 + size-table entry.

§1.10 fixed-length text: new extension/src/net/text_codec.h — to_wire()/
from_wire_str() apply a hard byte cap without splitting a UTF-8 multibyte
sequence; godot::String overloads are __has_include-guarded so mtnet stays
godot-free. classic_session.cpp routes 8 fixed-length name/sign/comment fills
through to_wire(); parser reads GC_CHAR_ADDITIONAL_INFO name via from_wire_str().
New net_text_codec_test (ctest net.text_codec). Locale codepage (CP949/CP1252)
transcoding remains a follow-up; wire bytes are currently UTF-8 (ASCII round-trips).

§2.2 two-packet PC/NPC merge: ClassicParser GC_CHARACTER_ADD drops invisible
races (20025/20038/20039); PC/NPC only stash a bare Entity into m_pending_actor;
everything else spawns immediately with a name from root/npclist.txt (new
npc_names.h + set_npclist_path(); $MT_ASSETS/$M2_ASSETS fallback; TODO(W0):
M2Client passes the asset root). GC_CHAR_ADDITIONAL_INFO with no pending entry
records m_last_error + m_unhandled_headers and drops (never half-creates);
on a hit it merges the pending Entity with the additional-info's 9 fields into
one mut_spawn_full() then erases the pending entry. New pending_actor_count()
test hook; net_classic_session_test updated (version assertions -> 0xf1;
stash -> merge -> spawn, immediate monster spawn, invisible-race drop,
no-pending additional-info is a no-op). Depends on W2 §2.2 steps 4-5
(entity_store mut_char_info touch / mut_shop_sign cleanup) — the merge works
with current W2 code as-is.

Build + ctest 17/17 (16 + net.text_codec); netbridge_test, p10_test green.
Docs: CLIENT-GAP.md W1 row + increment-50 change-log entry; CLIENT-GAP-FIX.md
§1.5/§1.10/§2.2 status + facts and C.5 batch record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 15:51:48 +09:00
shenleiandClaude Sonnet 5 d7711ba5f0 40250 classic: increment 50 (Phase 1 W3) — §8.3 in-game help window
Implement the in-game Help window as a 1:1 migration of uihelp.py's
HelpWindow (single-page: ENABLE_HELP_MULTIPAGE = 0 -> LoadDialogSinglePage).

- project/ui/help_ui.gd (new): loads the real helpwindow.py uiscript via
  UiScript -> UiBuild; _relabel() resolves the <HELP_*>/<CLOSE> placeholders
  (produced when UiManager has no locale callback) from the project Locale
  table (en) with verbatim English fallbacks, covering the original script's
  two duplicate help_02 rows (right/middle camera). open/close/toggle/is_open;
  close_button -> close; ESC handled by the UiManager window stack.
- project/ui/system_menu_ui.gd: help_button changed from the placeholder toast
  to close() + open_help(); adds const HelpUI, an injectable help_ui member,
  _ensure_help() (lazy build, reuses the menu's ui/client/assets_root),
  open_help(), toggle_help(); setup() gains an optional 6th arg (old 5-arg
  callers unaffected).
- project/game_scene.gd: single elif in _unhandled_input — KEY_H without
  ctrl/meta -> system_menu_ui.toggle_help() (Ctrl+H stays free for
  /user_horse_ride).
- project/help_ui_test.gd (new): headless SceneTree test — helpwindow.py load,
  19 verbatim help rows + taskbar labels bound and non-placeholder,
  close_button text, rows() helper, close/open/toggle, ESC stack top,
  SystemMenuUI.open_help() lazy build.

Tests: help_ui_test, system_menu_ui_test, gamescene_test, ui_test all PASS
(plus system_option_ui_test, char_status_ui_test). No build/ in this worktree
so C++ ctest not run (no C++ change). Real-server and visual (.sub textures)
not verified. Docs: CLIENT-GAP.md W3 row + increment 50 bullet;
CLIENT-GAP-FIX.md §8.3 status/改动/测试/验收 + C.5 batch record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 15:34:36 +09:00
shenleiandClaude Sonnet 5 9732f2799e 40250 classic: increment 50 (W4) — map BGM selection + background volume save/restore
§9.1 map BGM selection (bgm_director.gd):
- server bgm_changed(name, volume): non-empty name plays it as field music;
  empty name falls back to musicInfo.fieldMusic (METIN2THEMA = "M2BG.mp3")
- volume < 0 (server unspecified, GC_MAIN_CHARACTER3_BGM) -> default
  1/5*0.1 = 0.02; otherwise server volume; volume applied before play_bgm()
  fade-in (matches game.py:190 SetMusicVolume/SetSoundVolume then FadeInMusic)
- connect M2Client.world_reset -> re-run selection on map change
- new musicinfo.gd: METIN2THEMA/loginMusic/createMusic/selectMusic/fieldMusic
  constants + save/load_last_play_field_music -> user://BGM/lastplay.inf

§9.2 background/foreground volume save-restore:
- audio.gd: save_volume()/restore_volume() (backup fields echo reference
  m_fBackupMusicVolume / m_fBackupSoundVolume) + set_music_volume()
- app_lifecycle.gd: _enter_background() uses save_volume() + mute instead of
  stop_bgm(); _exit_background() calls restore_volume(); _bgm_name never cleared

§9.3 lifecycle ownership: DOC ONLY — design confirmed, deferred to Phase 2 / W0
(AppFlow sole lifecycle owner; M2Client drops NOTIFICATION_* keeps suspend/resume).
game_scene.gd / app_flow.gd / m2_client.* left untouched per workflow constraints.

Tests: bgm_test.gd (new) PASS; gamescene_test / netplay_test / p9_test green.
Docs: CLIENT-GAP.md increment 50 bullet + W4 progress row; CLIENT-GAP-FIX.md
§9.1 §9.2 状态/改动/测试/验收, §9.3 deferred note, C.5 batch record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 15:31:58 +09:00
shenleiandClaude Sonnet 5 f32064c74a 40250 classic: parallel-dev checkpoint through increment 49 (W0 interface freeze)
Captures the uncommitted parallel-development work (increments 46-49) on the
40250 classic client port, folded into docs/CLIENT-GAP.md + CLIENT-GAP-FIX.md.

Increment 49 (W0 interface-freeze batch, Phase 1 prerequisite) lands the
cross-workflow shared-file scaffolding so the 4 Phase 1 workflows can run in
isolated worktrees without contention:

- entity_store.{h,cpp}: Entity gains empire/affect_flags/owner_vid/state_flags;
  new mut_spawn_full() (single-shot two-packet merge, §2.3), mut_ownership()
  (§2.5), mut_map_bgm() + take_bgm_dirty()/bgm_name()/bgm_volume() (§9.1),
  drain_dirty() (§2.1/§2.5 bare-field-update queue); mut_char_info() now stores
  empire; reset_for_map_change() clears m_dirty.
- m2_client.cpp: new bgm_changed(name, volume) signal; classic + m2dev pump
  loops drain drain_dirty() -> entity_info and take_bgm_dirty() -> bgm_changed;
  entity_dict() exposes the 4 new keys.
- classic/classic_parser.{h,cpp}: GC_MAIN_CHARACTER3_BGM /
  GC_MAIN_CHARACTER4_BGM_VOL route bgm_name/bgm_volume to mut_map_bgm() (was
  discarded); new m_pending_actor staging map (W1 fills §2.2 merge logic).
- project/bgm_director.gd (new) + game_scene.gd: BGM consumption split out of
  net_world.gd so W2/W4 don't collide; §8.6/§4.8 keybind convergence (digits
  1-4 -> quickslots 0-3, F1-F4 -> quickslots 4-7, Ctrl+1..9 -> _emote()).

Tests: cmake --build build clean; ctest 16/16; 15 GDScript regressions green
(netbridge, gamescene, netplay, p9, p10, p2b, p8, system_menu_ui, skill,
combat_fx, skill_fx, player_motion, char_status_ui, chat, inventory), no skips.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 15:13:01 +09:00
shenlei 70f200e885 Merge remote-tracking branch 'origin/main' into feat/40250-classic-net 2026-09-02 09:13:00 +09:00
shenlei 519311f8fe Ignore classic build artifacts 2026-09-02 09:11:44 +09:00
578 changed files with 63456 additions and 1875 deletions
+14
View File
@@ -5,8 +5,11 @@
# build
/build/
# 渲染测试的落图输出(test/rendering 用例自建)
/project/build/
/build-ios/
/build-android/
/build-classic/
/cmake-build-*/
compile_commands.json
@@ -32,6 +35,17 @@ compile_commands.json
*.tmp
*.o
*.a
.env.live-smoke.local
# playable network tests: local scenario/fixture configs, credential files and
# run outputs never enter git. Only scenario.example.json and the static gate
# fixtures in test/playable are committed. Run outputs live under /build/playable.
/test/playable/*.local.json
/test/playable/*.local.txt
/test/playable/runs/
*.credentials
*.credentials.json
.env.*.local
# clangd
/.cache/
@@ -0,0 +1,56 @@
---
sketch: 001
name: mobile-hud
question: "暗黑系 ARPG 横屏战斗 HUD 如何兼顾 MMORPG 的完整系统入口?"
winner: "single"
tags: [mobile, landscape, hud, touch, combat, arpg, mmo, dark-glass]
---
# Sketch 001: Mobile HUD
## Design Question
在不遮挡 Godot 游戏世界的前提下,如何用暗黑系 ARPG 的横屏战斗框架,让移动、普通攻击、技能、物品、拾取/对话、任务、地图、聊天、队伍、公会以及背包等系统保持易达?本轮重点比较圆形战斗控件、深色金属边框、职业资源条与低频功能收纳方式。
## How to View
直接打开:
`open .planning/sketches/001-mobile-hud/index.html`
也可以使用文件链接打开 [index.html](/Users/shenlei/Work/mt/mtgodot-poc/.planning/sketches/001-mobile-hud/index.html)。
页面无需构建工具。草图以横屏为基准:手机为 844×390,平板和桌面为 16:9。右下角工具栏切换手机/平板/桌面预览和标注模式;首页下方的“全量 UI 移动端交互图”支持按模块筛选,点击节点可查看入口、触控方式、页面形态和返回路径。
## Single Direction
- **暗黑 ARPG 横屏 HUD** — 左上按“名称→头像→HP/MP/资源短条”纵向排列,顶部中央目标条,右上按“背包→小地图→菜单”排列,地图通过点击小地图进入;左下摇杆、右下环形技能盘,底部中央不放功能栏。角色、装备、技能、坐骑和表情只通过左上头像进入;菜单仅保留“社交、成长、活动、商业、系统”五个一级入口。
## Covered Functions
移动摇杆、相机/地图入口、玩家状态、目标、普攻、技能、药水/物品、拾取、对话、钓鱼、副本入口、任务、背包、角色、技能、地图、队伍、公会、好友、聊天、情侣/爱意、商店、商城、仓库、私人商店、交易、兑换、Cube 制作、精炼、龙魂、坐骑、观战、表情、系统、帮助、登出、角色切换、死亡复活、加载和断线重连。
## Interaction Maps
- 进入游戏:登录 → 服务器/频道 → 角色列表 → 加载 → 首页 HUD。
- 战斗:移动摇杆 → 目标选择 → 普攻/技能 → 掉落/场景交互 → 死亡/复活。
- 任务:任务栏 → 任务日志 → 小地图/大地图 → 自动寻路 → NPC 对话 → 副本结算。
- 角色与物品:头像/菜单 → 角色/装备/技能,以及背包 → 物品详情 → 使用/装备/出售 → 仓库。
- 社交:右上菜单 → 社交 → 聊天/好友/情侣/公会;组队后队员状态固定在左上头像下方,点击队员查看资料或定位。
- 经济:NPC/玩家 → 商店/商城/交易/私人商店 → Cube 制作 → 精炼/龙魂。
- 系统状态:右上菜单 → 设置/帮助/角色切换/登出,以及死亡、加载、断线重连等状态流程。
## What to Look For
- 暗黑系圆形战斗盘是否能让右手拇指快速区分普攻、技能和药水。
- 战斗控件是否遮挡角色、敌人、掉落物或任务目标。
- 当前任务条是否足够醒目,点击任务条能否直接进入任务面板。
- 低频功能是否通过菜单一跳到达,同时不让战斗 HUD 变得拥挤。
- 名称是否位于头像正上方,HP/MP/资源短条是否位于头像下方且不抢占战斗画面。
- 右上菜单是否位于最外侧,小地图是否紧挨菜单,背包是否替代原地图按钮。
- 横屏手机布局在平板/桌面预览下是否有足够的延展空间。
- 技能冷却、按钮按压、抽屉打开、聊天发送和摇杆复位反馈是否足够明确。
## Next Decision
当前方向已确定为单一方案,交互图已补齐全部功能入口。下一步可按交互图逐条映射到 Godot Control 节点,并优先实现首页 HUD、任务、背包、菜单和战斗操作。草图只借鉴暗黑系 ARPG 的布局语言,不复用参考图中的原始素材。
@@ -0,0 +1,664 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>移动端 HUD 草图 · 暗黑 ARPG</title>
<link rel="stylesheet" href="../themes/default.css">
<style>
* { box-sizing: border-box; }
:root { color-scheme: dark; }
html, body { margin: 0; min-height: 100%; background: radial-gradient(circle at 50% 0%, #1c3048 0, #0b1018 42rem); color: var(--color-text); font-family: var(--font-sans); }
body { padding: 34px 20px 60px; }
button, input { font: inherit; }
button { color: inherit; cursor: pointer; }
.page-head { max-width: 1040px; margin: 0 auto 24px; }
.eyebrow { color: var(--color-primary); font-size: var(--text-xs); letter-spacing: .18em; text-transform: uppercase; }
h1 { margin: 5px 0; font-size: clamp(1.45rem, 3vw, 2.2rem); }
.page-head p { margin: 6px 0 0; max-width: 720px; color: var(--color-text-muted); line-height: 1.55; }
.variant { max-width: 1040px; margin: auto; }
.variant-intro { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin: 0 auto 14px; max-width: 920px; }
.variant-intro h2 { margin: 0; font-size: var(--text-lg); }
.variant-intro p { margin: 0; color: var(--color-text-muted); font-size: var(--text-sm); }
.device-wrap { display: flex; justify-content: center; }
.device { position: relative; width: min(844px, calc(100vw - 40px)); aspect-ratio: 844 / 390; overflow: hidden; border: 1px solid rgba(221, 238, 255, .45); border-radius: 30px; background: #0a1018; box-shadow: 0 28px 80px rgba(0,0,0,.55), 0 0 0 7px rgba(255,255,255,.035); transition: width .25s ease, aspect-ratio .25s ease, border-radius .25s ease; }
.preview-tablet .device { width: min(1280px, calc(100vw - 40px)); aspect-ratio: 16 / 9; border-radius: 24px; }
.preview-desktop .device { width: min(1440px, calc(100vw - 40px)); aspect-ratio: 16 / 9; border-radius: 18px; }
.scene { position: absolute; inset: 0; overflow: hidden; background: linear-gradient(145deg, rgba(16,39,54,.85), rgba(8,17,25,.88) 48%, rgba(32,22,42,.94)); }
.scene::before { content: ""; position: absolute; inset: -12%; opacity: .55; background: radial-gradient(ellipse at 25% 37%, rgba(95,181,187,.28), transparent 24%), radial-gradient(ellipse at 78% 23%, rgba(144,97,221,.28), transparent 22%), linear-gradient(145deg, transparent 35%, rgba(255,211,108,.08) 36%, transparent 37% 58%, rgba(116,185,255,.08) 59%, transparent 60%); transform: rotate(-7deg); }
.scene::after { content: ""; position: absolute; inset: 0; background: linear-gradient(transparent 57%, rgba(4,8,13,.42) 82%, rgba(4,8,13,.78)); pointer-events: none; }
.terrain { position: absolute; inset: 32% 4% 16%; opacity: .4; background: repeating-linear-gradient(164deg, transparent 0 19px, rgba(173,214,213,.16) 20px 21px), repeating-linear-gradient(22deg, transparent 0 36px, rgba(46,70,82,.42) 37px 39px); clip-path: polygon(0 25%, 25% 4%, 53% 24%, 75% 0, 100% 21%, 100% 100%, 0 100%); }
.hero { position: absolute; left: 48%; top: 40%; width: 48px; height: 82px; transform: translate(-50%, -50%); border-radius: 45% 45% 28% 28%; background: linear-gradient(90deg, #152d44, #b7d7e8 45%, #315375 58%, #101d2d); box-shadow: 0 0 18px rgba(116,185,255,.45); }
.hero::before { content: ""; position: absolute; left: 9px; top: -26px; width: 30px; height: 32px; border-radius: 50%; background: radial-gradient(circle at 40% 42%, #ffe9c5 0 18%, #6f4c3e 19% 54%, #1a2432 55%); }
.hero::after { content: "✦"; position: absolute; right: -29px; top: 6px; color: var(--color-accent); font-size: 28px; text-shadow: 0 0 14px #ffb638; }
.mob { position: absolute; left: 68%; top: 44%; width: 58px; height: 42px; border-radius: 48% 52% 35% 35%; background: linear-gradient(140deg, #2b4155, #8c6672 45%, #201824); box-shadow: 0 0 18px rgba(255,111,120,.2); }
.mob::before { content: ""; position: absolute; left: 13px; top: -17px; width: 34px; height: 22px; border-radius: 55% 45% 35% 35%; background: #5f3442; }
.mob::after { content: "· ·"; position: absolute; left: 9px; top: -13px; color: #ffb1a9; letter-spacing: 6px; font-size: 13px; }
.top-status { position: absolute; z-index: 2; top: 14px; left: 14px; right: 14px; display: flex; align-items: center; gap: 9px; }
.avatar { width: 44px; height: 44px; flex: 0 0 auto; display: grid; place-items: center; border: 1px solid rgba(255,211,108,.78); border-radius: 50%; background: radial-gradient(circle, #658fb6, #182637 62%, #0b121c); box-shadow: 0 0 0 3px rgba(12,20,30,.58); font-size: 20px; }
.status-copy { min-width: 0; flex: 0 0 148px; width: 148px; }
.status-line { display: flex; align-items: center; justify-content: space-between; gap: 7px; font-size: var(--text-xs); }
.status-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.level { color: var(--color-accent); font-family: var(--font-mono); }
.meter { height: 7px; margin-top: 4px; overflow: hidden; border: 1px solid rgba(255,255,255,.18); border-radius: 8px; background: rgba(3,8,13,.72); }
.meter i { display: block; height: 100%; border-radius: inherit; }
.meter.hp i { width: 78%; background: linear-gradient(90deg, #f05767, #ff9d82); }
.meter.mp i { width: 62%; background: linear-gradient(90deg, #4f91f8, #8dc8ff); }
.meter.resource { height: 5px; margin-top: 3px; }
.meter.resource i { width: 68%; background: linear-gradient(90deg, #39217d, #8c55ed 70%, #caa0ff); }
.mini-map { position: relative; width: 74px; height: 74px; flex: 0 0 auto; overflow: hidden; border: 1px solid rgba(213,235,255,.52); border-radius: 50%; background: radial-gradient(circle at 44% 58%, rgba(101,178,161,.5) 0 12%, transparent 13%), linear-gradient(35deg, #17313a 0 38%, #284b58 39% 44%, #17313a 45% 63%, #4c385d 64%); box-shadow: inset 0 0 0 4px rgba(8,14,22,.55), 0 6px 14px rgba(0,0,0,.3); }
.mini-map::before { content: ""; position: absolute; left: 47%; top: 43%; width: 7px; height: 7px; border-radius: 50%; background: var(--color-accent); box-shadow: 0 0 0 4px rgba(255,211,108,.18), 16px -20px 0 -2px var(--color-danger); }
.target-card { position: absolute; z-index: 2; top: 94px; left: 50%; width: 190px; transform: translateX(-50%); padding: 8px 11px; opacity: .92; border: 1px solid rgba(255,111,120,.36); border-radius: 12px; background: rgba(13,19,29,.62); backdrop-filter: blur(12px); box-shadow: var(--shadow-sm); }
.target-card strong { display: block; font-size: var(--text-sm); }
.target-card small { color: #ffabb0; font-size: var(--text-xs); }
.target-card .meter { height: 5px; margin-top: 6px; }
.target-card .meter i { width: 43%; background: linear-gradient(90deg, #e65060, #ffab87); }
.quest-pill { position: absolute; z-index: 2; top: 102px; right: 14px; display: flex; align-items: center; gap: 6px; border: 1px solid rgba(255,211,108,.36); border-radius: 12px; padding: 8px 10px; background: rgba(13,19,29,.54); color: #ffe9ab; font-size: var(--text-xs); backdrop-filter: blur(12px); }
.chat-bubble { position: absolute; z-index: 2; left: 50%; top: 57%; transform: translateX(-50%); max-width: 168px; padding: 7px 10px; border: 1px solid rgba(255,255,255,.23); border-radius: 12px 12px 12px 3px; background: rgba(10,16,24,.72); color: #e3efff; font-size: var(--text-xs); backdrop-filter: blur(10px); }
.chat-bubble::after { content: ""; position: absolute; left: 11px; bottom: -7px; border: 6px solid transparent; border-top-color: rgba(10,16,24,.72); border-left: 0; }
.joystick { position: absolute; z-index: 4; left: 22px; bottom: 116px; width: 110px; height: 110px; border: 1px solid rgba(213,235,255,.24); border-radius: 50%; background: radial-gradient(circle, rgba(116,185,255,.2) 0 28%, rgba(116,185,255,.07) 29% 62%, rgba(10,16,24,.15) 63%); box-shadow: inset 0 0 0 12px rgba(9,17,26,.22); touch-action: none; }
.joystick::before { content: ""; position: absolute; inset: 18px; border: 1px dashed rgba(213,235,255,.24); border-radius: 50%; }
.stick { position: absolute; left: 50%; top: 50%; width: 54px; height: 54px; transform: translate(-50%,-50%); border: 1px solid rgba(213,235,255,.64); border-radius: 50%; background: radial-gradient(circle at 35% 28%, rgba(255,255,255,.32), rgba(73,134,193,.66) 38%, rgba(19,49,75,.88)); box-shadow: 0 5px 12px rgba(0,0,0,.36); transition: transform .12s ease; }
.joystick.is-active .stick { box-shadow: 0 0 0 6px rgba(116,185,255,.14), 0 5px 12px rgba(0,0,0,.36); }
.combat-cluster { position: absolute; z-index: 4; right: 16px; bottom: 116px; width: 156px; height: 156px; }
.skill { position: absolute; display: grid; place-items: center; width: 52px; height: 52px; border: 1px solid rgba(213,235,255,.54); border-radius: 50%; background: radial-gradient(circle at 36% 30%, rgba(255,255,255,.33), rgba(55,88,129,.8) 45%, rgba(10,19,31,.92)); box-shadow: 0 6px 14px rgba(0,0,0,.36); font-size: 20px; transition: transform .15s ease, filter .15s ease; }
.skill:hover, .skill:focus-visible { transform: scale(1.08); filter: brightness(1.25); }
.skill.main { right: 0; bottom: 14px; width: 78px; height: 78px; border-color: rgba(255,211,108,.74); background: radial-gradient(circle at 35% 25%, #ffdf9a, #ad5b4d 42%, #2b1b2e 78%); font-size: 29px; }
.skill.s1 { left: 12px; top: 5px; }
.skill.s2 { left: 52px; top: 52px; }
.skill.s3 { right: 4px; top: 3px; }
.skill.s4 { left: 0; bottom: 10px; }
.skill em { position: absolute; right: -2px; bottom: -2px; display: none; min-width: 18px; padding: 2px 4px; border-radius: 8px; background: #0a111c; color: #f9fcff; font-size: 10px; font-style: normal; }
.skill.cooling { filter: grayscale(.7) brightness(.58); }
.skill.cooling em { display: block; }
.context-action { position: absolute; z-index: 4; right: 23px; bottom: 279px; display: flex; align-items: center; gap: 6px; border: 1px solid rgba(101,225,178,.45); border-radius: 18px; padding: 7px 10px; background: rgba(14,44,46,.72); color: #adffe1; font-size: var(--text-xs); backdrop-filter: blur(10px); }
.utility-btn { min-width: 41px; height: 38px; display: grid; place-items: center; flex: 1; border: 1px solid rgba(213,235,255,.12); border-radius: 12px; background: rgba(116,185,255,.08); font-size: 17px; transition: all .15s ease; }
.utility-btn:hover, .utility-btn.active { border-color: rgba(116,185,255,.5); background: rgba(116,185,255,.21); transform: translateY(-2px); }
.utility-btn span { display: block; margin-top: 1px; color: var(--color-text-muted); font-size: 9px; }
.floating-cooldown { position: absolute; z-index: 5; left: 50%; top: 69%; transform: translate(-50%, -50%); padding: 9px 13px; border: 1px solid rgba(255,211,108,.42); border-radius: 16px; background: rgba(22,18,19,.74); color: #ffe4a0; font-size: var(--text-xs); opacity: 0; transition: opacity .2s ease; }
.floating-cooldown.show { opacity: 1; }
.modal-layer { position: absolute; z-index: 8; inset: 0; display: none; align-items: flex-end; background: rgba(2,6,10,.48); backdrop-filter: blur(6px); }
.modal-layer.open { display: flex; }
.sheet { width: 100%; max-height: 74%; overflow: auto; padding: 17px; border-top: 1px solid rgba(213,235,255,.32); border-radius: 24px 24px 0 0; background: rgba(12,21,33,.95); box-shadow: 0 -16px 42px rgba(0,0,0,.4); transform: translateY(20px); animation: rise .18s ease forwards; }
@keyframes rise { to { transform: translateY(0); } }
.sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.sheet-head h3 { margin: 0; font-size: var(--text-lg); }
.sheet-title-row { display: flex; align-items: center; gap: 8px; }
.close-btn { width: 32px; height: 32px; border: 1px solid var(--color-border); border-radius: 50%; background: var(--color-surface-soft); }
.back-btn { width: 32px; height: 32px; border: 1px solid var(--color-border); border-radius: 50%; background: var(--color-surface-soft); }
.action-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.action-item { min-height: 58px; border: 1px solid rgba(213,235,255,.13); border-radius: 14px; background: rgba(116,185,255,.07); color: var(--color-text); transition: all .15s ease; }
.action-item:hover { border-color: rgba(116,185,255,.5); background: rgba(116,185,255,.16); transform: translateY(-2px); }
.action-item b { display: block; font-size: 20px; }
.action-item span { color: var(--color-text-muted); font-size: 10px; }
.menu-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.menu-item { min-height: 88px; padding: 10px 7px; border: 1px solid rgba(213,235,255,.16); border-radius: 15px; background: linear-gradient(145deg, rgba(116,185,255,.13), rgba(10,16,24,.35)); color: var(--color-text); text-align: center; transition: transform .15s ease, border-color .15s ease, background .15s ease; }
.menu-item:hover, .menu-item:focus-visible { transform: translateY(-2px); border-color: rgba(255,211,108,.72); background: rgba(116,185,255,.23); outline: none; }
.menu-item b { display: block; margin-bottom: 5px; color: var(--color-accent); font-size: 25px; line-height: 1; }
.menu-item span { display: block; font-size: var(--text-sm); }
.menu-item small { display: block; margin-top: 4px; color: var(--color-text-muted); font-size: 9px; line-height: 1.3; }
.sheet-note { margin: 10px 0 0; color: var(--color-text-muted); font-size: var(--text-xs); line-height: 1.4; }
.chat-list { display: grid; gap: 8px; margin-bottom: 10px; }
.chat-line { padding: 8px 10px; border-radius: 12px; background: rgba(255,255,255,.055); font-size: var(--text-sm); }
.chat-line strong { color: var(--color-primary); }
.chat-compose { display: flex; gap: 8px; }
.chat-compose input { min-width: 0; flex: 1; padding: 10px; border: 1px solid var(--color-border); border-radius: 12px; background: rgba(0,0,0,.25); color: var(--color-text); outline: none; }
.send-btn { padding: 0 13px; border: 1px solid rgba(101,225,178,.4); border-radius: 12px; background: rgba(101,225,178,.14); color: #adffe1; }
.annotation-label { position: fixed; z-index: 50; display: none; padding: 5px 7px; pointer-events: none; border: 1px solid rgba(255,211,108,.45); border-radius: 7px; background: #101a27; color: #ffe4a0; font: 11px var(--font-mono); box-shadow: var(--shadow-sm); }
body.annotate [data-label]:hover { outline: 1px dashed rgba(255,211,108,.7); outline-offset: 3px; }
.toolbar { position: fixed; z-index: 30; right: 16px; bottom: 16px; display: flex; align-items: center; gap: 7px; padding: 8px 10px; border: 1px solid rgba(255,255,255,.16); border-radius: 12px; background: rgba(5,9,14,.78); color: #fff; font-size: 11px; opacity: .44; transition: opacity .2s ease; backdrop-filter: blur(12px); }
.toolbar:hover { opacity: 1; }
.toolbar button, .toolbar select { border: 1px solid rgba(255,255,255,.19); border-radius: 7px; padding: 5px 7px; background: rgba(255,255,255,.08); color: #fff; }
.toolbar button:hover, .toolbar select:hover { background: rgba(116,185,255,.22); }
.toast { position: fixed; z-index: 60; left: 50%; bottom: 72px; transform: translate(-50%, 12px); padding: 9px 14px; opacity: 0; border: 1px solid rgba(213,235,255,.28); border-radius: 12px; background: rgba(9,16,25,.93); color: var(--color-text); box-shadow: var(--shadow-md); transition: all .18s ease; pointer-events: none; font-size: var(--text-sm); }
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.interaction-section { max-width: 1040px; margin: 54px auto 0; }
.interaction-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-bottom: 18px; }
.interaction-head h2 { margin: 5px 0 6px; font-size: clamp(1.35rem, 2.6vw, 2rem); }
.interaction-head p { max-width: 720px; margin: 0; color: #c6b399; line-height: 1.55; }
.interaction-legend { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; max-width: 330px; }
.legend-chip { display: inline-flex; align-items: center; gap: 5px; padding: 6px 9px; border: 1px solid rgba(224, 181, 105, .26); border-radius: 999px; background: rgba(93, 53, 25, .2); color: #d8bd91; font-size: 10px; white-space: nowrap; }
.legend-chip i { width: 7px; height: 7px; border-radius: 50%; background: #e0b15e; box-shadow: 0 0 8px rgba(224, 177, 91, .7); }
.legend-chip.menu i { background: #8f6be8; box-shadow: 0 0 8px rgba(143, 107, 232, .7); }
.legend-chip.full i { background: #68baa0; box-shadow: 0 0 8px rgba(104, 186, 160, .7); }
.legend-chip.modal i { background: #e26c5c; box-shadow: 0 0 8px rgba(226, 108, 92, .7); }
.flow-filter { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 14px; }
.flow-filter button { padding: 7px 12px; border: 1px solid rgba(224, 181, 105, .24); border-radius: 999px; background: rgba(44, 24, 14, .62); color: #c8b18e; font-size: 11px; }
.flow-filter button.active, .flow-filter button:hover { border-color: rgba(239, 195, 113, .72); background: rgba(163, 100, 44, .28); color: #f5d998; }
.flow-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.flow-card { min-width: 0; padding: 14px; border: 1px solid rgba(224, 181, 105, .25); border-radius: 16px; background: linear-gradient(145deg, rgba(64, 35, 20, .72), rgba(20, 11, 8, .88)); box-shadow: 0 10px 26px rgba(0,0,0,.18); }
.flow-card.is-hidden { display: none; }
.flow-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.flow-card-head h3 { margin: 0; color: #f4e0b5; font-size: 14px; }
.flow-index { color: #d8a758; font: 11px var(--font-mono); letter-spacing: .08em; }
.flow-scope { color: #a99478; font-size: 10px; }
.flow-track { display: flex; align-items: stretch; gap: 6px; overflow-x: auto; padding: 2px 1px 5px; scrollbar-width: thin; }
.flow-node { position: relative; min-width: 104px; min-height: 67px; padding: 9px 8px; border: 1px solid rgba(224, 181, 105, .24); border-radius: 12px; background: rgba(16, 9, 7, .8); color: #f5ead7; text-align: left; transition: transform .15s ease, border-color .15s ease, background .15s ease; }
.flow-node:hover, .flow-node:focus-visible { transform: translateY(-2px); border-color: rgba(239, 195, 113, .85); background: rgba(117, 68, 32, .36); outline: none; }
.flow-node b { display: block; margin-bottom: 5px; font-size: 12px; }
.flow-node small { display: block; color: #bba586; font-size: 10px; line-height: 1.35; }
.flow-node.menu-node { border-color: rgba(143, 107, 232, .4); }
.flow-node.full-node { border-color: rgba(104, 186, 160, .4); }
.flow-node.modal-node { border-color: rgba(226, 108, 92, .42); }
.flow-arrow { align-self: center; flex: 0 0 auto; color: #d8a758; font-size: 16px; opacity: .78; }
.flow-detail { margin-top: 14px; min-height: 132px; padding: 16px 18px; border: 1px solid rgba(239, 195, 113, .34); border-radius: 16px; background: rgba(20, 10, 7, .84); box-shadow: 0 15px 32px rgba(0,0,0,.22); }
.flow-detail.empty { display: grid; place-items: center; color: #bba586; text-align: center; }
.flow-detail h3 { margin: 4px 0 8px; color: #f4e0b5; font-size: 17px; }
.detail-kicker { color: #d8a758; font: 10px var(--font-mono); letter-spacing: .14em; text-transform: uppercase; }
.detail-grid { display: grid; grid-template-columns: 1.1fr 1fr 1fr; gap: 14px; }
.detail-block strong { display: block; margin-bottom: 4px; color: #d8bd91; font-size: 11px; }
.detail-block p { margin: 0; color: #c8b18e; font-size: 11px; line-height: 1.5; }
.detail-block p + p { margin-top: 4px; }
.flow-note { margin: 12px 0 0; padding-top: 10px; border-top: 1px solid rgba(224, 181, 105, .14); color: #9f8b70; font-size: 10px; line-height: 1.5; }
.hidden { display: none !important; }
@media (max-width: 620px) { body { padding-inline: 10px; } .variant-intro { display: block; padding-inline: 4px; } .variant-intro p { margin-top: 4px; } .toolbar { right: 8px; bottom: 8px; } }
/* Diablo-inspired ARPG direction: warm bronze frames, dark glass, circular combat controls. */
body { background: radial-gradient(circle at 50% 0%, #3a2416 0, #170e0b 35rem, #090706 70rem); }
.page-head p { color: #c6b399; }
.device { border-color: rgba(224, 181, 105, .72); border-radius: 22px; box-shadow: 0 28px 80px rgba(0,0,0,.7), 0 0 0 6px rgba(204, 147, 64, .08), inset 0 0 0 1px rgba(255, 229, 175, .14); }
.scene { background: linear-gradient(145deg, #725031, #b98045 40%, #4b2b1d 78%, #1b100c); }
.scene::before { opacity: .7; background: radial-gradient(ellipse at 34% 40%, rgba(255, 224, 152, .28), transparent 24%), radial-gradient(ellipse at 70% 28%, rgba(42, 16, 13, .4), transparent 27%), repeating-linear-gradient(151deg, rgba(255, 220, 155, .06) 0 2px, transparent 2px 38px); }
.scene::after { background: linear-gradient(180deg, rgba(12, 7, 5, .54), transparent 20%, transparent 62%, rgba(8, 5, 4, .7)); }
.terrain { opacity: .62; background: repeating-linear-gradient(164deg, transparent 0 17px, rgba(70, 35, 18, .18) 18px 20px), repeating-linear-gradient(22deg, transparent 0 35px, rgba(255, 218, 152, .16) 36px 38px); }
.hero { background: linear-gradient(90deg, #1a1210, #d2a85d 43%, #6b3d25 59%, #110b09); box-shadow: 0 0 20px rgba(255, 198, 93, .48); }
.hero::before { background: radial-gradient(circle at 40% 42%, #ffe0b2 0 18%, #6d3b2b 19% 54%, #160e0c 55%); }
.hero::after { color: #ffe0a1; text-shadow: 0 0 14px #ef812e; }
.mob { background: linear-gradient(140deg, #493021, #963f32 45%, #1c1110); box-shadow: 0 0 18px rgba(214, 69, 45, .35); }
.mob::before { background: #4f1e1b; }
.mob::after { color: #ffc0a0; }
.top-status { top: 15px; left: 18px; right: auto; }
.player-status { display: flex; width: 148px; flex-direction: column; align-items: center; gap: 3px; }
.player-name { display: flex; width: 148px; align-items: center; justify-content: center; gap: 6px; color: #f4e0b5; font: 600 11px var(--font-mono); text-shadow: 0 2px 4px #000; white-space: nowrap; }
.player-meters { width: 44px; }
.party-rail { position: absolute; z-index: 4; top: 112px; left: 18px; display: grid; width: 148px; gap: 5px; }
.party-member { display: grid; grid-template-columns: 27px minmax(0, 1fr) auto; align-items: center; gap: 6px; min-height: 34px; padding: 4px 6px 4px 4px; border: 1px solid rgba(223, 181, 111, .32); border-radius: 8px; background: rgba(19, 10, 7, .65); color: #f4e0b5; text-align: left; box-shadow: 0 4px 10px rgba(0,0,0,.28); transition: transform .15s ease, border-color .15s ease, background .15s ease; }
.party-member:hover, .party-member:focus-visible { transform: translateX(2px); border-color: rgba(239, 195, 113, .76); background: rgba(87, 46, 22, .78); outline: none; }
.party-avatar { display: grid; width: 25px; height: 25px; place-items: center; border: 1px solid rgba(239, 195, 112, .6); border-radius: 50%; background: radial-gradient(circle, #89684e, #291711 68%); color: #f7d58f; font-size: 12px; }
.party-copy { min-width: 0; }
.party-copy strong { display: block; overflow: hidden; color: #e9d5ae; font-size: 9px; line-height: 1.15; text-overflow: ellipsis; white-space: nowrap; }
.party-hp { height: 4px; margin-top: 3px; overflow: hidden; border-radius: 4px; background: rgba(5, 3, 2, .72); }
.party-hp i { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #8a2525, #e85a46); }
.party-distance { color: #bfa77f; font: 9px var(--font-mono); }
.top-right-controls { position: absolute; z-index: 4; top: 14px; right: 14px; display: flex; align-items: flex-start; gap: 7px; }
.top-right-controls .menu-button { min-width: 42px; width: 42px; height: 42px; flex: 0 0 auto; background: rgba(17, 9, 7, .78); border-color: rgba(224, 177, 91, .5); box-shadow: 0 4px 10px rgba(0,0,0,.42); }
.avatar { border-color: rgba(239, 195, 112, .92); background: radial-gradient(circle, #99704c, #2b1a13 62%, #0d0907); box-shadow: 0 0 0 3px rgba(13, 8, 6, .7), 0 0 16px rgba(231, 167, 68, .22); }
.level { color: #f4ca7b; }
.meter { height: 9px; border-color: rgba(255, 227, 170, .25); background: rgba(11, 6, 5, .82); box-shadow: inset 0 1px 2px rgba(0,0,0,.45); }
.meter.hp i { background: linear-gradient(90deg, #761b1e, #e2432c 72%, #ff8961); }
.meter.mp i { background: linear-gradient(90deg, #3c247f, #8454f0 72%, #c58aff); }
.mini-map { width: 142px; height: 86px; border-radius: 9px; border-color: rgba(223, 187, 119, .72); background: radial-gradient(circle at 43% 58%, rgba(226, 179, 83, .42) 0 7%, transparent 8%), linear-gradient(32deg, #21150e 0 31%, #6a4323 32% 39%, #271711 40% 63%, #80603b 64%); box-shadow: inset 0 0 0 4px rgba(8, 5, 4, .68), 0 6px 14px rgba(0,0,0,.48); }
.mini-map::before { left: 47%; top: 43%; background: #f8d17e; box-shadow: 0 0 0 4px rgba(255, 205, 106, .2), 16px -20px 0 -2px #d65838; }
.mini-map::after { content: "荒漠边境"; position: absolute; right: 7px; bottom: 5px; color: rgba(245, 213, 157, .7); font: 10px var(--font-mono); letter-spacing: .08em; }
.target-card { top: 13px; width: 260px; padding: 4px 12px 6px; border: 0; border-radius: 0; background: rgba(13, 8, 6, .2); box-shadow: none; text-align: center; }
.target-card strong { color: #f4e0b5; font: 600 15px var(--font-mono); letter-spacing: .1em; text-shadow: 0 2px 4px #000; }
.target-card small { color: #d0b285; }
.target-card .meter { height: 11px; margin-top: 5px; border-color: rgba(10, 5, 4, .9); }
.target-card .meter i { background: linear-gradient(90deg, #6d171a, #d43d2e 75%, #f06d45); }
.quest-pill { top: 109px; right: 168px; border-color: rgba(224, 177, 91, .38); background: rgba(24, 13, 9, .65); color: #f1d49a; box-shadow: 0 5px 13px rgba(0,0,0,.25); }
.chat-bubble { top: 57%; background: rgba(17, 9, 7, .72); border-color: rgba(236, 197, 133, .28); color: #f4e6cb; }
.context-action { right: 235px; bottom: 104px; border-color: rgba(219, 173, 93, .58); background: rgba(25, 12, 8, .74); color: #f6d593; box-shadow: 0 5px 15px rgba(0,0,0,.32); }
.right-rail { position: absolute; z-index: 4; top: 111px; right: 14px; display: grid; gap: 8px; }
.right-rail .utility-btn { min-width: 42px; width: 42px; height: 42px; flex: 0 0 auto; background: rgba(17, 9, 7, .64); border-color: rgba(216, 171, 99, .3); box-shadow: 0 4px 10px rgba(0,0,0,.3); }
.right-rail .utility-btn span { color: #d8bd91; }
.joystick { left: 25px; bottom: 18px; width: 120px; height: 120px; border-color: rgba(223, 181, 111, .32); background: radial-gradient(circle, rgba(185, 119, 58, .26) 0 28%, rgba(74, 39, 22, .42) 29% 62%, rgba(12, 7, 5, .26) 63%); box-shadow: inset 0 0 0 12px rgba(27, 13, 8, .28), 0 7px 18px rgba(0,0,0,.35); }
.joystick::before { border-color: rgba(238, 202, 139, .2); }
.stick { border-color: rgba(236, 194, 117, .58); background: radial-gradient(circle at 35% 28%, rgba(255, 239, 196, .28), rgba(114, 70, 38, .82) 38%, rgba(30, 14, 9, .94)); }
.combat-cluster { right: 20px; bottom: 15px; width: 190px; height: 190px; }
.combat-cluster::before { content: ""; position: absolute; inset: 16px -1px -2px 8px; border: 1px solid rgba(185, 132, 68, .3); border-radius: 50%; background: radial-gradient(circle, rgba(21, 10, 8, .56), rgba(12, 6, 5, .12) 68%, transparent 69%); box-shadow: 0 0 0 11px rgba(12, 6, 5, .18); }
.combat-cluster::after { content: ""; position: absolute; inset: 5px -4px 12px 5px; border: 2px solid rgba(219, 169, 83, .28); border-left-color: transparent; border-bottom-color: transparent; border-radius: 50%; transform: rotate(34deg); pointer-events: none; }
.skill { z-index: 1; width: 58px; height: 58px; border-color: rgba(232, 191, 115, .58); background: radial-gradient(circle at 35% 26%, rgba(247, 225, 177, .24), rgba(62, 28, 22, .94) 46%, rgba(12, 6, 6, .98)); box-shadow: 0 6px 14px rgba(0,0,0,.6), inset 0 0 0 3px rgba(6, 3, 3, .36); }
.skill:hover, .skill:focus-visible { border-color: #f4d28d; filter: brightness(1.3); box-shadow: 0 0 0 3px rgba(238, 187, 90, .2), 0 7px 16px rgba(0,0,0,.6); }
.skill.main { width: 88px; height: 88px; border-color: rgba(234, 196, 123, .86); background: radial-gradient(circle at 35% 25%, #7e5cf1, #321d75 48%, #0e0820 78%); box-shadow: 0 0 0 4px rgba(15, 7, 28, .75), 0 0 18px rgba(107, 65, 220, .6), 0 7px 17px rgba(0,0,0,.65); font-size: 31px; }
.skill.main:hover, .skill.main:focus-visible { box-shadow: 0 0 0 5px rgba(232, 190, 100, .27), 0 0 24px rgba(138, 92, 255, .78); }
.skill.s1 { left: 10px; top: 14px; }
.skill.s2 { left: 62px; top: 70px; }
.skill.s3 { right: 2px; top: 15px; }
.skill.s4 { left: 13px; bottom: 2px; }
#mobile-hud .skill.s4 { left: auto; right: 16px; top: -52px; bottom: auto; border-color: rgba(223, 66, 44, .86); background: radial-gradient(circle at 35% 25%, #f06a40, #7d1f25 45%, #1a080b 78%); }
.skill em { background: #140907; color: #f2d491; }
.utility-btn { border-color: rgba(215, 171, 98, .2); border-radius: 7px; background: rgba(124, 76, 40, .16); }
.utility-btn:hover, .utility-btn.active { border-color: rgba(240, 200, 123, .72); background: rgba(173, 107, 46, .28); }
.utility-btn span { color: #cdb58d; }
.modal-layer { background: rgba(7, 3, 2, .6); }
.sheet { border-top-color: rgba(226, 181, 99, .5); background: rgba(25, 14, 10, .97); }
.action-item { border-color: rgba(220, 176, 100, .24); background: rgba(139, 82, 40, .12); }
.action-item:hover { border-color: rgba(239, 195, 113, .7); background: rgba(163, 100, 44, .24); }
.action-item span, .sheet-note { color: #c8b18e; }
.menu-item { border-color: rgba(220, 176, 100, .3); background: linear-gradient(145deg, rgba(128, 73, 34, .38), rgba(30, 15, 9, .72)); box-shadow: inset 0 1px 0 rgba(255, 224, 163, .08), 0 6px 14px rgba(0,0,0,.2); }
.menu-item:hover, .menu-item:focus-visible { border-color: rgba(239, 195, 113, .82); background: linear-gradient(145deg, rgba(164, 95, 40, .52), rgba(44, 20, 10, .82)); }
.menu-item b { color: #f0c979; }
.menu-item small { color: #bda887; }
.send-btn { border-color: rgba(117, 186, 123, .5); background: rgba(75, 122, 73, .2); color: #c3e8b9; }
.toolbar { background: rgba(10, 6, 4, .88); border-color: rgba(228, 183, 103, .32); }
.toolbar button:hover, .toolbar select:hover { background: rgba(171, 104, 43, .28); }
.toast { background: rgba(20, 10, 7, .96); border-color: rgba(224, 178, 98, .42); }
@media (max-height: 460px) and (orientation: landscape) {
body { padding-top: 66px; }
.top-status { top: 10px; }
.player-status, .player-name { width: 122px; }
.player-meters { width: 44px; }
.player-name { font-size: 9px; }
.party-rail { top: 91px; left: 10px; width: 122px; gap: 4px; }
.party-member { min-height: 30px; padding-block: 3px; }
.party-avatar { width: 22px; height: 22px; font-size: 10px; }
.top-right-controls { top: 9px; right: 9px; }
.mini-map { width: 108px; height: 64px; }
.right-rail { top: 82px; }
.status-copy { flex-basis: 122px; width: 122px; }
.quest-pill { top: 84px; right: 128px; }
.target-card { top: 8px; }
.joystick { width: 96px; height: 96px; left: 18px; bottom: 13px; }
.combat-cluster { right: 14px; bottom: 10px; transform: scale(.82); transform-origin: bottom right; }
.context-action { bottom: 88px; }
}
@media (max-width: 820px) {
.interaction-head { display: block; }
.interaction-legend { justify-content: flex-start; margin-top: 12px; }
.flow-grid { grid-template-columns: 1fr; }
.detail-grid { grid-template-columns: 1fr; gap: 9px; }
}
</style>
</head>
<body>
<header class="page-head">
<div class="eyebrow">Mobile ARPG Control HUD / Sketch 001</div>
<h1>暗黑 ARPG 风格 · 横屏 HUD 草图</h1>
<p>按 844×390 横屏手机设计,保持世界画面优先,战斗操作触手可达;底部中央保持留白,低频功能统一收纳到右上角菜单。点击首页控件或下方交互图节点,查看完整的移动端 UI 流程。</p>
</header>
<main id="sketch-canvas" class="preview-phone">
<section id="mobile-hud" class="variant">
<div class="variant-intro"><h2>暗黑 ARPG 横屏 HUD</h2><p>左上按“名称→头像→HP/MP/资源短条”排列;右上按“背包→小地图→菜单”排列,地图通过点击小地图进入,底部中央保持留白。</p></div>
<div class="device-wrap"><div class="device" data-label="844 × 390 landscape · 暗黑 ARPG">
<div class="scene">
<div class="terrain"></div><div class="hero" data-label="玩家角色"></div><div class="mob"></div>
<div class="top-status" data-label="玩家状态"><div class="player-status"><div class="player-name"><span>Azure · 暮光骑士</span><span class="level">Lv.42</span></div><button class="avatar demo-btn" data-panel="character" data-label="打开角色状态"></button><div class="player-meters"><div class="meter hp" aria-label="生命值"><i></i></div><div class="meter mp" aria-label="法力值"><i></i></div><div class="meter resource" aria-label="职业资源:奥能 68%"><i></i></div></div></div></div>
<div class="party-rail" data-label="队伍信息:仅组队后显示"><button class="party-member demo-btn" data-panel="party" data-label="队员 Mira · 点击查看或定位"><span class="party-avatar">M</span><span class="party-copy"><strong>Mira · 圣骑士</strong><span class="party-hp"><i style="width: 83%"></i></span></span><span class="party-distance">12m</span></button><button class="party-member demo-btn" data-panel="party" data-label="队员 Ken · 点击查看或定位"><span class="party-avatar">K</span><span class="party-copy"><strong>Ken · 游侠</strong><span class="party-hp"><i style="width: 61%"></i></span></span><span class="party-distance">34m</span></button></div>
<div class="top-right-controls" data-label="顶部背包、地图与菜单"><button class="utility-btn top-bag demo-btn" data-panel="inventory" data-label="打开背包"><span>背包</span></button><button class="mini-map demo-btn" data-panel="map" aria-label="打开地图" data-label="小地图 / 地图"></button><button class="utility-btn menu-button demo-btn" data-panel="more" data-label="打开菜单"><span>菜单</span></button></div>
<button class="target-card demo-btn" data-panel="target" data-label="目标板"><strong>血狼 · Lv.40</strong><small>敌对目标</small><div class="meter"><i></i></div></button>
<button class="quest-pill demo-btn" data-panel="quest" data-label="任务追踪">◆ 采集狼牙 <b>2/5</b></button>
<div class="chat-bubble" data-label="头顶聊天气泡">附近:小心精英怪!</div>
<button class="context-action demo-btn" data-toast="靠近目标后:拾取 / 对话 / 钓鱼 / 进入副本">✦ 交互</button>
<div class="joystick" data-joystick data-label="虚拟摇杆"><div class="stick"></div></div>
<div class="combat-cluster" data-label="战斗技能区">
<button class="skill s1 demo-btn" data-skill="冰刃" data-toast="释放技能:冰刃"><em>8</em></button>
<button class="skill s2 demo-btn" data-skill="冲锋" data-toast="释放技能:冲锋"><em>5</em></button>
<button class="skill s3 demo-btn" data-skill="治疗" data-toast="释放技能:治疗"><em>12</em></button>
<button class="skill s4 demo-btn" data-skill="药水" data-toast="使用物品:红色药水"><em>3</em></button>
<button class="skill main demo-btn" data-skill="普攻" data-toast="普通攻击"></button>
</div>
<div class="floating-cooldown" data-cooldown>技能冷却中</div>
<div class="modal-layer" data-modal><div class="sheet"><div class="sheet-head"><div class="sheet-title-row"><button class="back-btn hidden" data-back aria-label="返回上一级"></button><h3 data-sheet-title>功能</h3></div><button class="close-btn" data-close>×</button></div><div data-sheet-body></div></div></div>
</div>
</div></div>
</section>
</main>
<section class="interaction-section" aria-labelledby="interaction-title">
<div class="interaction-head">
<div>
<div class="eyebrow">Interaction map / all screens</div>
<h2 id="interaction-title">全量 UI 移动端交互图</h2>
<p>每张图表示一条完整操作路径。点击任意节点,查看它的入口位置、触控方式、页面形态和返回路径;任务始终从任务栏进入,组队信息固定显示在左上玩家头像下方。</p>
</div>
<div class="interaction-legend" aria-label="页面形态图例">
<span class="legend-chip"><i></i>常驻 HUD</span>
<span class="legend-chip menu"><i></i>右上角菜单</span>
<span class="legend-chip full"><i></i>全屏面板</span>
<span class="legend-chip modal"><i></i>场景弹窗</span>
</div>
</div>
<div class="flow-filter" role="tablist" aria-label="交互图筛选">
<button class="active" data-flow-filter="all" role="tab" aria-selected="true">全部流程</button>
<button data-flow-filter="entry" role="tab" aria-selected="false">进入游戏</button>
<button data-flow-filter="combat" role="tab" aria-selected="false">战斗与任务</button>
<button data-flow-filter="character" role="tab" aria-selected="false">角色与物品</button>
<button data-flow-filter="social" role="tab" aria-selected="false">社交与经济</button>
<button data-flow-filter="system" role="tab" aria-selected="false">系统状态</button>
</div>
<div class="flow-grid" id="flow-grid">
<article class="flow-card" data-flow-group="entry">
<div class="flow-card-head"><h3>进入游戏与角色入口</h3><span class="flow-index">01 · ENTRY</span></div>
<div class="flow-track">
<button class="flow-node full-node" data-detail="login"><b>登录</b><small>账号密码<br>失败提示</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="server"><b>服务器</b><small>频道选择<br>连接确认</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="character_select"><b>角色列表</b><small>选择 / 创建<br>删除 / 改名</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="loading"><b>加载</b><small>资源加载<br>进入场景</small></button><span class="flow-arrow"></span>
<button class="flow-node" data-detail="hud"><b>首页 HUD</b><small>开始操作</small></button>
</div>
</article>
<article class="flow-card" data-flow-group="combat">
<div class="flow-card-head"><h3>首页战斗与掉落</h3><span class="flow-index">02 · COMBAT</span></div>
<div class="flow-track">
<button class="flow-node" data-detail="hud"><b>首页 HUD</b><small>左上状态<br>右上入口</small></button><span class="flow-arrow"></span>
<button class="flow-node" data-detail="movement"><b>移动摇杆</b><small>拖动移动<br>松手复位</small></button><span class="flow-arrow"></span>
<button class="flow-node" data-detail="target"><b>锁定目标</b><small>点击目标<br>显示血条</small></button><span class="flow-arrow"></span>
<button class="flow-node" data-detail="combat_action"><b>普攻 / 技能</b><small>点击释放<br>冷却反馈</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="loot"><b>掉落 / 交互</b><small>拾取、对话<br>进入副本</small></button><span class="flow-arrow"></span>
<button class="flow-node" data-detail="death"><b>死亡 / 复活</b><small>战斗失败<br>回城或复活</small></button>
</div>
</article>
<article class="flow-card" data-flow-group="combat">
<div class="flow-card-head"><h3>任务、地图与副本</h3><span class="flow-index">03 · QUEST</span></div>
<div class="flow-track">
<button class="flow-node" data-detail="quest"><b>任务栏</b><small>首页常驻<br>直接点击</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="quest_log"><b>任务日志</b><small>列表 / 奖励<br>任务详情</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="map"><b>小地图 / 大地图</b><small>点击小地图<br>地图信标</small></button><span class="flow-arrow"></span>
<button class="flow-node" data-detail="navigation"><b>自动寻路</b><small>导航至目标<br>到达提示</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="npc_quest"><b>NPC 对话</b><small>接取 / 完成<br>任务奖励</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="dungeon_result"><b>副本结算</b><small>奖励 / 再次挑战<br>退出副本</small></button>
</div>
</article>
<article class="flow-card" data-flow-group="character">
<div class="flow-card-head"><h3>角色、装备与技能</h3><span class="flow-index">04 · CHARACTER</span></div>
<div class="flow-track">
<button class="flow-node" data-detail="character_entry"><b>玩家头像</b><small>左上角<br>唯一入口</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="character_status"><b>角色属性</b><small>等级、属性<br>成长信息</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="equipment"><b>装备</b><small>穿戴、替换<br>查看对比</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="skills"><b>技能</b><small>主动、被动<br>坐骑、表情</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="skill_upgrade"><b>技能加点</b><small>查看消耗<br>确认升级</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="inspect"><b>查看玩家</b><small>点击角色<br>查看装备</small></button>
</div>
</article>
<article class="flow-card" data-flow-group="character">
<div class="flow-card-head"><h3>背包、物品与仓库</h3><span class="flow-index">05 · ITEMS</span></div>
<div class="flow-track">
<button class="flow-node menu-node" data-detail="inventory"><b>背包</b><small>右上快捷入口<br>菜单也可进入</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="item_tooltip"><b>物品详情</b><small>点击选中<br>长按查看</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="item_action"><b>使用 / 装备 / 出售</b><small>按钮操作<br>危险操作确认</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="safebox"><b>仓库</b><small>背包与仓库<br>左右切换</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="select_item"><b>任务物品选择</b><small>选择数量<br>提交确认</small></button>
</div>
</article>
<article class="flow-card" data-flow-group="social">
<div class="flow-card-head"><h3>队伍状态、聊天、好友与公会</h3><span class="flow-index">06 · SOCIAL</span></div>
<div class="flow-track">
<button class="flow-node" data-detail="party"><b>左侧队伍条</b><small>头像下方<br>成员状态</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="party"><b>队员信息</b><small>查看 / 定位<br>离队设置</small></button><span class="flow-arrow"></span>
<button class="flow-node menu-node" data-detail="social_entry"><b>右上菜单</b><small>社交入口<br>不含队伍</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="chat"><b>聊天</b><small>综合、私聊<br>队伍、公会</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="friend"><b>好友</b><small>申请、私聊<br>查看资料</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="love"><b>情侣 / 爱意</b><small>关系信息<br>互动操作</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="guild"><b>公会</b><small>成员、技能<br>公告、公会战</small></button>
</div>
</article>
<article class="flow-card" data-flow-group="social">
<div class="flow-card-head"><h3>商店、交易与制作</h3><span class="flow-index">07 · ECONOMY</span></div>
<div class="flow-track">
<button class="flow-node modal-node" data-detail="commerce_entry"><b>NPC / 玩家</b><small>靠近后点击<br>出现交互</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="shop"><b>商店 / 商城</b><small>购买、出售<br>商城物品</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="trade"><b>玩家交易</b><small>拖入物品<br>双方确认</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="private_shop"><b>私人商店</b><small>浏览、购买<br>摆摊管理</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="cube"><b>制作</b><small>放入材料<br>制作确认</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="refine"><b>精炼 / 龙魂</b><small>强化结果<br>失败确认</small></button>
</div>
</article>
<article class="flow-card" data-flow-group="system">
<div class="flow-card-head"><h3>系统设置与异常状态</h3><span class="flow-index">08 · SYSTEM</span></div>
<div class="flow-track">
<button class="flow-node menu-node" data-detail="system_entry"><b>右上菜单</b><small>系统入口<br>随时可打开</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="options"><b>系统 / 游戏设置</b><small>音量、画面<br>操作、屏蔽</small></button><span class="flow-arrow"></span>
<button class="flow-node full-node" data-detail="help"><b>帮助</b><small>操作说明<br>功能说明</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="character_switch"><b>选择角色</b><small>离开当前角色<br>二次确认</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="logout"><b>登出 / 退出</b><small>保存状态<br>确认离开</small></button><span class="flow-arrow"></span>
<button class="flow-node modal-node" data-detail="reconnect"><b>断线重连</b><small>连接恢复<br>重新进入场景</small></button>
</div>
</article>
</div>
<aside class="flow-detail empty" id="flow-detail" aria-live="polite">
<div><div class="detail-kicker">Tap a node</div><p>点击上方任意节点,查看该界面的移动端入口、位置、交互方式和返回路径。</p></div>
</aside>
</section>
<div class="toolbar" id="sketch-tools" aria-label="草图工具">
<span>横屏预览</span><button data-viewport="phone">手机</button><button data-viewport="tablet">平板</button><button data-viewport="desktop">桌面</button>
<select id="theme-switcher" aria-label="主题"><option value="default">暗黑铜金</option></select>
<button id="annotation-toggle">标注</button>
</div>
<div class="toast" id="toast" role="status"></div><div class="annotation-label" id="annotation-label"></div>
<script>
const body = document.body;
const canvas = document.getElementById('sketch-canvas');
const toast = document.getElementById('toast');
let toastTimer;
const featureCatalog = [
['♟', '社交', 'social', '聊天 · 好友 · 公会'],
['✧', '成长', 'growth', '强化 · 制作 · 龙魂'],
['▣', '活动', 'adventure', '副本 · 钓鱼'],
['◈', '商业', 'market', '商店 · 交易 · 仓库'],
['⚙', '系统', 'system', '设置 · 帮助 · 退出']
];
const featureCopy = {
inventory: '<p class="sheet-note">装备 / 腰带 / 物品拖放 · 右侧滑出面板,回到战斗时自动收起。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="拖动红药到快捷栏">♥<span>红药 ×12</span></button><button class="action-item demo-btn" data-toast="查看装备">⚔<span>装备</span></button><button class="action-item demo-btn" data-toast="打开腰带">▤<span>腰带</span></button><button class="action-item demo-btn" data-toast="整理背包">✦<span>整理</span></button></div>',
character: '<p class="sheet-note">头像入口承载 HP / MP、状态、装备、技能、坐骑和表情动作。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="打开状态页">♥<span>状态</span></button><button class="action-item demo-btn" data-toast="查看装备">⚔<span>装备</span></button><button class="action-item demo-btn" data-panel="skills"><b>✦</b><span>技能</span></button><button class="action-item demo-btn" data-panel="mount"><b>♞</b><span>坐骑</span></button><button class="action-item demo-btn" data-panel="emote"><b>☺</b><span>表情</span></button><button class="action-item demo-btn" data-toast="打开称号页">◆<span>称号</span></button></div>',
quest: '<p class="sheet-note">任务追踪使用半透明卡片;点击任务行进入日志,地图信标从卡片右上角进入。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="追踪:采集狼牙">◆<span>追踪</span></button><button class="action-item demo-btn" data-toast="打开任务日志">▤<span>日志</span></button><button class="action-item demo-btn" data-toast="显示地图信标">⌖<span>导航</span></button><button class="action-item demo-btn" data-toast="收起任务条">×<span>隐藏</span></button></div>',
map: '<p class="sheet-note">小地图点击后升起全屏地图,保留副本目的地罗盘和任务信标。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="地图缩放 +"><span>放大</span></button><button class="action-item demo-btn" data-toast="地图缩放 -"><span>缩小</span></button><button class="action-item demo-btn" data-toast="查看世界地图">⌖<span>世界地图</span></button><button class="action-item demo-btn" data-toast="回到玩家位置">●<span>定位</span></button></div>',
target: '<p class="sheet-note">目标板支持切换目标、查看血量和情境动作;按住目标卡可进入更多交互。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="切换目标">⇄<span>切换</span></button><button class="action-item demo-btn" data-toast="开始自动攻击">⚔<span>攻击</span></button><button class="action-item demo-btn" data-toast="打开目标信息">? <span>信息</span></button><button class="action-item demo-btn" data-toast="取消目标">×<span>清除</span></button></div>',
chat: '<div class="chat-list"><div class="chat-line"><strong>[队伍]</strong> Mira:副本入口在北门。</div><div class="chat-line"><strong>[附近]</strong> 你:收到,马上到。</div><div class="chat-line"><strong>[公会]</strong> Ken:今晚 20:00 公会战。</div></div><div class="chat-compose"><input placeholder="输入消息…"><button class="send-btn" data-send>发送</button></div>',
party: '<p class="sheet-note">队伍信息从左上玩家头像下方的队员条进入;未组队时该区域自动隐藏。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="定位队员">⌖<span>定位</span></button><button class="action-item demo-btn" data-toast="查看队员资料">♟<span>资料</span></button><button class="action-item demo-btn" data-toast="打开队伍设置">⚙<span>设置</span></button><button class="action-item demo-btn" data-toast="离开队伍确认">×<span>离队</span></button></div>',
more: '<p class="sheet-note">菜单只保留五个一级入口;角色相关功能点击左上头像进入,背包、小地图和任务仍从首页直接进入。</p><div class="menu-grid">' + featureCatalog.map(([icon, name, key, note]) => `<button class="menu-item demo-btn" data-panel="${key}" data-toast="打开${name}"><b>${icon}</b><span>${name}</span><small>${note}</small></button>`).join('') + '</div>',
guild: '<p class="sheet-note">公会信息、成员、会徽、公会战和公告从左侧社交轨进入。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="打开公会成员">♟<span>成员</span></button><button class="action-item demo-btn" data-toast="查看公会技能">✧<span>技能</span></button><button class="action-item demo-btn" data-toast="查看公会会徽">✦<span>会徽</span></button><button class="action-item demo-btn" data-toast="查看公会战">⚔<span>公会战</span></button></div>'
};
Object.assign(featureCopy, {
social: '<p class="sheet-note">社交菜单只保留聊天、好友、公会、情侣和观战;队伍状态固定在左上玩家头像下方。</p><div class="action-grid"><button class="action-item demo-btn" data-panel="chat"><b>☵</b><span>聊天</span></button><button class="action-item demo-btn" data-panel="friend"><b>♧</b><span>好友</span></button><button class="action-item demo-btn" data-panel="guild"><b>♜</b><span>公会</span></button><button class="action-item demo-btn" data-panel="love"><b>♥</b><span>情侣</span></button><button class="action-item demo-btn" data-panel="observer"><b>◉</b><span>观战</span></button></div>',
growth: '<p class="sheet-note">成长功能统一处理角色长期养成;技能、坐骑和表情仍归入角色页面,避免重复入口。</p><div class="action-grid"><button class="action-item demo-btn" data-panel="refine"><b>⚒</b><span>精炼</span></button><button class="action-item demo-btn" data-panel="dragon"><b>✧</b><span>龙魂</span></button><button class="action-item demo-btn" data-panel="cube"><b>▣</b><span>制作</span></button></div>',
adventure: '<p class="sheet-note">活动通过任务追踪、场景交互或这里进入;地图仍只通过右上小地图打开。</p><div class="action-grid"><button class="action-item demo-btn" data-panel="dungeon"><b>▣</b><span>副本</span></button><button class="action-item demo-btn" data-panel="fishing"><b>♨</b><span>钓鱼</span></button></div>',
market: '<p class="sheet-note">涉及货币与物品流转的功能统一归入商业;背包仍在首页右上角单独打开。</p><div class="action-grid"><button class="action-item demo-btn" data-panel="shop"><b>▤</b><span>商店</span></button><button class="action-item demo-btn" data-panel="mall"><b>◈</b><span>商城</span></button><button class="action-item demo-btn" data-panel="trade"><b>⇄</b><span>交易</span></button><button class="action-item demo-btn" data-panel="private_shop"><b>▥</b><span>私人商店</span></button><button class="action-item demo-btn" data-panel="safebox"><b>▤</b><span>仓库</span></button><button class="action-item demo-btn" data-panel="exchange"><b>↔</b><span>兑换</span></button></div>',
friend: '<p class="sheet-note">好友与申请使用统一列表;选择好友后可私聊、查看资料或邀请组队。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="查看好友申请"><span>申请</span></button><button class="action-item demo-btn" data-toast="打开好友资料">♧<span>资料</span></button><button class="action-item demo-btn" data-toast="发起私聊">☵<span>私聊</span></button><button class="action-item demo-btn" data-toast="邀请组队">♟<span>组队</span></button></div>',
love: '<p class="sheet-note">情侣与爱意信息放在社交分组内,避免成为首页固定入口。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="查看关系信息">♥<span>关系</span></button><button class="action-item demo-btn" data-toast="查看爱意等级">✦<span>等级</span></button><button class="action-item demo-btn" data-toast="发送互动">☺<span>互动</span></button><button class="action-item demo-btn" data-toast="打开情侣设置">⚙<span>设置</span></button></div>',
skills: '<p class="sheet-note">技能分为主动、被动、坐骑和表情动作;战斗中的技能按钮由此处配置。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="打开主动技能">❄<span>主动</span></button><button class="action-item demo-btn" data-toast="打开被动技能">✧<span>被动</span></button><button class="action-item demo-btn" data-toast="打开坐骑技能">♞<span>坐骑</span></button><button class="action-item demo-btn" data-toast="打开表情动作">☺<span>表情</span></button></div>',
shop: '<p class="sheet-note">NPC 商店支持购买和出售;商城使用同一套商品详情与确认交互。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="打开购买列表"><span>购买</span></button><button class="action-item demo-btn" data-toast="打开出售列表"><span>出售</span></button><button class="action-item demo-btn" data-toast="查看商品详情">? <span>详情</span></button><button class="action-item demo-btn" data-toast="确认交易金额">✓<span>确认</span></button></div>',
mall: '<p class="sheet-note">商城从菜单进入,购买前查看价格、限购和获得方式,支付动作使用二次确认。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="浏览商城分类">▤<span>分类</span></button><button class="action-item demo-btn" data-toast="查看商品详情">? <span>详情</span></button><button class="action-item demo-btn" data-toast="加入购物车"><span>加入</span></button><button class="action-item demo-btn" data-toast="确认购买">✓<span>购买</span></button></div>',
safebox: '<p class="sheet-note">仓库采用背包与仓库左右切换;长按物品看详情,拖动完成存入或取出。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="存入仓库">↓<span>存入</span></button><button class="action-item demo-btn" data-toast="取出物品">↑<span>取出</span></button><button class="action-item demo-btn" data-toast="整理仓库">✦<span>整理</span></button><button class="action-item demo-btn" data-toast="切换商城仓库">▤<span>商城仓库</span></button></div>',
trade: '<p class="sheet-note">玩家交易通过点击玩家或交互按钮进入;双方放入物品后分别确认,避免误操作。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="拖入交易物品">⇄<span>放入</span></button><button class="action-item demo-btn" data-toast="查看对方物品">? <span>查看</span></button><button class="action-item demo-btn" data-toast="锁定交易内容">▣<span>锁定</span></button><button class="action-item demo-btn" data-toast="等待对方确认">✓<span>确认</span></button></div>',
private_shop: '<p class="sheet-note">私人商店通过点击摊位或玩家进入;浏览和摆摊管理共用一个全屏面板。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="浏览私人商店">▤<span>浏览</span></button><button class="action-item demo-btn" data-toast="购买摊位物品"><span>购买</span></button><button class="action-item demo-btn" data-toast="打开摆摊管理">⚒<span>管理</span></button><button class="action-item demo-btn" data-toast="关闭私人商店">×<span>关闭</span></button></div>',
exchange: '<p class="sheet-note">兑换界面展示兑换材料、产出和数量;滑动数量后点击兑换并确认。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="选择兑换材料">▥<span>材料</span></button><button class="action-item demo-btn" data-toast="调整兑换数量">↔<span>数量</span></button><button class="action-item demo-btn" data-toast="查看兑换产出">✦<span>产出</span></button><button class="action-item demo-btn" data-toast="确认兑换">✓<span>兑换</span></button></div>',
cube: '<p class="sheet-note">Cube 制作将材料槽放在中央,底部只保留一个主要制作按钮,制作完成后弹出结果。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="选择制作配方">▤<span>配方</span></button><button class="action-item demo-btn" data-toast="放入制作材料"><span>材料</span></button><button class="action-item demo-btn" data-toast="查看制作结果">? <span>预览</span></button><button class="action-item demo-btn" data-toast="确认制作">⚒<span>制作</span></button></div>',
refine: '<p class="sheet-note">精炼和龙魂使用同一套强化确认流程:选择装备、查看成功率和消耗,再确认结果。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="选择强化装备">⚔<span>装备</span></button><button class="action-item demo-btn" data-toast="查看强化材料">▥<span>材料</span></button><button class="action-item demo-btn" data-toast="查看成功率"><span>成功率</span></button><button class="action-item demo-btn" data-toast="确认强化">✧<span>强化</span></button></div>',
dragon: '<p class="sheet-note">龙魂属于制作与强化分支,保留独立页面入口但不放在首页常驻 HUD。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="查看龙魂槽位">✧<span>槽位</span></button><button class="action-item demo-btn" data-toast="选择龙魂">◇<span>选择</span></button><button class="action-item demo-btn" data-toast="查看龙魂属性">? <span>属性</span></button><button class="action-item demo-btn" data-toast="确认镶嵌">✓<span>镶嵌</span></button></div>',
fishing: '<p class="sheet-note">钓鱼是场景情境交互:靠近钓点后出现交互按钮,完成小游戏后返回场景。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="开始钓鱼">♨<span>开始</span></button><button class="action-item demo-btn" data-toast="收杆">↗<span>收杆</span></button><button class="action-item demo-btn" data-toast="查看鱼获">▥<span>鱼获</span></button><button class="action-item demo-btn" data-toast="退出钓鱼">×<span>退出</span></button></div>',
dungeon: '<p class="sheet-note">副本入口通过任务导航或场景交互按钮进入;副本内显示目标罗盘,完成后进入结算。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="查看副本目标">⌖<span>目标</span></button><button class="action-item demo-btn" data-toast="查看队伍状态">♟<span>队伍</span></button><button class="action-item demo-btn" data-toast="退出副本">×<span>退出</span></button><button class="action-item demo-btn" data-toast="开始副本">▣<span>进入</span></button></div>',
mount: '<p class="sheet-note">坐骑从技能页面配置,也可在场景中通过情境按钮召唤和收起。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="召唤坐骑">♞<span>召唤</span></button><button class="action-item demo-btn" data-toast="打开坐骑技能">✧<span>技能</span></button><button class="action-item demo-btn" data-toast="更换坐骑">⇄<span>更换</span></button><button class="action-item demo-btn" data-toast="收起坐骑">×<span>收起</span></button></div>',
observer: '<p class="sheet-note">观战从社交菜单或队员资料进入,观战时隐藏自身战斗操作,保留退出观战按钮。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="选择观战对象">◉<span>对象</span></button><button class="action-item demo-btn" data-toast="切换观战视角">↔<span>视角</span></button><button class="action-item demo-btn" data-toast="查看战斗信息">? <span>信息</span></button><button class="action-item demo-btn" data-toast="退出观战">×<span>退出</span></button></div>',
emote: '<p class="sheet-note">表情动作从技能菜单配置,战斗中不占常驻按钮;选择后在角色头顶显示反馈。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="播放表情:挥手">☺<span>挥手</span></button><button class="action-item demo-btn" data-toast="播放表情:欢呼">✦<span>欢呼</span></button><button class="action-item demo-btn" data-toast="播放动作:坐下">♟<span>动作</span></button><button class="action-item demo-btn" data-toast="设置快捷表情"><span>配置</span></button></div>',
system: '<p class="sheet-note">系统菜单从右上角进入,采用右侧抽屉;设置和帮助进入全屏面板,离开游戏使用确认弹窗。</p><div class="action-grid"><button class="action-item demo-btn" data-panel="options"><b>⚙</b><span>设置</span></button><button class="action-item demo-btn" data-panel="help"><b>?</b><span>帮助</span></button><button class="action-item demo-btn" data-toast="打开角色选择">♜<span>角色</span></button><button class="action-item demo-btn" data-toast="打开退出确认">×<span>退出</span></button></div>',
options: '<p class="sheet-note">设置分为音频、画面、操作和社交屏蔽;滑动条与开关都在全屏面板中完成。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="调整音乐音量">♫<span>音乐</span></button><button class="action-item demo-btn" data-toast="调整音效音量">♪<span>音效</span></button><button class="action-item demo-btn" data-toast="调整镜头距离">◎<span>镜头</span></button><button class="action-item demo-btn" data-toast="保存设置">✓<span>保存</span></button></div>',
help: '<p class="sheet-note">帮助按移动、战斗、任务、社交和系统分类,使用短卡片说明,支持上下滚动。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="查看移动说明">↕<span>移动</span></button><button class="action-item demo-btn" data-toast="查看战斗说明">⚔<span>战斗</span></button><button class="action-item demo-btn" data-toast="查看系统说明">⚙<span>系统</span></button><button class="action-item demo-btn" data-toast="查看快捷操作">✦<span>快捷操作</span></button></div>',
reconnect: '<p class="sheet-note">断线时覆盖场景显示连接状态;恢复成功后自动回到原位置,失败时提供重试或返回角色列表。</p><div class="action-grid"><button class="action-item demo-btn" data-toast="重新连接中">↻<span>重试</span></button><button class="action-item demo-btn" data-toast="查看网络状态">⌁<span>网络</span></button><button class="action-item demo-btn" data-toast="返回角色列表">♜<span>角色列表</span></button><button class="action-item demo-btn" data-toast="关闭提示">×<span>关闭</span></button></div>'
});
const titles = { inventory: '背包', character: '角色', social: '社交', growth: '成长', adventure: '活动', market: '商业', skills: '技能', quest: '任务', map: '地图', party: '社交 / 队伍', friend: '好友', love: '情侣 / 爱意', guild: '公会', target: '目标', chat: '聊天', shop: '商店', mall: '商城', safebox: '仓库', trade: '玩家交易', private_shop: '私人商店', exchange: '兑换', cube: 'Cube 制作', refine: '精炼 / 强化', dragon: '龙魂', fishing: '钓鱼', dungeon: '副本', mount: '坐骑', observer: '观战', emote: '表情动作', system: '系统', help: '帮助', options: '设置', more: '功能菜单' };
const flowDetails = {
login: { kind: '全屏入口', title: '登录界面', entry: '启动游戏后全屏显示;账号密码输入区保持在横屏安全区中央。', touch: '点击输入框唤起键盘,点击登录提交;错误信息在表单下方即时提示。', back: '登录成功进入服务器选择;失败留在当前页并保留已输入账号。', note: '不与战斗 HUD 共存。' },
server: { kind: '全屏入口', title: '服务器 / 频道选择', entry: '登录成功后进入;服务器列表居中,推荐频道置顶。', touch: '点击频道选中,再点击进入;网络状态和人数使用标签显示。', back: '返回登录界面,或进入角色列表。', note: '连接失败直接复用断线重连提示。' },
character_select: { kind: '全屏入口', title: '角色列表与角色管理', entry: '服务器选择后进入;角色卡横向排列。', touch: '点击角色选中,点击创建、删除、改名;删除和改名必须二次确认。', back: '返回服务器选择或进入加载界面。', note: '阵营、职业和等级放在角色卡内,不进入战斗 HUD。' },
loading: { kind: '状态弹窗', title: '加载界面', entry: '角色进入场景、切换地图或副本时覆盖全屏。', touch: '无需操作;加载失败显示重试按钮。', back: '加载完成进入首页 HUD,异常时转断线重连。', note: '避免用户在资源未完成时点击世界。' },
hud: { kind: '常驻 HUD', title: '首页 HUD', entry: '进入场景后的默认页面;世界画面优先。', touch: '左上头像看角色,右上背包/小地图/菜单,任务栏直接进入任务,底部两侧完成移动和战斗。', back: '所有功能面板关闭后回到这里。', note: '名称在头像顶部,HP/MP 条在头像下方且与头像同宽。' },
movement: { kind: '常驻 HUD', title: '移动摇杆', entry: '首页左下角,仅作为战斗操作控件。', touch: '按住并拖动控制方向,松手复位;支持边缘拖动防止拇指滑出。', back: '没有独立页面,松手即返回待机状态。', note: '底部中央保持空白,不放功能菜单。' },
target: { kind: '常驻 HUD', title: '目标选择与目标板', entry: '点击敌人、NPC 或目标板触发;目标板位于顶部中央。', touch: '点击切换目标,长按查看信息;普通攻击和技能自动作用于当前目标。', back: '点击空白清除目标,或选择新目标。', note: '目标血条不与玩家血条混淆。' },
combat_action: { kind: '常驻 HUD', title: '普通攻击与技能', entry: '首页右下角环形战斗盘。', touch: '点击普攻;点击技能释放;长按可预览需要瞄准的技能;冷却用灰度和数字反馈。', back: '释放完成回到战斗状态,技能配置从菜单进入。', note: '药水可作为战斗盘上的物品按钮,不另设底部功能栏。' },
loot: { kind: '场景弹窗', title: '掉落物与情境交互', entry: '靠近掉落物、NPC、钓点或副本入口时,在角色附近出现交互按钮。', touch: '点击拾取、对话、钓鱼或进入;掉落物名称显示在世界中并支持点击。', back: '动作完成回到场景;物品自动进入背包并给出短暂提示。', note: '不在首页固定显示拾取菜单。' },
death: { kind: '场景弹窗', title: '死亡与复活', entry: 'HP 归零后覆盖场景中央。', touch: '点击原地复活、回城或等待;危险选项二次确认。', back: '复活后回到首页 HUD,回城后进入加载流程。', note: '死亡时暂时禁用摇杆和技能按钮。' },
quest: { kind: '常驻 HUD', title: '任务追踪栏', entry: '首页上方常驻显示,不放入右上菜单。', touch: '点击任务行进入日志;点击目标或导航标记进入地图/自动寻路。', back: '关闭任务日志后回到首页,任务进度仍保留。', note: '任务栏是任务系统的唯一首页快捷入口。' },
quest_log: { kind: '全屏面板', title: '任务日志', entry: '点击任务追踪栏打开;采用列表 + 详情布局。', touch: '点击任务切换详情,滚动查看奖励;点击追踪更新首页任务栏。', back: '关闭后返回首页 HUD,不改变当前追踪任务。', note: '任务地图标记从详情页进入。' },
map: { kind: '全屏面板', title: '小地图与大地图', entry: '点击右上小地图进入;没有独立地图按钮。', touch: '双指缩放、拖动地图、点击信标导航,点击定位回到玩家位置。', back: '点击关闭或系统返回回到首页 HUD。', note: '背包已替代原地图快捷按钮。' },
navigation: { kind: '常驻 + 场景', title: '自动寻路', entry: '从任务详情、地图信标或 NPC 目标触发。', touch: '点击导航开始;到达后显示交互按钮;移动摇杆可随时接管。', back: '手动移动、点击停止或到达目的地后结束。', note: '不额外占用常驻 HUD 按钮。' },
npc_quest: { kind: '场景弹窗', title: 'NPC 对话与任务', entry: '靠近 NPC 后点击角色或情境交互按钮。', touch: '点击对话选项;接取/完成任务使用明确的主按钮。', back: '关闭对话回到场景;完成后刷新任务栏和奖励提示。', note: '商店、仓库和任务可以在同一 NPC 对话入口分流。' },
dungeon_result: { kind: '场景弹窗', title: '副本结算', entry: '副本完成或失败后自动弹出。', touch: '滚动查看奖励,点击再次挑战或退出;奖励领取使用主按钮。', back: '退出回到场景,或再次挑战进入加载界面。', note: '副本内保留目标罗盘和队伍状态。' },
character_entry: { kind: '头像入口', title: '角色功能入口', entry: '只点击左上角玩家头像进入。', touch: '点击头像进入角色页,在其中切换属性、装备、技能、坐骑和外观。', back: '关闭角色面板回到首页 HUD。', note: '角色不出现在右上菜单。' },
character_status: { kind: '全屏面板', title: '角色属性', entry: '左上头像 → 角色属性。', touch: '左右切换属性分类,滚动查看等级、属性、经验和资源。', back: '返回角色入口,再返回首页。', note: '战斗中只展示精简状态条。' },
equipment: { kind: '全屏面板', title: '装备界面', entry: '左上头像 → 角色 → 装备。', touch: '点击装备槽查看,长按物品详情,拖动穿戴或替换。', back: '关闭装备页返回角色或首页。', note: '物品对比使用上下对照卡片。' },
skills: { kind: '全屏面板', title: '技能与快捷栏配置', entry: '左上头像 → 角色 → 技能。', touch: '点击技能查看说明,长按查看数值,拖动到首页技能槽。', back: '保存配置后回到首页,技能按钮即时更新。', note: '主动、被动、坐骑、表情分标签显示。' },
skill_upgrade: { kind: '场景弹窗', title: '技能升级 / 加点', entry: '技能详情页点击升级。', touch: '点击加点预览消耗,再点击确认;资源不足显示原因。', back: '确认后刷新技能等级,取消回到技能详情。', note: '升级不可误触,必须二次确认。' },
inspect: { kind: '场景弹窗', title: '查看其他玩家装备', entry: '点击场景玩家后选择查看,或从队伍/好友资料进入。', touch: '点击玩家资料页签查看装备、等级和公会;不可修改对方数据。', back: '关闭资料卡回到场景或社交列表。', note: '交易、私聊、组队使用同一上下文操作栏。' },
inventory: { kind: '全屏面板', title: '背包', entry: '右上背包快捷按钮或菜单 → 背包。', touch: '点击选中,长按详情,拖动整理/装备/放入快捷栏;使用和出售用明确按钮。', back: '关闭返回首页,物品操作结果用 toast 提示。', note: '腰带、装备栏、任务物品作为背包内页。' },
item_tooltip: { kind: '场景弹窗', title: '物品详情提示', entry: '背包、仓库、商店或掉落物点击/长按。', touch: '点击物品查看详情;长按保持详情卡,点击操作按钮执行动作。', back: '点空白或返回关闭详情卡。', note: '详情卡不遮挡关键操作区域。' },
item_action: { kind: '场景弹窗', title: '使用、装备、出售', entry: '物品详情卡底部操作区。', touch: '点击主操作,数量类动作弹出数量选择;出售和稀有物品使用二次确认。', back: '完成后刷新背包,取消回到详情卡。', note: '拖放和点击操作同时支持。' },
safebox: { kind: '全屏面板', title: '仓库与扩展物品栏', entry: '菜单 → 背包 → 仓库,或靠近仓库 NPC 后进入。', touch: '左右切换背包/仓库/商城仓库;拖动物品存入或取出。', back: '关闭面板返回 NPC 对话或首页。', note: '私人商店背包沿用同一物品组件。' },
select_item: { kind: '场景弹窗', title: '任务物品选择', entry: '任务奖励、提交材料或任务详情中的选择按钮。', touch: '点击物品,滑动或加减调整数量,提交前显示所需数量。', back: '确认后回到任务流程,取消不消耗物品。', note: '数量选择不使用难操作的小型输入框。' },
social_entry: { kind: '右上角菜单', title: '社交功能入口', entry: '右上菜单 → 社交;队伍不出现在社交菜单。', touch: '点击聊天、好友、情侣、公会或观战进入对应页面。', back: '关闭社交页面回到首页 HUD。', note: '组队信息固定显示在左上玩家头像下方。' },
chat: { kind: '全屏面板', title: '聊天与战斗日志', entry: '菜单 → 社交 → 聊天;新消息以短暂提示或气泡显示。', touch: '点击频道切换,点击输入框唤起键盘,长按消息进行复制/私聊。', back: '关闭回到首页,未读数显示在菜单角标。', note: '综合、私聊、系统、战斗、队伍、公会统一在频道中。' },
party: { kind: '左侧 HUD', title: '队伍信息', entry: '组队后显示在左上玩家头像下方;未组队时自动隐藏。', touch: '点击队员查看资料或定位,长按打开队伍设置;邀请通过好友或玩家交互触发。', back: '关闭队员卡回到首页,队伍血量和距离继续常驻显示。', note: '队伍不出现在社交菜单,也不设置独立首页按钮。' },
friend: { kind: '全屏面板', title: '好友与好友申请', entry: '菜单 → 社交 → 好友,或点击玩家资料。', touch: '点击申请、同意、私聊、查看资料;列表上下滚动。', back: '返回社交菜单或首页。', note: '好友申请通过临时弹窗提醒。' },
love: { kind: '全屏面板', title: '情侣 / 爱意', entry: '菜单 → 社交 → 情侣 / 爱意。', touch: '点击关系资料和互动动作;关键关系操作使用确认。', back: '关闭返回社交或首页。', note: '低频关系信息不占用首页 HUD。' },
guild: { kind: '全屏面板', title: '公会', entry: '菜单 → 社交 → 公会。', touch: '顶部标签切换成员、技能、公告、公会战和会徽;列表支持滚动。', back: '返回社交菜单或首页。', note: '公会邀请通过临时确认弹窗进入。' },
commerce_entry: { kind: '场景弹窗', title: 'NPC / 玩家情境入口', entry: '靠近 NPC、玩家、摊位或制作设施后显示交互按钮。', touch: '点击交互按钮,从上下文菜单选择对话、商店、交易、仓库或制作。', back: '关闭上下文菜单返回场景。', note: '只在附近有可交互对象时出现。' },
shop: { kind: '全屏面板', title: 'NPC 商店与商城', entry: 'NPC 交互 → 商店,或菜单 → 商店 / 商城。', touch: '点击商品查看详情,数量选择后购买/出售;支付前二次确认。', back: '返回 NPC 对话或首页。', note: '商城使用同一套物品详情和确认组件。' },
trade: { kind: '全屏面板', title: '玩家交易', entry: '点击玩家 → 交易,或场景交互菜单进入。', touch: '拖动物品到交易栏,双方锁定后分别确认;内容变化时重新确认。', back: '取消交易返回场景,已锁定物品自动退回。', note: '交易金额和物品变化必须明显反馈。' },
private_shop: { kind: '全屏面板', title: '私人商店', entry: '点击玩家摊位进入浏览;自己的摊位从菜单管理。', touch: '点击商品购买,管理页拖动物品上架;上架/下架有确认提示。', back: '关闭返回场景,摊位状态保留。', note: '使用背包的物品组件,减少学习成本。' },
cube: { kind: '全屏面板', title: 'Cube 制作', entry: '菜单 → 交易与制作 → Cube,或靠近制作设施。', touch: '选择配方,拖入材料,查看产出预览,点击制作。', back: '结果弹窗关闭后回到制作页或首页。', note: '制作按钮放在面板底部,不占用首页底部。' },
refine: { kind: '全屏 + 弹窗', title: '精炼 / 强化 / 龙魂', entry: '菜单 → 交易与制作 → 精炼,龙魂作为同组页签。', touch: '选择装备、查看材料和成功率,确认强化;结果用中央弹窗反馈。', back: '返回制作菜单或首页,失败也明确显示消耗。', note: '龙魂当前代码入口需单独开启后再接入正式菜单。' },
system_entry: { kind: '右上角菜单', title: '系统入口', entry: '右上角菜单最外侧按钮。', touch: '点击菜单打开右侧抽屉,点击遮罩或关闭按钮收起。', back: '回到首页 HUD,世界输入恢复。', note: '菜单打开时暂停战斗点击,防止误操作。' },
options: { kind: '全屏面板', title: '系统设置与游戏设置', entry: '菜单 → 系统 → 设置。', touch: '滑动音量、镜头距离等,点击开关控制 PK、名称、伤害数字和屏蔽项。', back: '即时保存后返回菜单或首页。', note: '设置项按音频、画面、操作、社交分组。' },
help: { kind: '全屏面板', title: '帮助界面', entry: '菜单 → 系统 → 帮助。', touch: '点击分类卡片,上下滚动查看说明;可从快捷入口直接跳转。', back: '返回系统菜单或首页。', note: '帮助文案与实际移动端手势保持一致。' },
character_switch: { kind: '全屏 + 弹窗', title: '选择角色', entry: '菜单 → 系统 → 选择角色。', touch: '点击后先保存并弹出确认,再回到角色列表。', back: '取消确认继续当前角色。', note: '切换角色触发加载流程。' },
logout: { kind: '场景弹窗', title: '登出 / 退出游戏', entry: '菜单 → 系统 → 登出或退出。', touch: '点击后弹出确认,显示未保存状态;确认后返回登录或关闭游戏。', back: '取消后回到系统菜单。', note: '属于高风险操作,必须二次确认。' },
reconnect: { kind: '状态弹窗', title: '断线重连', entry: '网络中断时覆盖当前页面,不清除当前 HUD 状态。', touch: '自动重试;用户可点击重试、查看网络或返回角色列表。', back: '连接成功恢复原场景,失败则进入角色列表。', note: '恢复后保留任务追踪、目标和队伍状态。' }
};
function showToast(message) {
toast.textContent = message;
toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toast.classList.remove('show'), 1400);
}
function currentVariantRoot(el) { return el.closest('.variant'); }
function renderPanel(root, key, history = []) {
const modal = root.querySelector('[data-modal]');
const title = root.querySelector('[data-sheet-title]');
const bodyNode = root.querySelector('[data-sheet-body]');
const back = root.querySelector('[data-back]');
title.textContent = titles[key] || key;
bodyNode.innerHTML = featureCopy[key] || `<p class="sheet-note">${titles[key] || key} 面板草图。</p>`;
modal.dataset.current = key;
modal.dataset.history = JSON.stringify(history);
back.classList.toggle('hidden', history.length === 0);
bindDynamic(bodyNode);
}
function openPanel(trigger, key) {
const root = currentVariantRoot(trigger);
const modal = root.querySelector('[data-modal]');
const history = modal.classList.contains('open') && modal.dataset.current ? JSON.parse(modal.dataset.history || '[]') : [];
if (modal.classList.contains('open') && modal.dataset.current && modal.dataset.current !== key) history.push(modal.dataset.current);
renderPanel(root, key, history);
modal.classList.add('open');
}
function closePanel(trigger) {
const modal = trigger.closest('[data-modal]');
modal.classList.remove('open');
modal.dataset.current = '';
modal.dataset.history = '[]';
}
function backPanel(trigger) {
const modal = trigger.closest('[data-modal]');
const history = JSON.parse(modal.dataset.history || '[]');
const key = history.pop();
if (!key) return;
renderPanel(trigger.closest('.variant'), key, history);
}
function bindDynamic(scope = document) {
scope.querySelectorAll('[data-toast]').forEach(btn => {
if (btn.dataset.bound) return;
btn.dataset.bound = '1';
btn.addEventListener('click', () => showToast(btn.dataset.toast));
});
const send = scope.querySelector('[data-send]');
if (send && !send.dataset.bound) {
send.dataset.bound = '1';
send.addEventListener('click', () => {
const input = scope.querySelector('input');
if (!input.value.trim()) { showToast('请输入聊天内容'); return; }
showToast('消息已发送'); input.value = '';
});
}
}
const flowDetail = document.getElementById('flow-detail');
function renderFlowDetail(key) {
const detail = flowDetails[key];
if (!detail) return;
flowDetail.classList.remove('empty');
flowDetail.innerHTML = `<div class="detail-kicker">${detail.kind}</div><h3>${detail.title}</h3><div class="detail-grid"><div class="detail-block"><strong>入口 / 所在位置</strong><p>${detail.entry}</p></div><div class="detail-block"><strong>移动端触控</strong><p>${detail.touch}</p></div><div class="detail-block"><strong>返回路径</strong><p>${detail.back}</p></div></div><p class="flow-note">设计备注:${detail.note}</p>`;
flowDetail.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
document.querySelectorAll('[data-detail]').forEach(node => node.addEventListener('click', () => renderFlowDetail(node.dataset.detail)));
document.querySelectorAll('[data-flow-filter]').forEach(filter => filter.addEventListener('click', () => {
const value = filter.dataset.flowFilter;
document.querySelectorAll('[data-flow-filter]').forEach(item => {
const active = item === filter;
item.classList.toggle('active', active);
item.setAttribute('aria-selected', active ? 'true' : 'false');
});
document.querySelectorAll('.flow-card').forEach(card => card.classList.toggle('is-hidden', value !== 'all' && card.dataset.flowGroup !== value));
showToast(value === 'all' ? '显示全部交互流程' : '已筛选交互流程');
}));
document.addEventListener('click', event => {
const btn = event.target.closest('.demo-btn');
if (btn) {
if (btn.dataset.panel) openPanel(btn, btn.dataset.panel);
if (btn.dataset.skill) {
showToast(btn.dataset.toast || ('触发:' + btn.dataset.skill));
if (btn.dataset.skill !== '普攻') {
btn.classList.add('cooling');
const badge = btn.querySelector('em');
if (badge) badge.textContent = '3';
setTimeout(() => { btn.classList.remove('cooling'); if (badge) badge.textContent = ''; }, 900);
}
}
}
const close = event.target.closest('[data-close]');
if (close) closePanel(close);
const back = event.target.closest('[data-back]');
if (back) backPanel(back);
if (event.target.matches('[data-modal]')) closePanel(event.target.querySelector('[data-close]'));
});
document.querySelectorAll('[data-viewport]').forEach(btn => btn.addEventListener('click', () => {
canvas.className = 'preview-' + btn.dataset.viewport;
showToast('预览尺寸:' + btn.textContent);
}));
document.getElementById('annotation-toggle').addEventListener('click', event => {
body.classList.toggle('annotate');
event.currentTarget.textContent = body.classList.contains('annotate') ? '关闭标注' : '标注';
showToast(body.classList.contains('annotate') ? '标注模式已开启' : '标注模式已关闭');
});
const annotation = document.getElementById('annotation-label');
document.addEventListener('mousemove', event => {
if (!body.classList.contains('annotate')) return;
const el = event.target.closest('[data-label]');
if (!el) { annotation.style.display = 'none'; return; }
annotation.textContent = el.dataset.label;
annotation.style.left = Math.min(window.innerWidth - 180, event.clientX + 12) + 'px';
annotation.style.top = Math.min(window.innerHeight - 40, event.clientY + 12) + 'px';
annotation.style.display = 'block';
});
document.querySelectorAll('[data-joystick]').forEach(joystick => {
let active = false;
const stick = joystick.querySelector('.stick');
const move = event => {
if (!active) return;
const rect = joystick.getBoundingClientRect();
const x = (event.clientX ?? event.touches?.[0]?.clientX) - (rect.left + rect.width / 2);
const y = (event.clientY ?? event.touches?.[0]?.clientY) - (rect.top + rect.height / 2);
const limit = 26, length = Math.max(1, Math.hypot(x, y)), scale = Math.min(1, limit / length);
stick.style.transform = `translate(calc(-50% + ${x * scale}px), calc(-50% + ${y * scale}px))`;
};
const release = () => { active = false; joystick.classList.remove('is-active'); stick.style.transform = 'translate(-50%, -50%)'; showToast('移动摇杆已复位'); };
joystick.addEventListener('pointerdown', event => { active = true; joystick.classList.add('is-active'); joystick.setPointerCapture(event.pointerId); move(event); });
joystick.addEventListener('pointermove', move); joystick.addEventListener('pointerup', release); joystick.addEventListener('pointercancel', release);
});
bindDynamic();
</script>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
# Sketch Manifest
## Design Direction
面向移动端 MMORPG 的暗黑系 ARPG 横屏 HUD:让战斗世界保持清晰,把高频操作放在拇指自然覆盖区,底部中央保持留白,把低频系统收纳进菜单和右上入口。整体采用深色玻璃、暖铜金属边框、血红生命条、紫色职业资源与圆形技能盘;任务通过当前任务条直接进入。角色相关功能固定由左上头像进入;组队后队员信息固定显示在该头像下方,队伍不出现在社交菜单。菜单参考暗黑 ARPG 的分组方式,仅保留社交、成长、活动、商业、系统五个一级入口,其余功能最多第二层进入。
## Reference Points
参考用户提供的暗黑系 ARPG 横屏战斗 HUD:左上按名称、头像、HP/MP/资源短条纵向排列,顶部中央目标条,右上按背包、小地图、菜单排列,左下虚拟摇杆、右下环形技能盘;底部中央保持留白。当前草图以项目已有的移动、相机缩放、战斗、掉落物交互、聊天、背包、任务和社交入口为内容基础,并补充进入游戏、任务副本、角色物品、社交经济、系统状态等全量交互图;使用自绘占位符,不复用原始游戏素材。
## Sketches
| # | Name | Design Question | Winner | Tags |
|---|------|-----------------|--------|------|
| 001 | mobile-hud | 暗黑系 ARPG 横屏战斗 HUD 如何兼顾 MMORPG 的完整系统入口? | single | mobile, landscape, hud, touch, combat, arpg, mmo, dark-glass |
+40
View File
@@ -0,0 +1,40 @@
:root {
--color-bg: #140e0b;
--color-surface: rgba(29, 20, 16, 0.78);
--color-surface-strong: rgba(17, 12, 10, 0.94);
--color-surface-soft: rgba(178, 132, 73, 0.14);
--color-border: rgba(191, 148, 84, 0.34);
--color-border-bright: rgba(238, 199, 126, 0.72);
--color-text: #f5ead7;
--color-text-muted: #b7a58e;
--color-primary: #dfb86e;
--color-primary-hover: #f5d998;
--color-accent: #f0c979;
--color-danger: #c83f39;
--color-success: #73bb7d;
--color-mana: #7957ed;
--color-energy: #b468f4;
--color-glass-highlight: rgba(255, 231, 185, 0.18);
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "SFMono-Regular", Consolas, monospace;
--text-xs: 0.7rem;
--text-sm: 0.78rem;
--text-base: 0.92rem;
--text-lg: 1.1rem;
--text-xl: 1.35rem;
--text-2xl: 1.7rem;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
--space-12: 48px;
--radius-sm: 10px;
--radius-md: 16px;
--radius-lg: 24px;
--radius-full: 9999px;
--shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.2);
--shadow-md: 0 12px 28px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 18px 44px rgba(0, 0, 0, 0.5);
}
+4
View File
@@ -24,6 +24,10 @@
[`docs/SHINSOO-WORLD-RENDERING.md`](docs/SHINSOO-WORLD-RENDERING.md)(正式移植,Phase 2 bring-up 之后);
距参考图「目视等价」还差哪些保真化工作见 [`docs/PARITY-GAP.md`](docs/PARITY-GAP.md)。
当前进入游戏、模型/发型、贴地与技能栏问题的分阶段修复步骤,以及结合两个 Godot 项目的
对照验证和 macOS 包验收要求,见 [`docs/RENDERING-REPAIR-PLAN.md`](docs/RENDERING-REPAIR-PLAN.md)
(2026-09-08,实施计划;不表示其中工作已完成)。
---
## 现状
+27
View File
@@ -0,0 +1,27 @@
# Native tree geometry
These 87 GLBs are offline SpeedTreeRT extractions of this workspace's existing
SPT assets, not procedural replacements. Their embedded bark/composite textures
and meshes are game artwork: the same internal-use/licensing restrictions as
`assets/Tree` apply. Do not treat them as freely redistributable open-source art.
`manifest.json` maps original SPT SHA-256 to the GLB filename and SHA-256.
Both hashes are checked by `script/stage_tree_geometry.mjs`; the runtime also
verifies both before loading. The source conversion index lives in the sibling
`metin2-godot-main/generated/content-v1/40250-full-unpacked-final` workspace.
No proprietary Windows DLL is shipped or loaded by the Mac client.
Coordinates are already Godot Y-up metres. Keep original height, origin, UVs,
branches, fronds and leaf cards; do not apply the former proxy's 12m scale.
Leaf materials use double-sided alpha scissor with cutoff 0.3.
This is real **static** geometry: full SpeedTree wind, camera-facing leaf
clusters and native LOD transitions are not reconstructed by these GLBs.
Validation: all 87 species import; all 81 species referenced by current static
AreaData have a conversion; six principal maps render 2,092 native trees with
zero proxies. The 31 legacy SPTs under `Zone/ymir work/zone/b/tree` have different
source hashes and are not converted or aliased to these meshes. They are not
referenced by current tree property records. One unused historical property
references missing `christmastree.spt`; the actual `christmastree1/2/3.spt`
variants are converted. Seasonal event spawning is separate from static maps.
+963
View File
@@ -0,0 +1,963 @@
{
"schema_version": 1,
"coordinates": "godot_y_up_meters",
"trees": {
"1d20b68d50ce007b4814da28936b1992ccb399f1c264df3a5dcaa8b92de5be5e": {
"source": "Tree/ymir work/tree/b1_baobab_rt.spt",
"source_sha256": "1d20b68d50ce007b4814da28936b1992ccb399f1c264df3a5dcaa8b92de5be5e",
"glb": "1d20b68d50ce007b4814da28936b1992ccb399f1c264df3a5dcaa8b92de5be5e.glb",
"glb_sha256": "642c31b558023112fc6c5fba9955c235d39d97a7c05eb56613bd76a4cf33e83e",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"9e5b696ab020cd18232e18c866454c5d52deb1076998db0a1fbf8703d1599741": {
"source": "Tree/ymir work/tree/b1_baobab_rt2.spt",
"source_sha256": "9e5b696ab020cd18232e18c866454c5d52deb1076998db0a1fbf8703d1599741",
"glb": "9e5b696ab020cd18232e18c866454c5d52deb1076998db0a1fbf8703d1599741.glb",
"glb_sha256": "4a4d6013668a084dc3b00d44e9cc7474457ddd1f06e64229a348ff8f0a445326",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"de17c0c13f956cf75d92997063a12c5dbca1a26234ad14a5f868a03362a11b48": {
"source": "Tree/ymir work/tree/b1_beech_rt.spt",
"source_sha256": "de17c0c13f956cf75d92997063a12c5dbca1a26234ad14a5f868a03362a11b48",
"glb": "de17c0c13f956cf75d92997063a12c5dbca1a26234ad14a5f868a03362a11b48.glb",
"glb_sha256": "18f49b44e9b435f91da8b75958aab7aef4b0d368301da7b3789f51e0570481ac",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"cdb81537efcda4c26781463a6018d59c42edcb12ca3d5b724c029810bf434de9": {
"source": "Tree/ymir work/tree/b1_beech_rt2.spt",
"source_sha256": "cdb81537efcda4c26781463a6018d59c42edcb12ca3d5b724c029810bf434de9",
"glb": "cdb81537efcda4c26781463a6018d59c42edcb12ca3d5b724c029810bf434de9.glb",
"glb_sha256": "9da1a6ab558c70dda232c36a2bd85bb710d24031149dac0198af3532b0bd4145",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"9d7ee35f949b4764090b1a916ade1005b622b31fea49b2d4b8586076711c8fc7": {
"source": "Tree/ymir work/tree/b1_beech_rt3.spt",
"source_sha256": "9d7ee35f949b4764090b1a916ade1005b622b31fea49b2d4b8586076711c8fc7",
"glb": "9d7ee35f949b4764090b1a916ade1005b622b31fea49b2d4b8586076711c8fc7.glb",
"glb_sha256": "edf0fbac525cfc48f87527aa25d3f49df6f010ee51f56ae78e0ac69d00aa4ba5",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"25d677d269f8cfc38ca1cd3f692810cb293ab13db46b576b278cc695f7cf3f16": {
"source": "Tree/ymir work/tree/b1_beech_rt4.spt",
"source_sha256": "25d677d269f8cfc38ca1cd3f692810cb293ab13db46b576b278cc695f7cf3f16",
"glb": "25d677d269f8cfc38ca1cd3f692810cb293ab13db46b576b278cc695f7cf3f16.glb",
"glb_sha256": "edc5c99aab45da766baa28ee402086b2b107326ec52d81b3324c06ad38def6ee",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"66392538f04a55d484f9c92cdc9d19d21341f499f30162d288e75812669f1f04": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt.spt",
"source_sha256": "66392538f04a55d484f9c92cdc9d19d21341f499f30162d288e75812669f1f04",
"glb": "66392538f04a55d484f9c92cdc9d19d21341f499f30162d288e75812669f1f04.glb",
"glb_sha256": "9abdfc7f413b5aa4117f36106720dc979ebfa4c647c28a833904aa4d9e713466",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0b90a99d6480452b12b5f78a9b61e453180ec4a83f4246bdf1b91c02b949f4e8": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt2.spt",
"source_sha256": "0b90a99d6480452b12b5f78a9b61e453180ec4a83f4246bdf1b91c02b949f4e8",
"glb": "0b90a99d6480452b12b5f78a9b61e453180ec4a83f4246bdf1b91c02b949f4e8.glb",
"glb_sha256": "acd4675f30e805d5543c17f0a7a52fe37f31f6e67874b196b7395997e1ea8e83",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"f6691c8b816eea9bc8c49189d7f780f531ed9ec82d432fbba3a847d0776c27db": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt3.spt",
"source_sha256": "f6691c8b816eea9bc8c49189d7f780f531ed9ec82d432fbba3a847d0776c27db",
"glb": "f6691c8b816eea9bc8c49189d7f780f531ed9ec82d432fbba3a847d0776c27db.glb",
"glb_sha256": "56effe5240c0cf44f74c033412fa2bf25a8329be74c66a62da3d2288ae24ab10",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d7ca320772595a77c28856c45a36674e18918b9e7bc5101cd5cb8cdf12de27e1": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt4.spt",
"source_sha256": "d7ca320772595a77c28856c45a36674e18918b9e7bc5101cd5cb8cdf12de27e1",
"glb": "d7ca320772595a77c28856c45a36674e18918b9e7bc5101cd5cb8cdf12de27e1.glb",
"glb_sha256": "d371eba315defbfb6956cc8cfbd26dde7ff9240d21f832c37a6fc63559bfcca2",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"cb9102770aca71a6e269e806c6e6e74e5d4cb8096abbf078180ab27c8e1367c3": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt5.spt",
"source_sha256": "cb9102770aca71a6e269e806c6e6e74e5d4cb8096abbf078180ab27c8e1367c3",
"glb": "cb9102770aca71a6e269e806c6e6e74e5d4cb8096abbf078180ab27c8e1367c3.glb",
"glb_sha256": "28c7a3e30c21d3874247e10a0bf6107de1775d29b0effa664edeb743c036f953",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2851666e8f3556aa6606642bcd529672ab45b0f8db1304d6fbb4466900a2ca5a": {
"source": "Tree/ymir work/tree/b1_pagodatree_rt.spt",
"source_sha256": "2851666e8f3556aa6606642bcd529672ab45b0f8db1304d6fbb4466900a2ca5a",
"glb": "2851666e8f3556aa6606642bcd529672ab45b0f8db1304d6fbb4466900a2ca5a.glb",
"glb_sha256": "0af3909bf5fe30ae77f918e7641e12791b8c2d3f40ec576aa375b3005bb8f528",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d35194db82fe4ca4a7668f95d5d95f116ab6b08c326b7baf0c0ab074276db953": {
"source": "Tree/ymir work/tree/b1_pagodatree_rt2.spt",
"source_sha256": "d35194db82fe4ca4a7668f95d5d95f116ab6b08c326b7baf0c0ab074276db953",
"glb": "d35194db82fe4ca4a7668f95d5d95f116ab6b08c326b7baf0c0ab074276db953.glb",
"glb_sha256": "a6546ab05a6722bf2eaefc0b23ae399d97050a280631488bb82d651ed131ea52",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"a566eff0304a67971167e4e7d3bf254ab0c7a33478fcd657cec613c16058e900": {
"source": "Tree/ymir work/tree/b1_pagodatree_rt3.spt",
"source_sha256": "a566eff0304a67971167e4e7d3bf254ab0c7a33478fcd657cec613c16058e900",
"glb": "a566eff0304a67971167e4e7d3bf254ab0c7a33478fcd657cec613c16058e900.glb",
"glb_sha256": "fd60281f1384d80b51f838ee512e5d7506fef17d8ad9c28c962a4970e5e02be6",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"1bc5e65c0e4efcb60f7ea930f683b80001b8dd97762b6d25281946a295685149": {
"source": "Tree/ymir work/tree/b1_sassafras_rt_fall.spt",
"source_sha256": "1bc5e65c0e4efcb60f7ea930f683b80001b8dd97762b6d25281946a295685149",
"glb": "1bc5e65c0e4efcb60f7ea930f683b80001b8dd97762b6d25281946a295685149.glb",
"glb_sha256": "69ba5cd343cb193e12d5fa9203356e08e5536e6a0083650ca4be88f87472fcf8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"5c6dabb419b86756136240a32b532746d6f86759c8e744f1701c24bcb840262a": {
"source": "Tree/ymir work/tree/b1_sassafras_rt_fall2.spt",
"source_sha256": "5c6dabb419b86756136240a32b532746d6f86759c8e744f1701c24bcb840262a",
"glb": "5c6dabb419b86756136240a32b532746d6f86759c8e744f1701c24bcb840262a.glb",
"glb_sha256": "14cb7ad036580d3387112e2098d52ee6815d158c533ed52f3db0604a00c33b09",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d76530cfba5e75a94a9634fd76f86a02126c22bd51cccfc3d995eff8974742f2": {
"source": "Tree/ymir work/tree/b2_cedaroflebanon_rt.spt",
"source_sha256": "d76530cfba5e75a94a9634fd76f86a02126c22bd51cccfc3d995eff8974742f2",
"glb": "d76530cfba5e75a94a9634fd76f86a02126c22bd51cccfc3d995eff8974742f2.glb",
"glb_sha256": "d8df7faf73265c6a5b2a725cfacab421b609cd9a4bd2bad41997e349e9ee4fa7",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"73d757a146ca27a9459ccc3027cd9464ce415f1a6731771cbff37827a3ee1caa": {
"source": "Tree/ymir work/tree/b2_cedaroflebanon_rt2.spt",
"source_sha256": "73d757a146ca27a9459ccc3027cd9464ce415f1a6731771cbff37827a3ee1caa",
"glb": "73d757a146ca27a9459ccc3027cd9464ce415f1a6731771cbff37827a3ee1caa.glb",
"glb_sha256": "c6c15c69d0b60f232293e74f8da191b3ae62f4a69fba61d0e7fe94008c51176e",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"fd2e5bb94c5223e1eaca97544e757e135252edb05f0ddfa991e3abad3fb78b3f": {
"source": "Tree/ymir work/tree/b2_cedaroflebanon_rt3.spt",
"source_sha256": "fd2e5bb94c5223e1eaca97544e757e135252edb05f0ddfa991e3abad3fb78b3f",
"glb": "fd2e5bb94c5223e1eaca97544e757e135252edb05f0ddfa991e3abad3fb78b3f.glb",
"glb_sha256": "81e2b1c1cd753ba17e8d875bc5ad0104c0f2a8cc1831d4decbc64d7a82fb1202",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"50557c6da89615d82f9d81799315549bed25e9ae09dd128a6f161f83a17f04f5": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt.spt",
"source_sha256": "50557c6da89615d82f9d81799315549bed25e9ae09dd128a6f161f83a17f04f5",
"glb": "50557c6da89615d82f9d81799315549bed25e9ae09dd128a6f161f83a17f04f5.glb",
"glb_sha256": "d632d027f5e228e6d85a986068ada05996461692623dc082569b9078965af7dd",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d012fcd397e8dac1d1778b3ad700ee36f08d282bd53ca6466a7232824e453f5a": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt2.spt",
"source_sha256": "d012fcd397e8dac1d1778b3ad700ee36f08d282bd53ca6466a7232824e453f5a",
"glb": "d012fcd397e8dac1d1778b3ad700ee36f08d282bd53ca6466a7232824e453f5a.glb",
"glb_sha256": "ce88fcfb1f04048912665592e0ad4da69679163c23b8b2f541641bad7e319f2d",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"065babe8c785f54d23b99e91e9d505b91eadaae1dff492bcfe236b657acf9272": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt3.spt",
"source_sha256": "065babe8c785f54d23b99e91e9d505b91eadaae1dff492bcfe236b657acf9272",
"glb": "065babe8c785f54d23b99e91e9d505b91eadaae1dff492bcfe236b657acf9272.glb",
"glb_sha256": "aea108dfb8fcaf7021b54df8c27a194ac5486fac08fb32dbc15c00b7834d2596",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"61f67291ff5d7c49014991dddb3d03550ae18e157f1acf62daf249ca40206bf3": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt4.spt",
"source_sha256": "61f67291ff5d7c49014991dddb3d03550ae18e157f1acf62daf249ca40206bf3",
"glb": "61f67291ff5d7c49014991dddb3d03550ae18e157f1acf62daf249ca40206bf3.glb",
"glb_sha256": "fcd95e7569a5a6d8af5e987f3764d520d941fa518827ae247514b003c2de5a87",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"f40054b4e07fdc6071f8b5bd6ad85782d5207c51fa116bfb8e0aca4d8675badd": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt5.spt",
"source_sha256": "f40054b4e07fdc6071f8b5bd6ad85782d5207c51fa116bfb8e0aca4d8675badd",
"glb": "f40054b4e07fdc6071f8b5bd6ad85782d5207c51fa116bfb8e0aca4d8675badd.glb",
"glb_sha256": "0824f3fc789c420d6eb543cf932f617eabb8f01437e5bf6ef9aacf116e0eca83",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"80422882e2d1ec63f6c5080c9c308634b8e8acc1342ebe1ebc74039fe2384631": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt_winter.spt",
"source_sha256": "80422882e2d1ec63f6c5080c9c308634b8e8acc1342ebe1ebc74039fe2384631",
"glb": "80422882e2d1ec63f6c5080c9c308634b8e8acc1342ebe1ebc74039fe2384631.glb",
"glb_sha256": "f2a4fa095e0d7ca96a49afc371b9f9189a5b370ca4543827fc3098971b779a31",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"8c9d519de4a1652c2b923e11927be3818942235052cc67e6825d9705a09feaeb": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt_winter2.spt",
"source_sha256": "8c9d519de4a1652c2b923e11927be3818942235052cc67e6825d9705a09feaeb",
"glb": "8c9d519de4a1652c2b923e11927be3818942235052cc67e6825d9705a09feaeb.glb",
"glb_sha256": "97a8e81c46cf1f5dbe8084535e17286c7df07e7b71d61ae7c591adff1dd62fda",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"fa2b9ac46f0f3be36f386fb8630f034393bde37d9cc5b2ec8b38470bdbb14c59": {
"source": "Tree/ymir work/tree/b2_japanesemaple_rt_fall.spt",
"source_sha256": "fa2b9ac46f0f3be36f386fb8630f034393bde37d9cc5b2ec8b38470bdbb14c59",
"glb": "fa2b9ac46f0f3be36f386fb8630f034393bde37d9cc5b2ec8b38470bdbb14c59.glb",
"glb_sha256": "34831b921865e35889704938333c80d69daaba09762e4fe8351c673811053dea",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"86b5d9065583eabca29ad89b7ccb67ac640bada57af8504c6448027fa05bd655": {
"source": "Tree/ymir work/tree/b2_japanesemaple_rt_fall2.spt",
"source_sha256": "86b5d9065583eabca29ad89b7ccb67ac640bada57af8504c6448027fa05bd655",
"glb": "86b5d9065583eabca29ad89b7ccb67ac640bada57af8504c6448027fa05bd655.glb",
"glb_sha256": "f38ffdbebb6f60e4e92d3b8993cc3001c502a8223c72bfb3688812d9217a67b2",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"13292b7af3472178d07e40005acd73b09d61f1d9f9570606cc74f115a42a1637": {
"source": "Tree/ymir work/tree/b2_redoak_rt.spt",
"source_sha256": "13292b7af3472178d07e40005acd73b09d61f1d9f9570606cc74f115a42a1637",
"glb": "13292b7af3472178d07e40005acd73b09d61f1d9f9570606cc74f115a42a1637.glb",
"glb_sha256": "6fc61c59dc838a768f10b749a9b7ce1ea659e4a9d6f9fc6ee6c887c0532adc6c",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"55ee1c716113b30ebc1e4166698765f041f1c094bf28d4ec86f8ca61202e5116": {
"source": "Tree/ymir work/tree/b2_redoak_rt2.spt",
"source_sha256": "55ee1c716113b30ebc1e4166698765f041f1c094bf28d4ec86f8ca61202e5116",
"glb": "55ee1c716113b30ebc1e4166698765f041f1c094bf28d4ec86f8ca61202e5116.glb",
"glb_sha256": "3200e730f4551792a504f20e4046e7bcf466e1e51d90229c9328b3ea63bc47cb",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"c50b043a6ceed03fbdac17cd0a861b1de1d06f4c5933a9078d073f42867e1a22": {
"source": "Tree/ymir work/tree/b2_redoak_rt3.spt",
"source_sha256": "c50b043a6ceed03fbdac17cd0a861b1de1d06f4c5933a9078d073f42867e1a22",
"glb": "c50b043a6ceed03fbdac17cd0a861b1de1d06f4c5933a9078d073f42867e1a22.glb",
"glb_sha256": "6781839f9fcdcdbae7b156b5708436e2580781a11bb784ff11f5f088a841471a",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0d62629ce2423bdfce51595038cab213dec628acb4cf063b0b271279a28e8832": {
"source": "Tree/ymir work/tree/b2_redoak_rt4.spt",
"source_sha256": "0d62629ce2423bdfce51595038cab213dec628acb4cf063b0b271279a28e8832",
"glb": "0d62629ce2423bdfce51595038cab213dec628acb4cf063b0b271279a28e8832.glb",
"glb_sha256": "febe8dcbe1880cec8f91dc5844a55749c8d183e86c1b581d07440f6bf38ab794",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"eace2fb1af759da813559a33d38690596a066397983994cfea75232441ca3086": {
"source": "Tree/ymir work/tree/b3_beech_rt.spt",
"source_sha256": "eace2fb1af759da813559a33d38690596a066397983994cfea75232441ca3086",
"glb": "eace2fb1af759da813559a33d38690596a066397983994cfea75232441ca3086.glb",
"glb_sha256": "1d29e58f3e04b4dbfe4cbfb91403bddcfed2af0f282636f506d358b5f15cccb1",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"65846f0ea0ac8ace155a11cc6a41631d3330e6db25ba9a9c8b8171b96fdfde67": {
"source": "Tree/ymir work/tree/b3_beech_rt2.spt",
"source_sha256": "65846f0ea0ac8ace155a11cc6a41631d3330e6db25ba9a9c8b8171b96fdfde67",
"glb": "65846f0ea0ac8ace155a11cc6a41631d3330e6db25ba9a9c8b8171b96fdfde67.glb",
"glb_sha256": "2bdbe584e694ec4516803ac0966f741cf39920f20c4692b6f5f4496e5fff0b06",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"09c12d2bf8ef2c5ada7d54323b7b11816bebc56cc3fddd7373aba4eed7d3527e": {
"source": "Tree/ymir work/tree/b3_beech_rt3.spt",
"source_sha256": "09c12d2bf8ef2c5ada7d54323b7b11816bebc56cc3fddd7373aba4eed7d3527e",
"glb": "09c12d2bf8ef2c5ada7d54323b7b11816bebc56cc3fddd7373aba4eed7d3527e.glb",
"glb_sha256": "cae93d33e0849b769cb9070bb094239a2d68c33e22942df24836190757c3cd98",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e622c7689d6525c09b22244a9a112ce9448c1c189cbe7f0468da9de82b8d9c36": {
"source": "Tree/ymir work/tree/b3_beech_rt_fall.spt",
"source_sha256": "e622c7689d6525c09b22244a9a112ce9448c1c189cbe7f0468da9de82b8d9c36",
"glb": "e622c7689d6525c09b22244a9a112ce9448c1c189cbe7f0468da9de82b8d9c36.glb",
"glb_sha256": "59e6e7c3378af28c44a3293b1bd026023cbcc9c5447be1684e63a88d9b3691f8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"8aaf7a77ff61d8dd3828057df396ae27a6471faa7db94a6f2c93c0522ce22aa0": {
"source": "Tree/ymir work/tree/b3_beech_rt_fall2.spt",
"source_sha256": "8aaf7a77ff61d8dd3828057df396ae27a6471faa7db94a6f2c93c0522ce22aa0",
"glb": "8aaf7a77ff61d8dd3828057df396ae27a6471faa7db94a6f2c93c0522ce22aa0.glb",
"glb_sha256": "3e684dc00afd7451b28ed3f89a00baa7ea6c9d45c442caad332b9596ed0b872c",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"6600b5cae4c4f473b5cea7e9d90614ffeb3559807d4605d6d729ab7b3182eb77": {
"source": "Tree/ymir work/tree/b3_pagodatree_rt_winter.spt",
"source_sha256": "6600b5cae4c4f473b5cea7e9d90614ffeb3559807d4605d6d729ab7b3182eb77",
"glb": "6600b5cae4c4f473b5cea7e9d90614ffeb3559807d4605d6d729ab7b3182eb77.glb",
"glb_sha256": "bec163b2c1fa99b26d97bad86155a401ab19c067b2acb160a747fceb5b168226",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"ea34cd93e320c38e79837ab348022ea6cab4a62b295fd20a54c7a4a9688129f5": {
"source": "Tree/ymir work/tree/b3_pagodatree_rt_winter2.spt",
"source_sha256": "ea34cd93e320c38e79837ab348022ea6cab4a62b295fd20a54c7a4a9688129f5",
"glb": "ea34cd93e320c38e79837ab348022ea6cab4a62b295fd20a54c7a4a9688129f5.glb",
"glb_sha256": "63aa6ef7fe8181bc1877e86a6b3f1f3bd617a2af303208006289c8e68a76b859",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e03e7c40dd56befae1804550d90f114a1496f7a5c0fa18ae111de5a19372639b": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt.spt",
"source_sha256": "e03e7c40dd56befae1804550d90f114a1496f7a5c0fa18ae111de5a19372639b",
"glb": "e03e7c40dd56befae1804550d90f114a1496f7a5c0fa18ae111de5a19372639b.glb",
"glb_sha256": "54bb8cf813b9c6f05f14ea7dbb39689a3a26f9737e7526969c2bea070b8bf78f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d59d8a429a63272bcd21b403cd581556b57a140f5cdc65c7af329d30b6770d26": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt2.spt",
"source_sha256": "d59d8a429a63272bcd21b403cd581556b57a140f5cdc65c7af329d30b6770d26",
"glb": "d59d8a429a63272bcd21b403cd581556b57a140f5cdc65c7af329d30b6770d26.glb",
"glb_sha256": "6b85b0a649472c7268791a0adce367099b4fbb783d07ad3a508e0aa88d77c331",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2bec6d9317e3947386c56c4183db454fe629af9fa87de759324928e681eb5742": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt3.spt",
"source_sha256": "2bec6d9317e3947386c56c4183db454fe629af9fa87de759324928e681eb5742",
"glb": "2bec6d9317e3947386c56c4183db454fe629af9fa87de759324928e681eb5742.glb",
"glb_sha256": "3a3fc8e8afa93b2a65806fd5d6eb625a05426c6f8a99c50a77ac1c197bf9a6f5",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0a15ca2a0809cc74e6d72ed48b137906f02f0e44994228fa9f840d76a6ca52cc": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt4.spt",
"source_sha256": "0a15ca2a0809cc74e6d72ed48b137906f02f0e44994228fa9f840d76a6ca52cc",
"glb": "0a15ca2a0809cc74e6d72ed48b137906f02f0e44994228fa9f840d76a6ca52cc.glb",
"glb_sha256": "39dc26f0713574b98e8bba0a26ff448bd6fabd97c5b66880f155516f536d35a4",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"dbc300acb7356f4f51e897a2e248a2883241ea570d5d8cded218b31401206534": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt5.spt",
"source_sha256": "dbc300acb7356f4f51e897a2e248a2883241ea570d5d8cded218b31401206534",
"glb": "dbc300acb7356f4f51e897a2e248a2883241ea570d5d8cded218b31401206534.glb",
"glb_sha256": "08dc609a9aa5b16ce943dfd6bc63d2db45daf3bc7252fd6d1ee58a7ce6449685",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0e8ab0197ee0db8e3a40e4b8d6c5c8f0b291284d2ccb7e13cee41d0f0792cc6f": {
"source": "Tree/ymir work/tree/b3_umbrellathorn_rt_flowers.spt",
"source_sha256": "0e8ab0197ee0db8e3a40e4b8d6c5c8f0b291284d2ccb7e13cee41d0f0792cc6f",
"glb": "0e8ab0197ee0db8e3a40e4b8d6c5c8f0b291284d2ccb7e13cee41d0f0792cc6f.glb",
"glb_sha256": "580a3ff0582a7723250650e8b0828912000faf494afb25ace42578e0d1397bb8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"3c5c75535819e405b61cd21c89d514a8d2b0e6afdd75428cc0687745a3314cfe": {
"source": "Tree/ymir work/tree/b3_umbrellathorn_rt_flowers2.spt",
"source_sha256": "3c5c75535819e405b61cd21c89d514a8d2b0e6afdd75428cc0687745a3314cfe",
"glb": "3c5c75535819e405b61cd21c89d514a8d2b0e6afdd75428cc0687745a3314cfe.glb",
"glb_sha256": "2483acf297ce1f35e3ef297ff14e84c33b16bfae885fb04f373a601ad41cfab6",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"a6592779c9460b004be7956123a13f439bef0669dbfdad4bd330b78c44b100d2": {
"source": "Tree/ymir work/tree/b3_umbrellathorn_rt_flowers3.spt",
"source_sha256": "a6592779c9460b004be7956123a13f439bef0669dbfdad4bd330b78c44b100d2",
"glb": "a6592779c9460b004be7956123a13f439bef0669dbfdad4bd330b78c44b100d2.glb",
"glb_sha256": "0d5fe2e83f4621f6cefbbf8e645306af026c540364d05d91e2757d8d4775ceff",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"44ec99e53b3158a6619ec7a5cb4110bfb67ff9943224b068a8bc99fa1d92c0d0": {
"source": "Tree/ymir work/tree/b3_umbrellathorn_rt_flowers4.spt",
"source_sha256": "44ec99e53b3158a6619ec7a5cb4110bfb67ff9943224b068a8bc99fa1d92c0d0",
"glb": "44ec99e53b3158a6619ec7a5cb4110bfb67ff9943224b068a8bc99fa1d92c0d0.glb",
"glb_sha256": "1efd17b58f6dd39b3df3e7de84d20a1d4740e8d53c05a08080a3e335619e6ac8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d85676762825b8324ebeba85bc9a0e2242da8d225c5699c042f469abd17d01d7": {
"source": "Tree/ymir work/tree/christmastree1.spt",
"source_sha256": "d85676762825b8324ebeba85bc9a0e2242da8d225c5699c042f469abd17d01d7",
"glb": "d85676762825b8324ebeba85bc9a0e2242da8d225c5699c042f469abd17d01d7.glb",
"glb_sha256": "6836fef2133fbc14a4ac17f3762cfe64b596e2cc1dbae818c80fe4eb421a510f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"6277b6d6dd747b0a6aa067fc2a27e8e7dad3bd8c9c755d6b124bc474b4107a1d": {
"source": "Tree/ymir work/tree/christmastree2.spt",
"source_sha256": "6277b6d6dd747b0a6aa067fc2a27e8e7dad3bd8c9c755d6b124bc474b4107a1d",
"glb": "6277b6d6dd747b0a6aa067fc2a27e8e7dad3bd8c9c755d6b124bc474b4107a1d.glb",
"glb_sha256": "451bd2edca71c9aa5c658ce8bc1353b7315530220f1d1458c037d5ea007b2794",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"074755f44edc3d836c20b8e254c61af805c3e7f3cbe1e65f76bb35bd3d66ca8f": {
"source": "Tree/ymir work/tree/christmastree3.spt",
"source_sha256": "074755f44edc3d836c20b8e254c61af805c3e7f3cbe1e65f76bb35bd3d66ca8f",
"glb": "074755f44edc3d836c20b8e254c61af805c3e7f3cbe1e65f76bb35bd3d66ca8f.glb",
"glb_sha256": "6445e98dd9760e3371dbcd13673b539b2c6be6450dc883605c50b3d5fac3f825",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0ce9fe068b0a227cb4ce1c36dbeff5792ae6488f012c028b54ab89d7ef2b2f3b": {
"source": "Tree/ymir work/tree/n1_baobab_rt_winter.spt",
"source_sha256": "0ce9fe068b0a227cb4ce1c36dbeff5792ae6488f012c028b54ab89d7ef2b2f3b",
"glb": "0ce9fe068b0a227cb4ce1c36dbeff5792ae6488f012c028b54ab89d7ef2b2f3b.glb",
"glb_sha256": "d49a2346357486cab5368ecb1a32a8e728bd45318b301b86755c41d481f3712f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0d766295707ebd0e6af56bfc49ed1bca8bc11f7b15f79be44e221bbe9e18d53e": {
"source": "Tree/ymir work/tree/n1_beech_rt_winter.spt",
"source_sha256": "0d766295707ebd0e6af56bfc49ed1bca8bc11f7b15f79be44e221bbe9e18d53e",
"glb": "0d766295707ebd0e6af56bfc49ed1bca8bc11f7b15f79be44e221bbe9e18d53e.glb",
"glb_sha256": "031cde20e4c46d5f75cda94fd783834614d31584f7af36e9be2218dabc6a0cbb",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e5497e163459d6ad06745e4b8a5ab95422fb59143e7aa76cf40ac65d242872b3": {
"source": "Tree/ymir work/tree/n1_beech_rt_winter02.spt",
"source_sha256": "e5497e163459d6ad06745e4b8a5ab95422fb59143e7aa76cf40ac65d242872b3",
"glb": "e5497e163459d6ad06745e4b8a5ab95422fb59143e7aa76cf40ac65d242872b3.glb",
"glb_sha256": "f542b8c33350569b80f2a3c88fc1db22be93ed51fbcfe46654d2b29fccaa413a",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"41997e16e6110e21096ffa2d85f0d2135e7b8a4ec82a869f899a6325776a2225": {
"source": "Tree/ymir work/tree/n1_beech_rt_winter03.spt",
"source_sha256": "41997e16e6110e21096ffa2d85f0d2135e7b8a4ec82a869f899a6325776a2225",
"glb": "41997e16e6110e21096ffa2d85f0d2135e7b8a4ec82a869f899a6325776a2225.glb",
"glb_sha256": "fd2fd865a012ba6f53b0bdd0eea8971c7858826cc4cbb08d3a6db1becf027fc0",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"b60fa3d6329d76d4b85da226e88c36241bf8fb4e3a3fc5e1316f0a64d7e2c6ad": {
"source": "Tree/ymir work/tree/n1_coloradobluespruce_rt_01.spt",
"source_sha256": "b60fa3d6329d76d4b85da226e88c36241bf8fb4e3a3fc5e1316f0a64d7e2c6ad",
"glb": "b60fa3d6329d76d4b85da226e88c36241bf8fb4e3a3fc5e1316f0a64d7e2c6ad.glb",
"glb_sha256": "82d906bb624b0041c3d687e3af51cff8ed524ba75e6432a5f293554d5d4ff137",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"265aef90b037111a8d08bd4f258501872be9fc0b4bacf6ad17820241c5f959a1": {
"source": "Tree/ymir work/tree/n1_coloradobluespruce_rt_02.spt",
"source_sha256": "265aef90b037111a8d08bd4f258501872be9fc0b4bacf6ad17820241c5f959a1",
"glb": "265aef90b037111a8d08bd4f258501872be9fc0b4bacf6ad17820241c5f959a1.glb",
"glb_sha256": "6265bd35d1da9f9daa34a15b34dd9cadc5af9d6346c0b9104b2c3d582c510225",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2e3fe98b0ca1aae0ad512741f9465e42653b4c4df7345bc915e6b6b3bf80d27d": {
"source": "Tree/ymir work/tree/n1_coloradobluespruce_rt_03.spt",
"source_sha256": "2e3fe98b0ca1aae0ad512741f9465e42653b4c4df7345bc915e6b6b3bf80d27d",
"glb": "2e3fe98b0ca1aae0ad512741f9465e42653b4c4df7345bc915e6b6b3bf80d27d.glb",
"glb_sha256": "cbeb5930c57e2dbefbe9c50987275e7abc2b1ac9aa9cf190f60de71dae75f52b",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"4c3b7f1bc32ca54a581a22eb99a5229c066a2e3db84d73c4a260b41ecb4151d6": {
"source": "Tree/ymir work/tree/n1_commonolive_rt_winter.spt",
"source_sha256": "4c3b7f1bc32ca54a581a22eb99a5229c066a2e3db84d73c4a260b41ecb4151d6",
"glb": "4c3b7f1bc32ca54a581a22eb99a5229c066a2e3db84d73c4a260b41ecb4151d6.glb",
"glb_sha256": "a2fb958788a84301f9008e4efe1ea9994314d28b4b3954649c64b3033bf912c0",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e63e6fd8aec38f5a4dea10edce507870f834cf1da5fbd8b5e789dc75f773957c": {
"source": "Tree/ymir work/tree/n1_montereycypress_rt_winter_01.spt",
"source_sha256": "e63e6fd8aec38f5a4dea10edce507870f834cf1da5fbd8b5e789dc75f773957c",
"glb": "e63e6fd8aec38f5a4dea10edce507870f834cf1da5fbd8b5e789dc75f773957c.glb",
"glb_sha256": "9dc165cb8f6417f9e27486db714809722b5e85ff4ed827b21a0fe1d52feee89f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"bd813274fcc6fc3c8bf26a9bc85eb6b3c18d0a4a68dd0a51e13183f4634680da": {
"source": "Tree/ymir work/tree/n1_montereycypress_rt_winter_02.spt",
"source_sha256": "bd813274fcc6fc3c8bf26a9bc85eb6b3c18d0a4a68dd0a51e13183f4634680da",
"glb": "bd813274fcc6fc3c8bf26a9bc85eb6b3c18d0a4a68dd0a51e13183f4634680da.glb",
"glb_sha256": "f64d701e3ec752f566eef179ed62d613fd20d7dcc0bd86dcb95c1e3a0ef56c86",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"1af978f7efd1be5ba7af90056c4b933f74154c60a47a20fe88f4872aaf7bc862": {
"source": "Tree/ymir work/tree/n1_montereycypress_rt_winter_03.spt",
"source_sha256": "1af978f7efd1be5ba7af90056c4b933f74154c60a47a20fe88f4872aaf7bc862",
"glb": "1af978f7efd1be5ba7af90056c4b933f74154c60a47a20fe88f4872aaf7bc862.glb",
"glb_sha256": "f80e080f78042b751116ef056edf98501922de0bb46c6e0efb950e17115afbbe",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"36389501b6156b934f960d826ac2e3ecae86430cf53dc88dd6c30f5a8415aba9": {
"source": "Tree/ymir work/tree/n1_tulip_rt_winter_01.spt",
"source_sha256": "36389501b6156b934f960d826ac2e3ecae86430cf53dc88dd6c30f5a8415aba9",
"glb": "36389501b6156b934f960d826ac2e3ecae86430cf53dc88dd6c30f5a8415aba9.glb",
"glb_sha256": "ce0f23b6d5931ebeea06862a4ffef37b2274dc0b8643ec2f5768f1b0df3ae858",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 1,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0df26ab085d443bccc61d8ec878b61117f64db3b3ffa31e70fa0fce6d5f59361": {
"source": "Tree/ymir work/tree/n1_whitepine_rt_low_01.spt",
"source_sha256": "0df26ab085d443bccc61d8ec878b61117f64db3b3ffa31e70fa0fce6d5f59361",
"glb": "0df26ab085d443bccc61d8ec878b61117f64db3b3ffa31e70fa0fce6d5f59361.glb",
"glb_sha256": "48bd12ddffdc0aee165a29bfc6fd0dcbfe1366aec5542bcee102bf8d5d82c3e2",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"ac5bb9b19999fe6d20793898edc7b709b1ba792e6cac2cc7ed5fcd64abdfb03f": {
"source": "Tree/ymir work/tree/n1_whitepine_rt_low_02.spt",
"source_sha256": "ac5bb9b19999fe6d20793898edc7b709b1ba792e6cac2cc7ed5fcd64abdfb03f",
"glb": "ac5bb9b19999fe6d20793898edc7b709b1ba792e6cac2cc7ed5fcd64abdfb03f.glb",
"glb_sha256": "fa1fe6e118c91b6d1276b17ed05297eed6349e4c29726e11a51de48252dbf787",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"34314fd26754a94e9215723fd970a09b360eef0f8fa7e93c55c44b6dadd4b4c8": {
"source": "Tree/ymir work/tree/n2_aloevera_rt_flowers_01.spt",
"source_sha256": "34314fd26754a94e9215723fd970a09b360eef0f8fa7e93c55c44b6dadd4b4c8",
"glb": "34314fd26754a94e9215723fd970a09b360eef0f8fa7e93c55c44b6dadd4b4c8.glb",
"glb_sha256": "96d7b310fa8d13e910b11e5524a1a30886e21c9e715290c64d1df2e9b4298d17",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e0c3f6cfe9200acf3fe66fcc33fa52e2bbab55a48a35815178e74ee80e717c36": {
"source": "Tree/ymir work/tree/n2_aloevera_rt_flowers_02.spt",
"source_sha256": "e0c3f6cfe9200acf3fe66fcc33fa52e2bbab55a48a35815178e74ee80e717c36",
"glb": "e0c3f6cfe9200acf3fe66fcc33fa52e2bbab55a48a35815178e74ee80e717c36.glb",
"glb_sha256": "968300bd31c235557926343e46d7335ae75644aff6d0ca58f3a3e4a1963c5e51",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"f7c94a390c712edae5557d03849373c713d4ce28fba6080d87fc761216c4a26d": {
"source": "Tree/ymir work/tree/n2_bananatree_rt_01.spt",
"source_sha256": "f7c94a390c712edae5557d03849373c713d4ce28fba6080d87fc761216c4a26d",
"glb": "f7c94a390c712edae5557d03849373c713d4ce28fba6080d87fc761216c4a26d.glb",
"glb_sha256": "0b43c6087d26a19e6863a1d488eb6e3e524240793d850b27f844e4c194818afa",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"23b662d2b6d87d268460e4ae100b618ecfac6dd5262f59a2912e6dfe74530436": {
"source": "Tree/ymir work/tree/n2_bananatree_rt_02.spt",
"source_sha256": "23b662d2b6d87d268460e4ae100b618ecfac6dd5262f59a2912e6dfe74530436",
"glb": "23b662d2b6d87d268460e4ae100b618ecfac6dd5262f59a2912e6dfe74530436.glb",
"glb_sha256": "c5bd49676be213f69141b644b109a890f682fcc31ae8bf4fb0d078b72b28d8b8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"93ded82028c0f2e97d0403ec045b89bbc1ac859ad8b84a2e1930e0503907b354": {
"source": "Tree/ymir work/tree/n2_cinnamonfern_rt_01.spt",
"source_sha256": "93ded82028c0f2e97d0403ec045b89bbc1ac859ad8b84a2e1930e0503907b354",
"glb": "93ded82028c0f2e97d0403ec045b89bbc1ac859ad8b84a2e1930e0503907b354.glb",
"glb_sha256": "027fe6300b18a9608ffe465d1e1c77783a8c05c17e99c2999e058095ed62de05",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 1,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"dc389cbfc25b6e3226e961e7e392ed96c7a789c0dfc1d392b0a1950e81f59f9e": {
"source": "Tree/ymir work/tree/n2_cinnamonfern_rt_02.spt",
"source_sha256": "dc389cbfc25b6e3226e961e7e392ed96c7a789c0dfc1d392b0a1950e81f59f9e",
"glb": "dc389cbfc25b6e3226e961e7e392ed96c7a789c0dfc1d392b0a1950e81f59f9e.glb",
"glb_sha256": "0c537775ba7e98e11b6d5b74a6665dfd5eb74c94f70ad39c80c4f0979b5246ac",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 1,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"82566982c7decbccbbb9e50f0b1d399a86bed94a968e0a78e2ce8b25989d2462": {
"source": "Tree/ymir work/tree/n2_cinnamonfern_rt_03.spt",
"source_sha256": "82566982c7decbccbbb9e50f0b1d399a86bed94a968e0a78e2ce8b25989d2462",
"glb": "82566982c7decbccbbb9e50f0b1d399a86bed94a968e0a78e2ce8b25989d2462.glb",
"glb_sha256": "1d38479f01b5dac599ff8dbc80dab1657671950047da6bae43e7a201562da106",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 1,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"af5abcd685bf92c9597740c9c85168f6104e62c12e30117ca5759bf555f2c6ff": {
"source": "Tree/ymir work/tree/n2_coconutpalm_rt_01.spt",
"source_sha256": "af5abcd685bf92c9597740c9c85168f6104e62c12e30117ca5759bf555f2c6ff",
"glb": "af5abcd685bf92c9597740c9c85168f6104e62c12e30117ca5759bf555f2c6ff.glb",
"glb_sha256": "3344943038999faf012f8d1520b2feaaab367c06a6b24748685a233b89f7ec70",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e45bdc521daaeb53191a995485513b046b0602b60d55185e465ded20a8242828": {
"source": "Tree/ymir work/tree/n2_coconutpalm_rt_02.spt",
"source_sha256": "e45bdc521daaeb53191a995485513b046b0602b60d55185e465ded20a8242828",
"glb": "e45bdc521daaeb53191a995485513b046b0602b60d55185e465ded20a8242828.glb",
"glb_sha256": "3f46282c3c8d094206d516475cf1e086e87af584c4537c3d48da1e2ee544a248",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"75b89b54281bedd408be91863cfbf1131a7c9fce5c0e153e601db3c7195c8dd6": {
"source": "Tree/ymir work/tree/n2_coconutpalm_rt_03.spt",
"source_sha256": "75b89b54281bedd408be91863cfbf1131a7c9fce5c0e153e601db3c7195c8dd6",
"glb": "75b89b54281bedd408be91863cfbf1131a7c9fce5c0e153e601db3c7195c8dd6.glb",
"glb_sha256": "1e19c9fe65ef02d0277206f4737eeafec5541ba6bc2619dcb015e31bf8d76800",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"8a6d6468b7f97040b08ba70718650abbd0d0c4684fa6e092b0a603717ed0ec28": {
"source": "Tree/ymir work/tree/n2_curlypalm_rt_01.spt",
"source_sha256": "8a6d6468b7f97040b08ba70718650abbd0d0c4684fa6e092b0a603717ed0ec28",
"glb": "8a6d6468b7f97040b08ba70718650abbd0d0c4684fa6e092b0a603717ed0ec28.glb",
"glb_sha256": "a0859095e4b20c71d33e3cde76696584880df357d3642c07d79355cb0d732809",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"de5e22bb1610c61bcb00e3835698b6a9b50b67263a438b9ba7400157dce42b5e": {
"source": "Tree/ymir work/tree/n2_curlypalm_rt_02.spt",
"source_sha256": "de5e22bb1610c61bcb00e3835698b6a9b50b67263a438b9ba7400157dce42b5e",
"glb": "de5e22bb1610c61bcb00e3835698b6a9b50b67263a438b9ba7400157dce42b5e.glb",
"glb_sha256": "c8b1d7ad7faf78e14b0c50f7c33dd9712a2c79dbec54a574945e2b20f7e22d4a",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"97ce0c94956eeafad580424081ae4ab767f01e948ab9461a9b48e5cf8bd824af": {
"source": "Tree/ymir work/tree/n2_curlypalm_rt_03.spt",
"source_sha256": "97ce0c94956eeafad580424081ae4ab767f01e948ab9461a9b48e5cf8bd824af",
"glb": "97ce0c94956eeafad580424081ae4ab767f01e948ab9461a9b48e5cf8bd824af.glb",
"glb_sha256": "700aa9caba46ed1b7141d58a653d77ea9eb96911172d1a4dacb414328c8f18ee",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"6e24a4896134057a1bc495f0f8af32ca74a7b51e0d54baa612f6dcd36a99b54e": {
"source": "Tree/ymir work/tree/n2_datepalm_rt_01.spt",
"source_sha256": "6e24a4896134057a1bc495f0f8af32ca74a7b51e0d54baa612f6dcd36a99b54e",
"glb": "6e24a4896134057a1bc495f0f8af32ca74a7b51e0d54baa612f6dcd36a99b54e.glb",
"glb_sha256": "7c2a38b68b0b33d46db13830c12302a1962584e51e628386d654509998e1795f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"55de8a592ad3d9294583bcf27c80fba3fe71afdc669c5657ee7cc77a4002d255": {
"source": "Tree/ymir work/tree/n2_datepalm_rt_02.spt",
"source_sha256": "55de8a592ad3d9294583bcf27c80fba3fe71afdc669c5657ee7cc77a4002d255",
"glb": "55de8a592ad3d9294583bcf27c80fba3fe71afdc669c5657ee7cc77a4002d255.glb",
"glb_sha256": "e7391ec2d87aa7df6f87943e4faaf91de5951dccb045e6935844c997f7de1179",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2007d4ba90f12b8a00d2a0a539954e420c37f2151f438c5ad06eb63a78fe853d": {
"source": "Tree/ymir work/tree/n2_datepalm_rt_03.spt",
"source_sha256": "2007d4ba90f12b8a00d2a0a539954e420c37f2151f438c5ad06eb63a78fe853d",
"glb": "2007d4ba90f12b8a00d2a0a539954e420c37f2151f438c5ad06eb63a78fe853d.glb",
"glb_sha256": "ec41d7cb6c58864cc32bccca5e364d8fa4746501d4bf9a37d3f64c611834e0c9",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2bc2d707e302c58d099e64615ae127371c93700f91d52d31e8ec6f63010e2c0f": {
"source": "Tree/ymir work/tree/n2_joshuatree_rt_01.spt",
"source_sha256": "2bc2d707e302c58d099e64615ae127371c93700f91d52d31e8ec6f63010e2c0f",
"glb": "2bc2d707e302c58d099e64615ae127371c93700f91d52d31e8ec6f63010e2c0f.glb",
"glb_sha256": "27937a1b7cb30186a7041b4c8e405e531a3152004d5365082d9e0908e3998670",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 4,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"111cb80abecb5c5a35aaa8315ad6ec4c59772af927729780e0add02c9ba262b5": {
"source": "Tree/ymir work/tree/n2_joshuatree_rt_02.spt",
"source_sha256": "111cb80abecb5c5a35aaa8315ad6ec4c59772af927729780e0add02c9ba262b5",
"glb": "111cb80abecb5c5a35aaa8315ad6ec4c59772af927729780e0add02c9ba262b5.glb",
"glb_sha256": "6082d34315db4cd58e606b4b35cdbd07ab374d7587580512e07b0e27cfdedb11",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 4,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"a89a40ac942ee7a3d701a6835450a513a336e9835d20750036374e27c259b2d5": {
"source": "Tree/ymir work/tree/n2_joshuatree_rt_03.spt",
"source_sha256": "a89a40ac942ee7a3d701a6835450a513a336e9835d20750036374e27c259b2d5",
"glb": "a89a40ac942ee7a3d701a6835450a513a336e9835d20750036374e27c259b2d5.glb",
"glb_sha256": "5964a02431360700dabd0f88cddde3419141f9f2626b48068b80a6da4110ddf6",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"19685a989e47f21d14fb9c2f2e71fd86930f8841a3dafae42b608988c3317119": {
"source": "Tree/ymir work/tree/n2_palmetto_rt_01.spt",
"source_sha256": "19685a989e47f21d14fb9c2f2e71fd86930f8841a3dafae42b608988c3317119",
"glb": "19685a989e47f21d14fb9c2f2e71fd86930f8841a3dafae42b608988c3317119.glb",
"glb_sha256": "6fc27607202b42a36b0cea01228e05e3ffd36fb21800ed5ba171831c8a4b7ae3",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e167a7cb1d19c9d118be00d5dff39221695a582ec7caeabfae1f7826aff0d823": {
"source": "Tree/ymir work/tree/n2_palmetto_rt_02.spt",
"source_sha256": "e167a7cb1d19c9d118be00d5dff39221695a582ec7caeabfae1f7826aff0d823",
"glb": "e167a7cb1d19c9d118be00d5dff39221695a582ec7caeabfae1f7826aff0d823.glb",
"glb_sha256": "1597e961990b4bae697f92238b93978910c39d839f8585baaa44cda135197cde",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d096c9a78ede5fb7db97685b6dab4967f9dad9a24dbf147b762faffdf1281fc4": {
"source": "Tree/ymir work/tree/n2_palmetto_rt_03.spt",
"source_sha256": "d096c9a78ede5fb7db97685b6dab4967f9dad9a24dbf147b762faffdf1281fc4",
"glb": "d096c9a78ede5fb7db97685b6dab4967f9dad9a24dbf147b762faffdf1281fc4.glb",
"glb_sha256": "cb67edad8c58da648f2e7b19e5cce0d1ec665bb7738f0fa91ac0fd7dab1176ec",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
}
}
}
+56 -10
View File
@@ -3,24 +3,53 @@
#
# ./build-macos-client.sh [debug|release]
#
# 产物:build/export/mtgodot-poc.app + 旁边一个 assets 符号链接(不进包,2.1G)。
# 产物:build/export/mtgodot-poc.app(资源和 BGM 自包含在 Contents/Resources/)。
# 依赖:Godot 4.7.1 的 macOS 导出模板(editor 里 “管理导出模板” 装,或放
# ~/Library/Application Support/Godot/export_templates/4.7.1.stable/macos.zip)。
set -euo pipefail
cd "$(dirname "$0")"
REPO="$(pwd)"
CFG="${1:-debug}"
OUT="$REPO/build/export"
OUT="${MT_MAC_OUTPUT_DIR:-$REPO/build/export}"
APP="$OUT/mtgodot-poc.app"
GODOT="${GODOT:-godot}"
MAC_ARCHES="${MT_MAC_ARCHES:-universal}"
ENGINE_OVERRIDE="${MT_MAC_ENGINE:-}"
if [ -n "$ENGINE_OVERRIDE" ]; then
test -x "$ENGINE_OVERRIDE"
ENGINE_INFO="$(lipo -info "$ENGINE_OVERRIDE")"
case "$MAC_ARCHES:$ENGINE_INFO" in
universal:*x86_64*arm64*|universal:*arm64*x86_64*|arm64:*arm64*|x86_64:*x86_64*) ;;
*) echo "错误:自定义引擎架构不匹配:$ENGINE_INFO" >&2; exit 1 ;;
esac
echo "自定义引擎:$ENGINE_OVERRIDE"
"$ENGINE_OVERRIDE" --version
fi
BUILD_TYPE="Debug"
if [ "$CFG" = release ]; then
BUILD_TYPE="Release"
fi
echo "== 1. GDExtension .dylib =="
if [ "$MAC_ARCHES" = universal ]; then
CMAKE_ARCHES="arm64;x86_64"
else
CMAKE_ARCHES="$MAC_ARCHES"
fi
cmake -S . -B build -DCMAKE_BUILD_TYPE="$BUILD_TYPE" -DCMAKE_OSX_ARCHITECTURES="$CMAKE_ARCHES"
cmake --build build --target mtgodot -j8
DYLIB="project/bin/libmtgodot.macos.template_${CFG}.dylib"
if [ ! -f "$DYLIB" ]; then
# CMake 目前只出 debug 名;release 就复用它
cp -f project/bin/libmtgodot.macos.template_debug.dylib "$DYLIB"
fi
if [ "$MAC_ARCHES" = universal ]; then
LIPO_INFO="$(lipo -info "$DYLIB")"
case "$LIPO_INFO" in
*x86_64*arm64*|*arm64*x86_64*) ;;
*) echo "错误:GDExtension 不是 universal$LIPO_INFO" >&2; exit 1 ;;
esac
fi
echo "== 2. 导出 =="
mkdir -p "$OUT"
@@ -30,16 +59,33 @@ else
"$GODOT" --headless --path project --export-debug macOS "$APP"
fi
echo "== 3. 资源(不打进包=="
# .app 同级放一个 assets 链接;AssetRoot 会在 <.app 同级>/assets 找到它。
ln -sfn "$REPO/assets" "$OUT/assets"
ln -sfn "$REPO/bgm" "$OUT/bgm"
echo "== 3. 资源(自包含=="
mkdir -p "$APP/Contents/Resources"
ditto "$REPO/assets" "$APP/Contents/Resources/assets"
ditto "$REPO/bgm" "$APP/Contents/Resources/bgm"
test -f "$APP/Contents/Resources/assets/asset_index.txt"
echo "== 4. ad-hoc 签名(Apple Silicon 直跑=="
codesign --force --deep --sign - "$APP" 2>/dev/null || true
echo "== 4. 签名(默认 ad-hocCODESIGN_IDENTITY 可切正式证书=="
if [ -n "$ENGINE_OVERRIDE" ]; then
cp "$ENGINE_OVERRIDE" "$APP/Contents/MacOS/mtgodot-poc"
fi
CODESIGN_IDENTITY="${CODESIGN_IDENTITY:--}"
ENTITLEMENTS="$REPO/macos.entitlements"
test -f "$ENTITLEMENTS"
# The exported Godot executable loads the GDExtension from Contents/Frameworks.
# Because this package uses hardened runtime, the main executable must carry
# disable-library-validation or macOS rejects the ad-hoc/formally signed dylib
# at runtime even when the bundle passes a static codesign verification.
find "$APP/Contents/Frameworks" -type f \( -name '*.dylib' -o -name '*.so' \) -exec \
codesign --force --options runtime --timestamp=none --sign "$CODESIGN_IDENTITY" {} \;
codesign --force --options runtime --timestamp=none --entitlements "$ENTITLEMENTS" \
--sign "$CODESIGN_IDENTITY" "$APP/Contents/MacOS/mtgodot-poc"
codesign --force --options runtime --timestamp=none --entitlements "$ENTITLEMENTS" \
--sign "$CODESIGN_IDENTITY" "$APP"
xattr -dr com.apple.quarantine "$APP" 2>/dev/null || true
echo
echo "OK -> $APP"
echo "跑: open '$APP' (用 $OUT/assets"
echo "或: MT_ASSETS='$REPO/assets' '$APP/Contents/MacOS/mtgodot-poc' (看控制台日志)"
echo "跑: open '$APP'"
echo "开发覆盖资源:MT_ASSETS='$REPO/assets' '$APP/Contents/MacOS/mtgodot-poc'"
+5 -1
View File
@@ -5,6 +5,7 @@ cd "$(dirname "$0")"
CONFIG="${1:-Debug}" # Debug | Release
JOBS="${JOBS:-$(sysctl -n hw.ncpu)}"
MAC_ARCHES="${MT_MAC_ARCHES:-$(uname -m)}" # arm64, x86_64, or universal
# godot-cpp + the vendored deps (zstd, libsodium-cmake and its nested libsodium)
# are submodules — recurse. miniLZO is vendored in-tree, nothing to fetch.
@@ -15,7 +16,10 @@ if [ ! -f extension/godot-cpp/CMakeLists.txt ] \
git submodule update --init --recursive
fi
cmake -S . -B build -DCMAKE_BUILD_TYPE="$CONFIG"
if [ "$MAC_ARCHES" = universal ]; then
MAC_ARCHES="arm64;x86_64"
fi
cmake -S . -B build -DCMAKE_BUILD_TYPE="$CONFIG" -DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHES"
cmake --build build --config "$CONFIG" -j "$JOBS"
echo
+4 -4
View File
@@ -1,7 +1,7 @@
<!-- generated-by: gsd-doc-writer -->
# Android 真机测试指南
本文说明如何在 Android 真机上安装 `mtgodot-poc`、生成并导入资源包、验证资源挂载,以及进行 40250 客户端测试。
本文说明如何在 Android 真机上安装本客户端(APK 产物名仍是 `mtgodot-poc.apk`,包名 `org.internal.mtgodotpoc`、生成并导入资源包、验证资源挂载,以及进行 40250 客户端测试。
## 1. 当前测试范围与已知前置条件
@@ -10,7 +10,7 @@
当前仓库的资源部署存在一个需要特别注意的限制:
- `export-android.sh --install` 会把资源推到 `/sdcard/Android/data/org.internal.mtgodotpoc/files/assets.zip`
- `project/asset_pack.gd` 首先查找 `user://assets.zip`,当前没有显式把上述外部目录加入候选路径。
- `project/asset_pack.gd` 的候选顺序是 `MT_ASSETS_ZIP` 环境变量 → `user://assets.zip``OS.get_user_data_dir()/assets.zip` → 可执行文件目录 → `res://../assets.zip`(见 `_find_local_zip()`);上述外部目录不在候选路径
- 因此,Debug 真机测试应把外部目录中的压缩包再复制到应用私有 `files/` 目录,确保客户端的 `user://assets.zip` 能命中。
另外,40250 classic 网络栈当前由进程环境变量 `MT_PROTOCOL=classic` 选择。Android 通过 Activity 启动 APK 时不能直接继承 Mac 终端的环境变量;如果 APK 尚未增加 Android 侧的协议配置入口或将 classic 设为移动端默认值,则只能完成 APK、资源和 UI 验证,不能据此宣称已经完成真实 40250 登录验收。
@@ -34,7 +34,7 @@
进入仓库并设置环境变量。路径按本机安装位置调整:
```bash
cd /Users/shenlei/Work/mt/mtgodot-poc
cd /Users/shenlei/Work/mt/metin2-client
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
@@ -369,7 +369,7 @@ shasum -a 256 build/export/mtgodot-poc.apk \
首次测试按以下顺序执行:
```bash
cd /Users/shenlei/Work/mt/mtgodot-poc
cd /Users/shenlei/Work/mt/metin2-client
./pack-assets.sh
./export-android.sh Debug
+6 -6
View File
@@ -1,4 +1,4 @@
# mtgodot-poc — 未完成工作清单
# metin2-client — 未完成工作清单
> 更新:2026-08-29 · 配套 [`GODOT-POC-PLAN.md`](./GODOT-POC-PLAN.md)(计划)、[`MIDREVIEW.md`](./MIDREVIEW.md)(中期评审)、
> [`SHINSOO-WORLD-RENDERING.md`](./SHINSOO-WORLD-RENDERING.md)(世界渲染实施)、[`PARITY-GAP.md`](./PARITY-GAP.md)(距参考图的保真化差距)。
@@ -55,8 +55,8 @@
| B6 | 贴图采样设置(filter / wrap / mip / 各向异性) | ⬜ | S | 现在 shader 里写死 `filter_linear_mipmap, repeat_enable`。 |
| B7 | 顶点色 | ✅ 排查=不适用 | — | 全资产 `.gr2`PC/monster2/effect)无 `DiffuseColor`/`Color0` 顶点成员(grep assets = 0)。Metin2 逐顶点色 = 地形雾(`MapOutdoorRenderSTP` 运行时动态 VB,非资产,Godot `Environment` 雾已覆盖)+ `.mse` `colorfactor`/粒子色(属 E7/§1.2)。libgr2 / `build_mesh` 无需动。详见 PARITY §2.6。 |
| B8 | 切线帧(若确有法线贴图资产) | ⬜ | M | PNT332 无切线。xrender 时期结论:Metin2 资产**基本不用**法线贴图 —— 先排查有没有,再决定是否做(`SurfaceTool::generate_tangents()` 或从 UV 算)。 |
| B9 | 镜面 / 自发光 | 🟡 | M | Sphere-map 高光管线已端口(`SRC_MIX`/`SRC_SKIN``spec_map`/`spec_power`/`spec_enable`,复现 `Material.cpp:305 __ApplySpecularRenderState`:不透明面、相机空间 `reflect().xy``spheremap.jpg``EMISSION += sphere*tex.a*power`)。`Metin2Model.specular_power` 属性,默认 0 休眠 → 需装备层用 `item_proto bSpecular/100` 逐部位驱动`SRC_ADD` / `SRC_ADD_SKIN` 已覆盖 additive 发光的一种,且 additive 动画面不再漏蒙皮。 |
| B10 | 背面剔除 / 双面 | 🟡 | S | 世界静态对象(W3/B1)已按 `two_sided` + 贴图名切 `CULL_DISABLED`角色 `m2_material` shader 仍写死 `cull_back``MaterialDesc::two_sided` 死字段)。需给角色也按材质切。 |
| B9 | 镜面 / 自发光 | 🟡 | M | Sphere-map 高光管线已端口(`SRC_MIX`/`SRC_SKIN``spec_map`/`spec_power`/`spec_enable`,复现 `Material.cpp:305 __ApplySpecularRenderState`:不透明面、相机空间 `reflect().xy``spheremap.jpg``EMISSION += sphere*tex.a*power`)。`Metin2Model.specular_power` 属性,默认 0 休眠。✅ 装备层已接:`project/ui/equip_model.gd:283-288` `_apply_body()``item_proto``specular` 列并 `set("specular_power", sp / 100.0)`。⬜ 只驱动身体/护甲一个部位(武器 / 发型 / 时装未接),且 power 未按受控参考帧标定(PARITY §0 门禁)`SRC_ADD` / `SRC_ADD_SKIN` 已覆盖 additive 发光的一种,且 additive 动画面不再漏蒙皮。 |
| B10 | 背面剔除 / 双面 | ✅ 首版 | S | 世界静态对象(W3/B1)已按 `two_sided` + 贴图名切 `CULL_DISABLED`✅ 角色也已按材质切:`m2_material.cpp:188-211` `shader_for()``two_sided``cull_back` replace 成 `cull_disabled`opaque/blend × 普通/蒙皮 四个变体各自缓存),`metin2_model.cpp:1182`(身体)/ `:769`(武器)经 `decide_two_sided()` 驱动,`_load_hair()` 直接置 true。⬜ 判据仍是启发式 —— `gr2::MaterialInfo.two_sided` 按材质名(`2side`/`twoside`/`double`/`leaf`/`tree`/`ivy`/`fence`+ 表面名兜底(`hair`/`cloak`/`cape`/`skirt`/`ribbon`/`leaf`),不是真 `ExtendedData` 的 "Two-sided"。 |
| B11 | winding 逐模型验 | ⬜ | S | `flip_winding` 默认 false(4 个骨架验过,未全量)。 |
---
@@ -117,14 +117,14 @@
| ID | 项 | 状态 | 规模 | 说明 |
|---|---|---|---|---|
| F0 | vendored 原生依赖 | ✅ | S | libsodium / libzstd / miniLZO 从 Homebrew 改为**源码内建**`extension/third_party/`pinned submodule + vendored miniLZO)—— 同一棵树可交叉编 NDK / iOS SDK。别名 `mt3p::sodium` / `mt3p::zstd` / `mt3p::minilzo`,无源码改动,macOS 9/9 CTest + 真实 item/mob_proto 解码逐字一致。详见 `docs/THIRD-PARTY.md`。miniLZO 是 GPL —— 自用可接受(同 oodle1.c),发布前须换。 |
| F0 | vendored 原生依赖 | ✅ | S | libsodium / libzstd / miniLZO 从 Homebrew 改为**源码内建**`extension/third_party/`pinned submodule + vendored miniLZO)—— 同一棵树可交叉编 NDK / iOS SDK。2026-09-02 起第四个:**Crypto++ 8.4.0**vendored 源码,`MT_PROTOCOL=classic` 的 DH2/CTR cipher 用,公有领域许可)。别名 `mt3p::sodium` / `mt3p::zstd` / `mt3p::minilzo` / `mt3p::cryptopp`,无源码改动,macOS 9/9 CTest + 真实 item/mob_proto 解码逐字一致。详见 `docs/THIRD-PARTY.md`。miniLZO 是 GPL —— 自用可接受(同 oodle1.c),发布前须换。 |
| F1 | iOSiPhone 16 / Metal | 🟡 | M | ✅ 交叉编译打通:`./build-ios.sh``libmtgodot.ios.template_{debug,release}.a`arm64 / minos 15.0),全依赖(libsodium/zstd/miniLZO/libgr2/formats/mtnet/godot-cpp)随之交叉编过;iOS 强制 extension 为 STATIC(不能 dlopen),`.gdextension` 加了 `[dependencies] ios.*` 归档清单。⬜ 剩 Godot 4.7 iOS 导出模板 + 导出 preset 链 `project/bin/ios/*.a` + 免费 provisioning 签名 + iPhone 16 真机跑 + Release/strip。预留 23 天。详见 `docs/PLATFORMS.md`。 |
| F2 | Android(一加 13 / Vulkan | 🟡 | M | ✅ CMake 已接 `CMAKE_SYSTEM_NAME==Android`SHARED `.so` + `arm64` 名字后缀);`./build-android.sh` 走 NDK 自带 `android.toolchain.cmake``arm64-v8a` / `android-24`)。⬜ 本机无 NDK —— 装 NDK 后 `./build-android.sh``.so`,再 Godot Android 导出 presetGradle+ 一加 13 真机跑。**只验 Vulkan**,不做 Compatibility(GLES3) / 老机。详见 `docs/PLATFORMS.md`。 |
| F3 | 三端一致性 | ⬜ | S | 同工程 / 同 t / 同机位截图,iPhone 16 · 一加 13 · Mac 对齐。 |
| F4 | 移动端性能 / 帧预算 | 🟡 | M | ✅ **GPU 纹理压缩**`extension/src/texture_util.{h,cpp}` `make_color_texture()` —— 移动端(`OS.has_feature("mobile")`)或 `MTGODOT_TEXCOMP=1` 时对颜色贴图运行时 ASTC 8x8~2bpp vs RGBA8 32bpp),编码失败回退 RGBA8;桌面默认关,逐调用点保留原 mipmap 标志 → OFF 时与旧路径逐字节一致。接入:角色皮肤(`_load_dds`)、建筑 albedo`static_object`)、树皮/树冠(`tree_placeholder`);splat 控制图 / 阴影图 / 水 / HUD 保持 RGBA8。macOS+iOS 编过,ctest 9/9,桌面渲染无变化,`MTGODOT_TEXCOMP=1` 角色渲染正常。⬜ 剩:一加 13 / iPhone 16 单 + N 角色 bench`MTGODOT_GPUSKIN=1` 人群)、构建成本(~170ms/角色,A5/G2)、移动端画质核对。 |
| F5 | App 生命周期 | 🟡 | SM | ✅ `project/app_lifecycle.gd`AppLifecycle 节点):把 `NOTIFICATION_APPLICATION_PAUSED/RESUMED` / `FOCUS_IN/OUT` / `OS_MEMORY_WARNING` / `WM_GO_BACK_REQUEST` 收成信号,默认动作 = 后台暂停树 + 停 BGM + 降 `max_fps`,前台还原;`bind(m2client, audio)` 一键接。`M2Client`C++):`_notification` 收 PAUSED/RESUMED → `suspend()/resume()`(挂起时不 pump socket),`reconnect()` 用存好的凭据重登;新信号 `suspended`/`resumed`。已接 `login.gd`(回前台若在游戏中自动重连)+ `world_demo.gd`。headless smokesuspend/resume/reconnect 绑定 + 幂等 + 信号)通过,ctest 9/9,iOS 编过。渲染上下文丢失由 Godot Vulkan 自恢复,GPU 资源全 RenderingServer 托管,无需处理。⬜ 剩真机验证后台 30s→掐连接→回前台重连、iOS 内存告警实际触发。 |
| F6 | 打包 | ⬜ | SM | macOS / iOS / Android export preset、资产打包策略、iOS 签名。自用,无商店上架。 |
| F8 | 实体点选精度 | ✅ 首版 | S | `player_controller._ray_pick_t()`:射线 vs 每个 pickable 下所有 `MeshInstance3D` 的**世界 AABB 合并 + slab 相交**(逐实体精确,大怪 / 小怪不再被固定 1.4 m 半径误判;`grow(0.15)` 宽容度)。无网格回退旧胶囊近似。对齐 `EterGrnLib/ModelInstanceCollisionDetection::Intersect` 的**结果**,不复刻逐三角。`eterngrn_polish_test.gd` 覆盖(命中 t / 未命中 -1 / 擦边 / 无网格回退)。⬜ 逐三角 / 骨骼包围体、`GetHeight`。 |
| F9 | 实体点选精度 | ✅ 首版 | S | `player_controller._ray_pick_t()`:射线 vs 每个 pickable 下所有 `MeshInstance3D` 的**世界 AABB 合并 + slab 相交**(逐实体精确,大怪 / 小怪不再被固定 1.4 m 半径误判;`grow(0.15)` 宽容度)。无网格回退旧胶囊近似。对齐 `EterGrnLib/ModelInstanceCollisionDetection::Intersect` 的**结果**,不复刻逐三角。`eterngrn_polish_test.gd` 覆盖(命中 t / 未命中 -1 / 擦边 / 无网格回退)。⬜ 逐三角 / 骨骼包围体、`GetHeight`。 |
| F7 | 触屏输入 / 手势相机 | ✅ | S | `game_camera.gd`:单指拖拽环绕(8px deadzone,低于视作点击)/ 双指捏合缩放;`player_controller.gd`:单指轻点(≤12px 位移 & ≤0.35s)= 点地 / 点选,拖拽或双指手势时不触发。`project.godot``emulate_mouse_from_touch`(避免与轻点重复触发),桌面鼠标路径不变。两脚本 `--check-only` + 项目导入无错。 |
| F8 | `godot-cpp` 钉稳定版 | ✅ | S | gitlink 精确锁定 `101ae38034304346a46ea9ea84ae156d3e860496``.gitmodules` 已移除移动的 `master` branch 配置;目标 API 固定为 Godot 4.7。 |
@@ -183,7 +183,7 @@ UI / Python 脚本层、多角色 AI、网络、玩法逻辑、eterpackPhase
### Sprint 1 —— 小项收尾(~1 周)
已完成:`A3` · `A6` · `F8` · `I1`,并补完 `A9`/`A10` 正确性问题。
已完成:`A3` · `A6` · `F8``godot-cpp` 钉版)· `I1`,并补完 `A9`/`A10` 正确性问题。
剩余:`A5`CPU 路径原地更新)· `A7`(前台 Metal stress 基线)· `A8`+`H1`(桥接层畸形用例与全语料 draw 冒烟)。
### Sprint 2 —— 组合基础(~2 周)
+10290
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More