From 7d2d67a3f502b7bf2e31b04eec58207f397392f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=94=E9=A6=99=E4=B8=B8=E5=AD=90?= <15518898337@163.com> Date: Wed, 3 Jun 2026 22:21:00 +0800 Subject: [PATCH] feat(jump-hop): redesign sling platform gameplay --- .hermes/shared-memory/decision-log.md | 37 +- .hermes/shared-memory/pitfalls.md | 73 +- ...³•创作】跳一跳俯视角玩法模æ¿PRD-2026-05-19.md | 652 ++---- ...„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md | 6 + ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 26 +- packages/shared/src/contracts/jumpHop.ts | 54 +- .../branding/jump-hop-taonier-character.png | Bin 0 -> 177512 bytes public/creation-type-references/jump-hop.webp | Bin 0 -> 27150 bytes .../api-server/src/character_visual_assets.rs | 18 +- .../api-server/src/creation_entry_config.rs | 23 + .../crates/api-server/src/custom_world_ai.rs | 11 +- server-rs/crates/api-server/src/jump_hop.rs | 508 +++-- .../api-server/src/match3d/item_assets.rs | 11 +- .../crates/api-server/src/match3d/works.rs | 33 +- .../crates/api-server/src/modules/jump_hop.rs | 12 +- .../api-server/src/puzzle/generation.rs | 11 +- .../api-server/src/puzzle/vector_engine.rs | 4 +- .../src/square_hole/visual_assets.rs | 11 +- .../crates/module-jump-hop/src/application.rs | 405 ++-- .../crates/module-runtime/src/application.rs | 4 +- server-rs/crates/module-runtime/src/lib.rs | 23 + .../crates/shared-contracts/src/jump_hop.rs | 72 +- .../crates/spacetime-client/src/jump_hop.rs | 443 ++-- .../crates/spacetime-client/src/mapper.rs | 4 +- .../src/mapper/bark_battle.rs | 1 + .../spacetime-client/src/mapper/jump_hop.rs | 67 +- .../spacetime-client/src/mapper/runtime.rs | 164 +- .../spacetime-client/src/module_bindings.rs | 36 + .../get_jump_hop_leaderboard_procedure.rs | 59 + .../jump_hop_leaderboard_entry_row_type.rs | 72 + ...ump_hop_leaderboard_entry_snapshot_type.rs | 19 + .../jump_hop_leaderboard_entry_table.rs | 166 ++ .../jump_hop_leaderboard_get_input_type.rs | 17 + ...p_hop_leaderboard_procedure_result_type.rs | 21 + .../jump_hop_run_jump_input_type.rs | 4 +- .../jump_hop_run_start_input_type.rs | 1 + .../jump_hop_tile_asset_snapshot_type.rs | 3 + .../spacetime-client/src/wooden_fish.rs | 49 +- .../crates/spacetime-module/src/jump_hop.rs | 266 ++- .../spacetime-module/src/jump_hop/tables.rs | 16 + .../spacetime-module/src/jump_hop/types.rs | 40 +- .../crates/spacetime-module/src/migration.rs | 4 +- .../src/runtime/creation_entry_config.rs | 30 + .../JumpHopWorkspace.test.tsx | 60 + .../jump-hop-creation/JumpHopWorkspace.tsx | 191 +- .../JumpHopResultView.test.tsx | 180 ++ .../jump-hop-result/JumpHopResultView.tsx | 449 ++-- .../JumpHopRuntimeShell.test.tsx | 919 ++++++++ .../jump-hop-runtime/JumpHopRuntimeShell.tsx | 1855 ++++++++++++++--- .../PlatformEntryFlowShellImpl.test.ts | 32 + .../PlatformEntryFlowShellImpl.tsx | 192 +- ...gEntryFlowShell.agent.interaction.test.tsx | 278 ++- src/services/jump-hop/jumpHopClient.ts | 55 +- .../jump-hop/jumpHopRuntimeModel.test.ts | 498 +++++ src/services/jump-hop/jumpHopRuntimeModel.ts | 479 +++++ .../jump-hop/useJumpHopLeaderboard.test.tsx | 86 + .../jump-hop/useJumpHopLeaderboard.ts | 85 + .../miniGameDraftGenerationProgress.test.ts | 24 +- .../miniGameDraftGenerationProgress.ts | 44 +- 59 files changed, 6930 insertions(+), 1973 deletions(-) create mode 100644 public/branding/jump-hop-taonier-character.png create mode 100644 public/creation-type-references/jump-hop.webp create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_leaderboard_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_snapshot_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_procedure_result_type.rs create mode 100644 src/components/jump-hop-creation/JumpHopWorkspace.test.tsx create mode 100644 src/components/jump-hop-result/JumpHopResultView.test.tsx create mode 100644 src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx create mode 100644 src/components/platform-entry/PlatformEntryFlowShellImpl.test.ts create mode 100644 src/services/jump-hop/jumpHopRuntimeModel.test.ts create mode 100644 src/services/jump-hop/jumpHopRuntimeModel.ts create mode 100644 src/services/jump-hop/useJumpHopLeaderboard.test.tsx create mode 100644 src/services/jump-hop/useJumpHopLeaderboard.ts diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 93ddf685..59875ad9 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1028,13 +1028,38 @@ - éªŒè¯æ–¹å¼ï¼šä»Žå¹³å°æŽ¨èæˆ–å…¬å¼€è¯¦æƒ…è¿›å…¥è·³ä¸€è·³ä½œå“æ—¶ï¼Œè·¯ç”± source type 为 `jump-hop`ã€public code 为 `JH-*`,è¿è¡Œæ€å¯åŠ¨æ¶ˆè´¹åŽç«¯è¿”回的完整 profile / run æ•°æ®ï¼›åŽç«¯ smoke 统一使用 `npm run dev:api-server` å¯åŠ¨å¹¶æ£€æŸ¥ `/healthz`。 - å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`ã€`docs/ã€å¼€å‘è¿ç»´ã€‘本地开å‘验è¯ä¸Žç”Ÿäº§è¿ç»´-2026-05-15.md`。 -## 2026-05-26 跳一跳地å—图集改为专用 2x3 六格切分 +## 2026-05-28 跳一跳é‡è®¾è®¡ä¸º 5x5 地å—图集与弹弓拖拽 -- 背景:跳一跳创作在地å—生图阶段误用了通用系列素æå›¾é›† helper,`item_names.len() > grid_size` 的校验会让 6 个地å—类型在 `grid_size = 3` 时直接失败;å³ä½¿ç»•过校验,通用 helper ä»ä»¥â€œæ¯ç‰©å“多视图â€è¯­ä¹‰åˆ‡å›¾ï¼Œä¸ç¬¦åˆè·³ä¸€è·³åœ°å—的一次性六格资产模型。 -- 决策:跳一跳地å—图集固定采用专用 `2行*3列` 六格布局,按 `start / normal / target / finish / bonus / accent` 顺åºåˆ‡åˆ†å¹¶åˆ†åˆ«æŒä¹…化为独立 PNG 资产;图集 prompt ä¸å†è°ƒç”¨é€šç”¨ç³»åˆ—ç´ æ `build_generated_asset_sheet_prompt`。 -- å½±å“范围:`server-rs/crates/api-server/src/jump_hop.rs`ã€`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 -- éªŒè¯æ–¹å¼ï¼š`cargo test -p api-server jump_hop_tile_atlas -- --nocapture` 通过;六张切片都应有独立 OSS 对象与 `JumpHopTileAsset` 记录,ä¸å†åªæœ‰ atlas 预览路径。 -- å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`。 +- 背景:旧跳一跳模æ¿ä»ä¿ç•™è§’è‰²ç”Ÿå›¾ã€æœ‰é™è·¯å¾„ã€score/combo å’Œ `2x3` 地å—图集å£å¾„,和当å‰â€œä¿¯è§†è§’å¹³å°è·³è·ƒ + 主题生æˆåœ°å—æ±  + æ— é™è·¯å¾„â€çš„产å“需求ä¸ä¸€è‡´ã€‚ +- 决策:`jump-hop` v1 创作端åªä¿ç•™ä¸»é¢˜è¾“入;image2 生æˆä¸€å¼  `5x5`ã€å…± 25 个 2D 地å—图标的图集,åŽç«¯æŒ‰å‡åŒ€ç½‘格切出 25 个 `JumpHopTileAsset`。角色ä¸å†å•独生图,è¿è¡Œæ€ä½¿ç”¨é™¶æ³¥å„¿ logo 逿˜Ž PNG 角色;è¿è¡Œæ€è¾“入为按ä½åŽæ‹‰è“„åŠ›ã€æ¾æ‰‹åå‘弹出,å‰ç«¯æäº¤ `chargeMs + dragVectorX + dragVectorY`,åŽç«¯è£å†³è½ç‚¹ã€‚è‰ç¨¿è¯•玩必须使用 `runtimeMode=draft`,正å¼ä½œå“使用 `runtimeMode=published`;排行榜按作å“维度æ¯çީ家åªä¿ç•™ 1 æ¡æœ€ä½³è®°å½•,排åºä¸ºæˆåŠŸè·³è·ƒæ¬¡æ•°é™åºã€æ¸¸æˆæ—¶é•¿å‡åºã€æ›´æ–°æ—¶é—´å‡åºã€‚ +- 决策补充:跳一跳创作入å£çš„事实æºä»æ˜¯ SpacetimeDB `creation_entry_type_config`。默认ç§å­å’Œæ—§é»˜è®¤è¡Œéƒ½å¿…é¡»åŒæ­¥è¿ç§»åˆ° `subtitle=主题驱动平å°è·³è·ƒ`ã€`image_src=/creation-type-references/jump-hop.webp`ï¼›åŽç«¯åªåœ¨ç³»ç»Ÿé»˜è®¤æ—§å€¼å‘½ä¸­æ—¶è‡ªåŠ¨çº å,é¿å…覆盖åŽå°æ‰‹åЍé…置。 +- å½±å“范围:`jump-hop` PRDã€`api-server` 生æˆç¼–排ã€`module-jump-hop` 领域规则ã€`spacetime-module` / `spacetime-client` 跳一跳契约ã€å‰ç«¯å·¥ä½œå° / 结果页 / runtime / å¹³å°å£³è°ƒç”¨é“¾ã€‚ +- éªŒè¯æ–¹å¼ï¼š`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`ã€`cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`ã€`cargo check -p api-server --manifest-path server-rs/Cargo.toml`ã€`npm run check:spacetime-schema`ã€è·³ä¸€è·³å·¥ä½œå°å’Œ runtime 定å‘å‰ç«¯æµ‹è¯•。 +- å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`ã€`docs/ã€åŽç«¯æž¶æž„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md`。 + +## 2026-06-01 跳一跳è¿è¡Œæ€åœ°å—视觉尺寸和命中åŠå¾„ç»Ÿä¸€æ”¾å¤§ä¸€å€ + +- 背景:当å‰è·³ä¸€è·³è¿è¡Œæ€é‡Œåœ°å—视觉尺寸åå°ï¼Œçީ家å馈“很难跳上去â€ï¼Œä½†ä»…放大å‰ç«¯å±•示会造æˆç”»é¢å’ŒåŽç«¯è£å†³è„±èŠ‚ã€‚ +- 决策:`jump-hop` è¿è¡Œæ€çš„地å—视觉尺寸ã€`width/height` çŽ©æ³•ä¸–ç•Œå°ºå¯¸ä»¥åŠ `landingRadius/perfectRadius` åŒæ­¥ä¹˜ä»¥ 2ï¼›å‰ç«¯å¹³å°æ¸²æŸ“抽æˆç»Ÿä¸€å°ºå¯¸ helper,ä¿è¯å•测å¯ä»¥ç›´æŽ¥æ ¡éªŒæ”¾å¤§ç»“果。 +- å½±å“范围:`server-rs/crates/module-jump-hop/src/application.rs`ã€`src/services/jump-hop/jumpHopRuntimeModel.ts`ã€`src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`ã€å¯¹åº”å®šå‘æµ‹è¯•。 +- éªŒè¯æ–¹å¼ï¼š`npm test -- src/services/jump-hop/jumpHopRuntimeModel.test.ts src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx`ã€`cargo test -p module-jump-hop --manifest-path server-rs/Cargo.toml -- --nocapture`。 +- å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + +## 2026-06-02 跳一跳起跳è·ç¦»å‡åŠå¹¶åŠ å…¥é£žè¡ŒåŠ¨ç”»ç¼“å†² + +- 背景:用户å馈当å‰è·³è·ƒåˆ°ç›®æ ‡ä½ç½®éœ€è¦æ‹–å¾—å¤ªè¿œï¼Œä¸”æ¾æ‰‹åŽç¼ºå°‘角色翻腾到目标地å—的过渡动画,导致跳跃手感å硬。 +- 决策:`jump-hop` çš„ `chargeToDistanceRatio` 统一从 `0.004` æå‡åˆ° `0.008`,让åŒç­‰è·³è·ƒè·ç¦»æ‰€éœ€æ‹–动è·ç¦»å‡åŠï¼›å‰ç«¯ runtime 把“åŽç«¯çœŸå®ž runâ€å’Œâ€œå½“å‰å±å¹•显示æ€â€æ‹†å¼€ï¼Œæ¾æ‰‹çž¬é—´å…ˆç”Ÿæˆ `visualJump`,用当å‰è§’色ä½ç½®ä½œä¸ºèµ·ç‚¹ã€å‰ç«¯é¢„测è½ç‚¹ä½œä¸ºç»ˆç‚¹ï¼Œæ’­æ”¾çº¦ `560ms` 的飞行动画;该路径ä¸å¾—等待åŽç«¯æ–° run。角色弹到预测è½ç‚¹åŽè‹¥æ–° run 尚未返回,必须åœåœ¨é¢„测è½ç‚¹ç­‰å¾…,å†è¿›å…¥çº¦ `1440ms` çš„ç›¸æœºå±‚æŽ¨è¿›è¿‡æ¸¡ã€‚æŽ¨è¿›æœŸé—´åœ°å— DOM 层和 DOM 角色层统一包在åŒä¸€ä¸ª camera layer 下移动,旧当å‰åœ°å—自然离开视野,新预览地å—从上方露出,é¿å… p1/p2 å•独 top/left 过渡导致角色和地å—ä¸åŒæ­¥ã€‚ç›¸æœºæŽ¨è¿›å¿…é¡»åŒæ—¶ä½¿ç”¨ X/Y å移,从旧目标地å—ä½ç½®æ–œå‘滑到新当å‰åœ°å—èšç„¦ä½ç½®ï¼Œä¸èƒ½å…ˆæ¨ªå‘瞬切居中å†çºµå‘推进。地å—ä¿ç•™å½“å‰ / 目标 / 预览的深度尺寸差异,但该差异通过固定基准宽高上的 CSS transform scale è¡¨è¾¾ï¼Œå¹¶åœ¨ç›¸æœºæŽ¨è¿›æœŸé—´åŒæ ·ä½¿ç”¨ `1440ms` ç¼“åŠ¨ï¼›å½“å‰æ€ä¸å†é¢å¤–å  CSS scale。 +- å½±å“范围:`server-rs/crates/module-jump-hop/src/application.rs`ã€`src/services/jump-hop/jumpHopRuntimeModel.ts`ã€`src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`ã€è·³ä¸€è·³è¿è¡Œæ€å®šå‘测试。 +- éªŒè¯æ–¹å¼ï¼š`npm test -- src/services/jump-hop/jumpHopRuntimeModel.test.ts src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx`ã€`cargo test -p module-jump-hop --manifest-path server-rs/Cargo.toml -- --nocapture`ã€`npm run check:encoding`。 +- å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + +## 2026-06-03 跳一跳角色形象改为陶泥儿 logo 逿˜Ž PNG + +- 背景:跳一跳è¿è¡Œæ€æ­¤å‰ä»ä½¿ç”¨æ—§å†…ç½® / CSS è§’è‰²å½¢è±¡ï¼Œå’Œç”¨æˆ·è¦æ±‚的陶泥儿 logo 角色ä¸ä¸€è‡´ï¼Œä¹Ÿå®¹æ˜“å’Œ DOM 地å—å±‚å‡ºçŽ°é®æŒ¡å±‚级问题。 +- 决策:`jump-hop` v1 ä¸å†æ¸²æŸ“内置 3D 角色几何体;è¿è¡Œæ€å’Œç»“果页统一使用 `public/branding/jump-hop-taonier-character.png`,该文件由陶泥儿 logo 处ç†ä¸ºé€æ˜Ž PNG åŽæŽ¥å…¥ã€‚è“„åŠ›æ—¶è§’è‰²æ²¿æ‹–æ‹½æ–¹å‘æ˜Žæ˜¾æ‹‰é•¿ï¼Œè½åœ°åŽå‘åæ–¹å‘回弹两次。`characterAsset` 继续仅作为历å²å…¼å®¹æè¿°å­—段,ä¸èƒ½é‡æ–°æ‰“开角色生图槽或把角色图片作为创作者å¯é…置输入。 +- å½±å“范围:`src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`ã€`src/components/jump-hop-result/JumpHopResultView.tsx`ã€è·³ä¸€è·³ PRD 和平å°é“¾è·¯æ–‡æ¡£ã€‚ +- éªŒè¯æ–¹å¼ï¼šè·³ä¸€è·³è¿è¡Œæ€ / ç»“æžœé¡µæµ‹è¯•éœ€è¦æ–­è¨€è§’色图片 src 为 `/branding/jump-hop-taonier-character.png`,并确认旧默认角色 fallback ä¸å†å‡ºçŽ°ã€‚ +- å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 # 2026-05-20 陶泥儿主视觉é…色回收为暖白/陶土橙 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 3dea8cd4..2c9ed44c 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -1564,14 +1564,45 @@ - 验è¯ï¼š`npm run typecheck`,并跑 `npm test -- src/routing/appPageRoutes.test.ts` 覆盖 JumpHop 阶段路径。 - å…³è”:`src/components/platform-entry/platformEntryTypes.ts`ã€`src/routing/appPageRoutes.ts`ã€`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/rpg-entry/RpgEntryHomeView.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 -## 跳一跳地å—图集ä¸è¦å¥—通用系列素æ n 行模型 +## 跳一跳地å—图集固定走 5x5 åœ°å—æ±  -- 现象:跳一跳åˆå§‹è‰ç¨¿ç”Ÿæˆæ—¶æŠ¥ `系列素æå›¾é›†çš„物å“行数ä¸èƒ½è¶…过 n。`,或者å³ä½¿ç»•过报错也åªç”Ÿæˆäº† atlas 预览路径,地å—切片没有真正è½ç›˜ã€‚ -- 原因:跳一跳地å—åªæœ‰ 6 个固定 tileType,但旧实现把它塞进通用系列素æ helper,并使用 `grid_size = 3` / `item_names = 6` çš„è¯­ä¹‰å†²çªæ¨¡åž‹ï¼›éšåŽåˆåªä¿ç•™ atlas 资产与模拟路径,没把六个切片é€ä¸€ä¸Šä¼ å¹¶ç¡®è®¤åˆ° `JumpHopTileAsset`。 -- 处ç†ï¼šè·³ä¸€è·³åœ°å—改用专用 `2行*3列` 图集 prompt,按 `start / normal / target / finish / bonus / accent` 顺åºåˆ‡ 6 å¼  PNG,并对æ¯å¼ åˆ‡ç‰‡å„自走 OSS 上传ã€asset_object 确认和 entity bind。 -- 验è¯ï¼š`cargo test -p api-server jump_hop_tile_atlas -- --nocapture` 通过åŽï¼Œå†çœ‹ `jump_hop.rs` ä¸åº”å†è°ƒç”¨ `build_generated_asset_sheet_prompt` 处ç†åœ°å—图集;公开结果里应能拿到 6 个独立 `JumpHopTileAsset`。 +- 现象:跳一跳åˆå§‹è‰ç¨¿ç”Ÿæˆæ—¶æŠ¥ `系列素æå›¾é›†çš„物å“行数ä¸èƒ½è¶…过 n。`,或者生æˆå®ŒæˆåŽåªæœ‰ atlas 预览路径,地å—切片没有真正è½ç›˜ã€‚ +- 原因:旧模æ¿å…ˆåŽå°è¯•过通用系列素æ helper å’Œ `2x3` 六格固定 tileType,但当å‰è·³ä¸€è·³å·²ç»é‡è®¾è®¡ä¸ºâ€œä¸»é¢˜ -> 5x5 地å—图集 -> 25 个等æƒåœ°å—æ±  -> æ— é™è·¯å¾„â€ï¼Œæ—§çš„物å“行数 / 固定类型模型都会把创作链路带å。 +- 处ç†ï¼šè·³ä¸€è·³åœ°å—固定生æˆä¸€å¼  `5x5` 主题图集,åŽç«¯æŒ‰å‡åŒ€ç½‘格切出 25 å¼  PNG,并对æ¯å¼ åˆ‡ç‰‡å„自走 OSS 上传ã€asset_object 确认和 entity bindï¼›ä¸è¦å†æ¢å¤ `2行*3列`ã€`start / normal / target / finish / bonus / accent` å…­æ ¼å£å¾„。 +- 验è¯ï¼š`jump_hop.rs` ä¸åº”å†è°ƒç”¨é€šç”¨ç‰©å“行数模型处ç†åœ°å—图集;公开结果里应能拿到 25 个独立 `JumpHopTileAsset`,è¿è¡Œæ€æ— é™è·¯å¾„ä»Žåœ°å—æ± éšæœºå–æã€‚ - å…³è”:`server-rs/crates/api-server/src/jump_hop.rs`ã€`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 +## 跳一跳地å—切片ä¸è¦æŒ‰ tileType å¤ç”¨èµ„äº§æ§½ä½ + +- 现象:跳一跳生æˆå®ŒæˆåŽï¼Œè¿è¡Œæ€çœ‹èµ·æ¥ä»åƒåœ¨æ˜¾ç¤ºé»˜è®¤å‡ ä½•地å—,或者地å—å›¾ç‰‡åœ¨åŠ è½½æ—¶é¢‘é—ªï¼›ç»“æžœé¡µåœ°å—æ± ä¹Ÿå¯èƒ½åªçœ‹åˆ°å°‘é‡é‡å¤ç´ æã€‚ +- 原因:`tileType` åªæ˜¯è·¯å¾„å¹³å°çš„玩法类型标签,25 个 atlas 切片里会é‡å¤å‡ºçް `normal / target / bonus / accent` 等类型。若åŽç«¯æŒä¹…化时用 `tileType` ç”Ÿæˆ slot/path,åŒç±»åž‹åˆ‡ç‰‡ä¼šå†™å…¥åŒä¸€ä¸ª `/generated-jump-hop-assets///image.png`,åŽä¸Šä¼ çš„切片覆盖先上传的切片,å‰ç«¯æ¢ç­¾ç¼“存也会读到é‡å¤æˆ–旧对象。 +- 处ç†ï¼šåŽç«¯åˆ‡å›¾åŽå¿…须按 atlas å•元格写入 `tile-01` 到 `tile-25` 的唯一 slot/pathï¼›å‰ç«¯ç»“果页和è¿è¡Œæ€å±•示生æˆå›¾æ—¶ç”¨ `assetObjectId` 作为 `refreshKey`,é¿å…é‡ç”ŸæˆåŽå¤ç”¨æ—§ç­¾å或旧图片缓存。 +- 验è¯ï¼š`cargo test -p api-server jump_hop --manifest-path server-rs/Cargo.toml -- --nocapture` åº”åŒ…å« `jump_hop_tile_asset_slots_are_unique_for_twenty_five_slices`ï¼›å‰ç«¯è¿è¡Œæ€æµ‹è¯•åº”æ–­è¨€åœ°å—æ¢ç­¾å¸¦ `assetObjectId` 刷新键。 +- å…³è”:`server-rs/crates/api-server/src/jump_hop.rs`ã€`src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`ã€`src/components/jump-hop-result/JumpHopResultView.tsx`。 + +## 跳一跳è½ç‚¹è¾…助标识ä¸è¦å†ç”¨èˆžå°é«˜åº¦å¸¸é‡æ‹è„‘袋投影 + +- 现象:拖拽时è½ç‚¹è¾…助标识虽然会动,但看起æ¥åƒé™æ€ç‚¹ä½æ¼‚移,和真实å¯è½åœ°çš„ä½ç½®å¯¹ä¸ä¸Šã€‚ +- åŽŸå› ï¼šè¾…åŠ©æ ‡è¯†å¦‚æžœåªæŒ‰ `stageSize.height` 和一个固定比例估算投影è·ç¦»ï¼Œå†åŽ»è·Ÿæ‹–æ‹½å‘é‡åˆæˆï¼Œå°±ä¼šå’Œå½“å‰åœ°å—到目标地å—的真实å±å¹•跨度脱节;三维场景层级过高时还会把辅助点直接盖ä½ã€‚ +- 处ç†ï¼šè¾…助标识必须使用当å‰åœ°å—与目标地å—之间的真实å±å¹•è·ç¦»å’ŒåŽç«¯ `chargeToDistanceRatio` åšæŠ•å½±ï¼Œå†æ˜ å°„到å±å¹•åæ ‡ï¼›åŒæ—¶æŠŠè¾…助层 z-index 放到三维角色层之上,é¿å…è¢«åœºæ™¯å±‚é®æŒ¡ã€‚ +- 验è¯ï¼šæ‹–拽åŠç¨‹æ—¶è¾…助点应è½åœ¨å½“å‰åœ°å—和目标地å—之间,完整拖拽时应逼近目标地å—中心;è¿è¡Œæ€æˆªå›¾é‡Œè¾…助点必须始终压在地å—与角色之上。 +- å…³è”:`src/services/jump-hop/jumpHopRuntimeModel.ts`ã€`src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`。 + +## 跳一跳è½ç‚¹è¾…助和åŽç«¯è£å†³å¿…é¡»ç»Ÿä¸€åæ ‡æ¢ç®— + +- 现象:è½ç‚¹è¾…助标识已ç»åŽ‹åœ¨ç›®æ ‡åœ°å—å›¾ç‰‡ä¸Šï¼Œæ¾æ‰‹åŽåŽç«¯ä»åˆ¤å®šå¤±è´¥ï¼ŒçŽ©å®¶çœ‹åˆ°çš„æ˜¯â€œæ˜Žæ˜Žçž„å‡†äº†å´æ²¡è½ä¸ŠåŽ»â€ã€‚ +- 原因:å‰ç«¯è¾…助标识使用å±å¹•åƒç´ å标绘制,而åŽç«¯è£å†³ä½¿ç”¨ä¸–ç•Œåæ ‡ã€‚å±å¹• y è½´å‘下为正ã€ä¸–界 y è½´å‘ä¸Šä¸ºæ­£ï¼›åŒæ—¶å±å¹• x/y æ¯ä¸ªä¸–界å•ä½å¯¹åº”çš„åƒç´ æ¯”例ä¸åŒã€‚è‹¥å‰ç«¯ç›´æŽ¥æŠŠå±å¹•åƒç´ æ‹–拽å‘é‡å‘ç»™åŽç«¯ï¼Œè¾…助点和åŽç«¯è½ç‚¹æ–¹å‘会ä¸ä¸€è‡´ã€‚ +- 处ç†ï¼šå‰ç«¯è¿è¡Œæ€ä¿ç•™åŽŸå§‹å±å¹•拖拽å‘é‡ç”¨äºŽç”»å¼¹å¼“和辅助点,但æäº¤åŽç«¯å‰å¿…须按当å‰åœ°å—到目标地å—çš„å±å¹•跨度 / 世界跨度把 xã€y 分别æ¢ç®—æˆä¸–界尺度一致的å‘é‡ï¼›åŽç«¯ç»§ç»­åªè´Ÿè´£åå‘弹射和è½ç‚¹è£å†³ã€‚ +- 验è¯ï¼šå‰ç«¯å›žå½’测试è¦åŒæ—¶è¦†ç›–辅助点完整拖拽到目标地å—ï¼Œä»¥åŠæäº¤ç»™åŽç«¯çš„å‘é‡å·²å®Œæˆä¸–界尺度æ¢ç®—ï¼›åŽç«¯é¢†åŸŸæµ‹è¯•覆盖å±å¹•å‘åŽä¸‹æ‹‰æ—¶åº”å‘世界 y 正方å‘跳出并命中。 +- å…³è”:`src/services/jump-hop/jumpHopRuntimeModel.ts`ã€`src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`ã€`server-rs/crates/module-jump-hop/src/application.rs`。 + +## è·³ä¸€è·³åˆ›ä½œå…¥å£æ—§æ–‡æ¡ˆå…ˆæŸ¥ SpacetimeDB é…ç½® + +- 现象:`JumpHopWorkspace` å·²åªå‰©ä¸»é¢˜è¾“入,但创作 Tab 的跳一跳模æ¿å¡ä»æ˜¾ç¤ºæ—§çš„â€œä¿¯è§†è§’è·³è·ƒé—¯å…³â€æˆ–拼图å‚考图。 +- 原因:创作入å£å¡ç‰‡äº‹å®žæºæ˜¯ SpacetimeDB `creation_entry_type_config` å’Œ `/api/creation-entry/config`,å‰ç«¯åªåšå±•ç¤ºæ´¾ç”Ÿï¼›å¦‚æžœåªæ”¹å·¥ä½œå°ã€PRD 或å‰ç«¯ç»„件,已有库里的旧入å£è¡Œä¸ä¼šè‡ªåЍå˜åŒ–ã€‚å½“å‰ `api-server` 读å–å…¥å£é…置时优先订阅缓存,缓存命中åŽä¸ä¼šå†èµ° procedure æ’­ç§ï¼Œæ‰€ä»¥åªæŠŠè¿ç§»å†™åœ¨ `get_creation_entry_config` 里ä¸å¤Ÿã€‚ +- 处ç†ï¼šåŒæ­¥æ›´æ–° `module-runtime` 默认入å£ç§å­ï¼Œå¹¶åœ¨ `spacetime-module/src/runtime/creation_entry_config.rs` 加åªå‘½ä¸­æ—§ç³»ç»Ÿé»˜è®¤å€¼çš„è¿ç§»ï¼›åŒæ—¶åœ¨ `spacetime-client` 的入å£é…置读模型里åšåŒä¸€æ¡æ—§ç³»ç»Ÿé»˜è®¤è¡Œçš„读路径纠å。跳一跳当å‰é»˜è®¤å€¼ä¸º `subtitle=主题驱动平å°è·³è·ƒ`ã€`image_src=/creation-type-references/jump-hop.webp`。 +- 验è¯ï¼šæœ¬åœ° `GET /api/creation-entry/config` çš„ `jump-hop` 项应返回新 subtitle 和新 imageSrcï¼›è‹¥ä»æ—§ï¼Œæ£€æŸ¥æœ¬åœ° SpacetimeDB 是å¦å·²å‘å¸ƒå½“å‰ `spacetime-module`,以åŠåŽå°æ˜¯å¦æ‰‹åŠ¨è¦†ç›–è¿‡å…¥å£é…置。若缓存路径和 procedure 路径返回ä¸ä¸€è‡´ï¼Œä¼˜å…ˆæ€€ç–‘读模型映射没åšçº åï¼Œè€Œä¸æ˜¯å‰ç«¯å±•示层。 + ## image2 dry-run 带å‚考图时ä¸è¦ç›´æŽ¥æ‰“å° data URL - 现象:使用 VectorEngine `gpt-image-2-all` 生æˆå¸¦å‚考图的概念图时,如果 dry-run 直接打å°å®Œæ•´è¯·æ±‚体,å‚考图会被转æˆè¶…é•¿ `data:image/png;base64,...`,终端日志会被数百万字符淹没。 @@ -1650,6 +1681,22 @@ - 验è¯ï¼š`npm test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile profile page matches the reference layout sections|profile scan action opens camera scanner instead of recharge panel"`。 - å…³è”:`src/components/rpg-entry/RpgEntryHomeView.tsx`ã€`docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 +## 旧创作入å£å…ˆç¡®è®¤æ˜¯ä¸æ˜¯æ—§ worktree 在å“应 + +- 现象:æµè§ˆå™¨é‡Œæ˜Žæ˜Žè¿˜çœ‹åˆ°è·³ä¸€è·³æ—§å…¥å£ï¼Œæ¯”如 `俯视角跳跃闯关` å’Œ `puzzle.webp`ï¼Œä½†å½“å‰ worktree é‡Œå·²ç»æ”¹æˆäº† `主题驱动平å°è·³è·ƒ` å’Œ `jump-hop.webp`。 +- åŽŸå› ï¼šæœ¬æœºå¸¸åŒæ—¶å­˜åœ¨ä¸¤ä¸ªå¼€å‘栈,旧 worktree å¯èƒ½è¿˜åœ¨å ç”¨ `3000/8082/3101/3102`ï¼Œè€Œå½“å‰ worktree å¯èƒ½è·‘在å¦ä¸€ç»„端å£ã€‚åªçœ‹é¡µé¢æ–‡æ¡ˆå°±ä¸‹ç»“论,容易把旧进程误认æˆå½“剿”¹åŠ¨æ²¡ç”Ÿæ•ˆã€‚ +- 处ç†ï¼šå…ˆç”¨ `Get-NetTCPConnection` / `Get-CimInstance Win32_Process` 确认端å£å¯¹åº”çš„å¯æ‰§è¡Œæ–‡ä»¶å’Œå‘½ä»¤è¡Œï¼Œå†åˆ†åˆ«è¯·æ±‚ `/api/creation-entry/config` 比对旧端å£ä¸Žå½“å‰ worktree 端å£ã€‚å¿…è¦æ—¶ä»¥å½“å‰ worktree 的实际端å£ä¸ºå‡†é‡æ–°æ‰“开页é¢ã€‚ +- 验è¯ï¼šæ—§ç«¯å£è¿”回旧跳一跳入å£ï¼Œå½“å‰ worktree 端å£è¿”回新跳一跳入å£ï¼›ä¸¤è¾¹çš„ `api-server` / `vite-cli` 命令行应指å‘ä¸åŒä»“库路径。 +- å…³è”:`scripts/dev.mjs`ã€`docs/ã€å¼€å‘è¿ç»´ã€‘本地开å‘验è¯ä¸Žç”Ÿäº§è¿ç»´-2026-05-15.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + +## 3001 无法访问先查旧 worktree å ç«¯å£å’Œ SpacetimeDB 版本 + +- 现象:`http://127.0.0.1:3001/` 打ä¸å¼€ï¼Œä½† `3000 / 3101 / 8082` 仿œ‰è¿›ç¨‹ï¼›`npm run dev` 直接退出,没有把新栈拉起æ¥ã€‚ +- 原因:旧 worktree çš„ `api-server`ã€`spacetime-standalone` å’Œ Vite 还活ç€ï¼Œæˆ–è€…å½“å‰ worktree 的本机 SpacetimeDB CLI 默认版本低于仓库é”定版本,`scripts/dev.mjs` 会先校验版本å†å¯åŠ¨å¹¶ç›´æŽ¥æŠ¥é”™é€€å‡ºã€‚ +- 处ç†ï¼šå…ˆåœæŽ‰å ç”¨ç«¯å£çš„æ—§è¿›ç¨‹ï¼Œå†æ‰§è¡Œ `spacetime version list` å’Œ `spacetime version use 2.3.0`,确认本机 CLI/standalone 与仓库一致åŽé‡æ–°å¯åЍ `npm run dev -- --no-interactive --web-port 3001 --api-port 8083 --spacetime-port 3103 --admin-web-port 3104`。 +- 验è¯ï¼š`http://127.0.0.1:3001/`ã€`http://127.0.0.1:8083/healthz`ã€`http://127.0.0.1:3103/v1/ping` 都返回 200,且进程命令行指å‘å½“å‰ worktree è·¯å¾„è€Œä¸æ˜¯åˆ«çš„仓库。 +- å…³è”:`scripts/dev.mjs`ã€`.hermes/shared-memory/pitfalls.md`ã€`docs/ã€å¼€å‘è¿ç»´ã€‘本地开å‘验è¯ä¸Žç”Ÿäº§è¿ç»´-2026-05-15.md`。 + ## 微信历å²å­¤å„¿ä½œå“ä¸è¦è®©æ–°æ³¨å†Œè´¦å·é¡¶æ›¿ - çŽ°è±¡ï¼šæ¸…ç©ºç”¨æˆ·æ•°æ®æˆ–è¿ç§»åކ岿•°æ®åŽï¼Œæ—§ä½œå“çš„ `owner_user_id` 为空或失效,新注册用户会因为顺åºå·å¤ç”¨æˆ–æ—§ ID 残留顶替作å“归属,导致刚注册就看到别人的è‰ç¨¿æˆ–å·²å‘布作å“。 @@ -1665,3 +1712,19 @@ - 处ç†ï¼šæŽ¨èé¡µæ‹–æ‹½åªæ ¡éªŒå½“剿˜¯å¦æœ‰ä½œå“ã€å¤šä½œå“å¯åˆ‡æ¢ä»¥åŠæ˜¯å¦æ­£åœ¨æäº¤åŠ¨ç”»ï¼Œä¸å†è¦æ±‚登录;登录æ€ç›¸å…³æ“作ä»ç”±ç‚¹èµžã€æ”¹é€ ç­‰æŒ‰é’®è‡ªèº«æƒé™æŽ§åˆ¶ã€‚ - 验è¯ï¼š`npx vitest run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx` 覆盖访客æ€çºµå‘滑动ä¸å¼¹ç™»å½•且触å‘ä¸‹ä¸€æ¡æŽ¨è。 - å…³è”:`src/components/rpg-entry/RpgEntryHomeView.tsx`ã€`src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`。 + +## 跳一跳飞行动画ä¸è¦ç›´æŽ¥ç”¨æœ€æ–° run é‡ç»˜åœ°å—çª—å£ + +- çŽ°è±¡ï¼šè·³ä¸€è·³æ¾æ‰‹åŽå¦‚æžœåŽç«¯å¾ˆå¿«è¿”回下一帧 run,地å—窗å£ä¼šç«‹åˆ»å‰ç§»ï¼Œè§’色翻腾动画看起æ¥åƒæ²¡æ’­æ”¾ï¼›è‹¥åŒæ—¶åˆ·æ–°å›¾ç‰‡èµ„产,还å¯èƒ½è¢«è¯¯è®¤ä¸ºåœ°å—频闪。 +- 原因:åŽç«¯ run 是规则真相,å‰ç«¯ runtime åˆéœ€è¦ä½Žå»¶è¿Ÿè¡¨çŽ°ã€‚å¦‚æžœ DOM å¹³å°å±‚直接用最新 `run.currentPlatformIndex` 渲染,åŽç«¯å›žåŒ…会抢在动画å‰å®Œæˆè§†è§‰åˆ‡æ¢ã€‚ +- 处ç†ï¼šå‰ç«¯ä¿ç•™ç‹¬ç«‹ `displayRun`ï¼Œæ¾æ‰‹åŽå…ˆè¿›å…¥ `isJumpAnimating=true`,角色在当å‰çª—å£å†…æ’值飞å‘目标地å—;约 `300ms` åŽå†æŠŠ `displayRun` 切到最新åŽç«¯ run,并进入约 `1440ms` çš„ `platformAdvancing` 表现æ€ã€‚æŽ¨è¿›æœŸé—´åœ°å— DOM 层和 Three.js 角色层必须统一包在åŒä¸€ä¸ª camera layer 下移动,旧当å‰åœ°å—用相机å移自然离开视野,新预览地å—从上方露出;ä¸è¦å†è®© p1/p2 å„自 top/left è¿‡æ¸¡ã€‚ç›¸æœºå±‚å¿…é¡»åŒæ—¶è®¾ç½® `--jump-hop-camera-shift-x` 与 `--jump-hop-camera-shift-y`,从旧目标地å—ä½ç½®æ–œå‘滑到新当å‰åœ°å—èšç„¦ä½ç½®ï¼Œé¿å…先横å‘瞬切居中å†çºµå‘推进。地å—ä¿ç•™å½“å‰ / 目标 / 预览的深度尺寸差异,但深度差异必须用固定宽高 + CSS transform scale 缓动实现,ä¸èƒ½ç›´æŽ¥æ”¹å®½é«˜çž¬åˆ‡ï¼›å½“剿€ä¸è¦é¢å¤–å  CSS scale。正å¼èƒœè´Ÿã€æˆåŠŸè·³è·ƒæ¬¡æ•°ã€æ—¶é•¿å’ŒæŽ’行榜ä»ä»¥åŽç«¯ run 为准,å‰ç«¯åªå»¶è¿Ÿæ˜¾ç¤ºæ€ã€‚ +- 验è¯ï¼š`npm test -- src/services/jump-hop/jumpHopRuntimeModel.test.ts src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx` 应覆盖动画期间平å°ä»åœåœ¨æ—§çª—å£ï¼ŒåŠ¨ç”»ç»“æŸåŽè¿›å…¥ `data-platform-advancing=true`,Three 角色层与地å—层åŒåœ¨ `jump-hop-camera-layer` 内,通过 `--jump-hop-camera-shift-x` å’Œ `--jump-hop-camera-shift-y` 完æˆç›¸æœºæ–œå‘推进,并校验å¯è§åœ°å—按深度ä¿ç•™ä¸åŒè§†è§‰å°ºå¯¸ã€è¿è¡Œæ€å¹³å°å®½é«˜ä½¿ç”¨å›ºå®šåŸºå‡†å€¼ã€æŽ¨è¿›æ€ transform transition 为 `1440ms`。 +- å…³è”:`src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`ã€`src/services/jump-hop/jumpHopRuntimeModel.ts`ã€`server-rs/crates/module-jump-hop/src/application.rs`。 + +## å«ä¸­æ–‡ image2 live 验è¯ä¸è¦ç”¨ PowerShell 管é“å–‚ Node æºç  + +- 现象:本地用 `@'...'@ | node -` è·‘ VectorEngine / gpt-image-2 live éªŒè¯æ—¶ï¼Œ`request.json` 里的中文 prompt å¯èƒ½å…¨éƒ¨å˜æˆ `????`,生æˆå›¾ä¼šå˜æˆå®Œå…¨ä¸ç›¸å…³çš„ UIã€å»ºç­‘æµ·æŠ¥æˆ–å…¶å®ƒéšæœºå†…å®¹ï¼Œå®¹æ˜“è¯¯åˆ¤ä¸ºæ¨¡åž‹ä¸æœä»Žæç¤ºè¯ã€‚ +- 原因:Windows PowerShell 管é“到 Node stdin æ—¶å¯èƒ½æŒ‰æœ¬æœºéž UTF-8 ç¼–ç ä¼ è¾“脚本文本,JS æºç é‡Œçš„中文字符串在进入 Node å‰å·²ç»æŸåï¼›Rust åŽç«¯çœŸå®žè¯·æ±‚ä¸ä¼šèµ°è¿™æ¡ç¼–ç è·¯å¾„。 +- 处ç†ï¼šå«ä¸­æ–‡æç¤ºè¯çš„ live 验è¯ä¼˜å…ˆå†™æˆ UTF-8 `.mjs` æ–‡ä»¶å†æ‰§è¡Œï¼Œæˆ–使用能确认 UTF-8 çš„è¿è¡Œå…¥å£ï¼›æ‰§è¡ŒåŽå…ˆæ£€æŸ¥æœ¬æ¬¡ `request.json` 是å¦ä¿ç•™çœŸå®žä¸­æ–‡ï¼Œå†åˆ¤æ–­ç”Ÿå›¾è´¨é‡ã€‚ä¸è¦åŸºäºŽ `????` prompt 生æˆçš„图片调整项目æç¤ºè¯ã€‚ +- 验è¯ï¼šç”Ÿæˆå‰åŽæ£€æŸ¥ `request.json`,其中 `prompt` å­—æ®µåº”æ˜¾ç¤ºä¸­æ–‡è€Œä¸æ˜¯é—®å·ï¼›åŒä¸€æç¤ºè¯åœ¨ UTF-8 文件脚本下应能得到符åˆä¸»é¢˜çš„图。 +- å…³è”:`.codex/skills/gpt-image-2-apimart/SKILL.md`ã€`server-rs/crates/api-server/src/jump_hop.rs`。 diff --git a/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md b/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md index a3ab635a..6bbc45af 100644 --- a/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md +++ b/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md @@ -2,491 +2,193 @@ ## 1. 目标 -新增一个å¯åˆ›ä½œã€å¯è¯•玩ã€å¯å‘布的玩法模æ¿ï¼š +`jump-hop` é‡å®šä¹‰ä¸ºç«–å±ä¿¯è§†è§’å¹³å°è·³è·ƒæ¸¸æˆã€‚创作者åªè¾“入主题,系统生æˆä¸€å¼ è¯¥ä¸»é¢˜çš„ `5x5` 地å—资æºå›¾é›†ï¼Œåˆ‡æˆ 25 个 2D 地å—ç´ æï¼›è¿è¡Œæ€ä½¿ç”¨æŠ é™¤ç™½åº•åŽçš„陶泥儿 logo 逿˜Ž PNG 作为玩家角色,并和这些 2D 地å—èµ„äº§ç»„æˆæ— é™å¹³å°æµã€‚ -```text -跳一跳 -``` +首版目标: -本模æ¿å‚考拼图模æ¿çš„åˆ›ä½œé—­çŽ¯ï¼Œæ²¿ç”¨â€œåˆ›ä½œå…¥å£ -> 生æˆè¿‡ç¨‹é¡µ -> 结果页 -> 试玩 -> å‘布â€çš„å¹³å°é“¾è·¯ï¼Œä½†çŽ©æ³•æœ¬ä½“æ”¹ä¸ºä¿¯è§†è§’ / ç­‰è·è§†è§’的跳跃闯关。 - -é¦–ç‰ˆè¦æ±‚: - -1. åˆå§‹è‰ç¨¿ç”Ÿæˆæ—¶ï¼Œè§’色形象å•独调用一次生图; -2. åˆå§‹è‰ç¨¿ç”Ÿæˆæ—¶ï¼Œåœ°å—åªè°ƒç”¨ä¸€æ¬¡ç”Ÿå›¾ï¼Œè¾“出 3D 视图的 2D 图片图集; -3. è¿è¡Œæ€ä¸æŽ¥çœŸå®ž 3D 网格,ä¸ç”Ÿæˆ GLB / glTFï¼› -4. 作å“å¯ä»¥ç›´æŽ¥è¿›å…¥è¯•玩和å‘布。 +1. 创作输入åªä¿ç•™ä¸»é¢˜ï¼Œæ ‡é¢˜ã€ç®€ä»‹ã€æ ‡ç­¾å’Œæç¤ºè¯ç”±ç³»ç»Ÿæ´¾ç”Ÿï¼› +2. image2 åªç”Ÿæˆä¸€å¼  `5x5` 地å—图集,åŽç«¯å‡åŒ€åˆ‡æˆ 25 å¼  PNGï¼› +3. 角色ä¸å†å•独生图,v1 使用 `public/branding/jump-hop-taonier-character.png` 陶泥儿 logo 逿˜Ž PNGï¼› +4. è¿è¡Œæ€æ¯å±åªå±•示 3 个地å—:当å‰åœ°å—ã€ç›®æ ‡åœ°å—ã€ä¸‹ä¸€é¢„览地å—ï¼› +5. æ“作方å¼ä¸ºæŒ‰ä½å±å¹•å‘åŽæ‹–åŠ¨è“„åŠ›ï¼Œæ¾æ‰‹åŽè§’è‰²å‘æ‹–æ‹½åæ–¹å‘弹出; +6. åªè¦è½ç‚¹æœªå‘½ä¸­ä¸‹ä¸€ä¸ªåœ°å—,本局立å³å¤±è´¥å¹¶å†»ç»“计时; +7. æˆç»©è®°å½•æˆåŠŸè·³è·ƒæ¬¡æ•°å’Œæ¸¸æˆæ—¶é•¿ï¼› +8. 排行榜按作å“维度展示玩家 IDã€æˆåŠŸè·³è·ƒæ¬¡æ•°å’Œæ¸¸æˆæ—¶é•¿ï¼ŒæŽ’åºä¸ºæˆåŠŸè·³è·ƒæ¬¡æ•°é™åºã€æ¸¸æˆæ—¶é•¿å‡åºã€æ›´æ–°æ—¶é—´å‡åºã€‚ ## 2. 模æ¿å®šä½ -æ¨¡æ¿ ID: +- æ¨¡æ¿ ID:`jump-hop` +- 展示å:`跳一跳` +- 工程域:`jump-hop` +- 创作入å£å¡ï¼š`subtitle = 主题驱动平å°è·³è·ƒ`,`imageSrc = /creation-type-references/jump-hop.webp` +- è¿è¡Œæ€ï¼š`DOM å¹³å° / DOM 角色 + Three.js 逿˜Žæ‰©å±•层 + DOM HUD` +- ç”»é¢æ¯”例:移动端竖å±ä¼˜å…ˆï¼Œæ¡Œé¢ç«¯å±…中承载 `9:16` +- ç´ æç­–略:2D 地å—图集 + 陶泥儿 logo 逿˜Žè§’色 +- 渲染分层:生æˆåœ°å—切片必须由 DOM å¹³å°å±‚直接渲染为图片;角色必须由 DOM 逿˜Ž PNG å±‚æ¸²æŸ“å¹¶ä¿æŒæœ€é«˜å±‚级,Three.js 逿˜Žç”»å¸ƒåªä½œä¸ºåŽç»­æ‰©å±•层,ä¸èƒ½æŠŠåœ°å—图片或角色回退为 WebGL å ä½æè´¨ + +æœ¬çŽ©æ³•ä¸æ˜¯æ¨ªç‰ˆå¹³å°è·³è·ƒï¼Œä¹Ÿä¸æ˜¯å…³å¡åˆ¶é—¯å…³ã€‚å¹³å°ä»Žå±å¹•下方å‘上无é™å»¶å±•,目标地å—在当å‰åœ°å—上方ä¸åŒ x è½´ä½ç½®éšæœºå‡ºçŽ°ã€‚ + +## 3. åˆ›ä½œå·¥å…·å¹³å°æŽ¥å…¥å£°æ˜Ž + +- å·¥ä½œå°æ¨¡å¼ï¼šè¡¨å•è¾“å…¥åˆ›ä½œå·¥ä½œå° +- åˆ›ä½œé“¾è·¯ï¼šå…¥å£ -> å·¥ä½œå° -> 生æˆé¡µ -> 结果页 -> 试玩 -> å‘布 -> è¿è¡Œæ€ +- å•图资产槽ä½ï¼šæ— ç‹¬ç«‹è§’色图槽ä½ï¼›v1 固定使用陶泥儿 logo 逿˜Ž PNG 角色 +- ç³»åˆ—ç´ ææ§½ä½ï¼š + - `batchId = jump-hop-tile-atlas` + - `sheetSpec = 5x5 / 1:1 / PNG / 纯绿色绿幕背景 / åŽç«¯åˆ‡å›¾é€æ˜ŽåŒ–` + - `slotSpecs = tile-01 ... tile-25`,æ¯ä¸ª slot 必须对应唯一 OSS path / `assetObjectId` + - 切图规则:按原图宽高å‡åˆ†ä¸º 5 行 5 列,从上到下ã€ä»Žå·¦åˆ°å³åˆ‡å‡º 25 å¼  PNGï¼›æ¯æ ¼é€æ˜ŽåŒ–åŽåªä¿ç•™æœ€å¤§çš„ alpha 连通主体,å†è£è¾¹å¹¶è¡¥é€æ˜Žå®‰å…¨è¾¹ï¼Œé¿å…相邻格越界碎片或方形æ‚边进入 tile + - 逿˜ŽåŒ–è§„åˆ™ï¼šç”Ÿæˆæ—¶è¦æ±‚绿幕背景,åŽç«¯ä¸Šä¼  OSS 剿Рæˆé€æ˜Ž PNG,并清ç†ä¸Žä¸»ä½“分离的å°åž‹æ®‹ç‰‡ + - 失败回写:生æˆå¤±è´¥æ—¶ session ä¿æŒ failed,å¯ä»Žç”Ÿæˆé¡µé‡è¯• + - 局部é‡ç”Ÿæˆï¼šç»“果页å…许é‡ç”Ÿæˆåœ°å—图集,ä»åªè°ƒç”¨ä¸€æ¬¡ image2ï¼›å‰ç«¯å±•示生æˆå›¾æ—¶ä»¥ `assetObjectId` 作为刷新键,é¿å…åŒä¸€è·¯å¾„é‡å†™åŽçš„æ—§ç­¾å或旧缓存 +- API 命å空间:`/api/creation/jump-hop/*`ã€`/api/runtime/jump-hop/*` +- 业务真相:åŽç«¯è£å†³è½ç‚¹ã€å¤±è´¥ã€æˆåŠŸè·³è·ƒæ¬¡æ•°ã€å†»ç»“时长和排行榜 +- 创作工具模å¼ä¾‹å¤–:无 +- 验è¯å‘½ä»¤ï¼š`npm run check:encoding`ã€`npm run typecheck`ã€`cargo test -p module-jump-hop --manifest-path server-rs/Cargo.toml` + +## 4. 创作输入 + +主题是唯一必填项。工作å°ä¸å±•示角色æç¤ºè¯ã€åœ°å—æç¤ºè¯ã€é£Žæ ¼å¡ã€éš¾åº¦å¡ã€ç»ˆç‚¹æ°›å›´æˆ–规则说明。 + +æäº¤åŽç³»ç»Ÿè‡ªåŠ¨æ´¾ç”Ÿï¼š + +1. ä½œå“æ ‡é¢˜ï¼šä¸»é¢˜ä¸ºç©ºç™½ä¿®å‰ªåŽçš„短标题,默认å‰ç¼€ä¸å¤–露; +2. 作å“简介:基于主题生æˆä¸€å¥çŸ­ç®€ä»‹ï¼› +3. 标签:`跳一跳`ã€`休闲` 和主题关键è¯ï¼› +4. åœ°å—æç¤ºè¯ï¼šå›´ç»•ä¸»é¢˜ç”Ÿæˆ 25 个风格一致的俯视角清爽游æˆåŒ– 2D å¹³å°ç´ æï¼Œæ¯ä¸€å—都是符åˆä¸»é¢˜çš„å•独å¯è·³è·ƒå¹³å°ï¼›å®žé™… image2 prompt 使用“独立å¯è½è„šå¹³å°ç´ æ / å¹³å°è£¸ç´ æ / 完整平å°â€æŽªè¾žï¼Œä¸å†æŠŠæ­£å‘主体æè¿°æˆå›¾æ ‡é›†æˆ–游æˆç•Œé¢èµ„æºï¼› +5. åˆå§‹å¹³å°æµå‚数:固定 v1 æ ‡å‡†å‚æ•°ï¼Œä¸è®©åˆ›ä½œè€…手工调规则。 + +## 5. 地å—图集 + +image2 åªç”Ÿæˆä¸€å¼  `1:1` 图片,画é¢ä¸º `5x5` å‡åŒ€åˆ†å¸ƒå¹³å°è£¸ç´ æï¼›å®žé™…æç¤ºè¯å¿…须先约æŸâ€œç”»é¢åªåŒ…å« 25 个独立跳一跳å¯è½è„šå¹³å°ç´ æâ€ï¼Œå¹¶æ˜Žç¡®ä¸æ˜¯æ¸¸æˆç•Œé¢ã€æ£‹ç›˜ã€èƒŒåŒ…ã€è£…å¤‡æ æˆ–图标集页é¢ã€‚ + +å›¾é›†è¦æ±‚: + +1. æ¯æ ¼åªæ”¾ä¸€ä¸ªå®Œæ•´åœ°å—资æºï¼› +2. 资æºä¸ºçº¯ 2D å¹³é¢ç´ æï¼Œä½†è¦è¡¨çŽ°ä¸ºç¬¦åˆä¸»é¢˜ä¸”有设计感的俯视角清爽游æˆåŒ–立体感平å°ï¼Œæœ‰é¡¶é¢ã€ä¸»ä½“内部明暗和清晰轮廓;主题元素必须直接æˆä¸ºå¹³å°ä¸»ä½“,例如“水果â€åº”生æˆè‹¹æžœåˆ‡ç‰‡ã€æ©™å­åˆ‡ç‰‡ã€è¥¿ç“œå—ã€è‰èŽ“ã€è èã€é¦™è•‰ç­‰æ°´æžœé€ åž‹å¹³å°ï¼› +3. 25 ä¸ªåœ°å—æ¥è‡ªåŒä¸€ä¸»é¢˜ã€åŒä¸€å…‰å‘å’ŒåŒä¸€æè´¨ä½“系; +4. 背景为纯绿色绿幕,方便åŽç«¯é€æ˜ŽåŒ–ï¼› +5. ä¸åŒ…å«è§’è‰²ã€æ–‡å­—ã€æ°´å°ã€UIã€æ¸¸æˆé¢æ¿ã€æ£‹ç›˜ã€èƒŒåŒ…ã€è£…备æ ã€æŒ‰é’®ã€æ ‡é¢˜ã€å¤–层边框ã€ç½‘格线ã€åœºæ™¯èƒŒæ™¯ã€è½åœ°æŠ•å½±ã€æŽ¥è§¦é˜´å½±ã€æ–¹å½¢é˜´å½±ã€æ–¹å½¢åº•æ¿ã€ç™½åº•ã€ç°åº•或黑底; +6. 地å—ä¸èƒ½è·¨æ ¼ã€è´´è¾¹æˆ–进入相邻格,主体必须居中并ä¿ç•™è‡³å°‘ 18% 纯绿色安全留白;æ¯ä¸ªå¹³å°ä¹‹é—´åªèƒ½æ˜¯çº¯ç»¿è‰²ç©ºç™½ï¼Œä¸ç”»å®¹å™¨æ¡†æˆ–棋盘格。 + +切片顺åºå›ºå®šä¸ºï¼š ```text -jump-hop +tile-01 tile-02 tile-03 tile-04 tile-05 +tile-06 tile-07 tile-08 tile-09 tile-10 +tile-11 tile-12 tile-13 tile-14 tile-15 +tile-16 tile-17 tile-18 tile-19 tile-20 +tile-21 tile-22 tile-23 tile-24 tile-25 ``` -用户展示å: +è¿è¡Œæ€éšæœºä½¿ç”¨è¿™ 25 个地å—作为åŽç»­å¹³å°å¤–观。起点地å—å¯å¤ç”¨ç¬¬ä¸€ä¸ªåˆ‡ç‰‡ï¼Œå…¶ä½™å¹³å°ä»Žå®Œæ•´æ± ä¸­éšæœºé€‰æ‹©ã€‚ + +## 6. è¿è¡Œæ€è§„则 + +### 6.1 平尿µ + +è¿è¡Œæ€ä»Žåº•部åˆå§‹åœ°å—开始,åŽç»­åœ°å—æŒç»­å‘å±å¹•上方生æˆã€‚æ¯æ¬¡ç›¸æœºçª—å£åªä¿ç•™ 3 个地å—å¯è§ï¼š + +1. 当å‰åœ°å—ï¼› +2. 目标地å—ï¼› +3. 下一预览地å—。 + +æœåŠ¡ç«¯ä¿å­˜å½“å‰ run çš„è·¯å¾„ç¼“å†²ï¼Œå¹¶åœ¨æ¯æ¬¡æˆåŠŸè½åœ°åŽæŒ‰åŒä¸€ seed è¡¥é½åŽç»­åœ°å—。å‰ç«¯åªå±•示æœåŠ¡ç«¯å¿«ç…§ï¼Œä¸è‡ªè¡Œç”Ÿæˆæ­£å¼è·¯å¾„。 + +### 6.2 æ“作 + +1. 用户按ä½å½“å‰åœ°å—或画é¢ï¼› +2. å‘åŽæ‹–动形æˆè“„力å‘é‡ï¼› +3. æ¾æ‰‹åŽè§’è‰²æ²¿æ‹–æ‹½åæ–¹å‘弹出; +4. 拖拽è·ç¦»å†³å®šåŠ›åº¦ï¼Œæ‹–æ‹½æ–¹å‘决定è½ç‚¹æ–¹å‘ï¼› +5. 力度和方å‘都由å‰ç«¯æäº¤ç»™åŽç«¯è£å†³ã€‚ + +æ‰‹æ„Ÿå‚æ•°å›ºå®šç”±åŽç«¯ `module-jump-hop` æä¾›ï¼š`chargeToDistanceRatio = 0.008`。该值表示åŒç­‰ä¸–界跳跃è·ç¦»åªéœ€è¦æ—§ç‰ˆ `0.004` é…置的一åŠå±å¹•拖动è·ç¦»ï¼›æ—§ä½œå“è¿è¡Œæ—¶è‹¥ä»æºå¸¦ `0.004`,开局归一化为 `0.008`。 + +æ¾æ‰‹åŽå‰ç«¯å¿…须立å³ç”Ÿæˆ `visualJump`,用当å‰è§’色ä½ç½®ä½œä¸ºèµ·ç‚¹ã€å‰ç«¯é¢„测è½ç‚¹ä½œä¸ºç»ˆç‚¹ï¼Œæ’­æ”¾çº¦ `560ms` 的角色飞行动画;角色从当å‰åœ°å—å¼¹å‘预测è½ç‚¹ï¼Œè“„åŠ›é˜¶æ®µè§’è‰²åº”æ²¿æ‹–æ‹½æ–¹å‘æ˜Žæ˜¾æ‹‰é•¿ï¼Œè½åœ°åŽå†å‘åæ–¹å‘回弹两次。动画期间 DOM 地å—窗å£ä¿æŒåœ¨æœ¬æ¬¡èµ·è·³å‰çš„ 3 å—布局,动画路径ä¸å¾—等待åŽç«¯æ–° run。若åŽç«¯æ–° run 晚于飞行动画返回,角色必须åœåœ¨é¢„测è½ç‚¹ç­‰å¾…,直到新 run 到达åŽå†æŠŠæ˜¾ç¤ºæ€åˆ‡åˆ°åŽç«¯è¿”回的最新 run,并进入约 `1440ms` çš„ç›¸æœºæŽ¨è¿›è¿‡æ¸¡ã€‚æŽ¨è¿›è¿‡æ¸¡ä¸­ï¼Œåœ°å— DOM 层和 DOM 角色层必须放在åŒä¸€ä¸ªç›¸æœºå±‚里统一ä½ç§»ï¼Œä¸å…许 p1/p2 å•独改 `top/left` åšè¿‡æ¸¡ï¼›æ—§å½“å‰åœ°å—éšç›¸æœºæŽ¨è¿›è‡ªç„¶ç¦»å¼€è§†é‡Žï¼Œæ–°é¢„览地å—从上方自然露出,é¿å…角色和地å—ä¸åŒæ­¥æˆ–é—ªçŽ°ã€‚ç›¸æœºæŽ¨è¿›å¿…é¡»åŒæ—¶æºå¸¦ X/Y å移,从旧目标地å—ä½ç½®æ–œå‘滑到新当å‰åœ°å—èšç„¦ä½ç½®ï¼Œä¸å…许先横å‘瞬切居中åŽå†åªåšçºµå‘滑动。地å—å¯ä»¥ä¿ç•™å½“å‰ / 目标 / 预览的深度尺寸差异,但该差异必须通过固定基准宽高上的 CSS `transform: scale(...)` 表达,并在相机推进期间用åŒä¸€ `1440ms` 缓动过渡;ä¸å¾—通过直接改宽高造æˆçž¬åˆ‡å˜å¤§ã€‚当å‰åœ°å—高亮ä¸å¾—é¢å¤–通过 CSS `scale` 放大。该动画åªå±žäºŽè¡¨çŽ°å±‚ï¼Œå‘½ä¸­ã€å¤±è´¥ã€æˆåŠŸè·³è·ƒæ¬¡æ•°å’Œå†»ç»“æ—¶é•¿ä»ä»¥åŽç«¯è£å†³ä¸ºå‡†ã€‚ + +### 6.3 判定 + +1. 目标永远是当å‰åœ°å—åŽçš„下一个地å—ï¼› +2. è½ç‚¹è¿›å…¥ä¸‹ä¸€ä¸ªåœ°å—è½åœ°åŠå¾„,则æˆåŠŸï¼› +3. è½ç‚¹æœªè¿›å…¥ä¸‹ä¸€ä¸ªåœ°å—è½åœ°åŠå¾„,则失败; +4. 失败åŽçŠ¶æ€æ”¹ä¸º `failed`,计时冻结; +5. v1 没有通关状æ€ã€comboã€perfect 或生命数。 + +### 6.4 计分与时间 + +- æˆåŠŸè·³è·ƒæ¬¡æ•°ï¼šæ¯æˆåŠŸè½åˆ°ä¸‹ä¸€ä¸ªåœ°å—åŽ `+1`ï¼› +- æ¸¸æˆæ—¶é•¿ï¼š`startedAtMs` 到 `finishedAtMs`,失败时冻结; +- è¿è¡Œä¸­æ—¶é•¿ç”±å‰ç«¯æ ¹æ®æœåŠ¡ç«¯ `startedAtMs` 展示; +- 失败åŽåªå±•示冻结时长。 + +## 7. 排行榜 + +排行榜按作å“维度生æˆã€‚æ¯ä½çީ家åªä¿ç•™ 1 æ¡æœ€ä½³è®°å½•。 + +排åºè§„则固定为: ```text -跳一跳 +successfulJumpCount desc -> durationMs asc -> updatedAt asc ``` -体验关键è¯ï¼š - -1. 俯视角; -2. ç­‰è·æ„Ÿåœ°å—ï¼› -3. å•局闯关; -4. é•¿æŒ‰è“„åŠ›ï¼Œæ¾æ‰‹èµ·è·³ï¼› -5. è½»é‡ä¼‘闲。 - -首版采用竖å±ä¼˜å…ˆçš„移动端体验,桌é¢ç«¯ä¿æŒå±…ä¸­å±•ç¤ºï¼Œç”»é¢æ¯”例以 `9:16` 为主。å‚考图的核心视觉è¦ç‚¹æ˜¯ï¼š - -1. 大é¢ç§¯ç•™ç™½æˆ–浅色æ¸å˜èƒŒæ™¯ï¼› -2. 角色站在å•个地å—上; -3. åœ°å—æœ‰æ˜Žæ˜¾é¡¶é¢ã€ä¾§é¢å’ŒæŠ•影; -4. 整体是俯视角 / ç­‰è·è§†è§’ï¼Œè€Œä¸æ˜¯æ¨ªç‰ˆå¹³å°è·³è·ƒï¼› -5. UI 克制,åªä¿ç•™å¿…è¦æŽ§åˆ¶ï¼Œä¸å †è¯´æ˜Žæ–‡æ¡ˆã€‚ - -## 3. 与拼图模æ¿çš„å¤ç”¨è¾¹ç•Œ - -å¯ä»¥å¤ç”¨ï¼š - -1. 创作入å£å’Œæ¨¡æ¿åˆ†æµï¼› -2. 生æˆè¿‡ç¨‹é¡µï¼› -3. 结果页的è‰ç¨¿ä¿å­˜ã€è¿”回编辑ã€è¯•玩ã€å‘布ã€åˆ†äº«é“¾è·¯ï¼› -4. ä½œå“æž¶å±•示和è‰ç¨¿æ¢å¤å£å¾„ï¼› -5. å¹³å°ç»Ÿä¸€çš„å‘布与公开展示æµç¨‹ã€‚ - -ä¸å¤ç”¨ï¼š - -1. 拼图关å¡åˆ‡ç‰‡é€»è¾‘ï¼› -2. 拼图拖拽拼å—逻辑; -3. 拼图 UI 背景和多关å¡ç¼–辑结构; -4. 任何方格拼åˆè¯­ä¹‰ã€‚ - -## 4. 工程接入范围 - -首版需è¦åšåˆ°å®Œæ•´çŽ©æ³•é—­çŽ¯ï¼Œä¸åªåšå…¥å£å ä½ã€‚ - -新增å‰ç«¯é˜¶æ®µï¼š - -```text -jump-hop-workspace -jump-hop-generating -jump-hop-result -jump-hop-runtime -jump-hop-gallery-detail -``` - -新增å‰ç«¯ç»„件建议: - -1. `src/components/jump-hop-creation/JumpHopWorkspace.tsx`ï¼› -2. `src/components/jump-hop-result/JumpHopResultView.tsx`ï¼› -3. `src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`ï¼› -4. `src/services/jump-hop/jumpHopClient.ts`。 - -新增共享契约建议: - -1. `packages/shared/src/contracts/jumpHop.ts`ï¼› -2. `server-rs/crates/shared-contracts/src/jump_hop.rs`。 - -新增åŽç«¯æ¨¡å—建议: - -1. `server-rs/crates/module-jump-hop`:纯领域规则,包å«è·¯å¾„生æˆã€è“„力æ¢ç®—ã€è½ç‚¹åˆ¤å®šã€é€šå…³ / å¤±è´¥çŠ¶æ€æœºï¼› -2. `server-rs/crates/api-server/src/jump_hop.rs` å’Œ `src/jump_hop/` å­æ¨¡å—:HTTP handlerã€ç”Ÿæˆç¼–排ã€èµ„产ä¿å­˜å’Œ DTO 映射; -3. `server-rs/crates/spacetime-module/src/jump_hop.rs`:sessionã€work profileã€runtime runã€å…¬å¼€ view å’Œ reducer / procedureï¼› -4. `server-rs/crates/spacetime-client/src/jump_hop.rs`:api-server 访问 SpacetimeDB çš„ facadeï¼› -5. `server-rs/crates/api-server/src/modules/jump_hop.rs`:路由挂载。 - -å…¥å£é…置事实æºå¿…须走 SpacetimeDB `creation_entry_type_config` 默认ç§å­å’ŒåŽå°é…置接å£ï¼Œä¸æ–°å¢žå‰ç«¯ç¡¬ç¼–ç å…¥å£é…置。 - -## 5. 创作输入 - -创作者需è¦å¡«å†™ä»¥ä¸‹å†…容: - -1. 作å“主题æè¿°ï¼Œå¿…å¡«ï¼› -2. 角色形象æè¿°ï¼Œå¿…å¡«ï¼› -3. 地å—风格å¡ï¼Œå¿…选; -4. 难度,必选; -5. å¯é€‰çš„终点氛围或节å¥å好。 - -推è的最å°è¾“å…¥å½¢æ€æ˜¯ï¼š - -1. 一å¥è¯ä¸»é¢˜ï¼› -2. 角色一å¥è¯æè¿°ï¼› -3. 风格å¡ï¼› -4. 难度å¡ã€‚ - -ä¸åœ¨é¦–版开放手工拖拽平å°ç¼–辑器。平å°è·¯å¾„ã€åœ°å—é—´è·å’Œç»ˆç‚¹ä½ç½®ç”±ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆï¼Œåˆ›ä½œè€…åªè´Ÿè´£é£Žæ ¼ä¸Žéš¾åº¦é€‰æ‹©ã€‚ - -### 5.1 地å—é£Žæ ¼å¡ - -建议æä¾›ä»¥ä¸‹é£Žæ ¼ï¼š - -1. æžç®€ç§¯æœ¨ï¼› -2. 纸模玩具; -3. 霓虹玻璃; -4. 森林石å—ï¼› -5. 未æ¥é‡‘属; -6. 自定义。 - -### 5.2 难度 - -建议æä¾›ä»¥ä¸‹ç¦»æ•£æ¡£ä½ï¼š - -1. è½»æ¾ï¼› -2. 标准; -3. 进阶; -4. 挑战。 - -难度主è¦å½±å“: - -1. å¹³å°è·¯å¾„长度; -2. å¹³å°é—´è·ï¼› -3. å¯è½ç‚¹å®¹å·®ï¼› -4. 完美è½ç‚¹çª—å£ï¼› -5. 终点å‰çš„节å¥å˜åŒ–。 - -## 6. 生æˆè§„则 - -本模æ¿å¿…须把生图责任拆æˆä¸¤æ¡ç‹¬ç«‹é“¾è·¯ï¼š - -### 6.1 角色形象åªç”Ÿä¸€æ¬¡ - -角色形象必须åªè°ƒç”¨ä¸€æ¬¡ç”Ÿå›¾ï¼Œè¾“出一张å¯ç›´æŽ¥è¿›å…¥è¿è¡Œæ€çš„主角色图。 - -è§’è‰²å›¾è¦æ±‚: - -1. å•人主角; -2. 全身å¯è§ï¼› -3. 逿˜ŽèƒŒæ™¯ï¼› -4. 角色站姿或轻微å‰å€¾å§¿æ€ï¼› -5. 镜头和é€è§†å¿…须匹é…俯视角场景; -6. ä¸è¦æ±‚多视角,ä¸è¦æ±‚多帧动画图集。 - -角色图生æˆåŽä½œä¸ºä½œå“级锚点资产使用,结果页ã€å°é¢åˆæˆã€è¯•玩和å‘布都å¤ç”¨åŒä¸€å¼ å›¾ã€‚åŽç»­å¦‚æžœåªä¿®æ”¹æ ‡é¢˜ã€æ ‡ç­¾ã€éš¾åº¦æˆ–路径,ä¸åº”é»˜è®¤é‡æ–°ç”Ÿè§’è‰²ã€‚åªæœ‰ç”¨æˆ·åœ¨ç»“果页明确点击“é‡ç”Ÿæˆè§’è‰²â€æ—¶ï¼Œæ‰å…许å†è°ƒç”¨ä¸€æ¬¡è§’色生图。 - -### 6.2 地å—åªç”Ÿä¸€æ¬¡å›¾é›† - -地å—å¿…é¡»åªè°ƒç”¨ä¸€æ¬¡ç”Ÿå›¾ï¼Œè¾“出一张 3D 视图的 2D 图片图集,å†ç”±åŽç«¯åˆ‡æˆè¿è¡Œæ€å¯ç”¨çš„地å—资产。该图集使用跳一跳专用 `2行*3列` 六格布局,ä¸å¥—用通用“æ¯ä¸ªç‰©å“ä¸€è¡Œã€æ¯è¡Œ n 个ä¸åŒè§†å›¾â€çš„ç³»åˆ—ç´ ææ¨¡åž‹ã€‚ - -地å—å›¾é›†è¦æ±‚: - -1. ç»Ÿä¸€ä½¿ç”¨ç­‰è· / 俯视角; -2. 必须表现出顶é¢ã€ä¾§é¢å’ŒæŠ•影; -3. å¿…é¡»ä¸Žè§’è‰²å›¾ä¿æŒåŒä¸€å…‰å‘ï¼› -4. 必须有清晰的立体层次,但ä»ç„¶æ˜¯ 2D 图片; -5. 六格必须按固定顺åºåŒ…å«ä»¥ä¸‹åœ°å—类型: - - 起点地å—ï¼› - - 普通地å—ï¼› - - 目标地å—ï¼› - - 终点地å—ï¼› - - 奖励地å—ï¼› - - 视觉强调地å—。 - -固定格ä½ä¸ºï¼š - -| æ ¼ä½ | tileType | 语义 | -| --- | --- | --- | -| 第 1 行第 1 列 | `start` | èµ·ç‚¹åœ°å— | -| 第 1 行第 2 列 | `normal` | æ™®é€šåœ°å— | -| 第 1 行第 3 列 | `target` | ç›®æ ‡åœ°å— | -| 第 2 行第 1 列 | `finish` | ç»ˆç‚¹åœ°å— | -| 第 2 行第 2 列 | `bonus` | å¥–åŠ±åœ°å— | -| 第 2 行第 3 列 | `accent` | è§†è§‰å¼ºè°ƒåœ°å— | - -图集生æˆåŽæŒ‰åœ°å—类型切分并去掉背景,è¿è¡Œæ€ç›´æŽ¥æ¶ˆè´¹åˆ‡å¥½çš„ PNG,ä¸åœ¨å‰ç«¯åšå¤æ‚æ‹¼æŽ¥ã€‚åªæœ‰ç”¨æˆ·åœ¨ç»“果页明确点击“é‡ç”Ÿæˆåœ°å—â€æ—¶ï¼Œæ‰å…许å†è°ƒç”¨ä¸€æ¬¡åœ°å—图集生图。 - -### 6.3 䏿–°å¢žç¬¬ä¸‰æ¬¡ç”Ÿæˆ - -é¦–ç‰ˆä¸æŠŠå°é¢ã€åˆ†äº«æµ·æŠ¥ã€è·¯å¾„é¢„è§ˆå†æ‹†æˆç¬¬ä¸‰æ¬¡å›¾åƒç”Ÿæˆã€‚å°é¢å’Œåˆ†äº«å›¾å¿…须由角色图 + 地å—图集在本地或åŽç«¯è½»é‡åˆæˆï¼Œä¸é¢å¤–增加新的角色生图次数。 - -### 6.4 è·¯å¾„å…ƒæ•°æ® - -除图片资产外,系统还必须生æˆè·³è·ƒè·¯å¾„元数æ®ï¼š - -1. å¹³å°åºåˆ—ï¼› -2. å¹³å°ä¸­å¿ƒç‚¹ï¼› -3. å¹³å°å®½åº¦ï¼› -4. å¹³å°é—´è·ï¼› -5. 终点索引; -6. è¯„åˆ†å’Œå®¹å·®å‚æ•°ã€‚ - -路径由领域规则自动生æˆï¼Œåˆ›ä½œè€…ä¸ç›´æŽ¥ç¼–è¾‘åæ ‡ã€‚路径元数æ®ä¸ä¾èµ– LLM 或图片生æˆã€‚ - -### 6.5 推è的难度区间 - -| 难度 | 平尿•°é‡ | å¹³å°é—´è· | èŠ‚å¥ | -| --- | ---: | --- | --- | -| è½»æ¾ | 12 - 14 | 短 | 宽容 | -| 标准 | 16 - 18 | 中 | 稳定 | -| 进阶 | 20 - 24 | 中长 | 紧凑 | -| 挑战 | 26 - 32 | é•¿ | 高压 | - -å¹³å°å®½åº¦å’Œå®¹å·®ç”±ç³»ç»ŸæŒ‰éš¾åº¦è‡ªåŠ¨ç¼©æ”¾ï¼Œä¸è¦æ±‚创作者手工填写。 - -## 7. å¥‘çº¦è‰æ¡ˆ - -### 7.1 è‰ç¨¿ç»“æž„ - -`JumpHopDraft` 至少包å«ï¼š - -1. `templateId = "jump-hop"`ï¼› -2. `templateName = "跳一跳"`ï¼› -3. `profileId`ï¼› -4. `workTitle`ï¼› -5. `workDescription`ï¼› -6. `themeTags`ï¼› -7. `difficulty`ï¼› -8. `stylePreset`ï¼› -9. `characterPrompt`ï¼› -10. `tilePrompt`ï¼› -11. `characterAsset`ï¼› -12. `tileAtlasAsset`ï¼› -13. `tileAssets[]`ï¼› -14. `path`ï¼› -15. `coverComposite`ï¼› -16. `generationStatus`。 - -### 7.2 资产结构 - -`JumpHopCharacterAsset` 至少包å«ï¼š - -1. `assetId`ï¼› -2. `imageSrc`ï¼› -3. `imageObjectKey`ï¼› -4. `assetObjectId`ï¼› -5. `generationProvider`ï¼› -6. `prompt`ï¼› -7. `width`ï¼› -8. `height`。 - -`JumpHopTileAsset` 至少包å«ï¼š - -1. `tileType`ï¼› -2. `imageSrc`ï¼› -3. `imageObjectKey`ï¼› -4. `assetObjectId`ï¼› -5. `sourceAtlasCell`ï¼› -6. `visualWidth`ï¼› -7. `visualHeight`ï¼› -8. `topSurfaceRadius`ï¼› -9. `landingRadius`。 - -`tileType` 首版é™å®šï¼š - -```text -start | normal | target | finish | bonus | accent -``` - -### 7.3 路径结构 - -`JumpHopPath` 至少包å«ï¼š - -1. `seed`ï¼› -2. `difficulty`ï¼› -3. `platforms[]`ï¼› -4. `finishIndex`ï¼› -5. `cameraPreset`ï¼› -6. `scoring`。 - -`JumpHopPlatform` 至少包å«ï¼š - -1. `platformId`ï¼› -2. `tileType`ï¼› -3. `x`ï¼› -4. `y`ï¼› -5. `width`ï¼› -6. `height`ï¼› -7. `landingRadius`ï¼› -8. `perfectRadius`ï¼› -9. `scoreValue`。 - -### 7.4 è¿è¡Œæ€å¿«ç…§ - -`JumpHopRunSnapshot` 至少包å«ï¼š - -1. `runId`ï¼› -2. `profileId`ï¼› -3. `status = playing | failed | cleared`ï¼› -4. `currentPlatformIndex`ï¼› -5. `score`ï¼› -6. `combo`ï¼› -7. `lastJump`ï¼› -8. `startedAtMs`ï¼› -9. `finishedAtMs`。 - -`lastJump` 至少包å«ï¼š - -1. `chargeMs`ï¼› -2. `jumpDistance`ï¼› -3. `targetPlatformIndex`ï¼› -4. `landedX`ï¼› -5. `landedY`ï¼› -6. `result = miss | hit | perfect | finish`。 - -## 8. API è‰æ¡ˆ - -HTTP 路由建议: - -```text -POST /api/creation/jump-hop/sessions -GET /api/creation/jump-hop/sessions/{sessionId} -POST /api/creation/jump-hop/sessions/{sessionId}/actions -POST /api/creation/jump-hop/works/{profileId}/publish -GET /api/runtime/jump-hop/works/{profileId} -POST /api/runtime/jump-hop/runs -POST /api/runtime/jump-hop/runs/{runId}/jump -POST /api/runtime/jump-hop/runs/{runId}/restart -GET /api/runtime/jump-hop/gallery -GET /api/runtime/jump-hop/gallery/{publicWorkCode} -``` - -动作类型建议: - -```text -compile-draft -regenerate-character -regenerate-tiles -update-work-meta -update-difficulty -``` - -`compile-draft` 是长耗时动作。å‰ç«¯è¿›å…¥ç”Ÿæˆé¡µåŽå¿…é¡»æŒä¹…化 `generationStatus=generating`,刷新åŽèƒ½ä»Žä½œå“æž¶æ¢å¤ç”Ÿæˆé¡µã€‚失败å‰éœ€è¦å¤è¯» session;如果åŽç«¯å·²ç»å®Œæˆè‰ç¨¿å¹¶å†™å›žèµ„产,å‰ç«¯æŒ‰æˆåŠŸæ”¶å°¾ã€‚ - -## 9. SpacetimeDB 表和 view - -建议新增表: - -1. `jump_hop_agent_session`ï¼› -2. `jump_hop_work_profile`ï¼› -3. `jump_hop_runtime_run`ï¼› -4. `jump_hop_event`ï¼› -5. `jump_hop_leaderboard_entry`ï¼Œé¦–ç‰ˆå¯æš‚ä¸å¯¹å¤–展示; -6. `jump_hop_gallery_view`ï¼› -7. `jump_hop_gallery_card_view`。 - -表结构新增字段必须按 SpacetimeDB è¿ç§»è§„则放在结构体末尾并设置明确默认值。新增或调整表ã€reducerã€procedureã€view åŽå¿…é¡»åŒæ­¥ `migration.rs`ã€è¡¨ç›®å½•ã€ç”Ÿæˆ bindings,并执行 `npm run check:spacetime-schema`。 - -公开列表主路径应优先订阅 `jump_hop_gallery_card_view` åŽåœ¨ `api-server` 本地 cache 构造列表å“应,ä¸è¦è®©æ¯ä¸ª HTTP 请求都调用 SpacetimeDB procedure 组装全é‡åˆ—表。 - -## 10. 结果页能力 - -结果页必须展示: - -1. ä½œå“æ ‡é¢˜ï¼› -2. 作å“简介; -3. 角色形象; -4. 地å—图集; -5. 路径预览; -6. 标签; -7. 试玩; -8. å‘布; -9. 返回编辑。 - -结果页还必须支æŒï¼š - -1. å•独é‡ç”Ÿæˆè§’色; -2. å•独é‡ç”Ÿæˆåœ°å—图集; -3. å•独修改标题和简介; -4. å•独调整标签和难度。 - -结果页ä¸åº”强制å†èµ°ä¸€æ¬¡å°é¢ç”Ÿå›¾ã€‚å°é¢åªåšåˆæˆï¼Œä¸æ–°å¢žå›¾åƒç”Ÿæˆè°ƒç”¨ã€‚ - -## 11. è¿è¡Œæ€è§„则 - -è¿è¡Œæ€é‡‡ç”¨ 2D 表现,但画é¢è§†è§‰ä¸Šå¿…é¡»ä¿ç•™å‚考图那ç§ä¿¯è§†è§’ / ç­‰è·æ„Ÿã€‚ - -### 11.1 核心玩法 - -1. 玩家长按蓄力; -2. æ¾æ‰‹åŽè§’色按蓄力长度起跳; -3. 跳跃è·ç¦»å†³å®šæ˜¯å¦è½åˆ°ä¸‹ä¸€ä¸ªåœ°å—ï¼› -4. è½åœ¨ç›®æ ‡åŒºåŸŸå†…判定æˆåŠŸï¼› -5. è½åœ¨åœ°å—外或越界判定失败; -6. 到达终点地å—判定通关。 - -### 11.2 判定规则 - -1. åªåšä¸€ä¸ªå½“å‰å±€é¢çš„起跳判定; -2. ä¸åšå¤æ‚连招动作树; -3. 䏿–°å¢žç”Ÿå‘½æ•°ã€ä½“力ã€å›žåˆæ•°ï¼› -4. 䏿–°å¢žè®¡æ—¶èµ›ä½œä¸ºé¦–版核心规则; -5. ä¸æŠŠå‰ç«¯åŠ¨ç”»ç»“æžœå½“æˆæœ€ç»ˆçœŸç›¸ï¼Œé€šå…³ä¸Žå¤±è´¥å¿…须能回写è¿è¡Œæ€çжæ€ã€‚ - -### 11.3 角色动画 - -角色ä¸éœ€è¦å¤šå¸§ç”Ÿå›¾ï¼Œè¿è¡Œæ€åªé€šè¿‡ä½ç§»ã€ç¼©æ”¾ã€è½»å¾®æ—‹è½¬å’ŒæŠ•å½±å˜åŒ–表达: - -1. 蓄力时轻微压缩; -2. 起跳时å‘上抬å‡ï¼› -3. ç©ºä¸­ä¿æŒå¯è¯»è½®å»“ï¼› -4. è½åœ°æ—¶è½»å¾®å¼¹æ€§å›žå¼¹ï¼› -5. 失败时从地å—边缘跌è½ã€‚ - -### 11.4 æ‘„åƒæœºä¸Žæž„图 - -1. 相机以当å‰è§’色和下一地å—为中心; -2. 至少ä¿è¯ä¸‹ä¸€ä¸ªè½ç‚¹ä¸€ç›´å¯è§ï¼› -3. ç”»é¢è¦ç•™å‡ºé¡¶éƒ¨å’Œåº•部的 UI 安全区; -4. ä¸è¦æŠŠåœ°å—åšå¾—太满,ä¿ç•™å‚考图那ç§ç–朗感。 - -### 11.5 UI - -è¿è¡Œæ€ UI åªä¿ç•™å¿…è¦å…ƒç´ ï¼š - -1. 分数; -2. æš‚åœï¼› -3. 釿–°å¼€å§‹ï¼› -4. 分享; -5. 结算按钮。 - -ä¸é»˜è®¤å±•示大段规则说明。首进如果需è¦å¼•导,åªèƒ½ç”¨ä¸€æ¬¡è½»é‡æç¤ºï¼Œä¸å…许常驻一å±çš„说明文案。 - -## 12. 视觉规范 - -本模æ¿çš„è§†è§‰ç›®æ ‡æ˜¯â€œåƒ 3Dï¼Œä½†ä»æ˜¯ 2D 图片â€ã€‚ - -å¿…é¡»éµå®ˆï¼š - -1. 平尿œ‰æ˜Žç¡®åŽšåº¦ï¼› -2. ä¾§é¢å¯è§åˆ†å±‚或æè´¨å˜åŒ–ï¼› -3. 投影统一且方å‘一致; -4. 背景干净,颜色克制; -5. 角色尺寸在å°å±ä¸Šä¾ç„¶å¯è¯»ï¼› -6. 地å—ä¸èƒ½å‡ºçŽ°è¿‡å¤šæ–‡å­—ã€æŒ‰é’®æˆ–装饰信æ¯ï¼› -7. ä¸èƒ½æŠŠè¿è¡Œæ€åšæˆé‡ UI 颿¿ã€‚ - -建议的背景策略: - -1. 以陿€æµ…色æ¸å˜æˆ–纯色背景为主; -2. ä¸æŠŠèƒŒæ™¯ä¹Ÿåšæˆæ¯æ¬¡éƒ½ç”Ÿæˆçš„é‡èµ„产; -3. 让地å—和角色æˆä¸ºç”»é¢çš„第一视觉焦点。 - -## 13. å‘布åŽä½“验 - -å‘布åŽçš„作å“必须支æŒï¼š - -1. è¿›å…¥ä½œå“æž¶å’Œå…¬å¼€å±•示; -2. 分享; -3. 试玩; -4. 釿–°è¿›å…¥ç»“果页编辑。 - -å‘布åŽçš„å¡ç‰‡å°é¢åº”优先由角色图和地å—å›¾åˆæˆï¼Œä¸è¦æ±‚å•独å†ç”Ÿæˆå°é¢å›¾ã€‚ - -é¦–ç‰ˆä¸æ–°å¢žæŽ’行榜ã€å›žæ”¾å’Œå¯¹å±€å¯¹æŠ—。åŽç»­å¦‚è¦æ‰©å±•排行,å¯å¦èµ·ç‰ˆæœ¬ï¼Œä¸è¦å¡žè¿›é¦–版模æ¿èŒƒå›´ã€‚ - -## 14. 验收 - -1. 创作入å£èƒ½çœ‹åˆ° `跳一跳` 模æ¿ï¼› -2. 创作者å¯ä»¥å¡«å†™ä¸»é¢˜ã€è§’色æè¿°ã€é£Žæ ¼å’Œéš¾åº¦ï¼› -3. æäº¤åŽåªç”Ÿæˆä¸€æ¬¡è§’色图和一次地å—图集; -4. 结果页能看到角色图ã€åœ°å—图集和路径预览; -5. 结果页å¯å•独é‡ç”Ÿæˆè§’色或地å—ï¼› -6. 试玩进入跳一跳è¿è¡Œæ€ï¼› -7. é•¿æŒ‰è“„åŠ›ã€æ¾æ‰‹èµ·è·³ã€è½ç‚¹åˆ¤å®šã€å¤±è´¥å’Œé€šå…³éƒ½å¯ç”¨ï¼› -8. 作å“å¯ä»¥ä¿å­˜ã€å‘布和分享; -9. å‰ç«¯ä¸ç›´æŽ¥è¯»å–或暴露生图密钥; -10. å‘布åŽçš„å°é¢ä¸ä¾èµ–第三次é¢å¤–生图。 -11. `npm run check:spacetime-schema` 在 schema å˜æ›´åŽé€šè¿‡ï¼› -12. `npm run check:encoding` 通过。 +展示字段: + +1. rankï¼› +2. playerIdï¼› +3. successfulJumpCountï¼› +4. durationMsï¼› +5. updatedAt。 + +è‰ç¨¿è¯•玩å¯ä»¥å±•ç¤ºæœ¬åœ°ç»“æžœï¼Œä½†æ­£å¼æŽ’è¡Œæ¦œåªæ¶ˆè´¹åŽç«¯ run 记录。匿å runtime guest 也按 guest subject 作为 playerId å‚与当次作å“维度排行。 + +## 8. 结果页 + +结果页展示: + +1. 陶泥儿 logo 逿˜Žè§’色预览; +2. 25 个地å—èµ„æºæ± é¢„览; +3. é¦–å± 3 å—å¹³å°é¢„览; +4. 试玩; +5. å‘布; +6. 返回编辑; +7. é‡ç”Ÿæˆåœ°å—。 + +结果页ä¸å†å±•ç¤ºè§’è‰²å›¾ç‰‡ç”Ÿæˆæ§½ä½ï¼Œä¹Ÿä¸æä¾›ç‹¬ç«‹è§’色é‡ç”Ÿæˆã€‚ + +## 9. 契约è¦ç‚¹ + +公开语义ä¿ç•™ï¼š + +1. `themeText`ï¼› +2. `tileAtlasAsset`ï¼› +3. `tileAssets[]`ï¼› +4. `defaultCharacter`ï¼› +5. `path.platforms[]` 作为æœåŠ¡ç«¯è·¯å¾„ç¼“å†²ï¼› +6. `currentPlatformIndex`ï¼› +7. `successfulJumpCount`ï¼› +8. `startedAtMs` / `finishedAtMs` / `durationMs`ï¼› +9. `leaderboard`。 + +旧语义处ç†ï¼š + +1. `characterAsset` 仅作为角色æè¿°å…¼å®¹å­—段,ä¸å†è¡¨ç¤ºç”Ÿæˆå›¾ç‰‡ï¼›å‰ç«¯å›ºå®šä½¿ç”¨é™¶æ³¥å„¿ logo 逿˜Ž PNGï¼› +2. `score` 兼容映射为æˆåŠŸè·³è·ƒæ¬¡æ•°ï¼› +3. `combo` 固定为 0,ä¸ä½œä¸ºå…¬å¼€çŽ©æ³•è¯­ä¹‰ï¼› +4. `cleared` 状æ€ä¸å†ç”± v1 产生; +5. æ—§ finite path åªä½œä¸ºæœåŠ¡ç«¯è·¯å¾„ç¼“å†²å…¼å®¹å½¢æ€ã€‚ + +## 10. 验收 + +1. åˆ›ä½œé¡µåªæ˜¾ç¤ºä¸»é¢˜è¾“入; +2. 生æˆé“¾è·¯åªè°ƒç”¨ä¸€æ¬¡åœ°å—图集 image2,ä¸å†è°ƒç”¨è§’色生图; +3. 地å—图集为 `5x5`,åŽç«¯åˆ‡å‡º 25 ä¸ªåœ°å— PNGï¼› +4. 结果页ä¸ä¾èµ–旧角色图片槽; +5. è¿è¡Œæ€ä¸ºç«–å±ä¿¯è§†è§’,首å±ä¿æŒ 3 个地å—å¯è§ï¼› +6. 拖拽方å‘和力度会影å“è½ç‚¹ï¼› +7. 未è½åˆ°ä¸‹ä¸€ä¸ªåœ°å—ç«‹å³å¤±è´¥ï¼› +8. æˆåŠŸè·³è·ƒæ¬¡æ•°ç´¯åŠ ï¼Œå¤±è´¥åŽè®¡æ—¶å†»ç»“ï¼› +9. 排行榜按æˆåŠŸè·³è·ƒæ¬¡æ•°ä¼˜å…ˆæŽ’åºï¼› +10. 作å“å¯ä¿å­˜ã€å‘布ã€åˆ†äº«å¹¶ä»Žå…¬å¼€å…¥å£å¯åŠ¨ã€‚ +11. è¿è¡Œæ€åœ°å—必须显示 `tileAssets[]` 中的生æˆåˆ‡ç‰‡å›¾ç‰‡ï¼›æ‹–拽蓄力ã€è®¡æ—¶åˆ·æ–°å’Œè§’色ä½ç½®æ›´æ–°ä¸å¾—销æ¯é‡å»ºé€æ˜Žç”»å¸ƒã€å¹³å°å›¾ç‰‡å±‚或 DOM 角色层。 +12. åŒç­‰è·³è·ƒè·ç¦»çš„æ‹–动è·ç¦»å¿…须比旧 `0.004` 系数缩短一åŠï¼Œæ¾æ‰‹åŽå¿…须先看到角色飞行动画,å†çœ‹åˆ°åœ°å—窗å£å‰ç§»ã€‚ diff --git a/docs/ã€åŽç«¯æž¶æž„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md b/docs/ã€åŽç«¯æž¶æž„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md index edf312bd..5a4a9e27 100644 --- a/docs/ã€åŽç«¯æž¶æž„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md +++ b/docs/ã€åŽç«¯æž¶æž„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md @@ -404,6 +404,12 @@ npm run check:server-rs-ddd - Rust 结构体:`JumpHopEventRow` - æºç ï¼š`server-rs/crates/spacetime-module/src/jump_hop/tables.rs` +### `jump_hop_leaderboard_entry` + +- Rust 结构体:`JumpHopLeaderboardEntryRow` +- æºç ï¼š`server-rs/crates/spacetime-module/src/jump_hop/tables.rs` +- 说明:跳一跳作å“维度排行榜 read model,æ¯ä¸ª `profile_id + player_id` åªä¿ç•™ 1 æ¡æœ€ä½³è®°å½•;排åºå£å¾„为æˆåŠŸè·³è·ƒæ¬¡æ•°é™åºã€æ¸¸æˆæ—¶é•¿å‡åºã€æ›´æ–°æ—¶é—´å‡åºï¼Œè‰ç¨¿è¯•玩ä¸ä½œä¸ºå…¬å¼€æŽ’行榜语义。 + ### `jump_hop_runtime_run` - Rust 结构体:`JumpHopRuntimeRunRow` diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index d5207f03..e4981107 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -122,23 +122,31 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ— 对外å称:`跳一跳`。工程域:`jump-hop`。PRD è§ `docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘è·³ä¸€è·³ä¿¯è§†è§’çŽ©æ³•æ¨¡æ¿PRD-2026-05-19.md`。 -首版定ä½ä¸ºä¿¯è§†è§’ / ç­‰è·è§†è§’ 2D 休闲跳跃模æ¿ï¼Œé“¾è·¯å¯¹é½æ‹¼å›¾çš„创作闭环: +当å‰å®šä½ä¸ºç«–å±ä¿¯è§†è§’ 2D å¹³å°è·³è·ƒæ¨¡æ¿ï¼Œé“¾è·¯å¯¹é½å¹³å°åˆ›ä½œé—­çŽ¯ï¼š ```text -åˆ›ä½œå…¥å£ -> 模æ¿è¾“å…¥ -> 生æˆè¿‡ç¨‹é¡µ -> 结果页 -> 试玩 -> å‘布 -> è¿è¡Œæ€ +åˆ›ä½œå…¥å£ -> 主题输入 -> 生æˆè¿‡ç¨‹é¡µ -> 结果页 -> 试玩 -> å‘布 -> è¿è¡Œæ€ ``` +创作入å£é…置事实æºä»æ˜¯ SpacetimeDB `creation_entry_type_config`:默认 `visible=true`ã€`open=true`ã€`badge=å¯åˆ›å»º`ã€`subtitle=主题驱动平å°è·³è·ƒ`ã€`image_src=/creation-type-references/jump-hop.webp`。旧库中ä»åœç•™åœ¨ `subtitle=俯视角跳跃闯关` 且 `image_src=/creation-type-references/puzzle.webp` 的系统默认行会在入å£é…ç½®æ’­ç§æµç¨‹ä¸­è‡ªåЍè¿ç§»ï¼›åŒæ—¶ `spacetime-client` 的入å£é…置读模型也会对åŒä¸€æ¡æ—§ç³»ç»Ÿé»˜è®¤è¡Œåšçº å,é¿å…订阅缓存长期回放è€å£å¾„。åŽå°æ‰‹åŠ¨æ”¹è¿‡çš„è·³ä¸€è·³å…¥å£é…ç½®ä¸è¢«è¦†ç›–。 + ç´ æç”Ÿæˆè§„则固定为: -1. åˆå§‹è‰ç¨¿ç”Ÿæˆæ—¶ï¼Œè§’色形象å•独调用一次生图; -2. åˆå§‹è‰ç¨¿ç”Ÿæˆæ—¶ï¼Œåœ°å—å•独调用一次生图,输出 3D 视图的 2D 图片图集; -3. 跳一跳地å—图集使用专用 `2行*3列` 六格布局,åŽç«¯æŒ‰ `start / normal / target / finish / bonus / accent` 顺åºåˆ‡åˆ†ä¸ºé€æ˜Ž PNGï¼› -4. å°é¢å’Œåˆ†äº«å›¾ç”±è§’色图与地å—图轻é‡åˆæˆï¼Œä¸å†é¢å¤–调用第三次生图; -5. 显å¼é‡ç”Ÿæˆè§’è‰²æˆ–åœ°å—æ—¶ï¼Œåªé‡ç”Ÿæˆå¯¹åº”资产槽ä½ã€‚ +1. 创作端åªä¿ç•™ä¸»é¢˜è¾“å…¥ï¼Œä½œå“æ ‡é¢˜ã€ç®€ä»‹ã€æ ‡ç­¾å’Œåœ°å—æç¤ºè¯ç”±ç³»ç»Ÿæ´¾ç”Ÿï¼› +2. v1 ä¸å†å•独生æˆè§’色图片,è¿è¡Œæ€å›ºå®šä½¿ç”¨æŠ é™¤ç™½åº•åŽçš„陶泥儿 logo 逿˜Ž PNG 作为玩家角色; +3. 地å—åªè°ƒç”¨ä¸€æ¬¡ image2,输出一张 `5行*5列`ã€`1:1`ã€çº¯ç»¿è‰²ç»¿å¹•背景的主题地å—图集; +4. åŽç«¯æŒ‰ä»Žä¸Šåˆ°ä¸‹ã€ä»Žå·¦åˆ°å³å‡åŒ€åˆ‡åˆ†ä¸º `tile-01` 到 `tile-25` çš„é€æ˜Ž PNG,æ¯ä¸ªåˆ‡ç‰‡å¿…须使用唯一 slot/path æŒä¹…化,ä¸èƒ½æŒ‰é‡å¤çš„ `tileType` å¤ç”¨æ§½ä½ï¼› +5. 结果页åªå±•示陶泥儿 logo 逿˜Žè§’色预览ã€åœ°å—æ± é¢„è§ˆå’Œé¦–å± 3 地å—预览;ä¸å†æä¾›æ—§è§’è‰²å›¾ç”Ÿæˆæ§½ã€‚ -è¿è¡Œæ€è§„则真相必须沉到 `module-jump-hop`,å‰ç«¯åªåšè“„力表现ã€è§’色ä½ç§»ã€æŠ•影和è½åœ°å馈。通关ã€å¤±è´¥ã€åˆ†æ•°ã€comboã€è¿è¡Œæ€å¿«ç…§å’Œå‘布作å“状æ€ä»¥åŽç«¯ä¸ºå‡†ã€‚公开列表应走 `jump_hop_gallery_card_view` 订阅缓存,ä¸è¦æ¯æ¬¡ HTTP 请求调用 procedure 组装全é‡åˆ—表。 +è¿è¡Œæ€è§„则真相必须沉到 `module-jump-hop`,å‰ç«¯åªåšæ‹–æ‹½è“„åŠ›ã€è§’色ä½ç§»ã€æŠ•影和è½åœ°åé¦ˆã€‚å¤±è´¥ã€æˆåŠŸè·³è·ƒæ¬¡æ•°ã€æ¸¸æˆæ—¶é•¿å†»ç»“ã€è¿è¡Œæ€å¿«ç…§å’Œå‘布作å“状æ€ä»¥åŽç«¯ä¸ºå‡†ã€‚v1 ä¸ä¿ç•™å…¬å¼€ combo / perfect / 通关语义,旧 `score` 兼容映射为æˆåŠŸè·³è·ƒæ¬¡æ•°ã€‚å…¬å¼€åˆ—è¡¨åº”èµ° `jump_hop_gallery_card_view` 订阅缓存,ä¸è¦æ¯æ¬¡ HTTP 请求调用 procedure 组装全é‡åˆ—表。 -å¹³å°é¦–页推èã€ç²¾é€‰ã€æœ€æ–°ã€å…¬å¼€è¯¦æƒ…ã€æœç´¢ã€å·²çŽ©ä½œå“和公开试玩统一按 `sourceType='jump-hop'` 与 `JH-*` 公开作å“å·è¯†åˆ«è·³ä¸€è·³ä½œå“ï¼›ä»Žå…¬å¼€è¯¦æƒ…æˆ–æŽ¨èæµå¯åЍè¿è¡Œæ€æ—¶ï¼Œè‹¥å¡ç‰‡æ‘˜è¦ä¸è¶³ä»¥æºå¸¦è§’色图ã€åœ°å—图集和路径é…置,必须先补读完整 work profile å†ä¼ å…¥è¿è¡Œæ€ã€‚å¹³å°å£³å±‚å¿…é¡»åŒæ­¥æ³¨å†Œ `jump-hop-workspace`ã€`jump-hop-generating`ã€`jump-hop-result`ã€`jump-hop-runtime`ã€`jump-hop-gallery-detail` 阶段,并在 `appPageRoutes.ts` 映射 `/creation/jump-hop/workspace`ã€`/creation/jump-hop/generating`ã€`/creation/jump-hop/result`ã€`/gallery/jump-hop/detail`ã€`/runtime/jump-hop`ï¼ŒåŒæ—¶æŒæœ‰ sessionã€workã€runã€galleryã€busy/error 与生æˆè¿›åº¦çжæ€ï¼Œé¿å…åªåˆå…¥æ¸²æŸ“åˆ†æ”¯ä½†é—æ¼çŠ¶æ€æºæˆ–分享路径导致 typecheck 失败ã€åˆ·æ–°å›žé¦–页。 +æ¯å±åªå±•示 3 个地å—:当å‰åœ°å—ã€ç›®æ ‡åœ°å—和下一预览地å—ã€‚å¹³å°æµæŒ‰åŒä¸€ seed æ— é™ç”Ÿæˆï¼Œå‰ç«¯ä¸å¾—è‡ªè¡Œç”Ÿæˆæ­£å¼è·¯å¾„。排行榜按作å“维度展示玩家 IDã€æˆåŠŸè·³è·ƒæ¬¡æ•°å’Œæ¸¸æˆæ—¶é•¿ï¼›æ¯ä½çީ家åªä¿ç•™ 1 æ¡æœ€ä½³è®°å½•,排åºå›ºå®šä¸º `æˆåŠŸè·³è·ƒæ¬¡æ•° desc -> æ¸¸æˆæ—¶é•¿ asc -> æ›´æ–°æ—¶é—´ asc`。 + +è¿è¡Œæ€æ¸²æŸ“分层固定为:DOM å¹³å°å±‚直接使用 `tileAssets[]` 的生æˆåˆ‡ç‰‡å›¾ç‰‡æ˜¾ç¤ºåœ°å—,图片读å–ç»§ç»­èµ°å¹³å°èµ„产æ¢ç­¾ï¼Œå¹¶ä»¥ `assetObjectId` 作为刷新键é¿å…é‡ç”ŸæˆåŽæ²¿ç”¨æ—§ç­¾å或旧图片缓存;DOM 角色层固定使用 `public/branding/jump-hop-taonier-character.png` 陶泥儿 logo 逿˜Ž PNG å¹¶ä¿æŒæœ€é«˜å±‚级;Three.js 逿˜Žç”»å¸ƒä»…作为åŽç»­æ‰©å±•层。拖拽蓄力ã€è®¡æ—¶åˆ·æ–°å’Œè§’色ä½ç½®å˜åŒ–åªèƒ½æ›´æ–° refs 或 DOM 状æ€ï¼Œä¸å¾—销æ¯é‡å»ºé€æ˜Žç”»å¸ƒæˆ–å¹³å°å›¾ç‰‡å±‚,å¦åˆ™ä¼šé€ æˆåœ°å—和角色层频闪。 + +è·³ä¸€è·³å½“å‰æ‹–拽手感统一采用 `chargeToDistanceRatio=0.008`,用于把åŒç­‰è·³è·ƒè·ç¦»æ‰€éœ€æ‹–拽è·ç¦»ç¼©çŸ­åˆ°æ—§ `0.004` 的一åŠï¼›å¦‚果历å²è·¯å¾„ä»ä¿å­˜æ—§ç³»æ•°ï¼Œ`start_run` ä¼šåœ¨å¼€å±€å½’ä¸€åŒ–åˆ°æ–°ç³»æ•°ã€‚æ¾æ‰‹åŽè¿è¡Œæ€å¿…须立å³ç”Ÿæˆ `visualJump`,用当å‰è§’色ä½ç½®ä½œä¸ºèµ·ç‚¹ã€å‰ç«¯é¢„测è½ç‚¹ä½œä¸ºç»ˆç‚¹ï¼Œæ’­æ”¾çº¦ `560ms` çš„è§’è‰²é£žè¡ŒåŠ¨ç”»ï¼šè“„åŠ›æ—¶è§’è‰²æ²¿æ‹–æ‹½æ–¹å‘æ˜Žæ˜¾æ‹‰é•¿ï¼Œè§’色弹å‘预测è½ç‚¹ï¼Œè½åœ°åŽå‘åæ–¹å‘回弹两次;动画路径ä¸å¾—等待åŽç«¯æ–° run。若åŽç«¯æ–° run 晚于飞行动画返回,角色必须åœåœ¨é¢„测è½ç‚¹ç­‰å¾…,直到新 run 到达åŽå†æŠŠæ˜¾ç¤ºæ€åˆ‡åˆ°åŽç«¯æœ€æ–° run,并用约 `1440ms` 的相机层推进过渡承接新窗å£ã€‚æŽ¨è¿›æ—¶åœ°å— DOM 层和 DOM 角色层统一包在åŒä¸€ä¸ª camera layer 下移动,旧当å‰åœ°å—自然离开视野,新预览地å—ä»Žä¸Šæ–¹éœ²å‡ºï¼Œç¦æ­¢ç”¨ p1/p2 å„自 `top/left` 过渡造æˆè§’色和地å—ä¸åŒæ­¥ã€‚ç›¸æœºå±‚æŽ¨è¿›å¿…é¡»åŒæ—¶ä½¿ç”¨ X/Y å移,从旧目标地å—ä½ç½®æ–œå‘滑到新当å‰åœ°å—èšç„¦ä½ç½®ï¼Œä¸å¾—先横å‘瞬切到居中å†çºµå‘滑动。地å—å…许ä¿ç•™å½“å‰ / 目标 / 预览的深度尺寸差异,但该差异必须通过固定基准宽高上的 `transform: scale(...)` 缓动呈现,并与相机推进使用åŒä¸€ `1440ms` 节å¥ï¼›ä¸è¦ç›´æŽ¥ä¿®æ”¹å®½é«˜é€ æˆçž¬åˆ‡ï¼Œä¹Ÿä¸è¦å†ç»™å½“剿€é¢å¤–å  CSS scale。 + +å¹³å°é¦–页推èã€ç²¾é€‰ã€æœ€æ–°ã€å…¬å¼€è¯¦æƒ…ã€æœç´¢ã€å·²çŽ©ä½œå“和公开试玩统一按 `sourceType='jump-hop'` 与 `JH-*` 公开作å“å·è¯†åˆ«è·³ä¸€è·³ä½œå“ï¼›ä»Žå…¬å¼€è¯¦æƒ…æˆ–æŽ¨èæµå¯åЍè¿è¡Œæ€æ—¶ï¼Œè‹¥å¡ç‰‡æ‘˜è¦ä¸è¶³ä»¥æºå¸¦åœ°å—图集和路径é…置,必须先补读完整 work profile å†ä¼ å…¥è¿è¡Œæ€ã€‚å¹³å°å£³å±‚å¿…é¡»åŒæ­¥æ³¨å†Œ `jump-hop-workspace`ã€`jump-hop-generating`ã€`jump-hop-result`ã€`jump-hop-runtime`ã€`jump-hop-gallery-detail` 阶段,并在 `appPageRoutes.ts` 映射 `/creation/jump-hop/workspace`ã€`/creation/jump-hop/generating`ã€`/creation/jump-hop/result`ã€`/gallery/jump-hop/detail`ã€`/runtime/jump-hop`ï¼ŒåŒæ—¶æŒæœ‰ sessionã€workã€runã€galleryã€busy/error 与生æˆè¿›åº¦çжæ€ï¼Œé¿å…åªåˆå…¥æ¸²æŸ“åˆ†æ”¯ä½†é—æ¼çŠ¶æ€æºæˆ–分享路径导致 typecheck 失败ã€åˆ·æ–°å›žé¦–页。 è·³ä¸€è·³ä½œå“æž¶èµ°åˆ›ä½œä¸­å¿ƒçš„统一作å“列表:å‰ç«¯é€šè¿‡ `/api/creation/jump-hop/works` 拉å–ä½œå“æ‘˜è¦ï¼Œè‰ç¨¿æ€ä¼šä¸Ž pending notice åˆå¹¶åŽæ˜¾ç¤ºåœ¨ä½œå“架里,已å‘布作å“点击åŽä¼šå…ˆæŒ‰ profileId 读å–完整详情å†è¿›å…¥è¯¦æƒ…或è¿è¡Œæ€ã€‚生æˆä¸­ä½œå“ä»ä»¥åŽç«¯æ‘˜è¦é‡Œçš„ `generationStatus` 为准,刷新åŽåº”能æ¢å¤ç­‰å¾…é®ç½©ï¼Œä¸èƒ½åªä¾èµ–内存 notice。 diff --git a/packages/shared/src/contracts/jumpHop.ts b/packages/shared/src/contracts/jumpHop.ts index 19fafe66..2127fd7f 100644 --- a/packages/shared/src/contracts/jumpHop.ts +++ b/packages/shared/src/contracts/jumpHop.ts @@ -24,7 +24,6 @@ export type JumpHopTileType = export type JumpHopActionType = | 'compile-draft' - | 'regenerate-character' | 'regenerate-tiles' | 'update-work-meta' | 'update-difficulty'; @@ -35,19 +34,21 @@ export type JumpHopJumpResult = 'miss' | 'hit' | 'perfect' | 'finish'; export interface JumpHopWorkspaceCreateRequest { templateId: string; - workTitle: string; - workDescription: string; - themeTags: string[]; - difficulty: JumpHopDifficulty; - stylePreset: JumpHopStylePreset; - characterPrompt: string; - tilePrompt: string; + themeText: string; + workTitle?: string; + workDescription?: string; + themeTags?: string[]; + difficulty?: JumpHopDifficulty; + stylePreset?: JumpHopStylePreset; + characterPrompt?: string; + tilePrompt?: string; endMoodPrompt?: string | null; } export interface JumpHopActionRequest { actionType: JumpHopActionType; profileId?: string | null; + themeText?: string | null; workTitle?: string | null; workDescription?: string | null; themeTags?: string[] | null; @@ -73,12 +74,23 @@ export interface JumpHopCharacterAsset { height: number; } +export interface JumpHopDefaultCharacter { + characterId: string; + displayName: string; + modelKind: 'builtin-three'; + bodyColor: string; + accentColor: string; +} + export interface JumpHopTileAsset { tileType: JumpHopTileType; + tileId?: string; imageSrc: string; imageObjectKey: string; assetObjectId: string; sourceAtlasCell: string; + atlasRow?: number; + atlasCol?: number; visualWidth: number; visualHeight: number; topSurfaceRadius: number; @@ -126,11 +138,13 @@ export interface JumpHopDraftResponse { templateId: string; templateName: string; profileId: string | null; + themeText: string; workTitle: string; workDescription: string; themeTags: string[]; difficulty: JumpHopDifficulty; stylePreset: JumpHopStylePreset; + defaultCharacter?: JumpHopDefaultCharacter | null; characterPrompt: string; tilePrompt: string; endMoodPrompt: string | null; @@ -167,6 +181,7 @@ export interface JumpHopWorkSummaryResponse { profileId: string; ownerUserId: string; sourceSessionId: string | null; + themeText: string; workTitle: string; workDescription: string; themeTags: string[]; @@ -185,6 +200,7 @@ export interface JumpHopWorkProfileResponse { summary: JumpHopWorkSummaryResponse; draft: JumpHopDraftResponse; path: JumpHopPath; + defaultCharacter?: JumpHopDefaultCharacter | null; characterAsset: JumpHopCharacterAsset; tileAtlasAsset: JumpHopCharacterAsset; tileAssets: JumpHopTileAsset[]; @@ -208,6 +224,7 @@ export interface JumpHopGalleryCardResponse { profileId: string; ownerUserId: string; authorDisplayName: string; + themeText: string; workTitle: string; workDescription: string; coverImageSrc: string | null; @@ -237,6 +254,8 @@ export interface JumpHopRuntimeRunSnapshotResponse { ownerUserId: string; status: JumpHopRunStatus; currentPlatformIndex: number; + successfulJumpCount: number; + durationMs: number; score: number; combo: number; path: JumpHopPath; @@ -251,10 +270,13 @@ export interface JumpHopRunResponse { export interface JumpHopStartRunRequest { profileId: string; + runtimeMode?: 'draft' | 'published'; } export interface JumpHopJumpRequest { - chargeMs: number; + dragDistance: number; + dragVectorX?: number; + dragVectorY?: number; clientEventId: string; } @@ -265,3 +287,17 @@ export interface JumpHopRestartRunRequest { export interface JumpHopJumpResponse { run: JumpHopRuntimeRunSnapshotResponse; } + +export interface JumpHopLeaderboardEntry { + rank: number; + playerId: string; + successfulJumpCount: number; + durationMs: number; + updatedAt: string; +} + +export interface JumpHopLeaderboardResponse { + profileId: string; + items: JumpHopLeaderboardEntry[]; + viewerBest?: JumpHopLeaderboardEntry | null; +} diff --git a/public/branding/jump-hop-taonier-character.png b/public/branding/jump-hop-taonier-character.png new file mode 100644 index 0000000000000000000000000000000000000000..0dcbaf411ced0578576d644ec75a5c114b653b3b GIT binary patch literal 177512 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

hB>DhG2*!?tO!Sk2O-krohdk|E zw=yu>w}35Oekxay^^7DZX?mKNsaXL$h~CD23!w(%faS)jL`FB5utkV`hPcJv39T09 zS#S(xUdW@MuJv@G;5*c=$i3>NUc*%+LKg%}@<0K*0&mi~TcRJ4SAyrqUqaAr%WNPR z*53nHBYxfGz*<3f7p8%SB84rQgsrAW0{9x{%Mi{0v*jv_Osl}*xDzjqvf$h>0t%!C zi>My%c~njG^XJ<~g}D%bKnxf%Gypc?KBmS6)e9U(S(bUM^Zx!`SycbO)a&5VM6?1s zFZ7_A0a+v}tO|9G5DjB^hoCnSV3lVntO+L+q>2gL);*XMA^5gmVQN{SZb79k2E|N{D)0@5u4rx1UKH z;##~1lC>8V5wM&H6izeq#5r9Gp~V@W8$;1O(V{RbR*RlsN+(#<;7H+W=o{9?U(7=r znr~{lz)a-xjOl_2Lm<)ey|I?fdPRR2j>SlZL=`8;hyli2+Z9p<7~`?Evh=2D4a&zP z$F+Hiu{#J=P&b|a=0+A7Yb3yD~Z5Cb$Bmbfx7 z=Xosz=@5u<_IO`JSs=fIq4PAmCaO55+1-Uf3H{XFD|IpmXw)^badvUGL%@u=N9_=w z>tv)ZNQA~7*Lv5yys!%xX?}rtkWtYJCy=J9Oy}PrT&zoo-`e$16yQDLFLng=NZN%~Lg>JWH7OIf!JK%r)E! z(zw4${o@%|PK_uoHjSZDq`(Gbv67HCyB2GZap9qrY6;>7X91rzD#a!EFkLs*^ca?j zct%<)q(WS%i|W^9(HawmuvYEFxdsDpXJ7^_);~xKOPjim==BJ=oM{}6?-W$Zeq!+4 zQ;2hpITMV4xvmtKKr?S_tY2io`7y=VF=8$WF{Q+x(SUC81Gfz>T7U{2P@`r!|KXbf6%Gjnwqrt63 z-=Yd51ic!{SNB%<1pHi~k1^lK<$*{9?KCVUv9`1+Nuev0YKEI4X%i+?<_2NCk?z^~ zv(pETJp`)Vy?qxB)O4=hvvcUUehNjfD8!h3&sFP8XF|ZzK%Y5@I)AY=O~@XB#ya)1 zrpDX^ZKB05CH~lhD9Q%G%g1WP|H7m(iNiZNa^!p|<&wYsOlvxMk+(x+0r0kQ?1r7m z#^x3PQ_fpmN<~IB+KO3pLYR$gdYI=x7gKMS*l;p@0i#Kn63q5?AD$t8Px~&a62dY8 zO;63>h`~h#Gnn`(ajQ~I_XY};px5Dp3@(57gf zjrj`XacVCTNgx&)5Gd{<2pt+d7T*aEkW#t;poXSNt4wAnq^UjBNkgzx_)04lEFaiM zM0sHEfuW<*k$@)suRhbrMZmQJfRYocN!F64fmplNI) zJ*WX>(F-300TC7KV5(shNJ5A*Gq~h*=bXSy8_96^KA_(Xuo+e!m-?ucB2HSv{#;>$ zbtA?pYA8rweO3e@2}9IflBP|U4Y=|t_>d9eMM2<*PSc1J%!`DQQUmlqs&~7g1DbhO zA8hulFvQ$<0u+txf%TC1dMMfEl!z?>a~(neiv>jvyC &ESaXCdg9GpuU6ujjWlj zz508T&_{HO2xr_a&c{*^1~``Q>%=+`3`?1eGCow)jPJb+ve-Vt_YW=Ee{)m0zW~+< z!4*jCu=%ycwA}@`E;*bk42nOFv!p@|%72ISi!%>D|K8qCmd{Qykn$bQ`qDv+Ca}}} z8T%ik0U1ua4BKP)AZbuOH}?@Nld{iwfmjK+A96oJG|1^_sF2X;Oq9@d)6z{hss-S3 zIfU&n2>{p)m%7>P{4~;Bl1RoOcK};%5t!GEj#0Z(UuK(%5?Tcj$}rnuPs6`V4nRs| zEyH{AJ0l`$DmH2vzEQg=1$L>i;nFQa2$=*Z3YP@h-u-mJ!U8ad_*}R<5|tEkKY)%Y zo}`&O^i0eL=}@>75NGdC} zX3p)Ur|Cvc3bQm;%gr**w4>xb7evJmIFrUX^|=X`jL8fg^G1L!QMiL}3>g|{Gww@} zrSDk_yA15YqVLPj)_$3EQ(En+Z{N>3w28EuruLlRU|Ku{`^XSD=$}Vmxho~JM0U+C@Ze#PDnX=br`88FOF2qzCC?1+fL1sF&O zHFQ(4Vo(W!Sv05@y3TBe{^lSS-VX)SK&uP*MEL$Ce>n&`G;A@S+jt?g(25{)Ek{tl zr>S^wN=?HJ-#g5#=u@iSQJPmYdl3D`RI9QMZfz_zsV&n25rQ6LEj>2qrh6gXAP|n_zf3mT?4~Q6N4bMGW^KKRLKh02^?a%kQJb?8O zwIY4LDUjhb;jOLI!{E233SgR(eSd|If0OuQU9-#bGdp)QQD%mss{@JlX32Nd@< zmJX9k84#Lv&;5&bSOd7Qh|<@jJqln=Aq{w(JH zq*YLMD67)#!6HzluKBcVo;+2}|NUgO#OI5?9Yz6Qt2=W1+@hlJKy@oKH23 z8bAB1Sq)kBFhhq(d_;>+#uwx?z-SClLVDHs()$$A7xH)5M1qi|Biz?$DD80BQ$rUf z2`yd;Uv)!ZKC&ug?lUfutRX=B5U4UYeTF^)*a&w`UWWy5`>s{=Jmo5P1iO%cz;qW# z=X|BfSR@P$)(crH2q>timjXwg9SCMjz0K&3IP=^y8Bw8Jz?FiWQ|vWpr#J)9XjMew z29EP!`vu5Jvp_Cgn>S6=J=Jg#wtJ%@E@8P#%#iLC7v^ErNtXthJ>4v6jyHC0fX+$T z-QOvTex3rL3QYk>beCFKZjk<*g0O1YDJ&{IqDw94F5RlS1+cZm&BC*Yg$aR3Wi;Y& z_7r`lC19`rM+M`SuRewC@u? z5Rk}G&NTg|FpCZtO!)Y4bo_eBay4)n*O)v^%RE;1p$iWRhE7Erg7on8J2~Djv34wws8x0WE5JkO86P_}0pIoLf z4kC)@B37P)T0|w}W)Nj7G#AKJTsQHjaF0-;6VXk4mPV-QyT=tR4gj2szSI0i!#Y&+ z$=#KVw*^z?#E=xK`_z6)tCGr-=F$jMKbS@aQ?;$pdNm|dK(&4b(kcd_A(h-@lw1q3 z)-((lzsJj)w862bRkLTYB0e`K5~k?X;*(u#I_#oC1%!b3C*6VZn`z`U*Q*sT_950b z=!3czA=cJK@i4_GTB~J+1ElJ}OYAAJ3}9@M=fb{?f2M)Lr*{!Y=Y`@rSA%CINt(^Vn^7Keep60D2}vb;Ygv%|)d(-*adW z&`gD%2smumgbHaTz0jHg7P3o-HWvxlK+r)5mjl2YP3n@EhgsILHz0g0foq*ch*k;) z4$yO)0Q!Tz*}oCxKyH*{R@R-mUTRD^aADvKC4-2RVU{Z)fHvS@T(}klF2>|wcQ>OF zOev~D1D?y;%AD&)ix9%03!CB#{MW%0bUuQ4sm2LJG6lrOY?0#7fU6EvyS9%mq`{h? z=GFv?InjoIBIaxrIIyImoi^{Sel1W>6;5N|5j@JVCn&m3E@}d!^CD6W=y#w3@*Ewoika^ay zO7P7P(mX&To}UsJ1#}lspuOffOeU%T(iDtPj?D6Tz%9UX#X&zNeVOmi%ii9exdQw= zf!-~de%juL04eT>2=x^~NewCr-b`$iN~~c;kV=LW<_We8p_O||_Zs%&@Z_}7393+G zY_{r?QYN#_OGoa0@MHC=GiL@bI0Wrb2>{qYM_zL8hwH+d5f0hX;!x1UkIx!s8d^F{ zvXsd4fAOM}XyL~PK_<{H4-EiYEC%BFGPCmF;62t8WDjG>FbT^_Ej)}1qM^p%@{s%8 zHN_|nU`65PAy3dABxt2^Ffj|SV7gdWWbkg0zM(y3?PVH3T0sTj89D!H0YLiH(4f#? zr-%-61ELw`U;7-ooe=U+!kT_^C4q?MbVPPDbq{e)MDS}g%A5nY2&aXHH(-8X^F}9O zm54g(WIS9>jLj`n5Cq%Oxp|WcGwd4}5UfquTf$|zAK2yGn2Hney(HtXG5g{w(B&XLdwlOJ-95sf3VlNVo-_4JB= z@kfu)T171SYvqnzpdn5ouI@IET)g#;umBvwcDMw9Xh-k3`-$ns#sxn+VJ5@5PFmy# z4dJjFPNeFnDsV$oswjB$&Vgyh=7n^P`_g2UnsAyJp=APeKq9+3Och)lW0Bp}{8d84T=By|{jN;S=#RYC%PGGTo%v+}dQ$AlJ* z!LLBQSeIe^HV9r?M6&ifrzhxW6Scu>bDAQpxr`&N8CX*Vk~R?{+)pqLV7)Yi5c?_B zT_W#Algd!Q+_TsPcnJlyaE}Nt%Z*3DESYigL_BM-P;0stj|I58e>;Yt+&KxFhqWcqgPRIZhc$mk!bDWH~~v_J&rNi0I1dRtr*Yj9YI zScq_lJOh)i%#NL^IRI)H;Gt}XQUKU2<+<+IiSrPb;~X}oZzMV)-yN8h(q$Q%s9!W7 zuL-_s%wZyE$P;-d!jYhP(e$WHAhc{A1bL2K6$b&uPjFn}^9LLDTcSLx_$Y(irN$pj z9zJLfr!zU9f&xMU=rX{wa5$r5CNNn1amJOak&fYIm|r2v5GA0Ed1kLz!=;0$RAb{) z#qEKoqbSgD`I@X`aj(=qYRc)t7n`D3R+6*NA!ItS6~AUs*A&B~;~E{pzC|*iU=GW5 zt_(gYg)G5D>WYVUihCz3LTB##WV(8PGeL@@iL}hQzk+C^KO7kD?-R%vk8>e7I}JwJ z2)Z&i(hUI!rofVCB+GO2K5F!7nX$bd79ox4clSajr@>#|7iihZoS>lBYJZGp59WpW z=eioMCcvWro5I@2@k6+FL4m>@K2un!%)Sd>JHlPl8iw;(*Q}QQUF_|Z$<(v;G5mN>dcKAmQSgK>T8ms81Jv|Te^VHGXv#gKuK5SRE44D zdluca9KGd^PgY=fIi&4S3IH|f!}Rp6PcPz7+l~mS*AJke@pTho3n2|>TuVlbXaR0H z_!^NABT9|ppoq(frD(zALhVLgZLit~hoR*57urAsets_+Ai2{cB`@#K`9`Dy-h$3J!X6A&1cOM&YD!GQII_fR z_$6eXbPb~Z0g{);9Iwo8T?^DxsJ2DqX%9`wuEqK+{4)Vs2@;T~1_%X7i6cG!kpz?V(&&Z7>Ez&8yC<<(s$aPM)O_p_=x0)Ih-d^KfA}T zsrOcseYJd9oehAKE!mF?0q6pA21MVWi0tfa*)!OGv20`&(Zu#ZBX_qz3wp#B#vk8a z0v1&KfUZ|Cee99Mt!Oc=OjuOiL@wxu+p5EUb1mQ9URG}r80r*r!`KcTJMQ|O zQi>M>8tP<+pdD5LU~BW{mwa$JF~gALQxBr61PCT85pO{lR9o}S1+6^<0hQ9lgx`Q$ zk?kvd%P>PX;axo8)ND-F+peDmfkR0uYehuoqW3H%#*!S);b$LbAIG%B;Xb`pjuX z9~4r;BqG*cF&-i`D*Bx39@ixk%+SKd@)2fWw>sB%q+l63jO;ybRn2 zvca9kCesz$Q?uIGUqsEv7*Vd+p98BJ2z{;Y1&8Y&(@IBIo22#UTWtW^os6S};E_CTQ$ISP=7%R{&I&sddxL4KQ*7&Qddn znG~7fVCZ)(Jf17H&IJu-Ey0C;sB2T{A574F_L;x~48p(m&zVC;&E#r!CU;8`5n_6p zFVpAY3==771+*kg9z;uJpVsV;H-hQni&{kxM9Ht<%n|915}}Rwk)cxg6@hS`0S@2c z_bjer8j*m_tX_M{g3{TH4)C#mPRI$9($R^Zjw3EA0M4C%K6o0oJ4&3xY-p>DY7r;^-F(RcTbYzYxO6kcIhB5$cEVIWtGNnaBWrL0^#Ir(2e; zeoi&jg+1UN0HB09%Ql7oZiThNjmD5h3iI_jZZ0R|$9Pj_E4w>;Wp8huVv^2NnE2gF zq5`##9N9Fvg@zCjwKRsSF7d6Yz~ugg)gbl^EiEGSm-WI7RtO?8_I9(U&_`+yWzrox ze);&#w>?pRKlAwGqX2LyS}gR@n{NL|ckJX9=93U0Ef|uP>M zAnr7+$^lG4#A^>pRkIZ;Ky3;@z7!~25CN5n8VJz9Enj_W9LHRd0n?I@CPoMqqU2NA z<7X9hh;YueG@_5!RD#~5q7jJ&0*`3ZV5GGC#9gj2K~oG6WGoWH+2k#Z!-r79I`Lt; zhm}MkJX|13V(xv&2oe;2VM^qRel*L6Z$tq17E3olPpl?u2nHNSeeP6MaXOX;bS8m~kq{YPpCv(ppXb0fRlE1rj@Wi` zgZojNlztk5Ohfxb;tLv-A(OJto6k|mhsi-8;3<904@&>i@ByVo=o@v2brvxQ?gm(9 z%!RL|k7wibEuXyOwQv07;VS}scr5^IFL~vceR6uk%}K-FnRGnD;{b+2vej664wD-$3qtniCE;A&LEXIm2!wRgIp*O4eASKLZ?HSV1?ye z#f(WI35+QyiV7!)0D)jM6e8Heq50PcdHBksOesV1%yOHWZr%pAq7qQJ8Pum7`z-q- zLJKx&%$;LHvd?@L)uj*uMCpw?8^I`@vSQysRYPDl;AfVEE$pFiY9?Ufaq;INZL?XZ z=#z(9TJa%i@v6`5hHk5boyx*wEe>y9A3@z0Je=HXzQMuRvKlCNSz{7+$yEfLVJh1u zX;|D(1{kg?%&R4^3P530`m{L-*Q4EHH~@(#2$Q=r)4-m{{sw)BP1zJjV)!V7875_a zzF+os_ACM+nP`T75{F5Fv5mfflB6Y*l|Br_iotrxnt)CUt?(n7!c7vE>;q_MBUn;K zN-4zGQ(SmXi~H$xTBfIO`oQMq<|hWVVh&Y1d;&l&^v5U1Z+L$p`-aJRBL52l53pJhs0A*MwIzJ}(unkswIlAqa$cmmG%Bf(THM1)*bFbYJAVg?G2s4sGAnvP;c?#JZy+IysFlSmkXqIi!`kCCv`vU)}7kLS@zG{ik z)&^#MSZ9U=PUjTEgoc--JmIDV?nq z60m12OB==<-*epuNPqMZ8$yiCS3}E9XmoBFFffZ1Uvb7uwzsG? zNeyse?y20tSnWymWNQRSeFse72?U^&#Ics(p2i};c}lTwIur0 z{qV5rR&<&?M$Ft`u>M@Xzm^i^csM{A)8Ig>01lmr6A9^ELMf?XQAOIVB-7pLWCM@j8B^sQ-L=vH%h#3K~-8G?#6i>SN8A z4SdU3BV<6#t&;sgDr2z#K-l3Z6g$zhNH`?PcVK2md0PLnH!vAoA`lwj`oav_-vN}Z z6Yg{B`^(Meh_eo?jQO>_Z&~p#1i{Jx%&xyIbD3u=!Am2VL%8hCo+eM$N#>thYQUfn z?a~O(=wD)W9HtQ8*HYL#XCa*6+IYBDt5iY5->;UY_>PP}g)!bAXQ@Ua`7w!z1kcf* zR)w0pCh#CGcO9`xd{K6;>{=;YF9kj|Oq1c7z)0~? z-sXLeGt3O9f_KoG8o5w1X8o*+x>6Q|BqKo^YKgm*lxJ2ft(Z$xaRPXo6I(GvO-Y{#fBGCWn5|AXZ8) z^k29>BNx!kK|hSZw>ioa6CearA0rti!DMis_e=c76adm{KvTNl9kJ)silKnKq_r6r z?hH*)C_&5>h$r)8xDhZnMulWP1PEgNAT-v@_zKK}0GK*ml)nuO0;MW{vnKjOd0-BK zmKhmaZm1kLLMWB4V?Ty#M$d@3=&F{@@Lv(=!%{<6-5@Dc+M{#c{Jv#cWg_-JWzgtk zoy;2cM|_`#C0aaY2(@u@V1RtM_;uSxa&Z|P#w9IX~`WYM96^xb?Q-00WCH81=#F^Is`@9Zs zS`8>CPd|P8Yrpch?4rjHVG)4CEC9d_aQu~T{I%(cQ=jN2l_aG++0fNBVLxharFP4m zE-5Ukb(n}=$+83gAa4WsAd+@qo0D0C3`d_}9)_fjPV=D&TZ8D}{Sntf!+g;bJY#r3 z{U?utP|YVlG}Q|&Lm!B?AhAJr!Iwkoa^JFmlm-F@swQ%6V!ARHx|-*+f1sPnh}cKj z4$N>y1}x0jz(kQnO_27gA#rI_$p~vrRM^qXG2B-GFruO$oaMsE7-E?H#GG=PAO%Re zTqT$cgl?+ui7;h=RivsVDNhr(Fh93c)jIZ=VuV3@s#xUDaKE-+3(Epa9b1-0Ulem4 z#N^R`-t=G66YCzw@j(7*mfOvU7OYJJ1{7w3x)dq=W^@X*NJi{LZhC<02vdv#B2*A^ zZ{XRs%yrJ}ryu|~>faYslnuS~gCedh({b?yDhy<;)@V{~ulfAlm8U$A5Qr)2$zw zOlS7*#@8;U@er~@3#vZ!+~tN#flYnkTiRg&AzcSCYyd^gu?U6@bpyaM67|rGlrq+s z;sF!O)V!fZNQzwg;~NP&0Z^T;`5z?n~nh_bWw&-}%XX(#%EeWg9$WBINVCgWNhB6VR5Kh5Elzr;C6uJTh zXZ~_qi=al4?8ObXk*rCgINCK702YczL2wFC8Gw3P($<6|)r>iYv^#i231%I*4a|E8 z3Yy(67;?t7fUR}$*xfJx<^8T!&gdsr2!lGQY(NlJ^IxXF-S=*5vZBOksugq)NDGQ;&m#iN z08OQ#<$+M!hK`~hS^|~?G?=^z34t*7Ll6fpT&Yf^B1J|p69DNvc?&cXMFGd~U;-4N z8IHMqV>9R(-4GSvMu&K+fbyDfNJq~%Vm+C3Oux|VOv-?nlQPVjmYMfNR02SjqNGQ1 zH30Bhz!qILc1hG32w@MI6{)F)>6{^9m_IPK_#fulEd(CP!Nf|a^oFrhYbfkuZIMo2ST5_33y+eNk-#X6+?+pRVHiCV$NcY7$tmNw1DY8#@?5*b@@urshW_; zivtM+C*5JFGr-fB$8P9dVb2V&k;Qr%7CngPq<4`2f$OskCul&Pph1l|)rxxRP78sf zUref!;m(q(Z}c0-c1}L>XwR;9(X33eL;%yYBz_e)HI6D-9e0!;V&NJ;1KW zWde%;LKEBq={9u@Qmr_QQHvrDNN!Zr!%l#hGZP%%=)^*ZGYE|N4`cg7^9y9M6D@=m zrLPqX1ib{Y4}+tqP(e0TzEPM2Lezq*dx9STkK*JSae*1NJ-Dttht(ibm)dw)+%U$4 zMSwseRRmq|N$Ee)|0xux8UdmlFG$@aEHqYGQ4B4OA5(y4cC96Iu}r@rk0kN*y%1v( zij+h#%(r;DhG|(d$i1&sF#K405eQmBJS5t=Sj`M%2wS87!WyA`H?K;D77Q=a%o0CP z8Pc*Z-8??WSe8`-t;{AW%6aulA1|T}GGzt^5Jbp+M0F}co0Gv5<#uomzM_>2B z>5aEN!DL*lN1xC$@I^BDI51gSa>yLhM9`B#HoPc(kYGGuxN7r^oom-h!W#-B0LaGk zX$po`VMGUf31;Cm!J!ZI8S?BP6f9BPMrcG>hBX2%2cb@4rWL(Q35Zzwdb(IVIL=ex zf{{XC$TQlOx6Sp$*loIz1Si%3nddMM@YiEG8W%|TOr3Zmp;2gwT1d&}U(-D!3!+O~ zj%P@exoLLL=-^I~0thhen%#y1Lzrjd8faCRQ(_~mOe(;Mt*I2#N4+>K7}P@49QWpvKDb|+P zVy!}u>(-P9=CaX^70iv&9^5o)1@cnbzE+WmkJ5_g{dYr6<~|fMa^xxZ*!JZsWmeq^QOz?2EC$l~xv<>&R9hOMoit9YI#^dP z-J4lHJ;X6H86sc52OsAAVzM)WpfSYYgxVWxB>Ym&&cCL~CY0H-L03HA?d$$(m+{RoS3P0(Be{*4_T zl}SR=HLU}mBkV@~w#~T&fCznqpHLVDg^~b_DG?zN9)OG*9u%pI(U3t5h-4dAtd-GO zT05ZSfm5bq2AB#{YG`PT5KKY(JCjZ%HP*!55uIxN)BdDsa2C5Zh?#@G(hOhC;Xa-O zn_;!6ug8266v?%KDP0V?%!LGZ0W43E2n$R#61>#;XWoC*KY$R*IQCcziwwYcQD6(Z zJ24KJ2Y+t(k3&-gsFtz}QuNDS2Z~>XKCLJTFd>8;oLLusCyj8-{n7wB1PegpI4LUHE&PJt{zlRlTrv@pO+83G8cZg%Xt zvbVQa=DT|oe9Zj^9HAm_IObH(@sZ>VLcZZxLeR5IbHRY?!9Im`V2aYn6tYs$ilOmL z!%bxPNQW>qxF5xAP`6TrYz5@mmohtAHg3D~mrE(n9Cq{np%wt@!k@8fGG*t~T@U@* zVs?DL?-zl!OBKWkfiee}Lj56DlODCIrAUvYp==ozHPYPmG`-Rfni?t5=JcThEia5e zqsAx{JLoKqa3tS=x!evj5lcZZK(ujS^?jkW_;-okRqdQ6T`&uKv`|j40n8Ch{D?mf z<_Rk}qXq&L$pen6h-9>45{}|BNR>;CK0a$nUthcY>{9`Ej4Te!^O&lrGnVW~!AbxU zI3x6s&qR59Y;#&S;L6BHwCidbOCTmQzf=&+rl0Xi5b$s-c}#z5OiGf?5}@UxX?YlE z-eh$Wc@owk%XY}ciai(B65BI2=M^7-Ifo4)L2Mke*15nQ3r9ROd4wk-PF#5-!*fLt zQ1C1F5sm?@0GTk6Lb~sxFf~KFhhzaf4*7BJMbHQvCvzcGH%KDjmowPF7sPLP-x*vV z)(y3j!;Bu417XbIF7ZaMw@t7`mUTf zeg2k5UQ^Bg{b4u%A9ewN@t?PU)z|+@IeGIF(<&0uQk77Hz2l^KJTIF+Zbh3(hF6~L2%KxKp8nd zfeAkeN=g)|0HZ)$zd-{~h+;0@RJ2KEt8NSsJ!Z+H))WNM6ozCo!TP+(Gg9mFP@b%M zn9B*51QRbc75FBltEG7fFjFla<82ejuKHGgAnES62$9{r9JHF+g~_Lz2T7&EUDYTCvA=OU(i}@6CQ)}N z4I~UX_8Tk@k;a_Svsg&10ce#7a;ij`6`||O^oE;1bl;ob`m6QsGl$&#UkbtfsZ~0olB8RiY&uJ;~&b#1MDF-bppcS+n zxPY2-1%X?hDb)#?4}~l~|7%kloCuaHSXM}l%rfCM9$;N<8ibPgrwXC?u$*p`qc6GV z{nhQ;-h6o5p%wsa&pH*}I{xr$e|~>6_MT8*LecI>23mh|j8i%B66iV8ToZRAw10V#?AHYCCegGK(Mxl7#D4PqM{k)v3c{iUYUIH}H{$+* zb&GU>uZHkxYvtzuqbIg*diCr7wLxn}|G%}vE&x!gzwH&T`?>Da&Cfaz-C8hujdvCW z7*wABXwAL+R;&goFND97WE=pp)WXwjn$K2{H%)xhPBRp+A$C&b&zZXTNujk4k~B%c_>jOkZ+4n9|$+mfz%r4iKs$=$1P``_SCvks8WqQm~Log z$Zp0O3gM&l5T-O8f`sK#rr3fs(1_>PYKYFnT=GHLW-I{1)S!9ex5f=2yFnQmOhk%HopIPEhRMz9&TmU2GWVG-DNlyF<6FTB;D+*nd%aBF(cj&H96ycPr{Wt|4$d650UW z4x~3TDk?y%T2#PHvF^cGm9L}kEj99MtPFO{FPJP(XOjyd_I#WHk3a~4jL!hH5Za+@ z_cR+T(39>PXy*{v6ZOusfwLLdo)qBH!r$gkLkPpYzhO0IUmBG?HdWbl)cX7lRfFz3(XufQY8&B*59anDgCP0iCH;%??XR( z^U0GRUVZA}ZKD9-cCor!&K|$}fuCD!o~ZdXYQ88^QTKR8gX)Jb%;y@@82&vEiHQx! z!WfZnosaETN{RaU%8X#nkD=iNgAS{Np|6A5K(q4SQw=r)A;3-ioWw1);Wm^^K~p$X z_6*g6qK^P6LV!WmJyt4}ab=lVWx9eHFmN;ucf6NSVfLwd1svOubTove47+0ZA3#fy zEfIZ=CnY98#%y7boclN^`YzF@F}8e0TmzsY#{kzsllL&!UNIQ}+A$msnzg!zg0Twd zk;bF}p{$8<97F|B1#--XSr_k6M=zKaV3050jBa^+C!8iXSl%>SC#@0)SB|Wgd|MurSoNH{UJW zS1w!Jy9@hDBXBNJVK6Nfuu8BhvCulOXvkb-A}TO}J_LxF`$a-KvBtzB;xn{+SOA1k z(Rw+cGaFlM^>yI`!bItNDNH%$YMyQqaTPMggFL0V5pb_BVdR&-bTq z`RLT#0Fe_w)vai%CHQ7cstair1v4~u9idD1sV2=(OTuttOQ9cR93)|jFeI;D7Jwbx z9I1feQzcqCG*}Q$zdEiWSStW{;XhZcmgz|)oX_YbwJn&zL({|9Y!Y#;_{t;64hEKC zX+vm*n5ZK$jrBO*K~poH5t?DjU}A38MdN!jG!Lc}ub`N7lv;6s6UoFg0128biDRhE zh9`lxj;*4&4_Kt71S|JgV>!yiISp7*s?>4ss74PfRqRK7Rzo~N(OHxUZ&EgtVj*Om z>Ss=TX#!heYqEl~g4N*J!QPPdL7y7`E-@2<>kz9C1*dQW0Fh779!&D~fDmNFl$T;$ zXtFvlyd7r zp@6P#n&vUANe&K*@E!Q1!z_m+My4sn`;GC6Ms7eN5=xxN^p(>Bp(wEh$sp)QGjkxXr^O*(5Y<4~e3%`F_i;va6L(woHqnnhyo!zp#wObjVfNx!3 z@N(lZ9=gF&2!q-WLI<#-oBP965z}Vz_#ycF45uK3wQ>(?xG!is8(^F00u;ej1ON&O zgLxOS*eFtnw((_LPL`WT_fOsb%AYQ!JTZd*+BOOR-e8Q{yZJS5{-^V!CofjMJgs&B z)f5_=RQTtNasXKilr3V(dU ztSBhpLNI=4;rPlKtw`kYAiN`RL~`iDM;ro*CxWS3jsV>_A^x3K&x6d7$!ws7q;{|Q z5>gyU3xdys`b%?N_-+FJ(lTQ{O4<#?yeTPVA!txb6iuFjU|vc=1;e2gBj#HHP7pSe ziOIrQxz`w`_(l;rDEvN2hX6iZ&<~4>no?uvSAgKKc;J zs37E$*kk_V9>)tn7)^7K$Tqomx1x zTGu9#9De~;wrmGtOvn|kro%%Ljh&9NQ6~#kI|3r zmLrxkMhlBcY^$c?ozD}N0t^^`GCzQyRVkdMA*$Gy>QJV2L@j(dZ)x?2wNe4yDn9xn z5m905UkiCvJV4d_!8}P1fXm2wBR1@%S6uYMN&zZ02S7#iS7iRQY+boh_9Epk+#}G| z;YNdW2)d&N5ygp+S!1m!Xbsi`U{||cEIm9cA^?!iT^}QE1lu%z@;8VaHor7-$)W2u z%8C14_OrL%cVCVFAKRdf7JzIJ^`E}^(XaTi`Nr`@SyUSv^dZ;K0Q?3txMb@APK`IvFOw(6k(#j^QM9hknTc7|{Pq zJ01zMDutR#*8GYR51l56+)>DUW};Ia&$PNG!&(xU0U(1>W1cwe)(9Z~ZhwoJ2a6_= z&hVl@z{ewixp2)G1ToRcnCnC`yOk#^@_6_Y=ej0q3C&#l$v=YLR{Q~pZN$=3egKvu z1_#WBjSuD$sznI`WE{K9k69CHXFD<10`K=1s7F zTQL~QIMg;(2EuRj`HLKszH^|)aZYl5C{e`tZ0%}xJfi<&O>G?hy_LMhp2Hl(*~z>N zadtWXF}Lap5vG}%X`0cg$xHd>erB~0J){oXs9U(%zWVuVM$!7g&PNRq>`ZMn3xX8^ zt0@28ko-P2U%Q$8smX)FqBotC{rSG7{H+=9a=sa^k>g`?3j(cfMi;`6PwYyryN%cO ziyS~&aj;+H2rw0L;+Cq*15rnMf7_(=%8+3VT>$bK*k_l8O?aAgr*1xXf8Hl%a5Y=2txkWpWM_$R zMnld4zct5~>b1-hO#-llkjMcpNKH3={%WdBlrywVXj2#hi``JmPb~(%NoepuD^pt( z!%&(RWNdh30vW<&9NN9Qai|gcbC6YzAy|Azj;%qV53B=Xp<UgWJN4FJX<_MtGgpgC->cSW3qY2y}$b4*Szf)n)MzV+b94?J9FmDGC(5l zJAUs&Kew12@sP0kV_^LZyN1eA!LUfnV++E6hOA_@@mPXL2J=atn%`&0Q91~$&_@tc z*9`R_pcBYF;D?AwGgJeR!?`vUt^X1g+QJvi;xazoIA6(s0D3|gSNO{PClIKV-Z?h9 zHKI6);2?4y<1;?SI4~j*2t*J>_Zw4ZqsV}`(-M#mu0Kt^&^jk6`$Lsa1S9ck@Rqi? z4)`7tEU8v$33nit|@SW!b98Rr2s2tEu}m) zg8$aHQ2-dU*pTxZUis#K-XFj5`AX9)*N<2a>&~x*gSyyNIg=S9i{k_hN=;+jxbZ?1 z&2WMePKE*B@#jR9wWj5IKD0L9XGFQ8bX*!SD58t;btXM@lb?Jz2C2@t+QNxew-;uHoOh(aJ)JcbEs8p%I_OweS>XK)aOU}Na&74U3mc|u*|PvRD` z5M%EfB*GeC5J&~;mec%6Lhik1Dc2N3(kzU(h^7z1jsB4Mwl8jgT|OlP6w*Mj%b64w z)h-H!FNVsY!dwtVvp6NkZHO$uq}Xfh7hM+8=eR$CX{xfN3YA5C&_~f^bqvkigijt? zuLx0OzH*xZE-6JyR7g`FLSjy2fAUvlT)Iedm0yy(3Ys}l2qAE}up2I*;S+K(<-1Il z4-l;^0V4u19;^@W430yo`#8<>_I?Ozlyk&Q5h1|LaRDSDP65cLIQ2pH zQ3wW6;rg;6@JP%K=)eAj=fWh*XXqkDZgxlUrv!z5lbH)gXKwts7SdqWX%OG}3P25e zD@HH&Q_Yvq0{T_G0=9uv*}n^H1cX#pf)kud3M%ass=lZGeF~m~{iNDA64zLGUP#l4 zS&<5zaN$_oy_>;(FQ=B6nHs}3wr<0%ZL301jt6~7VXHQjzBa1?c2Jo5eOrYapnkS> z@x`g@RW)yf*)q~c;4>x=+a{lut?liyyR~h_e!Q|`_Lt#&)W~Z}TugB67|Ym~Q5Z%J z_6?_)%p5ojlPJQS0hbU|h(;jQxg2N@IRA~wp7zp^FJ(Wm?@2859loFP{QD#>EBNoi zO^?0d$4V)?W9Gku+b966X}yC1`+L9o?LRZU{r>lN6@ojFZ72yr1b_p1 zo&ImUmnQZ}TPOsvmo3!Rx^|=v7k}?p*QPrjq1p&FG?khi6QcFI*ca1ro(YS=N zZ*ZFtzt6de@Q6Gt(qsLu-D|loa`oLPC2WcN8-fV(gVbvSb#KhNYUIP(;;_`^zJnFe zjX3z+mF0Y1u3Wfa&q?iDBnG%_=r+qz&F<=mP=pwVC-$jwMOH_|4rJs9wrTi7Rfcb; z3~LdX1f5^dGWxO*2B~=|+WAi%V?Zku(-{)jY&M%5x%;7CeDKZR@JsdkG4tQSZ4?03 zwKHeHi$8GU;n)25V&m96EvuCIsU#yqAa|)Xm@$L!sHUe$d{B=A#d)Ynr1_FAaj6mp zOn_=&h;8?N1_&@P294IF8lQbFSX!qtV)hH6I>wYfX~h+|O48R1VljHgtH-~9h45$l zykw#j+!60U7PPVoB5Jho_~UgEFn9TTxE?V`xhE4VxrXRO(`mRM%4u(J-`5IU@x0s> zxrP{-Ag~!8!dPpVqQzt)?^9ulwHpHNU|JX$D5Xc3V}(TGvEhnBeg}P@$F5^f(ZlZ= zX)VkHYX>)MCV27E$-e&xL{;rSriGpjGbICv+2*9kx(3)MXEb$vmFnQcxf-tO{X)07?aWt3C@M%QiN6Kj_V(*xHag{yY9bfedGY zAj!dmv)nd_T;@I??HS0>iZ8VNhvZ(%NTYS~^yOQ=?5lpXl=93N`gip<3IGSUx~CR* zeZ#l>=;WrCd}LZas z$&|TxhaUtABDL=|&8%wSDK`bfFjF%o9ZcFIN7TY&P26OfS{M5WeM2t&%!7d%7P;|q ziiehhSTC`tq;{H?t5obmLnX7toX3d^z@=>&U}0ctM~ws`I1y!2|5+^pH3>lE`KV{s za|%cFfOp378911>?`B=Oa`{Tx+1`m-45?Mu=Ot(_Y+aJ4nii1A1A&O51_Oh&@Y0VV ze8xw?;7}j|&8RsAD%07Si{k}p);2C?R}14%y7u_s zMk57LinO9mY(hHM^0k8chMvbzzX3v?2zvsJlF>A#VtV4l9F0a_M`(VMP^UTZWG2uM z#hh=%F5ksBG<|^9kk@F$fY5D@N&5~$&P;j!ITY6z7h;^Byr(ZBCDWhB8lgLb^g^;?1hU31nP&GG-TmgrUhkaNz)p4K3O`?cgv~ zbup#6ZwTDdzJmeEytsTO{eq=Os`jGq{xJnl6j*X*jCBhjtgf$_l0{;oR*tNDsiCI@ zi;`m#UYgBc%8)o2ltjbltiWBxK1FD!3L3E%NG}bcfmSvX$~?m;vnyH>AoLO&BkGA& z3`v;8yujY1?CMi^W#a++R)d{-vUoIJgC)9b$G|1*aEU0)jofP)+M-`>67 z@a_NeLdizN(+4IRKSn9+%tmWVdb z5svrz^FVt^AQ7Ykgkvj9H2{LzRp-*FdwrH7t66Ae@WJsG^ z&LsvZf5(8C;#U0*;d)}&Ad^C9QlZ!bH7ck|qf^Ty!nfBZ91VN3UzxumBpXZj_MMH#hXAU_wEj;QPl)M8B9c>Pt zK0|1ZjzxY7Z7G)~);npRKYuozO^)94&@aC1ZSVZwa1O_I^)?Cs*Jw2&U}IzB-=2E( z^*_>Y9<$8e;?hcz9SqZsEKdzIA%wk!8yE*rp8)2sg0YtQ4Ui624E}0J^^VRCp&@P| zeUQ||W`(8+I>d|yc8&9xh<#1a5{1nMyf6v5SZ{-rC3>CAOo}II3R^;%Oe-72DSM9O z08q6S0S}!U05C;M2%}9k*Ti=+IjJONuc>+@0m(p+yf49xq`zS{&ho+js?gqA;$0zDKtq&XJjL(G}xyN-BwDeyF8 z8mM@w0MFPu#6>chQ><>6_UwauG+uY$i{Y+HVOb&Z(eG< zl*Rqu`0YP9x#hkObjyZMh06+z#p8DC1x~n9Lq!1=m?x=G07PXaeRIcV_>M4~i0!SP zEjt^ed|Vq9!c_joF@TomL@D}!M}rE{Mktsn8Jja(31##^W9ftFtFZKeR>xcL-p~;8K zhQ4K3PHc=R8fmyD8kT`LK?!MucE#S%)e*u4b5EB+mcZOcWWMmb%REznvptp}lA4iC zpB}p__&|anF=mEkivQlmA^=4`Ps8YJ&!+j9AV*B4<}HXyL0Yaj_euhF6sm#>e<{G_ zp%VvQ?o|pUA>{BJhfp5-i&l!kS|&n7h10j%vgp67BWL6R`$J`QHP8$>M^#C|+BIHpd1ptv?#+3e>W6PCFSElzb z88@!=#6V(RlAI8E=&>&8!U57h=|Z3@rqjvhJrDoV%YOeK{%5*N$9DBL3INxi7vQ}& zy!Na9(f;P~{jyv}8hGF6epbQQX)AAPp?gT4CYA{XMxa%iKhP5h$kg2}@FQk=+rg=&akAO7B8YJ+{$oEI4*P0qFk*!MB1b^bjk zFUsukn#OEw`!TXrVtyhkhCAf*eCK3hAEvfjGfUT2)ASg(?oo(_02 zuPKY&4^_kR=DYjl;@N8Y4Gd1zdD4~ERjQw9X=IM^99y~^CRItBbf<2)c;_45`u9sIpBRz< z*WX3~;5r(t05ARO-~W$h_dNWo^%!?%8*r*(M}yqnnyFAI)5$#lL@$G%B90a2QJz%) z6V<|?(4_eeiCMs;6A6!mEse>V>PBSiGeVR<tI!Gs6H0I-t#G_4H~8{Ei|z%v@L z=vJ1SBLkop=h4cGks*)o<|-#gBD44Z0IZro%G?wVAWip9TDZm6O*~EtkR~XYxxYe< z82M5S0OkCJbFoiE*a+){dt;&)#fk=>=f>T4Ie$LnRuCg@C@d>!)mzo56WvcGp7+A? zV|(3%NilE6e71Q1fjbS;6>FqoZMyiJ_{^poljHY4`ZEuF{U31cfAsxdcN+zO>uVr$ zAG-PVU;B3#$4_6fViR?Db5ol^E6$W@DtUB7^GE!9sGu&I4Q!d}gRRL0cw zm01Kyv>;iC+0#XVRDWRBj4nF}Na4TneGXjA+%g$WIH7?IA&-m5KrX5=+1_`g7SBEh zK?Ne2#37(a^eFr`Ng5f#iKmc zYKRtrEQbJu1)=?cBT0sU2$2Sft#TLMEd(3A_p!?ShYwwWm#XP2@(@By0^`#3?ZO)v zfp@`NTsq;DPttdumKNOB#9U?=9b7ev;)i5Iiwde}<{9A`;yPlcg-J(oA(Ww&Wk_qB zG7}nsa&29pkoUrE4MD_#B@{T)k^)5>Oc2@rh+@-&WST#{>4-`&VXo_IhQABv&X@h& zJ==G=d*W=+y^*yNat*U@*gFR?bDRw*`>d8ZFyAz!B9C^uKL+b(Ks_oeSPPLF$;{*Z zvL2~K#IIWL%g5e#{c3Oo71& zhS(^b2!ROu(;`d{;nvhhn2Hr@2(TO!>SF!C6*>2nf1lBXq^+){_)^kPOXn1))izHgb5vC zpA08x_o@1TbG({quUWa&Cc}AVtBmy^SW{~(`)BV*hN1fQ4vdA)Q8mg%$gA%)os(oH zcwXHn=1Ri&WQ<+{S=TPc&0{7v0JnhiB}dfpBH=;-^GwG$g#)Ao_hdp#D`&T}wN);i zzfh)Ai#{e(wliVt0@@Pe)bppf2=+}5tGe^kTSj~aXb5m0EJ`+bKHkm5Qwl|Zu8dh8kTAIQ#0CM7#wT$JY4)a2A8PO~L7C0G+g zLxJ{6bT<6I)DhyI_?MXE^!MgW3i6x&%;;N#$quKspY7CAby#XK!Puy=iiTk`1S^Wn zRU5^uj!9Jq;UkUTB?5thkMkd+Fv2eQ{$^UlDnj$G#L+j}DeucZ()X#66J2Y~A>UfJ z91Yb@ZE&z+Yht1EiX4yZKaMB0Z@i1cRt>h%n^#{&|9=xECJ{06U0Q}QmoRI`exmXl z42kA#lu7pcg6HNwYPLpd~e6aLp>hpufI_}g=M9B-S zkf?b^8u*J+GYXSK0+FK*fSh=%@E1f8N`OZkk-#LumBvGkRJJK-G+qZG3U$Pt5*}!O z0}X_Fa8yErNst<_`hH`Q@ZG>QIXL>MXNSs>@yL9da)rd4*Z*Y>pd`75q-c@}0Y(_a z2=gy%2hFhUAWWLcSybbKO6>qlOuml+gkEW#0|moD00v5orDy!hwIMJf-7U;r0fcsF z>uRM4KfNCS;cha1d(s*b4EGExV=7j{KBQ>HFud3&S-nuB(E;cOfHSZ%oI7#C<7_E_ z4}e^eK>Ia<6vjh75A1_U_tM{1eb$`00nkHji+Yi}+7^6hVlZrfRqb{$Da-k?Ts(U& zP=TS@N9e2=QF6EgmmHZnL6kVxM1;0Vg`W84@M8^A04BIth2Ov~Aqs%_j%sKO#osqs z>~|nlAQUO>Zi*^w|Uon?|tyQ{`B7~rR=0RYiuv1jRL?6Z+7I4Zyf*S z8(;f1KQKRXV%G$F^9F=ZZ|R}DRh)%fU?^PR<|HFsLlD8Qn$-y+8;6OSkk&m1F$zsL z{mGIkjUX#g^`!_feRATzOHI~4Go?7rzYJ((*eb<^0hxxaJt*>|oe1!4#vaQ3vUQe?3Z5CWD90aOHYl=4$uDfW{C zThHuy5#*V{*{aoqrN2DxiC+;K6??(29Ue~kw`3`8%m)(5;m>qRu~;l7%VVduZu#N!?c*5Fpv&?FUkR;qk zb3h=86f{Te0VzyUi-AUz`C^m<>-6N5lRVm2lCKwhSukohq`7 zVwLVASR0fn8+!rhQUo>iapU=|ok13(%#))7B&c!u@b`&Mq$D!Ov*-phgkU(9$&xYr^@* zykV=r&l!DGVMd=}3BVS$-0#NZ#4BF^uU_#7{|`0i|JXi*HVOcrUc-@i;;wJ_j=$O8 ze8)4BnocWrF2a;zcefg`_%lT@PJ*i@V|yBEhT|DYR#A2oSqGEUW(r|X^dW#Bdv;cO zb8T2{x1q^`xr9|yM@>yvwJHpNrEPs+SrJ_{zKEY@!Wk0F05>%vy>N6j8Nzm;QjN$! zm(wOBN-(3Jj&qCm2{)k9h;Uo}7PXbA}}&=AW2*m9}(! zIDaYYfl#}JDC*t|v4%Cn`7y*%VH{M@SE1t`-iDwvnG;!(7Kfo|`}9}id!0G1Ye>0S z4kDR?n~niI%LG6SZ^V06vJN^Bu<0naCwmPG90a)rp20uJo@e_Y%wGoL$7g*-saoWwlV!JY$4j4h z@Ezav*R0&Hl8Ujt;5G^XpCKUd^xY5q_>GT!^^eS_M_Aj`2|52J43Pd?+)r(<)iSs! zRvDBIU3qUFI;ShH@q`2-3?kyu;nx*Qgo^Mm4|<3kblkW^LiDtcT6K>7LP(?ddz17; z0vXZVJK7Y*oGDsVi>B8Y;XxmTDfqBOKv2eQ)f_7`>DoSqFBbv_e9h`P!rAGLAh17Zl%; zIr`K09y^JQTVIKM6S+4ipd%GC&Ipq@L0|~cv;Mnce!z%%1`?an&8Ge33zzyUmo6LP z-{v_mATZyju#SGCwnKEA=adIrtSJJh#ah53FyP5$&1bAXa1j2e5I$i=6Zrz}4e^%c zM&r2kW!Y;A!Pv(lX`6_C;-;Njzw#UZ-t7;+{Ab3nzt6ml0>Ebu2)uIdH+@T$c%k8rsbY z#lb|HpJBF4tyWnw8Mg2x76lO(1(*voUzl}0PCHGU{w!z$z&9%^6O$`#6`1PGDh8&P zyapQl$_lct_|Rq=%N_(FM&MJLU0uEAcu_=0Gp<261M?TXDiO~%>D67N6P+0QrvH)C zfdU(_06}2W0+IU$o#97(4kinbn6E3v1_IyozsWGtf1cScTQhhIF%w`aL2Mp|hxcZL z0rKj%h<%2N(R~)o7;lL$Q%;ok3n%pvMtUtVvC*0@!X*AmtvTv)2=KSDTM>JsXAy!} zX99s+HEMyMY1z7Txolm$P&TKvE^xSFrrtjm`Z31Zvgj-^ESAH3T{9RLOx@bR#f(kD z5I;Q{s)4geQtkrMHd^fBS2;&?ThU{MIuEhGRFH3-NgM+9|Fm@d#`MVKVt<4nh5@g_9^k@eZa* zgn1$IoTeHvGwA=0(&+2raI6WYmL& z+&BIdxKQZb%|u5+%-RhTntd#O`t)6$bsWeL*5+7%#^ftua*RM30vyFf-MFh^NPrg0 z;zgRjU9ZjE<)xMDr+5Wi9$do&$}}{fT`@(={Shd~EjY$xz@*K?N?G6pkdn zKmg?!k-;$xm~&IyDi{(h8PP9?)ux3DLThdS5sTD<4QIT6w0HIt9V21EN?ROmH+>7a6WH)d=B}YU8H{)!J3rRtW zxDwap@37v*Jj50pPRLUh>$Re)z>^k{L3INN=OmvYCj`PX071vFmadigPrhPd1+A+3?pFf`?Pm6VgOt)iJ*5O zwIiI+)RrNKr<&LRbZG(=G(EjHN|0#&_d1|lTWjYl_Do%Efj z8!gU56zb_hy0nnj+Slojsqgg`zeA}11jk#tP1gZQ!>6!>DH;nBnmPVp@Qc8u!=Mxi^2$llu6&*c#LUkm=JuEEGw8{lRBX)OsOAV znFV>HGBi^!!#%N1Yl@ozK=ff^E|Q>TJhk7L_zX3_s}RwbE=s%(_lA!VV`vqu#$=0B z!OcDc6AqV!2v%IMl&)s^+ukado_d##QZ=JL z9-Iw8a#6#P*}IW ztBZrPZ{IBfIE-#jYdwV7MtA(-SN+0c-}xW>_3CmRP5+<0HVOcrod#O*t9N|uxBi9Y z4Yxj5YNpDdfo5$Mq7O3|wd)`nOR>6Bu@h4sEp4`H2(Jq!f%50j?C8su5G4u{h(w^i zLvWQ7xS(ZWDxj|&90Lqmly3YCbA0=|+%tSGOTyvVFyo3ff0D76oSXegMj8ct8OfQdeQZ3Vsq73C9!f0~$M5XAJ|E zPdYmqPDr^2Q%0>eO(!aZ!Nl!V1QYm`4BziY)PnHiLCbyC2n=*fdGGkdaK~`o<#})u z82Ua<3CaBAKCB^`3{C>qDg?^msl~Vk0uRpyg@YjuK)9V9^mHfjT-N!lK7R0mrmOweAlU z;MaYoMJ0tZ7ufP%g*G^zGRD+({bV}pW_R5Gv3tJhU4NmJ@&Oec$M%`GQ2_WXwrZNX z^%bxA(G!ooMWfhaMqMV#bG799l`nA47A8 zxh}e3+I@sl;p`&!y1hrVFVWl(-;9DMu>5(y#hnlietUK(f6&@!7cu93t3sG2LWRFI zG?I5wdqico-YO?z9(+oK43Q&-;1Tmg_ydx?WOP~zZ6yA}R6#gn;;KRKTyDt;O-{h02&F{l6fMB|4@lgkV{1ITaj&?1RNHRvOY=V7!ADn1LZiH5 zvxN377MsM4C_p6lhHBDW6I?H2Y3DPoLvBfczgg;Fz*W$2jyAFebKrB5N3`iDjRq3R ztFzB6fOK?3o(ErvZaV3=x3Rr*>edkM#F#6%IU%TYldd-b>FDvj z8(;S|fB)`RfB8>_>3?MYpT#x`0H3`EpYZlW-}J7(xnT){rm*V!1!C@U1a$fzQ#uzy zV+=MlEo9OYEy#RiVe77dKrl%7Ijfo1IXLqKe$#DUV+aR1i{P_$xzuj;O#(U zIX1WuA~X&|lhV3wVs)1Xf*tr+&WVzfd?CY^jB%tU!ZgJ~$VkM438c=V`2*4bp{{}S zx&$EuQzd{Y{jj*MQBcuknovbM;+$n<0JI=aaf|iJ_j2rl!O$pusN!HoT;kxQEu>Jz z0LjA0`580{4|**|h;?&wb!f!&8+cR$G8$SL zS_oWWmXaU@?sKw8Gunkq$N;Lq51_2D0M&XELWHOY8#FeKX{D=ZxBKll!r_;{F%Y+DJVdjfHwkuY)gzHn+)2e_>SS<&Z*PTqny+xW*xni z7;#!zB96U9LF?0@kq8*AWtY^HLD^ammH6xPp$+#+Xp?B2^z*cYm4S2Qpet-qwv?Albzo*^T2!KGJ0}XFegcuEBn)O~m>>>hTS@Tq=&?dvua?wal9EXhpSp|zu zrd@yK%9Z}yb7ymf#T=iCL2i5l%Ee*u;zqa#?x~owc$U91T^2dM*k7W2;BSYZFa6N{@A%_?sg&}u z6?4PbJ}Ye$06rIOGMU_X*Zn_!`VDXWYs-_Tub7?G%U6bZG7c+T*#T;<2U-ka1ot$W zAT(A*tcMBB&m~D}K4{>P1Ak)Vd=oMxsxuf*=j%uQhDI}vIdIE^^fCM( z_Dop2%xX6+mo8r{=bwLG1diMbLx8GyD}=de&63Y_UN1bNk2R42Nga!a9vY4TsyI3VdPjz9FQ=B^gd3nTL(mA zud(MM5E$Xr3TesRQVWQO`mqaBaQz9D?S|(rIt6WSkToDbQKOCCH9m-;ma8FS8ODUi zB*lqw%ey2}jAH|{pyWV49DPN)VMUm?ugLri!Y*4Axb~uD{-Bb%inzroYT_`PxUTlG z9C&K4G=d<^jphH8r-zoy3zI-|HEgbBUCex*PH%Rn;R-_Nn}rr3hujRG6AVWo4EwF( z20~M|@A-ID6J>3heKufMDEW&qSLO|cInImM+wlyL&VH(!N*QC|Wor6eh~9AyZJM=a z1eRID?9inuI~e1S-)3F8bn#NTc=ke>)@)tK?8(=9YGFP?a3)0abKkYXj3o>Tri<`D zVh@`sREc7+e7a;1Y=%pXKBV-r(%;yo;)%&~VKd6aQcla5D~%5TwxoWvB1rx5SAE5g zz5H$8_Wh-ld87S|?X%iO0pN4fa8%Ac{LX*xFK<5ZnqMkqqo$#Wu+}uoIdCA3%`y!3 zqVgh?z(R#<2(g*<6*vVLI`X2+srIKzI98fUm6^5-2a<=HkAt9ThyocqG4S$AiEvQ$ zCaBwx$$**r7d3UP+`|h}_~0`n!lC6dgII7dLvnNVdxP?&zt?Y59g||#nq?k*7G7n! z0vd%iulFg{exzGOITT&G-QweRh1 z>2d%;il=6 z$HqbP%ps!vDL+rAg8N3(oGNUg6#}?3gaC^`pfT&J`6X>pq9);r_S`fNW}N|u#8w(6>N8(*z9DY{RezR5hs>-{)2q2u8ytT>TtkQ8QK#tIt ze)C^5F98ZMxnaqj0zNdq+QMBG07xU%fT;~PDNW8`_^=G=}~Lm?n>2r(LSpBN}~kGYJla3rSzA{@~#=hfzcr zQ+-!xXNhhfTn45HtxAa{gfTKB!2u)^I+RI;*(Gyl&y-=Wvj(WxrXkq}Fr^jmsW37f zKs#sa82Lf&fi+Ed_<^mFRM$OrumHN(@eHKN#(H6# zj0uSwQH~qenukL}k_1SZhP<%cncSXxGHZGWxNl|u4;LnX*Ql6!pGWI1Jr%fRO03hj z?7CC8p1bp{@BH(3Jo>tSK8E~#e%dGie2!Zjl;ula^Tz+>#y5P!4=j(I+>b1?R7MB9 z+&&s60J-UdSo$G`Y#qMg@TpczfTzxEv0+Ty0smkS6`vw|;@aLQu0YM>RgHC}`gy^e(aTF5Jr6Bdm|xQ&i`HK+^QiFeP1?&-cr@XP+(m z+uOwhfKzw?u7y!Ks4=0(tNvpe1)-|8jSPV+Qb~%CiNrw&A+-Gb453(~WQ|s^4n|l` za9mOML(YPR#rN>9swQvE@ zpl@0)^?)#Ai#a-C)-~x2EA>eP0O^|M0O008_*AqY4w-sXq0Dj-iV;vah>Sr)&N}4p z+7j{DOd>uhK}xzP|59eOM)AMuuD{B?SK-VneZA&8z!Vzi{qVX>al5vEd{*;e-OJW@ z@_>#)h*}qzh<>eiIm83Gxz=Dr){SB9I|dp$EFOdBVebz_ulS`hx{CadOnLz_g`nIm zqe7mQ_sxPloO4CT(ZrB_H$YoA?f15}%Ehzi%5u?{u5%+FGCIQWpHrSdIE@}OTrjft zA(ugz^_lVlbw37kh-#23Y*G`K(!7>}C!_$rhrWBbP88w`iMhn2_r-77x{NX?Je|-1RhhO(^ z`bpPyEGOd<_!8Q+LH{O}3WEnm=_<}GSfTp+1jJ(!e*K~f*;HUbf}okFrcSiJyjSxLfD37f5cj`JrmIPUI1ZH_39{7v)9%TG zO%JQn)P9u~mxHVxS&2_(Futgs`UX`tE}m@Q#3 z#C>NhL-TsoXtnDr{Rbf6s#c12{5~ZK4D~*_(6}-+Lx#wZRn5;l5YATmIh^1AfNMfy zMr4Z)d?hfyE%*bWIt6EUljWR8vMdo;M9z%Yw{9yPyI<$Vx(jE|l`H2iMD&Vr!6x1q$@^w&>efbte{()rWbaO!t!Cq!xGXCBeT7AW| zEAzd1Ie+$i+27r>FhA4W@s1kyXY0N2^H>?NW$Fyq06?|Dq7pC-O=R@K-hJfQy z40UXV9SFr6p@Nmcnri`qQ)CRK;1cK1B6*|cfC&K=jAu7=?ryNeLNRuPMxxqcURd{mC0Imt|5ZR1RdCpA2u0{4G-6gW&-;v=D(hsyOub}7myRX zlB*yu!xw?fZZMQ04hTT3(S$`Ima8gIx_5+#_EET*2oggJ!umT^)P ze;v}I@4Llva^%LBoO{XFe#>9E@6B)hAzm;t|Id3H1%S_g1Esut-`9NeUpV#pxBl(r zkrVr6X~zFb=z|V;7H@5*Kz6o!;EV( zPawOs(__ITJV*GV-guXQfLjV8%@` zhGt&ySP=~TEXdw!`WQ)Qq<7V-ks4~8D;69AQ>!MlDgD|j(^ND3Reye0%EgQ4Ev;{T z&eX62P?YQMlTr)VO-!pcku5=XaN$Y*0_)4fE8{tvtw`=yXnlx!45eY;Q+9$&QpL^6s$qbn~L0wr|=EZ;t(tRKyi-9e&;!4$eIq%Dn zQ@8Bg^v1XS?U%pfU4NsLveSxWWBYuzQ2_YDG_bde54`P<{Kez1dCLzkXGf#3FSLay z)&bXI0BdmAGtmhMGQ#~PYcbqe)JDih1N;-hyOJj${EpQ4Bt6-IBa|R@N+Iwew(@~% zBogkZE>v90f3Ks^2T3?gvywQqMxz%kMy5dMrMx4Vtz~7@mD0ZQhajL5#);`~9sRg< zI0m>0;(IB6#EQ39yzV!>YvAT|cL#viTDou7=*=|l|EQ0e*5^#xN}epyJ*i;eNL^i-ZCH}5l4eHu zhIg+G`32MVF$Z-STC8lI0!L(*c&g-_2Nw!#euAmHdm(Ta;clsxf@z2XtZW%!mxf^K zN|}{KKRtPBar*WD&JREK$Nv4lI%fL&!naWX_`)@?wNJk6TmR&lW3T<{AMK|{7FA)P zH7u>5$+oP80-=PliMx-RR>Jdyk>Ppv9^ta<7&{n0zfUTX+Q-m>poAXWCaW8YLcID6 zDs+RSJi@6PUKHcZ?&T9#3n$S58f8w+uf5W%gVl5N zGv>kHOS=H9s*e8(Z$mp?skV4E{c7d)10%y^6;#PdV4Z>nnwm17N?oeSLw%*WC*t+j zY}wpL2$x7V4CcfhYl(DHz=egzWT)Z2ZUjmM?FCrR-{Fg3o`AXzKfZOcR!O83?s z(XoAg+Xw>u!Z#d(2R`tFf8#IizW*2BIhpS}?AGd#G! z@GCyHgxhTg!%H>Rdff|JvutDLCDF^f6%@Z>5g2|x==b=-zZx*QMs~nW8qa;L2GuzE zRKwIsgyY(PbtQ45Z-uMxVqFB+X4(iRx`YoK0UvMXeBV$GarA;l6xuAm0HPa)LY9LC zYp(yUU-TQ(Bjv;) zulo_q-}7}xS>RA5-~>>xRA%*Fqgkt(oAw0BL=u|SI?H|#%@sd&^SbukLwSJIPCF3J!_I4!p$!9IX ztPk6PdHll~XSp{lk(Uea6|r9h0F);-A=)eg?yjJ=4fA@f8i@Yd0P1yJSs>{UC1y<1 z00^tOFCy1JH1irEP<{5bpcs#XP6bl^hKawFugd(-oLNcnIkqoQ8!Z4|Jk5^Le%XKHhyKA||L=A`_^WU2mUBB- zHDQN<_MHHF)DJ<}m6*^?3?Jhf&6}+W3#@-F2O0bZ44B{f;cqp|B)qN|wAu;pi@qzm z98YK_VXfujDu#!lh2T1#v1+jZH*N1)(NaZ_!dS^98*fS@ejVJOfs)(K_3(2tVTRwW zm}=I)vHhjBXNY?|F33!9_O*0jnoI^c;z%fdAu z+cJ#?H_;%g^!t3(r{4~kbj0qjP1id#b6iarIN(lsgo5+0x&;`5hxv(hz+MZG9g+;K zeO4Rd(h9!Ziz!4(5^#vwkUvF<~DqF8j8=phCpJjgcu9fv6c5=WAJM{8(>5M#h`|(k1$BJ z*+8&_cF*r{R)Z%;*#zhN^*@o4+#r&M<@349d>&~Lf*4%KU$2>$Nxvww?&JfHzUR^J z{?GoSo9?>rf3N5zCO;k97eyOg0AE}UWa2&df5-pn@!5T^d`~yqaK0~Qw<@W-4rYAQFkKrt_;d+F>z<^{gwuZpFR!($ojnJwsg{vIO>@NcPr_ z-Q2khYBQ`Gz=yD)<99bpWhq!=ZeU8zHM2JW^fi;MIU;gG%xG+dprq0t5dY|8JTsB` zV4d;01ot!fa@G3hi)DHK`LpHx^Jh!HoLgcalt_m5PZtmK;fKE;;Kv$75WasC48&R! z9F=3Tlt9A#Ycrr+oM$UtBw7FLH=Y+HKB6QVLUz`fgiw-v8n^+?i%=~JaGj*}Fx>_< z>5y-Rnk&I06?Ffw+Cf+luqCUzV%ATlo0H=Yy!-=J6pNvMc;a1p)no+x0E%mnhuqXZ%?S;gcyy4k}|HN`E+iYx>m; z7fh(NHKZ^z@Yf%>;)8z%@o&2em&3u2SRt^3esewK_eKaC^uN*i2V+D(@qvhe_&=&1 z!o9v&PWpw{xwYS_SIcEoCLw71js79z4Z3a!0v3vga7D7ou7dZfE#EK?hI@#Vz2vv( zuVv;dqLssX8rdk z@LY4u5bneBV8=hSY;*MmV@Ar}01*yCKq5ayR139mN4O-AY|O`6OBvI01FS;07Toe6 zq6+tl&wmB^LEymcWwtRncK@UAed)LSiU0eZum1A?)#8>iW7OEbh}tNXeX+JUG#8(J z@|&Odp}+N4%O^kdVBeKRwbZG3)=rfX(lDy4eNNp0!+4+8VLd=cv*;KZm;LQ87#JPb zXmH5QzRmz4g2swfVgRhW4%WHa^!Xa6^qI6_-@&Byd%aG-yWTc<_KHPia81*jI70tJdF`zsFs(9uAJxrt~tw#%L4SxT8NzIzc^0C`@8449DYmIRF3p2@VxF<*mq)@|ONnY_;9`AV@%n6Oy2` zgF;7Gwj!0VP4RM>@&0Amcav$idG{k9d+E1*&wqOF>t6p)MzjAHcN;ALU#u-qRyW*s z&p*HW?SK4CfBXF(FU!d^B88}`8-a*QH`9Ug2`ykHtuH`K2PI=sy2sarQQeXcwD&am z=n=EdM^ou)tqsj9BS3kzCuqhI!> zYnsApN~_zMnV*gyYrtI1nyG04)Z|XzdD!u7Z}k?+H=i%cxwGfWbI(0r=6m~PV=@h6 z8Mvr3ega)%d~ji?RiuP8?&m_7xULfbc<5JX{gHrQqGi4CPrbpC2-~@i`QyGu=%MUG zF!7jI0pZIsL_#76aU!{b;k!%1pRcYuySCOJkPM_0Q0qE&HCKU`{+o8w$;O@cKY7pF z-}UG2eckK-Sup>r?5?qWq1(7XU*s(gQFq}ZANcl<|Fa+X@5(bDdANdkOJ89}8LE}Y z-6)_Y+-W^XOw}~JOsoULD-Ten6NF|OfT$vZP+Y6FtR3wdj&=4D(HVOb={0%O&{_IEI|A#;Ezx~5MSDyLk1CF~&x2O?3Xe~nx6-hsu{KPB5x?q9*Jg9LtMJ9Fsz4r@YcAj`grpLm6GuFQh z*DX3{bb33_!vB~`AToO%H1#2 zN&$v>-~(Ssvck1PU8nNH=RUYsnYFBu7>}q;HZPNWG#Y+NK~V(D=E3%{t^T;$_sMlg zRI5UOF|P_{vnYU?ZEsR`_IAqF#Y<&xci;ATbpcdHSeabl5rdH>qYmZWYu?*_M1G8j z%eS%l^>Ekwcsw>gv-U;iK%A$jOJKk{$*@TDUhThz?;(zVoz*4;DwOEejDr4OSST`a z6hb!YziV0_^o!LuF&8qocp|>RUa`?lW_8Z{zS}HEUi$Lie(4|juJ611)nER@lS!@j z*X*FNeG#`&0QeGWkg6{{^YL$b@<;xsKi@z3yRWIJqGbr>QO=zw&y)kw>z3ot8qn|w zXzyzj=d}d+Dt6_oDW6bv4tmx~zSx!{`af0pz_X)33v(OtFvq_kUgPeCUIT%IR!;N@+LtgE#7?^_Lf;K;q85+A{OXVfN=(Ct znnAW&Yx~q3<4O=zzvi+R!u*HNV7~_sQ6{QC9Uvnd^m9A*_(%AKS>AZvyAVqcWjF4H zp@3`&>pbj$hHVb^jmH%FgQ{9* zUmx$yFuRO_@1=U4$3+k)!hsRPTXjTV3}y~au(=b!rMJD>Vr|L*rKKKY?n)<|m$rSdDBIAn}&RuFX3 zl8{~j{-m~iJz8qyA8xYXDRt87c@f5>oolqv^)7%m)8W8EV7AfJ=9<*TO^2j4{9}y} za-e41+6~g3F@S|%)#?Gt+=8rz0>r`3I0(E!TD<{vI~| z$R5lv#flpM0LQ~{V%vMBKu?R?tt8-q)I-Zj`1kEO)qP>#Wgdb|zX)|daRA5&h;xSU z#_A6=;UX9|5)NUGv;G7k8*S6qT;SnMa8N0uiac;&C%`>lWC|8dvLAN%pq z?Ej_PMgibU-W~AN+n)T9zw?)tpZMKZEtmbW<_ki&Rs#wX3>71u*J%8VhaRefnIlxR z^pW*PbN^(^{bYPS{AOPJH`u$npkQ`4Y=FC$ z%+-L3gW>H*tiJ}9Skj)bAQ)H&iFoYMjwi4rKk&~VBpg9bku5^`Q|mTVp`%~K_Ns0V zInU86?_-(?)3TE*#BBiUNYo9e^kzEUoSb^uW54{!cm3b~%*}V-{R<=5?@POl0>GEP zC7{3fiI2VG$$$EH|D0I@7E6nV3fk40;g?_{r{4Zyrn$)dFHO+2> z*ew@*+1cGL+n2Y>?#`YG1+xn7t7UhY_oo|RKL**45W^SGG;E($?~D7TmH-0iYt#Ti zgW-d(g>@+ZSo+@Z*M~4fy_(v3L>$DmNGlB47Jxs^&xN(K5WoohwdaXt!SnN1VPaCc z2p`1l?oJpmcG+wp7Q_tz6^cEkFJ&IN9cz7r6EA!1ufP0VfBL_=<$*{3#aP(qOTUc* zz?XP~1b+GXr+?oQKm2$8pUWqI`}O@Yq5}q6wreZy=|sR&*ab+O*J-XvIMx|zSDDbj zVtG){S53%4V3j!}vl(=#F+M=w8;S}8t@=QIx$cJms;#^_i?$3DV}COY6*QgZm0%0} zzR8uKfURo?0gYLFbr#o-Ywbrzi!%*vycJ1#nF+GVE}5;u6*O3d34j4%sW8izogox6 zKrIG(&C)^<^P)n0l7QTZ{i5$Do74WpBVYFOulRTWlmF(XJMaD3k@>$k+9&|L7+N53 zE6(VR>?V?ZT(o;aj4^Z;`DIWcJi2Uu%e zw7Gb`zWK!{8(yL zi4hsRe{i-g?Ksy64;e)c5vxKJxy4x$OG= zy}sX{m&ttYHOk>@w0x_cbJc6~2pB-p2^!rKlQl^PoRC6LSr;r)uWI!bqL5*uB`P{- zV5Mlrlu%Gv58m(nIN*=avq%Dxgg3~^cY)=qt?BuUshn>x#!N6=Rfsqxpd)DSuPe4 z=I6m`%{-E$LU5GDm4EC0;a?nUmJfH;GAZj@iv3=>Z(E{*dYN3Xlee$DkfARiu*)F6+OhAPlZ~7{9hWr+7RFSS2>jFrg9J_eN-fK}0x|6YU7PuBlXEXVCoW z@VPb|ESXUvmxXdSX7=a3kT=9}jYMx!*5!!}>a38kUgRj7{MQF9D&C+MR0w@IP z+BHA7PXx3~%)%)ICDZUBX2uM}MUsrIL+h5bHi>Ve367ky*X{;P=txG=;F0$`f zpV$RRBTc62H+p}=vSVPj5VG)~rEn4U&$W6_PpOG<99UQNcYyB7B2vqS&%DOi@9oda z&h}2(+TOOTeO7?g-izZYK{1%MYRF!0&m{*6ESi68!3e{pv16L&2)H!XVy$e0lmOX(RN z0{gee3>%mUs?a`T8_6wNEdz5{y_SD z`E7L(Anu=LXV}D%%2nI}rrHP!9Tt!JZx|iIU64s%F|ah2_`kDv${Pzq&_Wn8u$h!p zrG+J*-^?dNHX^J`-q~6{(!`wI6E@dm_Z!rCsM+zF)HjI;K%yHy3mrG@U$kTm{}+KL zLk5vz*xw1JR_ojQOhDSp>7x*+uF~>cOd!s9UPbu2s;0DjfSvIJgUy8?NNmN zB1sX1a^9nsJWmfCn}9e7-vM4Exa+%acIN{hzwg`r2$eVF5PWy1=c;$IZXW^@Qlo2wG$Xd z*J%=xU?A-PPHLin3bb+mDg5|)EwpHuFc~Tp8QxG6-Le{`DYNlqGA%1oyHcN~iCgTP zq`J(ZF8Y*@J5BoNe&l3Jtx8SHah1yu!)GR;M;9FIrA}n|NQ#}p7DSBTr}5dDvGC=r zLaY7&0QK`#*>9$T1~bo)H${S{89(MEB#z8b7ky%9B9mQgjcQkM15a0GF^~KEmFaJn zot>RBuP!Rzwq+J3ex2Z}Gk@80+ygq9IbV1T_OqtTX6PvemC(o;wIJI#p2GNeS(kyK zCXE+iQGpcNx}(y%1mV^M6B8tq`A71gMsO8_nAQo1tb>p>xl8RDvG`Cephe4e`1H2B zp1JdF-|^>P`W0XGPeic)T{w@F0_QeawpZbY^@LwO@x_raN(PRB& zHl0joQ+Ed-)u?EJ6>R6qP1Ous#h_-Z1l7nym_fnkNf4JwT&zmlfFP!m6CDoUuxp^E z0ou=^wE%3V#1rrHIIdu{p~aCfcAjt5t_cdw%)o7Ix?=|56R)FC(i>xlc=W^$xL?}y z4%8O~jmLX}c?1U}sRwg}JGCOjsfEFxj07}+r+{U~-Vb*S7>>l@+o$y0h#EQ&;nWW- z05J$Mwi4A9g!PeZa>lTM=csZeYE$~4_#BtFTsqm`n_J9&HS^E+_iTPWdtL>3igJ8i zt!41c`at;(Q7w`HLyrh75M-XvoO7c#z*M-A2KyyV_lZ%lRKP00pg$03<9Q)QaUU>F zBqIqqQi3ZrCjhK*0(_+Pg#tl4sVp^&t3|r1grmM^z@T2f9_xX=y!J)E>330Pn4r4PL?CbkCxeV7BR)n zu#NcxtzDU!<{KvqasU{#8Kf(%K3!brrbrmigJLP#U(x*55I&ZWaASJLFe}juLMu#6 z8TZOZF|AtjK}+kDnZJhbRZNNd{<|`(ne&vfUN@VW955zCY9z+Ng~&8@^M;VJ;9_DpAW`>X zt%_STo29KNKL;pM+zt4fLXlVsps6bY8;soWFll^~{h9na*FcK?aJ}0F2aX}7ot*3Z z&0R5umpO8@KmD3F|L`l{{;t1OO4;^Dj-bC6Ya62gUc3#Y@#1U$==c1kQ?GvWzv;VK zx3|07U$}6goPFlGa_+h3%l4JaWwBUVnbfYEdEpe(;Oo|yMpd-n4TF&SN*7G$HRhQ1 zfz+9uN>=8Af-2Auomr+c09?yWGA@f|^Vro~+5IxzLY{Nl> zz(s93Lg`>wKsbdkM)k+zYw|z7INoUU9Eol;st@uK5$+N1PtD)g*ZNQ~B>`NV@6F5Q z%a_XY&pub4efqg_@%*K-zq9Lrwyy3e01^E;@L?aw{8jM=(;8kcfXv)$trnj3GNj}& zEdVj|0Kt|?nJV~%3_e#WB4`>*61DX>55R-)x7Dt-n~^xyK~9JU@CxLAT5uqIsCb-5 zLNos1S#b{HuDWZx>8v~Y@MFL5y6^ro|9zz!XU?2KRN&ZNq-_KNzQ|iW#AnW&S?=sy ze(SIPx&QKS?mzL{FDtX@vP`Dt?dxiIV6BL@xmh-58)b82R%RO;Wx6pdWim5LwOGze zzo^MqoN0LCw5rjD$uq!%BtGT<4zgG?lSPPKd`;xD`8b1t+IKJmlPZJYraij~CNL|L zNaA`{IZe|A$YO<-S?^$;56zq*dUyfd+EpVI|2BRIMuTr#wkvVytvs^QFk^}I1{h7W zNO3Zo6`^YCsaG}@joRq2pfFYE*coiPB3xLh!5sz*wSUK4c<{J)A<$(Ji{Nem6J7#5 zXK^IXL>B#`?C$NCy`9~%zq?l!`}1N6DGdwaweiwGy&|i`x?U}5)Vlc-CQ1{3tm7~X z@_Fy&8_NCK!(<^ZMC1T2w2`pS>R` zp15H;8Br4M43A?>H!8g~>TvW)5n%&{0)pHye&y@I6+Af;^4;e%oB_k6t}_5_GaLfu z5QcElrqL%hBizweoDO3I7_(XzB02$@szF1{AmA<#Vgr=JO?qSUd;9xkch{Nzyb6gX z;Dz-9bJH+&ouT&I);Y3f3pc;UzeHo^5^;@J1+kV%w@(MsAKG`Lt#g0im9iJZHN=q( zQebM-%n~vx^7o7DLfsP@<(nH~xPqQwu-hT$4uF%s89`wVN7jnlVWx$~R<*t=i$ zP2cg~TX7#B?5O>}$lHhkI1DY)>b>-<-u4fl`K@37>dU|UFaPjzxv=oR3M?v%Wtmi$ ze^9-eacyyVyX;y?)oGbdyE2>2%4S#wy4kc;(V>QDFG3)229&;kYn3637UaS}=uM&A z18wevQ?dwp$MadFs}Wu(l)TC2wULdR3sVX=;@| zuaHnAVpfxUytv5&paTOSlSzkCupydE!SC^R%2Un=JYN9bhYEISg)l)Z^3~S3{7mrE zm*sp>_V@S8-h4l_{QbIZB1w?D05sEHx{Y&g*E9aK(%_3V27d zHCe6)PtQA(A%}Pn!Xc{bQCP5T8-kSyZi+d`xvzi2$^#*R1|FIQg87S^AjcFQe<@{P zp{&*YBsBsU*r0;UQ1EHyVN$w{jdJo8um8!HebaaR{R-Qt0>NndKOAku0343ihd=M^ z!*Bni-`~CDo~IpmzU=!&Ulxl<`UAQuSGMN$jE#Z7#{G>k8K=VQ6V1oC!Z$*sbvl!Q-Z#7o|DY2eKgo(GUE<#I;Tzd-ScgYVjc-os`M6 zE7RG`guUv^ua=(O{ast<^Ut3(fBw1W&z7x=m&$y1k70fl%x75?2RM7*LmKkgPBld< zv7A_vLgEc#d#>jTCNxe4We^+|9|iF{>KAKcjINWxO-k`hq@VWKqKID0nX zAlxDzXP?5YN42DDhMe6GMAj|FZE^{@IOjR$){^;1c1F6lI6;#5D_shh@UAzgX196! zeNWu``@a2uD5X3%n*I-08!Z5bq@6i)#vG0}_5IJ@^xCibiRaEe{U^%)w)=yb87`Z< z8O5AfE3O4Ys%Y-L-HHjgV#L4xHrtq$jg5*Us2GB3CFsgDjIs1l*1s(zFHOtFtmzaL z^%r>r;5tCZ!s=kYX@ogxM#3h2F*NFUPh679=|CgpyCtu?0r$^G5HCA(bMz}SA6JT;9E>e_DmejO~s-u}#sK3w5229j}!PuWK%GUn8 z?C!oq8Wq7b7^s!v^J9a7UORlR!{B19_$pnz*b&v$&7w!V&*J zhluaOYT$bqSPTxNnQJ5^!knU?x^~t1H#>H6|J0+e|0lOU^61Y=y#Lq^NgD-#!_z=w zF5mz4@A$_T-usJxVDj{b@2H;rwOM`!ch>)SvYq;$&P>FfPPZ>hi|?M7W!bh2Z`0|- zaReJ1Wn<%reV%PrSHh%BYrf%9)9!*n)g(Wu`nq9S^5)WSsYz!h24V|XeMk#679VQq z(!qzpAX`&vFk|a{INw*me5rkB3suZsBX8^-_Xq{|))2*;ay3n;dZ48sD`(49@|`bP)nz7Sr)@HDMEiFu-r*Jkk+?@rZC*n%oRo zzxgE}z31z{9@I7}_h{|&d@`OC-edFZ#UJp1Gw zWw|dZ#^8EkZmz*(jI=7eMt}@4UIhb}CTgOW{k^?1DLY1$XI;e+RP%Wi3O34YHZ9%8 zw9IM_{;F9<`L`-SAWY8~FsiDdhWa8jX#>D{f*H6MtV(?D1h_A1%S+d-Y2hOX9;fXnIV)&+Eu8D%RrTTqyY#!%(-bns5uaP2|NRk z3jmo$*W{lX#bM|&Pug!xrvm$ybsZKK#j)65l>NO$SSmrqQ82B(@i*gB=mmO1zBlc?_CSP*<8GjjST*L98Wr0AlL-8wlefll9Pi8-GmO z7D7$(uJ+$xBGC)`uDT1fj=X(KVI-pt>^|6uzxr0LaEJbUV_>xSroc?u*e7aurml!%bwz;{xnm-m=g zT*#NJ+epzebS8{)Y{kyA6Hs+XNXmhKS>(06tUVsU!oMUn&w^J8lyYsdoTk>VlQl-jnR~=MsiGmYdi=9=uEJy>KK#p0 z6)H8vNfWf2yvi2Gm4i4J&?(BvxY#%+stvn~w?s7w{5TpRou`OZmEn9< za%koAaUB>fPk@MKDGW4Nn&qT&2p4yud**YHesG zV-B(V6%?nkNpw*R5^Z_}(DO;y^=WL$S$28^=>oK{(L41?~(l-&$?`Hnmc#Vw(+twNa|yx%5y(|Gqa_bx&9qR4j}pud zzJOgJD1}&NBKrUG2AlS+ruHhOII8v*h_Yk#tKiVPQhl*0AyB>Hyl5nK*$!>PsNL9o zVf7~jFQJGW|E=16nnnd~6E)_aUH>hH9U1vqWf+)MO|cwE7Ic1Bf#(oC1|dIIo0EI_ zVjPKZ@zpeSJiB6R{}5RJri^#{s}b{U0grf08-+n-`NSqJzbokJt7xX_W?W6@AM|zWAKrS0 z4RZ`FgmQ^Uc+y_7#w&;NL5!Tyus85ut~s)#_P{r!At(y!%CmX%I$}%Q{to@pT3JAO zKuv*{pfcX^{4-wJUhz!uBeE}p0ZU0~f=-a7vIf4*CuX{`-NV^>z=q#6BRWDpzVlyr z7l&dF?#=Zu$(uu6I*`lO>4S)?WBT zRemM~#4_{YQ&-zIk4!A*?XC&TMb%j)#$#I6~ww-}24q)C(_aLos&kYf=)(i?B{M@A} zGyh8d_G|RoS$YHhrvUr#Uf-kcA)j2%aj|#6LFDd7Z-MFl7*`2uvo3bb?*9Z;pV01s zn=cgE=tAET@MMy%3D5~A#qphKMkHojyZtZ&olQ1ls4TxbpBpyXCe<8HHh@ap1$!o>>ZF%LV5TY$b4HT#}ywk4ljTHVU(AYpUZ|o@)bN7N$P~p~+AAD<3LztuEbm;zU9=h~drvA9}3V zt`j!?AjF*I&w;g~<-~7Ym%q!i?i_`Ka0Bmqyb@s|>%CA?_q~vuUny9us-f3iCHgEV zKF>ZP`4y>G`F%?V5eTPEwo#-xMV$AD|=J&e$`pey*O9si(SrQv>ToFKy-+m zDNMd8gl}hms=cOhpkrf) zB)%(KyX`*u6oi6^RO5;#ncGM8=-9dr1_ECf)SYf8^vy7P`d8qvHZySB-2C-SnF%}k3Y)zGatAs81V9Rx7UP+pKTJR$ zHJ4IKo2tSCbrq~S7|7nH%>8uo3WX?f3e_VuRqL)9l$GAah3Dc}z2??3s`|HRX?ItH z?&BeRG}KO`@pZw-OzB8|WC%AS1zze?&iO}*5WPTzJtwpa-FjucugJ97HJ?Jx26wA6 z9$SC_4#b|J5kI?~Pi}s$oA2HYB;g^p%}$^x#zS@um+V6$-druqWMf6*&cftRQSa47 z=7}lixtK|Sk3LRph3g%4#%3b#{$YgX%?)fK-2Y_UlS|A$Cv-rw!00(FgrUS)D>AhB zJdV^)aSW*{@`oQmAISpQGM#8C8Pde`w;bV^+m^sT9;R?>q}b8j6NV!83<7`qJ2~H- z&%bquw1hC?XG<}Xima``C^|0Ur;z1b2r*T+Lj`c(AJ*5qx`Zkb4%L``&m)eDmo+bxUhGW^ zo^p@|#!a;K>LUmI(!%;h%;18hhrZ?h`Q+Q_fyL`OAD-w_0>o$Uj|KT!R2O_k(AsWV zO~W3!y7TM}+E(wbxi3ZL|MMo2o_Dqp*eFqWgV-;-B(6{-msrZIkT2X0EP4C;*lZAF zgJG1?HPkQH3dHKiyhr+Z($ZgFTQ_EmOT}z9f&nzINw%&`r#7UY(7e)|v>#bXgGfAX z5BeArMJ`mCS`Lxo|5+X*stov93+|<0BPB2k=10!m(_|Wu{=nLjNdKAEw8W-3n#Z}f z#Ek*lj#Az53|%6x6y~$Spu8Y_uPKi`xx{->s9-($7q@2Y#I4GC?NB1A+{2SdjZN+y z_CQYHG+HZ-2AO+HLW)sv`DrV=bLjm(s#^GuA00 z1@p}acep8!q_rv9Vu3*P_p0e5y6F>WAJ_@u) zmQ#2s3}Wyao+z zp{l1sI;zHVxf&-ES~1j>6uDwTBhfBIJ)6$lcKhW9pDv1t@PE2Zb(%WYG@I7M>`4Z6 zIy2uHT9g@`pHsA7k1|u0qR_3uWwAv5QEYIH+THz6uCoQQ?UirX8BI>*0Fhc@=K~H} zOo`tLg#|Jzcsf|z`YSw#uA~%W#;gc_SPf*2D$u>J=25h2TYQngVdED&8LlhpiQyeA z>%yIxU>|U=Ec*qamXF%W5LhxZtSJlm4KaBRxa%J}6+n5WTAV>!SBWP0`yM4#cm7ju zUkoi3_Yc||!yDK92RsO^xS4*8R#|oOgGOg3I2fZ<1AL;f7)|nd z{EBKFvGM3shi2=^C~sgt5QY3U`r&h>=-*tifwu$@vsZOa&nVKTQyZZ;tY{NKv7s*G z0iopIPW_dU?BRyuN;tHB=M#W{oDxL=2oOUX_NgyPi5p!UF@lH_fku^B0YAgJpHFkV z)$qsqHp2RL2U9+I1v%-k#16T6u2F3Wy}K{ls|o9oYN#=d#G#0^piU&dD(6xc4l8@H z+1*W`IxTF*XBRSqNQA^g3O(+wb`?Xn)R3h-{F_uh(9jpo^!JRWdGv>XDSEa>u&QrB zRD^2$WodOR5A=Q}$8~d|0rnAHagO_}WopA|3m1zM0=uU&*-0bsZAl}iA8cnh#AuUA zmVDCs{oNtO_f%kdc?1jBgq#Ex-@s=z6(Fm*=x;cA`!t^615CRMAmILr`y3ZS9(n|> zns#Z4VX~Z7?Xy*r*&UW6H7SsZQF4-&C%h9KnF!weMkT1f{NF$kq`acJAHX7}J6j%v7gEj)!l!;1>) zOwRD6?0ME+KG^K)wXfWN6nr=<6ZRR*nC;NiZKpRXq}qH zK^D7(8B5sgvv~L_q+HxB%CR1&Q4@b)n`}fUKz1EAv|y>$c^|REndr45KIAd-Z@1h9 zjhsvb_|`lMJ&o4oXuEflP=9@I`;R^i68tzy>LVVCFze835puSnk#t(n$=U3INRfI;zFc*B(ZUb z@gH$BcwwFyw~3(!oxSGSn-jj(ydT^#wRO*perG0AZ6C#_P1sXTHJI?} zJ(GDQf9*#!B(o@$!Cuy^#f{&6srnM0pOBCRDuDvNkQ~-y)8`CTbhNQHp|vb(VbFN^ zXVz3(h0PP4lG;u<tJ3U;)#U(QPNu0-^}%n?wB*mm3fp zV`8Kwzzpotg$Y5dc!vxKu%A&)F`V`kC@P<|*1|Xz-~>06|He_9Hk?fD+DD1G)2>|D zoK5K`y1rV!bq;%9IFv zzR;!r;YW^bXS}3iRyqO+C-!rXq!4Xdm>S(}&A!Ie1h3Jct~UQA@YhnJWD}%V$Pwr_ z3+*r8NNT0%s8MtDmW^d$3lb1Ty=d%{W!*)n&%eQ6t6viWjWeJ7&I4Ye$7$#<#O?nr ze(F(3FPngu-W(y=Ou65G`D+zl9}O|MrC!N?E{8~FzEUmz`59-Omm#}@!I?=bPa_LO z#6`g)Fdr}ka~3HM3|e*L+M86}(eYM#wRfJEF@@X-p5_Ld z^jQ-YT~7sy$mBX!@YYw~l;w4<)|EaP31ZstgvM>2-mmP)vnN29)_Aa<|8<_kR1-o zT|9(2!A^$HTkZ+#bx*7NmQu0y$yDsG+R#FX+i&|M(S1}j`5XKas7SpTGywT{qU4gk zoU~pv-lG|(-4*j)MO%K>yCvEj`OUgFz5EP=2%$0i)MO`ke7j`XV)Wv+i@FDxrdb}aJAd}4Bi^iD>dd8q>6hI|5^iCektKq59 zr(l_!@umBuShKYiAB}+v-az=Uq|`e$!!tWRw>U!FcZPO<#8BOUgeoPQ^=e+nJQ>D+ z>H1%GCD&JI1~zp+c3A8=`=M6B7pC(JjWj25Vecmpb>|bO)9cgXvw!wbL2c*%7KA=6 z5a=GXs{OJ+X>`x&Z?{vFs((rxWn)!qm!VU%$F<<2@sV3r(E_M|e)C2X;z?bl$<<_f z{bv5B)G`53b29ZBJGXsnSs<|Y94GN(C{`qGI3;P(*%rblzGlCxFpBn!_*ou6BdU}H z6?BGsGUZ1}m^}JsjuahtY5rnOeW311bMO~5?zgM!rTaeZnoY5euc_4QRB0K zKS0B$j4aXnG@K1JyHx%RBBUZ3%rNR5++y{CW_&qGAH@B_91c5h|ESwelh3bCnBD6p zq-AHZ`YH4h7IzlU6#nq(-UQC+@z_E0+otxPRu{i9dtKghrS2a8PepzB9hyafriRPW zqU*!!?X%ZoEJ(_lxq1+6rkpjx?IqI0>?7SBMMvQyBeibpH~ zH>^ty$t$5D>@N6yf~imqyefoNtRaP<)KAPES;{x>+S5cX;Wc4aB&|bx=}mWqOa22x zRlxsKw0g3hiQ}W7yc#zFeF8Se_`5EaPDZj$uZ7~ZYZPtR#*G~I&r=({<(%l1iN+a| z!*Z{@u0P9fbn&qPAO32LHq{P}C>aJmTX z6VUgL*#CHX6i{>?|T3}ye>~(XYfLy;!RR;Ek$Gdd6lk<)Xz2Mp+V+Jb!yl%87R9fS5q6qqkC5kemFFwZ9*p|b zR1xsY5cl^}x^zukjw(Um?@(Ldkst5{lcHi%H4;^Id6PJ`s%?_G|~bt;b9B zAHMC9QzteE}IxRT*^(Jy&TtY?AVk(-L} zjVcp5Z3yyiPuACa<*XhgyEM5B*=5N|HFPN@EJ=x=ctx4>|ep5 zofYQ&@Z|qY4Xdrcco-DteO-{FHgq&dpGu9guo$SX;ysIGrHUG?qcvLSk0DSN14E)P zg%rZR3I9gmXmG~yg7<1Iza|M;9L@!X@B#G(^M=wk<~CW(0uZayHf89W?JmIqpNnhW zBwmqi|I#;v;nV3`Q?Cb^mZ!&;P_?iYTic9Q>z_OOP-X7r?f009#J1_a{2f#$zW z{hDy#w?oqHb&yC6YOpt|LofJt@bJQxSIq%td!*IJ2C7G0m9zY{ee~EE4Bp@H+~DNh z`ay=H%Dln9`@#1aTQa2E&3GB5ADaR}b$)ybcJ+l@tk1Y-@WFCtvC&9YSu8n6Oga4< zgC){GhTGwJ zfq|6G&v@4zC-LN_F=}MiaIRvJ?NvNE7QG`Eu!Y`n8aIXM;asz3dmeQtZYz;osyCzA z=NGC;K~7!+_sGMHgd*zpF`19l7t@c3rqe}%VSqM!Oac?$BTMo^aSz*rm256pqn_bS z5q0MU<;7!dexdJW1_s8EN6Ei9#rQ}bs!XEYu3W$d@H%%MAYRiKkjIq^;F1y0-(`vG zbLg$6Rz1XX3)gwddZ69~QnDLvjaY6%vSpTXht~rv&DE;JX3;Q}(TPrx6v2JXPkpH}b2fdOI`kCKzblP5dM`eg_q;I9wqD%gn#*XOq9cq{otnH)V zOxA3z?*O7@f=cn8E{-o@dFqxs#`{s8I`(&cr`=73>{Zq5pyf$R3t^GEm6oF>70S-1 zEu)qPi#XVLI2byW!&c~PKN&DA2p8-j)tlEMN>DrZKL&Nb6j?!Tupn+%!52Pf+kLKh zx2e;=z>rnWsKP<~rB~z`v9BXaj1)BGB`Ak94Ue0uQ{|61=H0LRA5a_J=K!{}ZeQrK z<9cywln(rykq~nAd(f9ZS+8=p0)o%*Vw3M(D!IqYP5k6@C3mtLZ)^*`8<=PzW#ji@ z>EqO)f8Z_{47eELw}}>BMMB0Wvb%m@U&UYMc=>ao&40R7brsYy-gEaGm1;y)_;;`# zIWaRSZbc;N=f4$?!q6B4>Lq3_CHEc%xEr4fkuY=P&0=vypTysfiJ;{!0PLkCL*T5a zP~4^C!m;ION6xWz2MGXt&dBNBxih+%2W;GLIxQ6#d8D>pR9W7?Uo&yYM=`cv-XSY- zs84daNbJ6L0zcn6y1-cwX$u!H%Si}XttoyKrLA^Omd4H@DZi!L6|`=UaFWvac!r7pR{E00 znt~!i>Ftnr2uE8Acwmz)8pN2Vyn-Q$uAgRkt78$dY2Uzy+$@NNg?WL_tZz4DG{3|v zl#a?V*+__DfOm;^_ijmJxZQSofH(L6mmz%X$qkLywqW0FYS0cKQS`?NEPRSLOK-P9 z^RpoWk^e*5(GpbAR-h^)A-4*IvwZeB-7#s;tsWxkKHo~u)%v6z3whd{-nbfSepx@m z>w4}iOo}=jO&=`~>F3U>jd5s%=cp@HivbksbZVi2;o{fq;`bTZaiGgd65z9lQSHrh zF;$w;N6)9-vAtDa|7MP21Mw3!@0p!DJ754CY0F|=y*vUsf;(_pqgod0xWPosY2aT1#HIC*n(w<>8>5X8% z*>qaKo=e^7lLCOVFw-GrFJWh`p4`#ZuR|QjXe?apy8cDrt768lMJJjtSW6WT zkMYBbL=c6PF}JDsu|8WOZFt3UoPItNTe+iToK(8uj5tVx-jAf-6Kd z`{o}|ld<)!WpX;eNR^msjw%H}ki5%t`Sb@&5uyFUv4LNyRv+0DR}%(L7bd6O0pRPd z{9?z)%1on|6{n3CO{b@1Lgp{N$osIvlii7(IF;Yq2P3z`9vkq~X9#-K;3iT1J2Ea+ zjmm5L;B}F`N)$E=(D%CTcUpN|NW4EY6!q6zZS^(HpJ2Bw<={+@8Dzbm$(=mVyYf}5 z&${IxwE|`4J#&tJVYngStF@X?#MuCizU39-zn02L+?6p-(nOeXlQHH^cmJYzZ9zx9 z1&xNeS~4*Cv|zrS2`%H~c+!P2YkvRqHopk+ps~#s&BsGPaEb@SeJy*x>(1ro`tZ@x zOO;&7{RSp*KItrzfmzi}ptAkSjeC-S+!&-O3bEtQY&241`|fGB5L{YCz}AEIT}#%# zNXLFItG-D4OkX#2L;LhEv7EbG3ESkHL+;JrK!oq59gCh>reYo0*p z5Zp4**X3^yycFU_KTo}8HTQ3?mt&)7+YHupo(F@$8&;54CnKM8r0$0?#T@T#@I!Lt zQ%dR-q}%`Kv@&YrET(RSt1zdXv8h@Hl^L|`N#wT)Mo{Ur6V&B13ul_V4c3}tp~LMB z-|~|ColDDSn&rQ%7ITr-f+r#SF>k&BYjP$OsmAF)g_5DN(VIkOEHa*YiZ*XDga)$r zCUa2*ZekHOwj>E>T!twt!KAFn0VD+v)B{6W)Go$|opx8CG^~u@;z-nwcWx^0+>6Y) z5pz>4anWK|Ue7p{1=&gRkNrhPa}S>oP|B`HkH+K8#rT05S3OJh8P|5s(N@h_Q}9(O zrS3m-pvZsM>$hR#Icb#Qu)KLs$ZH@uHSF352s>VW-RA+EI309+VTuzsEW2~a9yPHy zdusRx@n`iLWvni0VA#3K5PKT-+N|0bUW1DS! zAm&Nl^5CweSKq{cH8P`^Xzu-turDDYMkiC3FdZ=pzP?@V;^_QAOUH)IeUm+bbM zrpQ|Mg}fe@rU>=;$;TJ43M>_5i&MIi@TuSks7Qg9xeGn`Rq~#i+p?_KeA5W){?FXU z<#Z))0?Eqm*pg}%fBd7e{&>CY*N}8=&T3Jee)E_#Or4W7+aTUF z{S{t;t_6WBkn-|yzfDvmxwx*fb9p_|U-%!6B~p7Y7RG@N6V6x+2MVY=*qRz|L(|dcNexsI9#bfx3@AZm={cw^^U5IIi!l;?TSsmPC0G39<3Ii>%-m5oG$uW zQzxf^o{*R1D0l#yRX997W8I@*3eD?W<9l@cSD$-Q1!8yzVeNC&`)&-RWzS9Je~g!u zNiO^nIY(A{XuE}`#h~} z;Wqq7j&bnNzG>ZmZp9)lyJJJMga-hpmWRaod4Qj__wzuw%7*v-u#@kD8Fl*+7!62) z4%={-0=inxWwXYm@|{m>@64|ZLB0M5n_MXDfu@*_>l+)L58LY0?XagHLTxlIOI@x;kT|b$1foNGs~+rqbsj@9p8NC@ETW9CdhO5 z@80=HZT)<{T+T`CukSA_G0kG2=U5-{2Ct{=i<}LYo8d2ztIbw&$CfQqnj6n0TWHs} zuzrfQm9E_XLFeGgPi758N!Gm$xI(hU0vf?2ePI&$XI4)GFKD8t(c_BTMaN}i+C@ySAg=xg$il`}U-3s=_(YO z8m>cAP-Mj-M-2JhctUFMy}QMYYfH-adk6q4Qa&op$`IwrG2ZkqB#B2uzPuuJ5C^I? zu#ngB^XbBoigIkWuW6gt?#{4N$C-NZx&L8eG1d3IVfgslS4mQvLKSkV_1Niiy6_>P zCr^j1bg{s#+M~}DOW&JR()8Q%Qkt`gmQb=6X0a7!!Ih7nOQUk0|CVM)O>eZcbb!{j zuWvjr41|29!L9Yplhyj7o!#9a7k{VwU2~xL?8Qa)spoxc>ce?LT~?~ix1{5^oXna4 z%m~NN9>Lwzg_yh*0tA#Hq@gkE{z{YY0y=JQgD@1M#TsP$iR9(U-`A%q%1$D0z9~06 zN5wrcKWntd3PPk{%^5nek;rO!-lkBLz<0BmGX3b!@> z_q@AD4_Np0*eTKjE@rn~m9AbT=KK-z+dhrdbL{>;4vUjW6xaT|`q7zw=yBoi(@G+J zJ@s0u{y&W9Bqe(&lA&3Eo_nTxiqLnZ1aRXle}cK1HYd&dOat9dpz}Fxu@(s zPfzEu z7)iO(MU`FrTwvBx3<>hGb_^KAZR zBU?Qxo1HmjO=OtFM?{Xq$44&pW+NYF+EuI1VFl_=OWt$IH<|)G4!YDEO#sGCyLi4$ z0rsPK*`prZ{%B$cnURW8b$Xz1wEn;zpZf;dj*x2ZX|N@Vcz!csSWtVCxJW8Rdh<0x zo(CoG&WFJqh1rt07qg;%^p?hxj;xHaAV;Y@+45VP<5#AQ-zRM{kV#@#L#9gcJd8Je zirJvq=r!rc-ibH!OxY?^WFx^$+fpvhD2r0uy-LL6R^RjX@2cO&_L^F+Q#HWa`f)5( zx65u=<_GF-SQ9${?03Hh_bi!NY7VY7dfmrYeo=Bv`kG0-mIpU5;cn^&YsK3%&}ond zy}o*LVEkzdc%!s8&GE9ZFq+w3Xg^i0FI>0M>gIMax3_b1mQ5ISHqPw9^-D|u((3E` zH8d>y%*>*;xwd;HGx@=#N$1od<-xR`<0;0kt9$i+JjQQs`Vls03AxWKG@APS&5%yW^B!7i_B3`lmZ~zbHlt_ ziRs2saN~p-9*^wv6opdAnWse?#B;+Ba0VSOm{4ryxoBI3Gx^R{1mjM>#8GT)h`PYh z(c9O`<9T4}_Anv)sFqMKb6CZ2Rp>uoySo2u?Ywsvpr^b0(64gi7o#~|&)+(X!wi@2 zt{x|Mq$Um4N(ST3RN-nvq-!fxHx-BC*x{SeMI92U{Z@Zy^xf=2qVjTi*5JoV*Z2Rl zp!e+$rk)?uQzKz&YreqrY39V$ii>gkrRm4jy5{NOl=@ZsZ-TL0f?w>X0Erm=_Oab0 z*UeZsk^avWO)0hQ3SCXJ{NhcupvQkeBiJ;pEJE}4Wm5}_Q-)|*_uSXsy|=wTudR(X z=Z}Rs#^}9|zNWI2xHzOW0H7fztf;QYsR;`J0FeKFfnfi9gk%*ZE1&=X5WJx{vcc2K zNdcRlqa$n-G{I}nRB45ALxVQ+Ild#YQ!DJc`+y9ZoI{ImcOI3WJ-M=k5!2U+8_g(W zpE-ojxX;TviG#LwcbM6|_PBiu@~rcJJ}4W{U@T3k-T zX$dq!>`?^7KICE};V8tW>jzK`9-&aw#FIj2kt>1~QWlAsg^ZGBR&d1~Ww6R-58&l& z)IzZF;euoG{E2&{c#?2Zki`j+5Oea}K@aFbfv#!nM4w{c62BKgVV<&)zudmp^2_wk z1_}hcbNL;=ykF;a&-rHi1bp6K2Ve`ucU%X&P=`eJPd$!sI!_3hqe9^^XX4qL@;fz< z$4a2$*}~i>g@(}g`jT-j|6n9s=UhRBG~G9pVZ>XxozgY5;3W;hf53vgXthABW64A5 zLG))u;`utoT@us|4?~{pbC%4jqnIP1i?u{dlXwee`;lf?Aj(pwI|o&j@f!d~869z$ z+-8LE%T!ZRiH@#x0mg=gyss~M&(Q1FcTE#&7z}N6uyJ~~p3HL^5Dfk5{h|5~f~oaE z9N#WmRJhy2PX`mtc;Ht{2-_<#08R5JFhGEE`0nGCnPKT9J%YMkQD9?RF8uKAiWO<; zLPb6(j7JpG82KD+FZ)RmgIhAR+0P-$W1;w8dlS5aq1e!3$ZjL}C(xiF5Oa#b0HRb7Ij# z+ER3h?j&fED%9X}TE9U~8MFj5V~L2WDIkltT>+iA+4qejkFjWgd}A z2q%EyPxqnuQs0Q59F%h-3&C@Q4Si8IOl1i5erB+|C~$GnU9)5aqPi$%g3YSK zXj8DNT~E;VPE$|Q#(*qS)XsnaXG#YLC!p?+F%fHKI6DyuVR#z5;!%}{QopPzM(U0w zQ{LT?0ttJ&o zbR%iizzW=wNpp}GHg3<{#GYoJ7Uq&=6*MhdOs{j?-wT;TX#R0xsM_9$5X;svBpyyS z;yU)KE7`}aYq%$;yY@tg;jOKkyN6Nt;We7Hz{T*tcmw`(5bjeEHY@QxMQ#}dP`a(e zl;OQ@!{FT&{6={7R7LPoj~36xp%=-`izVg|>m8|%!gTgAh0`J%6jqPcOnRK7Hl3tG zZ*V>nO;h-pNKYa)B3_QtphF@`=7X_I)Zl<(MA+bia+HJ`O?8~eT``A1T~dLHKwVBV zDygRyr!VO=`7=y)C04C?!hDLdcE>z`;8$4 zf{y}(=Qab>{h}yfWmVz44p3;N32B98L#!PJk@Ve2tyqLE?gU{;Vl7w<6`@yIiba!Q z7LqR06+lZFJ^UuMSr=YttSLo}k*2Z`C6S34q{Cif-gyBZP1dhLn^cRJA>reg*g#!K}(SIEOJ3RXT zI`?1a_>X_!wf$cXCt^1S0B)Q>b3kYS-~e!bYV;@(;sRpiD0C_;(9qv40rzYc_a_AX zV`pS=Nxntd>qz#%x#On(fUP(2_0??RPhM;MgqMKSkD7bTcz=s$fh&XhybYlBd-7}Y z6)?NI(trL7#{6+BZ|)x9E?~ZUHy{y63Y7WS1_DbBUOF}XLxJgE63@UBuos70gY$qz zflgrC*DMeStX*_?t99C53^)ZYJi&cWTm!p+YzFt;j(m&2`bSV83J|v{Z-(U= zcX9Yj<7@i~*tIPNobO%-UIC5Xfe&aO)9=Hd!1aXBZ6d;Vz8ZnRfa^!V!|;v5m%s&| z^M}kw_w$$47vbCWaW@lpuAjuqmW7|4zeg_crUT`p_L=Z)E8ieI010UHS-Ze;2t0Oe z{T%-6zVq+(Z}?>R5_loJ%bgVnI@Y+$%?DO}B?iRYSiX0^6B;7z=A8#z`_KU8fEpjA z?*i-H7TejtXyE8qz^B8d!wTlz^qIk3K##wc?_9ve=j$u!d&Gs-+da_d^ZGiU>wC_^ z0{GaOhl(N^ZTY60plnU48yg67+y6Rk!^|}kHs6*Rn4u?sW({iQ##3Gfle%zKk`GSR zL`W#Sr{aE15Hl{x?hZF?kN;Fyw1X)vuDkmXf;!A|ZEu(0pM8}~GNAk{Te_&Ck@TU~ zfn>d27=@|w-)F?AaP4sTs>VusDuduw%B0PfE9MLc+zDX63oNI%;#@WWc3jti!XvPOnyBa)?pW%mn3i78Z3WOUrx<_xB0D=HRNlk37W2e&Q?# zAWdZ)4EibZ7Jh>v3{kQK#a+iJeC7=v{IZdx4ZyW}g}l6ovsWAe!$^!&BdRRofk^G1 z6}nYq-8qEAWX!}-&3y5r=)z5M<4LBHjPKY1%Y1%|EkYg8a0)AygYkzu1guIwm5gn?V?P1EFS)p`5kA1wT$BzlF z(fAOkh&?W$DDXZf~5Qnro6_eG4t^^ zBFMn3BT35|)&Jn2uT{S67qPyAKO06*(Pa&Pmj+CoSsjlP43Y{6CD`?Oa7IHtgAC+w zjg<5E*{l#W-qmFj1s|Foxo^N*jg$RN&V8QfcRRDF!_FKVy(7yX-hzC=OrdDAr@acG zdB_I~4l&4NT{A?=)QbEI)yB+ZxhRungwcMLA&qOW3a#G|{mz%3C61{|J9g!qq?pZy zN(CS(j4#$&A~KV!WbkBhPu<}qJ#v;{w2z-_d|7p(=+a@vmSh3|ew*27R~tCIAJaIy zv7w*M9!)$|-m!`|xScY8z`6^wvgjbdCVF~Y>eze^a} ziCj~}KPFf|%^I~Kn(*szVOqORRM;O8-x5Um{NV@CgKWqX?CvlpZHTT8DXnfs4$03j zIK8MpdiM72P}L+)PAOxnMe6aCmnLf#M5%)~VE@eNASZU(@uu9xW3oxwQ5Tqc-Xrv1 z{iig2wp0e=+M%>8+q&}}I{9ns9jRsd+OQI<@sg*aO@xav9zu|r3$SAvBQ^#3d&RBF zwDC+iBwEk^DHa(VD3VqE2leH1VUQ3XX@fge^DE7%p3q_M+q8lVdL+q-7mDyyiY{>O z!xO!)nD7ghAxjqXRRTMH{?DOnLB}6Z1HAMMA2M=Hrh_m%@10(w2gp1~Tdxo3Sc|SYxRs6``Pm&Z*(w&$= zFJHt1#4yvB&=TB0?pB)<_~Ca~+;h-{BwEB!)1ASvkZ3MJ-LAn0FTsAdi$C%_Zw}EZ zk|6!SH7WrSSw~;-P0s}va(*;t!9*+Y0c|Q5J7yMK^X&#=~$aujfi^`Zk!K+yvl6?YDR(Kh+3KcSI zUBk8tQvxhxRx4Jfsie<$A2Ofx%=L*~uNW0m#LXE5+e(Tir-R{<#WmOeIF{2&Hnnm} z{A~N5Fb*1n+~*DpwjP>x_cz2+6P%WtCMTznn`YGi4c-(egUzO(B>G2F#Qgq$5(51f;Ry+fmFZu^hlZBszcibW>+JcF=$7X115`az&PIMV z@bx7!n^bKO`olmvsU=Id_>nU3w6$L~KJb@dT3!P3ognx@53X{BQ+a1j8CF|q{Y`k* zdmH<+^rm~>n%*cD!)=asHWAu3`cT2jb%xKUPD4`8J9fG0i*!R?**5IH#=4CIyysgd zSwgCE;09;{<+S3G8!CBFwQ2qOR}Q^W7j7fTNww}E&&y5IRO!AA!WK8z(hnjcI@DyJSp?Rg?SobNQxBbV4R4xd1>+esPGMAM$()I{XXIQD@|d zw%?ksK_-SsxU>B7dWjNjFRT&Sw z`3Hab(Xzv&7~p}Ar;O_|#nF)V-0o%Ejp3jXuo+_+`aNepjxI2+@SdodjXX&wU*N>fJggJBPK>Lsm|7MHUb)&OelPmPJIw9=NyEI|^0q zZi0dX1~50eMU9Wh-iW2#^3%-zlgR1B-;{cW3Spsm4s%nox3-3{ znG80`9#?w|2;WoM(7HoSOd}Gj>^bjZ5HfZ2O5F^XeB*JI8D@%QChGp)osUY} zaIM9Ih8|wHKMDWRzIUM}2uOY_NJwE~9agQNRZI@PHk8nIwaJN=1sItPzv$kX2SxT( z3v!KZCCO%qNr0%78u`OPu1JK(T*x4v19P!=3X5 zuj-OEEyssRveAlJVPn7$=g%N@(tIJ|<=U*|)z2LPL82OOcvY6#%_rQsWg)R@cY|*T z-ObPVe+ySVv2PzgWW4_!?sBN7NE-lHW_@CzLgYsN0}nn4EY|iX5B(;p`v^mA_SHSE z{s|KrJSj@eyw~(A*}*S}f{)phUe8=lAn^PjjV3;{A}oYkYm=hMGT)kzs`p2RayFeb zuooR>Lx4(Yif3o8uvGm>D0$9t=$|5IqwHNs_G)$Oao|F^*QX#e*JHtkl{##a+F=`9%t1}>%*wexVDY61N z=gLds#Fz}n;0~+ah4=SJo@3jnv4Zh+1c;MDl@tT{5fMY|L?M*?=()gaz_gtg@yUco=rMWI-q18H0z8WKs6MUm#Lln$X&`ibFL{;!7J7zcJKM6r?8 zG=49G64p=Q2gA{NT0;No49aKO(Q~iwCEboSGx1{|ttMoQo%kz_>7NaN515O8EIBxV zKmXK9*-Uxs!oE(bJ4^keo$HAMo%)SCP|(s(2g6%7|MF|;ZM5wB*jEPCw)Q5@P)iTQ zs8#eu*H1GtzlU7!@HPuK`rCHk3yXXcpqKPuf+WJ?m+ZRpfJ@P%Hhfpr{+sf)>}?r9ZA3@BbY1+VLWFaoT{;r1GkK zEl^*ZPi0*wyM>KdlPN`HPtWs-)_8LWRxa&n9oa^2jCG|NYe1k#~GG;aRonAXC z2LnB>+Pvx#mJ6;JLs|Q;M3k)mL`nnmtTR8l=vb^1cbPzlkjFYrOYckT%5MTRtkCUxS(i)#ai%ed$F?5n2L`|971;vgsQ1tyGX!v*8- z>An%R+nJLzJfXvWf|AHIKVJ<-?&n|U#*5cqGw!&Hg-_?oH&`!6z9El-AALQej0q!q z6@4#t@>XByjjUbsG3l;h$gTo(EiA(_)>*p$@>kB=Q)@X!0iBQ19rWnYvrD|r$XS}F z$fwYxylm`DI5V%f-LD0{FxCzpSF0ZW3pYZeKb>697FW<)2xbzzpX=SoWXex&w9jz- zg;<7~(R@cRO{q?QY+6}{@PN4A2UBM5;tIigKl#tXl^VGJOa|qXHIZ*;suoj;G`ixR z==gifHmWiADg3jA9Lqq%gZl-qa5g{3edr!2RVBxLahiJX%D43|<+l_NONHC^JeGcT zfJyRw3?Qa_A6Z(weo5`@P14ZBte5f3_EObj!!5&8&h(_?O(lK%>lE^cbzia>#Z}VD zhuP)(haw)KAxx9T`43ypljzpz7Fd6?tczp9gyB8=zf`xjnQLM_>_HRoPN|XccwKup3|8eyWCpqxkC|v{P-5ZC7HI zr#8qQrk*_OT?d|9Nj!l7*8h2qzv4Nq`$C5ExX!8!x`!OMV>V-(qZWI`z39Wlr!eh` z%)fj}vLzP1)6i@ed~l8!b^Q9E=y z8eR@&#jFSdlnpX-mf&Wh2>yJUbkvfo8(<@HR}L^R4%Zq6_gzkC&hKY8C)sHzz;}&G z(7F(4L*0gnJ~be^+Sx2s5E)HEzgSc#C;HFE^#8*-nOjQPe2mZ~|F$Ae_W2YrmuD9N zRsuLxHGW!tPB}lW>h{N!_9;tcav(v!o3&1?J-i}12|1;-aw0d{9CMi^TKq?IPa$8U zd@^ZWHuZ1W{nLp56xB7<4lR2g8a17(7PB}j*rz>KE3rsogzWK`0d!y@!dCeGRX^or z0BvzjwpK;pl{n@7Kl1+62-Ul)6ZyBgtvGK%T#jC?frv%zZ1|oimgaE-@g(Z}=K0*a zUOIfIgnDH0N?Q&k59Ldv!gF9kTg>SOZ z4^)CoJLy8g8UCoonKGfovHVDEswt>-p!?EPV7OeLz{xYlCLnXiHBBN2E zBrHjhykT&7_tdw2n{%{90~nDb(!T#ctPLz`wujY$TvEhn?(M=eeF-SQI_7^G9Nus( z|4s57Zhf-=#$vvvMIBXM0|Eqfp&KVP?o2kbldyj@02e!cxHirEe>o^Jfi7^%D7XDA z9p}-Z(boSW?ti%@6dM0p9%B9)(0Vdr3>6iBrTPEMY}u5A$-fSpHZnN{6AlA<*z1` zUOj%tiLLpzk?4c;aFYe4{P=A;Mcg65S8}*PnJc zVVgCa6leokBt*)5&NciEhk4&whZfs^!gxy0A$2mBc+e=^rW9@{0&@UBFic#jx>Q9V z)E?y1O74IhAJ2|UZr6sufmKOz%viR<;ZV9LiV(;&wo%W&83O4|Pal9o`>v#*G-lgu;3QG~skJ-(drQ-k(z4?@eJn}rB7+`GQoEUFpz)gYh* zo+vr+^6P(=UuG7QL24@7eh4IQAZKr;P9?F2a|vdy1~yt!U;zNw4H}VS)|8WV`4@b< zP|Ep$(-;#%!Y=az6UiSyQ{|Z&(DEPKx0CrBuZI4h-0RAsqOpdwm%kSf7XRG-ik%*T zFa&oeObvhpk7|IpWS1#dfKN!fqv0`rBoC*@g!Y*s5)1+bhejq64E%Db_%(JJp~~A~ zEZ<>KxWbd54-8%Vy)fjAk|Aqkat0Z*BXVOx2P&rtUt|#&TDXHEmS%rtoWzF*P@7GT+q|p|DtO=9R4^)m@c@k z2c!g*_-)=6zn)(@I5JO7etOG6VRbT9<`B)aW^S!4+BdICoPZT|kT$O6Qt6oCCYp^S z{6H*xt7>sV6e%Lq;!Y57=XqY65~wAhQ$u?-Rwkq$Pr!h(0jZWZ{1t0Y+^SqG091=F z$%i%=>i+}r>pAX_1>$uTj%>Rv3cu39t-g^gqqFdK>}V-?J$l}XSHX7nW&*~Y^!|{ z!e<`Y;Avp43?y@J=cQj=psl%K8(V<6zz~o^&JSjQ?@Q(^Hfs5o?hIj>0BfA}~LwlhhTMf#hF z82mRVTNHoW>8i5xlvK6)Y-X0esMg?j9< z-k?^YkZL5UFG=zF&|g0<-lR}r$k%lspvQKZj;KN|ExIKSJ%r`iVq?GH?eKsdc$8Si znBa9kw)l+zH*i6(*qll?lcG^KPZNUILk-RtQvpQDLs7G>Ft7U$pU5q&nC-Sj478 zATi;u(qDE-t*52^oN|{3ZU9VPItc>_nVLg=JTLFQ9tNe~otYqb^8JhNeRWvUXE(+Phm8y*}-@LgLQg)a$RPMGfp(1Y(0w(5c$}q-?OWyH2phnwQyBcD( z?wig)95FPK3D%#H@CivBG9~jZ3cnvQZq-f$L}r_MUwpq7g47)HR*{FPsqM7S9~zk2cD$Ql{hwQKo-W44;m za8S0}wNcQ&O%JkOYUS?9u)ybH3inV}w*B7ok+t}3UK82xvZ|hyF(Nrzm^HuU3StHZ zBeb>$Mr>?_ikwmGPQoupB^`g-_i3V}HA9tNUw^PQDI^)@C?&1-(wQfl(OXilS?wEGrmAm!g zBeewfz2Oq*gc<5IrT|^_f2QiKL-d)vO`YM@z%<3}_{9ToqE7ikLYXdvikwB@4Wf6h#j+fa*h3ta3SP+s3 zTSNg!<|A^ksknSiJY#9oASA8Hcyq_*-+M-D=)NJ1Bu@IJ;p8u{%Zr zuBwaJ`;D+kgq;yuH`p>DR3EMdPG{XG%BSZRqIdS^aD-w`jJkOzeU$S*Fc-HH!M%S_ zC6r6ku4OHefnx6SNq1N{A*>&NY)?1%FhUB!ED=3iJ1vb*gZOL;HbaxKT>63W*vr|6 z^7Gve>qc1~Y~41^u9_17HFC8|LcDe;unH|$P@|^-8J<(t($0Y2nbH?jlAK?P&Hv~f zvU2wyI|lhBRK)4xHY(lGu~rRKj<7D7oKlxBB`l3fneaT)UV15w<`u6fF!}rAdTS}# zWrH1p1`Q7G7I^STER1sCQWs%d9`Mq)bi#j7b@PQL(Ep+tU=|C7{;N)3@luXDVTF}C zGX|QN7l=SAMxXkC&NW1wQL|S|t z7Yi(hJO{w=9-;38tFU>gc-_TdA+WP$8$_lHG~E~C`$M|ReK?vinN0{blr&a0nOR2C z99y)Qby+#lo6>jVVNteBlolJi6FYV2d3F)oArywU0E=rPcCfAEp$20*0AcPzv)&

C9hMPynh3^`-_v~OSfLCnN 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 W000000000000000000000001#m6u!q literal 0 HcmV?d00001 diff --git a/server-rs/crates/api-server/src/character_visual_assets.rs b/server-rs/crates/api-server/src/character_visual_assets.rs index 5aa1028d..08adcaa2 100644 --- a/server-rs/crates/api-server/src/character_visual_assets.rs +++ b/server-rs/crates/api-server/src/character_visual_assets.rs @@ -94,13 +94,11 @@ pub async fn generate_character_visual( .map_err(|error| character_visual_error_response(&request_context, error))?; let result = async { - let settings = require_openai_image_settings(&state)? - .with_external_api_audit_context( - &request_context, - Some(owner_user_id.clone()), - Some(character_id.clone()), - ) - ; + let settings = require_openai_image_settings(&state)?.with_external_api_audit_context( + &request_context, + Some(owner_user_id.clone()), + Some(character_id.clone()), + ); let http_client = build_openai_image_http_client(&settings)?; state @@ -324,10 +322,8 @@ pub(crate) async fn generate_character_primary_visual_for_profile( &model, &prompt, )?; - let settings = require_openai_image_settings(state)?.with_external_api_audit_actor( - Some(owner_user_id.to_string()), - Some(character_id.clone()), - ); + let settings = require_openai_image_settings(state)? + .with_external_api_audit_actor(Some(owner_user_id.to_string()), Some(character_id.clone())); let http_client = build_openai_image_http_client(&settings)?; state .ai_task_service() diff --git a/server-rs/crates/api-server/src/creation_entry_config.rs b/server-rs/crates/api-server/src/creation_entry_config.rs index ab0e06fc..27a3b127 100644 --- a/server-rs/crates/api-server/src/creation_entry_config.rs +++ b/server-rs/crates/api-server/src/creation_entry_config.rs @@ -255,6 +255,29 @@ mod tests { ); } + #[test] + fn test_creation_entry_config_response_updates_jump_hop_metadata() { + let config = test_creation_entry_config_response(); + let jump_hop = config + .creation_types + .iter() + .find(|item| item.id == "jump-hop") + .expect("test creation entry config should include jump-hop"); + + assert_eq!(jump_hop.title, "\u{8df3}\u{4e00}\u{8df3}"); + assert!(jump_hop.visible); + assert!(jump_hop.open); + assert_eq!(jump_hop.badge, "\u{53ef}\u{521b}\u{5efa}"); + assert_eq!( + jump_hop.subtitle, + "\u{4e3b}\u{9898}\u{9a71}\u{52a8}\u{5e73}\u{53f0}\u{8df3}\u{8dc3}" + ); + assert_eq!( + jump_hop.image_src, + "/creation-type-references/jump-hop.webp" + ); + } + #[test] fn test_creation_entry_config_response_keeps_baby_object_match_visible() { let config = test_creation_entry_config_response(); diff --git a/server-rs/crates/api-server/src/custom_world_ai.rs b/server-rs/crates/api-server/src/custom_world_ai.rs index 0aa81311..932f5099 100644 --- a/server-rs/crates/api-server/src/custom_world_ai.rs +++ b/server-rs/crates/api-server/src/custom_world_ai.rs @@ -553,12 +553,11 @@ pub async fn generate_custom_world_scene_image( "scene_image", asset_id.as_str(), async { - let settings = require_openai_image_settings(&state)? - .with_external_api_audit_context( - &request_context, - Some(owner_user_id.to_string()), - normalized.profile_id.clone(), - ); + let settings = require_openai_image_settings(&state)?.with_external_api_audit_context( + &request_context, + Some(owner_user_id.to_string()), + normalized.profile_id.clone(), + ); let http_client = build_openai_image_http_client(&settings)?; let reference_image = if let Some(reference_image_src) = normalized.reference_image_src.as_deref() { diff --git a/server-rs/crates/api-server/src/jump_hop.rs b/server-rs/crates/api-server/src/jump_hop.rs index 910c18f2..6a80629b 100644 --- a/server-rs/crates/api-server/src/jump_hop.rs +++ b/server-rs/crates/api-server/src/jump_hop.rs @@ -13,15 +13,15 @@ use serde_json::{Value, json}; use shared_contracts::jump_hop::{ JumpHopActionRequest, JumpHopActionType, JumpHopCharacterAsset, JumpHopDraftResponse, JumpHopGalleryDetailResponse, JumpHopGenerationStatus, JumpHopJumpRequest, JumpHopJumpResponse, - JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopSessionResponse, - JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopTileAsset, JumpHopTileType, - JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, JumpHopWorksResponse, - JumpHopWorkspaceCreateRequest, + JumpHopLeaderboardResponse, JumpHopRestartRunRequest, JumpHopRunResponse, + JumpHopSessionResponse, JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, + JumpHopTileAsset, JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, + JumpHopWorksResponse, JumpHopWorkspaceCreateRequest, }; use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros}; use spacetime_client::SpacetimeClientError; use std::{ - collections::BTreeMap, + collections::{BTreeMap, VecDeque}, time::{SystemTime, UNIX_EPOCH}, }; @@ -46,8 +46,7 @@ use crate::{ work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success}, }; -const JUMP_HOP_TILE_ITEM_NAMES: [&str; 6] = - ["start", "normal", "target", "finish", "bonus", "accent"]; +const JUMP_HOP_TILE_ITEM_COUNT: usize = 25; const JUMP_HOP_PROVIDER: &str = "jump-hop"; const JUMP_HOP_CREATION_PROVIDER: &str = "jump-hop-creation"; @@ -55,8 +54,8 @@ const JUMP_HOP_RUNTIME_PROVIDER: &str = "jump-hop-runtime"; const JUMP_HOP_TEMPLATE_ID: &str = "jump-hop"; const JUMP_HOP_TEMPLATE_NAME: &str = "跳一跳"; const JUMP_HOP_RUNTIME_RUNS_ROUTE: &str = "/api/runtime/jump-hop/runs"; -const JUMP_HOP_TILE_ATLAS_ROWS: u32 = 2; -const JUMP_HOP_TILE_ATLAS_COLS: u32 = 3; +const JUMP_HOP_TILE_ATLAS_ROWS: u32 = 5; +const JUMP_HOP_TILE_ATLAS_COLS: u32 = 5; #[derive(Clone, Debug, PartialEq, Eq)] struct JumpHopTileAtlasSlice { @@ -239,6 +238,35 @@ pub async fn get_jump_hop_runtime_work( )) } +pub async fn get_jump_hop_leaderboard( + State(state): State, + Path(profile_id): Path, + Extension(request_context): Extension, + Extension(principal): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &profile_id, "profileId")?; + let leaderboard = state + .spacetime_client() + .get_jump_hop_leaderboard(profile_id, principal.subject().to_string()) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_RUNTIME_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + JumpHopLeaderboardResponse { + profile_id: leaderboard.profile_id, + items: leaderboard.items, + viewer_best: leaderboard.viewer_best, + }, + )) +} + pub async fn start_jump_hop_run( State(state): State, Extension(request_context): Extension, @@ -247,6 +275,7 @@ pub async fn start_jump_hop_run( ) -> Result, Response> { let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?; ensure_non_empty(&request_context, &payload.profile_id, "profileId")?; + let is_draft_runtime = payload.runtime_mode.as_deref() == Some("draft"); let owner_user_id = principal.subject().to_string(); let principal_kind = principal.kind().as_str(); let run = state @@ -261,23 +290,25 @@ pub async fn start_jump_hop_run( ) })?; - record_work_play_start_after_success( - &state, - &request_context, - build_jump_hop_work_play_tracking_draft( - &principal, - run.profile_id.clone(), - JUMP_HOP_RUNTIME_RUNS_ROUTE, + if !is_draft_runtime { + record_work_play_start_after_success( + &state, + &request_context, + build_jump_hop_work_play_tracking_draft( + &principal, + run.profile_id.clone(), + JUMP_HOP_RUNTIME_RUNS_ROUTE, + ) + .owner_user_id(run.owner_user_id.clone()) + .run_id(run.run_id.clone()) + .profile_id(run.profile_id.clone()) + .extra(json!({ + "runStatus": run.status, + "principalKind": principal_kind, + })), ) - .owner_user_id(run.owner_user_id.clone()) - .run_id(run.run_id.clone()) - .profile_id(run.profile_id.clone()) - .extra(json!({ - "runStatus": run.status, - "principalKind": principal_kind, - })), - ) - .await; + .await; + } Ok(json_success_body( Some(&request_context), @@ -391,15 +422,17 @@ async fn maybe_generate_jump_hop_assets( owner_user_id: &str, payload: &mut JumpHopActionRequest, ) -> Result<(), Response> { - if !matches!(payload.action_type, JumpHopActionType::CompileDraft) { + if !matches!( + payload.action_type, + JumpHopActionType::CompileDraft | JumpHopActionType::RegenerateTiles + ) { return Ok(()); } - if payload.character_asset.is_some() - && payload.tile_atlas_asset.is_some() + if payload.tile_atlas_asset.is_some() && payload .tile_assets .as_ref() - .is_some_and(|assets| !assets.is_empty()) + .is_some_and(|assets| assets.len() >= JUMP_HOP_TILE_ITEM_COUNT) { return Ok(()); } @@ -414,12 +447,11 @@ async fn maybe_generate_jump_hop_assets( let settings = require_openai_image_settings(state) .map(|settings| { - settings - .with_external_api_audit_context( - request_context, - Some(owner_user_id.to_string()), - Some(profile_id.clone()), - ) + settings.with_external_api_audit_context( + request_context, + Some(owner_user_id.to_string()), + Some(profile_id.clone()), + ) }) .map_err(|error| { jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error) @@ -428,58 +460,19 @@ async fn maybe_generate_jump_hop_assets( jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error) })?; - let character_prompt = payload - .character_prompt + let theme_text = payload + .theme_text .as_deref() - .unwrap_or("俯视角å¯çˆ±ä¸»è§’ï¼Œé€æ˜ŽèƒŒæ™¯"); - let tile_prompt = payload.tile_prompt.as_deref().unwrap_or("ç­‰è·ç«‹ä½“地å—图集"); + .or(payload.work_title.as_deref()) + .unwrap_or("跳一跳"); + let tile_prompt = payload.tile_prompt.as_deref().unwrap_or(theme_text); - let character_generated = create_openai_image_generation( - &http_client, - &settings, - character_prompt, - Some("文字ã€Logoã€æ°´å°ã€æŒ‰é’®ã€UI å­—ã€ä½Žæ¸…晰度ã€ç•¸å½¢è‚¢ä½“ã€å¤šä½™è§’色ã€è£åˆ‡ä¸»ä½“"), - "1024*1024", - 1, - &[], - "跳一跳角色资产生æˆå¤±è´¥", - ) - .await - .map_err(|error| jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error))?; - let character_image = character_generated - .images - .into_iter() - .next() - .ok_or_else(|| { - jump_hop_error_response( - request_context, - JUMP_HOP_CREATION_PROVIDER, - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "vector-engine", - "message": "è·³ä¸€è·³è§’è‰²èµ„äº§ç”ŸæˆæˆåŠŸä½†æœªè¿”å›žå›¾ç‰‡ã€‚", - })), - ) - })?; - let character_asset = persist_jump_hop_generated_image_asset( - state, - owner_user_id, - profile_id.as_str(), - "character", - character_prompt, - character_image, - LegacyAssetPrefix::JumpHopAssets, - 768, - 768, - request_context, - ) - .await?; - - let sheet_prompt = build_jump_hop_tile_atlas_prompt(tile_prompt); + let sheet_prompt = build_jump_hop_tile_atlas_prompt(theme_text, tile_prompt); let tile_generated = create_openai_image_generation( &http_client, &settings, sheet_prompt.as_str(), - Some("文字ã€Logoã€æ°´å°ã€æŒ‰é’®ã€UI å­—ã€ä½Žæ¸…晰度ã€ç•¸å½¢è‚¢ä½“ã€å¤šä½™è§’色ã€è£åˆ‡ä¸»ä½“"), + Some(build_jump_hop_tile_atlas_negative_prompt()), "1024*1024", 1, &[], @@ -527,7 +520,12 @@ async fn maybe_generate_jump_hop_assets( .await?, ); } - payload.character_asset = Some(character_asset); + if payload.character_asset.is_none() { + payload.character_asset = Some(build_jump_hop_default_character_asset( + profile_id.as_str(), + theme_text, + )); + } payload.tile_atlas_asset = Some(tile_atlas_asset); payload.tile_assets = Some(tile_assets); payload.cover_composite = payload.cover_composite.clone().or_else(|| { @@ -538,28 +536,29 @@ async fn maybe_generate_jump_hop_assets( Ok(()) } -fn build_jump_hop_tile_atlas_prompt(tile_prompt: &str) -> String { +fn build_jump_hop_tile_atlas_prompt(theme_text: &str, tile_prompt: &str) -> String { + let theme_text = theme_text.trim(); + let theme_text = if theme_text.is_empty() { + "跳一跳" + } else { + theme_text + }; let subject_text = tile_prompt.trim(); let subject_text = if subject_text.is_empty() { - "ç­‰è·ç«‹ä½“地å—图集" + theme_text } else { subject_text }; - let cell_plan = [ - "第1行第1列:start 起点地å—", - "第1行第2列:normal 普通地å—", - "第1行第3列:target 目标地å—", - "第2行第1列:finish 终点地å—", - "第2行第2列:bonus 奖励地å—", - "第2行第3列:accent 视觉强调地å—", - ] - .join("ï¼›"); format!( - "生æˆä¸€å¼ 1:1图片。固定生æˆ2行*3列的跳一跳地å—ç´ æå›¾é›†ï¼Œç”»é¢æ˜¯{subject_text}。严格按六个å•元格排布:{cell_plan}。æ¯ä¸ªå•å…ƒæ ¼åªæ”¾ä¸€ä¸ªå®Œæ•´ç­‰è·/俯视角 2D 地å—,必须表现顶é¢ã€ä¾§é¢åŽšåº¦å’Œç»Ÿä¸€æŠ•å½±ï¼Œå…‰å‘一致,地å—主体居中且四周ä¿ç•™ç•™ç™½ã€‚æ¯æ ¼èƒŒæ™¯å¿…须是统一纯绿色绿幕背景(高饱和亮绿色,接近 #00FF00),背景平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€æ— é“具,方便åŽç»­æŠ æˆé€æ˜Žã€‚ç´ ææœ¬èº«ä¸å¾—使用与绿幕相åŒçš„纯绿色;若æè´¨å¤©ç„¶å«ç»¿è‰²ï¼Œå¿…é¡»ä½¿ç”¨æ›´æ·±ã€æ›´é»„或更è“的绿色并用清晰æè¾¹ä¸Žç»¿å¹•åŒºåˆ†ã€‚ç¦æ­¢ä¸»ä½“跨格ã€è´´è¾¹æˆ–è¶Šç•Œï¼Œç¦æ­¢ä»»ä½•内容进入相邻格å­ã€‚ä¸è¦å‡ºçŽ°æ–‡å­—ã€æ°´å°ã€UIã€è¾¹æ¡†ã€ç½‘æ ¼çº¿ã€æ ‡ç­¾ã€è§’色或场景。" + "生æˆä¸€å¼ 1:1图片,主题为“{theme_text}â€ã€‚\nç”»é¢åªåŒ…å«25个独立的跳一跳å¯è½è„šå¹³å°ç´ æï¼ŒæŒ‰äº”行五列å‡åŒ€æ‘†æ”¾åœ¨çº¯ç»¿è‰²ç»¿å¹•画布上;ä¸è¦ç”»æˆæ¸¸æˆç•Œé¢ã€æ£‹ç›˜ã€èƒŒåŒ…ã€è£…å¤‡æ æˆ–图标集页é¢ã€‚\n视觉方å‘为俯视角平å°è·³è·ƒæ¸¸æˆï¼Œç”»é¢å†…容是{subject_text}。\næ¯ä¸€å—å¹³å°éƒ½å¿…须直接使用主题元素åšä¸»ä½“造型,主题è¦ä¸€çœ¼å¯è§ï¼›ä¾‹å¦‚ä¸»é¢˜ä¸ºæ°´æžœæ—¶ï¼Œåº”æ˜¯è‹¹æžœåˆ‡ç‰‡ã€æ©™å­åˆ‡ç‰‡ã€è¥¿ç“œå—ã€è‰èŽ“ã€è èã€é¦™è•‰ç­‰æ°´æžœé€ åž‹å¹³å°ï¼Œä¸å¾—å˜æˆçŸ³æ¿ã€é‡‘属按钮ã€å¾½ç« æˆ–装备。\nåªç”»å¹³å°è£¸ç´ æï¼Œä¸ç”»å¤–层颿¿ã€æ£‹ç›˜åº•座ã€èœå•ã€æŒ‰é’®ã€æ ‡é¢˜ã€æ–‡å­—ã€è§’æ ‡ã€è£…饰边框ã€å·¥å…·æ ã€è£…å¤‡ã€æ­¦å™¨ã€å¾½ç« ã€é“具或角色。\n整体风格为清爽自然的休闲手游平å°ç´ æï¼Œå2D/2.5D手绘质感,哑光æè´¨ï¼Œå¹²å‡€è‰²å—,轻微主体内部明暗,é¿å…å†™å®žæ‘„å½±ã€æ²¹äº®é«˜å…‰ã€å¡‘æ–™æ„Ÿã€æš—黑幻想风和厚é‡CG渲染。\næ¯æ ¼ä¸€ä¸ªå®Œæ•´å¹³å°ï¼Œæ˜¯ç¬¦åˆä¸»é¢˜ä¸”有设计感的立体感平å°ï¼Œæœ‰é¡¶é¢å’Œæ¸…晰轮廓;ä¸è¦é»˜è®¤ç”Ÿæˆç°è‰²çŸ³æ¿æˆ–金属地砖,除éžä¸»é¢˜æœ¬èº«å°±æ˜¯çŸ³å¤´æˆ–金属。\næ¯æ ¼ä¸»ä½“必须居中,视觉尺寸åªå å•æ ¼56%-64%,四周至少ä¿ç•™18%纯绿色绿幕安全留白;任何å¶ç‰‡ã€è£…饰ã€è½®å»“和光影都ä¸å¾—è´´è¾¹ã€è·¨æ ¼æˆ–越界。\næ¯ä¸ªå¹³å°åªä¿ç•™ä¸»ä½“内部明暗和外轮廓,ä¸ç»˜åˆ¶è½åœ°æŠ•å½±ã€æŽ¥è§¦é˜´å½±ã€æ–¹å½¢é˜´å½±ã€åº•æ¿ã€ç™½åº•ã€ç°åº•ã€é»‘底或背景色å—,è¿è¡Œæ€ä¼šç»Ÿä¸€æ·»åŠ é˜´å½±ã€‚\n25个平å°åŒä¸€æè´¨ä½“ç³»ã€åŒä¸€å…‰å‘,但形状和细节有å˜åŒ–ï¼›æ¯ä¸ªå¹³å°ä¹‹é—´åªèƒ½æ˜¯çº¯ç»¿è‰²ç©ºç™½ï¼Œä¸ç”»åˆ†éš”线ã€ç½‘格线ã€å®¹å™¨æ¡†æˆ–棋盘格。\næ•´å¼ ç”»å¸ƒèƒŒæ™¯ã€æ ¼é—´ç©ºç™½å’Œæ¯æ ¼èƒŒæ™¯éƒ½å¿…须是接近 #00FF00 的纯绿色绿幕,背景平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€æ— é»‘底;主体自身ä¸å¾—使用接近 #00FF00 的纯绿。\nç¦æ­¢è·¨æ ¼ã€è´´è¾¹ã€è¶Šç•Œã€æ–‡å­—ã€æ°´å°ã€UIã€è¾¹æ¡†ã€ç½‘格线ã€è§’色ã€åœºæ™¯ã€æ¸¸æˆé¢æ¿æˆ–é“具界é¢ã€‚\nEnglish guardrail: isolated top-down fruit-shaped jump pad assets only, green screen background, no text, no poster, no architecture, no building, no UI screen, no inventory icons." ) } +fn build_jump_hop_tile_atlas_negative_prompt() -> &'static str { + "文字ã€Logoã€æ°´å°ã€æŒ‰é’®ã€UI å­—ã€æ¸¸æˆç•Œé¢ã€æ£‹ç›˜ã€èƒŒåŒ…ã€è£…备æ ã€å›¾æ ‡é›†é¡µé¢ã€å¤–层颿¿ã€èœå•ã€å·¥å…·æ ã€ä½Žæ¸…晰度ã€ç•¸å½¢è‚¢ä½“ã€å¤šä½™è§’色ã€è£åˆ‡ä¸»ä½“ã€å†™å®žæ‘„å½±ã€æ²¹äº®é«˜å…‰ã€å¡‘æ–™è´¨æ„Ÿã€æš—黑幻想风ã€åŽšé‡CG渲染ã€ç°è‰²çŸ³æ¿ã€é‡‘属地砖ã€å»ºç­‘ã€æ¥¼æˆ¿ã€æµ·æŠ¥ã€è£…å¤‡ã€æ­¦å™¨ã€å¾½ç« ã€é“具图标ã€UI图标å¡ã€æ ‡é¢˜ã€è¯´æ˜Žæ–‡å­—ã€è£…饰边框ã€è½åœ°æŠ•å½±ã€æŽ¥è§¦é˜´å½±ã€æ–¹å½¢é˜´å½±ã€æ–¹å½¢åº•æ¿ã€ç™½åº•ã€ç°åº•ã€é»‘åº•ã€æš—色背景ã€èƒŒæ™¯è‰²å—ã€è´´è¾¹ã€è·¨æ ¼ã€è¶Šç•Œ" +} + fn slice_jump_hop_tile_atlas( image: &crate::openai_image_generation::DownloadedOpenAiImage, ) -> Result, AppError> { @@ -583,8 +582,8 @@ fn slice_jump_hop_tile_atlas( ); } - let mut slices = Vec::with_capacity(JUMP_HOP_TILE_ITEM_NAMES.len()); - for index in 0..JUMP_HOP_TILE_ITEM_NAMES.len() { + let mut slices = Vec::with_capacity(JUMP_HOP_TILE_ITEM_COUNT); + for index in 0..JUMP_HOP_TILE_ITEM_COUNT { let row = index as u32 / JUMP_HOP_TILE_ATLAS_COLS; let col = index as u32 % JUMP_HOP_TILE_ATLAS_COLS; let x0 = col.saturating_mul(width) / JUMP_HOP_TILE_ATLAS_COLS; @@ -598,6 +597,9 @@ fn slice_jump_hop_tile_atlas( y1.saturating_sub(y0).max(1), ); let cleaned = crop_generated_asset_sheet_view_edge_matte(cropped); + let cleaned = keep_jump_hop_largest_alpha_component(cleaned); + let cleaned = crop_generated_asset_sheet_view_edge_matte(cleaned); + let cleaned = pad_jump_hop_tile_slice_image(cleaned); let mut cursor = std::io::Cursor::new(Vec::new()); cleaned .write_to(&mut cursor, image::ImageFormat::Png) @@ -617,26 +619,116 @@ fn slice_jump_hop_tile_atlas( Ok(slices) } +fn pad_jump_hop_tile_slice_image(image: image::DynamicImage) -> image::DynamicImage { + let source = image.to_rgba8(); + let (width, height) = source.dimensions(); + if width == 0 || height == 0 { + return image::DynamicImage::ImageRgba8(source); + } + + // 中文注释:生图å¶å°”会让主体贴近å•元格边缘;切片入库å‰è¡¥é€æ˜Žå®‰å…¨è¾¹ï¼Œ + // é¿å…è¿è¡Œæ€ç¼©æ”¾æˆ–滤镜让主体看起æ¥è¢«è£æŽ‰ã€‚ + let pad_x = (width / 12).clamp(8, 24); + let pad_y = (height / 12).clamp(8, 24); + let mut padded = image::RgbaImage::from_pixel( + width.saturating_add(pad_x.saturating_mul(2)), + height.saturating_add(pad_y.saturating_mul(2)), + image::Rgba([0, 0, 0, 0]), + ); + image::imageops::overlay(&mut padded, &source, pad_x.into(), pad_y.into()); + image::DynamicImage::ImageRgba8(padded) +} + +fn keep_jump_hop_largest_alpha_component(image: image::DynamicImage) -> image::DynamicImage { + let mut source = image.to_rgba8(); + let (width, height) = source.dimensions(); + if width == 0 || height == 0 { + return image::DynamicImage::ImageRgba8(source); + } + + // 中文注释:模型å¶å°”会让相邻格的å¶ç‰‡ã€æžœæ¢—æˆ–é˜´å½±è¶Šç•Œè¿›å½“å‰æ ¼ï¼› + // æ¯æ ¼åªä¿ç•™æœ€å¤§çš„ alpha 连通主体,能去掉这些å°ç¢Žç‰‡å†å…¥åº“。 + let width_usize = width as usize; + let height_usize = height as usize; + let pixel_count = width_usize.saturating_mul(height_usize); + let mut visited = vec![false; pixel_count]; + let mut best_component = Vec::::new(); + + for start in 0..pixel_count { + if visited[start] || source.as_raw()[start * 4 + 3] <= 16 { + visited[start] = true; + continue; + } + + let mut queue = VecDeque::from([start]); + let mut component = Vec::::new(); + visited[start] = true; + + while let Some(index) = queue.pop_front() { + component.push(index); + let x = index % width_usize; + let y = index / width_usize; + + for offset_y in -1i32..=1 { + for offset_x in -1i32..=1 { + if offset_x == 0 && offset_y == 0 { + continue; + } + let next_x = x as i32 + offset_x; + let next_y = y as i32 + offset_y; + if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 + { + continue; + } + let next = next_y as usize * width_usize + next_x as usize; + if visited[next] { + continue; + } + visited[next] = true; + if source.as_raw()[next * 4 + 3] > 16 { + queue.push_back(next); + } + } + } + } + + if component.len() > best_component.len() { + best_component = component; + } + } + + if best_component.is_empty() { + return image::DynamicImage::ImageRgba8(source); + } + + let mut keep = vec![false; pixel_count]; + for index in best_component { + keep[index] = true; + } + for index in 0..pixel_count { + if keep[index] { + continue; + } + let pixel = + source.get_pixel_mut((index % width_usize) as u32, (index / width_usize) as u32); + pixel.0[3] = 0; + } + + image::DynamicImage::ImageRgba8(source) +} + fn jump_hop_tile_type_by_index(index: usize) -> JumpHopTileType { match index { 0 => JumpHopTileType::Start, - 1 => JumpHopTileType::Normal, - 2 => JumpHopTileType::Target, - 3 => JumpHopTileType::Finish, - 4 => JumpHopTileType::Bonus, - _ => JumpHopTileType::Accent, + value if value % 11 == 0 => JumpHopTileType::Bonus, + value if value % 7 == 0 => JumpHopTileType::Accent, + value if value % 3 == 0 => JumpHopTileType::Target, + _ => JumpHopTileType::Normal, } } -fn jump_hop_tile_asset_slot_name(tile_type: &JumpHopTileType) -> &'static str { - match tile_type { - JumpHopTileType::Start => "tile-start", - JumpHopTileType::Normal => "tile-normal", - JumpHopTileType::Target => "tile-target", - JumpHopTileType::Finish => "tile-finish", - JumpHopTileType::Bonus => "tile-bonus", - JumpHopTileType::Accent => "tile-accent", - } +fn jump_hop_tile_asset_slot_name(tile_index: usize) -> String { + format!("tile-{:02}", tile_index + 1) } #[allow(clippy::too_many_arguments)] @@ -648,7 +740,7 @@ async fn persist_jump_hop_tile_asset( tile_slice: JumpHopTileAtlasSlice, request_context: &RequestContext, ) -> Result { - let slot = jump_hop_tile_asset_slot_name(&tile_slice.tile_type); + let slot = jump_hop_tile_asset_slot_name(tile_index); let image = crate::openai_image_generation::DownloadedOpenAiImage { bytes: tile_slice.bytes, mime_type: "image/png".to_string(), @@ -658,7 +750,7 @@ async fn persist_jump_hop_tile_asset( state, owner_user_id, profile_id, - slot, + slot.as_str(), &format!( "跳一跳地å—切片 {}:{}", tile_index + 1, @@ -674,10 +766,13 @@ async fn persist_jump_hop_tile_asset( Ok(JumpHopTileAsset { tile_type: tile_slice.tile_type, + tile_id: Some(slot), image_src: persisted.image_src, image_object_key: persisted.image_object_key, asset_object_id: persisted.asset_object_id, source_atlas_cell: tile_slice.source_atlas_cell, + atlas_row: Some((tile_index as u32 / JUMP_HOP_TILE_ATLAS_COLS) + 1), + atlas_col: Some((tile_index as u32 % JUMP_HOP_TILE_ATLAS_COLS) + 1), visual_width: 256, visual_height: 192, top_surface_radius: 42.0, @@ -685,6 +780,22 @@ async fn persist_jump_hop_tile_asset( }) } +fn build_jump_hop_default_character_asset( + profile_id: &str, + theme_text: &str, +) -> JumpHopCharacterAsset { + JumpHopCharacterAsset { + asset_id: format!("{profile_id}-builtin-character"), + image_src: "builtin://jump-hop/default-character".to_string(), + image_object_key: String::new(), + asset_object_id: format!("{profile_id}-builtin-character"), + generation_provider: "builtin-three".to_string(), + prompt: format!("内置默认 3D 角色:{}", theme_text.trim()), + width: 0, + height: 0, + } +} + async fn persist_jump_hop_generated_image_asset( state: &AppState, owner_user_id: &str, @@ -868,17 +979,26 @@ fn build_jump_hop_work_play_tracking_draft( } fn build_jump_hop_draft(payload: &JumpHopWorkspaceCreateRequest) -> JumpHopDraftResponse { + let theme_text = normalize_theme_text(&payload.theme_text, &payload.work_title); JumpHopDraftResponse { template_id: JUMP_HOP_TEMPLATE_ID.to_string(), template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), profile_id: None, - work_title: payload.work_title.trim().to_string(), - work_description: payload.work_description.trim().to_string(), + theme_text: theme_text.clone(), + work_title: clean_or_default(&payload.work_title, &theme_text), + work_description: clean_or_default( + &payload.work_description, + &format!("{theme_text}主题的俯视角跳跃作å“"), + ), theme_tags: normalize_tags(payload.theme_tags.clone()), difficulty: payload.difficulty.clone(), style_preset: payload.style_preset.clone(), - character_prompt: payload.character_prompt.trim().to_string(), - tile_prompt: payload.tile_prompt.trim().to_string(), + default_character: Some(default_jump_hop_character()), + character_prompt: clean_or_default(&payload.character_prompt, "内置默认 3D 角色"), + tile_prompt: clean_or_default( + &payload.tile_prompt, + &format!("{theme_text}主题的俯视角清爽游æˆåŒ–立体感平å°ç´ æ"), + ), end_mood_prompt: payload .end_mood_prompt .as_ref() @@ -897,13 +1017,7 @@ fn validate_workspace_request( request_context: &RequestContext, payload: &JumpHopWorkspaceCreateRequest, ) -> Result<(), Response> { - ensure_non_empty(request_context, &payload.work_title, "workTitle")?; - ensure_non_empty( - request_context, - &payload.character_prompt, - "characterPrompt", - )?; - ensure_non_empty(request_context, &payload.tile_prompt, "tilePrompt")?; + ensure_non_empty(request_context, &payload.theme_text, "themeText")?; if payload.template_id.trim() != JUMP_HOP_TEMPLATE_ID { return Err(jump_hop_error_response( request_context, @@ -917,6 +1031,32 @@ fn validate_workspace_request( Ok(()) } +fn normalize_theme_text(theme_text: &str, fallback: &str) -> String { + clean_or_default(theme_text, fallback) + .chars() + .take(60) + .collect::() +} + +fn clean_or_default(value: &str, fallback: &str) -> String { + let value = value.trim(); + if value.is_empty() { + fallback.trim().to_string() + } else { + value.to_string() + } +} + +fn default_jump_hop_character() -> shared_contracts::jump_hop::JumpHopDefaultCharacter { + shared_contracts::jump_hop::JumpHopDefaultCharacter { + character_id: "jump-hop-default-runner".to_string(), + display_name: "默认角色".to_string(), + model_kind: "builtin-three".to_string(), + body_color: "#f59e0b".to_string(), + accent_color: "#2563eb".to_string(), + } +} + fn ensure_non_empty( request_context: &RequestContext, value: &str, @@ -1020,32 +1160,82 @@ mod tests { use super::*; #[test] - fn jump_hop_tile_atlas_prompt_uses_dedicated_two_by_three_layout() { - let prompt = build_jump_hop_tile_atlas_prompt("森林石å—风格等è·åœ°å—"); + fn jump_hop_tile_atlas_prompt_uses_dedicated_five_by_five_floor_layout() { + let prompt = build_jump_hop_tile_atlas_prompt("森林冒险", "森林主题清爽游æˆåŒ–立体感平å°"); - assert!(prompt.contains("2行*3列")); - assert!(prompt.contains("第1行第1列:start 起点地å—")); - assert!(prompt.contains("第2行第3列:accent 视觉强调地å—")); + assert!(prompt.contains("五行五列")); + assert!(prompt.contains("å…±25个")); + assert!(prompt.contains("å¯è½è„šå¹³å°ç´ æ")); + assert!(prompt.contains("ä¸è¦ç”»æˆæ¸¸æˆç•Œé¢")); + assert!(prompt.contains("主题è¦ä¸€çœ¼å¯è§")); + assert!(prompt.contains("æ¯æ ¼ä¸€ä¸ªå®Œæ•´å¹³å°")); + assert!(prompt.contains("清爽自然的休闲手游平å°ç´ æ")); + assert!(prompt.contains("符åˆä¸»é¢˜ä¸”有设计感的立体感平å°")); + assert!(prompt.contains("四周至少ä¿ç•™18%纯绿色绿幕安全留白")); + assert!(prompt.contains("ä¸ç»˜åˆ¶è½åœ°æŠ•å½±")); + assert!(prompt.contains("ä¸ç”»åˆ†éš”线ã€ç½‘格线ã€å®¹å™¨æ¡†æˆ–棋盘格")); + assert!(prompt.contains("English guardrail")); + assert!(!prompt.contains("按5行*5列")); + assert!(!prompt.contains("2D地æ¿å›¾æ ‡")); + assert!(!prompt.contains("清爽自然的游æˆå›¾æ ‡")); + assert!(!prompt.contains("边缘厚度暗示")); + assert!(!prompt.contains("统一投影")); assert!(!prompt.contains("æ¯ä¸ªç‰©å“生æˆ")); assert!(!prompt.contains("ä¸åŒè§†å›¾")); } #[test] - fn jump_hop_tile_atlas_slices_one_png_per_tile_type() { - let width = 300; - let height = 200; - let colors = [ - [220, 24, 24, 255], - [240, 150, 32, 255], - [248, 220, 72, 255], - [52, 168, 84, 255], - [38, 132, 255, 255], - [156, 92, 220, 255], - ]; + fn jump_hop_tile_atlas_negative_prompt_blocks_oily_and_square_shadow_artifacts() { + let negative_prompt = build_jump_hop_tile_atlas_negative_prompt(); + + assert!(negative_prompt.contains("油亮高光")); + assert!(negative_prompt.contains("厚é‡CG渲染")); + assert!(negative_prompt.contains("游æˆç•Œé¢")); + assert!(negative_prompt.contains("图标集页é¢")); + assert!(negative_prompt.contains("建筑")); + assert!(negative_prompt.contains("方形阴影")); + assert!(negative_prompt.contains("方形底æ¿")); + } + + #[test] + fn jump_hop_tile_slice_keeps_largest_alpha_component() { + let mut image = image::RgbaImage::from_pixel(80, 80, image::Rgba([0, 0, 0, 0])); + for y in 12..52 { + for x in 12..52 { + image.put_pixel(x, y, image::Rgba([220, 70, 50, 255])); + } + } + for y in 68..74 { + for x in 36..42 { + image.put_pixel(x, y, image::Rgba([40, 190, 80, 255])); + } + } + + let cleaned = keep_jump_hop_largest_alpha_component(image::DynamicImage::ImageRgba8(image)) + .to_rgba8(); + + assert_eq!(cleaned.get_pixel(20, 20).0[3], 255); + assert_eq!( + cleaned.get_pixel(38, 70).0[3], + 0, + "相邻格侵入的å°ç¢Žç‰‡ä¸åº”扩大当å‰åœ°å—切片边界" + ); + } + + #[test] + fn jump_hop_tile_atlas_slices_twenty_five_png_tiles() { + let width = 500; + let height = 500; let mut atlas = image::RgbaImage::new(width, height); - for row in 0..2 { - for col in 0..3 { - let color = image::Rgba(colors[row * 3 + col]); + for row in 0..5 { + for col in 0..5 { + let index = row * 5 + col; + let color = image::Rgba([ + 40 + index as u8 * 3, + 24 + index as u8 * 5, + 120 + index as u8 * 2, + 255, + ]); for y in row as u32 * 100..(row as u32 + 1) * 100 { for x in col as u32 * 100..(col as u32 + 1) * 100 { atlas.put_pixel(x, y, color); @@ -1065,20 +1255,48 @@ mod tests { let slices = slice_jump_hop_tile_atlas(&image).expect("atlas should slice"); - assert_eq!(slices.len(), JUMP_HOP_TILE_ITEM_NAMES.len()); + assert_eq!(slices.len(), JUMP_HOP_TILE_ITEM_COUNT); for (index, slice) in slices.iter().enumerate() { assert_eq!(slice.tile_type, jump_hop_tile_type_by_index(index)); assert_eq!( slice.source_atlas_cell, - format!("row-{}-col-{}", index / 3 + 1, index % 3 + 1) + format!("row-{}-col-{}", index / 5 + 1, index % 5 + 1) ); let decoded = image::load_from_memory(slice.bytes.as_slice()) .expect("tile slice should decode") .to_rgba8(); + assert_eq!( + decoded.dimensions(), + (116, 116), + "跳一跳地å—切片应在 100x100 å•å…ƒæ ¼å¤–è¡¥é€æ˜Žå®‰å…¨è¾¹" + ); + let color = [ + 40 + index as u8 * 3, + 24 + index as u8 * 5, + 120 + index as u8 * 2, + 255, + ]; assert!( - decoded.pixels().any(|pixel| pixel.0 == colors[index]), + decoded.pixels().any(|pixel| pixel.0 == color), "第 {index} 个地å—切片应ä¿ç•™å¯¹åº”æ ¼å­çš„主体颜色" ); } } + + #[test] + fn jump_hop_tile_asset_slots_are_unique_for_twenty_five_slices() { + let slots = (0..JUMP_HOP_TILE_ITEM_COUNT) + .map(jump_hop_tile_asset_slot_name) + .collect::>(); + let unique_slots = slots + .iter() + .cloned() + .collect::>(); + + assert_eq!( + unique_slots.len(), + JUMP_HOP_TILE_ITEM_COUNT, + "25 个地å—切片必须写入 25 个独立 slot/path,ä¸èƒ½æŒ‰é‡å¤çš„ tile_type 互相覆盖" + ); + } } diff --git a/server-rs/crates/api-server/src/match3d/item_assets.rs b/server-rs/crates/api-server/src/match3d/item_assets.rs index 726f0c7e..f21ecbbf 100644 --- a/server-rs/crates/api-server/src/match3d/item_assets.rs +++ b/server-rs/crates/api-server/src/match3d/item_assets.rs @@ -755,12 +755,11 @@ async fn generate_match3d_material_sheet_from_level_scene( config: &Match3DConfigJson, background_asset: Option<&Match3DGeneratedBackgroundAsset>, ) -> Result { - let settings = require_openai_image_settings(state)? - .with_external_api_audit_context( - request_context, - Some(owner_user_id.to_string()), - Some(profile_id.to_string()), - ); + let settings = require_openai_image_settings(state)?.with_external_api_audit_context( + request_context, + Some(owner_user_id.to_string()), + Some(profile_id.to_string()), + ); let http_client = build_openai_image_http_client(&settings)?; let prompt = build_match3d_item_spritesheet_prompt(); let reference = load_match3d_level_scene_reference_image(state, background_asset).await?; diff --git a/server-rs/crates/api-server/src/match3d/works.rs b/server-rs/crates/api-server/src/match3d/works.rs index 99d4ef06..bcdea311 100644 --- a/server-rs/crates/api-server/src/match3d/works.rs +++ b/server-rs/crates/api-server/src/match3d/works.rs @@ -304,12 +304,11 @@ pub(super) async fn generate_match3d_cover_image_asset( reference_image_srcs: Vec, ) -> Result { require_match3d_oss_client(state)?; - let settings = require_openai_image_settings(state)? - .with_external_api_audit_context( - request_context, - Some(owner_user_id.to_string()), - Some(profile_id.to_string()), - ); + let settings = require_openai_image_settings(state)?.with_external_api_audit_context( + request_context, + Some(owner_user_id.to_string()), + Some(profile_id.to_string()), + ); let http_client = build_openai_image_http_client(&settings)?; let cover_prompt = build_match3d_cover_generation_prompt(config, prompt); let generated = if let Some(uploaded_image) = resolve_match3d_reference_image_for_edit( @@ -459,12 +458,11 @@ pub(super) async fn generate_match3d_level_asset_bundle( prompt: &str, ) -> Result { require_match3d_oss_client(state)?; - let settings = require_openai_image_settings(state)? - .with_external_api_audit_context( - request_context, - Some(owner_user_id.to_string()), - Some(profile_id.to_string()), - ); + let settings = require_openai_image_settings(state)?.with_external_api_audit_context( + request_context, + Some(owner_user_id.to_string()), + Some(profile_id.to_string()), + ); let http_client = build_openai_image_http_client(&settings)?; let level_scene_prompt = build_match3d_level_scene_generation_prompt(config); @@ -607,12 +605,11 @@ pub(super) async fn generate_match3d_container_image( prompt: &str, ) -> Result { require_match3d_oss_client(state)?; - let settings = require_openai_image_settings(state)? - .with_external_api_audit_context( - request_context, - Some(owner_user_id.to_string()), - Some(profile_id.to_string()), - ); + let settings = require_openai_image_settings(state)?.with_external_api_audit_context( + request_context, + Some(owner_user_id.to_string()), + Some(profile_id.to_string()), + ); let http_client = build_openai_image_http_client(&settings)?; let reference_image = load_match3d_container_reference_image()?; let container_prompt = build_match3d_container_generation_prompt(config, prompt); diff --git a/server-rs/crates/api-server/src/modules/jump_hop.rs b/server-rs/crates/api-server/src/modules/jump_hop.rs index 48864e8d..f2604406 100644 --- a/server-rs/crates/api-server/src/modules/jump_hop.rs +++ b/server-rs/crates/api-server/src/modules/jump_hop.rs @@ -7,8 +7,9 @@ use crate::{ auth::{require_bearer_auth, require_runtime_principal_auth}, jump_hop::{ create_jump_hop_session, execute_jump_hop_action, get_jump_hop_gallery_detail, - get_jump_hop_runtime_work, get_jump_hop_session, jump_hop_run_jump, list_jump_hop_gallery, - list_jump_hop_works, publish_jump_hop_work, restart_jump_hop_run, start_jump_hop_run, + get_jump_hop_leaderboard, get_jump_hop_runtime_work, get_jump_hop_session, + jump_hop_run_jump, list_jump_hop_gallery, list_jump_hop_works, publish_jump_hop_work, + restart_jump_hop_run, start_jump_hop_run, }, state::AppState, }; @@ -54,6 +55,13 @@ pub fn router(state: AppState) -> Router { "/api/runtime/jump-hop/works/{profile_id}", get(get_jump_hop_runtime_work), ) + .route( + "/api/runtime/jump-hop/works/{profile_id}/leaderboard", + get(get_jump_hop_leaderboard).route_layer(middleware::from_fn_with_state( + state.clone(), + require_runtime_principal_auth, + )), + ) .route( "/api/runtime/jump-hop/runs", post(start_jump_hop_run).route_layer(middleware::from_fn_with_state( diff --git a/server-rs/crates/api-server/src/puzzle/generation.rs b/server-rs/crates/api-server/src/puzzle/generation.rs index a17766f7..c03ad4bf 100644 --- a/server-rs/crates/api-server/src/puzzle/generation.rs +++ b/server-rs/crates/api-server/src/puzzle/generation.rs @@ -310,12 +310,11 @@ pub(crate) async fn generate_puzzle_level_asset_bundle( level_name: &str, puzzle_image: &PuzzleDownloadedImage, ) -> Result { - let settings = require_puzzle_vector_engine_settings(state)? - .with_external_api_audit_context( - request_context, - Some(owner_user_id.to_string()), - Some(session_id.to_string()), - ); + let settings = require_puzzle_vector_engine_settings(state)?.with_external_api_audit_context( + request_context, + Some(owner_user_id.to_string()), + Some(session_id.to_string()), + ); let http_client = build_puzzle_image_http_client(state, PuzzleImageModel::GptImage2)?; let puzzle_reference = build_puzzle_downloaded_image_reference(puzzle_image); let scene_generated = create_puzzle_vector_engine_image_generation( diff --git a/server-rs/crates/api-server/src/puzzle/vector_engine.rs b/server-rs/crates/api-server/src/puzzle/vector_engine.rs index 4c6f9b2f..4338966b 100644 --- a/server-rs/crates/api-server/src/puzzle/vector_engine.rs +++ b/server-rs/crates/api-server/src/puzzle/vector_engine.rs @@ -117,11 +117,9 @@ impl PuzzleVectorEngineSettings { ) -> Self { self.external_api_audit_user_id = user_id; self.external_api_audit_profile_id = profile_id; - self.external_api_audit_request_id = - Some(request_context.request_id().to_string()); + self.external_api_audit_request_id = Some(request_context.request_id().to_string()); self } - } pub(crate) struct ParsedPuzzleImageDataUrl { diff --git a/server-rs/crates/api-server/src/square_hole/visual_assets.rs b/server-rs/crates/api-server/src/square_hole/visual_assets.rs index 75ad863e..3379f2a4 100644 --- a/server-rs/crates/api-server/src/square_hole/visual_assets.rs +++ b/server-rs/crates/api-server/src/square_hole/visual_assets.rs @@ -398,12 +398,11 @@ async fn generate_square_hole_image_data_url( size: &str, failure_context: &str, ) -> Result { - let settings = require_openai_image_settings(state)? - .with_external_api_audit_context( - request_context, - Some(owner_user_id.to_string()), - Some(profile_id.to_string()), - ); + let settings = require_openai_image_settings(state)?.with_external_api_audit_context( + request_context, + Some(owner_user_id.to_string()), + Some(profile_id.to_string()), + ); let http_client = build_openai_image_http_client(&settings)?; let generated = create_openai_image_generation( &http_client, diff --git a/server-rs/crates/module-jump-hop/src/application.rs b/server-rs/crates/module-jump-hop/src/application.rs index e7f835bf..71a990d5 100644 --- a/server-rs/crates/module-jump-hop/src/application.rs +++ b/server-rs/crates/module-jump-hop/src/application.rs @@ -5,61 +5,18 @@ use crate::{ JumpHopPlatform, JumpHopRunSnapshot, JumpHopRunStatus, JumpHopScoring, JumpHopTileType, }; +const JUMP_HOP_PLATFORM_SIZE_MULTIPLIER: f32 = 2.0; +const JUMP_HOP_CHARGE_TO_DISTANCE_RATIO: f32 = 0.008; + pub fn generate_jump_hop_path(seed: &str, difficulty: JumpHopDifficulty) -> JumpHopPath { let config = difficulty_config(difficulty); - let mut rng = DeterministicRng::new(seed, difficulty.as_str()); - let platform_count = rng.range_u32(config.min_platforms, config.max_platforms) as usize; - let mut platforms = Vec::with_capacity(platform_count); - let mut x = 0.0f32; - let mut y = 0.0f32; - - for index in 0..platform_count { - let tile_type = if index == 0 { - JumpHopTileType::Start - } else if index + 1 == platform_count { - JumpHopTileType::Finish - } else if index % 7 == 0 { - JumpHopTileType::Bonus - } else if index % 5 == 0 { - JumpHopTileType::Target - } else if index % 4 == 0 { - JumpHopTileType::Accent - } else { - JumpHopTileType::Normal - }; - let width = rng.range_f32(config.min_width, config.max_width); - let height = width * rng.range_f32(0.86, 1.04); - let landing_radius = width * config.landing_radius_factor; - let perfect_radius = landing_radius * config.perfect_radius_factor; - - platforms.push(JumpHopPlatform { - platform_id: format!("jump-hop-platform-{index:03}"), - tile_type, - x, - y, - width, - height, - landing_radius, - perfect_radius, - score_value: if tile_type == JumpHopTileType::Bonus { - 180 - } else { - 100 - }, - }); - - if index + 1 < platform_count { - let distance = rng.range_f32(config.min_gap, config.max_gap); - let direction = if rng.next_u32() % 2 == 0 { 1.0 } else { -1.0 }; - x += distance * 0.62 * direction; - y += distance; - } - } + let platform_count = 8usize; + let platforms = build_platforms_until(seed, difficulty, platform_count); JumpHopPath { seed: seed.trim().to_string(), difficulty, - finish_index: platform_count.saturating_sub(1) as u32, + finish_index: u32::MAX, platforms, camera_preset: "portrait-isometric-9x16".to_string(), scoring: JumpHopScoring { @@ -85,6 +42,7 @@ pub fn start_run( if path.platforms.is_empty() { return Err(JumpHopError::EmptyPath); } + let path = normalize_jump_hop_path_platform_size(path); Ok(JumpHopRunSnapshot { run_id, @@ -103,7 +61,9 @@ pub fn start_run( pub fn apply_jump( run: &JumpHopRunSnapshot, - charge_ms: u32, + drag_distance: f32, + drag_vector_x: Option, + drag_vector_y: Option, jumped_at_ms: u64, ) -> Result { if run.status != JumpHopRunStatus::Playing { @@ -111,46 +71,42 @@ pub fn apply_jump( } let current_index = run.current_platform_index as usize; let next_index = current_index + 1; + let path = extend_jump_hop_path(run.path.clone(), next_index + 3); let current = run .path .platforms .get(current_index) .ok_or(JumpHopError::EmptyPath)?; - let target = run - .path + let target = path .platforms .get(next_index) .ok_or(JumpHopError::NoNextPlatform)?; - let capped_charge = charge_ms.min(run.path.scoring.max_charge_ms); - let jump_distance = capped_charge as f32 * run.path.scoring.charge_to_distance_ratio; + let capped_drag_distance = drag_distance.clamp(0.0, run.path.scoring.max_charge_ms as f32); + let jump_distance = capped_drag_distance * run.path.scoring.charge_to_distance_ratio; let vector_x = target.x - current.x; let vector_y = target.y - current.y; let target_distance = vector_x.hypot(vector_y).max(0.0001); - let unit_x = vector_x / target_distance; - let unit_y = vector_y / target_distance; + let (unit_x, unit_y) = normalize_jump_direction( + drag_vector_x, + drag_vector_y, + vector_x / target_distance, + vector_y / target_distance, + ); let landed_x = current.x + unit_x * jump_distance; let landed_y = current.y + unit_y * jump_distance; let landing_error = (landed_x - target.x).hypot(landed_y - target.y); + let target_landing_radius = target.landing_radius; let mut next = run.clone(); - let result = if landing_error <= target.perfect_radius { - if next_index as u32 == run.path.finish_index { - JumpHopJumpResultKind::Finish - } else { - JumpHopJumpResultKind::Perfect - } - } else if landing_error <= target.landing_radius { - if next_index as u32 == run.path.finish_index { - JumpHopJumpResultKind::Finish - } else { - JumpHopJumpResultKind::Hit - } + next.path = path; + let result = if landing_error <= target_landing_radius { + JumpHopJumpResultKind::Hit } else { JumpHopJumpResultKind::Miss }; next.last_jump = Some(JumpHopLastJump { - charge_ms: capped_charge, + charge_ms: capped_drag_distance.round() as u32, jump_distance, target_platform_index: next_index as u32, landed_x, @@ -166,23 +122,8 @@ pub fn apply_jump( } next.current_platform_index = next_index as u32; - next.combo = next.combo.saturating_add(1); - next.score = next.score.saturating_add(target.score_value); - if matches!( - result, - JumpHopJumpResultKind::Perfect | JumpHopJumpResultKind::Finish - ) { - next.score = next - .score - .saturating_add(run.path.scoring.perfect_bonus) - .saturating_add(next.combo.saturating_mul(run.path.scoring.hit_bonus)); - } else { - next.score = next.score.saturating_add(run.path.scoring.hit_bonus); - } - if result == JumpHopJumpResultKind::Finish { - next.status = JumpHopRunStatus::Cleared; - next.finished_at_ms = Some(jumped_at_ms); - } + next.combo = 0; + next.score = next.current_platform_index; Ok(next) } @@ -201,9 +142,31 @@ pub fn restart_run( ) } +fn normalize_jump_hop_path_platform_size(mut path: JumpHopPath) -> JumpHopPath { + let should_scale_legacy_path = path + .platforms + .iter() + .any(|platform| platform.width < 1.2 && platform.landing_radius < 0.75); + if !should_scale_legacy_path { + if (path.scoring.charge_to_distance_ratio - JUMP_HOP_CHARGE_TO_DISTANCE_RATIO).abs() + > f32::EPSILON + { + path.scoring.charge_to_distance_ratio = JUMP_HOP_CHARGE_TO_DISTANCE_RATIO; + } + return path; + } + + for platform in &mut path.platforms { + platform.width *= JUMP_HOP_PLATFORM_SIZE_MULTIPLIER; + platform.height *= JUMP_HOP_PLATFORM_SIZE_MULTIPLIER; + platform.landing_radius *= JUMP_HOP_PLATFORM_SIZE_MULTIPLIER; + platform.perfect_radius *= JUMP_HOP_PLATFORM_SIZE_MULTIPLIER; + } + path.scoring.charge_to_distance_ratio = JUMP_HOP_CHARGE_TO_DISTANCE_RATIO; + path +} + struct DifficultyConfig { - min_platforms: u32, - max_platforms: u32, min_gap: f32, max_gap: f32, min_width: f32, @@ -214,54 +177,143 @@ struct DifficultyConfig { max_charge_ms: u32, } +fn build_platforms_until( + seed: &str, + difficulty: JumpHopDifficulty, + required_count: usize, +) -> Vec { + let config = difficulty_config(difficulty); + let mut platforms = Vec::with_capacity(required_count); + let mut x = 0.0f32; + let mut y = 0.0f32; + + for index in 0..required_count { + platforms.push(build_platform(seed, difficulty, index, x, y, &config)); + if index + 1 < required_count { + let mut rng = DeterministicRng::new(seed, &format!("{}:{index}", difficulty.as_str())); + let distance = rng.range_f32(config.min_gap, config.max_gap); + let lane = rng.range_f32(0.42, 0.86); + let direction = if rng.next_u32() % 2 == 0 { 1.0 } else { -1.0 }; + x += distance * lane * direction; + y += distance; + } + } + + platforms +} + +fn build_platform( + seed: &str, + difficulty: JumpHopDifficulty, + index: usize, + x: f32, + y: f32, + config: &DifficultyConfig, +) -> JumpHopPlatform { + let mut rng = DeterministicRng::new(seed, &format!("platform:{}:{index}", difficulty.as_str())); + let tile_type = if index == 0 { + JumpHopTileType::Start + } else if index % 11 == 0 { + JumpHopTileType::Bonus + } else if index % 7 == 0 { + JumpHopTileType::Accent + } else if index % 3 == 0 { + JumpHopTileType::Target + } else { + JumpHopTileType::Normal + }; + let width = rng.range_f32(config.min_width, config.max_width); + let height = width * rng.range_f32(0.88, 1.06); + let landing_radius = width * config.landing_radius_factor; + + JumpHopPlatform { + platform_id: format!("jump-hop-platform-{index:05}"), + tile_type, + x, + y, + width: width * JUMP_HOP_PLATFORM_SIZE_MULTIPLIER, + height: height * JUMP_HOP_PLATFORM_SIZE_MULTIPLIER, + landing_radius: landing_radius * JUMP_HOP_PLATFORM_SIZE_MULTIPLIER, + perfect_radius: landing_radius + * config.perfect_radius_factor + * JUMP_HOP_PLATFORM_SIZE_MULTIPLIER, + score_value: 1, + } +} + +fn extend_jump_hop_path(mut path: JumpHopPath, required_count: usize) -> JumpHopPath { + if path.platforms.len() >= required_count { + return path; + } + path.platforms = build_platforms_until(&path.seed, path.difficulty, required_count); + path.finish_index = u32::MAX; + path +} + +fn normalize_jump_direction( + drag_vector_x: Option, + drag_vector_y: Option, + fallback_x: f32, + fallback_y: f32, +) -> (f32, f32) { + let Some(drag_x) = drag_vector_x.filter(|value| value.is_finite()) else { + return (fallback_x, fallback_y); + }; + let Some(drag_y) = drag_vector_y.filter(|value| value.is_finite()) else { + return (fallback_x, fallback_y); + }; + // å‰ç«¯æäº¤çš„æ˜¯å±å¹•拖拽å‘é‡ï¼šx è½´åŒå‘,y è½´å‘下为正。 + // 真实起跳需è¦â€œåå‘弹出â€ï¼ŒåŒæ—¶æŠŠå±å¹• y ç¿»å›žä¸–ç•Œåæ ‡çš„å‘上为正。 + let jump_x = -drag_x; + let jump_y = drag_y; + let length = jump_x.hypot(jump_y); + if length < 0.0001 { + (fallback_x, fallback_y) + } else { + (jump_x / length, jump_y / length) + } +} + fn difficulty_config(difficulty: JumpHopDifficulty) -> DifficultyConfig { match difficulty { JumpHopDifficulty::Easy => DifficultyConfig { - min_platforms: 12, - max_platforms: 14, min_gap: 1.0, max_gap: 1.45, min_width: 0.9, max_width: 1.08, landing_radius_factor: 0.62, perfect_radius_factor: 0.32, - charge_to_distance_ratio: 0.004, + charge_to_distance_ratio: JUMP_HOP_CHARGE_TO_DISTANCE_RATIO, max_charge_ms: 700, }, JumpHopDifficulty::Standard => DifficultyConfig { - min_platforms: 16, - max_platforms: 18, min_gap: 1.22, max_gap: 1.78, min_width: 0.82, max_width: 1.0, landing_radius_factor: 0.54, perfect_radius_factor: 0.26, - charge_to_distance_ratio: 0.004, + charge_to_distance_ratio: JUMP_HOP_CHARGE_TO_DISTANCE_RATIO, max_charge_ms: 780, }, JumpHopDifficulty::Advanced => DifficultyConfig { - min_platforms: 20, - max_platforms: 24, min_gap: 1.45, max_gap: 2.05, min_width: 0.72, max_width: 0.94, landing_radius_factor: 0.48, perfect_radius_factor: 0.22, - charge_to_distance_ratio: 0.004, + charge_to_distance_ratio: JUMP_HOP_CHARGE_TO_DISTANCE_RATIO, max_charge_ms: 860, }, JumpHopDifficulty::Challenge => DifficultyConfig { - min_platforms: 26, - max_platforms: 32, min_gap: 1.7, max_gap: 2.35, min_width: 0.66, max_width: 0.88, landing_radius_factor: 0.42, perfect_radius_factor: 0.18, - charge_to_distance_ratio: 0.004, + charge_to_distance_ratio: JUMP_HOP_CHARGE_TO_DISTANCE_RATIO, max_charge_ms: 950, }, } @@ -289,13 +341,6 @@ impl DeterministicRng { (self.state >> 32) as u32 } - fn range_u32(&mut self, min: u32, max: u32) -> u32 { - if max <= min { - return min; - } - min + self.next_u32() % (max - min + 1) - } - fn range_f32(&mut self, min: f32, max: f32) -> f32 { if max <= min { return min; @@ -319,14 +364,67 @@ mod tests { let challenge = generate_jump_hop_path("seed-a", JumpHopDifficulty::Challenge); assert_eq!(first, second); - assert!((16..=18).contains(&first.platforms.len())); - assert!((26..=32).contains(&challenge.platforms.len())); + assert_eq!(first.platforms.len(), 8); + assert_eq!(challenge.platforms.len(), 8); assert_eq!(first.platforms.first().unwrap().tile_type.as_str(), "start"); - assert_eq!(first.platforms.last().unwrap().tile_type.as_str(), "finish"); + assert_eq!(first.finish_index, u32::MAX); } #[test] - fn jump_resolution_distinguishes_perfect_hit_and_miss() { + fn difficulty_charge_to_distance_ratio_is_doubled() { + let easy = generate_jump_hop_path("seed-ratio-easy", JumpHopDifficulty::Easy); + let standard = generate_jump_hop_path("seed-ratio-standard", JumpHopDifficulty::Standard); + let advanced = generate_jump_hop_path("seed-ratio-advanced", JumpHopDifficulty::Advanced); + let challenge = generate_jump_hop_path("seed-ratio-challenge", JumpHopDifficulty::Challenge); + + assert_eq!(easy.scoring.charge_to_distance_ratio, 0.008); + assert_eq!(standard.scoring.charge_to_distance_ratio, 0.008); + assert_eq!(advanced.scoring.charge_to_distance_ratio, 0.008); + assert_eq!(challenge.scoring.charge_to_distance_ratio, 0.008); + } + + #[test] + fn generated_platforms_use_double_size_and_landing_radius() { + let path = generate_jump_hop_path("seed-size", JumpHopDifficulty::Standard); + let first_platform = path.platforms.first().expect("platform should exist"); + + assert!(first_platform.width >= 1.64); + assert!(first_platform.width <= 2.0); + assert!(first_platform.height >= 1.44); + assert!(first_platform.height <= 2.12); + assert!(first_platform.landing_radius >= 0.88); + assert!(first_platform.landing_radius <= 1.08); + } + + #[test] + fn start_run_normalizes_legacy_single_size_platforms() { + let mut path = generate_jump_hop_path("seed-legacy", JumpHopDifficulty::Standard); + for platform in &mut path.platforms { + platform.width /= 2.0; + platform.height /= 2.0; + platform.landing_radius /= 2.0; + platform.perfect_radius /= 2.0; + } + let legacy_width = path.platforms[0].width; + let legacy_landing_radius = path.platforms[0].landing_radius; + + let run = start_run( + "run-legacy".to_string(), + "user-legacy".to_string(), + "profile-legacy".to_string(), + path, + 100, + ) + .expect("run should start"); + + assert!((run.path.platforms[0].width - legacy_width * 2.0).abs() < 0.0001); + assert!( + (run.path.platforms[0].landing_radius - legacy_landing_radius * 2.0).abs() < 0.0001 + ); + } + + #[test] + fn jump_resolution_distinguishes_hit_and_miss() { let path = generate_jump_hop_path("seed-b", JumpHopDifficulty::Easy); let run = start_run( "run-1".to_string(), @@ -338,25 +436,25 @@ mod tests { .expect("run should start"); let target = &run.path.platforms[1]; let distance = target.x.hypot(target.y); - let perfect_charge = (distance / run.path.scoring.charge_to_distance_ratio) as u32; - - let perfect = apply_jump(&run, perfect_charge, 200).expect("jump should resolve"); - assert_eq!( - perfect.last_jump.as_ref().unwrap().result, - JumpHopJumpResultKind::Perfect - ); - assert_eq!(perfect.status, JumpHopRunStatus::Playing); - assert_eq!(perfect.current_platform_index, 1); + let target_charge = (distance / run.path.scoring.charge_to_distance_ratio) as u32; let hit = - apply_jump(&run, perfect_charge.saturating_add(80), 200).expect("jump should resolve"); + apply_jump(&run, target_charge as f32, None, None, 200).expect("jump should resolve"); assert_eq!( hit.last_jump.as_ref().unwrap().result, JumpHopJumpResultKind::Hit ); + assert_eq!(hit.status, JumpHopRunStatus::Playing); + assert_eq!(hit.current_platform_index, 1); - let miss = - apply_jump(&run, perfect_charge.saturating_add(900), 200).expect("jump should resolve"); + let miss = apply_jump( + &run, + target_charge.saturating_add(900) as f32, + None, + None, + 200, + ) + .expect("jump should resolve"); assert_eq!(miss.status, JumpHopRunStatus::Failed); assert_eq!( miss.last_jump.as_ref().unwrap().result, @@ -364,6 +462,39 @@ mod tests { ); } + #[test] + fn jump_resolution_uses_screen_drag_y_axis_for_forward_jump_direction() { + let path = generate_jump_hop_path("seed-screen-axis", JumpHopDifficulty::Easy); + let run = start_run( + "run-screen-axis".to_string(), + "user-screen-axis".to_string(), + "profile-screen-axis".to_string(), + path, + 100, + ) + .expect("run should start"); + let current = &run.path.platforms[0]; + let target = &run.path.platforms[1]; + let target_distance = (target.x - current.x).hypot(target.y - current.y); + let charge = (target_distance / run.path.scoring.charge_to_distance_ratio).round() as u32; + + let result = apply_jump( + &run, + charge as f32, + Some(-(target.x - current.x)), + Some(target.y - current.y), + 200, + ) + .expect("jump should resolve"); + + assert_eq!(result.status, JumpHopRunStatus::Playing); + assert_eq!( + result.last_jump.as_ref().unwrap().result, + JumpHopJumpResultKind::Hit + ); + assert_eq!(result.current_platform_index, 1); + } + #[test] fn restart_returns_to_first_platform_and_playing_state() { let path = generate_jump_hop_path("seed-c", JumpHopDifficulty::Easy); @@ -392,4 +523,32 @@ mod tests { assert_eq!(restarted.started_at_ms, 300); assert!(restarted.finished_at_ms.is_none()); } + + #[test] + fn successful_jump_extends_infinite_path_buffer_and_counts_jumps() { + let path = generate_jump_hop_path("seed-d", JumpHopDifficulty::Easy); + let mut run = start_run( + "run-1".to_string(), + "user-1".to_string(), + "profile-1".to_string(), + path, + 100, + ) + .expect("run should start"); + + for step in 0..9 { + let current = &run.path.platforms[run.current_platform_index as usize]; + let target = &run.path.platforms[run.current_platform_index as usize + 1]; + let distance = (target.x - current.x).hypot(target.y - current.y); + let charge = (distance / run.path.scoring.charge_to_distance_ratio).round() as u32; + run = apply_jump(&run, charge as f32, None, None, 200 + step) + .expect("jump should resolve"); + } + + assert_eq!(run.status, JumpHopRunStatus::Playing); + assert_eq!(run.current_platform_index, 9); + assert_eq!(run.score, 9); + assert!(run.path.platforms.len() >= 12); + assert!(run.finished_at_ms.is_none()); + } } diff --git a/server-rs/crates/module-runtime/src/application.rs b/server-rs/crates/module-runtime/src/application.rs index 2d997da6..060f0bac 100644 --- a/server-rs/crates/module-runtime/src/application.rs +++ b/server-rs/crates/module-runtime/src/application.rs @@ -120,9 +120,9 @@ pub fn default_creation_entry_type_snapshots( build_default_creation_entry_type_snapshot( "jump-hop", "跳一跳", - "俯视角跳跃闯关", + "主题驱动平å°è·³è·ƒ", "å¯åˆ›å»º", - "/creation-type-references/puzzle.webp", + "/creation-type-references/jump-hop.webp", true, true, 45, diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index 054ab40e..9d13e83d 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -293,6 +293,29 @@ mod tests { assert_eq!(wooden_fish.image_src, "/wooden-fish/default-hit-object.png"); } + #[test] + fn default_creation_entry_types_include_jump_hop_theme_only_entry() { + let configs = default_creation_entry_type_snapshots(1); + let jump_hop = configs + .iter() + .find(|item| item.id == "jump-hop") + .expect("jump-hop creation entry should be seeded"); + + assert_eq!(jump_hop.title, "跳一跳"); + assert_eq!(jump_hop.subtitle, "主题驱动平å°è·³è·ƒ"); + assert!(jump_hop.visible); + assert!(jump_hop.open); + assert_eq!(jump_hop.badge, "å¯åˆ›å»º"); + assert_eq!(jump_hop.sort_order, 45); + assert_eq!( + jump_hop.image_src, + "/creation-type-references/jump-hop.webp" + ); + assert_eq!(jump_hop.category_id, "recommended"); + assert_eq!(jump_hop.category_label, "热门推è"); + assert_eq!(jump_hop.category_sort_order, 20); + } + #[test] fn normalized_clamps_music_volume_into_valid_range() { let low = RuntimeSettings::normalized(-1.0, RuntimePlatformTheme::Light); diff --git a/server-rs/crates/shared-contracts/src/jump_hop.rs b/server-rs/crates/shared-contracts/src/jump_hop.rs index cd2c0a51..0684a314 100644 --- a/server-rs/crates/shared-contracts/src/jump_hop.rs +++ b/server-rs/crates/shared-contracts/src/jump_hop.rs @@ -44,7 +44,6 @@ pub enum JumpHopTileType { #[serde(rename_all = "kebab-case")] pub enum JumpHopActionType { CompileDraft, - RegenerateCharacter, RegenerateTiles, UpdateWorkMeta, UpdateDifficulty, @@ -71,12 +70,20 @@ pub enum JumpHopJumpResult { #[serde(rename_all = "camelCase")] pub struct JumpHopWorkspaceCreateRequest { pub template_id: String, + pub theme_text: String, + #[serde(default)] pub work_title: String, + #[serde(default)] pub work_description: String, + #[serde(default)] pub theme_tags: Vec, + #[serde(default = "default_jump_hop_difficulty")] pub difficulty: JumpHopDifficulty, + #[serde(default = "default_jump_hop_style_preset")] pub style_preset: JumpHopStylePreset, + #[serde(default)] pub character_prompt: String, + #[serde(default)] pub tile_prompt: String, #[serde(default)] pub end_mood_prompt: Option, @@ -89,6 +96,8 @@ pub struct JumpHopActionRequest { #[serde(default)] pub profile_id: Option, #[serde(default)] + pub theme_text: Option, + #[serde(default)] pub work_title: Option, #[serde(default)] pub work_description: Option, @@ -127,14 +136,30 @@ pub struct JumpHopCharacterAsset { pub height: u32, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopDefaultCharacter { + pub character_id: String, + pub display_name: String, + pub model_kind: String, + pub body_color: String, + pub accent_color: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct JumpHopTileAsset { pub tile_type: JumpHopTileType, + #[serde(default)] + pub tile_id: Option, pub image_src: String, pub image_object_key: String, pub asset_object_id: String, pub source_atlas_cell: String, + #[serde(default)] + pub atlas_row: Option, + #[serde(default)] + pub atlas_col: Option, pub visual_width: u32, pub visual_height: u32, pub top_surface_radius: f32, @@ -193,11 +218,14 @@ pub struct JumpHopDraftResponse { pub template_name: String, #[serde(default)] pub profile_id: Option, + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, pub difficulty: JumpHopDifficulty, pub style_preset: JumpHopStylePreset, + #[serde(default)] + pub default_character: Option, pub character_prompt: String, pub tile_prompt: String, #[serde(default)] @@ -251,6 +279,7 @@ pub struct JumpHopWorkSummaryResponse { pub owner_user_id: String, #[serde(default)] pub source_session_id: Option, + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, @@ -274,6 +303,8 @@ pub struct JumpHopWorkProfileResponse { pub summary: JumpHopWorkSummaryResponse, pub draft: JumpHopDraftResponse, pub path: JumpHopPath, + #[serde(default)] + pub default_character: Option, pub character_asset: JumpHopCharacterAsset, pub tile_atlas_asset: JumpHopCharacterAsset, pub tile_assets: Vec, @@ -305,6 +336,7 @@ pub struct JumpHopGalleryCardResponse { pub profile_id: String, pub owner_user_id: String, pub author_display_name: String, + pub theme_text: String, pub work_title: String, pub work_description: String, #[serde(default)] @@ -343,6 +375,8 @@ pub struct JumpHopRuntimeRunSnapshotResponse { pub owner_user_id: String, pub status: JumpHopRunStatus, pub current_platform_index: u32, + pub successful_jump_count: u32, + pub duration_ms: u64, pub score: u32, pub combo: u32, pub path: JumpHopPath, @@ -363,15 +397,29 @@ pub struct JumpHopRunResponse { #[serde(rename_all = "camelCase")] pub struct JumpHopStartRunRequest { pub profile_id: String, + #[serde(default)] + pub runtime_mode: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct JumpHopJumpRequest { - pub charge_ms: u32, + pub drag_distance: f32, + #[serde(default)] + pub drag_vector_x: Option, + #[serde(default)] + pub drag_vector_y: Option, pub client_event_id: String, } +fn default_jump_hop_difficulty() -> JumpHopDifficulty { + JumpHopDifficulty::Standard +} + +fn default_jump_hop_style_preset() -> JumpHopStylePreset { + JumpHopStylePreset::MinimalBlocks +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct JumpHopRestartRunRequest { @@ -384,6 +432,25 @@ pub struct JumpHopJumpResponse { pub run: JumpHopRuntimeRunSnapshotResponse, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopLeaderboardEntry { + pub rank: u32, + pub player_id: String, + pub successful_jump_count: u32, + pub duration_ms: u64, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopLeaderboardResponse { + pub profile_id: String, + pub items: Vec, + #[serde(default)] + pub viewer_best: Option, +} + #[cfg(test)] mod tests { use super::*; @@ -393,6 +460,7 @@ mod tests { fn jump_hop_workspace_request_uses_camel_case() { let payload = serde_json::to_value(JumpHopWorkspaceCreateRequest { template_id: "jump-hop".to_string(), + theme_text: "跳一跳".to_string(), work_title: "跳一跳".to_string(), work_description: "俯视角跳跃闯关".to_string(), theme_tags: vec!["休闲".to_string()], diff --git a/server-rs/crates/spacetime-client/src/jump_hop.rs b/server-rs/crates/spacetime-client/src/jump_hop.rs index 28a5ed25..0ba46095 100644 --- a/server-rs/crates/spacetime-client/src/jump_hop.rs +++ b/server-rs/crates/spacetime-client/src/jump_hop.rs @@ -1,15 +1,15 @@ use super::*; use crate::mapper::{ map_jump_hop_agent_session_procedure_result, map_jump_hop_gallery_card_view_row, - map_jump_hop_run_procedure_result, map_jump_hop_work_procedure_result, - map_jump_hop_works_procedure_result, + map_jump_hop_leaderboard_procedure_result, map_jump_hop_run_procedure_result, + map_jump_hop_work_procedure_result, map_jump_hop_works_procedure_result, }; use shared_contracts::jump_hop::{ JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset, JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryResponse, JumpHopGenerationStatus, - JumpHopJumpRequest, JumpHopRestartRunRequest, JumpHopRuntimeRunSnapshotResponse, - JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset, - JumpHopTileType, JumpHopWorkProfileResponse, + JumpHopJumpRequest, JumpHopLeaderboardResponse, JumpHopRestartRunRequest, + JumpHopRuntimeRunSnapshotResponse, JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, + JumpHopStylePreset, JumpHopWorkProfileResponse, }; use shared_kernel::build_prefixed_uuid_id; @@ -229,7 +229,7 @@ impl SpacetimeClient { let work = self .get_jump_hop_work_profile(profile_id, String::new()) .await?; - validate_jump_hop_runtime_ready(&work)?; + validate_jump_hop_runtime_ready(&work, "published")?; Ok(work) } @@ -242,13 +242,15 @@ impl SpacetimeClient { let work = self .get_jump_hop_work_profile(profile_id.clone(), String::new()) .await?; - validate_jump_hop_runtime_ready(&work)?; + let runtime_mode = normalize_jump_hop_runtime_mode(payload.runtime_mode.as_deref()); + validate_jump_hop_runtime_ready(&work, runtime_mode)?; let run_id = build_prefixed_uuid_id("jump-hop-run-"); let procedure_input = JumpHopRunStartInput { client_event_id: format!("{run_id}:start"), run_id, owner_user_id, profile_id, + runtime_mode: runtime_mode.to_string(), started_at_ms: current_unix_micros().div_euclid(1000), }; self.start_jump_hop_run_with_input(procedure_input).await @@ -303,7 +305,9 @@ impl SpacetimeClient { let procedure_input = JumpHopRunJumpInput { run_id, owner_user_id, - charge_ms: payload.charge_ms, + drag_distance: payload.drag_distance, + drag_vector_x: payload.drag_vector_x, + drag_vector_y: payload.drag_vector_y, client_event_id: payload.client_event_id, jumped_at_ms: current_unix_micros().div_euclid(1000), }; @@ -396,13 +400,39 @@ impl SpacetimeClient { self.get_jump_hop_work_profile(card.profile_id, String::new()) .await } + + pub async fn get_jump_hop_leaderboard( + &self, + profile_id: String, + viewer_player_id: String, + ) -> Result { + let procedure_input = JumpHopLeaderboardGetInput { + profile_id, + viewer_player_id, + limit: 50, + }; + + self.call_after_connect("get_jump_hop_leaderboard", move |connection, sender| { + connection.procedures().get_jump_hop_leaderboard_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_leaderboard_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } } fn validate_jump_hop_runtime_ready( work: &JumpHopWorkProfileResponse, + runtime_mode: &str, ) -> Result<(), SpacetimeClientError> { let status = work.summary.publication_status.trim().to_ascii_lowercase(); - if status != "published" { + if runtime_mode == "published" && status != "published" { return Err(SpacetimeClientError::validation_failed( "jump-hop runtime åªèƒ½å¯åЍ已å‘布作å“", )); @@ -412,11 +442,11 @@ fn validate_jump_hop_runtime_ready( "jump-hop runtime éœ€è¦ ready 状æ€ä½œå“", )); } - validate_jump_hop_character_asset_ready(&work.character_asset, "character_asset")?; - validate_jump_hop_character_asset_ready(&work.tile_atlas_asset, "tile_atlas_asset")?; - if work.tile_assets.is_empty() { + validate_jump_hop_default_character_ready(work)?; + validate_jump_hop_tile_atlas_asset_ready(&work.tile_atlas_asset, "tile_atlas_asset")?; + if work.tile_assets.len() < 25 { return Err(SpacetimeClientError::validation_failed( - "jump-hop runtime 缺少地å—资产", + "jump-hop runtime éœ€è¦ 25 个地å—资产", )); } for (index, asset) in work.tile_assets.iter().enumerate() { @@ -437,7 +467,34 @@ fn validate_jump_hop_runtime_ready( Ok(()) } -fn validate_jump_hop_character_asset_ready( +fn normalize_jump_hop_runtime_mode(value: Option<&str>) -> &'static str { + if value + .map(|value| value.trim().eq_ignore_ascii_case("draft")) + .unwrap_or(false) + { + "draft" + } else { + "published" + } +} + +fn validate_jump_hop_default_character_ready( + work: &JumpHopWorkProfileResponse, +) -> Result<(), SpacetimeClientError> { + let Some(default_character) = work.default_character.as_ref() else { + return Err(SpacetimeClientError::validation_failed( + "jump-hop runtime 缺少内置默认角色é…ç½®", + )); + }; + if default_character.model_kind.trim() != "builtin-three" { + return Err(SpacetimeClientError::validation_failed( + "jump-hop runtime 默认角色必须使用 builtin-three", + )); + } + Ok(()) +} + +fn validate_jump_hop_tile_atlas_asset_ready( asset: &JumpHopCharacterAsset, field: &str, ) -> Result<(), SpacetimeClientError> { @@ -475,7 +532,6 @@ enum JumpHopActionProcedure { #[derive(Clone, Copy)] enum JumpHopDraftMergeScope { CompileDraft, - RegenerateCharacter, RegenerateTiles, UpdateWorkMeta, UpdateDifficulty, @@ -484,7 +540,6 @@ enum JumpHopDraftMergeScope { #[derive(Clone, Copy)] enum JumpHopAssetRefresh { Preserve, - Character, Tiles, } @@ -496,12 +551,18 @@ fn build_jump_hop_action_plan( ) -> Result<(JumpHopActionProcedure, JumpHopDraftResponse), SpacetimeClientError> { let scope = match payload.action_type { JumpHopActionType::CompileDraft => JumpHopDraftMergeScope::CompileDraft, - JumpHopActionType::RegenerateCharacter => JumpHopDraftMergeScope::RegenerateCharacter, JumpHopActionType::RegenerateTiles => JumpHopDraftMergeScope::RegenerateTiles, JumpHopActionType::UpdateWorkMeta => JumpHopDraftMergeScope::UpdateWorkMeta, JumpHopActionType::UpdateDifficulty => JumpHopDraftMergeScope::UpdateDifficulty, }; - let mut draft = merge_action_into_draft(current.draft.clone(), payload, scope)?; + let mut base_draft = current.draft.clone(); + if matches!(payload.action_type, JumpHopActionType::RegenerateTiles) { + if let Some(draft) = base_draft.as_mut() { + draft.tile_atlas_asset = None; + draft.tile_assets.clear(); + } + } + let mut draft = merge_action_into_draft(base_draft, payload, scope)?; let profile_id = resolve_jump_hop_profile_id(&draft, &payload.action_type)?; draft.profile_id = Some(profile_id.clone()); @@ -514,16 +575,6 @@ fn build_jump_hop_action_plan( JumpHopAssetRefresh::Preserve, now_micros, )?), - JumpHopActionType::RegenerateCharacter => { - JumpHopActionProcedure::Compile(build_compile_input( - current, - owner_user_id, - &profile_id, - &mut draft, - JumpHopAssetRefresh::Character, - now_micros, - )?) - } JumpHopActionType::RegenerateTiles => JumpHopActionProcedure::Compile(build_compile_input( current, owner_user_id, @@ -563,6 +614,13 @@ fn merge_action_into_draft( { draft.work_title = value.trim().to_string(); } + if let Some(value) = payload + .theme_text + .as_ref() + .filter(|value| !value.trim().is_empty()) + { + draft.theme_text = value.trim().chars().take(60).collect(); + } if let Some(value) = payload.work_description.as_ref() { draft.work_description = value.trim().to_string(); } @@ -590,10 +648,7 @@ fn merge_action_into_draft( .filter(|value| !value.is_empty()); } } - if matches!( - scope, - JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateCharacter - ) { + if matches!(scope, JumpHopDraftMergeScope::CompileDraft) { if let Some(value) = payload .character_prompt .as_ref() @@ -622,10 +677,7 @@ fn merge_action_into_draft( { draft.profile_id = Some(profile_id.to_string()); } - if matches!( - scope, - JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateCharacter - ) { + if matches!(scope, JumpHopDraftMergeScope::CompileDraft) { if let Some(asset) = payload.character_asset.clone() { draft.character_asset = Some(asset); } @@ -665,28 +717,19 @@ fn build_compile_input( refresh: JumpHopAssetRefresh, now_micros: i64, ) -> Result { - let force_character = matches!(refresh, JumpHopAssetRefresh::Character); - let force_tiles = matches!(refresh, JumpHopAssetRefresh::Tiles); - if force_character { - draft.character_asset = None; - } - if force_tiles { - draft.tile_atlas_asset = None; - draft.tile_assets.clear(); - } - let character_asset = draft.character_asset.clone().ok_or_else(|| { - SpacetimeClientError::validation_failed( - "jump-hop compile-draft 缺少真实角色资产,请先由 api-server 生æˆå¹¶æŒä¹…化 asset_object", - ) - })?; + let character_asset = draft.character_asset.clone().unwrap_or_else(|| { + build_jump_hop_default_character_asset(profile_id, draft.theme_text.as_str()) + }); + draft.character_asset = Some(character_asset.clone()); + draft.default_character = Some(default_jump_hop_default_character()); let tile_atlas_asset = draft.tile_atlas_asset.clone().ok_or_else(|| { SpacetimeClientError::validation_failed( "jump-hop compile-draft 缺少真实地å—图集资产,请先由 api-server 生æˆå¹¶æŒä¹…化 asset_object", ) })?; - let tile_assets = if draft.tile_assets.is_empty() { + let tile_assets = if draft.tile_assets.len() < 25 { return Err(SpacetimeClientError::validation_failed( - "jump-hop compile-draft 缺少真实地å—资产,请先由 api-server 生æˆå¹¶æŒä¹…化 asset_object", + "jump-hop compile-draft éœ€è¦ 25 个真实地å—资产,请先由 api-server 生æˆå¹¶æŒä¹…化 asset_object", )); } else { draft.tile_assets.clone() @@ -705,7 +748,7 @@ fn build_compile_input( work_title: draft.work_title.clone(), work_description: draft.work_description.clone(), theme_tags_json: Some(json_string(&draft.theme_tags)?), - theme_text: Some(draft.work_title.clone()), + theme_text: Some(draft.theme_text.clone()), difficulty: Some(difficulty_to_str(&draft.difficulty).to_string()), style_preset: Some(style_to_str(&draft.style_preset).to_string()), character_prompt: Some(draft.character_prompt.clone()), @@ -785,13 +828,15 @@ fn default_draft() -> JumpHopDraftResponse { template_id: JUMP_HOP_TEMPLATE_ID.to_string(), template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), profile_id: None, + theme_text: JUMP_HOP_TEMPLATE_NAME.to_string(), work_title: JUMP_HOP_TEMPLATE_NAME.to_string(), work_description: "俯视角跳跃闯关".to_string(), theme_tags: vec!["跳一跳".to_string(), "休闲".to_string()], difficulty: JumpHopDifficulty::Standard, style_preset: JumpHopStylePreset::MinimalBlocks, - character_prompt: "俯视角å¯çˆ±ä¸»è§’ï¼Œé€æ˜ŽèƒŒæ™¯".to_string(), - tile_prompt: "ç­‰è·ç«‹ä½“地å—图集".to_string(), + default_character: Some(default_jump_hop_default_character()), + character_prompt: "内置默认 3D 角色".to_string(), + tile_prompt: "跳一跳主题的俯视角清爽游æˆåŒ–立体感平å°ç´ æ".to_string(), end_mood_prompt: None, character_asset: None, tile_atlas_asset: None, @@ -804,7 +849,7 @@ fn default_draft() -> JumpHopDraftResponse { fn build_config_json(draft: &JumpHopDraftResponse) -> Result { serde_json::to_string(&serde_json::json!({ - "themeText": draft.work_title, + "themeText": draft.theme_text, "difficulty": difficulty_to_str(&draft.difficulty), "stylePreset": style_to_str(&draft.style_preset), "characterPrompt": draft.character_prompt, @@ -814,94 +859,6 @@ fn build_config_json(draft: &JumpHopDraftResponse) -> Result, - profile_id: &str, - prompt: &str, - force_new: bool, - now_micros: i64, -) -> JumpHopCharacterAsset { - if !force_new { - if let Some(asset) = existing { - return asset; - } - } - let revision = force_new.then_some(now_micros); - let suffix = asset_revision_suffix(revision); - JumpHopCharacterAsset { - asset_id: format!("{profile_id}-character{suffix}"), - image_src: format!("/generated-jump-hop-assets/{profile_id}/character{suffix}.png"), - image_object_key: format!("generated-jump-hop-assets/{profile_id}/character{suffix}.png"), - asset_object_id: format!("{profile_id}-character{suffix}-object"), - generation_provider: "deterministic-placeholder".to_string(), - prompt: prompt.to_string(), - width: 768, - height: 768, - } -} - -fn ensure_tile_atlas_asset( - existing: Option, - profile_id: &str, - prompt: &str, - force_new: bool, - now_micros: i64, -) -> JumpHopCharacterAsset { - if !force_new { - if let Some(asset) = existing { - return asset; - } - } - let revision = force_new.then_some(now_micros); - let suffix = asset_revision_suffix(revision); - JumpHopCharacterAsset { - asset_id: format!("{profile_id}-tile-atlas{suffix}"), - image_src: format!("/generated-jump-hop-assets/{profile_id}/tile-atlas{suffix}.png"), - image_object_key: format!("generated-jump-hop-assets/{profile_id}/tile-atlas{suffix}.png"), - asset_object_id: format!("{profile_id}-tile-atlas{suffix}-object"), - generation_provider: "deterministic-placeholder".to_string(), - prompt: prompt.to_string(), - width: 1024, - height: 1024, - } -} - -fn ensure_tile_assets( - existing: Vec, - profile_id: &str, - force_new: bool, - now_micros: i64, -) -> Vec { - if !force_new && !existing.is_empty() { - return existing; - } - let suffix = asset_revision_suffix(force_new.then_some(now_micros)); - [ - JumpHopTileType::Start, - JumpHopTileType::Normal, - JumpHopTileType::Target, - JumpHopTileType::Finish, - JumpHopTileType::Bonus, - JumpHopTileType::Accent, - ] - .into_iter() - .enumerate() - .map(|(index, tile_type)| JumpHopTileAsset { - tile_type, - image_src: format!("/generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png"), - image_object_key: format!( - "generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png" - ), - asset_object_id: format!("{profile_id}-tile-{index}{suffix}-object"), - source_atlas_cell: format!("cell-{index}{suffix}"), - visual_width: 256, - visual_height: 192, - top_surface_radius: 42.0, - landing_radius: 34.0, - }) - .collect() -} - fn resolve_cover_composite( draft: &JumpHopDraftResponse, profile_id: &str, @@ -926,6 +883,22 @@ fn resolve_cover_composite( )) } +fn build_jump_hop_default_character_asset( + profile_id: &str, + theme_text: &str, +) -> JumpHopCharacterAsset { + JumpHopCharacterAsset { + asset_id: format!("{profile_id}-builtin-character"), + image_src: "builtin://jump-hop/default-character".to_string(), + image_object_key: String::new(), + asset_object_id: format!("{profile_id}-builtin-character"), + generation_provider: "builtin-three".to_string(), + prompt: format!("内置默认 3D 角色:{}", theme_text.trim()), + width: 0, + height: 0, + } +} + fn asset_revision_suffix(revision: Option) -> String { revision .filter(|value| *value > 0) @@ -957,6 +930,16 @@ fn style_to_str(value: &JumpHopStylePreset) -> &'static str { } } +fn default_jump_hop_default_character() -> shared_contracts::jump_hop::JumpHopDefaultCharacter { + shared_contracts::jump_hop::JumpHopDefaultCharacter { + character_id: "jump-hop-default-runner".to_string(), + display_name: "默认角色".to_string(), + model_kind: "builtin-three".to_string(), + body_color: "#f59e0b".to_string(), + accent_color: "#2563eb".to_string(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -968,8 +951,9 @@ mod tests { const NOW_MICROS: i64 = 1_763_456_789_000_000; #[test] - fn jump_hop_action_compile_draft_builds_compile_input_with_assets() { - let session = session_with_draft(draft_without_assets()); + fn jump_hop_action_compile_draft_builds_compile_input_with_25_tile_assets_and_builtin_character() + { + let session = session_with_draft(draft_without_character_asset()); let payload = action(JumpHopActionType::CompileDraft); let (plan, draft) = @@ -987,7 +971,7 @@ mod tests { .character_asset_json .as_deref() .unwrap_or("") - .contains("-character") + .contains("builtin-three") ); assert!( input @@ -1001,59 +985,19 @@ mod tests { .tile_assets_json .as_deref() .unwrap_or("") - .contains("tile-0-object") + .contains("old-tile-25-object") ); + assert_eq!(draft.tile_assets.len(), 25); assert_eq!(draft.generation_status, JumpHopGenerationStatus::Ready); } - #[test] - fn jump_hop_action_regenerate_character_replaces_only_character_asset_input() { - let session = session_with_draft(draft_with_assets()); - let mut payload = action(JumpHopActionType::RegenerateCharacter); - payload.character_prompt = Some("新的主角æç¤ºè¯".to_string()); - - let (plan, _draft) = - build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) - .expect("regenerate-character should build plan"); - - let JumpHopActionProcedure::Compile(input) = plan else { - panic!("regenerate-character should call compile_jump_hop_draft"); - }; - assert!( - !input - .character_asset_json - .as_deref() - .unwrap_or("") - .contains("old-character") - ); - assert!( - input - .character_asset_json - .as_deref() - .unwrap_or("") - .contains(&NOW_MICROS.to_string()) - ); - assert!( - input - .tile_atlas_asset_json - .as_deref() - .unwrap_or("") - .contains("old-tile-atlas") - ); - assert!( - input - .tile_assets_json - .as_deref() - .unwrap_or("") - .contains("old-normal-tile") - ); - } - #[test] fn jump_hop_action_regenerate_tiles_replaces_only_tile_asset_input() { let session = session_with_draft(draft_with_assets()); let mut payload = action(JumpHopActionType::RegenerateTiles); payload.tile_prompt = Some("æ–°çš„åœ°å—æç¤ºè¯".to_string()); + payload.tile_atlas_asset = Some(tile_atlas_asset("new-tile-atlas", NOW_MICROS)); + payload.tile_assets = Some(tile_assets("new", 25)); let (plan, _draft) = build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) @@ -1067,7 +1011,7 @@ mod tests { .character_asset_json .as_deref() .unwrap_or("") - .contains("old-character") + .contains("builtin-three") ); assert!( !input @@ -1081,24 +1025,43 @@ mod tests { .tile_assets_json .as_deref() .unwrap_or("") - .contains("old-normal-tile") + .contains("old-tile-01-object") ); assert!( input .tile_atlas_asset_json .as_deref() .unwrap_or("") - .contains(&NOW_MICROS.to_string()) + .contains("new-tile-atlas") ); assert!( input .tile_assets_json .as_deref() .unwrap_or("") - .contains(&NOW_MICROS.to_string()) + .contains("new-tile-25-object") ); } + #[test] + fn jump_hop_action_compile_draft_persists_theme_text_separately_from_title() { + let session = session_with_draft(draft_without_character_asset()); + let mut payload = action(JumpHopActionType::CompileDraft); + payload.theme_text = Some(" 森林蘑è‡è·³å° ".to_string()); + payload.work_title = Some("自动标题".to_string()); + + let (plan, draft) = + build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("compile-draft should build plan"); + + let JumpHopActionProcedure::Compile(input) = plan else { + panic!("compile-draft should call compile_jump_hop_draft"); + }; + assert_eq!(draft.theme_text, "森林蘑è‡è·³å°"); + assert_eq!(input.theme_text.as_deref(), Some("森林蘑è‡è·³å°")); + assert_eq!(input.work_title, "自动标题"); + } + #[test] fn jump_hop_action_update_work_meta_builds_update_input_without_asset_compile() { let session = session_with_draft(draft_with_assets()); @@ -1143,20 +1106,22 @@ mod tests { .character_asset .as_ref() .map(|asset| asset.asset_id.as_str()), - Some("old-character") + Some("jump-hop-profile-test-builtin-character") ); assert_eq!( draft .tile_assets .first() .map(|asset| asset.asset_object_id.as_str()), - Some("old-normal-tile-object") + Some("old-tile-01-object") ); } fn action(action_type: JumpHopActionType) -> JumpHopActionRequest { JumpHopActionRequest { action_type, + profile_id: None, + theme_text: None, work_title: None, work_description: None, theme_tags: None, @@ -1165,6 +1130,10 @@ mod tests { character_prompt: None, tile_prompt: None, end_mood_prompt: None, + character_asset: None, + tile_atlas_asset: None, + tile_assets: None, + cover_composite: None, } } @@ -1179,9 +1148,11 @@ mod tests { } } - fn draft_without_assets() -> JumpHopDraftResponse { + fn draft_without_character_asset() -> JumpHopDraftResponse { JumpHopDraftResponse { profile_id: None, + tile_atlas_asset: Some(tile_atlas_asset("old-tile-atlas", 0)), + tile_assets: tile_assets("old", 25), ..base_draft() } } @@ -1189,37 +1160,9 @@ mod tests { fn draft_with_assets() -> JumpHopDraftResponse { JumpHopDraftResponse { profile_id: Some(PROFILE_ID.to_string()), - character_asset: Some(JumpHopCharacterAsset { - asset_id: "old-character".to_string(), - image_src: "/generated-jump-hop-assets/old-character.png".to_string(), - image_object_key: "generated-jump-hop-assets/old-character.png".to_string(), - asset_object_id: "old-character-object".to_string(), - generation_provider: "old-provider".to_string(), - prompt: "旧角色æç¤ºè¯".to_string(), - width: 768, - height: 768, - }), - tile_atlas_asset: Some(JumpHopCharacterAsset { - asset_id: "old-tile-atlas".to_string(), - image_src: "/generated-jump-hop-assets/old-tile-atlas.png".to_string(), - image_object_key: "generated-jump-hop-assets/old-tile-atlas.png".to_string(), - asset_object_id: "old-tile-atlas-object".to_string(), - generation_provider: "old-provider".to_string(), - prompt: "æ—§åœ°å—æç¤ºè¯".to_string(), - width: 1024, - height: 1024, - }), - tile_assets: vec![JumpHopTileAsset { - tile_type: JumpHopTileType::Normal, - image_src: "/generated-jump-hop-assets/old-normal-tile.png".to_string(), - image_object_key: "generated-jump-hop-assets/old-normal-tile.png".to_string(), - asset_object_id: "old-normal-tile-object".to_string(), - source_atlas_cell: "old-cell".to_string(), - visual_width: 256, - visual_height: 192, - top_surface_radius: 42.0, - landing_radius: 34.0, - }], + character_asset: Some(build_jump_hop_default_character_asset(PROFILE_ID, "旧主题")), + tile_atlas_asset: Some(tile_atlas_asset("old-tile-atlas", 0)), + tile_assets: tile_assets("old", 25), path: Some(sample_jump_hop_path()), cover_composite: Some("/generated-jump-hop-assets/old-cover.png".to_string()), generation_status: JumpHopGenerationStatus::Ready, @@ -1227,16 +1170,58 @@ mod tests { } } + fn tile_atlas_asset(asset_id: &str, revision: i64) -> JumpHopCharacterAsset { + let suffix = asset_revision_suffix((revision > 0).then_some(revision)); + JumpHopCharacterAsset { + asset_id: asset_id.to_string(), + image_src: format!("/generated-jump-hop-assets/{asset_id}{suffix}.png"), + image_object_key: format!("generated-jump-hop-assets/{asset_id}{suffix}.png"), + asset_object_id: format!("{asset_id}-object"), + generation_provider: "vector-engine-image2".to_string(), + prompt: "æ—§åœ°å—æç¤ºè¯".to_string(), + width: 1024, + height: 1024, + } + } + + fn tile_assets(prefix: &str, count: usize) -> Vec { + (0..count) + .map(|index| JumpHopTileAsset { + tile_type: if index == 0 { + JumpHopTileType::Start + } else { + JumpHopTileType::Normal + }, + tile_id: Some(format!("tile-{:02}", index + 1)), + image_src: format!("/generated-jump-hop-assets/{prefix}-tile-{}.png", index + 1), + image_object_key: format!( + "generated-jump-hop-assets/{prefix}-tile-{}.png", + index + 1 + ), + asset_object_id: format!("{prefix}-tile-{:02}-object", index + 1), + source_atlas_cell: format!("row-{}-col-{}", index / 5 + 1, index % 5 + 1), + atlas_row: Some(index as u32 / 5 + 1), + atlas_col: Some(index as u32 % 5 + 1), + visual_width: 256, + visual_height: 192, + top_surface_radius: 42.0, + landing_radius: 34.0, + }) + .collect() + } + fn base_draft() -> JumpHopDraftResponse { JumpHopDraftResponse { template_id: JUMP_HOP_TEMPLATE_ID.to_string(), template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), profile_id: None, + theme_text: "旧主题".to_string(), work_title: "旧标题".to_string(), work_description: "æ—§æè¿°".to_string(), theme_tags: vec!["旧标签".to_string()], difficulty: JumpHopDifficulty::Standard, style_preset: JumpHopStylePreset::MinimalBlocks, + default_character: Some(default_jump_hop_default_character()), character_prompt: "旧角色æç¤ºè¯".to_string(), tile_prompt: "æ—§åœ°å—æç¤ºè¯".to_string(), end_mood_prompt: None, diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index fa080b9d..271f1be4 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -171,8 +171,8 @@ pub(crate) use self::inventory::{ }; pub(crate) use self::jump_hop::{ map_jump_hop_agent_session_procedure_result, map_jump_hop_gallery_card_view_row, - map_jump_hop_run_procedure_result, map_jump_hop_work_procedure_result, - map_jump_hop_works_procedure_result, + map_jump_hop_leaderboard_procedure_result, map_jump_hop_run_procedure_result, + map_jump_hop_work_procedure_result, map_jump_hop_works_procedure_result, }; pub(crate) use self::match3d::{ map_match3d_agent_session_procedure_result, map_match3d_click_item_procedure_result, diff --git a/server-rs/crates/spacetime-client/src/mapper/bark_battle.rs b/server-rs/crates/spacetime-client/src/mapper/bark_battle.rs index ae4c3253..19f51490 100644 --- a/server-rs/crates/spacetime-client/src/mapper/bark_battle.rs +++ b/server-rs/crates/spacetime-client/src/mapper/bark_battle.rs @@ -163,6 +163,7 @@ mod tests { let row = BarkBattleGalleryViewRow { work_id: "BB-33333333".to_string(), owner_user_id: "user-3".to_string(), + author_display_name: "声浪玩家".to_string(), source_draft_id: Some("bark-battle-draft-3".to_string()), config_version: 1, ruleset_version: "bark-battle-ruleset-v1".to_string(), diff --git a/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs b/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs index a2384840..e59a722d 100644 --- a/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs +++ b/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs @@ -1,10 +1,11 @@ use super::*; pub use shared_contracts::jump_hop::{ JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset, - JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse, + JumpHopDefaultCharacter, JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse, JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus, - JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath, - JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus, + JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, + JumpHopLeaderboardEntry, JumpHopLeaderboardResponse, JumpHopPath, JumpHopPlatform, + JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus, JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse, JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset, JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, @@ -61,6 +62,25 @@ pub(crate) fn map_jump_hop_run_procedure_result( Ok(map_jump_hop_run_snapshot(run)) } +pub(crate) fn map_jump_hop_leaderboard_procedure_result( + result: JumpHopLeaderboardProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + Ok(JumpHopLeaderboardResponse { + profile_id: result.profile_id, + items: result + .items + .into_iter() + .map(map_jump_hop_leaderboard_entry_snapshot) + .collect(), + viewer_best: result + .viewer_best + .map(map_jump_hop_leaderboard_entry_snapshot), + }) +} + pub(crate) fn map_jump_hop_gallery_card_view_row( row: JumpHopGalleryCardViewRow, ) -> JumpHopGalleryCardResponse { @@ -70,6 +90,7 @@ pub(crate) fn map_jump_hop_gallery_card_view_row( profile_id: row.profile_id, owner_user_id: row.owner_user_id, author_display_name: row.author_display_name, + theme_text: row.work_title.clone(), work_title: row.work_title, work_description: row.work_description, cover_image_src: empty_string_to_none(row.cover_image_src), @@ -108,11 +129,13 @@ fn map_jump_hop_work_snapshot( template_id: "jump-hop".to_string(), template_name: "跳一跳".to_string(), profile_id: Some(snapshot.profile_id.clone()), + theme_text: snapshot.work_title.clone(), work_title: snapshot.work_title.clone(), work_description: snapshot.work_description.clone(), theme_tags: snapshot.theme_tags.clone(), difficulty: parse_difficulty(&snapshot.difficulty), style_preset: parse_style_preset(&snapshot.style_preset), + default_character: Some(default_jump_hop_character()), character_prompt: snapshot.character_prompt.clone(), tile_prompt: snapshot.tile_prompt.clone(), end_mood_prompt: snapshot.end_mood_prompt.clone(), @@ -143,6 +166,7 @@ fn map_jump_hop_work_snapshot( profile_id: snapshot.profile_id, owner_user_id: snapshot.owner_user_id, source_session_id: empty_string_to_none(snapshot.source_session_id), + theme_text: snapshot.work_title.clone(), work_title: snapshot.work_title, work_description: snapshot.work_description, theme_tags: snapshot.theme_tags, @@ -159,6 +183,7 @@ fn map_jump_hop_work_snapshot( }, draft, path: map_jump_hop_path(snapshot.path), + default_character: Some(default_jump_hop_character()), character_asset, tile_atlas_asset, tile_assets: snapshot @@ -170,15 +195,18 @@ fn map_jump_hop_work_snapshot( } fn map_jump_hop_draft_snapshot(snapshot: JumpHopDraftSnapshot) -> JumpHopDraftResponse { + let theme_text = snapshot.work_title.clone(); JumpHopDraftResponse { template_id: snapshot.template_id, template_name: snapshot.template_name, profile_id: snapshot.profile_id, + theme_text, work_title: snapshot.work_title, work_description: snapshot.work_description, theme_tags: snapshot.theme_tags, difficulty: parse_difficulty(&snapshot.difficulty), style_preset: parse_style_preset(&snapshot.style_preset), + default_character: Some(default_jump_hop_character()), character_prompt: snapshot.character_prompt, tile_prompt: snapshot.tile_prompt, end_mood_prompt: snapshot.end_mood_prompt, @@ -211,10 +239,13 @@ fn map_character_asset(snapshot: JumpHopCharacterAssetSnapshot) -> JumpHopCharac fn map_tile_asset(snapshot: JumpHopTileAssetSnapshot) -> JumpHopTileAsset { JumpHopTileAsset { tile_type: parse_tile_type(&snapshot.tile_type), + tile_id: snapshot.tile_id, image_src: snapshot.image_src, image_object_key: snapshot.image_object_key, asset_object_id: snapshot.asset_object_id, source_atlas_cell: snapshot.source_atlas_cell, + atlas_row: snapshot.atlas_row, + atlas_col: snapshot.atlas_col, visual_width: snapshot.visual_width, visual_height: snapshot.visual_height, top_surface_radius: snapshot.top_surface_radius, @@ -263,6 +294,8 @@ fn map_jump_hop_run_snapshot(snapshot: JumpHopRunSnapshot) -> JumpHopRuntimeRunS crate::module_bindings::JumpHopRunStatus::Playing => JumpHopRunStatus::Playing, }, current_platform_index: snapshot.current_platform_index, + successful_jump_count: snapshot.current_platform_index, + duration_ms: jump_hop_duration_ms(snapshot.started_at_ms, snapshot.finished_at_ms), score: snapshot.score, combo: snapshot.combo, path: map_jump_hop_path(snapshot.path), @@ -286,6 +319,34 @@ fn map_jump_hop_run_snapshot(snapshot: JumpHopRunSnapshot) -> JumpHopRuntimeRunS } } +fn map_jump_hop_leaderboard_entry_snapshot( + snapshot: JumpHopLeaderboardEntrySnapshot, +) -> JumpHopLeaderboardEntry { + JumpHopLeaderboardEntry { + rank: snapshot.rank, + player_id: snapshot.player_id, + successful_jump_count: snapshot.successful_jump_count, + duration_ms: snapshot.duration_ms, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + +fn default_jump_hop_character() -> JumpHopDefaultCharacter { + JumpHopDefaultCharacter { + character_id: "jump-hop-default-runner".to_string(), + display_name: "默认角色".to_string(), + model_kind: "builtin-three".to_string(), + body_color: "#f59e0b".to_string(), + accent_color: "#2563eb".to_string(), + } +} + +fn jump_hop_duration_ms(started_at_ms: u64, finished_at_ms: Option) -> u64 { + finished_at_ms + .unwrap_or(started_at_ms) + .saturating_sub(started_at_ms) +} + fn parse_difficulty(value: &str) -> JumpHopDifficulty { match value { "easy" => JumpHopDifficulty::Easy, diff --git a/server-rs/crates/spacetime-client/src/mapper/runtime.rs b/server-rs/crates/spacetime-client/src/mapper/runtime.rs index 3e146d65..0d537266 100644 --- a/server-rs/crates/spacetime-client/src/mapper/runtime.rs +++ b/server-rs/crates/spacetime-client/src/mapper/runtime.rs @@ -280,7 +280,7 @@ pub(crate) fn build_creation_entry_config_record_from_rows( }, creation_types: creation_types .into_iter() - .map(|item| module_runtime::CreationEntryTypeSnapshot { + .map(|item| normalize_creation_entry_type_snapshot(module_runtime::CreationEntryTypeSnapshot { id: item.id, title: item.title, subtitle: item.subtitle, @@ -299,7 +299,7 @@ pub(crate) fn build_creation_entry_config_record_from_rows( ), category_sort_order: item.category_sort_order, updated_at_micros: item.updated_at.to_micros_since_unix_epoch(), - }) + })) .collect(), updated_at_micros: header.updated_at.to_micros_since_unix_epoch(), }, @@ -332,19 +332,21 @@ fn map_creation_entry_config_snapshot( creation_types: snapshot .creation_types .into_iter() - .map(|item| module_runtime::CreationEntryTypeSnapshot { - id: item.id, - title: item.title, - subtitle: item.subtitle, - badge: item.badge, - image_src: item.image_src, - visible: item.visible, - open: item.open, - sort_order: item.sort_order, - category_id: item.category_id, - category_label: item.category_label, - category_sort_order: item.category_sort_order, - updated_at_micros: item.updated_at_micros, + .map(|item| { + normalize_creation_entry_type_snapshot(module_runtime::CreationEntryTypeSnapshot { + id: item.id, + title: item.title, + subtitle: item.subtitle, + badge: item.badge, + image_src: item.image_src, + visible: item.visible, + open: item.open, + sort_order: item.sort_order, + category_id: item.category_id, + category_label: item.category_label, + category_sort_order: item.category_sort_order, + updated_at_micros: item.updated_at_micros, + }) }) .collect(), updated_at_micros: snapshot.updated_at_micros, @@ -358,6 +360,138 @@ fn creation_entry_text_or_default(value: Option, default_value: &str) -> .unwrap_or_else(|| default_value.to_string()) } +fn normalize_creation_entry_type_snapshot( + item: module_runtime::CreationEntryTypeSnapshot, +) -> module_runtime::CreationEntryTypeSnapshot { + // 中文注释:旧库里残留的跳一跳系统默认入å£è¡Œä»ä¼šä»Žè®¢é˜…缓存命中,这里统一åšè¯»æ¨¡åž‹çº å, + // 这样无论走订阅缓存还是 procedure 回退,创作页都åªä¼šçœ‹åˆ°æ–°çš„跳一跳入å£å£å¾„。 + if item.id == "jump-hop" + && item.title == "跳一跳" + && item.subtitle == "俯视角跳跃闯关" + && item.badge == "å¯åˆ›å»º" + && item.image_src == "/creation-type-references/puzzle.webp" + && item.visible + && item.open + && item.sort_order == 45 + { + return module_runtime::CreationEntryTypeSnapshot { + subtitle: "主题驱动平å°è·³è·ƒ".to_string(), + image_src: "/creation-type-references/jump-hop.webp".to_string(), + ..item + }; + } + + item +} + +#[cfg(test)] +mod tests { + use super::*; + use spacetimedb_sdk::Timestamp; + + fn build_creation_entry_header() -> CreationEntryConfig { + CreationEntryConfig { + config_id: "creation-entry-config".to_string(), + start_title: "新建作å“".to_string(), + start_description: "选择模æ¿åŽè¿›å…¥å¯¹åº”的创作表å•。".to_string(), + start_idle_badge: "æ¨¡æ¿ Tab".to_string(), + start_busy_badge: "正在开å¯".to_string(), + modal_title: "选择创作类型".to_string(), + modal_description: "先选玩法类型,å†è¿›å…¥å¯¹åº”创作工作å°ã€‚".to_string(), + updated_at: Timestamp::from_micros_since_unix_epoch(1_000_000), + event_title: None, + event_description: None, + event_cover_image_src: None, + event_prize_pool_mud_points: 0, + event_starts_at_text: None, + event_ends_at_text: None, + } + } + + fn build_old_jump_hop_row() -> CreationEntryTypeConfig { + CreationEntryTypeConfig { + id: "jump-hop".to_string(), + title: "跳一跳".to_string(), + subtitle: "俯视角跳跃闯关".to_string(), + badge: "å¯åˆ›å»º".to_string(), + image_src: "/creation-type-references/puzzle.webp".to_string(), + visible: true, + open: true, + sort_order: 45, + updated_at: Timestamp::from_micros_since_unix_epoch(2_000_000), + category_id: Some("recommended".to_string()), + category_label: Some("热门推è".to_string()), + category_sort_order: 20, + } + } + + #[test] + fn build_creation_entry_config_record_from_rows_normalizes_old_jump_hop_row() { + let record = build_creation_entry_config_record_from_rows( + build_creation_entry_header(), + vec![build_old_jump_hop_row()], + ); + + let jump_hop = record + .creation_types + .iter() + .find(|item| item.id == "jump-hop") + .expect("should contain jump-hop"); + + assert_eq!(jump_hop.subtitle, "主题驱动平å°è·³è·ƒ"); + assert_eq!(jump_hop.image_src, "/creation-type-references/jump-hop.webp"); + } + + #[test] + fn map_creation_entry_config_snapshot_normalizes_old_jump_hop_snapshot() { + let record = map_creation_entry_config_snapshot(CreationEntryConfigSnapshot { + config_id: "creation-entry-config".to_string(), + start_card: CreationEntryStartCardSnapshot { + title: "新建作å“".to_string(), + description: "选择模æ¿åŽè¿›å…¥å¯¹åº”的创作表å•。".to_string(), + idle_badge: "æ¨¡æ¿ Tab".to_string(), + busy_badge: "正在开å¯".to_string(), + }, + type_modal: CreationEntryTypeModalSnapshot { + title: "选择创作类型".to_string(), + description: "先选玩法类型,å†è¿›å…¥å¯¹åº”创作工作å°ã€‚".to_string(), + }, + event_banner: CreationEntryEventBannerSnapshot { + title: "主题创作赛".to_string(), + description: "用温暖的色彩,æå‡ºç§‹å¤©çš„æ•…事。".to_string(), + cover_image_src: "/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png".to_string(), + prize_pool_mud_points: 58_000, + starts_at_text: "2024.10.20 10:00".to_string(), + ends_at_text: "2024.11.20 23:59".to_string(), + }, + creation_types: vec![CreationEntryTypeSnapshot { + id: "jump-hop".to_string(), + title: "跳一跳".to_string(), + subtitle: "俯视角跳跃闯关".to_string(), + badge: "å¯åˆ›å»º".to_string(), + image_src: "/creation-type-references/puzzle.webp".to_string(), + visible: true, + open: true, + sort_order: 45, + category_id: "recommended".to_string(), + category_label: "热门推è".to_string(), + category_sort_order: 20, + updated_at_micros: 2_000_000, + }], + updated_at_micros: 1_000_000, + }); + + let jump_hop = record + .creation_types + .iter() + .find(|item| item.id == "jump-hop") + .expect("should contain jump-hop"); + + assert_eq!(jump_hop.subtitle, "主题驱动平å°è·³è·ƒ"); + assert_eq!(jump_hop.image_src, "/creation-type-references/jump-hop.webp"); + } +} + pub(crate) fn map_runtime_setting_procedure_result( result: RuntimeSettingProcedureResult, ) -> Result { diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index bcd66689..e1d4c952 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -365,6 +365,7 @@ pub mod get_custom_world_gallery_detail_by_code_procedure; pub mod get_custom_world_gallery_detail_procedure; pub mod get_custom_world_library_detail_procedure; pub mod get_jump_hop_agent_session_procedure; +pub mod get_jump_hop_leaderboard_procedure; pub mod get_jump_hop_run_procedure; pub mod get_jump_hop_work_profile_procedure; pub mod get_match_3_d_agent_session_procedure; @@ -433,6 +434,11 @@ pub mod jump_hop_gallery_view_table; pub mod jump_hop_jump_procedure; pub mod jump_hop_jump_result_kind_type; pub mod jump_hop_last_jump_type; +pub mod jump_hop_leaderboard_entry_row_type; +pub mod jump_hop_leaderboard_entry_snapshot_type; +pub mod jump_hop_leaderboard_entry_table; +pub mod jump_hop_leaderboard_get_input_type; +pub mod jump_hop_leaderboard_procedure_result_type; pub mod jump_hop_path_type; pub mod jump_hop_platform_type; pub mod jump_hop_run_get_input_type; @@ -1404,6 +1410,7 @@ pub use get_custom_world_gallery_detail_by_code_procedure::get_custom_world_gall pub use get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail; pub use get_custom_world_library_detail_procedure::get_custom_world_library_detail; pub use get_jump_hop_agent_session_procedure::get_jump_hop_agent_session; +pub use get_jump_hop_leaderboard_procedure::get_jump_hop_leaderboard; pub use get_jump_hop_run_procedure::get_jump_hop_run; pub use get_jump_hop_work_profile_procedure::get_jump_hop_work_profile; pub use get_match_3_d_agent_session_procedure::get_match_3_d_agent_session; @@ -1472,6 +1479,11 @@ pub use jump_hop_gallery_view_table::*; pub use jump_hop_jump_procedure::jump_hop_jump; pub use jump_hop_jump_result_kind_type::JumpHopJumpResultKind; pub use jump_hop_last_jump_type::JumpHopLastJump; +pub use jump_hop_leaderboard_entry_row_type::JumpHopLeaderboardEntryRow; +pub use jump_hop_leaderboard_entry_snapshot_type::JumpHopLeaderboardEntrySnapshot; +pub use jump_hop_leaderboard_entry_table::*; +pub use jump_hop_leaderboard_get_input_type::JumpHopLeaderboardGetInput; +pub use jump_hop_leaderboard_procedure_result_type::JumpHopLeaderboardProcedureResult; pub use jump_hop_path_type::JumpHopPath; pub use jump_hop_platform_type::JumpHopPlatform; pub use jump_hop_run_get_input_type::JumpHopRunGetInput; @@ -2400,6 +2412,7 @@ pub struct DbUpdate { jump_hop_event: __sdk::TableUpdate, jump_hop_gallery_card_view: __sdk::TableUpdate, jump_hop_gallery_view: __sdk::TableUpdate, + jump_hop_leaderboard_entry: __sdk::TableUpdate, jump_hop_runtime_run: __sdk::TableUpdate, jump_hop_work_profile: __sdk::TableUpdate, match_3_d_agent_message: __sdk::TableUpdate, @@ -2614,6 +2627,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "jump_hop_gallery_view" => db_update.jump_hop_gallery_view.append( jump_hop_gallery_view_table::parse_table_update(table_update)?, ), + "jump_hop_leaderboard_entry" => db_update.jump_hop_leaderboard_entry.append( + jump_hop_leaderboard_entry_table::parse_table_update(table_update)?, + ), "jump_hop_runtime_run" => db_update.jump_hop_runtime_run.append( jump_hop_runtime_run_table::parse_table_update(table_update)?, ), @@ -3043,6 +3059,12 @@ impl __sdk::DbUpdate for DbUpdate { diff.jump_hop_event = cache .apply_diff_to_table::("jump_hop_event", &self.jump_hop_event) .with_updates_by_pk(|row| &row.event_id); + diff.jump_hop_leaderboard_entry = cache + .apply_diff_to_table::( + "jump_hop_leaderboard_entry", + &self.jump_hop_leaderboard_entry, + ) + .with_updates_by_pk(|row| &row.entry_id); diff.jump_hop_runtime_run = cache .apply_diff_to_table::( "jump_hop_runtime_run", @@ -3528,6 +3550,9 @@ impl __sdk::DbUpdate for DbUpdate { "jump_hop_gallery_view" => db_update .jump_hop_gallery_view .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "jump_hop_leaderboard_entry" => db_update + .jump_hop_leaderboard_entry + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "jump_hop_runtime_run" => db_update .jump_hop_runtime_run .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -3871,6 +3896,9 @@ impl __sdk::DbUpdate for DbUpdate { "jump_hop_gallery_view" => db_update .jump_hop_gallery_view .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "jump_hop_leaderboard_entry" => db_update + .jump_hop_leaderboard_entry + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "jump_hop_runtime_run" => db_update .jump_hop_runtime_run .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -4130,6 +4158,7 @@ pub struct AppliedDiff<'r> { jump_hop_event: __sdk::TableAppliedDiff<'r, JumpHopEventRow>, jump_hop_gallery_card_view: __sdk::TableAppliedDiff<'r, JumpHopGalleryCardViewRow>, jump_hop_gallery_view: __sdk::TableAppliedDiff<'r, JumpHopGalleryViewRow>, + jump_hop_leaderboard_entry: __sdk::TableAppliedDiff<'r, JumpHopLeaderboardEntryRow>, jump_hop_runtime_run: __sdk::TableAppliedDiff<'r, JumpHopRuntimeRunRow>, jump_hop_work_profile: __sdk::TableAppliedDiff<'r, JumpHopWorkProfileRow>, match_3_d_agent_message: __sdk::TableAppliedDiff<'r, Match3DAgentMessageRow>, @@ -4422,6 +4451,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.jump_hop_gallery_view, event, ); + callbacks.invoke_table_row_callbacks::( + "jump_hop_leaderboard_entry", + &self.jump_hop_leaderboard_entry, + event, + ); callbacks.invoke_table_row_callbacks::( "jump_hop_runtime_run", &self.jump_hop_runtime_run, @@ -5444,6 +5478,7 @@ impl __sdk::SpacetimeModule for RemoteModule { jump_hop_event_table::register_table(client_cache); jump_hop_gallery_card_view_table::register_table(client_cache); jump_hop_gallery_view_table::register_table(client_cache); + jump_hop_leaderboard_entry_table::register_table(client_cache); jump_hop_runtime_run_table::register_table(client_cache); jump_hop_work_profile_table::register_table(client_cache); match_3_d_agent_message_table::register_table(client_cache); @@ -5556,6 +5591,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "jump_hop_event", "jump_hop_gallery_card_view", "jump_hop_gallery_view", + "jump_hop_leaderboard_entry", "jump_hop_runtime_run", "jump_hop_work_profile", "match_3_d_agent_message", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_leaderboard_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_leaderboard_procedure.rs new file mode 100644 index 00000000..519e5acd --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_leaderboard_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::jump_hop_leaderboard_get_input_type::JumpHopLeaderboardGetInput; +use super::jump_hop_leaderboard_procedure_result_type::JumpHopLeaderboardProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetJumpHopLeaderboardArgs { + pub input: JumpHopLeaderboardGetInput, +} + +impl __sdk::InModule for GetJumpHopLeaderboardArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_jump_hop_leaderboard`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_jump_hop_leaderboard { + fn get_jump_hop_leaderboard(&self, input: JumpHopLeaderboardGetInput) { + self.get_jump_hop_leaderboard_then(input, |_, _| {}); + } + + fn get_jump_hop_leaderboard_then( + &self, + input: JumpHopLeaderboardGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_jump_hop_leaderboard for super::RemoteProcedures { + fn get_jump_hop_leaderboard_then( + &self, + input: JumpHopLeaderboardGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopLeaderboardProcedureResult>( + "get_jump_hop_leaderboard", + GetJumpHopLeaderboardArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_row_type.rs new file mode 100644 index 00000000..369cbcce --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_row_type.rs @@ -0,0 +1,72 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopLeaderboardEntryRow { + pub entry_id: String, + pub profile_id: String, + pub player_id: String, + pub successful_jump_count: u32, + pub duration_ms: u64, + pub run_id: String, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for JumpHopLeaderboardEntryRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `JumpHopLeaderboardEntryRow`. +/// +/// Provides typed access to columns for query building. +pub struct JumpHopLeaderboardEntryRowCols { + pub entry_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub player_id: __sdk::__query_builder::Col, + pub successful_jump_count: __sdk::__query_builder::Col, + pub duration_ms: __sdk::__query_builder::Col, + pub run_id: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for JumpHopLeaderboardEntryRow { + type Cols = JumpHopLeaderboardEntryRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + JumpHopLeaderboardEntryRowCols { + entry_id: __sdk::__query_builder::Col::new(table_name, "entry_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + player_id: __sdk::__query_builder::Col::new(table_name, "player_id"), + successful_jump_count: __sdk::__query_builder::Col::new( + table_name, + "successful_jump_count", + ), + duration_ms: __sdk::__query_builder::Col::new(table_name, "duration_ms"), + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `JumpHopLeaderboardEntryRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct JumpHopLeaderboardEntryRowIxCols { + pub entry_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for JumpHopLeaderboardEntryRow { + type IxCols = JumpHopLeaderboardEntryRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + JumpHopLeaderboardEntryRowIxCols { + entry_id: __sdk::__query_builder::IxCol::new(table_name, "entry_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for JumpHopLeaderboardEntryRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_snapshot_type.rs new file mode 100644 index 00000000..f8269a17 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_snapshot_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopLeaderboardEntrySnapshot { + pub rank: u32, + pub player_id: String, + pub successful_jump_count: u32, + pub duration_ms: u64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for JumpHopLeaderboardEntrySnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_table.rs new file mode 100644 index 00000000..1d6ea6ec --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_entry_table.rs @@ -0,0 +1,166 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::jump_hop_leaderboard_entry_row_type::JumpHopLeaderboardEntryRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `jump_hop_leaderboard_entry`. +/// +/// Obtain a handle from the [`JumpHopLeaderboardEntryTableAccess::jump_hop_leaderboard_entry`] method on [`super::RemoteTables`], +/// like `ctx.db.jump_hop_leaderboard_entry()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_leaderboard_entry().on_insert(...)`. +pub struct JumpHopLeaderboardEntryTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `jump_hop_leaderboard_entry`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait JumpHopLeaderboardEntryTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`JumpHopLeaderboardEntryTableHandle`], which mediates access to the table `jump_hop_leaderboard_entry`. + fn jump_hop_leaderboard_entry(&self) -> JumpHopLeaderboardEntryTableHandle<'_>; +} + +impl JumpHopLeaderboardEntryTableAccess for super::RemoteTables { + fn jump_hop_leaderboard_entry(&self) -> JumpHopLeaderboardEntryTableHandle<'_> { + JumpHopLeaderboardEntryTableHandle { + imp: self + .imp + .get_table::("jump_hop_leaderboard_entry"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct JumpHopLeaderboardEntryInsertCallbackId(__sdk::CallbackId); +pub struct JumpHopLeaderboardEntryDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for JumpHopLeaderboardEntryTableHandle<'ctx> { + type Row = JumpHopLeaderboardEntryRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = JumpHopLeaderboardEntryInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopLeaderboardEntryInsertCallbackId { + JumpHopLeaderboardEntryInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: JumpHopLeaderboardEntryInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = JumpHopLeaderboardEntryDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopLeaderboardEntryDeleteCallbackId { + JumpHopLeaderboardEntryDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: JumpHopLeaderboardEntryDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct JumpHopLeaderboardEntryUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for JumpHopLeaderboardEntryTableHandle<'ctx> { + type UpdateCallbackId = JumpHopLeaderboardEntryUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> JumpHopLeaderboardEntryUpdateCallbackId { + JumpHopLeaderboardEntryUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: JumpHopLeaderboardEntryUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `entry_id` unique index on the table `jump_hop_leaderboard_entry`, +/// which allows point queries on the field of the same name +/// via the [`JumpHopLeaderboardEntryEntryIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_leaderboard_entry().entry_id().find(...)`. +pub struct JumpHopLeaderboardEntryEntryIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> JumpHopLeaderboardEntryTableHandle<'ctx> { + /// Get a handle on the `entry_id` unique index on the table `jump_hop_leaderboard_entry`. + pub fn entry_id(&self) -> JumpHopLeaderboardEntryEntryIdUnique<'ctx> { + JumpHopLeaderboardEntryEntryIdUnique { + imp: self.imp.get_unique_constraint::("entry_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> JumpHopLeaderboardEntryEntryIdUnique<'ctx> { + /// Find the subscribed row whose `entry_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("jump_hop_leaderboard_entry"); + _table.add_unique_constraint::("entry_id", |row| &row.entry_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `JumpHopLeaderboardEntryRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait jump_hop_leaderboard_entryQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `JumpHopLeaderboardEntryRow`. + fn jump_hop_leaderboard_entry( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl jump_hop_leaderboard_entryQueryTableAccess for __sdk::QueryTableAccessor { + fn jump_hop_leaderboard_entry( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("jump_hop_leaderboard_entry") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_get_input_type.rs new file mode 100644 index 00000000..0c66a38b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_get_input_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopLeaderboardGetInput { + pub profile_id: String, + pub viewer_player_id: String, + pub limit: u32, +} + +impl __sdk::InModule for JumpHopLeaderboardGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_procedure_result_type.rs new file mode 100644 index 00000000..b1ff0a33 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_leaderboard_procedure_result_type.rs @@ -0,0 +1,21 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::jump_hop_leaderboard_entry_snapshot_type::JumpHopLeaderboardEntrySnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopLeaderboardProcedureResult { + pub ok: bool, + pub profile_id: String, + pub items: Vec, + pub viewer_best: Option, + pub error_message: Option, +} + +impl __sdk::InModule for JumpHopLeaderboardProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_jump_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_jump_input_type.rs index e73b5530..090fbea8 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_jump_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_jump_input_type.rs @@ -9,7 +9,9 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; pub struct JumpHopRunJumpInput { pub run_id: String, pub owner_user_id: String, - pub charge_ms: u32, + pub drag_distance: f32, + pub drag_vector_x: Option, + pub drag_vector_y: Option, pub client_event_id: String, pub jumped_at_ms: i64, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_start_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_start_input_type.rs index 40578dae..d5c00ddf 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_start_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_start_input_type.rs @@ -10,6 +10,7 @@ pub struct JumpHopRunStartInput { pub run_id: String, pub owner_user_id: String, pub profile_id: String, + pub runtime_mode: String, pub client_event_id: String, pub started_at_ms: i64, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_asset_snapshot_type.rs index 6874988f..9ca1fe02 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_asset_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_asset_snapshot_type.rs @@ -8,10 +8,13 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[sats(crate = __lib)] pub struct JumpHopTileAssetSnapshot { pub tile_type: String, + pub tile_id: Option, pub image_src: String, pub image_object_key: String, pub asset_object_id: String, pub source_atlas_cell: String, + pub atlas_row: Option, + pub atlas_col: Option, pub visual_width: u32, pub visual_height: u32, pub top_surface_radius: f32, diff --git a/server-rs/crates/spacetime-client/src/wooden_fish.rs b/server-rs/crates/spacetime-client/src/wooden_fish.rs index 66304b09..2a3e36eb 100644 --- a/server-rs/crates/spacetime-client/src/wooden_fish.rs +++ b/server-rs/crates/spacetime-client/src/wooden_fish.rs @@ -801,6 +801,7 @@ mod tests { const SESSION_ID: &str = "wooden-fish-session-test"; const OWNER_USER_ID: &str = "user-test"; + const AUTHOR_DISPLAY_NAME: &str = "木鱼作者"; const PROFILE_ID: &str = "wooden-fish-profile-test"; const NOW_MICROS: i64 = 1_763_456_789_000_000; @@ -814,7 +815,13 @@ mod tests { payload.hit_sound_asset = Some(generated_hit_sound_asset("generated-compile-sound")); let (plan, draft) = - build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + build_wooden_fish_action_plan( + &session, + OWNER_USER_ID, + AUTHOR_DISPLAY_NAME, + &payload, + NOW_MICROS, + ) .expect("compile-draft should build plan"); let WoodenFishActionProcedure::Compile(input) = plan else { @@ -863,7 +870,13 @@ mod tests { payload.back_button_asset = Some(generated_back_button_asset("generated-compile-back")); let error = - match build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) { + match build_wooden_fish_action_plan( + &session, + OWNER_USER_ID, + AUTHOR_DISPLAY_NAME, + &payload, + NOW_MICROS, + ) { Ok(_) => panic!("compile-draft should not synthesize fake hit sound assets"), Err(error) => error, }; @@ -884,7 +897,13 @@ mod tests { payload.back_button_asset = Some(generated_back_button_asset("generated-compile-back")); let error = - match build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) { + match build_wooden_fish_action_plan( + &session, + OWNER_USER_ID, + AUTHOR_DISPLAY_NAME, + &payload, + NOW_MICROS, + ) { Ok(_) => panic!("compile-draft should not publish without background asset"), Err(error) => error, }; @@ -905,7 +924,13 @@ mod tests { payload.hit_sound_asset = Some(generated_hit_sound_asset("generated-compile-sound")); let error = - match build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) { + match build_wooden_fish_action_plan( + &session, + OWNER_USER_ID, + AUTHOR_DISPLAY_NAME, + &payload, + NOW_MICROS, + ) { Ok(_) => panic!("compile-draft should not publish without back button asset"), Err(error) => error, }; @@ -927,7 +952,13 @@ mod tests { payload.back_button_asset = Some(generated_back_button_asset("generated-back")); let (plan, _draft) = - build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + build_wooden_fish_action_plan( + &session, + OWNER_USER_ID, + AUTHOR_DISPLAY_NAME, + &payload, + NOW_MICROS, + ) .expect("regenerate-hit-object should build plan"); let WoodenFishActionProcedure::Compile(input) = plan else { @@ -988,7 +1019,13 @@ mod tests { ]); let (plan, draft) = - build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + build_wooden_fish_action_plan( + &session, + OWNER_USER_ID, + AUTHOR_DISPLAY_NAME, + &payload, + NOW_MICROS, + ) .expect("update-floating-words should build plan"); let WoodenFishActionProcedure::Update(input) = plan else { diff --git a/server-rs/crates/spacetime-module/src/jump_hop.rs b/server-rs/crates/spacetime-module/src/jump_hop.rs index 6f73da86..e78e5dd7 100644 --- a/server-rs/crates/spacetime-module/src/jump_hop.rs +++ b/server-rs/crates/spacetime-module/src/jump_hop.rs @@ -245,6 +245,29 @@ pub fn restart_jump_hop_run( } } +#[spacetimedb::procedure] +pub fn get_jump_hop_leaderboard( + ctx: &mut ProcedureContext, + input: JumpHopLeaderboardGetInput, +) -> JumpHopLeaderboardProcedureResult { + match ctx.try_with_tx(|tx| get_jump_hop_leaderboard_tx(tx, input.clone())) { + Ok((profile_id, items, viewer_best)) => JumpHopLeaderboardProcedureResult { + ok: true, + profile_id, + items, + viewer_best, + error_message: None, + }, + Err(message) => JumpHopLeaderboardProcedureResult { + ok: false, + profile_id: input.profile_id, + items: Vec::new(), + viewer_best: None, + error_message: Some(message), + }, + } +} + fn create_jump_hop_agent_session_tx( ctx: &ReducerContext, input: JumpHopAgentSessionCreateInput, @@ -543,6 +566,12 @@ fn start_jump_hop_run_tx( ) -> Result { require_non_empty(&input.run_id, "jump_hop run_id")?; let work = find_work(ctx, &input.profile_id)?; + let runtime_mode = normalize_runtime_mode(&input.runtime_mode); + if runtime_mode == JUMP_HOP_RUNTIME_MODE_PUBLISHED + && work.publication_status != JUMP_HOP_PUBLICATION_PUBLISHED + { + return Err("jump_hop published runtime åªèƒ½å¯åЍ已å‘布作å“".to_string()); + } let path = parse_json::(&work.path_json)?; let domain_run = start_run( input.run_id.clone(), @@ -554,7 +583,9 @@ fn start_jump_hop_run_tx( .map_err(|error| error.to_string())?; let snapshot = domain_run; upsert_run(ctx, &snapshot, input.started_at_ms); - increment_work_play_count(ctx, &work, input.started_at_ms); + if runtime_mode == JUMP_HOP_RUNTIME_MODE_PUBLISHED { + increment_work_play_count(ctx, &work, input.started_at_ms); + } insert_event( ctx, input.client_event_id, @@ -582,10 +613,19 @@ fn jump_hop_jump_tx( ) -> Result { let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?; let snapshot = parse_json::(&row.snapshot_json)?; - let domain_next = apply_jump(&snapshot, input.charge_ms, input.jumped_at_ms as u64) - .map_err(|error| error.to_string())?; + let domain_next = apply_jump( + &snapshot, + input.drag_distance, + input.drag_vector_x, + input.drag_vector_y, + input.jumped_at_ms as u64, + ) + .map_err(|error| error.to_string())?; let next = domain_next; replace_run(ctx, &row, &next, input.jumped_at_ms); + if next.status == module_jump_hop::JumpHopRunStatus::Failed { + upsert_jump_hop_leaderboard_entry(ctx, &next, input.jumped_at_ms); + } insert_event( ctx, input.client_event_id, @@ -602,6 +642,47 @@ fn jump_hop_jump_tx( Ok(next) } +fn get_jump_hop_leaderboard_tx( + ctx: &ReducerContext, + input: JumpHopLeaderboardGetInput, +) -> Result< + ( + String, + Vec, + Option, + ), + String, +> { + require_non_empty(&input.profile_id, "jump_hop profile_id")?; + let _ = find_work(ctx, &input.profile_id)?; + let limit = input.limit.clamp(1, 50) as usize; + let mut rows = ctx + .db + .jump_hop_leaderboard_entry() + .by_jump_hop_leaderboard_profile_id() + .filter(input.profile_id.as_str()) + .collect::>(); + sort_jump_hop_leaderboard_rows(&mut rows); + let ranked_rows = rows + .iter() + .enumerate() + .map(|(index, row)| (index as u32 + 1, row)) + .collect::>(); + let viewer_best = clean_optional(&input.viewer_player_id).and_then(|viewer_player_id| { + ranked_rows + .iter() + .find(|(_, row)| row.player_id == viewer_player_id) + .map(|(rank, row)| leaderboard_entry_snapshot(*rank, row)) + }); + let items = ranked_rows + .into_iter() + .take(limit) + .map(|(rank, row)| leaderboard_entry_snapshot(rank, row)) + .collect::>(); + + Ok((input.profile_id, items, viewer_best)) +} + fn restart_jump_hop_run_tx( ctx: &ReducerContext, input: JumpHopRunRestartInput, @@ -971,9 +1052,121 @@ fn insert_event( }); } +fn normalize_runtime_mode(value: &str) -> &'static str { + if value + .trim() + .eq_ignore_ascii_case(JUMP_HOP_RUNTIME_MODE_DRAFT) + { + JUMP_HOP_RUNTIME_MODE_DRAFT + } else { + JUMP_HOP_RUNTIME_MODE_PUBLISHED + } +} + +fn build_jump_hop_leaderboard_entry_id(player_id: &str, profile_id: &str) -> String { + format!("jump-hop-leaderboard-{player_id}-{profile_id}") +} + +fn upsert_jump_hop_leaderboard_entry( + ctx: &ReducerContext, + snapshot: &JumpHopRunSnapshot, + updated_at_ms: i64, +) { + let Some(finished_at_ms) = snapshot.finished_at_ms else { + return; + }; + let successful_jump_count = snapshot.current_platform_index; + let duration_ms = finished_at_ms.saturating_sub(snapshot.started_at_ms); + let entry_id = + build_jump_hop_leaderboard_entry_id(&snapshot.owner_user_id, &snapshot.profile_id); + let updated_at = Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000)); + if let Some(existing) = ctx + .db + .jump_hop_leaderboard_entry() + .entry_id() + .find(&entry_id) + { + let should_replace = + is_jump_hop_leaderboard_candidate_better(successful_jump_count, duration_ms, &existing); + ctx.db + .jump_hop_leaderboard_entry() + .entry_id() + .delete(&entry_id); + ctx.db + .jump_hop_leaderboard_entry() + .insert(JumpHopLeaderboardEntryRow { + entry_id, + profile_id: existing.profile_id, + player_id: existing.player_id, + successful_jump_count: if should_replace { + successful_jump_count + } else { + existing.successful_jump_count + }, + duration_ms: if should_replace { + duration_ms + } else { + existing.duration_ms + }, + run_id: if should_replace { + snapshot.run_id.clone() + } else { + existing.run_id + }, + updated_at, + }); + return; + } + + ctx.db + .jump_hop_leaderboard_entry() + .insert(JumpHopLeaderboardEntryRow { + entry_id, + profile_id: snapshot.profile_id.clone(), + player_id: snapshot.owner_user_id.clone(), + successful_jump_count, + duration_ms, + run_id: snapshot.run_id.clone(), + updated_at, + }); +} + +fn is_jump_hop_leaderboard_candidate_better( + successful_jump_count: u32, + duration_ms: u64, + existing: &JumpHopLeaderboardEntryRow, +) -> bool { + successful_jump_count > existing.successful_jump_count + || (successful_jump_count == existing.successful_jump_count + && duration_ms < existing.duration_ms) +} + +fn sort_jump_hop_leaderboard_rows(rows: &mut [JumpHopLeaderboardEntryRow]) { + rows.sort_by(|left, right| { + right + .successful_jump_count + .cmp(&left.successful_jump_count) + .then_with(|| left.duration_ms.cmp(&right.duration_ms)) + .then_with(|| left.updated_at.cmp(&right.updated_at)) + .then_with(|| left.player_id.cmp(&right.player_id)) + }); +} + +fn leaderboard_entry_snapshot( + rank: u32, + row: &JumpHopLeaderboardEntryRow, +) -> JumpHopLeaderboardEntrySnapshot { + JumpHopLeaderboardEntrySnapshot { + rank, + player_id: row.player_id.clone(), + successful_jump_count: row.successful_jump_count, + duration_ms: row.duration_ms, + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + fn is_publish_ready(row: &JumpHopWorkProfileRow) -> bool { !row.work_title.trim().is_empty() - && !row.character_asset_json.trim().is_empty() && !row.tile_atlas_asset_json.trim().is_empty() && !row.tile_assets_json.trim().is_empty() && !row.path_json.trim().is_empty() @@ -985,8 +1178,8 @@ fn default_config_from_seed(seed_text: &str) -> JumpHopCreatorConfigSnapshot { theme_text: seed.clone(), difficulty: JumpHopDifficulty::Standard.as_str().to_string(), style_preset: JUMP_HOP_STYLE_MINIMAL_BLOCKS.to_string(), - character_prompt: format!("{seed}çš„ä¿¯è§†è§’ä¸»è§’ï¼Œé€æ˜ŽèƒŒæ™¯ï¼Œå…¨èº«å¯è§"), - tile_prompt: format!("{seed}的等è·åœ°å—图集,包å«èµ·ç‚¹ã€æ™®é€šã€ç›®æ ‡å’Œç»ˆç‚¹åœ°å—"), + character_prompt: "内置默认 3D 角色".to_string(), + tile_prompt: format!("{seed}主题的俯视角清爽游æˆåŒ–立体感平å°ç´ æ"), end_mood_prompt: String::new(), } } @@ -1185,3 +1378,64 @@ fn clone_run(row: &JumpHopRuntimeRunRow) -> JumpHopRuntimeRunRow { updated_at: row.updated_at, } } + +#[cfg(test)] +mod tests { + use super::*; + + fn leaderboard_row( + player_id: &str, + successful_jump_count: u32, + duration_ms: u64, + updated_at_micros: i64, + ) -> JumpHopLeaderboardEntryRow { + JumpHopLeaderboardEntryRow { + entry_id: format!("entry-{player_id}"), + profile_id: "jump-hop-profile-test".to_string(), + player_id: player_id.to_string(), + successful_jump_count, + duration_ms, + run_id: format!("run-{player_id}"), + updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros), + } + } + + #[test] + fn jump_hop_leaderboard_sorts_by_jump_count_duration_and_update_time() { + let mut rows = vec![ + leaderboard_row("player-slow", 8, 8_000, 30), + leaderboard_row("player-late", 9, 6_000, 20), + leaderboard_row("player-fast", 9, 5_000, 40), + leaderboard_row("player-early", 9, 5_000, 10), + ]; + + sort_jump_hop_leaderboard_rows(&mut rows); + + let player_ids = rows + .into_iter() + .map(|row| row.player_id) + .collect::>(); + assert_eq!( + player_ids, + vec!["player-early", "player-fast", "player-late", "player-slow"] + ); + } + + #[test] + fn jump_hop_leaderboard_replaces_only_better_player_score() { + let existing = leaderboard_row("player", 6, 4_000, 10); + + assert!(is_jump_hop_leaderboard_candidate_better( + 7, 8_000, &existing + )); + assert!(is_jump_hop_leaderboard_candidate_better( + 6, 3_500, &existing + )); + assert!(!is_jump_hop_leaderboard_candidate_better( + 6, 4_500, &existing + )); + assert!(!is_jump_hop_leaderboard_candidate_better( + 5, 1_000, &existing + )); + } +} diff --git a/server-rs/crates/spacetime-module/src/jump_hop/tables.rs b/server-rs/crates/spacetime-module/src/jump_hop/tables.rs index 31715f0e..1524f75c 100644 --- a/server-rs/crates/spacetime-module/src/jump_hop/tables.rs +++ b/server-rs/crates/spacetime-module/src/jump_hop/tables.rs @@ -94,3 +94,19 @@ pub struct JumpHopEventRow { pub(crate) result: String, pub(crate) occurred_at: Timestamp, } + +#[spacetimedb::table( + accessor = jump_hop_leaderboard_entry, + index(accessor = by_jump_hop_leaderboard_profile_id, btree(columns = [profile_id])), + index(accessor = by_jump_hop_leaderboard_player_profile, btree(columns = [player_id, profile_id])) +)] +pub struct JumpHopLeaderboardEntryRow { + #[primary_key] + pub(crate) entry_id: String, + pub(crate) profile_id: String, + pub(crate) player_id: String, + pub(crate) successful_jump_count: u32, + pub(crate) duration_ms: u64, + pub(crate) run_id: String, + pub(crate) updated_at: Timestamp, +} diff --git a/server-rs/crates/spacetime-module/src/jump_hop/types.rs b/server-rs/crates/spacetime-module/src/jump_hop/types.rs index fe514a3d..05f6092f 100644 --- a/server-rs/crates/spacetime-module/src/jump_hop/types.rs +++ b/server-rs/crates/spacetime-module/src/jump_hop/types.rs @@ -14,6 +14,8 @@ pub const JUMP_HOP_GENERATION_READY: &str = "ready"; pub const JUMP_HOP_EVENT_RUN_STARTED: &str = "run-started"; pub const JUMP_HOP_EVENT_RUN_RESTARTED: &str = "run-restarted"; pub const JUMP_HOP_EVENT_JUMP: &str = "jump"; +pub const JUMP_HOP_RUNTIME_MODE_DRAFT: &str = "draft"; +pub const JUMP_HOP_RUNTIME_MODE_PUBLISHED: &str = "published"; #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct JumpHopAgentSessionCreateInput { @@ -96,6 +98,7 @@ pub struct JumpHopRunStartInput { pub run_id: String, pub owner_user_id: String, pub profile_id: String, + pub runtime_mode: String, pub client_event_id: String, pub started_at_ms: i64, } @@ -106,11 +109,13 @@ pub struct JumpHopRunGetInput { pub owner_user_id: String, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct JumpHopRunJumpInput { pub run_id: String, pub owner_user_id: String, - pub charge_ms: u32, + pub drag_distance: f32, + pub drag_vector_x: Option, + pub drag_vector_y: Option, pub client_event_id: String, pub jumped_at_ms: i64, } @@ -152,6 +157,31 @@ pub struct JumpHopRunProcedureResult { pub error_message: Option, } +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct JumpHopLeaderboardEntrySnapshot { + pub rank: u32, + pub player_id: String, + pub successful_jump_count: u32, + pub duration_ms: u64, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct JumpHopLeaderboardGetInput { + pub profile_id: String, + pub viewer_player_id: String, + pub limit: u32, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct JumpHopLeaderboardProcedureResult { + pub ok: bool, + pub profile_id: String, + pub items: Vec, + pub viewer_best: Option, + pub error_message: Option, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct JumpHopCreatorConfigSnapshot { @@ -181,10 +211,16 @@ pub struct JumpHopCharacterAssetSnapshot { #[serde(rename_all = "camelCase")] pub struct JumpHopTileAssetSnapshot { pub tile_type: String, + #[serde(default)] + pub tile_id: Option, pub image_src: String, pub image_object_key: String, pub asset_object_id: String, pub source_atlas_cell: String, + #[serde(default)] + pub atlas_row: Option, + #[serde(default)] + pub atlas_col: Option, pub visual_width: u32, pub visual_height: u32, pub top_surface_radius: f32, diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index fade23b3..c2b4ff24 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -13,7 +13,8 @@ use crate::bark_battle::tables::{ }; use crate::big_fish::big_fish_runtime_run; use crate::jump_hop::tables::{ - jump_hop_agent_session, jump_hop_event, jump_hop_runtime_run, jump_hop_work_profile, + jump_hop_agent_session, jump_hop_event, jump_hop_leaderboard_entry, jump_hop_runtime_run, + jump_hop_work_profile, }; use crate::match3d::tables::{ match_3_d_work_profile, match3d_agent_message, match3d_agent_session, match3d_runtime_run, @@ -244,6 +245,7 @@ macro_rules! migration_tables { jump_hop_work_profile, jump_hop_runtime_run, jump_hop_event, + jump_hop_leaderboard_entry, wooden_fish_agent_session, wooden_fish_work_profile, wooden_fish_runtime_run, diff --git a/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs b/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs index 3ee5e0cf..9da9b282 100644 --- a/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs +++ b/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs @@ -237,6 +237,7 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) { migrate_bark_battle_entry_to_open_default(ctx, now); migrate_baby_object_match_entry_from_old_coming_soon_default(ctx, now); migrate_wooden_fish_entry_from_old_puzzle_image_default(ctx, now); + migrate_jump_hop_entry_from_old_puzzle_default(ctx, now); } fn migrate_rpg_entry_from_old_hidden_default(ctx: &ReducerContext, now: Timestamp) { @@ -388,6 +389,35 @@ fn migrate_wooden_fish_entry_from_old_puzzle_image_default(ctx: &ReducerContext, }); } +fn migrate_jump_hop_entry_from_old_puzzle_default(ctx: &ReducerContext, now: Timestamp) { + let id = "jump-hop".to_string(); + let Some(row) = ctx.db.creation_entry_type_config().id().find(&id) else { + return; + }; + + // 中文注释:åªçº å跳一跳é‡è®¾è®¡å‰çš„系统默认入å£ï¼Œé¿å…覆盖åŽå°æ‰‹åЍé…置。 + let still_old_puzzle_default = row.title == "跳一跳" + && row.subtitle == "俯视角跳跃闯关" + && row.badge == "å¯åˆ›å»º" + && row.image_src == "/creation-type-references/puzzle.webp" + && row.visible + && row.open + && row.sort_order == 45; + if !still_old_puzzle_default { + return; + } + + ctx.db + .creation_entry_type_config() + .id() + .update(CreationEntryTypeConfig { + subtitle: "主题驱动平å°è·³è·ƒ".to_string(), + image_src: "/creation-type-references/jump-hop.webp".to_string(), + updated_at: now, + ..row + }); +} + fn default_creation_entry_type_configs(now: Timestamp) -> Vec { module_runtime::default_creation_entry_type_snapshots(now.to_micros_since_unix_epoch()) .into_iter() diff --git a/src/components/jump-hop-creation/JumpHopWorkspace.test.tsx b/src/components/jump-hop-creation/JumpHopWorkspace.test.tsx new file mode 100644 index 00000000..79ce86a8 --- /dev/null +++ b/src/components/jump-hop-creation/JumpHopWorkspace.test.tsx @@ -0,0 +1,60 @@ +/* @vitest-environment jsdom */ + +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, expect, test, vi } from 'vitest'; + +import { jumpHopClient } from '../../services/jump-hop/jumpHopClient'; +import { JumpHopWorkspace } from './JumpHopWorkspace'; + +vi.mock('../../services/jump-hop/jumpHopClient', () => ({ + jumpHopClient: { + createSession: vi.fn(), + }, +})); + +beforeEach(() => { + vi.mocked(jumpHopClient.createSession).mockReset(); + vi.mocked(jumpHopClient.createSession).mockResolvedValue({ + session: { + sessionId: 'jump-hop-session-test', + ownerUserId: 'user-test', + status: 'draft', + draft: null, + createdAt: '2026-05-27T00:00:00Z', + updatedAt: '2026-05-27T00:00:00Z', + }, + }); +}); + +test('跳一跳工作å°åªä¿ç•™ä¸»é¢˜è¾“入并自动派生æäº¤ payload', async () => { + const onSubmitted = vi.fn(); + + render( + {}} onSubmitted={onSubmitted} />, + ); + + expect(screen.getByLabelText('主题')).toBeTruthy(); + expect(screen.queryByLabelText('ä½œå“æ ‡é¢˜')).toBeNull(); + expect(screen.queryByLabelText('作å“简介')).toBeNull(); + expect(screen.queryByLabelText('角色æç¤ºè¯')).toBeNull(); + expect(screen.queryByLabelText('åœ°å—æç¤ºè¯')).toBeNull(); + + fireEvent.change(screen.getByLabelText('主题'), { + target: { value: '竹林茶馆' }, + }); + fireEvent.click(screen.getByRole('button', { name: '生æˆ' })); + + await waitFor(() => expect(onSubmitted).toHaveBeenCalledTimes(1)); + expect(jumpHopClient.createSession).toHaveBeenCalledWith({ + templateId: 'jump-hop', + themeText: '竹林茶馆', + workTitle: '竹林茶馆跳一跳', + workDescription: '竹林茶馆主题的俯视角平å°è·³è·ƒä½œå“', + themeTags: ['竹林茶馆', '跳一跳', '休闲'], + difficulty: 'standard', + stylePreset: 'minimal-blocks', + characterPrompt: '内置默认 3D 角色', + tilePrompt: '竹林茶馆主题的俯视角清爽游æˆåŒ–立体感平å°ç´ æ', + endMoodPrompt: null, + }); +}); diff --git a/src/components/jump-hop-creation/JumpHopWorkspace.tsx b/src/components/jump-hop-creation/JumpHopWorkspace.tsx index d5b31e63..e61301c6 100644 --- a/src/components/jump-hop-creation/JumpHopWorkspace.tsx +++ b/src/components/jump-hop-creation/JumpHopWorkspace.tsx @@ -2,9 +2,7 @@ import { ArrowLeft, Loader2, Send } from 'lucide-react'; import { useMemo, useState } from 'react'; import type { - JumpHopDifficulty, JumpHopSessionResponse, - JumpHopStylePreset, JumpHopWorkspaceCreateRequest, } from '../../../packages/shared/src/contracts/jumpHop'; import { jumpHopClient } from '../../services/jump-hop/jumpHopClient'; @@ -20,27 +18,31 @@ type JumpHopWorkspaceProps = { }; type JumpHopWorkspaceFormState = { - workTitle: string; - workDescription: string; - themeTags: string; - difficulty: JumpHopDifficulty; - stylePreset: JumpHopStylePreset; - characterPrompt: string; - tilePrompt: string; - endMoodPrompt: string; + themeText: string; }; const DEFAULT_FORM_STATE: JumpHopWorkspaceFormState = { - workTitle: '', - workDescription: '', - themeTags: '', - difficulty: 'easy', - stylePreset: 'minimal-blocks', - characterPrompt: '', - tilePrompt: '', - endMoodPrompt: '', + themeText: '', }; +function buildJumpHopWorkspacePayload( + formState: JumpHopWorkspaceFormState, +): JumpHopWorkspaceCreateRequest { + const themeText = formState.themeText.trim(); + return { + templateId: 'jump-hop', + themeText, + workTitle: `${themeText}跳一跳`, + workDescription: `${themeText}主题的俯视角平å°è·³è·ƒä½œå“`, + themeTags: [themeText, '跳一跳', '休闲'], + difficulty: 'standard', + stylePreset: 'minimal-blocks', + characterPrompt: '内置默认 3D 角色', + tilePrompt: `${themeText}主题的俯视角清爽游æˆåŒ–立体感平å°ç´ æ`, + endMoodPrompt: null, + }; +} + export function JumpHopWorkspace({ isBusy = false, error = null, @@ -52,14 +54,7 @@ export function JumpHopWorkspace({ const [isSubmitting, setIsSubmitting] = useState(false); const canSubmit = useMemo( - () => - Boolean( - formState.workTitle.trim() && - formState.workDescription.trim() && - formState.themeTags.trim() && - formState.characterPrompt.trim() && - formState.tilePrompt.trim(), - ), + () => Boolean(formState.themeText.trim()), [formState], ); @@ -73,20 +68,7 @@ export function JumpHopWorkspace({ setLocalError(null); try { - const payload: JumpHopWorkspaceCreateRequest = { - templateId: 'jump-hop', - workTitle: formState.workTitle.trim(), - workDescription: formState.workDescription.trim(), - themeTags: formState.themeTags - .split(/[,,ã€\s]+/) - .map((item) => item.trim()) - .filter(Boolean), - difficulty: formState.difficulty, - stylePreset: formState.stylePreset, - characterPrompt: formState.characterPrompt.trim(), - tilePrompt: formState.tilePrompt.trim(), - endMoodPrompt: formState.endMoodPrompt.trim() || null, - }; + const payload = buildJumpHopWorkspacePayload(formState); const response = await jumpHopClient.createSession(payload); onSubmitted(response, payload); } catch (caughtError) { @@ -111,143 +93,22 @@ export function JumpHopWorkspace({ -

+
-