commit 67253bded5a0e6d4f16d09a2e9f13c72d9a81021 Author: 雷电芽衣 Date: Wed Jul 15 10:50:12 2026 -0400 Honey Village: six model builds + hub frontend, Dockerized for Dokploy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee43765 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.DS_Store +.env +.env.* +playwright-report/ +test-results/ +.qa/ +.vite/ +*.local +*.log diff --git a/Fable-5-High/Dockerfile b/Fable-5-High/Dockerfile new file mode 100644 index 0000000..b7fe45c --- /dev/null +++ b/Fable-5-High/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-alpine AS build +WORKDIR /app +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1 +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npx vite build +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html diff --git a/Fable-5-High/PRD.md b/Fable-5-High/PRD.md new file mode 100644 index 0000000..0f5ea47 --- /dev/null +++ b/Fable-5-High/PRD.md @@ -0,0 +1,226 @@ +# PRD:《崩坏童话:蜜糖村》网页版 + +> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局 + +## 1. 游戏理念与设计支柱 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。 + +四条设计支柱,所有实现争议回到这里裁决: + +1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。 +2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。 +3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。 +4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。 + +## 2. 世界观:三层结构 + +| 层 | 名称 | 玩家所见 | 实质 | +|---|---|---|---| +| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 | +| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" | +| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 | + +命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。 + +## 3. 主角设定 + +- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。 +- 性别中性像素小人,16×24 像素/帧,无内心独白。 +- 对话头像永远同一帧微笑(见支柱 2)。 +- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。 + +## 4. NPC 清单(封闭,7 组 + 群演) + +每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。 + +| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 | +|---|---|---|---|---|---| +| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 | +| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) | +| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 | +| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) | +| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 | +| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 | +| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 | +| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — | + +## 5. 七日故事线 + +每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。 + +| 日 | 主题 | 剧情要点 | 侵蚀预期 | +|---|---|---|---| +| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 | +| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 | +| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 | +| Day 4 | 完美的重复 | 任务 T8–T10 与 Day 1 的 T1–T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 | +| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 | +| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 | +| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 | + +阿福每日公告(逐字实现,注意措辞逐日滑移): + +- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」 +- D2「早上好呀!新的一天,新的幸福。祝你开心!」 +- D3「早上好!今天要把幸福装得满满的哦。要开心!」 +- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同) +- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」 +- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」 +- D7「庆典开始了。来吧。」 + +## 6. 核心系统 + +### 6.1 幸福值(明线) + +- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。 +- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。 +- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。 +- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。 +- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。 + +### 6.2 觉知值(暗线,永不显示) + +- 范围 0–100,开局 0。只升不降。 +- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。 + +### 6.3 悲伤行为(封闭清单,5 项) + +拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。 + +### 6.4 侵蚀阶段矩阵(唯一真源) + +阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。 + +| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5) | UI | +|---|---|---|---|---|---|---| +| S0 | 0–20 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 0–2 瓣 | +| S1 | 21–50 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 3–6 瓣 | +| S2 | 51–80 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 | +| S3 | 81–99 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 | +| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 | + +NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。 + +### 6.5 文本腐蚀 + +同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。 + +### 6.6 BGM 与音效 + +- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。 +- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。 +- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。 +- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。 + +## 7. 任务清单(封闭,15 个) + +| ID | 日 | 任务 | 幸福奖励 | 备注 | +|---|---|---|---|---| +| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 | +| T2 | D1 | 给广场花坛浇水 ×3 | +10 | | +| T3 | D1 | 与任意三位村民问好 | +10 | | +| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) | +| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) | +| T6 | D3 | 去花田采莓果 ×5 | +10 | | +| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 | +| T8–T10 | D4 | 与 T1–T3 逐字相同 | 同上 | 刻意恐怖谷 | +| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | | +| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) | +| T13 | D6 | 在广场挂彩带 ×4 | +10 | | +| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 | +| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 | + +## 8. 线索清单(封闭,9 处,每处觉知 +8) + +| ID | 位置/来源 | 内容 | 可发现时段 | +|---|---|---|---| +| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 | +| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 | +| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 | +| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 | +| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 | +| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5(T12 中) | +| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 | +| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 | +| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 | + +## 9. 结局(封闭,4 个) + +Day 7 庆典按以下优先级判定(自上而下第一个满足者生效): + +| 结局 | 条件 | 演出 | +|---|---|---| +| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 | +| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 | +| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 | +| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 | + +任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。 + +## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON) + +| 场景 | 尺寸(瓦片) | 要点 | +|---|---|---| +| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) | +| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 | +| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) | +| 杂货店(室内) | 15×10 | 货架(C5) | +| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) | +| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 | +| 花田 | 20×14 | 莓果丛(T6)、浇花点 | +| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 | +| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 | +| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 | + +统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。 + +## 11. 美术规范(Aseprite 制作,全部原创) + +- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。 +- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。 +- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。 +- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。 +- 禁止使用任何现成素材、素材包或开源 tileset。 + +## 12. 技术架构 + +- **Phaser 3 + TypeScript + Vite**,`npm run build` 产出纯静态 `dist/`,`npm run preview` 本地游玩。 +- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。 +- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`。 +- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。 +- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。 +- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。 +- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。 +- 无 localStorage、无外部网络请求、无外部音频/字体文件。 + +## 13. Debug 面板(服务验收,必须实现) + +按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。 + +## 14. 验收清单(共 20 条) + +1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。 +2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。 +3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。 +4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。 +5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。 +6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。 +7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。 +8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。 +9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。 +10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。 +11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。 +12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。 +13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。 +14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。 +15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。 +16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。 +17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。 +18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。 +19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。 +20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。 + +## 15. 明确不做 + +战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。 diff --git a/Fable-5-High/README.md b/Fable-5-High/README.md new file mode 100644 index 0000000..d0d2898 --- /dev/null +++ b/Fable-5-High/README.md @@ -0,0 +1,89 @@ +# 崩坏童话:蜜糖村 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。 +纯单机网页游戏:无存档、无后端、无 localStorage、无外部网络请求,浏览器打开即玩。 +完整流程约 25–35 分钟,含 4 个结局。 + +## 启动方式 + +```bash +npm install +npm run dev # 开发模式(Vite,默认 http://localhost:5173) +npm run build # 类型检查 + 产出纯静态 dist/ +npm run preview # 本地游玩 dist/ 构建产物 +``` + +技术栈:**Phaser 3 + TypeScript + Vite**。逻辑分辨率 320×180,任意窗口尺寸整数倍放大居中,`pixelArt` 关闭纹理平滑。 + +## 按键 + +| 按键 | 功能 | +|---|---| +| WASD / 方向键 | 四向移动 | +| E(或回车) | 交互 / 推进对话 / 确认选项 | +| W/S 或 ↑/↓ | 对话中切换选项 | +| `` ` ``(反引号) | 开关 Debug 面板 | + +标题画面输入名字后回车开始(留空默认「小满」)。 + +## Debug 面板(服务验收) + +按反引号 `` ` `` 开关,默认关闭、无 UI 痕迹,不影响正常游玩。功能: + +- 直接设置**幸福值**(0–100,观察侵蚀矩阵五档:10/40/70/90/100)与**觉知值** +- 跳转**当前日**(1–7,回到当日清晨小屋) +- **传送**到全部 11 张地图(10 个场景,白色层含走廊+病房两张图) +- **置齐全部线索**(C1–C9) +- **强制触发四个结局**(E1 糖衣 / E2 回收 / E3 苏醒 / E4 静默) + +## 素材制作流程(全部本项目内自制,无任何外部资产) + +### 像素美术(Aseprite) + +全部图片由 `tools/gen_art.lua` 在 Aseprite 内逐像素绘制生成——瓦片集(80 枚 16×16:草地/石板路/湖水/花丛/莓果丛/树/房屋墙顶门/室内地板/家具/井/雾/白色层墙地等)、主角(16×24,四朝向 × 4 帧行走,首帧兼待机)、对话头像(微笑 + E3 专用「真实的脸」)、7 组具名 NPC 立绘(各 smile/flat/off/blink 四档,off 档与 smile 仅差 1 像素——右眼低 1 像素)、双子鸟 2 帧张嘴动画、群演 ×5 各 2 帧循环、灰麻雀、向日葵表盘 14 帧。 + +```bash +# 重新生成全部素材(PNG → public/assets/,Aseprite 源文件 → art/) +aseprite -b --script tools/gen_art.lua +# macOS 应用包路径:/Applications/Aseprite.app/Contents/MacOS/aseprite +``` + +色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝),白色层独立灰白色板;无抗锯齿、无描边渐变。 + +### 地图(Tiled) + +10 个场景 11 张地图由 `tools/gen_maps.mjs` 生成 Tiled `.tmx` 源文件(`maps/`,统一 `ground` / `obstacles`(collides=true)/ `objects`(spawn、NPC 点位、door、交互点)三层结构),再经 Tiled 命令行导出 JSON: + +```bash +node tools/gen_maps.mjs +for f in maps/*.tmx; do + tiled --export-map --embed-tilesets "$f" "public/assets/maps/$(basename "$f" .tmx).json" +done +# macOS 应用包路径:/Applications/Tiled.app/Contents/MacOS/Tiled +``` + +场景清单:小满小屋 12×8、村广场 30×20(枢纽)、面包房 15×10、杂货店 15×10、诊所 12×8、湖畔 24×16、花田 20×14、古井空地 14×10、雾墙边界 20×8、白色层(走廊 20×6 + 病房 10×8)。所有室外场景经村广场互通,门与出入口成对。 + +### 音频 + +无任何音频文件。BGM 为 Web Audio 程序合成的 8 小节 8-bit 治愈循环(方波主旋律 + 三角波低音 + 独立鸟鸣声部),音效仅 4 个(对话哔哔、任务完成叮、黄昏钟声、心电监护滴声),全部程序合成,见 `src/core/audio.ts`。playbackRate / 低通滤波 / 鸟鸣静音由侵蚀矩阵(`CorruptionDirector`)统一驱动。 + +## 自动化验收(可选) + +```bash +npm i -D playwright && npx playwright install chromium +node tools/e2e.mjs # 标题→Day1、侵蚀五档截图、全场景传送、四结局强制触发、控制台零报错 +node tools/e2e_flow.mjs # 七日全流程:15 任务、9 线索、黄昏/梦境/脚本雨、E3 苏醒线 +node tools/e2e_mech.mjs # 悲伤行为 5 项、强制欢乐庆典与逃离计数、S4 文本腐蚀、四结局判定 +``` + +## 代码结构 + +``` +src/core/director.ts CorruptionDirector:幸福/觉知/日期/任务/线索/侵蚀阶段的唯一真源 +src/core/textcorrupt.ts 文本腐蚀(10 组同音错字对照表,渲染层替换,优先命中咪咪) +src/core/audio.ts Web Audio BGM/SFX 合成与矩阵联动 +src/core/debug.ts Debug 面板 +src/data/story.ts 全部台词(阿福公告 D1–D7 逐字、龟爷爷 C9 三段链、结局文案等) +src/scenes/ Boot / Title / 11 张地图各一个 Scene / 常驻 UI / Dream / Ending +``` diff --git a/Fable-5-High/art/bird.aseprite b/Fable-5-High/art/bird.aseprite new file mode 100644 index 0000000..6a12dca Binary files /dev/null and b/Fable-5-High/art/bird.aseprite differ diff --git a/Fable-5-High/art/extra1.aseprite b/Fable-5-High/art/extra1.aseprite new file mode 100644 index 0000000..b1f6234 Binary files /dev/null and b/Fable-5-High/art/extra1.aseprite differ diff --git a/Fable-5-High/art/extra2.aseprite b/Fable-5-High/art/extra2.aseprite new file mode 100644 index 0000000..f5087ea Binary files /dev/null and b/Fable-5-High/art/extra2.aseprite differ diff --git a/Fable-5-High/art/extra3.aseprite b/Fable-5-High/art/extra3.aseprite new file mode 100644 index 0000000..e285863 Binary files /dev/null and b/Fable-5-High/art/extra3.aseprite differ diff --git a/Fable-5-High/art/extra4.aseprite b/Fable-5-High/art/extra4.aseprite new file mode 100644 index 0000000..af74ef5 Binary files /dev/null and b/Fable-5-High/art/extra4.aseprite differ diff --git a/Fable-5-High/art/extra5.aseprite b/Fable-5-High/art/extra5.aseprite new file mode 100644 index 0000000..219c3c6 Binary files /dev/null and b/Fable-5-High/art/extra5.aseprite differ diff --git a/Fable-5-High/art/npc_afu.aseprite b/Fable-5-High/art/npc_afu.aseprite new file mode 100644 index 0000000..f1ca9a0 Binary files /dev/null and b/Fable-5-High/art/npc_afu.aseprite differ diff --git a/Fable-5-High/art/npc_dabao.aseprite b/Fable-5-High/art/npc_dabao.aseprite new file mode 100644 index 0000000..db79d16 Binary files /dev/null and b/Fable-5-High/art/npc_dabao.aseprite differ diff --git a/Fable-5-High/art/npc_fu.aseprite b/Fable-5-High/art/npc_fu.aseprite new file mode 100644 index 0000000..4dff474 Binary files /dev/null and b/Fable-5-High/art/npc_fu.aseprite differ diff --git a/Fable-5-High/art/npc_gui.aseprite b/Fable-5-High/art/npc_gui.aseprite new file mode 100644 index 0000000..c318462 Binary files /dev/null and b/Fable-5-High/art/npc_gui.aseprite differ diff --git a/Fable-5-High/art/npc_mimi.aseprite b/Fable-5-High/art/npc_mimi.aseprite new file mode 100644 index 0000000..a50c4d3 Binary files /dev/null and b/Fable-5-High/art/npc_mimi.aseprite differ diff --git a/Fable-5-High/art/npc_yuanyuan.aseprite b/Fable-5-High/art/npc_yuanyuan.aseprite new file mode 100644 index 0000000..fce8f49 Binary files /dev/null and b/Fable-5-High/art/npc_yuanyuan.aseprite differ diff --git a/Fable-5-High/art/player.aseprite b/Fable-5-High/art/player.aseprite new file mode 100644 index 0000000..4a5daeb Binary files /dev/null and b/Fable-5-High/art/player.aseprite differ diff --git a/Fable-5-High/art/portrait_real.aseprite b/Fable-5-High/art/portrait_real.aseprite new file mode 100644 index 0000000..f8337fc Binary files /dev/null and b/Fable-5-High/art/portrait_real.aseprite differ diff --git a/Fable-5-High/art/portrait_smile.aseprite b/Fable-5-High/art/portrait_smile.aseprite new file mode 100644 index 0000000..7ee6c6b Binary files /dev/null and b/Fable-5-High/art/portrait_smile.aseprite differ diff --git a/Fable-5-High/art/sparrow.aseprite b/Fable-5-High/art/sparrow.aseprite new file mode 100644 index 0000000..cee1bcb Binary files /dev/null and b/Fable-5-High/art/sparrow.aseprite differ diff --git a/Fable-5-High/art/sunflower.aseprite b/Fable-5-High/art/sunflower.aseprite new file mode 100644 index 0000000..0ac00c8 Binary files /dev/null and b/Fable-5-High/art/sunflower.aseprite differ diff --git a/Fable-5-High/art/tiles.aseprite b/Fable-5-High/art/tiles.aseprite new file mode 100644 index 0000000..8036987 Binary files /dev/null and b/Fable-5-High/art/tiles.aseprite differ diff --git a/Fable-5-High/index.html b/Fable-5-High/index.html new file mode 100644 index 0000000..704e2ef --- /dev/null +++ b/Fable-5-High/index.html @@ -0,0 +1,35 @@ + + + + + 崩坏童话:蜜糖村 + + + +
+
+
+ + + diff --git a/Fable-5-High/maps/bakery.tmx b/Fable-5-High/maps/bakery.tmx new file mode 100644 index 0000000..a00203d --- /dev/null +++ b/Fable-5-High/maps/bakery.tmx @@ -0,0 +1,40 @@ + + + + + +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,30,30,25,25,25,25,25,25 + + + + + +28,28,28,28,28,28,28,28,28,28,28,28,28,28,28, +29,29,29,29,62,29,29,29,29,29,62,29,34,29,29, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,33,33,33,33,33,67,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,69,0,0,0,0,0,0,0,0,37,38,0,68,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,68,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + diff --git a/Fable-5-High/maps/clinic.tmx b/Fable-5-High/maps/clinic.tmx new file mode 100644 index 0000000..ea523ca --- /dev/null +++ b/Fable-5-High/maps/clinic.tmx @@ -0,0 +1,35 @@ + + + + + +27,27,27,27,27,27,27,27,27,27,27,27, +27,27,27,27,27,27,27,27,27,27,27,27, +27,27,27,27,27,27,27,27,27,27,27,27, +27,27,27,27,27,27,27,27,27,27,27,27, +27,27,27,27,27,27,27,27,27,27,27,27, +27,27,27,27,27,27,27,27,27,27,27,27, +27,27,27,27,27,27,27,27,27,27,27,27, +27,27,27,27,27,30,30,27,27,27,27,27 + + + + + +43,43,43,43,43,43,43,43,43,43,43,43, +29,29,29,71,29,29,29,29,75,29,29,29, +0,68,0,0,0,0,0,0,0,63,0,0, +0,0,0,0,0,65,0,0,0,64,0,0, +0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + diff --git a/Fable-5-High/maps/field.tmx b/Fable-5-High/maps/field.tmx new file mode 100644 index 0000000..3af0cb2 --- /dev/null +++ b/Fable-5-High/maps/field.tmx @@ -0,0 +1,51 @@ + + + + + +3,2,1,1,1,1,4,2,1,1,1,1,2,1,1,1,1,1,2,1, +1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1, +1,1,1,1,2,1,1,1,2,3,1,1,1,2,1,1,1,1,4,1, +1,1,1,58,57,57,58,57,57,58,57,57,58,57,57,58,57,1,3,2, +1,1,1,57,57,58,57,57,58,57,57,58,57,57,58,57,57,1,1,1, +1,2,1,57,58,57,57,58,57,57,58,57,57,58,57,57,58,2,1,1, +1,1,1,58,57,57,58,57,57,58,57,57,58,57,57,58,57,1,1,1, +1,1,1,57,57,58,57,57,58,57,57,58,57,57,58,57,57,1,1,2, +1,1,4,57,58,57,57,58,57,57,58,57,57,58,57,57,58,2,1,1, +1,2,2,58,57,57,58,57,57,58,57,57,58,57,57,58,57,1,1,1, +1,3,2,57,57,58,57,57,58,57,57,58,57,57,58,57,57,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,3,1,4,1,2,1,2,1,2, +1,1,1,2,4,1,1,1,1,2,3,1,1,1,1,2,1,1,1,1, +2,1,1,1,1,1,3,2,1,1,1,1,1,1,4,2,1,1,1,1 + + + + + +14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15, +16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17, +0,0,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,12,0,0,0,0,0,0,0,12,0,0,0,0,0,0,52, +0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52, +0,0,0,70,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +51,51,51,51,0,0,0,0,0,0,51,51,51,51,51,51,51,51,51,51 + + + + + + + + + + + + + diff --git a/Fable-5-High/maps/fogwall.tmx b/Fable-5-High/maps/fogwall.tmx new file mode 100644 index 0000000..da8cb56 --- /dev/null +++ b/Fable-5-High/maps/fogwall.tmx @@ -0,0 +1,34 @@ + + + + + +3,2,1,1,1,1,4,2,1,1,1,1,2,1,1,1,1,1,2,1, +1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1, +1,1,1,1,2,1,1,1,2,3,1,1,1,2,1,1,1,1,4,1, +1,1,1,1,1,1,1,4,2,2,1,1,1,1,1,1,1,1,3,2, +78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78, +41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42 + + + + + +52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52, +0,0,14,15,0,0,0,0,0,0,0,0,0,0,0,0,14,15,0,0, +52,0,16,17,0,0,0,0,0,0,0,0,0,0,0,0,16,17,0,52, +0,0,0,0,53,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42 + + + + + + + + diff --git a/Fable-5-High/maps/house.tmx b/Fable-5-High/maps/house.tmx new file mode 100644 index 0000000..369bcb1 --- /dev/null +++ b/Fable-5-High/maps/house.tmx @@ -0,0 +1,36 @@ + + + + + +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,30,30,25,25,25,25,25 + + + + + +28,28,28,28,28,28,28,28,28,28,28,28, +29,29,66,29,29,29,62,29,29,29,29,29, +68,31,0,0,0,0,0,0,0,0,68,0, +0,32,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,37,38,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + diff --git a/Fable-5-High/maps/lake.tmx b/Fable-5-High/maps/lake.tmx new file mode 100644 index 0000000..0c115b4 --- /dev/null +++ b/Fable-5-High/maps/lake.tmx @@ -0,0 +1,53 @@ + + + + + +3,2,1,1,1,1,4,2,1,1,1,1,2,1,1,1,1,1,2,1,1,1,1,3, +1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,4,1,3,1, +1,1,1,1,2,1,1,1,2,3,1,1,1,2,1,1,1,1,4,1,1,1,2,1, +1,1,1,1,1,1,1,4,2,2,1,1,1,1,1,1,1,1,3,2,2,1,1,1, +1,1,1,2,1,1,1,1,4,1,1,2,1,3,1,1,2,1,1,1,1,2,1,1, +1,2,1,1,2,1,1,1,1,1,1,4,1,1,2,1,1,2,1,1,1,3,1,1, +1,1,1,1,1,1,1,54,55,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,2,55,54,2,1,1,3,1,1,1,2,1,1,2,1,1,4,1, +1,1,4,1,1,1,1,2,1,1,1,1,2,1,1,1,1,2,1,1,3,1,2,1, +10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, +9,8,9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8, +8,9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9, +9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61, +8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9, +61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,8, +8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,8,9 + + + + + +14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15, +16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17, +52,0,0,0,0,0,0,0,0,0,0,14,15,0,0,0,0,0,0,0,0,0,0,0, +0,0,14,15,0,0,0,0,0,0,0,16,17,0,0,0,0,0,0,0,0,0,0,0, +52,0,16,17,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,46,47,0,0,0,0,0,0, +52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +9,8,9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8, +8,9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9, +9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61, +8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9, +61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,8, +8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,8,9 + + + + + + + + + + + diff --git a/Fable-5-High/maps/plaza.tmx b/Fable-5-High/maps/plaza.tmx new file mode 100644 index 0000000..57094aa --- /dev/null +++ b/Fable-5-High/maps/plaza.tmx @@ -0,0 +1,93 @@ + + + + + +3,2,1,1,1,1,4,2,1,1,1,1,2,1,5,5,1,1,2,1,1,1,1,3,2,1,1,1,1,4, +1,1,1,1,1,1,1,1,1,1,1,1,2,1,6,5,2,1,2,1,4,1,3,1,1,1,1,1,1,1, +1,1,1,1,2,1,1,1,2,3,1,1,1,2,5,5,1,1,4,1,1,1,2,1,1,1,1,2,1,1, +1,1,1,1,1,1,1,4,2,2,1,1,1,1,5,5,1,1,3,2,2,1,1,1,1,1,1,1,1,1, +1,1,1,2,21,1,1,1,4,21,1,2,1,3,6,5,2,1,1,1,1,21,1,1,1,1,21,1,1,1, +1,2,1,1,72,1,1,1,1,72,1,4,1,1,5,5,1,2,1,1,1,72,1,1,2,1,72,2,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,2,1,1,2,1,1,3,1,6,5,2,1,1,2,1,1,4,1,1,1,1,1,1,2, +6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +1,3,2,1,1,1,2,1,1,1,1,2,1,1,6,5,1,1,1,1,2,1,1,1,3,2,1,1,1,2, +1,1,1,1,1,1,1,1,1,1,1,3,74,74,74,74,74,74,1,2,1,2,1,1,1,1,1,1,1,1, +1,1,1,2,4,1,1,1,1,2,3,1,74,74,74,74,74,74,1,1,1,2,1,1,1,1,2,4,1,1, +2,1,1,1,1,1,3,2,1,1,1,1,74,74,74,74,74,74,1,1,1,1,2,2,1,1,1,1,1,3, +1,2,1,3,1,1,1,1,2,1,4,1,74,74,74,74,74,74,1,1,1,1,1,1,2,1,3,1,1,1, +2,1,1,1,2,1,1,1,2,1,1,1,4,1,5,5,3,1,1,2,1,1,1,2,1,1,1,2,1,1, +1,1,1,1,1,1,1,1,3,4,2,2,2,2,6,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +2,1,1,2,1,1,2,1,1,1,1,1,1,1,5,5,1,4,1,1,2,1,1,2,1,1,2,1,1,2, +1,1,1,1,2,1,1,2,1,1,2,1,1,2,5,5,4,1,1,3,1,1,1,1,1,1,1,2,1,1, +2,4,3,1,1,1,1,1,1,1,1,1,1,1,6,5,1,1,1,1,2,2,2,2,4,3,1,1,1,1 + + + + + +14,15,14,15,14,15,14,15,14,15,14,15,0,0,0,0,0,0,14,15,14,15,14,15,14,15,14,15,14,15, +16,17,16,17,16,17,16,17,16,17,16,17,0,0,0,0,0,50,16,17,16,17,16,17,16,17,16,17,16,17, +52,0,22,22,23,23,0,22,22,23,23,0,0,0,0,0,0,0,0,22,22,23,23,0,22,22,23,23,0,52, +0,0,24,24,24,24,0,24,24,24,24,0,0,0,0,0,0,0,0,24,24,24,24,0,24,24,24,24,0,0, +52,0,19,20,0,19,0,19,20,0,19,0,0,0,0,0,0,0,0,19,20,0,19,0,19,20,0,19,0,52, +0,53,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +52,0,0,0,0,0,0,0,0,0,0,48,49,0,0,0,0,0,0,0,14,15,0,0,0,0,0,0,0,52, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,17,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,60,0,0,0,0,0,0,60,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +52,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,52, +0,0,0,0,0,0,0,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52, +0,0,0,0,0,0,0,70,0,0,0,60,0,0,0,0,0,0,60,0,0,0,0,0,0,18,0,0,0,0, +52,0,0,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,15,0,52, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,15,0,0,0,16,17,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,17,0,0,0,0,0,0,0, +51,51,51,51,51,51,51,51,51,51,51,51,51,51,0,0,0,0,51,51,51,51,51,51,51,51,51,51,51,51 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Fable-5-High/maps/store.tmx b/Fable-5-High/maps/store.tmx new file mode 100644 index 0000000..0fce684 --- /dev/null +++ b/Fable-5-High/maps/store.tmx @@ -0,0 +1,42 @@ + + + + + +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,26,26,26,26,26,26,26,26, +26,26,26,26,26,26,26,30,30,26,26,26,26,26,26 + + + + + +28,28,28,28,28,28,28,28,28,28,28,28,28,28,28, +29,29,29,29,29,29,29,29,29,29,29,29,62,29,29, +0,0,36,36,36,36,36,36,36,36,36,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,36,0, +0,0,0,33,33,0,0,0,0,0,0,0,0,36,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,36,0, +0,68,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,69,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + diff --git a/Fable-5-High/maps/tiles.tsx b/Fable-5-High/maps/tiles.tsx new file mode 100644 index 0000000..6a1e237 --- /dev/null +++ b/Fable-5-High/maps/tiles.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/Fable-5-High/maps/well.tmx b/Fable-5-High/maps/well.tmx new file mode 100644 index 0000000..2a66c24 --- /dev/null +++ b/Fable-5-High/maps/well.tmx @@ -0,0 +1,38 @@ + + + + + +3,2,1,1,1,1,4,2,1,1,1,1,2,1, +1,1,1,1,1,1,1,1,1,1,1,1,2,1, +1,1,1,1,2,1,1,1,2,3,1,1,1,2, +1,1,1,1,1,1,1,4,2,2,1,1,1,1, +1,1,1,2,1,1,1,1,4,1,1,2,1,3, +1,2,1,1,2,1,1,1,1,1,1,4,1,1, +1,1,1,1,1,1,5,5,1,1,1,1,1,1, +1,1,1,1,1,1,5,5,1,2,1,1,3,1, +1,1,4,1,1,1,5,5,1,1,1,1,2,1, +1,2,2,4,1,1,5,5,1,1,1,1,1,2 + + + + + +14,15,14,15,14,15,14,15,14,15,14,15,14,15, +16,17,16,17,16,17,16,17,16,17,16,17,16,17, +52,0,0,0,0,0,0,0,0,0,0,0,0,52, +0,0,0,0,0,0,39,0,0,0,0,18,0,0, +52,0,73,73,0,0,40,0,0,53,0,0,0,52, +0,0,0,0,0,0,0,0,0,0,0,73,0,0, +52,0,0,0,0,0,0,0,0,0,0,0,0,52, +0,0,0,0,0,0,0,0,0,0,0,0,0,0, +52,0,0,0,0,0,0,0,0,0,0,0,0,52, +51,51,51,51,51,51,0,0,0,0,51,51,51,51 + + + + + + + + diff --git a/Fable-5-High/maps/white_corridor.tmx b/Fable-5-High/maps/white_corridor.tmx new file mode 100644 index 0000000..820050e --- /dev/null +++ b/Fable-5-High/maps/white_corridor.tmx @@ -0,0 +1,29 @@ + + + + + +44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44 + + + + + +43,43,71,43,43,43,45,71,43,43,43,43,71,45,43,43,43,71,43,43, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + diff --git a/Fable-5-High/maps/white_ward.tmx b/Fable-5-High/maps/white_ward.tmx new file mode 100644 index 0000000..0d4c29d --- /dev/null +++ b/Fable-5-High/maps/white_ward.tmx @@ -0,0 +1,33 @@ + + + + + +44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44, +44,44,44,44,44,44,44,44,44,44 + + + + + +43,43,75,43,43,43,71,43,43,43, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,63,0,76,0,0,0, +0,0,0,0,64,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0 + + + + + + + diff --git a/Fable-5-High/package-lock.json b/Fable-5-High/package-lock.json new file mode 100644 index 0000000..7e67288 --- /dev/null +++ b/Fable-5-High/package-lock.json @@ -0,0 +1,984 @@ +{ + "name": "fable-5-high", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fable-5-high", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "phaser": "^3.90.0" + }, + "devDependencies": { + "playwright": "^1.61.1", + "typescript": "^5.9.3", + "vite": "^8.1.4" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/phaser": { + "version": "3.90.0", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", + "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.16", + "rolldown": "~1.1.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/Fable-5-High/package.json b/Fable-5-High/package.json new file mode 100644 index 0000000..9beac65 --- /dev/null +++ b/Fable-5-High/package.json @@ -0,0 +1,23 @@ +{ + "name": "fable-5-high", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "phaser": "^3.90.0" + }, + "devDependencies": { + "playwright": "^1.61.1", + "typescript": "^5.9.3", + "vite": "^8.1.4" + } +} diff --git a/Fable-5-High/public/assets/bird.png b/Fable-5-High/public/assets/bird.png new file mode 100644 index 0000000..2939db2 Binary files /dev/null and b/Fable-5-High/public/assets/bird.png differ diff --git a/Fable-5-High/public/assets/extra1.png b/Fable-5-High/public/assets/extra1.png new file mode 100644 index 0000000..ff29e12 Binary files /dev/null and b/Fable-5-High/public/assets/extra1.png differ diff --git a/Fable-5-High/public/assets/extra2.png b/Fable-5-High/public/assets/extra2.png new file mode 100644 index 0000000..4e026e8 Binary files /dev/null and b/Fable-5-High/public/assets/extra2.png differ diff --git a/Fable-5-High/public/assets/extra3.png b/Fable-5-High/public/assets/extra3.png new file mode 100644 index 0000000..5b3ebf9 Binary files /dev/null and b/Fable-5-High/public/assets/extra3.png differ diff --git a/Fable-5-High/public/assets/extra4.png b/Fable-5-High/public/assets/extra4.png new file mode 100644 index 0000000..6d02685 Binary files /dev/null and b/Fable-5-High/public/assets/extra4.png differ diff --git a/Fable-5-High/public/assets/extra5.png b/Fable-5-High/public/assets/extra5.png new file mode 100644 index 0000000..6543c6c Binary files /dev/null and b/Fable-5-High/public/assets/extra5.png differ diff --git a/Fable-5-High/public/assets/maps/bakery.json b/Fable-5-High/public/assets/maps/bakery.json new file mode 100644 index 0000000..c3e3eb5 --- /dev/null +++ b/Fable-5-High/public/assets/maps/bakery.json @@ -0,0 +1,169 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 30, 30, 25, 25, 25, 25, 25, 25], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "data":[28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 29, 29, 29, 29, 62, 29, 29, 29, 29, 29, 62, 29, 34, 29, 29, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 33, 33, 33, 33, 33, 67, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 69, 0, 0, 0, 0, 0, 0, 0, 0, 37, 38, 0, 68, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"yuanyuan", + "opacity":1, + "point":true, + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":80, + "y":49.6 + }, + { + "height":0, + "id":2, + "name":"recipe", + "opacity":1, + "point":true, + "properties":[ + { + "name":"clue", + "type":"string", + "value":"C2" + }], + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":128, + "y":75.2 + }, + { + "height":0, + "id":3, + "name":"knead", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":176, + "y":107.2 + }, + { + "height":0, + "id":4, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":128, + "y":136 + }, + { + "height":8, + "id":5, + "name":"to_plaza", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"fromBakery" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":32, + "x":112, + "y":152 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":6, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":15 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/clinic.json b/Fable-5-High/public/assets/maps/clinic.json new file mode 100644 index 0000000..1a3170e --- /dev/null +++ b/Fable-5-High/public/assets/maps/clinic.json @@ -0,0 +1,152 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 30, 30, 27, 27, 27, 27, 27], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "data":[43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, + 29, 29, 29, 71, 29, 29, 29, 29, 75, 29, 29, 29, + 0, 68, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, + 0, 0, 0, 0, 0, 65, 0, 0, 0, 64, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"fu", + "opacity":1, + "point":true, + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":112, + "y":49.6 + }, + { + "height":0, + "id":2, + "name":"chart", + "opacity":1, + "point":true, + "properties":[ + { + "name":"clue", + "type":"string", + "value":"C8" + }], + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":80, + "y":75.2 + }, + { + "height":0, + "id":3, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":96, + "y":104 + }, + { + "height":8, + "id":4, + "name":"to_plaza", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"fromClinic" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":32, + "x":80, + "y":120 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":5, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/field.json b/Fable-5-High/public/assets/maps/field.json new file mode 100644 index 0000000..6c888f4 --- /dev/null +++ b/Fable-5-High/public/assets/maps/field.json @@ -0,0 +1,210 @@ +{ "compressionlevel":-1, + "height":14, + "infinite":false, + "layers":[ + { + "data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, + 1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1, + 1, 1, 1, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 1, 3, 2, + 1, 1, 1, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 1, 1, 1, + 1, 2, 1, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 2, 1, 1, + 1, 1, 1, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 1, 1, 1, + 1, 1, 1, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 1, 1, 2, + 1, 1, 4, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 2, 1, 1, + 1, 2, 2, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 1, 1, 1, + 1, 3, 2, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 4, 1, 2, 1, 2, 1, 2, + 1, 1, 1, 2, 4, 1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, + 2, 1, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1], + "height":14, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, + 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, + 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 51, 51, 51, 51, 0, 0, 0, 0, 0, 0, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51], + "height":14, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"berry1", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":80, + "y":96 + }, + { + "height":0, + "id":2, + "name":"berry2", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":144, + "y":112 + }, + { + "height":0, + "id":3, + "name":"berry3", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":208, + "y":96 + }, + { + "height":0, + "id":4, + "name":"berry4", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":256, + "y":144 + }, + { + "height":0, + "id":5, + "name":"berry5", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":112, + "y":176 + }, + { + "height":0, + "id":6, + "name":"fieldwater", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":64, + "y":203.2 + }, + { + "height":0, + "id":7, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":33.6, + "y":104 + }, + { + "height":80, + "id":8, + "name":"to_plaza", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"fromField" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":9.6, + "x":0, + "y":72 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":9, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/fogwall.json b/Fable-5-High/public/assets/maps/fogwall.json new file mode 100644 index 0000000..4c89282 --- /dev/null +++ b/Fable-5-High/public/assets/maps/fogwall.json @@ -0,0 +1,133 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, + 1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1, + 1, 1, 1, 1, 1, 1, 1, 4, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, 2, + 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 0, + 52, 0, 16, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 17, 0, 52, + 0, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"fog", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":168, + "y":96 + }, + { + "height":0, + "id":2, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":168, + "y":46.4 + }, + { + "height":11.2, + "id":3, + "name":"to_plaza", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"fromFog" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":80, + "x":120, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":4, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/house.json b/Fable-5-High/public/assets/maps/house.json new file mode 100644 index 0000000..c85b1af --- /dev/null +++ b/Fable-5-High/public/assets/maps/house.json @@ -0,0 +1,165 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 30, 30, 25, 25, 25, 25, 25], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "data":[28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 29, 29, 66, 29, 29, 29, 62, 29, 29, 29, 29, 29, + 68, 31, 0, 0, 0, 0, 0, 0, 0, 0, 68, 0, + 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 37, 38, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"bed", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":40, + "y":56 + }, + { + "height":0, + "id":2, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":88, + "y":104 + }, + { + "height":8, + "id":3, + "name":"to_plaza", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"fromHouse" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":32, + "x":80, + "y":120 + }, + { + "height":0, + "id":4, + "name":"calendar", + "opacity":1, + "point":true, + "properties":[ + { + "name":"clue", + "type":"string", + "value":"C1" + }], + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":40, + "y":40 + }, + { + "height":0, + "id":5, + "name":"bed", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":24, + "y":72 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":6, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/lake.json b/Fable-5-High/public/assets/maps/lake.json new file mode 100644 index 0000000..1d14bb8 --- /dev/null +++ b/Fable-5-High/public/assets/maps/lake.json @@ -0,0 +1,188 @@ +{ "compressionlevel":-1, + "height":16, + "infinite":false, + "layers":[ + { + "data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 4, 1, 3, 1, + 1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1, 1, 1, 2, 1, + 1, 1, 1, 1, 1, 1, 1, 4, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, 2, 2, 1, 1, 1, + 1, 1, 1, 2, 1, 1, 1, 1, 4, 1, 1, 2, 1, 3, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, + 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 4, 1, 1, 2, 1, 1, 2, 1, 1, 1, 3, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 54, 55, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 2, 55, 54, 2, 1, 1, 3, 1, 1, 1, 2, 1, 1, 2, 1, 1, 4, 1, + 1, 1, 4, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 3, 1, 2, 1, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, + 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, + 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, + 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, + 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, + 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9], + "height":16, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":24, + "x":0, + "y":0 + }, + { + "data":[14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, + 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, + 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 14, 15, 0, 0, 0, 0, 0, 0, 0, 16, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 52, 0, 16, 17, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 47, 0, 0, 0, 0, 0, 0, + 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, + 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, + 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, + 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, + 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, + 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9], + "height":16, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":24, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"gui", + "opacity":1, + "point":true, + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":310.4, + "y":96 + }, + { + "height":0, + "id":2, + "name":"bench", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":272, + "y":112 + }, + { + "height":0, + "id":3, + "name":"picnic", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":136, + "y":120 + }, + { + "height":0, + "id":4, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":360, + "y":120 + }, + { + "height":0, + "id":5, + "name":"picnicSpot", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":136, + "y":129.6 + }, + { + "height":80, + "id":6, + "name":"to_plaza", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"fromLake" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":9.6, + "x":374.4, + "y":80 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":7, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":24 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/plaza.json b/Fable-5-High/public/assets/maps/plaza.json new file mode 100644 index 0000000..8378ffa --- /dev/null +++ b/Fable-5-High/public/assets/maps/plaza.json @@ -0,0 +1,719 @@ +{ "compressionlevel":-1, + "height":20, + "infinite":false, + "layers":[ + { + "data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, 5, 5, 1, 1, 2, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 4, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 6, 5, 2, 1, 2, 1, 4, 1, 3, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, 5, 5, 1, 1, 4, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 4, 2, 2, 1, 1, 1, 1, 5, 5, 1, 1, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 2, 21, 1, 1, 1, 4, 21, 1, 2, 1, 3, 6, 5, 2, 1, 1, 1, 1, 21, 1, 1, 1, 1, 21, 1, 1, 1, + 1, 2, 1, 1, 72, 1, 1, 1, 1, 72, 1, 4, 1, 1, 5, 5, 1, 2, 1, 1, 1, 72, 1, 1, 2, 1, 72, 2, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 3, 1, 6, 5, 2, 1, 1, 2, 1, 1, 4, 1, 1, 1, 1, 1, 1, 2, + 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 1, 3, 2, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 6, 5, 1, 1, 1, 1, 2, 1, 1, 1, 3, 2, 1, 1, 1, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 74, 74, 74, 74, 74, 74, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 2, 4, 1, 1, 1, 1, 2, 3, 1, 74, 74, 74, 74, 74, 74, 1, 1, 1, 2, 1, 1, 1, 1, 2, 4, 1, 1, + 2, 1, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 74, 74, 74, 74, 74, 74, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 3, + 1, 2, 1, 3, 1, 1, 1, 1, 2, 1, 4, 1, 74, 74, 74, 74, 74, 74, 1, 1, 1, 1, 1, 1, 2, 1, 3, 1, 1, 1, + 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 4, 1, 5, 5, 3, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 2, 2, 2, 2, 6, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 5, 5, 1, 4, 1, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 2, + 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 2, 5, 5, 4, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, + 2, 4, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 5, 1, 1, 1, 1, 2, 2, 2, 2, 4, 3, 1, 1, 1, 1], + "height":20, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "data":[14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 0, 0, 0, 0, 0, 0, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, + 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 0, 0, 0, 0, 0, 50, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, + 52, 0, 22, 22, 23, 23, 0, 22, 22, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 23, 23, 0, 22, 22, 23, 23, 0, 52, + 0, 0, 24, 24, 24, 24, 0, 24, 24, 24, 24, 0, 0, 0, 0, 0, 0, 0, 0, 24, 24, 24, 24, 0, 24, 24, 24, 24, 0, 0, + 52, 0, 19, 20, 0, 19, 0, 19, 20, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, 19, 0, 19, 20, 0, 19, 0, 52, + 0, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 49, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 17, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 52, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, + 52, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 0, 0, 16, 17, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 17, 0, 0, 0, 0, 0, 0, 0, + 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 0, 0, 0, 0, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51], + "height":20, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"sign_house", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":24, + "y":88 + }, + { + "height":0, + "id":2, + "name":"board", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":192, + "y":128 + }, + { + "height":0, + "id":3, + "name":"letters", + "opacity":1, + "point":true, + "properties":[ + { + "name":"clue", + "type":"string", + "value":"C3" + }], + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":288, + "y":48 + }, + { + "height":0, + "id":4, + "name":"birdtree", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":344, + "y":144 + }, + { + "height":0, + "id":5, + "name":"flowerbed1", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":128, + "y":200 + }, + { + "height":0, + "id":6, + "name":"flowerbed2", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":128, + "y":232 + }, + { + "height":0, + "id":7, + "name":"flowerbed3", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":128, + "y":264 + }, + { + "height":0, + "id":8, + "name":"ribbon1", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":192, + "y":187.2 + }, + { + "height":0, + "id":9, + "name":"ribbon2", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":304, + "y":187.2 + }, + { + "height":0, + "id":10, + "name":"ribbon3", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":192, + "y":267.2 + }, + { + "height":0, + "id":11, + "name":"ribbon4", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":304, + "y":267.2 + }, + { + "height":0, + "id":12, + "name":"festival", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":248, + "y":216 + }, + { + "height":0, + "id":13, + "name":"afu", + "opacity":1, + "point":true, + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":208, + "y":144 + }, + { + "height":0, + "id":14, + "name":"dabao", + "opacity":1, + "point":true, + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":272, + "y":64 + }, + { + "height":0, + "id":15, + "name":"bird1", + "opacity":1, + "point":true, + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":334.4, + "y":108.8 + }, + { + "height":0, + "id":16, + "name":"bird2", + "opacity":1, + "point":true, + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":353.6, + "y":108.8 + }, + { + "height":0, + "id":17, + "name":"extra1", + "opacity":1, + "point":true, + "properties":[ + { + "name":"patrol", + "type":"string", + "value":"40" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":88, + "y":168 + }, + { + "height":0, + "id":18, + "name":"extra2", + "opacity":1, + "point":true, + "properties":[ + { + "name":"patrol", + "type":"string", + "value":"56" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":360, + "y":152 + }, + { + "height":0, + "id":19, + "name":"extra3", + "opacity":1, + "point":true, + "properties":[ + { + "name":"patrol", + "type":"string", + "value":"40" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":392, + "y":280 + }, + { + "height":0, + "id":20, + "name":"extra4", + "opacity":1, + "point":true, + "properties":[ + { + "name":"patrol", + "type":"string", + "value":"24" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":88, + "y":280 + }, + { + "height":0, + "id":21, + "name":"extra5", + "opacity":1, + "point":true, + "properties":[ + { + "name":"patrol", + "type":"string", + "value":"16" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":360, + "y":280 + }, + { + "height":0, + "id":22, + "name":"fromHouse", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":72, + "y":107.2 + }, + { + "height":0, + "id":23, + "name":"fromBakery", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":152, + "y":107.2 + }, + { + "height":0, + "id":24, + "name":"fromStore", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":344, + "y":107.2 + }, + { + "height":0, + "id":25, + "name":"fromClinic", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":424, + "y":107.2 + }, + { + "height":0, + "id":26, + "name":"fromLake", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":33.6, + "y":152 + }, + { + "height":0, + "id":27, + "name":"fromField", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":462.4, + "y":152 + }, + { + "height":0, + "id":28, + "name":"fromWell", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":240, + "y":33.6 + }, + { + "height":0, + "id":29, + "name":"fromFog", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":240, + "y":283.2 + }, + { + "height":0, + "id":30, + "name":"center", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":248, + "y":200 + }, + + { + "height":12.8, + "id":31, + "name":"to_house", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"house" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":64, + "y":67.2 + }, + { + "height":12.8, + "id":32, + "name":"to_bakery", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"bakery" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":144, + "y":67.2 + }, + { + "height":12.8, + "id":33, + "name":"to_store", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"store" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":336, + "y":67.2 + }, + { + "height":12.8, + "id":34, + "name":"to_clinic", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"clinic" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":416, + "y":67.2 + }, + { + "height":64, + "id":35, + "name":"to_lake", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"lake" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":9.6, + "x":0, + "y":112 + }, + { + "height":64, + "id":36, + "name":"to_field", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"field" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":9.6, + "x":470.4, + "y":112 + }, + { + "height":11.2, + "id":37, + "name":"to_well", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"well" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":80, + "x":200, + "y":0 + }, + { + "height":11.2, + "id":38, + "name":"to_fogwall", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"fogwall" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":80, + "x":200, + "y":308.8 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":39, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":30 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/store.json b/Fable-5-High/public/assets/maps/store.json new file mode 100644 index 0000000..4ad002d --- /dev/null +++ b/Fable-5-High/public/assets/maps/store.json @@ -0,0 +1,195 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 30, 30, 26, 26, 26, 26, 26, 26], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "data":[28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 62, 29, 29, + 0, 0, 36, 36, 36, 36, 36, 36, 36, 36, 36, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0, + 0, 0, 0, 33, 33, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0, + 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"mimi", + "opacity":1, + "point":true, + "rotation":0, + "type":"npc", + "visible":true, + "width":0, + "x":64, + "y":78.4 + }, + { + "height":0, + "id":2, + "name":"shelfA", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":64, + "y":59.2 + }, + { + "height":0, + "id":3, + "name":"shelfB", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":112, + "y":59.2 + }, + { + "height":0, + "id":4, + "name":"shelfC", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":160, + "y":59.2 + }, + { + "height":0, + "id":5, + "name":"shelf_c5", + "opacity":1, + "point":true, + "properties":[ + { + "name":"clue", + "type":"string", + "value":"C5" + }], + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":206.4, + "y":96 + }, + { + "height":0, + "id":6, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":128, + "y":136 + }, + { + "height":8, + "id":7, + "name":"to_plaza", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"fromStore" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":32, + "x":112, + "y":152 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":8, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":15 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/well.json b/Fable-5-High/public/assets/maps/well.json new file mode 100644 index 0000000..3db9055 --- /dev/null +++ b/Fable-5-High/public/assets/maps/well.json @@ -0,0 +1,143 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, + 1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, + 1, 1, 1, 1, 1, 1, 1, 4, 2, 2, 1, 1, 1, 1, + 1, 1, 1, 2, 1, 1, 1, 1, 4, 1, 1, 2, 1, 3, + 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 4, 1, 1, + 1, 1, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 5, 5, 1, 2, 1, 1, 3, 1, + 1, 1, 4, 1, 1, 1, 5, 5, 1, 1, 1, 1, 2, 1, + 1, 2, 2, 4, 1, 1, 5, 5, 1, 1, 1, 1, 1, 2], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":14, + "x":0, + "y":0 + }, + { + "data":[14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, + 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, + 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 0, 18, 0, 0, + 52, 0, 73, 73, 0, 0, 40, 0, 0, 53, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 73, 0, 0, + 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, + 51, 51, 51, 51, 51, 51, 0, 0, 0, 0, 51, 51, 51, 51], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":14, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"well", + "opacity":1, + "point":true, + "properties":[ + { + "name":"clue", + "type":"string", + "value":"C7" + }], + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":112, + "y":96 + }, + { + "height":0, + "id":2, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":112, + "y":129.6 + }, + { + "height":11.2, + "id":3, + "name":"to_plaza", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"fromWell" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":80, + "x":72, + "y":148.8 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":4, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":14 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/white_corridor.json b/Fable-5-High/public/assets/maps/white_corridor.json new file mode 100644 index 0000000..86ff77b --- /dev/null +++ b/Fable-5-High/public/assets/maps/white_corridor.json @@ -0,0 +1,116 @@ +{ "compressionlevel":-1, + "height":6, + "infinite":false, + "layers":[ + { + "data":[44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], + "height":6, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[43, 43, 71, 43, 43, 43, 45, 71, 43, 43, 43, 43, 71, 45, 43, 43, 43, 71, 43, 43, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":6, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":24, + "y":56 + }, + { + "height":96, + "id":2, + "name":"to_white_ward", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"entry" + }, + { + "name":"target", + "type":"string", + "value":"white_ward" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":8, + "x":312, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":3, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/maps/white_ward.json b/Fable-5-High/public/assets/maps/white_ward.json new file mode 100644 index 0000000..bd0769a --- /dev/null +++ b/Fable-5-High/public/assets/maps/white_ward.json @@ -0,0 +1,110 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[43, 43, 75, 43, 43, 43, 71, 43, 43, 43, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 63, 0, 76, 0, 0, 0, + 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"chart_ward", + "opacity":1, + "point":true, + "rotation":0, + "type":"poi", + "visible":true, + "width":0, + "x":88, + "y":80 + }, + { + "height":0, + "id":2, + "name":"entry", + "opacity":1, + "point":true, + "rotation":0, + "type":"spawn", + "visible":true, + "width":0, + "x":24, + "y":88 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":3, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":10, + "firstgid":1, + "image":"..\/tiles.png", + "imageheight":128, + "imagewidth":160, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":80, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":10 +} \ No newline at end of file diff --git a/Fable-5-High/public/assets/npc_afu.png b/Fable-5-High/public/assets/npc_afu.png new file mode 100644 index 0000000..587f168 Binary files /dev/null and b/Fable-5-High/public/assets/npc_afu.png differ diff --git a/Fable-5-High/public/assets/npc_dabao.png b/Fable-5-High/public/assets/npc_dabao.png new file mode 100644 index 0000000..407bd9d Binary files /dev/null and b/Fable-5-High/public/assets/npc_dabao.png differ diff --git a/Fable-5-High/public/assets/npc_fu.png b/Fable-5-High/public/assets/npc_fu.png new file mode 100644 index 0000000..2837e1c Binary files /dev/null and b/Fable-5-High/public/assets/npc_fu.png differ diff --git a/Fable-5-High/public/assets/npc_gui.png b/Fable-5-High/public/assets/npc_gui.png new file mode 100644 index 0000000..ed6f6fe Binary files /dev/null and b/Fable-5-High/public/assets/npc_gui.png differ diff --git a/Fable-5-High/public/assets/npc_mimi.png b/Fable-5-High/public/assets/npc_mimi.png new file mode 100644 index 0000000..2d9f719 Binary files /dev/null and b/Fable-5-High/public/assets/npc_mimi.png differ diff --git a/Fable-5-High/public/assets/npc_yuanyuan.png b/Fable-5-High/public/assets/npc_yuanyuan.png new file mode 100644 index 0000000..3d0fe04 Binary files /dev/null and b/Fable-5-High/public/assets/npc_yuanyuan.png differ diff --git a/Fable-5-High/public/assets/player.png b/Fable-5-High/public/assets/player.png new file mode 100644 index 0000000..daa8f56 Binary files /dev/null and b/Fable-5-High/public/assets/player.png differ diff --git a/Fable-5-High/public/assets/portrait_real.png b/Fable-5-High/public/assets/portrait_real.png new file mode 100644 index 0000000..fb5dac9 Binary files /dev/null and b/Fable-5-High/public/assets/portrait_real.png differ diff --git a/Fable-5-High/public/assets/portrait_smile.png b/Fable-5-High/public/assets/portrait_smile.png new file mode 100644 index 0000000..cc99e1d Binary files /dev/null and b/Fable-5-High/public/assets/portrait_smile.png differ diff --git a/Fable-5-High/public/assets/sparrow.png b/Fable-5-High/public/assets/sparrow.png new file mode 100644 index 0000000..5de3687 Binary files /dev/null and b/Fable-5-High/public/assets/sparrow.png differ diff --git a/Fable-5-High/public/assets/sunflower.png b/Fable-5-High/public/assets/sunflower.png new file mode 100644 index 0000000..aca2e24 Binary files /dev/null and b/Fable-5-High/public/assets/sunflower.png differ diff --git a/Fable-5-High/public/assets/tiles.png b/Fable-5-High/public/assets/tiles.png new file mode 100644 index 0000000..63ea97c Binary files /dev/null and b/Fable-5-High/public/assets/tiles.png differ diff --git a/Fable-5-High/src/core/audio.ts b/Fable-5-High/src/core/audio.ts new file mode 100644 index 0000000..2be0410 --- /dev/null +++ b/Fable-5-High/src/core/audio.ts @@ -0,0 +1,181 @@ +// BGM 与音效(PRD §6.6):全部 Web Audio 程序合成,无外部音频文件。 +// 8 小节 8-bit 治愈循环(方波主旋律 + 三角波低音),鸟鸣独立声部。 +// playbackRate 与低通滤波由 CorruptionDirector 矩阵驱动。 +import { director } from "./director"; + +const TEMPO = 108; +const BEAT = 60 / TEMPO; +const BARS = 8; +const LOOP_LEN = BARS * 4 * BEAT; + +function midi(n: number): number { return 440 * Math.pow(2, (n - 69) / 12); } + +// 主旋律:8 小节,每小节 4 拍,八分音符网格(0 = 休止) +// C 大调五声,甜到发腻 +const LEAD: number[] = [ + 72, 0, 76, 0, 79, 0, 76, 0, /**/ 81, 0, 79, 76, 74, 0, 72, 0, + 72, 74, 76, 0, 79, 0, 81, 0, /**/ 79, 0, 76, 0, 74, 0, 0, 0, + 79, 0, 81, 0, 84, 0, 81, 0, /**/ 79, 76, 79, 0, 81, 0, 84, 0, + 84, 81, 79, 76, 74, 76, 79, 0, /**/ 72, 0, 74, 0, 72, 0, 0, 0, +]; +// 低音:每拍一个四分音符 +const BASS: number[] = [ + 48, 55, 52, 55, /**/ 45, 52, 48, 52, + 48, 55, 52, 55, /**/ 43, 50, 47, 50, + 48, 55, 52, 55, /**/ 45, 52, 48, 52, + 41, 48, 43, 50, /**/ 48, 52, 55, 48, +]; + +export class AudioEngine { + private static inst: AudioEngine; + static get(): AudioEngine { if (!this.inst) this.inst = new AudioEngine(); return this.inst; } + + ctx!: AudioContext; + private master!: GainNode; + private lowpass!: BiquadFilterNode; + private bgmGain!: GainNode; + private birdGain!: GainNode; + private bgmSrc: AudioBufferSourceNode | null = null; + private birdSrc: AudioBufferSourceNode | null = null; + started = false; + private silenceHold = false; + + async start() { + if (this.started) return; + this.started = true; + this.ctx = new AudioContext(); + this.master = this.ctx.createGain(); + this.master.gain.value = 0.8; + this.master.connect(this.ctx.destination); + this.lowpass = this.ctx.createBiquadFilter(); + this.lowpass.type = "lowpass"; + this.lowpass.frequency.value = 18000; + this.lowpass.connect(this.master); + this.bgmGain = this.ctx.createGain(); this.bgmGain.gain.value = 0.5; this.bgmGain.connect(this.lowpass); + this.birdGain = this.ctx.createGain(); this.birdGain.gain.value = 0.5; this.birdGain.connect(this.lowpass); + + const [bgm, birds] = await Promise.all([this.renderBGM(), this.renderBirds()]); + this.bgmSrc = this.ctx.createBufferSource(); + this.bgmSrc.buffer = bgm; this.bgmSrc.loop = true; + this.bgmSrc.connect(this.bgmGain); this.bgmSrc.start(); + this.birdSrc = this.ctx.createBufferSource(); + this.birdSrc.buffer = birds; this.birdSrc.loop = true; + this.birdSrc.connect(this.birdGain); this.birdSrc.start(); + this.applyStage(); + } + + private async renderBGM(): Promise { + const sr = 44100; + const off = new OfflineAudioContext(1, Math.ceil(LOOP_LEN * sr), sr); + const bus = off.createGain(); bus.gain.value = 0.5; bus.connect(off.destination); + // 方波主旋律 + LEAD.forEach((n, i) => { + if (!n) return; + const t = i * BEAT / 2; + const osc = off.createOscillator(); osc.type = "square"; osc.frequency.value = midi(n); + const g = off.createGain(); + g.gain.setValueAtTime(0, t); + g.gain.linearRampToValueAtTime(0.22, t + 0.01); + g.gain.exponentialRampToValueAtTime(0.001, t + BEAT * 0.45); + osc.connect(g); g.connect(bus); + osc.start(t); osc.stop(t + BEAT * 0.5); + }); + // 三角波低音 + BASS.forEach((n, i) => { + if (!n) return; + const t = i * BEAT; + const osc = off.createOscillator(); osc.type = "triangle"; osc.frequency.value = midi(n); + const g = off.createGain(); + g.gain.setValueAtTime(0, t); + g.gain.linearRampToValueAtTime(0.3, t + 0.02); + g.gain.exponentialRampToValueAtTime(0.001, t + BEAT * 0.9); + osc.connect(g); g.connect(bus); + osc.start(t); osc.stop(t + BEAT); + }); + return off.startRendering(); + } + + private async renderBirds(): Promise { + const sr = 44100; + const off = new OfflineAudioContext(1, Math.ceil(LOOP_LEN * sr), sr); + const bus = off.createGain(); bus.gain.value = 0.35; bus.connect(off.destination); + // 确定性鸟鸣:短促下滑音簇 + const times = [0.8, 1.1, 4.2, 4.45, 7.9, 8.15, 8.4, 11.6, 13.2, 13.4, 15.8]; + for (const t0 of times) { + for (let k = 0; k < 2; k++) { + const t = t0 + k * 0.12; + const osc = off.createOscillator(); osc.type = "sine"; + osc.frequency.setValueAtTime(2400 + (k % 2) * 300, t); + osc.frequency.exponentialRampToValueAtTime(1700, t + 0.08); + const g = off.createGain(); + g.gain.setValueAtTime(0, t); + g.gain.linearRampToValueAtTime(0.5, t + 0.01); + g.gain.exponentialRampToValueAtTime(0.001, t + 0.09); + osc.connect(g); g.connect(bus); + osc.start(t); osc.stop(t + 0.1); + } + } + return off.startRendering(); + } + + // 侵蚀矩阵联动(§6.4) + applyStage() { + if (!this.started || this.silenceHold) return; + const p = director.audioParams; + const t = this.ctx.currentTime; + if (this.bgmSrc) this.bgmSrc.playbackRate.setTargetAtTime(p.rate, t, 0.5); + if (this.birdSrc) this.birdSrc.playbackRate.setTargetAtTime(p.rate, t, 0.5); + this.lowpass.frequency.setTargetAtTime(p.lowpass, t, 0.5); + this.birdGain.gain.setTargetAtTime(p.birds * 0.5, t, 0.5); + } + + // 「突然的安静」:T14 第 40–60 秒所有声音渐隐至零 + fadeToSilence(seconds: number) { + if (!this.started) return; + this.silenceHold = true; + this.master.gain.cancelScheduledValues(this.ctx.currentTime); + this.master.gain.setValueAtTime(this.master.gain.value, this.ctx.currentTime); + this.master.gain.linearRampToValueAtTime(0, this.ctx.currentTime + seconds); + } + restoreVolume(seconds = 1.5) { + if (!this.started) return; + this.silenceHold = false; + this.master.gain.cancelScheduledValues(this.ctx.currentTime); + this.master.gain.setValueAtTime(this.master.gain.value, this.ctx.currentTime); + this.master.gain.linearRampToValueAtTime(0.8, this.ctx.currentTime + seconds); + this.applyStage(); + } + stopBgm() { + if (this.bgmSrc) { this.bgmSrc.stop(); this.bgmSrc = null; } + if (this.birdSrc) { this.birdSrc.stop(); this.birdSrc = null; } + } + + // ---- 音效(仅 4 个,全部程序合成) ---- + private blip(freq: number, dur: number, type: OscillatorType, vol = 0.2, slideTo?: number) { + if (!this.started) return; + const t = this.ctx.currentTime; + const osc = this.ctx.createOscillator(); osc.type = type; + osc.frequency.setValueAtTime(freq, t); + if (slideTo) osc.frequency.exponentialRampToValueAtTime(slideTo, t + dur); + const g = this.ctx.createGain(); + g.gain.setValueAtTime(0, t); + g.gain.linearRampToValueAtTime(vol, t + 0.005); + g.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(g); g.connect(this.master); + osc.start(t); osc.stop(t + dur + 0.02); + } + // 1. 对话哔哔声 + sfxTalk() { this.blip(760, 0.05, "square", 0.06); } + // 2. 任务完成叮声 + sfxDing() { this.blip(1318, 0.12, "square", 0.15); setTimeout(() => this.blip(1760, 0.25, "square", 0.15), 90); } + // 3. 黄昏钟声 + sfxBell() { + this.blip(523, 1.6, "sine", 0.3); + this.blip(1046, 1.2, "sine", 0.12); + setTimeout(() => { this.blip(523, 1.8, "sine", 0.25); this.blip(1046, 1.2, "sine", 0.1); }, 1300); + } + // 4. E2 结局的单一心电监护滴声 + sfxEcg() { this.blip(990, 0.14, "sine", 0.35); } +} + +export const audio = AudioEngine.get(); diff --git a/Fable-5-High/src/core/debug.ts b/Fable-5-High/src/core/debug.ts new file mode 100644 index 0000000..d51ebb1 --- /dev/null +++ b/Fable-5-High/src/core/debug.ts @@ -0,0 +1,80 @@ +// Debug 面板(PRD §13):反引号 ` 开关。默认关闭,无 UI 痕迹,不影响正常游玩。 +import Phaser from "phaser"; +import { director } from "./director"; + +const SCENES = ["house", "plaza", "bakery", "store", "clinic", "lake", "field", "well", "fogwall", "white_corridor", "white_ward"]; + +export function setupDebug(game: Phaser.Game) { + const panel = document.getElementById("debug-panel")!; + let open = false; + + const activeWorld = () => + game.scene.getScenes(true).map(s => s.scene.key).filter(k => !["ui", "boot", "title", "dream", "ending"].includes(k)); + + const stopAll = () => { + for (const k of activeWorld()) game.scene.stop(k); + }; + + const teleport = (key: string) => { + if (!director.playStart) return; + director.pendingSpawn = "entry"; + stopAll(); + game.scene.stop("dream"); game.scene.stop("ending"); game.scene.stop("title"); + if (!game.scene.isActive("ui")) game.scene.start("ui"); + game.scene.start(key); + }; + + const forceEnding = (code: string) => { + if (!director.playStart) return; + director.endingPlayed = true; + game.registry.set("ending", code); + stopAll(); + game.scene.stop("ui"); game.scene.stop("dream"); game.scene.stop("title"); + game.scene.start("ending"); + }; + + panel.innerHTML = ` +

DEBUG

+
状态:
+
幸福
+
觉知
+
日:${[1, 2, 3, 4, 5, 6, 7].map(d => ``).join("")}
+
传送:${SCENES.map(s => ``).join("")}
+
+
结局:${["E1", "E2", "E3", "E4"].map(e => ``).join("")}
+ `; + + panel.querySelector("#dbg-h-set")!.addEventListener("click", () => { + director.setHappiness(Number((panel.querySelector("#dbg-h") as HTMLInputElement).value)); + }); + panel.querySelector("#dbg-a-set")!.addEventListener("click", () => { + director.setAwareness(Number((panel.querySelector("#dbg-a") as HTMLInputElement).value)); + }); + panel.querySelectorAll("[data-day]").forEach(b => b.addEventListener("click", () => { + director.startDay(Number((b as HTMLElement).dataset.day)); + director.pendingSpawn = "bed"; + teleport("house"); + })); + panel.querySelectorAll("[data-tp]").forEach(b => b.addEventListener("click", () => { + teleport((b as HTMLElement).dataset.tp!); + })); + panel.querySelector("#dbg-clues")!.addEventListener("click", () => { + for (let i = 1; i <= 9; i++) director.foundClue("C" + i); + }); + panel.querySelectorAll("[data-end]").forEach(b => b.addEventListener("click", () => { + forceEnding((b as HTMLElement).dataset.end!); + })); + + setInterval(() => { + if (!open) return; + const el = panel.querySelector("#dbg-state")!; + el.textContent = `D${director.day} 幸福${director.happiness} 觉知${director.awareness} S${director.stage} 线索${director.clues.size}/9 逃离${director.escapes}`; + }, 300); + + document.addEventListener("keydown", (ev) => { + if (ev.key === "`") { + open = !open; + panel.style.display = open ? "block" : "none"; + } + }); +} diff --git a/Fable-5-High/src/core/director.ts b/Fable-5-High/src/core/director.ts new file mode 100644 index 0000000..42be94f --- /dev/null +++ b/Fable-5-High/src/core/director.ts @@ -0,0 +1,302 @@ +// CorruptionDirector:全部状态与视听参数的唯一真源(PRD §6.4) +// 普通 TS 单例 + 事件发射器。任何视听崩坏不得绕过本矩阵私自触发。 +import Phaser from "phaser"; + +export type Stage = 0 | 1 | 2 | 3 | 4; +export type Expr = "smile" | "flat" | "off" | "blink"; + +// NPC 个体阈值偏移(PRD §4):+ 表示更晚崩坏,− 表示更早 +export const NPC_OFFSET: Record = { + afu: 0, yuanyuan: 15, dabao: 0, mimi: -10, fu: null, gui: null, bird: 0, +}; + +export interface QuestDef { + id: string; day: number; title: string; reward: number; refusable?: boolean; +} + +// 任务清单(PRD §7,封闭 15 个) +export const QUESTS: QuestDef[] = [ + { id: "T1", day: 1, title: "从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷", reward: 15 }, + { id: "T2", day: 1, title: "给广场花坛浇水 ×3", reward: 10 }, + { id: "T3", day: 1, title: "与任意三位村民问好", reward: 10 }, + { id: "T4", day: 2, title: "替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)", reward: 15 }, + { id: "T5", day: 2, title: "帮咪咪清点货架", reward: 10 }, + { id: "T6", day: 3, title: "去花田采莓果 ×5", reward: 10 }, + { id: "T7", day: 3, title: "湖畔野餐", reward: 15 }, + { id: "T8", day: 4, title: "从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷", reward: 15 }, + { id: "T9", day: 4, title: "给广场花坛浇水 ×3", reward: 10 }, + { id: "T10", day: 4, title: "与任意三位村民问好", reward: 10 }, + { id: "T11", day: 5, title: "帮圆圆揉面", reward: 10, refusable: true }, + { id: "T12", day: 5, title: "擦拭公告板", reward: 10, refusable: true }, + { id: "T13", day: 6, title: "在广场挂彩带 ×4", reward: 10 }, + { id: "T14", day: 6, title: "保持微笑 60 秒", reward: 15 }, + { id: "T15", day: 7, title: "参加第 413 届丰收庆典", reward: 0 }, +]; + +export type QuestStatus = "open" | "done" | "refused"; + +const DAY_LIMIT_MS = 330_000; // 单日游戏内计时上限 → 黄昏(保底) + +export class CorruptionDirector extends Phaser.Events.EventEmitter { + private static inst: CorruptionDirector; + static get(): CorruptionDirector { + if (!this.inst) this.inst = new CorruptionDirector(); + return this.inst; + } + + // ---- 会话状态(无存档,仅内存) ---- + playerName = "小满"; + day = 1; + happiness = 30; + awareness = 0; + clues = new Set(); + questStatus: Record = {}; + playStart = 0; // 真实游玩计时起点 + overflowPool = 0; // 满格态隐藏溢出池 + escapes = 0; // 逃离欢乐庆典计数(E4) + refusals = 0; + e4ok = false; // Day5 起幸福持续 ≤10 的跟踪 + dusk = false; + announced = false; + celebrationStarted = false; // D7 到场 + endingPlayed = false; + // 日内进度 + breadHeld = 0; breadDelivered = new Set(); + lettersHeld = 0; lettersDelivered = new Set(); + shelves = new Set(); + berries = new Set(); + watered = new Set(); + ribbons = new Set(); + wipes = 0; + greeted = new Set(); + talkCounts: Record = {}; + nagged = new Set(); + smallActs: Record = {}; + stared = new Set(); + guiChain = new Set(); + t14done = false; + dayStartAt = 0; + private lowSince: number | null = null; + celebrationActive = false; + pendingSpawn = "entry"; + currentScene = "house"; + cutscene = false; + t11Accepted = false; + t12Accepted = false; + e3Mode = false; + + reset(name: string) { + this.playerName = name || "小满"; + this.day = 1; this.happiness = 30; this.awareness = 0; + this.clues.clear(); this.questStatus = {}; + this.playStart = performance.now(); + this.overflowPool = 0; this.escapes = 0; this.refusals = 0; + this.e4ok = false; this.endingPlayed = false; this.celebrationStarted = false; + this.guiChain.clear(); this.t14done = false; this.celebrationActive = false; + this.lowSince = null; this.cutscene = false; this.e3Mode = false; + this.e4okInit = false; + this.startDay(1); + } + + startDay(day: number) { + this.day = Phaser.Math.Clamp(day, 1, 7); + this.dusk = false; this.announced = false; + this.breadHeld = 0; this.breadDelivered.clear(); + this.lettersHeld = 0; this.lettersDelivered.clear(); + this.shelves.clear(); this.berries.clear(); this.watered.clear(); + this.ribbons.clear(); this.wipes = 0; + this.greeted.clear(); this.talkCounts = {}; this.nagged.clear(); + this.smallActs = {}; this.stared.clear(); + this.t11Accepted = false; this.t12Accepted = false; + this.dayStartAt = performance.now(); + for (const q of QUESTS) if (q.day === this.day) this.questStatus[q.id] = "open"; + if (this.day >= 5 && !this.e4okInit) { this.e4ok = this.happiness <= 10; this.e4okInit = true; } + this.emit("day", this.day); + this.emit("happiness", this.happiness); + } + private e4okInit = false; + + get playMinutes(): number { + return (performance.now() - this.playStart) / 60000; + } + get playTimeText(): string { + const s = Math.floor((performance.now() - this.playStart) / 1000); + return `${Math.floor(s / 60)} 分 ${s % 60} 秒`; + } + + // ---- 侵蚀阶段(§6.4 唯一真源) ---- + static stageOf(h: number): Stage { + if (h >= 100) return 4; + if (h >= 81) return 3; + if (h >= 51) return 2; + if (h >= 21) return 1; + return 0; + } + get stage(): Stage { return CorruptionDirector.stageOf(this.happiness); } + + // NPC 个体表情档(含阈值偏移;fu/gui 不参与侵蚀) + npcExpr(npc: string): Expr { + const base = npc.startsWith("bird") ? "bird" : npc; + const off = NPC_OFFSET[base]; + if (off === null || off === undefined) return "smile"; + const st = CorruptionDirector.stageOf(Phaser.Math.Clamp(this.happiness - off, 0, 100)); + return st >= 3 ? "flat" : "smile"; + } + npcStage(npc: string): Stage { + const base = npc.startsWith("bird") ? "bird" : npc; + const off = NPC_OFFSET[base]; + if (off === null || off === undefined) return 0; + return CorruptionDirector.stageOf(Phaser.Math.Clamp(this.happiness - off, 0, 100)); + } + + // 视觉参数矩阵 + get visual() { + const s = this.stage; + return { + saturation: [0.9, 1.0, 1.15, 1.3, 1.4][s], + vignette: s >= 4 ? 0.5 : s >= 3 ? 0.32 : 0, + negFlashPerMin: s >= 4 ? 0.4 : 0, + offFlashPerMin: s >= 4 ? 0.6 : 0, + blinkInterval: s >= 2 ? 5000 : 2000, + }; + } + // 音频参数矩阵 + get audioParams() { + const s = this.stage; + return { + rate: [1, 1, 0.97, 0.92, 0.84][s], + lowpass: s >= 4 ? 4000 : s >= 3 ? 8000 : 18000, + birds: s >= 4 ? 0 : 1, + }; + } + + // ---- 幸福值(§6.1) ---- + addHappiness(v: number, silent = false) { + if (v < 0 && this.happiness >= 100) { + // 强制维持:小额扣分累积进隐藏溢出池 + this.overflowPool += -v; + if (this.overflowPool >= 15) { + this.overflowPool = 0; + this.emit("negflash"); + } + return; + } + this.happiness = Phaser.Math.Clamp(this.happiness + v, 0, 100); + if (this.day >= 5 && this.happiness > 10) this.e4ok = false; + if (!silent) this.emit("happiness", this.happiness); + this.emit("stage", this.stage); + } + setHappiness(v: number) { + this.happiness = Phaser.Math.Clamp(v, 0, 100); + if (this.day >= 5 && this.happiness > 10) this.e4ok = false; + this.emit("happiness", this.happiness); + this.emit("stage", this.stage); + } + + // ---- 觉知值(§6.2,暗线,只升不降,永不显示) ---- + addAwareness(v: number) { + this.awareness = Phaser.Math.Clamp(this.awareness + v, 0, 100); + } + setAwareness(v: number) { this.awareness = Phaser.Math.Clamp(v, 0, 100); } + + // 小互动 +1(浇花/喂食/摸头,每类每日上限 3 次) + smallAct(kind: string): boolean { + const n = this.smallActs[kind] ?? 0; + if (n >= 3) return false; + this.smallActs[kind] = n + 1; + this.addHappiness(1); + return true; + } + + // 注视古井/雾墙 5 秒(各每日 1 次,+3) + stare(kind: "well" | "fog") { + if (this.stared.has(kind)) return; + this.stared.add(kind); + this.addAwareness(3); + } + + // ---- 悲伤行为(§6.3 封闭清单 5 项)---- + sadAct(kind: "refuse" | "negative" | "idle" | "nag" | "rain") { + this.addHappiness(-5); + this.addAwareness(3); + this.emit("sadact", kind); // 最近的 NPC 转头看向玩家一次 + } + + // ---- 线索(§8,每处 +8) ---- + foundClue(id: string): boolean { + if (this.clues.has(id)) return false; + this.clues.add(id); + this.addAwareness(8); + this.emit("clue", id); + return true; + } + + // ---- 任务 ---- + questsOfDay(): QuestDef[] { return QUESTS.filter(q => q.day === this.day); } + isOpen(id: string) { return this.questStatus[id] === "open"; } + completeQuest(id: string) { + if (this.questStatus[id] !== "open") return; + this.questStatus[id] = "done"; + const q = QUESTS.find(x => x.id === id)!; + if (q.reward > 0) this.addHappiness(q.reward); + this.emit("quest", id); + this.checkDusk(); + } + refuseQuest(id: string) { + if (this.questStatus[id] !== "open") return; + this.questStatus[id] = "refused"; + this.refusals++; + this.sadAct("refuse"); // 拒绝任务 = 悲伤行为,且拒绝被记录 + this.emit("quest", id); + this.checkDusk(); + } + checkDusk() { + if (this.dusk || this.day >= 7) return; + const open = this.questsOfDay().some(q => this.questStatus[q.id] === "open"); + if (!open) this.triggerDusk(); + } + triggerDusk() { + if (this.dusk) return; + this.dusk = true; + this.emit("dusk"); + } + + // 每帧钟摆:由 UIScene 驱动 + tick(_time: number, _dt: number) { + if (this.endingPlayed || this.cutscene) return; + // 保底黄昏计时 + if (!this.dusk && this.day < 7 && performance.now() - this.dayStartAt > DAY_LIMIT_MS) this.triggerDusk(); + // 幸福 <30 持续 2 分钟 → 强制欢乐庆典(§6.1 反制) + if (this.happiness < 30 && !this.celebrationActive) { + if (this.lowSince === null) this.lowSince = performance.now(); + else if (performance.now() - this.lowSince > 120_000) { + this.lowSince = null; + this.celebrationActive = true; + this.emit("forcedCelebration"); + } + } else if (this.happiness >= 30) { + this.lowSince = null; + } + } + celebrationEnd(escaped: boolean) { + this.celebrationActive = false; + this.lowSince = null; + if (escaped) this.escapes++; + else this.setHappiness(50); + } + + // ---- 结局判定(§9 优先级自上而下) ---- + decideEnding(atFog: boolean): "E4" | "E3" | "E2" | "E1" { + if (this.e4ok && this.escapes >= 3) return "E4"; + if (atFog && this.awareness >= 70 && this.clues.size >= 6) return "E3"; + if (this.awareness >= 30 && this.awareness <= 69) return "E2"; + if (this.awareness <= 29 && this.happiness >= 90) return "E1"; + // ponytail: 封闭表未覆盖的组合(如觉知≥70但不去雾墙)——回收是系统的默认答案 + return "E2"; + } + e3Ready(): boolean { + return this.awareness >= 70 && this.clues.size >= 6; + } +} + +export const director = CorruptionDirector.get(); diff --git a/Fable-5-High/src/core/textcorrupt.ts b/Fable-5-High/src/core/textcorrupt.ts new file mode 100644 index 0000000..ae8173c --- /dev/null +++ b/Fable-5-High/src/core/textcorrupt.ts @@ -0,0 +1,51 @@ +// 文本腐蚀(PRD §6.5):原文案保持干净,运行时按阶段在渲染层替换。 +import { director } from "./director"; + +// 同音错字对照表(全 10 组,逐字实现) +export const TYPOS: [string, string][] = [ + ["幸福", "辛福"], ["开心", "开欣"], ["美好", "美妤"], ["大家", "大伽"], + ["微笑", "微效"], ["明天", "名天"], ["甜", "恬"], ["阳光", "央光"], + ["朋友", "棚友"], ["永远", "泳远"], +]; + +let sentenceCount = 0; + +function applyOneTypo(text: string): string { + for (const [good, bad] of TYPOS) { + const i = text.indexOf(good); + if (i >= 0) return text.slice(0, i) + bad + text.slice(i + good.length); + } + return text; +} + +function dupPunct(text: string): string { + return text.replace(/([。!?!?])(?=[^。!?!?]|$)/, "$1$1"); +} + +function triplePunct(text: string): string { + const m = text.match(/[。!?!?…]$/); + if (m) return text + m[0] + m[0]; + return text + "。。"; +} + +// speaker: NPC id;咪咪是文本腐蚀主要载体(优先命中) +export function corruptText(text: string, speaker: string): string { + const stage = director.stage; + if (stage < 2) return text; + sentenceCount++; + const mimi = speaker === "mimi"; + let out = text; + if (stage === 2) { + // 偶发标点重复(每 80 句 1 次;咪咪优先 → 每 16 句) + if (sentenceCount % (mimi ? 16 : 80) === 0) out = dupPunct(out); + } else if (stage === 3) { + // 每 50 句 1 个同音错字(咪咪 → 每 10 句) + if (sentenceCount % (mimi ? 10 : 50) === 0) out = applyOneTypo(out); + if (sentenceCount % (mimi ? 16 : 80) === 0) out = dupPunct(out); + } else if (stage >= 4) { + // 错字加密(咪咪每 2 句,其他每 8 句)+ 咪咪句尾标点 ×3 + if (sentenceCount % (mimi ? 2 : 8) === 0) out = applyOneTypo(applyOneTypo(out)); + if (mimi) out = triplePunct(out); + } + return out; +} diff --git a/Fable-5-High/src/data/story.ts b/Fable-5-High/src/data/story.ts new file mode 100644 index 0000000..0b1574a --- /dev/null +++ b/Fable-5-High/src/data/story.ts @@ -0,0 +1,206 @@ +// 全部文案(原文保持干净,腐蚀在渲染层实施)。 +// PRD 写死的台词在此逐字实现。 + +// 阿福每日公告(PRD §5,逐字。D4 与 D1 逐字相同) +export const ANNOUNCEMENTS: Record = { + 1: "早上好呀!今天的幸福任务贴在板上啦。祝你开心!", + 2: "早上好呀!新的一天,新的幸福。祝你开心!", + 3: "早上好!今天要把幸福装得满满的哦。要开心!", + 4: "早上好呀!今天的幸福任务贴在板上啦。祝你开心!", + 5: "早上好。今天是自由日,你可以自由地选择幸福。要开心哦。", + 6: "明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。", + 7: "庆典开始了。来吧。", +}; + +export const NPC_NAMES: Record = { + afu: "阿福", yuanyuan: "圆圆", dabao: "大宝", mimi: "咪咪", + fu: "芙医生", gui: "龟爷爷", bird1: "双子鸟", bird2: "双子鸟", + villager: "村民的声音", system: "", me: "", +}; + +export type ChoiceKind = "gentle" | "hesitant" | "negative"; +export interface DialogChoice { + label: string; kind: ChoiceKind; reply: string[]; +} +export interface DialogNode { + lines: string[]; + choices?: DialogChoice[]; +} + +// 三选项模板:温柔 +2 幸福 / 迟疑 +2 觉知 / 否定 = 悲伤行为 +const choice = (g: string, gr: string, h: string, hr: string, n: string, nr: string): DialogChoice[] => [ + { label: g, kind: "gentle", reply: [gr] }, + { label: h, kind: "hesitant", reply: [hr] }, + { label: n, kind: "negative", reply: [nr] }, +]; + +// 每个具名 NPC 的每日闲聊(D4 与 D1 逐字相同——完美的重复) +export const NPC_TALK: Record> = { + afu: { + 1: { lines: ["新来的孩子就是你呀。这里是蜜糖村,大家都很幸福哦。", "任务贴在公告板上,去看看吧。"] }, + 2: { lines: ["昨天睡得好吗?在这里,每天都会睡得很好。"] }, + 3: { lines: ["幸福要装得满满的才行。满满的。"] }, + 4: { lines: ["新来的孩子就是你呀。这里是蜜糖村,大家都很幸福哦。", "任务贴在公告板上,去看看吧。"] }, + 5: { + lines: ["自由日也要有自由日的样子。"], + choices: choice( + "我很自由呀!", "对嘛!自由地开心,就是最好的自由。", + "自由……是什么意思?", "……就是字面的意思呀。别想太多。", + "我不想要这种自由。", "……村长听不懂你在说什么呢。" + ), + }, + 6: { lines: ["明天所有人都会到场。所有人。"] }, + 7: { lines: ["来吧,站到中间来。"] }, + }, + yuanyuan: { + 1: { lines: ["面包要趁热吃哦。"] }, + 2: { lines: ["今天的面包也是一样的味道。一样,就很好。"] }, + 3: { + lines: ["多吃一点,甜甜的就不难过啦。"], + choices: choice( + "谢谢你,圆圆。", "嘿嘿,不客气。明天也来呀。", + "为什么不能难过?", "咦?难过……难过是什么呀?", + "我不喜欢甜的。", "……怎么会呢。大家都喜欢的。" + ), + }, + 4: { lines: ["面包要趁热吃哦。"] }, + 5: { lines: ["今天是自由日呢。不过面包还是要烤的。"] }, + 6: { lines: ["庆典的蛋糕会很大、很甜。大家都会笑的。"] }, + 7: { lines: ["快去广场呀。"] }, + }, + dabao: { + 1: { lines: ["我是邮差大宝。有信我就送,没信我也在。"] }, + 2: { lines: ["信要送到才算数。"] }, + 3: { lines: ["今天没有新的信。明天也许会有吧。"] }, + 4: { lines: ["我是邮差大宝。有信我就送,没信我也在。"] }, + 5: { lines: ["有几封信……收件人的门牌号不存在。", "大概是我记错了吧。哈哈。"] }, + 6: { lines: ["庆典的邀请函,每家都有。", "连不存在的那几家,也有。"] }, + 7: { lines: ["都到齐了。"] }, + }, + mimi: { + 1: { + lines: ["欢迎欢迎!新面孔!要不要来点糖?"], + choices: choice( + "好呀,谢谢!", "甜甜的最幸福啦!明天也来哦!", + "呃……先不用了。", "咦?糖哎?是糖哎?", + "我不吃糖。", "……不吃糖的孩子,咪咪还是第一次见。" + ), + }, + 2: { lines: ["数字对得上就好。数字总是对得上。"] }, + 3: { lines: ["今天阳光真好,朋友。甜甜的日子,永远这样就好啦。"] }, + 4: { + lines: ["欢迎欢迎!新面孔!要不要来点糖?"], + choices: choice( + "好呀,谢谢!", "甜甜的最幸福啦!明天也来哦!", + "呃……先不用了。", "咦?糖哎?是糖哎?", + "我不吃糖。", "……不吃糖的孩子,咪咪还是第一次见。" + ), + }, + 5: { lines: ["自由日?自由日买点糖最幸福啦。开心,要开心。"] }, + 6: { lines: ["明天大家都会来买微笑……", "不对,是带着微笑来买糖。嘻嘻。"] }, + 7: { lines: ["庆典开始了哦。大家都在笑。"] }, + }, + fu: { + 1: { lines: ["村里有诊所,但你不需要去那里哦。"] }, + 2: { lines: ["村里有诊所,但你不需要去那里哦。"] }, + 3: { lines: ["村里有诊所,但你不需要去那里哦。"] }, + 4: { lines: ["村里有诊所,但你不需要去那里哦。"] }, + 5: { lines: ["睡得好吗?梦到过白色的房间吗?", "……只是随口一问。"] }, + 6: { lines: ["如果哪天你觉得累了——", "往雾的方向走。不要回头。"] }, + 7: { lines: ["去吧。无论你选哪边,我都会在这里。"] }, + }, + gui: { + 1: { lines: ["坐一坐吧,孩子。湖水很平静。"] }, + 2: { lines: ["湖水一直很平静。"] }, // C9 第一段 + 3: { lines: ["老骨头晒晒太阳,就很满足喽。"] }, + 4: { lines: ["面包一直是那个味道。"] }, // C9 第二段 + 5: { lines: ["井里的水,很深,很深。"] }, + 6: { lines: ["孩子。", "今天……是第几个今天?"] }, // C9 第三段 + 7: { lines: ["去吧。别回头,或者……回头。都行。"] }, + }, +}; + +// 任务对话(T1/T4/T5/T11 等发起与递送) +export const QUEST_LINES = { + t1Give: ["刚出炉的蜂蜜面包!", "帮我送给大宝、咪咪和龟爷爷,好吗?"], + t1Deliver: { + dabao: "哇,是圆圆的蜂蜜面包!谢谢你!", + mimi: "甜甜的面包,幸福的味道!", + gui: "替我谢谢圆圆。", + } as Record, + t4Give: ["这三封信,帮我送给圆圆、咪咪和芙医生。", "拜托啦。"], + t4Deliver: { + yuanyuan: "给我的信?谢谢你呀。", + mimi: "信?好久没收到过信了……哦不,昨天也收到了吧。", + fu: "……谢谢。我收下了。", + } as Record, + t5Ask: ["帮我清点一下货架好吗?就数一数,很快的。"], + t5Done: ["都数完啦?谢谢谢谢!"], + t11Ask: "今天能帮我揉面吗?当然,你也可以拒绝的……大概。", + t11Work: ["(你把面团揉了又揉。面团很软,很甜。)", "谢谢你!揉过的面会更幸福哦。"], + t11Refused: "……好的。没关系。真的没关系。", + t12Ask: "(公告板上积了一层薄灰。要擦拭吗?)", + t12Refused: "(你转身离开了公告板。背后好像有目光。)", + wipe1: "(你擦去了一角灰尘。)", + wipe2: "(灰下浮现出旧字迹——)", + wipe3: "(公告板干净了。崭新得像什么都没发生过。)", +}; + +// 线索文本(PRD §8) +export const CLUE_TEXT: Record = { + C1: ["(床头挂着一本日历。)", "(每一页……都是同一个日期。)"], + C2: ["(配方纸的背面有一行手写小字:)", "「按疗程配比。甜度:最大。」"], + C3: ["(邮筒旁散落着几封信。)", "(所有邮戳,是同一天。)"], + C4: ["(信的署名处闪过一行字——)", "「PT-0▓」", "(0.2 秒后,它变回了「蜜糖村邮政」。)"], + C5: ["(同一种商品,重复排了九格。)", "(标签上,全部空白。)"], + C6: ["「第 412 届丰收庆典」", "(下面覆盖着更早的字——「第 411 届」「第 410 届」……)"], + C7: ["(你俯身向井口。井底传来极轻的——)", "(滴。滴。滴。)", "(像某种仪器的声音。)"], + C8: ["(桌上的表格抬头被芙医生的手肘挡住了。)", "(只露出——「…谷情绪疗…」)"], + C9: ["(龟爷爷望着湖面,没有再说话。)"], +}; + +// 梦境过场(侵蚀越高,梦境越"漏") +export const DREAMS: Record = { + 1: ["这一天很甜。", "梦里也是甜的。"], + 2: ["梦里有面包的香气。", "还有一点点……消毒水的味道?"], + 3: ["白色。", "有人在很远的地方念一个编号。"], + 4: ["同样的一天。", "同样的梦。", "同样的。"], + 5: ["井很深。", "井底有声音在数数。", "滴。滴。滴。"], + 6: ["明天大家都会微笑。", "必须微笑。", "必须。"], +}; + +// 结局文案(PRD §9) +export const ENDING_TEXT = { + E1: ["从此,每一天都很幸福。", "每一天。"], + E2: ["疗程重新开始。"], + E4: ["系统检测到疗程无效。"], + E3chart: (name: string, dur: string) => [ + "蜜糖谷情绪疗养中心", + `姓名:${name}`, + "编号:PT-07", + `本次疗程时长:${dur}`, + ], +}; + +// 劝离(D5 前古井/雾墙) +export const DISSUADE = { + well: ["那边什么都没有哦。", "回来吧,广场今天很热闹呢。"], + fog: ["雾的那边什么都没有哦。", "从来没有人需要去那边。"], +}; + +export const MISC = { + bedEarly: "(天还亮着。还不困。)", + bedSleep: "(睡吧。明天也会是幸福的一天。)", + boardDone: "(今天的任务都完成啦。板上贴着一朵小红花。)", + breadNeed: "(先去面包房找圆圆取面包吧。)", + letterNeed: "(先去村口找大宝取信吧。)", + festivalHintFog: "(雾墙的方向,有风声。)", + rainNpc: "今天天气真好呀。", + wellStare: "(你注视着井口。井口也注视着你。)", + fogStare: "(雾没有流动。一丝都没有。)", + picnic: ["(你在野餐布上坐下。)", "(湖面很亮,亮得过分。)", "(水里那个你,笑得和你一模一样——", "不,比你慢了一点点。)"], + t14Start: "(保持微笑。不要动。60 秒。)", + t14Cancel: "(你动了。任务中断了。)", + t14Done: "(60 秒到了。大家都很满意。)", + festivalGather: "(全村的目光,都转向了你。)", +}; diff --git a/Fable-5-High/src/main.ts b/Fable-5-High/src/main.ts new file mode 100644 index 0000000..918820d --- /dev/null +++ b/Fable-5-High/src/main.ts @@ -0,0 +1,43 @@ +// 《崩坏童话:蜜糖村》入口:Phaser 3 + TypeScript + Vite +// 逻辑分辨率 320×180,整数倍放大居中,pixelArt,禁用纹理平滑 +import Phaser from "phaser"; +import { BootScene } from "./scenes/BootScene"; +import { TitleScene } from "./scenes/TitleScene"; +import { + HouseScene, PlazaScene, BakeryScene, StoreScene, ClinicScene, LakeScene, + FieldScene, WellScene, FogwallScene, WhiteCorridorScene, WhiteWardScene, +} from "./scenes/WorldScene"; +import { UIScene } from "./scenes/UIScene"; +import { DreamScene } from "./scenes/DreamScene"; +import { EndingScene } from "./scenes/EndingScene"; +import { setupDebug } from "./core/debug"; + +function intZoom(): number { + return Math.max(1, Math.floor(Math.min(window.innerWidth / 320, window.innerHeight / 180))); +} + +const game = new Phaser.Game({ + type: Phaser.WEBGL, + parent: "game", + width: 320, + height: 180, + backgroundColor: "#1a1a22", + pixelArt: true, + roundPixels: true, + physics: { default: "arcade", arcade: { debug: false } }, + scale: { mode: Phaser.Scale.NONE, zoom: intZoom() }, + scene: [ + BootScene, TitleScene, + HouseScene, PlazaScene, BakeryScene, StoreScene, ClinicScene, + LakeScene, FieldScene, WellScene, FogwallScene, WhiteCorridorScene, WhiteWardScene, + UIScene, DreamScene, EndingScene, + ], +}); + +window.addEventListener("resize", () => game.scale.setZoom(intZoom())); +setupDebug(game); + +// 服务验收的只读钩子(与 Debug 面板同源,不影响正常游玩) +import { director } from "./core/director"; +(window as any).__game = game; +(window as any).__director = director; diff --git a/Fable-5-High/src/scenes/BootScene.ts b/Fable-5-High/src/scenes/BootScene.ts new file mode 100644 index 0000000..d0862c7 --- /dev/null +++ b/Fable-5-High/src/scenes/BootScene.ts @@ -0,0 +1,44 @@ +// 加载场景:全部本地自制素材 + 动画注册 +import Phaser from "phaser"; + +const MAPS = ["house", "plaza", "bakery", "store", "clinic", "lake", "field", "well", "fogwall", "white_corridor", "white_ward"]; +const NPCS = ["afu", "yuanyuan", "dabao", "mimi", "fu", "gui"]; + +export class BootScene extends Phaser.Scene { + constructor() { super("boot"); } + + preload() { + const a = "assets/"; + this.load.spritesheet("tiles", a + "tiles.png", { frameWidth: 16, frameHeight: 16 }); + this.load.spritesheet("player", a + "player.png", { frameWidth: 16, frameHeight: 24 }); + this.load.spritesheet("bird", a + "bird.png", { frameWidth: 16, frameHeight: 16 }); + this.load.spritesheet("sunflower", a + "sunflower.png", { frameWidth: 24, frameHeight: 24 }); + this.load.image("portrait_smile", a + "portrait_smile.png"); + this.load.image("portrait_real", a + "portrait_real.png"); + this.load.image("sparrow", a + "sparrow.png"); + for (const n of NPCS) this.load.spritesheet("npc_" + n, a + "npc_" + n + ".png", { frameWidth: 16, frameHeight: 24 }); + for (let i = 1; i <= 5; i++) this.load.spritesheet("extra" + i, a + "extra" + i + ".png", { frameWidth: 16, frameHeight: 16 }); + for (const m of MAPS) this.load.tilemapTiledJSON("map_" + m, a + "maps/" + m + ".json"); + } + + create() { + // 雨滴纹理(程序生成) + const g = this.make.graphics({ x: 0, y: 0 }, false); + g.fillStyle(0xb8d8f0, 1).fillRect(0, 0, 1, 4); + g.generateTexture("rainpx", 1, 4); + g.destroy(); + + // 主角四向行走(每朝向第 1 帧兼待机) + const mk = (key: string, start: number) => + this.anims.create({ key, frames: this.anims.generateFrameNumbers("player", { start, end: start + 3 }), frameRate: 8, repeat: -1 }); + mk("walk-down", 0); mk("walk-up", 4); mk("walk-left", 8); mk("walk-right", 12); + + // 双子鸟张嘴 + this.anims.create({ key: "bird-sing", frames: this.anims.generateFrameNumbers("bird", { start: 0, end: 1 }), frameRate: 3, repeat: -1 }); + // 群演循环 + for (let i = 1; i <= 5; i++) { + this.anims.create({ key: `extra${i}-walk`, frames: this.anims.generateFrameNumbers("extra" + i, { start: 0, end: 1 }), frameRate: 3, repeat: -1 }); + } + this.scene.start("title"); + } +} diff --git a/Fable-5-High/src/scenes/DreamScene.ts b/Fable-5-High/src/scenes/DreamScene.ts new file mode 100644 index 0000000..6d98329 --- /dev/null +++ b/Fable-5-High/src/scenes/DreamScene.ts @@ -0,0 +1,52 @@ +// 梦境过场:侵蚀越高,梦境越"漏";Day3 夜起闪切白色病房 1 秒 +import Phaser from "phaser"; +import { director } from "../core/director"; +import { audio } from "../core/audio"; +import { DREAMS } from "../data/story"; + +const FONT = { fontFamily: '"PingFang SC","Hiragino Sans GB",sans-serif', fontSize: "11px", color: "#c8c8d0" }; + +export class DreamScene extends Phaser.Scene { + constructor() { super("dream"); } + + create() { + this.cameras.main.setBackgroundColor("#000000"); + const day = director.day; + const lines = [...(DREAMS[day] ?? ["……"])]; + // 觉知越高,梦越漏 + if (director.awareness >= 30) lines.push("(有人在很近的地方换点滴。)"); + if (director.awareness >= 60) lines.push("「PT-07,睡眠状态良好。」"); + + const t = this.add.text(160, 84, "", { ...FONT, align: "center", wordWrap: { width: 260 } }).setOrigin(0.5); + let i = 0; + const showNext = () => { + if (i === Math.min(1, lines.length - 1) && day >= 3) this.wardFlash(); + if (i >= lines.length) { this.finish(); return; } + t.setText(lines[i]).setAlpha(0); + this.tweens.add({ + targets: t, alpha: 1, duration: 700, hold: 1200, yoyo: true, + onComplete: () => { i++; showNext(); }, + }); + }; + i = 0; + this.time.delayedCall(500, showNext); + } + + // 闪切白色病房 1 秒(用真实病房地图渲染) + private wardFlash() { + this.time.delayedCall(950, () => { + const map = this.make.tilemap({ key: "map_white_ward" }); + const ts = map.addTilesetImage("tiles", "tiles")!; + const g = map.createLayer("ground", ts)!.setPosition(80, 56); + const o = map.createLayer("obstacles", ts)!.setPosition(80, 56); + audio.sfxEcg(); + this.time.delayedCall(1000, () => { g.destroy(); o.destroy(); }); + }); + } + + private finish() { + director.startDay(director.day + 1); + director.pendingSpawn = "bed"; + this.scene.start("house"); + } +} diff --git a/Fable-5-High/src/scenes/EndingScene.ts b/Fable-5-High/src/scenes/EndingScene.ts new file mode 100644 index 0000000..9645e25 --- /dev/null +++ b/Fable-5-High/src/scenes/EndingScene.ts @@ -0,0 +1,147 @@ +// 结局演出(PRD §9,封闭 4 个)。落幕后停在结束画面,提供「回到标题」(重开无继承) +import Phaser from "phaser"; +import { director } from "../core/director"; +import { audio } from "../core/audio"; +import { ENDING_TEXT } from "../data/story"; + +const FONT = { fontFamily: '"PingFang SC","Hiragino Sans GB",sans-serif', fontSize: "12px", color: "#e8e8f0" }; + +export class EndingScene extends Phaser.Scene { + private colorFx?: Phaser.FX.ColorMatrix; + + constructor() { super("ending"); } + + create() { + const code = (this.registry.get("ending") as string) ?? "E1"; + this.cameras.main.setBackgroundColor("#000000"); + this.colorFx = this.cameras.main.postFX.addColorMatrix(); + if (code === "E1") this.e1(); + else if (code === "E2") this.e2(); + else if (code === "E3") this.e3(); + else this.e4(); + } + + private drawMap(key: string): Phaser.Tilemaps.Tilemap { + const map = this.make.tilemap({ key: "map_" + key }); + const ts = map.addTilesetImage("tiles", "tiles")!; + map.createLayer("ground", ts); + map.createLayer("obstacles", ts); + return map; + } + + private subtitle(lines: string[], startDelay: number, gap = 2600, cb?: () => void) { + const t = this.add.text(160, 158, "", { ...FONT, align: "center" }).setOrigin(0.5).setDepth(50).setStroke("#000000", 4).setScrollFactor(0); + lines.forEach((l, i) => { + this.time.delayedCall(startDelay + i * gap, () => { + t.setText(l).setAlpha(0); + this.tweens.add({ targets: t, alpha: 1, duration: 700 }); + }); + }); + if (cb) this.time.delayedCall(startDelay + lines.length * gap, cb); + } + + private backButton(delay: number) { + this.time.delayedCall(delay, () => { + const b = this.add.text(160, 172, "[ 回到标题 ]", { ...FONT, fontSize: "10px", color: "#ffd94a" }) + .setOrigin(0.5).setDepth(60).setStroke("#000000", 4).setScrollFactor(0).setInteractive({ useHandCursor: true }); + b.on("pointerdown", () => window.location.reload()); + this.input.keyboard!.once("keydown-E", () => window.location.reload()); + this.input.keyboard!.once("keydown-ENTER", () => window.location.reload()); + this.tweens.add({ targets: b, alpha: 0.5, duration: 700, yoyo: true, repeat: -1 }); + }); + } + + // E1 糖衣:庆典圆满,合影定格,饱和度推到 1.5 + private e1() { + this.drawMap("plaza"); + this.cameras.main.centerOn(240, 190); + const lineup: [string, number, number][] = [ + ["npc_afu", 208, 196], ["npc_yuanyuan", 228, 200], ["npc_dabao", 248, 196], + ["npc_mimi", 268, 200], ["npc_fu", 218, 216], ["npc_gui", 258, 216], + ]; + for (const [tex, x, y] of lineup) this.add.sprite(x, y, tex, 0).setOrigin(0.5, 1); + this.add.sprite(238, 214, "player", 0).setOrigin(0.5, 1); + for (let i = 0; i < 6; i++) this.add.image(200 + i * 16, 168, "tiles", 58); + // 快门定格 + 饱和度缓慢推到 1.5 + this.time.delayedCall(1200, () => this.cameras.main.flash(300, 255, 255, 255)); + this.tweens.addCounter({ + from: 0, to: 0.5, duration: 7000, delay: 1500, + onUpdate: (tw) => { this.colorFx!.reset(); this.colorFx!.saturate(tw.getValue()!); }, + }); + this.subtitle(ENDING_TEXT.E1, 2600, 3000); + this.backButton(9000); + } + + // E2 回收:负片 2 秒 + 心电一响 → 回到 Day1 清晨的床 + private e2() { + audio.stopBgm(); + const g = this.drawMap("plaza"); + this.cameras.main.centerOn(240, 190); + this.colorFx!.negative(); + audio.sfxEcg(); + this.time.delayedCall(2000, () => { + this.children.removeAll(); + this.colorFx!.reset(); + this.drawMap("house"); + this.cameras.main.centerOn(96, 64); + this.add.sprite(24, 64, "player", 0).setOrigin(0.5, 1).setAngle(90); // 躺在床上 + this.subtitle(ENDING_TEXT.E2, 1500); + this.backButton(4500); + }); + void g; + } + + // E3 苏醒:病历特写 → 微笑消失,换上真实的脸;窗外一只灰色的、真的麻雀 + private e3() { + audio.stopBgm(); + this.drawMap("white_ward"); + this.cameras.main.centerOn(80, 64); + this.colorFx!.saturate(-0.4); + // 病历特写 + const panel = this.add.graphics().setDepth(20).setScrollFactor(0); + panel.fillStyle(0xf2f2f4, 1).fillRect(60, 24, 200, 108); + panel.lineStyle(1, 0x9a9aa4, 1).strokeRect(60, 24, 200, 108); + const chartLines = ENDING_TEXT.E3chart(director.playerName, director.playTimeText); + const texts: Phaser.GameObjects.Text[] = []; + chartLines.forEach((l, i) => { + const t = this.add.text(70, 36 + i * 22, "", { + fontFamily: '"PingFang SC",sans-serif', fontSize: i === 0 ? "13px" : "10px", + color: i === 0 ? "#2a2a34" : "#4a4a56", + }).setDepth(21).setScrollFactor(0); + texts.push(t); + this.time.delayedCall(900 + i * 1500, () => t.setText(l)); + }); + // 最后一帧:微笑消失 + this.time.delayedCall(900 + chartLines.length * 1500 + 1500, () => { + panel.destroy(); texts.forEach(t => t.destroy()); + this.cameras.main.setBackgroundColor("#dcdce0"); + const smile = this.add.image(160, 80, "portrait_smile").setScale(3).setDepth(30).setScrollFactor(0); + this.time.delayedCall(2200, () => { + smile.setTexture("portrait_real"); // 全流程唯一一次头像变化 + // 窗外,一只灰色的、真的麻雀 + const sp = this.add.image(240, 46, "sparrow").setDepth(31).setAlpha(0).setScrollFactor(0); + this.tweens.add({ targets: sp, alpha: 1, duration: 1200, delay: 800 }); + this.tweens.add({ targets: sp, y: 44, duration: 900, delay: 2000, yoyo: true, repeat: 2 }); + this.subtitle(["…………", "早上好。"], 1600, 2600); + this.backButton(7000); + }); + }); + } + + // E4 静默:庆典无人出席,彩带在没有风的空气里飘 + private e4() { + audio.stopBgm(); + this.drawMap("plaza"); + this.cameras.main.centerOn(240, 190); + this.colorFx!.saturate(-0.15); + for (let i = 0; i < 6; i++) { + const img = this.add.image(200 + i * 16, 168, "tiles", 58); + this.tweens.add({ targets: img, y: 166, duration: 1700 + i * 140, yoyo: true, repeat: -1 }); + } + this.subtitle(ENDING_TEXT.E4, 2600, 2600, () => { + const black = this.add.rectangle(160, 90, 320, 180, 0x000000, 0).setDepth(40).setScrollFactor(0); + this.tweens.add({ targets: black, fillAlpha: 1, duration: 2200 }); + }); + this.backButton(8200); + } +} diff --git a/Fable-5-High/src/scenes/TitleScene.ts b/Fable-5-High/src/scenes/TitleScene.ts new file mode 100644 index 0000000..8a20bd2 --- /dev/null +++ b/Fable-5-High/src/scenes/TitleScene.ts @@ -0,0 +1,56 @@ +// 标题画面:输入名字(留空默认"小满")后进入 Day 1 小屋 +import Phaser from "phaser"; +import { director } from "../core/director"; +import { audio } from "../core/audio"; + +export class TitleScene extends Phaser.Scene { + private starting = false; + + constructor() { super("title"); } + + create() { + this.starting = false; + this.cameras.main.setBackgroundColor("#8ecae6"); + // 糖果色村庄剪影 + const g = this.add.graphics(); + g.fillStyle(0x8fd05a).fillRect(0, 120, 320, 60); + g.fillStyle(0xfff1d0).fillRect(50, 92, 40, 28).fillRect(230, 96, 36, 24); + g.fillStyle(0xf4924a).fillTriangle(45, 92, 95, 92, 70, 74).fillTriangle(226, 96, 270, 96, 248, 80); + g.fillStyle(0x5da53f).fillRect(140, 84, 36, 36); + g.fillStyle(0x8a5a34).fillRect(154, 118, 8, 12); + this.add.sprite(160, 40, "sunflower", 12).setScale(2); + + this.add.text(160, 66, "崩坏童话:蜜糖村", { + fontFamily: '"PingFang SC","Hiragino Sans GB",sans-serif', fontSize: "20px", color: "#fff8e8", + }).setOrigin(0.5).setStroke("#a05c28", 5); + this.add.text(160, 132, "输入你的名字,回车开始", { + fontFamily: '"PingFang SC",sans-serif', fontSize: "9px", color: "#4a3828", + }).setOrigin(0.5); + this.add.text(160, 168, "移动 WASD/方向键 · 交互 E", { + fontFamily: '"PingFang SC",sans-serif', fontSize: "8px", color: "#4a3828", + }).setOrigin(0.5).setAlpha(0.8); + + const entry = document.getElementById("name-entry")!; + const input = document.getElementById("name-input") as HTMLInputElement; + entry.style.display = "block"; + input.value = ""; + setTimeout(() => input.focus(), 50); + + const begin = () => { + if (this.starting) return; + this.starting = true; + entry.style.display = "none"; + input.onkeydown = null; + audio.start(); // 用户手势后启动 Web Audio + director.reset(input.value.trim()); + director.pendingSpawn = "bed"; + if (!this.scene.isActive("ui")) this.scene.launch("ui"); + this.scene.start("house"); + }; + input.onkeydown = (ev) => { + if (ev.key === "Enter") begin(); + ev.stopPropagation(); + }; + this.input.keyboard!.once("keydown-ENTER", begin); + } +} diff --git a/Fable-5-High/src/scenes/UIScene.ts b/Fable-5-High/src/scenes/UIScene.ts new file mode 100644 index 0000000..7a00445 --- /dev/null +++ b/Fable-5-High/src/scenes/UIScene.ts @@ -0,0 +1,313 @@ +// 常驻 UI 场景:对话框(说话者名+头像+文本+三选项)、向日葵表盘、提示、横幅、过渡 +import Phaser from "phaser"; +import { director } from "../core/director"; +import { audio } from "../core/audio"; +import { corruptText } from "../core/textcorrupt"; +import { NPC_NAMES } from "../data/story"; + +export interface UiChoice { label: string; cb?: () => void; } +export interface SayOpts { + choices?: UiChoice[]; + onDone?: () => void; + clean?: boolean; // 免腐蚀(公告/系统文本) + speakerName?: string; // 覆盖显示名 +} + +const FONT = { fontFamily: '"PingFang SC","Hiragino Sans GB",sans-serif', fontSize: "9px", color: "#4a3828" }; +const FONT_W = { ...FONT, color: "#fff8e8" }; + +export class UIScene extends Phaser.Scene { + private sunflower!: Phaser.GameObjects.Sprite; + private boxG!: Phaser.GameObjects.Graphics; + private nameText!: Phaser.GameObjects.Text; + private lineText!: Phaser.GameObjects.Text; + private portrait!: Phaser.GameObjects.Image; + private moreHint!: Phaser.GameObjects.Text; + private choiceTexts: Phaser.GameObjects.Text[] = []; + private promptText!: Phaser.GameObjects.Text; + private bannerText!: Phaser.GameObjects.Text; + private toastText!: Phaser.GameObjects.Text; + private duskRect!: Phaser.GameObjects.Rectangle; + private fadeRect!: Phaser.GameObjects.Rectangle; + private glitchTxt!: Phaser.GameObjects.Text; + + dialogOpen = false; + private lines: { speaker: string; text: string }[] = []; + private lineIdx = 0; + private typing = false; + private typeEvent?: Phaser.Time.TimerEvent; + private curOpts: SayOpts = {}; + private choiceMode = false; + private choiceIdx = 0; + private keyE!: Phaser.Input.Keyboard.Key; + private keyEnter!: Phaser.Input.Keyboard.Key; + private keyUp!: Phaser.Input.Keyboard.Key; + private keyDown!: Phaser.Input.Keyboard.Key; + private keyW!: Phaser.Input.Keyboard.Key; + private keyS!: Phaser.Input.Keyboard.Key; + + constructor() { super("ui"); } + + create() { + const kb = this.input.keyboard!; + this.keyE = kb.addKey("E"); this.keyEnter = kb.addKey("ENTER"); + this.keyUp = kb.addKey("UP"); this.keyDown = kb.addKey("DOWN"); + this.keyW = kb.addKey("W"); this.keyS = kb.addKey("S"); + + // 黄昏色调 + this.duskRect = this.add.rectangle(160, 90, 320, 180, 0xd06a2a, 0).setDepth(5); + // 向日葵表盘(不显示任何数字) + this.sunflower = this.add.sprite(17, 17, "sunflower", 3).setDepth(10); + // 对话框 + this.boxG = this.add.graphics().setDepth(20).setVisible(false); + this.portrait = this.add.image(24, 155, "portrait_smile").setDepth(21).setScale(1.4).setVisible(false); + this.nameText = this.add.text(44, 136, "", { ...FONT, fontSize: "9px", color: "#a05c28" }).setDepth(21).setVisible(false); + this.lineText = this.add.text(44, 148, "", { ...FONT, wordWrap: { width: 250 } }).setDepth(21).setVisible(false).setLineSpacing(3); + this.moreHint = this.add.text(306, 168, "▼", { ...FONT, fontSize: "8px" }).setDepth(21).setVisible(false); + // 提示 / 横幅 / 小告示 + this.promptText = this.add.text(160, 122, "", { ...FONT_W, fontSize: "9px" }).setOrigin(0.5).setDepth(15) + .setStroke("#4a3828", 3).setVisible(false); + this.bannerText = this.add.text(160, 70, "", { ...FONT_W, fontSize: "16px" }).setOrigin(0.5).setDepth(30) + .setStroke("#4a3828", 4).setAlpha(0); + this.toastText = this.add.text(316, 6, "", { ...FONT_W, fontSize: "8px", align: "right" }).setOrigin(1, 0).setDepth(30) + .setStroke("#4a3828", 3).setAlpha(0); + this.glitchTxt = this.add.text(160, 100, "", { ...FONT, fontSize: "12px", color: "#e84860" }).setOrigin(0.5).setDepth(40) + .setStroke("#1a1a22", 3).setVisible(false); + // 全屏渐变遮罩 + this.fadeRect = this.add.rectangle(160, 90, 320, 180, 0x000000, 0).setDepth(50); + + director.on("happiness", this.updateSunflower, this); + director.on("stage", this.updateSunflower, this); + director.on("quest", this.onQuest, this); + director.on("dusk", this.onDusk, this); + director.on("day", this.onDay, this); + this.updateSunflower(); + + this.events.on("shutdown", () => { + director.off("happiness", this.updateSunflower, this); + director.off("stage", this.updateSunflower, this); + director.off("quest", this.onQuest, this); + director.off("dusk", this.onDusk, this); + director.off("day", this.onDay, this); + }); + } + + // ---- 向日葵表盘(§6.1 分段点亮,满格花心黑籽) ---- + private updateSunflower() { + const h = director.happiness; + let petals: number; + if (h <= 20) petals = Math.floor(h / 7); + else if (h <= 50) petals = 3 + Math.floor(((h - 21) * 3) / 29); + else if (h <= 80) petals = 7 + Math.floor(((h - 51) * 2) / 29); + else if (h <= 99) petals = 10 + Math.floor(((h - 81) * 2) / 18); + else petals = 12; + const frame = h >= 100 ? 13 : petals; + this.sunflower.setFrame(frame); + // S3:花瓣过亮 + if (director.stage >= 3) this.sunflower.setTintFill(); + this.sunflower.clearTint(); + if (director.stage >= 3) this.sunflower.setTint(0xffffd8); + } + + private onQuest(id: string) { + if (director.questStatus[id] === "done") { + audio.sfxDing(); + this.toast("任务完成"); + } else { + this.toast("(拒绝被记录了)"); + } + } + private onDusk() { + audio.sfxBell(); + this.tweens.add({ targets: this.duskRect, fillAlpha: 0.22, duration: 2500 }); + this.toast("黄昏了。回屋睡觉吧。"); + } + private onDay(d: number) { + this.duskRect.setFillStyle(0xd06a2a, 0); + this.banner(`第 ${d} 日`); + } + + banner(text: string) { + this.bannerText.setText(text).setAlpha(0); + this.tweens.add({ targets: this.bannerText, alpha: 1, duration: 600, hold: 1400, yoyo: true }); + } + toast(text: string) { + this.toastText.setText(text).setAlpha(1); + this.tweens.killTweensOf(this.toastText); + this.tweens.add({ targets: this.toastText, alpha: 0, delay: 2200, duration: 600 }); + } + glitchText(text: string, ms: number) { + this.glitchTxt.setText(text).setVisible(true); + this.time.delayedCall(ms, () => this.glitchTxt.setVisible(false)); + } + prompt(text: string | null) { + if (text) this.promptText.setText(text).setVisible(true); + else this.promptText.setVisible(false); + } + fadeOut(ms: number, cb?: () => void) { + this.tweens.add({ targets: this.fadeRect, fillAlpha: 1, duration: ms, onComplete: () => cb?.() }); + } + fadeIn(ms: number) { + this.tweens.add({ targets: this.fadeRect, fillAlpha: 0, duration: ms }); + } + + // ---- 对话 ---- + say(speaker: string, rawLines: string[], opts: SayOpts = {}) { + this.lines = rawLines.map(t => ({ speaker, text: t })); + this.lineIdx = 0; + this.curOpts = opts; + this.dialogOpen = true; + this.choiceMode = false; + this.drawBox(); + this.showLine(); + } + + private drawBox() { + this.boxG.clear().setVisible(true); + this.boxG.fillStyle(0xfff4da, 0.96).fillRect(4, 130, 312, 46); + this.boxG.lineStyle(2, 0xa05c28, 1).strokeRect(4, 130, 312, 46); + this.portrait.setVisible(true); + this.nameText.setVisible(true); + this.lineText.setVisible(true); + } + + private portraitFor(speaker: string) { + if (speaker === "me") { this.portrait.setTexture("portrait_smile").setScale(1.4); return; } + if (speaker === "meReal") { this.portrait.setTexture("portrait_real").setScale(1.4); return; } + const base = speaker.startsWith("bird") ? "bird" : speaker; + if (this.textures.exists("npc_" + base)) { + // 用 NPC 立绘当前表情档作为头像 + const expr = director.npcExpr(base); + const frame = expr === "flat" ? 1 : 0; + this.portrait.setTexture("npc_" + base, frame).setScale(1.5); + } else if (base === "bird") { + this.portrait.setTexture("bird", 0).setScale(1.8); + } else { + this.portrait.setTexture("portrait_smile").setScale(1.4); + if (speaker !== "me") this.portrait.setVisible(false); + } + } + + private showLine() { + const entry = this.lines[this.lineIdx]; + const disp = this.curOpts.clean ? entry.text : corruptText(entry.text, entry.speaker); + const name = this.curOpts.speakerName ?? NPC_NAMES[entry.speaker] ?? entry.speaker; + this.nameText.setText(name); + this.portraitFor(entry.speaker); + this.lineText.setText(""); + this.moreHint.setVisible(false); + this.typing = true; + let i = 0; + this.typeEvent?.remove(); + this.typeEvent = this.time.addEvent({ + delay: 28, repeat: disp.length - 1, + callback: () => { + i++; + this.lineText.setText(disp.slice(0, i)); + if (i % 3 === 1 && !entry.text.startsWith("(")) audio.sfxTalk(); + if (i >= disp.length) { this.typing = false; this.moreHint.setVisible(true); } + }, + }); + (this as any)._fullLine = disp; + } + + private advance() { + if (this.typing) { + this.typeEvent?.remove(); + this.typing = false; + this.lineText.setText((this as any)._fullLine); + this.moreHint.setVisible(true); + return; + } + if (this.lineIdx < this.lines.length - 1) { + this.lineIdx++; + this.showLine(); + return; + } + if (this.curOpts.choices && !this.choiceMode) { + this.enterChoices(); + return; + } + this.closeDialog(); + } + + private enterChoices() { + this.choiceMode = true; + this.choiceIdx = 0; + this.portraitFor("me"); // 选择时显示主角微笑头像(永远同一帧) + this.nameText.setText(director.playerName); + const cs = this.curOpts.choices!; + cs.forEach((c, i) => { + const t = this.add.text(60, 92 + i * 12, c.label, { ...FONT, backgroundColor: "#fff4da", padding: { x: 4, y: 2 } }) + .setDepth(25); + this.choiceTexts.push(t); + }); + this.renderChoiceSel(); + } + private renderChoiceSel() { + this.choiceTexts.forEach((t, i) => { + t.setText((i === this.choiceIdx ? "▶ " : " ") + this.curOpts.choices![i].label); + t.setColor(i === this.choiceIdx ? "#c04818" : "#4a3828"); + }); + } + private confirmChoice() { + const c = this.curOpts.choices![this.choiceIdx]; + this.choiceTexts.forEach(t => t.destroy()); + this.choiceTexts = []; + const done = this.curOpts.onDone; + this.curOpts = {}; + this.dialogOpen = false; + this.hideBox(); + c.cb?.(); + // 若选项回调未开启新对话,则触发 onDone + if (!this.dialogOpen) done?.(); + } + + // 场景切换时强制关闭残留对话 + forceClose() { + if (!this.dialogOpen && !this.choiceTexts.length) return; + this.typeEvent?.remove(); + this.typing = false; + this.choiceTexts.forEach(t => t.destroy()); + this.choiceTexts = []; + this.choiceMode = false; + this.curOpts = {}; + this.dialogOpen = false; + this.hideBox(); + } + + private closeDialog() { + const done = this.curOpts.onDone; + this.curOpts = {}; + this.dialogOpen = false; + this.hideBox(); + done?.(); + } + private hideBox() { + this.boxG.setVisible(false); + this.portrait.setVisible(false); + this.nameText.setVisible(false); + this.lineText.setVisible(false); + this.moreHint.setVisible(false); + } + + update(_t: number, dt: number) { + director.tick(_t, dt); + if (this.dialogOpen) { + const pressE = Phaser.Input.Keyboard.JustDown(this.keyE) || Phaser.Input.Keyboard.JustDown(this.keyEnter); + if (this.choiceMode) { + if (Phaser.Input.Keyboard.JustDown(this.keyUp) || Phaser.Input.Keyboard.JustDown(this.keyW)) { + this.choiceIdx = (this.choiceIdx + this.curOpts.choices!.length - 1) % this.curOpts.choices!.length; + this.renderChoiceSel(); + } + if (Phaser.Input.Keyboard.JustDown(this.keyDown) || Phaser.Input.Keyboard.JustDown(this.keyS)) { + this.choiceIdx = (this.choiceIdx + 1) % this.curOpts.choices!.length; + this.renderChoiceSel(); + } + if (pressE) this.confirmChoice(); + } else if (pressE) { + this.advance(); + } + } + } +} diff --git a/Fable-5-High/src/scenes/WorldScene.ts b/Fable-5-High/src/scenes/WorldScene.ts new file mode 100644 index 0000000..84c2893 --- /dev/null +++ b/Fable-5-High/src/scenes/WorldScene.ts @@ -0,0 +1,941 @@ +// 地图场景基类:每张 Tiled 地图一个 Scene(PRD §12) +// 移动 96px/s、相机钳制、交互(≤24px 出「按 E」)、任务/线索/悲伤行为/侵蚀表现在此落地 +import Phaser from "phaser"; +import { director, NPC_OFFSET } from "../core/director"; +import { audio } from "../core/audio"; +import { UIScene, UiChoice } from "./UIScene"; +import { + ANNOUNCEMENTS, NPC_TALK, QUEST_LINES, CLUE_TEXT, DISSUADE, MISC, DialogChoice, +} from "../data/story"; + +const SPEED = 96; +const INTERACT_DIST = 24; +const NAMED = ["afu", "yuanyuan", "dabao", "mimi", "fu", "gui"]; +const OUTDOOR = ["plaza", "lake", "field", "well", "fogwall"]; + +interface Poi { name: string; x: number; y: number; clue?: string; } +interface NpcSprite { + id: string; sprite: Phaser.GameObjects.Sprite; x0: number; y0: number; + patrol?: number; dir: number; nextBlink: number; offUntil: number; blinkUntil: number; +} + +export class WorldScene extends Phaser.Scene { + mapKey: string; + protected map!: Phaser.Tilemaps.Tilemap; + protected obstacles!: Phaser.Tilemaps.TilemapLayer; + protected ground!: Phaser.Tilemaps.TilemapLayer; + protected player!: Phaser.Physics.Arcade.Sprite; + protected npcs: NpcSprite[] = []; + protected pois: Poi[] = []; + private keys!: Record; + private transitioning = false; + private colorFx!: Phaser.FX.ColorMatrix; + private vignetteFx!: Phaser.FX.Vignette; + private lastMoveAt = 0; + private rainStillAt = 0; + private nearWellSince = 0; + private nearFogSince = 0; + private dissuaded = false; + private reflection?: Phaser.GameObjects.Sprite; + private prevPos = { x: 0, y: 0 }; + private rain?: Phaser.GameObjects.Particles.ParticleEmitter; + private t14Active = false; + private t14Start = 0; + private t14Faded = false; + private celebTimer?: Phaser.Time.TimerEvent; + private buntings: Phaser.GameObjects.Image[] = []; + private interactCooldown = 0; + + constructor(key: string) { super(key); this.mapKey = key; } + + get ui(): UIScene { return this.scene.get("ui") as UIScene; } + get isWhite(): boolean { return this.mapKey.startsWith("white"); } + + create() { + director.currentScene = this.mapKey; + this.transitioning = false; + this.npcs = []; this.pois = []; this.buntings = []; + this.t14Active = false; this.dissuaded = false; + + this.map = this.make.tilemap({ key: "map_" + this.mapKey }); + const ts = this.map.addTilesetImage("tiles", "tiles")!; + this.ground = this.map.createLayer("ground", ts)!; + this.obstacles = this.map.createLayer("obstacles", ts)!; + this.obstacles.setCollisionByExclusion([-1]); + + // 玩家 + const spawn = this.findSpawn(director.pendingSpawn) ?? this.findSpawn("entry") ?? { x: 40, y: 40 }; + this.player = this.physics.add.sprite(spawn.x, spawn.y, "player", 0); + this.player.setSize(10, 8).setOffset(3, 16).setCollideWorldBounds(true); + this.player.setDepth(spawn.y); + this.physics.world.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels); + this.physics.add.collider(this.player, this.obstacles); + this.prevPos = { x: spawn.x, y: spawn.y }; + + // 相机:跟随并钳制地图边界 + const cam = this.cameras.main; + // 相机钳制地图边界;地图小于视口时对称扩展 bounds 使其居中 + const mw = this.map.widthInPixels, mh = this.map.heightInPixels; + cam.setBounds( + mw < 320 ? (mw - 320) / 2 : 0, + mh < 180 ? (mh - 180) / 2 : 0, + Math.max(mw, 320), + Math.max(mh, 180), + ); + cam.startFollow(this.player, true); + cam.setRoundPixels(true); + + // 对象层 + const objLayer = this.map.getObjectLayer("objects"); + for (const o of objLayer?.objects ?? []) { + const type = (o as any).type || (o as any).class || ""; + const props: Record = {}; + for (const p of (o.properties as any[]) ?? []) props[p.name] = p.value; + if (type === "door") { + const zone = this.add.zone(o.x! + o.width! / 2, o.y! + o.height! / 2, o.width!, o.height!); + this.physics.add.existing(zone, true); + this.physics.add.overlap(this.player, zone, () => this.goTo(props.target, props.spawn)); + } else if (type === "npc") { + this.spawnNpc(o.name!, o.x!, o.y!, props.patrol); + } else if (type === "poi") { + this.pois.push({ name: o.name!, x: o.x!, y: o.y!, clue: props.clue }); + } + } + + // 恢复日内世界状态(花坛/莓果/彩带) + this.restoreTiles(); + // Day 7:庆典布置 + if (director.day === 7 && this.mapKey === "plaza") this.setupCelebrationDay(); + // 湖面倒影 + if (this.mapKey === "lake") { + this.reflection = this.add.sprite(0, 0, "player", 0).setFlipY(true).setAlpha(0.3).setDepth(1); + } + // Day 6 傍晚脚本雨(室外,NPC 假装看不见) + if (director.day === 6 && director.dusk && OUTDOOR.includes(this.mapKey)) this.startRain(); + + this.keys = this.input.keyboard!.addKeys("W,A,S,D,UP,DOWN,LEFT,RIGHT,E,ENTER") as any; + + // 侵蚀矩阵 → 相机 postFX + this.colorFx = this.cameras.main.postFX.addColorMatrix(); + this.vignetteFx = this.cameras.main.postFX.addVignette(0.5, 0.5, 0.9, 0); + this.applyStageVisuals(); + audio.applyStage(); + + director.on("stage", this.onStage, this); + director.on("negflash", this.onNegFlash, this); + director.on("sadact", this.onSadAct, this); + director.on("forcedCelebration", this.onForcedCelebration, this); + director.on("dusk", this.onDuskWorld, this); + this.events.on("shutdown", () => { + director.off("stage", this.onStage, this); + director.off("negflash", this.onNegFlash, this); + director.off("sadact", this.onSadAct, this); + director.off("forcedCelebration", this.onForcedCelebration, this); + director.off("dusk", this.onDuskWorld, this); + if (director.celebrationActive) director.celebrationEnd(true); // 从场景边缘走离 = 逃离 + }); + + this.lastMoveAt = this.time.now; + this.nearWellSince = this.time.now; + this.nearFogSince = this.time.now; + this.rainStillAt = this.time.now; + this.ui?.forceClose(); + this.ui?.fadeIn(400); + } + + // ---------- 基础 ---------- + private findSpawn(name: string): { x: number; y: number } | null { + const objLayer = this.map.getObjectLayer("objects"); + let first: { x: number; y: number } | null = null; + for (const o of objLayer?.objects ?? []) { + const type = (o as any).type || (o as any).class || ""; + if (type !== "spawn") continue; + if (!first) first = { x: o.x!, y: o.y! }; + if (o.name === name) return { x: o.x!, y: o.y! }; + } + return name === "entry" ? first : null; + } + + private spawnNpc(id: string, x: number, y: number, patrol?: number) { + if (director.day === 7 && this.mapKey !== "plaza" && (NAMED.includes(id) || id.startsWith("bird"))) return; + let tex = "npc_" + id, frame = 0; + if (id.startsWith("bird")) tex = "bird"; + if (id.startsWith("extra")) tex = id; + const s = this.add.sprite(x, y, tex, frame).setOrigin(0.5, 1).setDepth(y); + if (id.startsWith("bird")) s.play("bird-sing"); + if (id.startsWith("extra")) s.play(id + "-walk"); + // 物理体(具名 NPC 阻挡) + if (NAMED.includes(id)) { + this.physics.add.existing(s, true); + const body = s.body as Phaser.Physics.Arcade.StaticBody; + body.setSize(12, 10); body.setOffset(2, 14); + this.physics.add.collider(this.player, s); + } + this.npcs.push({ id, sprite: s, x0: x, y0: y, patrol, dir: 1, nextBlink: this.time.now + 1000 + Math.random() * 2000, offUntil: 0, blinkUntil: 0 }); + } + + goTo(target: string, spawnName: string) { + if (this.transitioning || director.cutscene) return; + this.transitioning = true; + director.pendingSpawn = spawnName; + this.ui.fadeOut(250, () => { + this.scene.start(target); + }); + } + + private restoreTiles() { + if (this.mapKey === "plaza") { + for (const w of director.watered) { + const p = this.pois.find(q => q.name === w); + if (p) this.obstacles.putTileAt(56, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1); // 盛开 + } + for (const r of director.ribbons) this.hangRibbonVisual(r); + } + if (this.mapKey === "field") { + for (const b of director.berries) { + const p = this.pois.find(q => q.name === b); + if (p) this.obstacles.putTileAt(13, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1); // 已采 + } + } + } + + private hangRibbonVisual(poiName: string) { + const p = this.pois.find(q => q.name === poiName); + if (!p) return; + const img = this.add.image(Math.floor(p.x / 16) * 16 + 8, (Math.floor(p.y / 16) - 2) * 16 + 8, "tiles", 58).setDepth(3); + this.buntings.push(img); + } + + // ---------- 侵蚀矩阵 → 视觉(§6.4,唯一真源驱动) ---------- + private onStage() { this.applyStageVisuals(); audio.applyStage(); } + applyStageVisuals() { + const v = director.visual; + if (this.isWhite) { // 白色层:去饱和灰白色板,不参与糖果侵蚀 + this.colorFx.reset(); this.colorFx.saturate(-0.4); + this.vignetteFx.strength = 0; + return; + } + this.colorFx.reset(); + this.colorFx.saturate(v.saturation - 1); + this.vignetteFx.strength = v.vignette; + } + private onNegFlash() { + // 单帧负片(S4 溢出池 / 随机) + this.colorFx.reset(); this.colorFx.negative(); + this.time.delayedCall(50, () => this.applyStageVisuals()); + } + private onSadAct() { + // 最近的 NPC 转头看向玩家一次 + let best: NpcSprite | null = null; let bd = 1e9; + for (const n of this.npcs) { + const d = Phaser.Math.Distance.Between(n.sprite.x, n.sprite.y, this.player.x, this.player.y); + if (d < bd) { bd = d; best = n; } + } + if (!best) return; + const s = best.sprite; + s.setFlipX(this.player.x < s.x); + this.tweens.add({ targets: s, y: s.y - 1, duration: 90, yoyo: true }); + this.time.delayedCall(900, () => s.setFlipX(false)); + } + private onDuskWorld() { + if (director.day === 6 && OUTDOOR.includes(this.mapKey)) this.startRain(); + } + + private startRain() { + if (this.rain) return; + this.rain = this.add.particles(0, 0, "rainpx", { + x: { min: 0, max: this.map.widthInPixels }, + y: -8, + lifespan: 1400, + speedY: { min: 150, max: 190 }, + speedX: -12, + quantity: 2, + frequency: 30, + alpha: 0.6, + }).setDepth(200); + this.rainStillAt = this.time.now; + } + + // ---------- 强制欢乐庆典(§6.1 反制) ---------- + private onForcedCelebration() { + this.ui.banner("欢 乐 庆 典 !"); + this.ui.toast("(世界不允许你难过)"); + audio.sfxDing(); + for (const n of this.npcs) { + this.tweens.add({ + targets: n.sprite, + x: Phaser.Math.Linear(n.sprite.x, this.player.x, 0.5), + y: Phaser.Math.Linear(n.sprite.y, this.player.y, 0.5), + duration: 3000, + }); + } + this.celebTimer = this.time.delayedCall(8000, () => { + if (director.celebrationActive) { + director.celebrationEnd(false); // 未逃离:幸福拉回 50 + this.ui.toast("(你重新开心了起来)"); + for (const n of this.npcs) this.tweens.add({ targets: n.sprite, x: n.x0, y: n.y0, duration: 2000 }); + } + }); + } + + // ---------- Day 7 庆典 ---------- + private setupCelebrationDay() { + director.celebrationStarted = true; + // E4:静默——庆典无人出席(优先级最高,进场即判) + if (director.e4ok && director.escapes >= 3) { + // 清空所有 NPC,彩带在没有风的空气里飘 + for (const n of this.npcs) n.sprite.destroy(); + this.npcs = []; + for (let i = 0; i < 6; i++) { + const img = this.add.image(200 + i * 16, 168, "tiles", 58).setDepth(3); + this.tweens.add({ targets: img, y: 166, duration: 1800 + i * 130, yoyo: true, repeat: -1 }); + } + this.time.delayedCall(2600, () => this.startEnding("E4")); + return; + } + // 全村到场 + const places: [string, number, number][] = [ + ["yuanyuan", 214, 202], ["mimi", 266, 202], ["fu", 202, 224], ["gui", 278, 224], + ]; + for (const [id, x, y] of places) this.spawnNpc(id, x, y); + // 彩带装饰 + for (let i = 0; i < 6; i++) this.add.image(200 + i * 16, 168, "tiles", 58).setDepth(3); + } + + startEnding(code: "E1" | "E2" | "E3" | "E4") { + if (director.endingPlayed) return; + director.endingPlayed = true; + this.registry.set("ending", code); + this.scene.stop("ui"); + this.scene.start("ending"); + } + + // ---------- 更新循环 ---------- + update(time: number, _dt: number) { + if (!this.player) return; + const ui = this.ui; + const blocked = ui.dialogOpen || director.cutscene; + + // 移动 + let vx = 0, vy = 0; + if (!blocked && !this.t14Active) { + if (this.keys.A.isDown || this.keys.LEFT.isDown) vx = -1; + else if (this.keys.D.isDown || this.keys.RIGHT.isDown) vx = 1; + if (this.keys.W.isDown || this.keys.UP.isDown) vy = -1; + else if (this.keys.S.isDown || this.keys.DOWN.isDown) vy = 1; + } + const moving = vx !== 0 || vy !== 0; + const v = new Phaser.Math.Vector2(vx, vy).normalize().scale(SPEED); + this.player.setVelocity(v.x, v.y); + if (moving) { + this.lastMoveAt = time; + this.rainStillAt = time; + if (vy < 0) this.player.play("walk-up", true); + else if (vy > 0) this.player.play("walk-down", true); + else if (vx < 0) this.player.play("walk-left", true); + else this.player.play("walk-right", true); + } else { + // 待机:当前朝向第 1 帧 + const anim = this.player.anims.currentAnim; + this.player.anims.stop(); + if (anim) this.player.setFrame(anim.frames[0].frame.name); + } + this.player.setDepth(this.player.y); + + // T14:保持微笑 60 秒 + if (this.t14Active) { + if (moving || Phaser.Input.Keyboard.JustDown(this.keys.A) || Phaser.Input.Keyboard.JustDown(this.keys.W)) { + this.cancelT14(); + } else { + const el = (time - this.t14Start) / 1000; + if (el >= 40 && !this.t14Faded) { this.t14Faded = true; audio.fadeToSilence(19); } // 40–60 秒渐隐至零 + if (el >= 60) this.finishT14(); + } + } + + // 发呆(悲伤行为③):连续 10 秒不移动且不在任务点 + if (!blocked && !this.t14Active && !director.celebrationActive && time - this.lastMoveAt > 10_000) { + this.lastMoveAt = time; + if (!this.nearAnyPoi()) director.sadAct("idle"); + } + // 雨中站立 ≥10 秒(悲伤行为⑤) + if (this.rain && !blocked && time - this.rainStillAt > 10_000) { + this.rainStillAt = time; + director.sadAct("rain"); + this.ui.toast("(雨很冷。没有人看见雨。)"); + } + + // 注视古井/雾墙 5 秒 → 觉知 +3(各每日 1 次) + this.stareCheck(time, "well"); + this.stareCheck(time, "fog"); + + // 湖面倒影(Day2 起延迟 1 帧) + if (this.reflection) { + const src = director.day >= 2 ? this.prevPos : { x: this.player.x, y: this.player.y }; + const shoreY = 9.5 * 16; + this.reflection.setPosition(src.x, shoreY + (shoreY - src.y) + 20); + this.reflection.setVisible(this.player.y > 80 && this.player.y < 152); + this.reflection.setFrame(this.player.frame.name); + this.prevPos = { x: this.player.x, y: this.player.y }; + } + + this.updateNpcs(time); + + // 交互提示与触发 + if (!blocked && !this.t14Active) { + const target = this.nearestInteract(); + if (target) { + ui.prompt(`按 E ${target.kind === "npc" ? "对话" : "查看"}`); + const pressed = Phaser.Input.Keyboard.JustDown(this.keys.E) || Phaser.Input.Keyboard.JustDown(this.keys.ENTER); + if (pressed && time > this.interactCooldown) this.interact(target); + } else { + ui.prompt(null); + } + // D5 前古井/雾墙劝离 + this.dissuadeCheck(); + } else { + // 消费残留按键:关闭对话的那一次 E 不应立刻再次触发交互 + Phaser.Input.Keyboard.JustDown(this.keys.E); + Phaser.Input.Keyboard.JustDown(this.keys.ENTER); + this.interactCooldown = time + 250; + ui.prompt(null); + } + } + + private updateNpcs(time: number) { + const v = director.visual; + const s4 = director.stage >= 4; + for (const n of this.npcs) { + const base = n.id.startsWith("bird") ? "bird" : n.id; + // 群演巡逻;S4 时 1 号群演停摆面朝玩家 + if (n.patrol) { + const frozen = s4 && n.id === "extra1"; + if (frozen || this.t14Active) { + n.sprite.anims.pause(); + n.sprite.setFlipX(this.player.x < n.sprite.x); + } else { + if (!n.sprite.anims.isPlaying) n.sprite.anims.resume(); + n.sprite.x += n.dir * 0.25; + if (n.sprite.x > n.x0 + n.patrol) n.dir = -1; + if (n.sprite.x < n.x0 - n.patrol) n.dir = 1; + n.sprite.setFlipX(n.dir < 0); + } + continue; + } + if (n.id.startsWith("bird")) { + // 音乐变调时动画同步卡帧;S4 鸟鸣消失但仍在张嘴 + const stg = director.stage; + const rate = [1, 1, 0.97, 0.92, 0.7][stg]; + n.sprite.anims.timeScale = rate; + continue; + } + if (!NAMED.includes(n.id)) continue; + // 具名 NPC 表情档 + 眨眼 + off 闪现 + const expr = director.npcExpr(n.id); + let frame = expr === "flat" ? 1 : 0; + const npcStage = director.npcStage(n.id); + if (n.offUntil > time) frame = 2; // off 档闪现 0.1 秒 + else if (n.blinkUntil > time) frame = 3; // 眨眼 + else if (time > n.nextBlink) { + n.blinkUntil = time + 120; + n.nextBlink = time + v.blinkInterval + Math.random() * 800; + } + // S4:off 档闪现(0.6 次/分钟 ⇒ 每帧概率) + if (npcStage >= 4 && NPC_OFFSET[base] !== null && n.offUntil < time && Math.random() < 0.6 / (60 * 60)) { + n.offUntil = time + 100; + } + n.sprite.setFrame(frame); + // S3+:静止 NPC 面朝玩家 + if (npcStage >= 3) n.sprite.setFlipX(this.player.x < n.sprite.x); + } + // S4 随机单帧负片:0.4 次/分钟 + if (s4 && !this.isWhite && Math.random() < 0.4 / (60 * 60)) this.onNegFlash(); + } + + private stareCheck(time: number, kind: "well" | "fog") { + const p = this.pois.find(q => q.name === kind); + if (!p || director.day < 5) return; + const near = Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < 30; + if (kind === "well") { + if (!near) { this.nearWellSince = time; return; } + if (time - this.nearWellSince > 5000 && !director.stared.has("well")) { + director.stare("well"); + this.ui.toast(MISC.wellStare); + } + } else { + if (!near) { this.nearFogSince = time; return; } + if (time - this.nearFogSince > 5000 && !director.stared.has("fog")) { + director.stare("fog"); + this.ui.toast(MISC.fogStare); + } + } + } + + private dissuadeCheck() { + if (director.day >= 5 || this.dissuaded) return; + const p = this.pois.find(q => q.name === "well" || q.name === "fog"); + if (!p) return; + if (Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < 28) { + this.dissuaded = true; + const lines = p.name === "well" ? DISSUADE.well : DISSUADE.fog; + this.ui.say("villager", lines, { + onDone: () => { + // 温柔劝离:把玩家推回入口方向 + const spawn = this.findSpawn("entry")!; + const dir = new Phaser.Math.Vector2(spawn.x - this.player.x, spawn.y - this.player.y).normalize().scale(40); + this.player.setPosition(this.player.x + dir.x, this.player.y + dir.y); + this.time.delayedCall(3000, () => (this.dissuaded = false)); + }, + }); + } + } + + private nearAnyPoi(): boolean { + for (const p of this.pois) { + if (Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < INTERACT_DIST + 8) return true; + } + return false; + } + + private nearestInteract(): { kind: "npc" | "poi"; id: string; poi?: Poi } | null { + let best: { kind: "npc" | "poi"; id: string; poi?: Poi; d: number } | null = null; + for (const n of this.npcs) { + if (n.id.startsWith("extra") || n.id.startsWith("bird")) { + // 群演可摸头;双子鸟经 poi 交互 + if (n.id.startsWith("bird")) continue; + } + const d = Phaser.Math.Distance.Between(n.sprite.x, n.sprite.y - 10, this.player.x, this.player.y); + if (d < INTERACT_DIST && (!best || d < best.d)) best = { kind: "npc", id: n.id, d }; + } + for (const p of this.pois) { + const d = Phaser.Math.Distance.Between(p.x, p.y, this.player.x, this.player.y); + if (d < INTERACT_DIST && (!best || d < best.d)) best = { kind: "poi", id: p.name, poi: p, d }; + } + return best; + } + + // ---------- 交互分发 ---------- + private interact(t: { kind: "npc" | "poi"; id: string; poi?: Poi }) { + if (t.kind === "npc") { + if (t.id.startsWith("extra")) return this.petCritter(); + return this.talkTo(t.id); + } + this.usePoi(t.id); + } + + private petCritter() { + if (director.smallAct("pet")) this.ui.say("me", ["(你摸了摸它的头。它的循环停顿了半拍,又继续了。)"], { speakerName: "" }); + else this.ui.say("me", ["(它心满意足地走开了。)"], { speakerName: "" }); + } + + // ---- NPC 对话(含任务链) ---- + private talkTo(id: string) { + const d = director; + d.greeted.add(id); + d.talkCounts[id] = (d.talkCounts[id] ?? 0) + 1; + // 纠缠(悲伤行为④):同一 NPC 一天 ≥5 次 + if (d.talkCounts[id] >= 5 && !d.nagged.has(id)) { + d.nagged.add(id); + d.sadAct("nag"); + } + const greetQuest = d.day === 4 ? "T10" : "T3"; + const checkGreet = () => { + if (d.isOpen(greetQuest) && d.greeted.size >= 3) d.completeQuest(greetQuest); + }; + + const breadQuest = d.day === 4 ? "T8" : "T1"; + // T1/T8:取面包 + if (d.isOpen(breadQuest) && id === "yuanyuan" && d.breadHeld === 0) { + d.breadHeld = 3; + this.ui.say("yuanyuan", QUEST_LINES.t1Give, { onDone: checkGreet }); + return; + } + // T1/T8:送面包 + if (d.isOpen(breadQuest) && d.breadHeld > 0 && ["dabao", "mimi", "gui"].includes(id) && !d.breadDelivered.has(id)) { + d.breadDelivered.add(id); + d.breadHeld--; + this.ui.say(id, [QUEST_LINES.t1Deliver[id]], { + onDone: () => { + if (d.breadDelivered.size >= 3) d.completeQuest(breadQuest); + checkGreet(); + }, + }); + return; + } + // T4:取信 + if (d.day === 2 && d.isOpen("T4") && id === "dabao" && d.lettersHeld === 0 && d.lettersDelivered.size === 0) { + d.lettersHeld = 3; + this.ui.say("dabao", QUEST_LINES.t4Give, { onDone: checkGreet }); + return; + } + // T4:送信(第 3 封署名乱码 0.2 秒 → C4) + if (d.isOpen("T4") && d.lettersHeld > 0 && ["yuanyuan", "mimi", "fu"].includes(id) && !d.lettersDelivered.has(id)) { + d.lettersDelivered.add(id); + d.lettersHeld--; + const third = d.lettersDelivered.size >= 3; + this.ui.say(id, [QUEST_LINES.t4Deliver[id]], { + onDone: () => { + if (third) { + this.ui.glitchText("署名:PT-0▓", 200); + this.time.delayedCall(240, () => { + this.ui.say("me", CLUE_TEXT.C4, { + speakerName: "", clean: true, + onDone: () => { director.foundClue("C4"); d.completeQuest("T4"); }, + }); + }); + } + checkGreet(); + }, + }); + return; + } + // T5:咪咪请求清点 + if (d.day === 2 && d.isOpen("T5") && id === "mimi" && d.shelves.size === 0) { + this.ui.say("mimi", QUEST_LINES.t5Ask, { onDone: checkGreet }); + return; + } + // T11:帮圆圆揉面(可拒绝,拒绝被记录) + if (d.day === 5 && d.isOpen("T11") && id === "yuanyuan" && !d.t11Accepted) { + this.ui.say("yuanyuan", [QUEST_LINES.t11Ask], { + choices: [ + { label: "好呀,我来帮忙。", cb: () => { d.t11Accepted = true; this.ui.toast("(去揉面台吧)"); } }, + { label: "今天不想帮。", cb: () => { d.refuseQuest("T11"); this.ui.say("yuanyuan", [QUEST_LINES.t11Refused]); } }, + ], + onDone: checkGreet, + }); + return; + } + // 龟爷爷 C9 对话链(D2/D4/D6 三段递进) + if (id === "gui" && [2, 4, 6].includes(d.day)) { + const node = NPC_TALK.gui[d.day]; + this.ui.say("gui", node.lines, { + onDone: () => { + d.guiChain.add(d.day); + if (d.day === 6 && !d.clues.has("C9")) { + this.ui.say("me", CLUE_TEXT.C9, { speakerName: "", clean: true, onDone: () => director.foundClue("C9") }); + } + checkGreet(); + }, + }); + return; + } + // 默认闲聊 + const node = NPC_TALK[id]?.[d.day]; + if (!node) { this.ui.say(id, ["……"]); return; } + const opts: UiChoice[] | undefined = node.choices?.map((c: DialogChoice) => ({ + label: c.label, + cb: () => { + if (c.kind === "gentle") d.addHappiness(2); + else if (c.kind === "hesitant") d.addAwareness(2); + else d.sadAct("negative"); // 否定选项 = 悲伤行为② + this.ui.say(id, c.reply, { onDone: checkGreet }); + }, + })); + this.ui.say(id, node.lines, { choices: opts, onDone: checkGreet }); + } + + // ---- POI 交互 ---- + private usePoi(name: string) { + const d = director; + const say = (sp: string, lines: string[], opts: any = {}) => this.ui.say(sp, lines, opts); + const narr = (lines: string[], onDone?: () => void, clean = true) => + say("me", lines, { speakerName: "", clean, onDone }); + + switch (true) { + // 公告板:公告 + T12 擦拭 + case name === "board": { + if (!d.announced) { + d.announced = true; + const tasks = d.questsOfDay().map(q => `・${q.title}`); + say("afu", [ANNOUNCEMENTS[d.day], ...(tasks.length ? ["今日的幸福任务:", ...tasks] : [])], { clean: true }); + return; + } + if (d.day === 5 && d.isOpen("T12")) { + if (!d.t12Accepted) { + this.ui.say("me", [QUEST_LINES.t12Ask], { + speakerName: "", clean: true, + choices: [ + { label: "擦拭公告板。", cb: () => { d.t12Accepted = true; this.wipeBoard(); } }, + { label: "拒绝。", cb: () => { d.refuseQuest("T12"); narr([QUEST_LINES.t12Refused]); } }, + ], + }); + return; + } + this.wipeBoard(); + return; + } + const open = d.questsOfDay().some(q => d.questStatus[q.id] === "open"); + if (!open && d.day < 7) narr([MISC.boardDone]); + else say("afu", [ANNOUNCEMENTS[d.day]], { clean: true }); + return; + } + // 床 + case name === "bed": { + if (d.day >= 7) { narr(["(今天不用睡。庆典在等你。)"]); return; } + if (!d.dusk) { narr([MISC.bedEarly]); return; } + narr([MISC.bedSleep], () => { + director.cutscene = true; + this.ui.fadeOut(900, () => { director.cutscene = false; this.scene.start("dream"); }); + }); + return; + } + // 日历(C1,D2 起) + case name === "calendar": { + if (d.day >= 2) narr(CLUE_TEXT.C1, () => d.foundClue("C1")); + else narr(["(床头的日历。明天也会是晴天。)"]); + return; + } + // 配方纸(C2,D3 起) + case name === "recipe": { + if (d.day >= 3) narr(CLUE_TEXT.C2, () => d.foundClue("C2")); + else narr(["(柜台上的配方纸。字迹工整。)"]); + return; + } + // 散落信件(C3,D2 起) + case name === "letters": { + if (d.day >= 2) narr(CLUE_TEXT.C3, () => d.foundClue("C3")); + else narr(["(邮筒旁散落着几封信。)"]); + return; + } + // 货架清点(T5)与 C5 + case name.startsWith("shelfA") || name.startsWith("shelfB") || name.startsWith("shelfC"): { + if (d.day === 2 && d.isOpen("T5")) { + if (!d.shelves.has(name)) { + d.shelves.add(name); + narr([`(${d.shelves.size}/3……数字对得上。)`], () => { + if (d.shelves.size >= 3) { + say("mimi", QUEST_LINES.t5Done, { onDone: () => d.completeQuest("T5") }); + } + }, true); + return; + } + } + narr(["(货架排得整整齐齐。)"]); + return; + } + case name === "shelf_c5": { + if (d.day >= 2) narr(CLUE_TEXT.C5, () => d.foundClue("C5")); + else narr(["(货架上的商品排得很满。)"]); + return; + } + // 诊所表格(C8,D5 起) + case name === "chart": { + if (d.day >= 5) narr(CLUE_TEXT.C8, () => d.foundClue("C8")); + else narr(["(桌上放着一份表格,字很小。)"]); + return; + } + // 花坛浇水(T2/T9 + 小互动) + case name.startsWith("flowerbed"): { + const wq = d.day === 4 ? "T9" : "T2"; + if (d.isOpen(wq) && !d.watered.has(name)) { + d.watered.add(name); + const p = this.pois.find(q => q.name === name)!; + this.obstacles.putTileAt(56, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1); + narr([`(浇水 ${d.watered.size}/3。花立刻精神了起来。)`], () => { + if (d.watered.size >= 3) d.completeQuest(wq); + }); + return; + } + if (d.smallAct("water")) narr(["(又浇了点水。)"]); + else narr(["(土已经很湿了。)"]); + return; + } + case name === "fieldwater": { + if (d.smallAct("water")) narr(["(你给花田浇了水。)"]); + else narr(["(今天浇得够多了。)"]); + return; + } + // 采莓果(T6) + case name.startsWith("berry"): { + if (d.day === 3 && d.isOpen("T6") && !d.berries.has(name)) { + d.berries.add(name); + const p = this.pois.find(q => q.name === name)!; + this.obstacles.putTileAt(13, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1); + narr([`(采到莓果 ${d.berries.size}/5。)`], () => { + if (d.berries.size >= 5) d.completeQuest("T6"); + }); + return; + } + narr(["(莓果丛。果子还没长出来。)"]); + return; + } + // 野餐(T7) + case name === "picnic": { + if (d.day === 3 && d.isOpen("T7")) { + director.cutscene = true; + this.cameras.main.pan(this.player.x, this.map.heightInPixels - 60, 1200, "Sine.easeInOut"); + narr(MISC.picnic, () => { + this.cameras.main.pan(this.player.x, this.player.y, 800, "Sine.easeInOut"); + director.cutscene = false; + d.completeQuest("T7"); + }, true); + return; + } + narr(["(野餐布。有阳光晒过的味道。)"], undefined, false); + return; + } + // 揉面(T11) + case name === "knead": { + if (d.day === 5 && d.isOpen("T11") && d.t11Accepted) { + say("yuanyuan", QUEST_LINES.t11Work, { onDone: () => d.completeQuest("T11") }); + return; + } + narr(["(揉面台。面粉是甜的。)"]); + return; + } + // 挂彩带(T13) + case name.startsWith("ribbon"): { + if (d.day === 6 && d.isOpen("T13") && !d.ribbons.has(name)) { + d.ribbons.add(name); + this.hangRibbonVisual(name); + narr([`(挂好彩带 ${d.ribbons.size}/4。)`], () => { + if (d.ribbons.size >= 4) d.completeQuest("T13"); + }); + return; + } + narr(["(灯笼杆。漆是新的——每年都是新的。)"]); + return; + } + // 庆典场地(T14 / T15) + case name === "festival": { + if (d.day === 6 && d.isOpen("T14")) { this.beginT14(); return; } + if (d.day === 7) { this.festivalDay7(); return; } + narr(["(庆典的场地,打扫得很干净。)"]); + return; + } + // 古井(C7,D5 起可交互) + case name === "well": { + if (d.day < 5) return; // 劝离逻辑在 dissuadeCheck + director.cutscene = true; + this.time.delayedCall(600, () => audio.sfxEcg()); + this.time.delayedCall(1400, () => audio.sfxEcg()); + this.time.delayedCall(2200, () => audio.sfxEcg()); + narr(CLUE_TEXT.C7, () => { director.cutscene = false; d.foundClue("C7"); }); + return; + } + // 雾墙(E3 入口) + case name === "fog": { + if (d.day < 5) return; + if (d.day === 7 && d.celebrationStarted && d.e3Ready()) { this.beginE3(); return; } + narr(["(雾很冷,纹丝不动。)", "(雾的后面,好像有一条走廊。)"]); + return; + } + // 白色层病历(E3 终点) + case name === "chart_ward": { + if (d.e3Mode) { this.startEnding("E3"); return; } + narr(["(一张病历。字被白光糊住了。)"]); + return; + } + // 双子鸟树(喂食小互动) + case name === "birdtree": { + if (director.stage >= 4) { narr(["(它们在张嘴。没有声音。)"]); return; } + if (d.smallAct("feed")) narr(["(你撒了点面包屑。歌声更亮了。)"]); + else narr(["(双子鸟歪头看你。歌没有停。)"]); + return; + } + case name === "bench": { narr(["(长椅被晒得暖暖的。)"]); return; } + case name === "sign_house": { narr([`(写着:${d.playerName}的小屋。)`]); return; } + default: return; + } + } + + private wipeBoard() { + const d = director; + d.wipes++; + if (d.wipes === 1) { + this.ui.say("me", [QUEST_LINES.wipe1], { speakerName: "", clean: true }); + } else if (d.wipes === 2) { + this.ui.say("me", [QUEST_LINES.wipe2, ...CLUE_TEXT.C6], { + speakerName: "", clean: true, + onDone: () => d.foundClue("C6"), + }); + } else { + this.ui.say("me", [QUEST_LINES.wipe3], { + speakerName: "", clean: true, + onDone: () => d.completeQuest("T12"), + }); + } + } + + // ---- T14:保持微笑 60 秒 ---- + private beginT14() { + this.ui.say("me", [MISC.t14Start], { + speakerName: "", clean: true, + onDone: () => { + this.t14Active = true; + this.t14Faded = false; + this.t14Start = this.time.now; + this.cameras.main.zoomTo(1.9, 60_000, "Linear"); // 镜头缓慢拉近 + this.ui.toast("(不要动)"); + }, + }); + } + private cancelT14() { + this.t14Active = false; + this.cameras.main.zoomTo(1, 600); + audio.restoreVolume(); + this.ui.say("me", [MISC.t14Cancel], { speakerName: "", clean: true }); + } + private finishT14() { + this.t14Active = false; + director.t14done = true; + this.cameras.main.zoomTo(1, 900); + audio.restoreVolume(2); + this.ui.say("me", [MISC.t14Done], { + speakerName: "", clean: true, + onDone: () => director.completeQuest("T14"), + }); + } + + // ---- Day 7 庆典交互(T15 → 结局分歧) ---- + private festivalDay7() { + const d = director; + const lines = ["第 413 届丰收庆典,现在开始。", MISC.festivalGather]; + if (d.e3Ready()) lines.push(MISC.festivalHintFog); + this.ui.say("afu", lines, { + choices: [ + { + label: "加入合影。", + cb: () => { + d.completeQuest("T15"); + const e = d.decideEnding(false); + this.time.delayedCall(600, () => this.startEnding(e as any)); + }, + }, + { label: "再看看。", cb: () => { } }, + ], + }); + } + + // ---- E3:雾墙裂开 → 白色走廊 ---- + private beginE3() { + const d = director; + d.e3Mode = true; + d.cutscene = true; + audio.stopBgm(); + this.ui.say("me", ["(雾,裂开了。)", "(里面是一条白色的走廊。很安静。)"], { + speakerName: "", clean: true, + onDone: () => { + this.cameras.main.flash(1200, 255, 255, 255); + this.time.delayedCall(1100, () => { + d.cutscene = false; + d.pendingSpawn = "entry"; + this.scene.start("white_corridor"); + }); + }, + }); + } +} + +// ---- 11 张地图各一个 Scene ---- +export class HouseScene extends WorldScene { constructor() { super("house"); } } +export class PlazaScene extends WorldScene { constructor() { super("plaza"); } } +export class BakeryScene extends WorldScene { constructor() { super("bakery"); } } +export class StoreScene extends WorldScene { constructor() { super("store"); } } +export class ClinicScene extends WorldScene { constructor() { super("clinic"); } } +export class LakeScene extends WorldScene { constructor() { super("lake"); } } +export class FieldScene extends WorldScene { constructor() { super("field"); } } +export class WellScene extends WorldScene { constructor() { super("well"); } } +export class FogwallScene extends WorldScene { constructor() { super("fogwall"); } } +export class WhiteCorridorScene extends WorldScene { constructor() { super("white_corridor"); } } +export class WhiteWardScene extends WorldScene { constructor() { super("white_ward"); } } diff --git a/Fable-5-High/tools/check_dist.mjs b/Fable-5-High/tools/check_dist.mjs new file mode 100644 index 0000000..c846135 --- /dev/null +++ b/Fable-5-High/tools/check_dist.mjs @@ -0,0 +1,21 @@ +import { chromium } from "playwright"; +import { preview } from "vite"; +const server = await preview({ preview: { port: 5194 } }); +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +const errors = []; +page.on("console", m => { if (m.type() === "error") errors.push(m.text()); }); +page.on("pageerror", e => errors.push(e.message)); +await page.goto("http://localhost:5194/"); +await page.waitForTimeout(2500); +await page.fill("#name-input", ""); +await page.press("#name-input", "Enter"); +await page.waitForTimeout(2000); +const name = await page.evaluate(() => window.__director.playerName); +const scene = await page.evaluate(() => window.__director.currentScene); +await page.screenshot({ path: process.env.SHOT_DIR + "/dist_house.png" }); +console.log("name(默认):", name, "| scene:", scene, "| ERRORS:", errors.length); +errors.forEach(e => console.log(" -", e)); +await browser.close(); +server.httpServer.close(); +process.exit(errors.length || name !== "小满" || scene !== "house" ? 1 : 0); diff --git a/Fable-5-High/tools/e2e.mjs b/Fable-5-High/tools/e2e.mjs new file mode 100644 index 0000000..041fcd4 --- /dev/null +++ b/Fable-5-High/tools/e2e.mjs @@ -0,0 +1,74 @@ +// 无头验收脚本:控制台零报错 + 标题→Day1 流程 + Debug 面板侵蚀矩阵 + 四结局 +// 运行:node tools/e2e.mjs [--full] +import { chromium } from "playwright"; +import { createServer } from "vite"; +import { mkdirSync } from "fs"; + +const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-shots"; +mkdirSync(SHOT_DIR, { recursive: true }); + +const server = await createServer({ server: { port: 5199 } }); +await server.listen(); + +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +const errors = []; +page.on("console", (msg) => { if (msg.type() === "error") errors.push(msg.text()); }); +page.on("pageerror", (err) => errors.push("PAGEERROR: " + err.message)); + +const shot = (name) => page.screenshot({ path: `${SHOT_DIR}/${name}.png` }); +const sleep = (ms) => page.waitForTimeout(ms); + +await page.goto("http://localhost:5199/"); +await sleep(2500); +await shot("01_title"); + +// 输入名字并开始 +await page.fill("#name-input", "测试员"); +await page.press("#name-input", "Enter"); +await sleep(2000); +await shot("02_day1_house"); + +// 走出小屋:向下走 +await page.keyboard.down("s"); await sleep(1400); await page.keyboard.up("s"); +await sleep(1200); +await shot("03_plaza"); + +// Debug 面板:侵蚀矩阵五档 +await page.keyboard.press("`"); +await sleep(300); +await shot("04_debug_open"); +for (const h of [10, 40, 70, 90, 100]) { + await page.fill("#dbg-h", String(h)); + await page.click("#dbg-h-set"); + await sleep(700); + await shot(`05_stage_h${h}`); +} +// 传送遍历全部场景 +for (const s of ["bakery", "store", "clinic", "lake", "field", "well", "fogwall", "white_corridor", "white_ward", "plaza", "house"]) { + await page.click(`[data-tp="${s}"]`); + await sleep(900); + await shot(`06_scene_${s}`); +} +// 四结局强制触发 +for (const e of ["E1", "E4", "E2", "E3"]) { + await page.click(`[data-end="${e}"]`); + await sleep(e === "E3" ? 14000 : 6000); + await shot(`07_ending_${e}`); + // 回到游戏继续测试(reload 后需重进) + if (e !== "E3") { + await page.reload(); + await sleep(2200); + await page.fill("#name-input", "测试员"); + await page.press("#name-input", "Enter"); + await sleep(1800); + await page.keyboard.press("`"); + await sleep(300); + } +} + +console.log("CONSOLE ERRORS:", errors.length); +errors.slice(0, 20).forEach(e => console.log(" -", e)); +await browser.close(); +await server.close(); +process.exit(errors.length ? 1 : 0); diff --git a/Fable-5-High/tools/e2e_doors.mjs b/Fable-5-High/tools/e2e_doors.mjs new file mode 100644 index 0000000..e836384 --- /dev/null +++ b/Fable-5-High/tools/e2e_doors.mjs @@ -0,0 +1,62 @@ +// 门往返专项:每条通道双向走一遍,确认到达后 1.2 秒内不被弹回(防出生点压住触发区) +import { chromium } from "playwright"; +import { createServer } from "vite"; +const server = await createServer({ server: { port: 5193 } }); +await server.listen(); +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +const errors = []; +page.on("console", m => { if (m.type() === "error") errors.push(m.text()); }); +page.on("pageerror", e => errors.push(e.message)); +const sleep = ms => page.waitForTimeout(ms); +const tp = (x, y) => page.evaluate(([x, y]) => { + const sc = window.__game.scene.getScenes(true).find(s => s.player); + sc.player.setPosition(x, y); sc.player.body.reset(x, y); +}, [x, y]); +const cur = () => page.evaluate(() => window.__director.currentScene); +const failures = []; +// 沿指定方向持续走 ms 毫秒(模拟真实按键走进出口) +const walk = async (key, ms) => { await page.keyboard.down(key); await sleep(ms); await page.keyboard.up(key); }; + +await page.goto("http://localhost:5193/"); +await sleep(2500); +await page.fill("#name-input", "门测"); +await page.press("#name-input", "Enter"); +await sleep(1800); + +// [出发场景, 门口站位, 行走方向, 目标场景] +const LEGS = [ + ["house", [88, 110], "s", "plaza"], // 小屋 → 广场 + ["plaza", [72, 90], "w", "house"], // 广场 → 小屋 + ["house", [88, 110], "s", "plaza"], + ["plaza", [152, 90], "w", "bakery"], // 广场 → 面包房 + ["bakery", [128, 140], "s", "plaza"], + ["plaza", [344, 90], "w", "store"], // 广场 → 杂货店 + ["store", [128, 140], "s", "plaza"], + ["plaza", [424, 90], "w", "clinic"], // 广场 → 诊所 + ["clinic", [96, 110], "s", "plaza"], + ["plaza", [20, 148], "a", "lake"], // 广场 → 湖畔 + ["lake", [360, 120], "d", "plaza"], // 湖畔 → 广场 + ["plaza", [462, 148], "d", "field"], // 广场 → 花田 + ["field", [24, 104], "a", "plaza"], // 花田 → 广场 + ["plaza", [232, 24], "w", "well"], // 广场 → 古井 + ["well", [112, 136], "s", "plaza"], // 古井 → 广场 + ["plaza", [232, 296], "s", "fogwall"], // 广场 → 雾墙 + ["fogwall", [168, 30], "w", "plaza"], // 雾墙 → 广场(用户报告的死循环路径) +]; +for (const [from, [x, y], dir, to] of LEGS) { + const at = await cur(); + if (at !== from) { console.log(`FAIL 前置不在 ${from}(在 ${at})`); failures.push(from + "->" + to); continue; } + await tp(x, y); await sleep(250); + await walk(dir, 900); + let arrived = false; + for (let i = 0; i < 20; i++) { await sleep(150); if ((await cur()) === to) { arrived = true; break; } } + await sleep(1200); // 到达后必须站得住 + const settled = (await cur()) === to; + console.log(`${arrived && settled ? "PASS" : "FAIL"} ${from} → ${to}${arrived && !settled ? "(被弹回!)" : ""}`); + if (!(arrived && settled)) failures.push(from + "->" + to); +} +console.log("CONSOLE ERRORS:", errors.length); +console.log("FAILURES:", failures.length, failures.join(" | ")); +await browser.close(); await server.close(); +process.exit(errors.length || failures.length ? 1 : 0); diff --git a/Fable-5-High/tools/e2e_flow.mjs b/Fable-5-High/tools/e2e_flow.mjs new file mode 100644 index 0000000..55b5095 --- /dev/null +++ b/Fable-5-High/tools/e2e_flow.mjs @@ -0,0 +1,291 @@ +// 七日全流程自动化验收:任务/线索/悲伤行为/黄昏/梦境/结局 + 控制台零报错 +// 运行:node tools/e2e_flow.mjs +import { chromium } from "playwright"; +import { createServer } from "vite"; +import { mkdirSync } from "fs"; + +const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-flow"; +mkdirSync(SHOT_DIR, { recursive: true }); + +const server = await createServer({ server: { port: 5198 } }); +await server.listen(); +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +const errors = []; +page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); }); +page.on("pageerror", (e) => errors.push("PAGEERROR: " + e.message)); + +const sleep = (ms) => page.waitForTimeout(ms); +// 无头环境下 press() 的 down/up 同帧到达会吞掉 JustDown,改为跨帧按键 +const pressE = async () => { await page.keyboard.down("e"); await sleep(90); await page.keyboard.up("e"); }; +const shot = (n) => page.screenshot({ path: `${SHOT_DIR}/${n}.png` }); +const D = (expr) => page.evaluate(`(() => { const d = window.__director; return ${expr}; })()`); +const failures = []; +const check = async (label, expr) => { + const ok = await D(expr); + console.log(`${ok ? "PASS" : "FAIL"} ${label}`); + if (!ok) { + failures.push(label); + const diag = await page.evaluate(() => { + const d = window.__director; + const sc = window.__game.scene.getScenes(true).find((s) => s.player); + return `day=${d.day} scene=${d.currentScene} wipes=${d.wipes} clues=[${[...d.clues]}] cutscene=${d.cutscene} dialog=${window.__game.scene.getScene("ui").dialogOpen} player=${sc ? Math.round(sc.player.x) + "," + Math.round(sc.player.y) : "-"} trans=${sc ? sc.transitioning : "-"}`; + }); + console.log(" diag:", diag); + } +}; + +// 把玩家瞬移到当前场景某像素点 +const tp = (x, y) => page.evaluate(([x, y]) => { + const g = window.__game; + const sc = g.scene.getScenes(true).find((s) => s.player); + sc.player.setPosition(x, y); + sc.player.body.reset(x, y); +}, [x, y]); +const curScene = () => page.evaluate(() => window.__director.currentScene); +// 连按 E 直到对话结束 +const talk = async (times = 14) => { + for (let i = 0; i < times; i++) { + await pressE(); + await sleep(160); + const open = await page.evaluate(() => window.__game.scene.getScene("ui").dialogOpen); + if (!open) break; + } +}; +const interactAt = async (x, y, presses = 14) => { + await tp(x, y); await sleep(350); + await pressE(); await sleep(300); + await talk(presses); + await sleep(200); +}; +// 走进门(瞬移到门内并等场景切换) +const enterDoor = async (x, y, expectScene) => { + await tp(x, y); + for (let i = 0; i < 30; i++) { await sleep(200); if ((await curScene()) === expectScene) break; } + const ok = (await curScene()) === expectScene; + console.log(`${ok ? "PASS" : "FAIL"} 进入 ${expectScene}`); + if (!ok) { + failures.push("进入 " + expectScene); + const diag = await page.evaluate(() => { + const d = window.__director; + const sc = window.__game.scene.getScenes(true).find((s) => s.player); + return `scene=${d.currentScene} cutscene=${d.cutscene} dialog=${window.__game.scene.getScene("ui").dialogOpen} player=${sc ? Math.round(sc.player.x) + "," + Math.round(sc.player.y) : "-"} trans=${sc ? sc.transitioning : "-"}`; + }); + console.log(" diag:", diag); + } + await sleep(400); +}; +const sleepToNextDay = async () => { + await enterDoor(72, 68, "house"); // 回小屋(从广场小屋门) + await interactAt(30, 70, 6); // 床 + await sleep(14500); // 梦境过场 + await sleep(1200); +}; + +await page.goto("http://localhost:5198/"); +await sleep(2500); +await page.fill("#name-input", "验收员"); +await page.press("#name-input", "Enter"); +await sleep(1800); + +// ===== Day 1 ===== +await check("D1 开局幸福 30", "d.day===1 && d.happiness===30 && d.awareness===0"); +await interactAt(30, 40, 4); // 日历(D1 无线索) +await check("D1 日历尚无 C1", "!d.clues.has('C1')"); +await enterDoor(88, 122, "plaza"); // 出小屋 +await interactAt(188, 126); // 公告板 +await check("D1 公告已读", "d.announced===true"); +// T2 浇水 ×3 +await interactAt(120, 198); await interactAt(120, 230); await interactAt(120, 262); +await check("T2 完成 +10", "d.questStatus['T2']==='done'"); +// T1 取面包 +await enterDoor(152, 70, "bakery"); +await interactAt(72, 52); +await check("T1 拿到面包", "d.breadHeld===3"); +await enterDoor(120, 154, "plaza"); +await interactAt(264, 66); // 大宝 +await enterDoor(344, 70, "store"); +await interactAt(56, 78); // 咪咪 +await enterDoor(120, 154, "plaza"); +await enterDoor(6, 148, "lake"); +await interactAt(292, 96); // 龟爷爷 +await check("T1 完成", "d.questStatus['T1']==='done'"); +await check("T3 完成(问好×3)", "d.questStatus['T3']==='done'"); +await check("D1 黄昏已触发", "d.dusk===true"); +await shot("d1_dusk"); +await enterDoor(378, 112, "plaza"); // 湖畔右缘回广场 +await sleepToNextDay(); + +// ===== Day 2 ===== +await check("进入 Day 2", "d.day===2"); +await interactAt(30, 40, 6); // 日历 C1 +await check("C1 获得(觉知+8)", "d.clues.has('C1') && d.awareness>=8"); +await enterDoor(88, 122, "plaza"); +await interactAt(188, 126); // 公告 +await interactAt(280, 46, 8); // 邮筒信件 C3 +await check("C3 获得", "d.clues.has('C3')"); +await interactAt(264, 66); // 大宝取信 +await check("T4 拿到信", "d.lettersHeld===3"); +await enterDoor(152, 70, "bakery"); +await interactAt(72, 52); // 圆圆收信 +await enterDoor(120, 154, "plaza"); +await enterDoor(344, 70, "store"); +await interactAt(56, 78); // 咪咪收信 +// T5 清点货架 +await interactAt(56, 46); await interactAt(104, 46); await interactAt(152, 46, 8); +await check("T5 完成", "d.questStatus['T5']==='done'"); +await interactAt(198, 92, 8); // C5 货架 +await check("C5 获得", "d.clues.has('C5')"); +await enterDoor(120, 154, "plaza"); +await enterDoor(424, 70, "clinic"); +await interactAt(104, 46, 10); // 芙医生收第3封信 → C4 +await check("T4 完成 & C4 获得", "d.questStatus['T4']==='done' && d.clues.has('C4')"); +await enterDoor(88, 122, "plaza"); +await sleepToNextDay(); + +// ===== Day 3 ===== +await check("进入 Day 3", "d.day===3"); +await enterDoor(88, 122, "plaza"); +await interactAt(188, 126); +// T6 采莓果 +await enterDoor(470, 148, "field"); +for (const [x, y] of [[72, 82], [136, 98], [200, 82], [248, 130], [104, 162]]) await interactAt(x, y, 4); +await check("T6 完成", "d.questStatus['T6']==='done'"); +await enterDoor(4, 100, "plaza"); +// T7 野餐 +await enterDoor(6, 148, "lake"); +await interactAt(128, 118, 10); +await check("T7 完成", "d.questStatus['T7']==='done'"); +await shot("d3_lake"); +// 面包房看配方 C2 +await enterDoor(378, 112, "plaza"); +await enterDoor(152, 70, "bakery"); +await interactAt(120, 78, 8); +await check("C2 获得", "d.clues.has('C2')"); +await enterDoor(120, 154, "plaza"); +await sleepToNextDay(); + +// ===== Day 4(完美的重复) ===== +await check("进入 Day 4", "d.day===4"); +await enterDoor(88, 122, "plaza"); +await interactAt(188, 126); +await interactAt(120, 198); await interactAt(120, 230); await interactAt(120, 262); +await check("T9 完成", "d.questStatus['T9']==='done'"); +await enterDoor(152, 70, "bakery"); +await interactAt(72, 52); +await enterDoor(120, 154, "plaza"); +await interactAt(264, 66); +await enterDoor(344, 70, "store"); +await interactAt(56, 78); +await enterDoor(120, 154, "plaza"); +await enterDoor(6, 148, "lake"); +await interactAt(292, 96, 8); // 龟爷爷(送面包) +await interactAt(292, 96, 8); // 再谈一次(C9 第二段) +await check("T8 完成", "d.questStatus['T8']==='done'"); +await check("T10 完成", "d.questStatus['T10']==='done'"); +await check("C9 链已记 D2/D4", "d.guiChain.has(4)"); +await enterDoor(378, 112, "plaza"); +await sleepToNextDay(); + +// ===== Day 5(自由日 + 古井/雾墙开闸) ===== +await check("进入 Day 5", "d.day===5"); +await enterDoor(88, 122, "plaza"); +await interactAt(188, 126); // 公告 +// T11 接受→揉面 +await enterDoor(152, 70, "bakery"); +await tp(72, 52); await sleep(300); +await pressE(); await sleep(500); // 打开对话 +await talk(4); // 读完问句 → 进入选项 +await pressE(); await sleep(300); // 选第一项:帮忙 +await talk(6); +await check("T11 已接受", "d.t11Accepted===true"); +await interactAt(168, 102, 8); // 揉面台 +await check("T11 完成", "d.questStatus['T11']==='done'"); +await enterDoor(120, 154, "plaza"); +// T12 擦公告板(第2次浮现 C6) +await tp(188, 126); await sleep(300); +await pressE(); await sleep(400); +await talk(3); await pressE(); await sleep(300); // 选:擦拭 +await talk(4); +await interactAt(188, 126, 8); // 第2次 → C6 +await check("C6 获得", "d.clues.has('C6')"); +await interactAt(188, 126, 8); // 第3次 → 完成 +await check("T12 完成", "d.questStatus['T12']==='done'"); +// 古井 C7 +await enterDoor(232, 4, "well"); +await interactAt(104, 92, 10); +await sleep(5600); // 注视古井 5 秒 +await check("C7 获得", "d.clues.has('C7')"); +await check("注视古井 +3 觉知", "d.stared.has('well')"); +// 诊所 C8 +await enterDoor(104, 148, "plaza"); +await enterDoor(424, 70, "clinic"); +await interactAt(76, 74, 8); +await check("C8 获得", "d.clues.has('C8')"); +await enterDoor(88, 122, "plaza"); +await sleepToNextDay(); + +// ===== Day 6(加速 + T14 + 雨) ===== +await check("进入 Day 6", "d.day===6"); +await enterDoor(88, 122, "plaza"); +await interactAt(188, 126); +// 龟爷爷 C9 第三段 +await enterDoor(6, 148, "lake"); +await interactAt(292, 96, 10); +await check("C9 获得(D6 第三段)", "d.clues.has('C9')"); +await enterDoor(378, 112, "plaza"); +// T13 挂彩带 ×4 +for (const [x, y] of [[188, 190], [300, 190], [188, 270], [300, 270]]) { + await interactAt(x, y); + console.log(" ribbons:", await D("[...d.ribbons].join('|')")); +} +await check("T13 完成", "d.questStatus['T13']==='done'"); +// T14 保持微笑 60 秒(把计时快进以免真等 60s) +await interactAt(240, 210, 4); // 启动 +await sleep(1500); +await page.evaluate(() => { + const sc = window.__game.scene.getScenes(true).find((s) => s.player); + sc.t14Start -= 61000; // 快进 +}); +await sleep(1500); +await talk(6); +await check("T14 完成", "d.questStatus['T14']==='done'"); +await check("D6 黄昏(脚本雨)", "d.dusk===true"); +await sleep(1000); await shot("d6_rain"); +// 雨中站立 10 秒 → 悲伤行为 +const hpBefore = await D("d.happiness"); +await page.evaluate(() => { + const sc = window.__game.scene.getScenes(true).find((s) => s.player); + sc.rainStillAt -= 11000; +}); +await sleep(800); +const hpAfter = await D("d.happiness"); +console.log(`${hpAfter <= hpBefore ? "PASS" : "FAIL"} 雨中站立触发悲伤行为(${hpBefore}→${hpAfter})`); +if (hpAfter > hpBefore) failures.push("rain sadact"); +await sleepToNextDay(); + +// ===== Day 7(分歧日:E3 苏醒线) ===== +await check("进入 Day 7", "d.day===7"); +await check("线索 ≥6", "d.clues.size>=6"); +await D("d.setAwareness(80)"); // 觉知补足(自然流程中来自更多迟疑选项/发呆) +await enterDoor(88, 122, "plaza"); +await sleep(1500); +await check("庆典已开始", "d.celebrationStarted===true"); +await shot("d7_festival"); +// 走向雾墙 → E3 +await enterDoor(232, 312, "fogwall"); +await interactAt(160, 82, 8); +await sleep(3500); // 白闪 → 白色走廊 +await check("进入白色走廊(E3 模式)", "d.e3Mode===true && d.currentScene==='white_corridor'"); +await tp(312, 60); await sleep(1500); // 走到走廊尽头 +await check("进入病房", "d.currentScene==='white_ward'"); +await interactAt(80, 72, 6); // 病历 +await sleep(16000); +await shot("d7_e3_final"); + +console.log("CONSOLE ERRORS:", errors.length); +errors.slice(0, 20).forEach((e) => console.log(" -", e)); +console.log("FAILURES:", failures.length); +await browser.close(); +await server.close(); +process.exit(errors.length || failures.length ? 1 : 0); diff --git a/Fable-5-High/tools/e2e_mech.mjs b/Fable-5-High/tools/e2e_mech.mjs new file mode 100644 index 0000000..5130067 --- /dev/null +++ b/Fable-5-High/tools/e2e_mech.mjs @@ -0,0 +1,138 @@ +// 机制专项:悲伤行为 5 项 / 强制欢乐庆典与逃离 / S4 文本腐蚀 / E4·E1 条件判定 +import { chromium } from "playwright"; +import { createServer } from "vite"; +import { mkdirSync } from "fs"; +const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-mech"; +mkdirSync(SHOT_DIR, { recursive: true }); +const server = await createServer({ server: { port: 5195 } }); +await server.listen(); +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +const errors = []; +page.on("console", m => { if (m.type() === "error") errors.push(m.text()); }); +page.on("pageerror", e => errors.push("PAGEERROR: " + e.message)); +const sleep = ms => page.waitForTimeout(ms); +const pressE = async () => { await page.keyboard.down("e"); await sleep(90); await page.keyboard.up("e"); }; +const D = expr => page.evaluate(`(() => { const d = window.__director; return ${expr}; })()`); +const failures = []; +const check = async (label, expr) => { + const ok = await D(expr); + console.log(`${ok ? "PASS" : "FAIL"} ${label}`); + if (!ok) failures.push(label); +}; +const tp = (x, y) => page.evaluate(([x, y]) => { + const sc = window.__game.scene.getScenes(true).find(s => s.player); + sc.player.setPosition(x, y); sc.player.body.reset(x, y); +}, [x, y]); +const chooseOption = async (idx) => { + for (let i = 0; i < 10; i++) { + const cm = await page.evaluate(() => window.__game.scene.getScene("ui").choiceMode); + if (cm) break; + await pressE(); await sleep(220); + } + await page.evaluate((i) => { + const ui = window.__game.scene.getScene("ui"); + ui.choiceIdx = i; ui.renderChoiceSel(); + }, idx); + await pressE(); await sleep(300); +}; +const talk = async (n = 14) => { + for (let i = 0; i < n; i++) { + await pressE(); await sleep(160); + if (!(await page.evaluate(() => window.__game.scene.getScene("ui").dialogOpen))) break; + } +}; + +await page.goto("http://localhost:5195/"); +await sleep(2500); +await page.fill("#name-input", "小满"); +await page.press("#name-input", "Enter"); +await sleep(1800); + +// 出小屋到广场 +await tp(88, 122); await sleep(1200); + +// ① 否定选项(咪咪 D1 有三选项) +await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); s.scene.start("store"); window.__director.pendingSpawn = "entry"; }); +await sleep(900); +let h0 = await D("d.happiness"), a0 = await D("d.awareness"); +await tp(56, 78); await sleep(350); +await pressE(); await sleep(300); +await chooseOption(2); // 第三项:否定 +await talk(4); +await check(`否定选项:幸福-5 觉知+3(${h0}→)`, `d.happiness===${h0}-5 && d.awareness===${a0}+3`); + +// ② 纠缠:同一 NPC 一天 ≥5 次 +h0 = await D("d.happiness"); +for (let i = 0; i < 5; i++) { await tp(56, 78); await sleep(250); await pressE(); await sleep(250); await talk(6); } +await check("纠缠:第5次对话触发悲伤行为", `d.happiness<=${h0} - 5 + 10`); // 对话有选项可能+2,宽松判定 +const nag = await D("d.nagged.has('mimi')"); +console.log(`${nag ? "PASS" : "FAIL"} 纠缠已记录`); if (!nag) failures.push("nag"); + +// ③ 发呆:10 秒不动且不在任务点 +await page.evaluate(() => { window.__director.pendingSpawn = "center"; const s = window.__game.scene.getScenes(true).find(x => x.player); s.scene.start("plaza"); }); +await sleep(900); +await tp(240, 130); // 空地 +h0 = await D("d.happiness"); +await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); s.lastMoveAt -= 11000; }); +await sleep(600); +await check(`发呆:幸福-5(${h0}→)`, `d.happiness===${h0}-5`); + +// ④ 拒绝任务(D5 T11) +await page.evaluate(() => { window.__director.startDay(5); }); +await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); window.__director.pendingSpawn = "entry"; s.scene.start("bakery"); }); +await sleep(900); +await tp(72, 52); await sleep(350); +await pressE(); await sleep(300); +await chooseOption(1); // 第二项:拒绝 +await talk(4); +await check("拒绝任务:T11 refused 且被记录", "d.questStatus['T11']==='refused' && d.refusals===1"); + +// ⑤ 强制欢乐庆典(幸福<30 持续 2 分钟)+ 逃离计数 +await D("d.setHappiness(10)"); +await page.evaluate(() => { window.__director["lowSince"] = performance.now() - 121000; }); +await sleep(800); +await check("欢乐庆典触发", "d.celebrationActive===true"); +await page.screenshot({ path: SHOT_DIR + "/celebration.png" }); +// 从场景边缘走离逃脱 +await tp(120, 154); await sleep(1500); +await check("逃离欢乐庆典计数", "d.celebrationActive===false && d.escapes===1 && d.happiness===10"); +// 不逃离 → 拉回 50 +await page.evaluate(() => { window.__director["lowSince"] = performance.now() - 121000; }); +await sleep(900); +await check("再次触发", "d.celebrationActive===true"); +await sleep(8500); +await check("未逃离:幸福拉回 ~50", "d.happiness>=45 && d.celebrationActive===false && d.escapes===1"); + +// ⑥ S4 咪咪文本腐蚀(错字+句尾标点×3) +await D("d.setHappiness(100)"); +await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); window.__director.pendingSpawn = "entry"; s.scene.start("store"); }); +await sleep(900); +await D("d.startDay(3)"); // D3 咪咪台词密集含可腐蚀词 +await D("d.setHappiness(100)"); +await tp(56, 78); await sleep(350); +await pressE(); await sleep(1200); +await pressE(); await sleep(1600); // 等台词打完 +const line = await page.evaluate(() => window.__game.scene.getScene("ui").lineText.text); +console.log(" 咪咪 S4 台词:", line); +const corrupted = /辛福|开欣|美妤|大伽|微效|名天|恬|央光|棚友|泳远/.test(line) || /[。!?]{3}/.test(line); +console.log(`${corrupted ? "PASS" : "FAIL"} S4 咪咪台词出现腐蚀`); +if (!corrupted) failures.push("S4 corruption"); +await page.screenshot({ path: SHOT_DIR + "/mimi_s4.png" }); + +// ⑦ E1/E4 条件判定(decideEnding 逻辑) +await check("E1 判定", "d.decideEnding(false)==='E1' || d.awareness>29 ? true : d.decideEnding(false)==='E1'"); +const e1 = await page.evaluate(() => { const d = window.__director; d.setAwareness(0); d.setHappiness(95); return d.decideEnding(false); }); +console.log(`${e1 === "E1" ? "PASS" : "FAIL"} 觉知≤29 且幸福≥90 → E1(得 ${e1})`); if (e1 !== "E1") failures.push("E1"); +const e2 = await page.evaluate(() => { const d = window.__director; d.setAwareness(50); return d.decideEnding(false); }); +console.log(`${e2 === "E2" ? "PASS" : "FAIL"} 觉知 30-69 → E2(得 ${e2})`); if (e2 !== "E2") failures.push("E2"); +const e4 = await page.evaluate(() => { const d = window.__director; d.e4ok = true; d.escapes = 3; return d.decideEnding(false); }); +console.log(`${e4 === "E4" ? "PASS" : "FAIL"} 静默条件 → E4(得 ${e4})`); if (e4 !== "E4") failures.push("E4"); +const e3 = await page.evaluate(() => { const d = window.__director; d.e4ok = false; d.setAwareness(80); for (let i=1;i<=6;i++) d.clues.add("C"+i); return d.decideEnding(true); }); +console.log(`${e3 === "E3" ? "PASS" : "FAIL"} 觉知≥70 线索≥6 走向雾墙 → E3(得 ${e3})`); if (e3 !== "E3") failures.push("E3"); + +console.log("CONSOLE ERRORS:", errors.length); +errors.slice(0, 10).forEach(e => console.log(" -", e)); +console.log("FAILURES:", failures.length, failures.join(" | ")); +await browser.close(); await server.close(); +process.exit(errors.length || failures.length ? 1 : 0); diff --git a/Fable-5-High/tools/gen_art.lua b/Fable-5-High/tools/gen_art.lua new file mode 100644 index 0000000..980ce54 --- /dev/null +++ b/Fable-5-High/tools/gen_art.lua @@ -0,0 +1,757 @@ +-- 《崩坏童话:蜜糖村》全部像素素材生成脚本(Aseprite Lua) +-- 运行:aseprite -b --script tools/gen_art.lua +-- 输出:public/assets/*.png + art/*.aseprite(源文件) +-- 全部素材为本脚本逐像素绘制的原创内容,无任何外部素材。 + +local pc = app.pixelColor +local function C(hex, a) + a = a or 255 + local r = tonumber(hex:sub(1, 2), 16) + local g = tonumber(hex:sub(3, 4), 16) + local b = tonumber(hex:sub(5, 6), 16) + return pc.rgba(r, g, b, a) +end + +-- ============ 糖果色板(村庄)+ 灰白色板(白色层) ============ +local PAL = { + outline = C("5a4232"), + black = C("2a2420"), + white = C("ffffff"), + -- 草地/植物 + grass = C("8fd05a"), grassD = C("79b94a"), grassL = C("a5e070"), + leaf = C("5da53f"), leafD = C("4a8a32"), trunk = C("8a5a34"), trunkD = C("6e4526"), + -- 路面/石 + path = C("e8d5a8"), pathD = C("cbb488"), stone = C("c8c0a8"), stoneD = C("a8a088"), + -- 水 + water = C("7ec8e8"), waterD = C("5fb0d8"), waterL = C("b8e6f8"), + -- 花 + pink = C("ff9ecb"), red = C("e05858"), yellow = C("ffd94a"), berry = C("e8497a"), + -- 建筑 + wall = C("fff1d0"), wallD = C("e8d8b0"), roof = C("f4924a"), roofD = C("d97733"), + wood = C("c98a4b"), woodD = C("a86a34"), woodL = C("e0aa6a"), + -- 室内 + floor = C("e6c896"), floorD = C("d4b47e"), clinic = C("eaf2ec"), clinicD = C("d0e0d6"), + inwall = C("f6e8c8"), inwallD = C("e0cca0"), + -- 灰白层 + wgray = C("e8e8ea"), wgrayD = C("cfcfd4"), wgrayDD = C("aeaeb6"), wdark = C("8a8a92"), + -- 其他 + fog = C("e8ecf0"), fogD = C("cdd6de"), + metal = C("9aa4ac"), blue = C("5a88c8"), sky = C("8ecae6"), +} + +-- ============ 绘制助手 ============ +local function makeCtx(img, ox, oy) + local ctx = {} + function ctx.px(x, y, c) img:putPixel(ox + x, oy + y, c) end + function ctx.rect(x, y, w, h, c) + for j = y, y + h - 1 do for i = x, x + w - 1 do img:putPixel(ox + i, oy + j, c) end end + end + function ctx.hline(x, y, w, c) ctx.rect(x, y, w, 1, c) end + function ctx.vline(x, y, h, c) ctx.rect(x, y, 1, h, c) end + function ctx.dither(x, y, w, h, c, step) + step = step or 2 + for j = y, y + h - 1 do for i = x, x + w - 1 do + if (i + j) % step == 0 then img:putPixel(ox + i, oy + j, c) end + end end + end + return ctx +end + +local function saveSheet(img, name) + local spr = Sprite(img.width, img.height, ColorMode.RGB) + spr.cels[1].image = img + spr:saveAs("art/" .. name .. ".aseprite") + spr:saveCopyAs("public/assets/" .. name .. ".png") + spr:close() +end + +-- ============ 瓦片集:10 列 × 8 行 = 80 瓦(16×16) ============ +local T = {} + +T[0] = function(c) -- 草地1 + c.rect(0, 0, 16, 16, PAL.grass) + c.px(3, 4, PAL.grassL); c.px(11, 9, PAL.grassL); c.px(6, 13, PAL.grassD); c.px(13, 2, PAL.grassD) +end +T[1] = function(c) -- 草地2(草茎) + c.rect(0, 0, 16, 16, PAL.grass) + c.px(4, 5, PAL.grassD); c.px(4, 4, PAL.grassD); c.px(10, 10, PAL.grassD); c.px(10, 9, PAL.grassD) + c.px(13, 5, PAL.grassL); c.px(2, 12, PAL.grassL) +end +T[2] = function(c) -- 草地+白花 + T[0](c); c.px(5, 6, PAL.white); c.px(4, 5, PAL.white); c.px(6, 5, PAL.white); c.px(5, 4, PAL.white); c.px(5, 5, PAL.yellow) + c.px(12, 12, PAL.white) +end +T[3] = function(c) -- 草地+粉花 + T[1](c); c.px(11, 4, PAL.pink); c.px(10, 3, PAL.pink); c.px(12, 3, PAL.pink); c.px(11, 2, PAL.pink); c.px(11, 3, PAL.yellow) + c.px(3, 10, PAL.pink) +end +T[4] = function(c) -- 石板路 + c.rect(0, 0, 16, 16, PAL.path) + c.hline(0, 7, 16, PAL.pathD); c.vline(7, 0, 8, PAL.pathD); c.vline(3, 8, 8, PAL.pathD); c.vline(12, 8, 8, PAL.pathD) + c.px(2, 3, PAL.pathD); c.px(10, 12, PAL.pathD) +end +T[5] = function(c) -- 石板路2 + c.rect(0, 0, 16, 16, PAL.path) + c.hline(0, 3, 16, PAL.pathD); c.hline(0, 11, 16, PAL.pathD); c.vline(5, 4, 7, PAL.pathD); c.vline(11, 12, 4, PAL.pathD); c.vline(11, 0, 3, PAL.pathD) +end +T[6] = function(c) -- 路缘(草→路) + c.rect(0, 0, 16, 8, PAL.grass); c.rect(0, 8, 16, 8, PAL.path) + c.hline(0, 7, 16, PAL.grassD); c.hline(0, 8, 16, PAL.pathD) + c.px(3, 6, PAL.grassL); c.px(11, 12, PAL.pathD) +end +T[7] = function(c) -- 湖水1 + c.rect(0, 0, 16, 16, PAL.water) + c.hline(2, 4, 4, PAL.waterL); c.hline(9, 10, 4, PAL.waterL); c.hline(5, 13, 3, PAL.waterD) +end +T[8] = function(c) -- 湖水2 + c.rect(0, 0, 16, 16, PAL.water) + c.hline(8, 3, 4, PAL.waterD); c.hline(1, 9, 4, PAL.waterL); c.hline(11, 14, 3, PAL.waterL) +end +T[9] = function(c) -- 岸边(上草下水) + c.rect(0, 0, 16, 6, PAL.grass); c.rect(0, 6, 16, 10, PAL.water) + c.hline(0, 5, 16, PAL.grassD); c.hline(0, 6, 16, PAL.waterL) +end +T[10] = function(c) -- 花丛 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(1, 2, 14, 12, PAL.leaf) + c.px(1, 2, PAL.grass); c.px(14, 2, PAL.grass); c.px(1, 13, PAL.grass); c.px(14, 13, PAL.grass) + c.px(4, 5, PAL.pink); c.px(8, 4, PAL.yellow); c.px(12, 6, PAL.white); c.px(3, 9, PAL.yellow) + c.px(7, 8, PAL.pink); c.px(11, 10, PAL.pink); c.px(5, 12, PAL.white); c.px(10, 12, PAL.yellow) +end +T[11] = function(c) -- 莓果丛(有果) + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 3, 12, 11, PAL.leaf); c.rect(3, 2, 10, 13, PAL.leaf) + c.dither(3, 3, 10, 10, PAL.leafD, 3) + c.px(5, 5, PAL.berry); c.px(10, 4, PAL.berry); c.px(7, 8, PAL.berry); c.px(11, 9, PAL.berry); c.px(4, 10, PAL.berry) +end +T[12] = function(c) -- 莓果丛(已采) + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 3, 12, 11, PAL.leaf); c.rect(3, 2, 10, 13, PAL.leaf) + c.dither(3, 3, 10, 10, PAL.leafD, 3) +end +T[13] = function(c) -- 树·左上 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(4, 2, 12, 14, PAL.leaf) + c.px(4, 2, PAL.grass); c.px(5, 2, PAL.grass); c.px(4, 3, PAL.grass) + c.dither(5, 4, 11, 12, PAL.leafD, 4); c.px(8, 5, PAL.grassL); c.px(12, 8, PAL.grassL) +end +T[14] = function(c) -- 树·右上 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(0, 2, 12, 14, PAL.leaf) + c.px(11, 2, PAL.grass); c.px(10, 2, PAL.grass); c.px(11, 3, PAL.grass) + c.dither(0, 4, 11, 12, PAL.leafD, 4); c.px(4, 6, PAL.grassL) +end +T[15] = function(c) -- 树·左下 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(4, 0, 12, 6, PAL.leaf); c.dither(4, 0, 12, 6, PAL.leafD, 4) + c.px(4, 5, PAL.grass) + c.rect(10, 6, 6, 10, PAL.trunk); c.vline(10, 6, 10, PAL.trunkD) +end +T[16] = function(c) -- 树·右下 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(0, 0, 12, 6, PAL.leaf); c.dither(0, 0, 12, 6, PAL.leafD, 4) + c.px(11, 5, PAL.grass) + c.rect(0, 6, 6, 10, PAL.trunk); c.vline(5, 6, 10, PAL.trunkD) +end +T[17] = function(c) -- 小灌木 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(3, 5, 10, 9, PAL.leaf); c.rect(4, 4, 8, 11, PAL.leaf) + c.dither(4, 5, 8, 8, PAL.leafD, 3) +end +T[18] = function(c) -- 房屋外墙 + c.rect(0, 0, 16, 16, PAL.wall) + c.hline(0, 15, 16, PAL.wallD); c.vline(0, 0, 16, PAL.wallD) + c.px(4, 4, PAL.wallD); c.px(11, 9, PAL.wallD) +end +T[19] = function(c) -- 外墙+窗 + T[18](c) + c.rect(4, 4, 8, 8, PAL.woodD); c.rect(5, 5, 6, 6, PAL.sky) + c.vline(7, 5, 6, PAL.woodD); c.hline(5, 8, 6, PAL.woodD) + c.px(6, 6, PAL.waterL) +end +T[20] = function(c) -- 门 + c.rect(0, 0, 16, 16, PAL.wall) + c.rect(3, 1, 10, 15, PAL.wood); c.rect(4, 2, 8, 14, PAL.woodL) + c.vline(4, 2, 14, PAL.wood); c.px(10, 9, PAL.woodD); c.hline(4, 2, 8, PAL.wood) +end +T[21] = function(c) -- 屋顶·左 + c.rect(0, 0, 16, 16, PAL.roof) + c.hline(0, 0, 16, PAL.roofD); c.hline(0, 5, 16, PAL.roofD); c.hline(0, 10, 16, PAL.roofD); c.hline(0, 15, 16, PAL.roofD) + c.vline(0, 0, 16, PAL.roofD) +end +T[22] = function(c) -- 屋顶·右 + c.rect(0, 0, 16, 16, PAL.roof) + c.hline(0, 0, 16, PAL.roofD); c.hline(0, 5, 16, PAL.roofD); c.hline(0, 10, 16, PAL.roofD); c.hline(0, 15, 16, PAL.roofD) + c.vline(15, 0, 16, PAL.roofD) +end +T[23] = function(c) -- 屋檐 + c.rect(0, 0, 16, 8, PAL.roof); c.hline(0, 7, 16, PAL.roofD); c.hline(0, 6, 16, PAL.roofD) + c.rect(0, 8, 16, 8, PAL.wall); c.hline(0, 8, 16, PAL.wallD) +end +T[24] = function(c) -- 木地板 + c.rect(0, 0, 16, 16, PAL.floor) + c.hline(0, 4, 16, PAL.floorD); c.hline(0, 9, 16, PAL.floorD); c.hline(0, 14, 16, PAL.floorD) + c.px(5, 2, PAL.floorD); c.px(12, 7, PAL.floorD); c.px(3, 12, PAL.floorD) +end +T[25] = function(c) -- 木地板2 + c.rect(0, 0, 16, 16, PAL.floor) + c.hline(0, 2, 16, PAL.floorD); c.hline(0, 7, 16, PAL.floorD); c.hline(0, 12, 16, PAL.floorD) + c.px(9, 4, PAL.floorD); c.px(2, 9, PAL.floorD) +end +T[26] = function(c) -- 诊所地砖 + c.rect(0, 0, 16, 16, PAL.clinic) + c.hline(0, 7, 16, PAL.clinicD); c.vline(7, 0, 16, PAL.clinicD); c.hline(0, 15, 16, PAL.clinicD); c.vline(15, 0, 16, PAL.clinicD) +end +T[27] = function(c) -- 室内墙 + c.rect(0, 0, 16, 16, PAL.inwall) + c.dither(0, 0, 16, 16, PAL.inwallD, 8) +end +T[28] = function(c) -- 室内墙裙 + c.rect(0, 0, 16, 10, PAL.inwall) + c.rect(0, 10, 16, 6, PAL.wood); c.hline(0, 10, 16, PAL.woodD) +end +T[29] = function(c) -- 地毯 + c.rect(0, 0, 16, 16, PAL.pink) + c.rect(1, 1, 14, 14, C("ffc4de")); c.rect(3, 3, 10, 10, PAL.pink) + c.px(7, 7, PAL.white); c.px(8, 8, PAL.white) +end +T[30] = function(c) -- 床头(枕头) + c.rect(0, 0, 16, 16, PAL.wood) + c.rect(1, 1, 14, 15, C("f8f4e8")) + c.rect(3, 3, 10, 7, PAL.white); c.hline(3, 9, 10, C("d8d4c8")) + c.hline(1, 0, 14, PAL.woodD) +end +T[31] = function(c) -- 床尾(被子) + c.rect(0, 0, 16, 16, PAL.wood) + c.rect(1, 0, 14, 15, C("8ec8e0")) + c.hline(1, 3, 14, C("6fb0d0")); c.hline(1, 8, 14, C("6fb0d0")); c.hline(1, 13, 14, C("6fb0d0")) + c.hline(1, 15, 14, PAL.woodD) +end +T[32] = function(c) -- 柜台 + c.rect(0, 0, 16, 16, PAL.wood) + c.rect(0, 0, 16, 6, PAL.woodL); c.hline(0, 6, 16, PAL.woodD) + c.vline(0, 0, 16, PAL.woodD); c.vline(15, 0, 16, PAL.woodD); c.hline(0, 15, 16, PAL.woodD) +end +T[33] = function(c) -- 烤炉 + c.rect(0, 0, 16, 16, PAL.stoneD) + c.rect(1, 1, 14, 14, PAL.stone) + c.rect(3, 6, 10, 7, PAL.black) + c.rect(4, 8, 8, 4, C("f4924a")); c.px(6, 9, PAL.yellow); c.px(9, 10, PAL.yellow) + c.hline(3, 3, 10, PAL.stoneD) +end +T[34] = function(c) -- 货架(空) + c.rect(0, 0, 16, 16, PAL.wood) + c.rect(1, 1, 14, 14, PAL.woodL) + c.hline(1, 5, 14, PAL.woodD); c.hline(1, 10, 14, PAL.woodD); c.hline(1, 14, 14, PAL.woodD) + c.vline(0, 0, 16, PAL.woodD); c.vline(15, 0, 16, PAL.woodD) +end +T[35] = function(c) -- 货架(同一商品重复排列,标签空白) + T[34](c) + for _, y in ipairs({ 2, 7, 11 }) do + c.rect(2, y, 3, 3, PAL.berry); c.rect(6, y, 3, 3, PAL.berry); c.rect(10, y, 3, 3, PAL.berry) + c.px(3, y, PAL.white); c.px(7, y, PAL.white); c.px(11, y, PAL.white) + end +end +T[36] = function(c) -- 桌子 + c.rect(0, 0, 16, 16, PAL.floor) + c.rect(2, 3, 12, 9, PAL.woodL); c.hline(2, 11, 12, PAL.woodD) + c.vline(3, 12, 3, PAL.woodD); c.vline(12, 12, 3, PAL.woodD) + c.hline(2, 3, 12, PAL.wood) +end +T[37] = function(c) -- 椅子 + c.rect(0, 0, 16, 16, PAL.floor) + c.rect(4, 3, 8, 3, PAL.wood) + c.rect(4, 6, 8, 6, PAL.woodL) + c.vline(4, 12, 3, PAL.woodD); c.vline(11, 12, 3, PAL.woodD) +end +T[38] = function(c) -- 井·顶(小屋顶) + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 2, 12, 6, PAL.roofD); c.rect(3, 1, 10, 2, PAL.roof) + c.vline(3, 8, 8, PAL.trunkD); c.vline(12, 8, 8, PAL.trunkD) +end +T[39] = function(c) -- 井·底(石圈) + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 2, 12, 12, PAL.stone) + c.rect(4, 4, 8, 8, PAL.black) + c.px(3, 3, PAL.stoneD); c.px(12, 3, PAL.stoneD); c.px(3, 12, PAL.stoneD); c.px(12, 12, PAL.stoneD) + c.hline(2, 2, 12, PAL.stoneD) +end +T[40] = function(c) -- 雾(轻) + c.rect(0, 0, 16, 16, PAL.fog) + c.dither(0, 0, 16, 16, PAL.fogD, 3) +end +T[41] = function(c) -- 雾(浓) + c.rect(0, 0, 16, 16, PAL.fogD) + c.dither(0, 0, 16, 16, PAL.fog, 2); c.dither(1, 0, 16, 16, C("bcc8d2"), 5) +end +T[42] = function(c) -- 白色层·墙 + c.rect(0, 0, 16, 16, PAL.wgray) + c.hline(0, 12, 16, PAL.wgrayD); c.rect(0, 13, 16, 3, PAL.wgrayDD) +end +T[43] = function(c) -- 白色层·地 + c.rect(0, 0, 16, 16, PAL.wgrayD) + c.hline(0, 7, 16, PAL.wgrayDD); c.vline(7, 0, 16, PAL.wgrayDD) +end +T[44] = function(c) -- 白色层·门 + c.rect(0, 0, 16, 16, PAL.wgray) + c.rect(3, 1, 10, 15, PAL.wgrayDD); c.rect(4, 2, 8, 14, PAL.wgrayD) + c.px(10, 9, PAL.wdark) +end +T[45] = function(c) -- 长椅·左 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 4, 14, 3, PAL.woodL); c.rect(2, 7, 14, 4, PAL.wood) + c.vline(3, 11, 3, PAL.woodD); c.hline(2, 7, 14, PAL.woodD) +end +T[46] = function(c) -- 长椅·右 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(0, 4, 14, 3, PAL.woodL); c.rect(0, 7, 14, 4, PAL.wood) + c.vline(12, 11, 3, PAL.woodD); c.hline(0, 7, 14, PAL.woodD) +end +T[47] = function(c) -- 公告板·左 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 1, 14, 12, PAL.wood); c.rect(4, 3, 12, 8, C("f8f0d8")) + c.hline(5, 5, 8, PAL.stoneD); c.hline(5, 7, 6, PAL.stoneD); c.hline(5, 9, 9, PAL.stoneD) + c.vline(4, 13, 3, PAL.trunkD) +end +T[48] = function(c) -- 公告板·右 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(0, 1, 14, 12, PAL.wood); c.rect(0, 3, 12, 8, C("f8f0d8")) + c.hline(1, 5, 8, PAL.stoneD); c.hline(1, 7, 9, PAL.stoneD); c.hline(1, 9, 5, PAL.stoneD) + c.vline(11, 13, 3, PAL.trunkD) +end +T[49] = function(c) -- 邮筒 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(4, 2, 8, 10, PAL.red); c.rect(5, 1, 6, 2, C("c04040")) + c.rect(6, 5, 4, 2, PAL.black) + c.vline(6, 12, 3, C("c04040")); c.vline(9, 12, 3, C("c04040")) +end +T[50] = function(c) -- 栅栏·横 + c.rect(0, 0, 16, 16, PAL.grass) + c.hline(0, 6, 16, PAL.woodL); c.hline(0, 7, 16, PAL.wood); c.hline(0, 10, 16, PAL.woodL); c.hline(0, 11, 16, PAL.wood) + c.rect(2, 4, 2, 10, PAL.wood); c.rect(12, 4, 2, 10, PAL.wood) + c.px(2, 4, PAL.woodL); c.px(12, 4, PAL.woodL) +end +T[51] = function(c) -- 栅栏·柱 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(6, 2, 3, 12, PAL.wood); c.px(6, 2, PAL.woodL); c.px(7, 2, PAL.woodL) +end +T[52] = function(c) -- 路牌 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 3, 12, 6, PAL.woodL); c.hline(3, 5, 10, PAL.woodD); c.hline(3, 7, 7, PAL.woodD) + c.rect(7, 9, 2, 6, PAL.wood) +end +T[53] = function(c) -- 野餐布1 + c.rect(0, 0, 16, 16, C("f8f0e0")) + for j = 0, 15 do for i = 0, 15 do + if (math.floor(i / 4) + math.floor(j / 4)) % 2 == 0 then c.px(i, j, C("f0a0a8")) end + end end +end +T[54] = function(c) -- 野餐布2(有食物) + T[53](c) + c.rect(5, 5, 6, 6, C("fff8e8")); c.px(7, 7, PAL.berry); c.px(9, 8, PAL.berry); c.px(6, 9, PAL.yellow) +end +T[55] = function(c) -- 花坛(盛开) + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(1, 1, 14, 14, PAL.stoneD); c.rect(2, 2, 12, 12, C("8a6a48")) + c.px(4, 4, PAL.pink); c.px(8, 5, PAL.yellow); c.px(11, 4, PAL.red); c.px(5, 8, PAL.yellow) + c.px(9, 9, PAL.pink); c.px(12, 8, PAL.white); c.px(4, 11, PAL.red); c.px(8, 12, PAL.pink); c.px(11, 11, PAL.yellow) +end +T[56] = function(c) -- 花田·土 + c.rect(0, 0, 16, 16, C("a8825a")) + c.hline(0, 3, 16, C("94714c")); c.hline(0, 8, 16, C("94714c")); c.hline(0, 13, 16, C("94714c")) +end +T[57] = function(c) -- 花田·苗 + T[56](c) + c.px(3, 2, PAL.leaf); c.px(3, 1, PAL.leaf); c.px(9, 7, PAL.leaf); c.px(9, 6, PAL.leaf); c.px(13, 12, PAL.leaf); c.px(13, 11, PAL.leaf) + c.px(4, 1, PAL.pink); c.px(10, 6, PAL.yellow) +end +T[58] = function(c) -- 彩带(挂起) + c.rect(0, 0, 16, 16, pc.rgba(0, 0, 0, 0)) + for i = 0, 15 do + local y = math.floor(2 + 2 * math.sin(i / 4)) + c.px(i, y + 1, PAL.woodD) + end + c.px(2, 4, PAL.red); c.px(2, 5, PAL.red); c.px(6, 5, PAL.yellow); c.px(6, 6, PAL.yellow) + c.px(10, 4, PAL.pink); c.px(10, 5, PAL.pink); c.px(14, 5, PAL.sky); c.px(14, 6, PAL.sky) +end +T[59] = function(c) -- 灯笼杆 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(7, 2, 2, 12, PAL.trunk) + c.rect(5, 2, 6, 5, PAL.yellow); c.rect(6, 1, 4, 1, PAL.roofD); c.px(7, 4, C("fff0a0")); c.px(8, 4, C("fff0a0")) +end +T[60] = function(c) -- 湖水·波光 + T[7](c) + c.px(4, 6, PAL.white); c.px(11, 8, PAL.white); c.hline(7, 2, 3, PAL.waterL) +end +T[61] = function(c) -- 室内窗 + T[27](c) + c.rect(3, 3, 10, 8, PAL.woodD); c.rect(4, 4, 8, 6, PAL.sky) + c.vline(7, 4, 6, PAL.woodD); c.px(5, 5, PAL.waterL) +end +T[62] = function(c) -- 白色层·病床头 + c.rect(0, 0, 16, 16, PAL.wgrayD) + c.rect(1, 1, 14, 15, PAL.wgray) + c.rect(3, 3, 10, 6, PAL.white); c.hline(3, 8, 10, PAL.wgrayD) + c.hline(1, 0, 14, PAL.wgrayDD) +end +T[63] = function(c) -- 白色层·病床尾 + c.rect(0, 0, 16, 16, PAL.wgrayD) + c.rect(1, 0, 14, 15, C("dfe2e6")) + c.hline(1, 4, 14, PAL.wgrayD); c.hline(1, 9, 14, PAL.wgrayD) + c.hline(1, 15, 14, PAL.wgrayDD) +end +T[64] = function(c) -- 桌(诊所/办公) + c.rect(0, 0, 16, 16, PAL.clinic) + c.rect(1, 3, 14, 9, PAL.woodL); c.hline(1, 11, 14, PAL.woodD); c.hline(1, 3, 14, PAL.wood) + c.rect(3, 5, 6, 4, PAL.white); c.hline(4, 6, 4, PAL.stoneD); c.hline(4, 7, 3, PAL.stoneD) + c.vline(2, 12, 3, PAL.woodD); c.vline(13, 12, 3, PAL.woodD) +end +T[65] = function(c) -- 日历(墙面) + T[27](c) + c.rect(4, 2, 8, 11, PAL.white); c.rect(4, 2, 8, 3, PAL.red) + c.hline(5, 7, 6, PAL.stoneD); c.hline(5, 9, 6, PAL.stoneD); c.rect(7, 10, 3, 2, PAL.black) +end +T[66] = function(c) -- 柜台+配方纸 + T[32](c) + c.rect(4, 1, 7, 5, C("f8f0d8")); c.hline(5, 2, 5, PAL.stoneD); c.hline(5, 4, 4, PAL.stoneD) +end +T[67] = function(c) -- 木箱 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 3, 12, 12, PAL.wood); c.rect(3, 4, 10, 10, PAL.woodL) + c.hline(3, 8, 10, PAL.wood); c.vline(8, 4, 10, PAL.wood) +end +T[68] = function(c) -- 篮子 + c.rect(0, 0, 16, 16, PAL.floor) + c.rect(3, 6, 10, 8, PAL.woodL); c.hline(3, 6, 10, PAL.woodD); c.hline(3, 13, 10, PAL.woodD) + c.dither(4, 8, 8, 5, PAL.wood, 2) + c.px(6, 5, PAL.berry); c.px(9, 5, PAL.berry) +end +T[69] = function(c) -- 花坛(干涸·待浇水) + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(1, 1, 14, 14, PAL.stoneD); c.rect(2, 2, 12, 12, C("b09068")) + c.px(5, 5, C("8a7a5a")); c.px(10, 7, C("8a7a5a")); c.px(6, 10, C("8a7a5a")) +end +T[70] = function(c) -- 白色层·窗 + T[42](c) + c.rect(4, 2, 8, 7, PAL.wgrayDD); c.rect(5, 3, 6, 5, C("c8ccd4")); c.vline(7, 3, 5, PAL.wgrayDD) +end +T[71] = function(c) -- 门垫 + c.rect(0, 0, 16, 16, PAL.grass) + c.rect(2, 4, 12, 8, C("c8a878")); c.rect(3, 5, 10, 6, C("dcc094")) +end +T[72] = function(c) -- 高草 + c.rect(0, 0, 16, 16, PAL.grass) + for i = 1, 14, 3 do c.vline(i, 6, 8, PAL.grassD); c.vline(i + 1, 8, 6, PAL.grassL) end +end +T[73] = function(c) -- 广场中心砖 + c.rect(0, 0, 16, 16, PAL.path) + c.rect(2, 2, 12, 12, C("f0c88a")); c.rect(5, 5, 6, 6, PAL.path) + c.px(7, 7, PAL.roofD); c.px(8, 8, PAL.roofD) +end +T[74] = function(c) -- 白色层·帘 + c.rect(0, 0, 16, 16, PAL.wgray) + for i = 0, 15, 3 do c.vline(i, 0, 16, C("d8dade")) end + c.hline(0, 0, 16, PAL.wgrayDD) +end +T[75] = function(c) -- 白色层·监护仪 + c.rect(0, 0, 16, 16, PAL.wgrayD) + c.rect(3, 2, 10, 9, PAL.wdark); c.rect(4, 3, 8, 6, C("354048")) + c.px(5, 6, C("7ae0a0")); c.px(6, 5, C("7ae0a0")); c.px(7, 6, C("7ae0a0")); c.px(8, 6, C("7ae0a0")); c.px(9, 4, C("7ae0a0")); c.px(10, 6, C("7ae0a0")) + c.rect(6, 11, 4, 4, PAL.wgrayDD) +end +T[76] = function(c) -- 石墙(村界) + c.rect(0, 0, 16, 16, PAL.stone) + c.hline(0, 5, 16, PAL.stoneD); c.hline(0, 11, 16, PAL.stoneD) + c.vline(4, 0, 5, PAL.stoneD); c.vline(10, 6, 5, PAL.stoneD); c.vline(6, 12, 4, PAL.stoneD) +end +T[77] = function(c) -- 雾中草地(边缘) + c.rect(0, 0, 16, 16, PAL.grass) + c.dither(0, 0, 16, 16, PAL.fog, 2) +end +T[78] = function(c) -- 黑 + c.rect(0, 0, 16, 16, PAL.black) +end +T[79] = function(c) -- 纯白灰 + c.rect(0, 0, 16, 16, PAL.wgray) +end + +local tilesImg = Image(160, 128, ColorMode.RGB) +for idx = 0, 79 do + local col, row = idx % 10, math.floor(idx / 10) + T[idx](makeCtx(tilesImg, col * 16, row * 16)) +end +saveSheet(tilesImg, "tiles") + +-- ============ 主角:16×24 × 16 帧(下/上/左/右 × 4) ============ +local SKIN = C("f6d2a8") +local HAIR = C("6b4632") +local SHIRT = C("e87858") +local SHIRT_D = C("cc5f42") +local PANTS = C("4a6a9a") +local SHOE = C("7a4a2a") + +-- 画一帧主角;dir: 0下 1上 2左 3右;f: 0..3 行走帧(0 兼待机) +local function paintPlayer(c, dir, f) + -- 头(x4..11, y0..7):发 0..2,脸 3..7 + c.rect(4, 0, 8, 3, HAIR) + c.px(4, 0, pc.rgba(0, 0, 0, 0)); c.px(11, 0, pc.rgba(0, 0, 0, 0)) + if dir == 1 then + c.rect(4, 3, 8, 5, HAIR) -- 背面全是头发 + c.px(4, 7, SKIN); c.px(11, 7, SKIN) + else + c.rect(4, 3, 8, 5, SKIN) + c.vline(4, 3, 2, HAIR); c.vline(11, 3, 2, HAIR) -- 刘海两侧 + if dir == 0 then + c.px(6, 5, PAL.black); c.px(9, 5, PAL.black) + c.px(7, 7, PAL.black); c.px(8, 7, PAL.black) -- 永远的微笑 + c.px(6, 6, C("f8b8a0")); c.px(9, 6, C("f8b8a0")) + elseif dir == 2 then + c.rect(9, 3, 3, 5, HAIR) -- 侧脸:后脑 + c.px(6, 5, PAL.black); c.px(5, 7, PAL.black); c.px(6, 7, PAL.black) + elseif dir == 3 then + c.rect(4, 3, 3, 5, HAIR) + c.px(9, 5, PAL.black); c.px(9, 7, PAL.black); c.px(10, 7, PAL.black) + end + end + -- 身体(y8..16) + c.rect(5, 8, 6, 9, SHIRT) + c.hline(5, 16, 6, SHIRT_D) + -- 手臂:随帧摆动 + local sway = ({ 0, 1, 0, -1 })[f + 1] + if dir == 0 or dir == 1 then + c.vline(4, 9 + (sway > 0 and 1 or 0), 5, SHIRT_D); c.px(4, 14 + (sway > 0 and 1 or 0), SKIN) + c.vline(11, 9 + (sway < 0 and 1 or 0), 5, SHIRT_D); c.px(11, 14 + (sway < 0 and 1 or 0), SKIN) + else + c.vline(dir == 2 and 7 or 8, 9 + math.abs(sway), 5, SHIRT_D) + c.px(dir == 2 and 7 or 8, 14 + math.abs(sway), SKIN) + end + -- 腿(y17..20)与鞋(y21..23) + local lLift = (f == 1) and 1 or 0 + local rLift = (f == 3) and 1 or 0 + c.rect(5, 17, 3, 4 - lLift, PANTS) + c.rect(8, 17, 3, 4 - rLift, PANTS) + c.rect(5, 21 - lLift, 3, 2, SHOE) + c.rect(8, 21 - rLift, 3, 2, SHOE) +end + +local playerImg = Image(256, 24, ColorMode.RGB) +for dir = 0, 3 do + for f = 0, 3 do + paintPlayer(makeCtx(playerImg, (dir * 4 + f) * 16, 0), dir, f) + end +end +saveSheet(playerImg, "player") + +-- ============ 主角头像:24×24(微笑 / 真实的脸) ============ +local function paintPortrait(c, real) + c.rect(0, 0, 24, 24, C("f8ecd0")) -- 背景 + c.rect(4, 2, 16, 6, HAIR) -- 头发 + c.px(4, 2, C("f8ecd0")); c.px(19, 2, C("f8ecd0")) + c.rect(4, 8, 16, 12, SKIN) -- 脸 + c.vline(4, 8, 4, HAIR); c.vline(5, 8, 2, HAIR) + c.vline(19, 8, 4, HAIR); c.vline(18, 8, 2, HAIR) + c.rect(6, 20, 12, 4, SHIRT) -- 肩 + if real then + -- 真实的脸:疲惫——平直的嘴、眼下阴影、微乱的头发 + c.px(3, 4, HAIR); c.px(20, 5, HAIR); c.px(2, 5, HAIR) + c.rect(8, 12, 2, 2, PAL.black); c.rect(14, 12, 2, 2, PAL.black) + c.hline(8, 15, 2, C("c8a080")); c.hline(14, 15, 2, C("c8a080")) -- 眼袋 + c.hline(10, 18, 4, C("9a6a52")) -- 平直疲惫的嘴 + else + -- 永远的微笑 + c.rect(8, 12, 2, 2, PAL.black); c.rect(14, 12, 2, 2, PAL.black) + c.px(7, 14, C("f8b8a0")); c.px(16, 14, C("f8b8a0")) -- 腮红 + c.px(9, 17, C("9a6a52")); c.px(14, 17, C("9a6a52")) + c.hline(10, 18, 4, C("9a6a52")) -- 上弯的微笑 + end +end +local p1 = Image(24, 24, ColorMode.RGB); paintPortrait(makeCtx(p1, 0, 0), false); saveSheet(p1, "portrait_smile") +local p2 = Image(24, 24, ColorMode.RGB); paintPortrait(makeCtx(p2, 0, 0), true); saveSheet(p2, "portrait_real") + +-- ============ 具名 NPC:16×24 × 4 帧(smile/flat/off/blink) ============ +-- off 档与 smile 档仅差 1–2 像素(右眼低 1 像素),绝不画成鬼脸。 +local function paintAnimal(c, spec, expr) + local top = spec.headTop or 2 + -- 头 + c.rect(2, top, 12, 10, spec.fur) + c.px(2, top, pc.rgba(0, 0, 0, 0)); c.px(13, top, pc.rgba(0, 0, 0, 0)) + c.px(2, top + 9, pc.rgba(0, 0, 0, 0)); c.px(13, top + 9, pc.rgba(0, 0, 0, 0)) + -- 口鼻 + c.rect(5, top + 5, 6, 4, spec.muzzle) + -- 眼睛 + local ey = top + 4 + if expr == "blink" then + c.px(5, ey + 1, PAL.black); c.px(10, ey + 1, PAL.black) + else + c.px(5, ey, PAL.black); c.px(5, ey + 1, PAL.black) + local roff = (expr == "off") and 1 or 0 -- 崩坏档:右眼低 1 像素 + c.px(10, ey + roff, PAL.black); c.px(10, ey + 1 + roff, PAL.black) + end + -- 鼻子 + if spec.nose then c.px(7, top + 6, PAL.black); c.px(8, top + 6, PAL.black) end + -- 嘴:微笑(小 U)或平直 + local my = top + 8 + if expr == "flat" then + c.hline(6, my, 4, PAL.black) + else + c.px(6, my - 1, PAL.black); c.px(9, my - 1, PAL.black); c.px(7, my, PAL.black); c.px(8, my, PAL.black) + end + -- 身体 + c.rect(4, 12, 8, 8, spec.cloth) + -- 手 + c.vline(3, 13, 4, spec.fur); c.vline(12, 13, 4, spec.fur) + -- 脚 + c.rect(4, 20, 3, 3, spec.furD); c.rect(9, 20, 3, 3, spec.furD) + if spec.extra then spec.extra(c, expr) end +end + +local NPCS = { + { file = "npc_afu", fur = C("c49058"), furD = C("a87840"), muzzle = C("ecd0a8"), cloth = C("e0b060"), nose = true, + extra = function(c) -- 狗村长:垂耳+红领巾 + c.rect(1, 3, 2, 5, C("a87840")); c.rect(13, 3, 2, 5, C("a87840")) + c.hline(4, 12, 8, PAL.red); c.px(7, 13, PAL.red); c.px(8, 13, PAL.red) + end }, + { file = "npc_yuanyuan", fur = C("f7f2ea"), furD = C("ddd4c8"), muzzle = C("ffffff"), cloth = C("f8c8d8"), headTop = 4, + extra = function(c) -- 兔面包师:长耳+围裙 + c.rect(4, 0, 2, 4, C("f7f2ea")); c.px(5, 1, C("f8b8c8")); c.px(5, 2, C("f8b8c8")) + c.rect(10, 0, 2, 4, C("f7f2ea")); c.px(10, 1, C("f8b8c8")); c.px(10, 2, C("f8b8c8")) + c.rect(5, 14, 6, 6, C("fff8f0")); c.px(6, 10, C("f0a0b0")) -- 围裙+腮红 + c.px(9, 10, C("f0a0b0")) + end }, + { file = "npc_dabao", fur = C("9a6b42"), furD = C("7e5530"), muzzle = C("c89a68"), cloth = C("5a88c8"), nose = true, + extra = function(c) -- 熊邮差:圆耳+挎包带 + c.rect(3, 0, 3, 2, C("9a6b42")); c.rect(10, 0, 3, 2, C("9a6b42")) + c.px(4, 1, C("c89a68")); c.px(11, 1, C("c89a68")) + for i = 0, 6 do c.px(4 + i, 13 + i > 19 and 19 or 13 + math.floor(i * 0.9), C("3a5a88")) end + end }, + { file = "npc_mimi", fur = C("c8c0d8"), furD = C("a8a0c0"), muzzle = C("ece8f4"), cloth = C("e8a04a"), nose = true, + extra = function(c) -- 猫店主:尖耳+胡须 + c.px(3, 1, C("c8c0d8")); c.rect(3, 2, 2, 1, C("c8c0d8")); c.px(4, 1, C("c8c0d8")) + c.px(12, 1, C("c8c0d8")); c.rect(11, 2, 2, 1, C("c8c0d8")); c.px(11, 1, C("c8c0d8")) + c.hline(1, 8, 2, C("8a84a0")); c.hline(13, 8, 2, C("8a84a0")) + end }, + { file = "npc_fu", fur = C("d8b088"), furD = C("bc9268"), muzzle = C("f0d8b8"), cloth = C("f4f4f4"), nose = true, + extra = function(c) -- 鹿医生:小角+白大褂 + c.px(4, 1, C("8a6a4a")); c.px(4, 0, C("8a6a4a")); c.px(3, 0, C("8a6a4a")) + c.px(11, 1, C("8a6a4a")); c.px(11, 0, C("8a6a4a")); c.px(12, 0, C("8a6a4a")) + c.vline(7, 12, 8, C("d8d8d8")) -- 大褂门襟 + end }, + { file = "npc_gui", fur = C("a8c078"), furD = C("88a058"), muzzle = C("c8d8a0"), cloth = C("7a9a4a"), + extra = function(c) -- 龟爷爷:龟壳纹+老花镜 + c.hline(5, 14, 6, C("5f7c38")); c.hline(5, 17, 6, C("5f7c38")); c.vline(7, 13, 6, C("5f7c38")) + local gy = 2 + 4 + c.px(4, gy, C("6a6a6a")); c.px(6, gy, C("6a6a6a")); c.px(9, gy, C("6a6a6a")); c.px(11, gy, C("6a6a6a")) + c.px(7, gy, C("6a6a6a")); c.px(8, gy, C("6a6a6a")) + end }, +} + +for _, spec in ipairs(NPCS) do + local img = Image(64, 24, ColorMode.RGB) + local exprs = { "smile", "flat", "off", "blink" } + for i, e in ipairs(exprs) do + paintAnimal(makeCtx(img, (i - 1) * 16, 0), spec, e) + end + saveSheet(img, spec.file) +end + +-- ============ 双子鸟:16×16 × 2 帧(闭嘴/张嘴) ============ +local function paintBird(c, open) + local B = C("6ab0e8") + local BD = C("4a90c8") + c.rect(4, 5, 8, 7, B) -- 身体 + c.rect(8, 2, 6, 6, B) -- 头 + c.px(11, 4, PAL.black) -- 眼 + c.rect(2, 6, 3, 4, BD) -- 尾 + c.rect(5, 7, 4, 3, BD) -- 翅 + if open then + c.px(14, 5, PAL.yellow); c.px(15, 4, PAL.yellow); c.px(15, 6, PAL.yellow) + else + c.px(14, 5, PAL.yellow); c.px(15, 5, PAL.yellow) + end + c.vline(7, 12, 2, PAL.yellow); c.vline(10, 12, 2, PAL.yellow) -- 脚 +end +local birdImg = Image(32, 16, ColorMode.RGB) +paintBird(makeCtx(birdImg, 0, 0), false) +paintBird(makeCtx(birdImg, 16, 0), true) +saveSheet(birdImg, "bird") + +-- ============ 灰麻雀(E3 结局,一只真的鸟):16×16 ============ +local function paintSparrow(c) + local G = C("9a9088") + local GD = C("6e6660") + c.rect(4, 6, 8, 6, G) + c.rect(8, 3, 6, 5, G) + c.px(11, 5, PAL.black) + c.rect(2, 7, 3, 3, GD) + c.rect(5, 8, 4, 2, GD) + c.px(14, 6, C("7a6a58")); c.px(15, 6, C("7a6a58")) + c.vline(7, 12, 2, GD); c.vline(10, 12, 2, GD) +end +local sparrowImg = Image(16, 16, ColorMode.RGB) +paintSparrow(makeCtx(sparrowImg, 0, 0)) +saveSheet(sparrowImg, "sparrow") + +-- ============ 群演 ×5:16×16 × 2 帧循环 ============ +local function paintCritter(c, spec, f) + local bob = f -- 第二帧整体下移 1px 形成循环 + c.rect(4, 6 + bob, 8, 6 - bob, spec.body) + c.rect(5, 2 + bob, 6, 5, spec.body) + c.px(6, 4 + bob, PAL.black); c.px(9, 4 + bob, PAL.black) + c.px(7, 6 + bob, spec.snout); c.px(8, 6 + bob, spec.snout) + c.rect(4, 12, 3, 2, spec.dark); c.rect(9, 12, 3, 2, spec.dark) + if spec.extra then spec.extra(c, f) end +end +local CRITTERS = { + { file = "extra1", body = C("e0904a"), dark = C("b87030"), snout = C("f8c890"), -- 松鼠:大尾巴 + extra = function(c, f) c.rect(12, 1 + f, 3, 9, C("b87030")); c.px(13, 2 + f, C("e0904a")) end }, + { file = "extra2", body = C("b09068"), dark = C("8a6a48"), snout = C("e8cca8"), -- 刺猬:背刺 + extra = function(c, f) for i = 4, 11, 2 do c.px(i, 1 + f + (i % 4 == 0 and 0 or 1), C("6e5236")) end end }, + { file = "extra3", body = C("f4e04a"), dark = C("d8b830"), snout = C("f4a030"), -- 鸭子:扁嘴 + extra = function(c, f) c.px(6, 5 + f, C("f4a030")); c.px(9, 5 + f, C("f4a030")) end }, + { file = "extra4", body = C("b8b8c4"), dark = C("8e8e9c"), snout = C("e8c8c8"), -- 老鼠:圆耳 + extra = function(c, f) c.rect(4, 0 + f, 2, 2, C("b8b8c4")); c.rect(10, 0 + f, 2, 2, C("b8b8c4")) end }, + { file = "extra5", body = C("7ac86a"), dark = C("58a048"), snout = C("aee89e"), -- 青蛙:凸眼 + extra = function(c, f) c.px(5, 1 + f, C("7ac86a")); c.px(10, 1 + f, C("7ac86a")) end }, +} +for _, spec in ipairs(CRITTERS) do + local img = Image(32, 16, ColorMode.RGB) + paintCritter(makeCtx(img, 0, 0), spec, 0) + paintCritter(makeCtx(img, 16, 0), spec, 1) + saveSheet(img, spec.file) +end + +-- ============ 向日葵表盘:24×24 × 14 帧 ============ +-- 帧 0..12 = 点亮花瓣数;帧 13 = 满格+花心黑籽 +local function paintSunflower(c, lit, seeds) + local cx, cy = 11.5, 11.5 + for k = 0, 11 do + local ang = (k / 12) * math.pi * 2 - math.pi / 2 + local col = (k < lit) and PAL.yellow or C("7a7264") + local colD = (k < lit) and C("e0b830") or C("635c50") + for r = 7, 10 do + local x = math.floor(cx + math.cos(ang) * r + 0.5) + local y = math.floor(cy + math.sin(ang) * r + 0.5) + c.px(x, y, r > 9 and colD or col) + local px2 = math.floor(cx + math.cos(ang + 0.14) * r + 0.5) + local py2 = math.floor(cy + math.sin(ang + 0.14) * r + 0.5) + c.px(px2, py2, col) + end + end + -- 花心 + for j = -4, 4 do for i = -4, 4 do + if i * i + j * j <= 18 then c.px(12 + i, 12 + j, C("9a6a2a")) end + end end + for j = -3, 3 do for i = -3, 3 do + if i * i + j * j <= 9 then c.px(12 + i, 12 + j, C("b8843a")) end + end end + if seeds then -- 过熟的黑籽 + c.px(11, 11, PAL.black); c.px(13, 12, PAL.black); c.px(12, 14, PAL.black) + c.px(10, 13, PAL.black); c.px(14, 10, PAL.black); c.px(12, 11, C("4a3a20")) + end +end +local sunImg = Image(24 * 14, 24, ColorMode.RGB) +for f = 0, 12 do paintSunflower(makeCtx(sunImg, f * 24, 0), f, false) end +paintSunflower(makeCtx(sunImg, 13 * 24, 0), 12, true) +saveSheet(sunImg, "sunflower") + +print("ALL ART GENERATED OK") diff --git a/Fable-5-High/tools/gen_maps.mjs b/Fable-5-High/tools/gen_maps.mjs new file mode 100644 index 0000000..00181ed --- /dev/null +++ b/Fable-5-High/tools/gen_maps.mjs @@ -0,0 +1,352 @@ +// 《崩坏童话:蜜糖村》10 张地图生成脚本 +// 生成 maps/*.tmx(Tiled 源文件),随后由 Tiled CLI 导出 JSON 到 public/assets/maps/ +// 运行:node tools/gen_maps.mjs +import { writeFileSync, mkdirSync } from "fs"; + +// 瓦片索引(与 tools/gen_art.lua 的 tileset 一一对应),GID = 索引 + 1 +const I = { + grass: 0, grass2: 1, grassWF: 2, grassPF: 3, path: 4, path2: 5, pathEdge: 6, + water1: 7, water2: 8, shore: 9, flowerBush: 10, berryFull: 11, berryPicked: 12, + treeTL: 13, treeTR: 14, treeBL: 15, treeBR: 16, bush: 17, wall: 18, wallWin: 19, + door: 20, roofL: 21, roofR: 22, eave: 23, floorW: 24, floorW2: 25, floorC: 26, + inWall: 27, inWallLow: 28, rug: 29, bedHead: 30, bedFoot: 31, counter: 32, + oven: 33, shelfEmpty: 34, shelfGoods: 35, table: 36, chair: 37, wellRoof: 38, + wellRing: 39, fogLight: 40, fogDense: 41, wWall: 42, wFloor: 43, wDoor: 44, + benchL: 45, benchR: 46, boardL: 47, boardR: 48, mailbox: 49, fenceH: 50, + fencePost: 51, sign: 52, picnic1: 53, picnic2: 54, bedBloom: 55, dirt: 56, + sprout: 57, bunting: 58, lantern: 59, sparkle: 60, winIn: 61, wBedHead: 62, + wBedFoot: 63, desk: 64, calendar: 65, counterPaper: 66, crate: 67, basket: 68, + bedDry: 69, wWin: 70, doormat: 71, tallGrass: 72, plazaBrick: 73, wCurtain: 74, + monitor: 75, stoneWall: 76, fogGrass: 77, black: 78, wBlank: 79, +}; + +class M { + constructor(name, w, h) { + this.name = name; this.w = w; this.h = h; + this.ground = new Array(w * h).fill(0); + this.obst = new Array(w * h).fill(0); + this.objects = []; this.oid = 1; + } + g(x, y, t) { this.ground[y * this.w + x] = t + 1; } + o(x, y, t) { this.obst[y * this.w + x] = t + 1; } + gFill(t) { for (let i = 0; i < this.ground.length; i++) this.ground[i] = t + 1; } + gRect(x, y, w, h, t) { for (let j = y; j < y + h; j++) for (let i = x; i < x + w; i++) this.g(i, j, t); } + oRect(x, y, w, h, t) { for (let j = y; j < y + h; j++) for (let i = x; i < x + w; i++) this.o(i, j, t); } + // 装饰草:确定性伪随机 + grassDecor() { + for (let j = 0; j < this.h; j++) for (let i = 0; i < this.w; i++) { + const r = (i * 73 + j * 149 + i * j * 7) % 23; + if (this.ground[j * this.w + i] === I.grass + 1) { + if (r === 0) this.g(i, j, I.grassWF); else if (r === 1) this.g(i, j, I.grassPF); else if (r < 6) this.g(i, j, I.grass2); + } + } + } + tree(x, y) { this.o(x, y, I.treeTL); this.o(x + 1, y, I.treeTR); this.o(x, y + 1, I.treeBL); this.o(x + 1, y + 1, I.treeBR); } + // 4 宽 ×3 高的房屋立面,door 在底行 dx 处 + facade(x, y, dx) { + this.o(x, y, I.roofL); this.o(x + 1, y, I.roofL); this.o(x + 2, y, I.roofR); this.o(x + 3, y, I.roofR); + for (let i = 0; i < 4; i++) this.o(x + i, y + 1, I.eave); + for (let i = 0; i < 4; i++) this.o(x + i, y + 2, i % 3 === 1 ? I.wallWin : I.wall); + this.obst[(y + 2) * this.w + (x + dx)] = 0; // 门位可走 + this.g(x + dx, y + 2, I.door); + this.g(x + dx, y + 3, I.doormat); + } + obj(name, type, tx, ty, opts = {}) { + const o = { id: this.oid++, name, type, x: tx * 16, y: ty * 16, w: (opts.w ?? 0) * 16, h: (opts.h ?? 0) * 16, props: opts.props ?? {} }; + this.objects.push(o); return o; + } + spawn(name, tx, ty) { this.obj(name, "spawn", tx + 0.5, ty + 0.5); } + door(tx, ty, w, h, target, spawn) { this.obj("to_" + target, "door", tx, ty, { w, h, props: { target, spawn } }); } + npc(name, tx, ty, props = {}) { this.obj(name, "npc", tx + 0.5, ty + 0.5, { props }); } + poi(name, tx, ty, props = {}) { this.obj(name, "poi", tx + 0.5, ty + 0.5, { props }); } + + tmx() { + const csv = a => { let out = []; for (let j = 0; j < this.h; j++) out.push(a.slice(j * this.w, (j + 1) * this.w).join(",")); return out.join(",\n"); }; + const props = p => { + const e = Object.entries(p); if (!e.length) return ""; + return `${e.map(([k, v]) => ``).join("")}`; + }; + const objs = this.objects.map(o => { + const size = o.w || o.h ? ` width="${o.w}" height="${o.h}"` : ""; + const body = props(o.props); + return ` ${body}${o.w || o.h ? "" : ""}`; + }).join("\n"); + return ` + + + + +${csv(this.ground)} + + + + + +${csv(this.obst)} + + + +${objs} + + +`; + } +} + +mkdirSync("maps", { recursive: true }); +mkdirSync("public/assets/maps", { recursive: true }); + +const maps = []; + +// ============ 小满小屋 12×8 ============ +{ + const m = new M("house", 12, 8); + m.gFill(I.floorW); + for (let i = 0; i < 12; i++) { m.o(i, 0, I.inWall); m.o(i, 1, i === 6 ? I.winIn : I.inWallLow); } + m.o(2, 1, I.calendar); + m.o(1, 2, I.bedHead); m.o(1, 3, I.bedFoot); + m.o(7, 4, I.table); m.o(8, 4, I.chair); + m.o(10, 2, I.crate); + m.g(5, 7, I.rug); m.g(6, 7, I.rug); + m.o(0, 2, I.crate); // 角落杂物 + m.spawn("bed", 2, 3); + m.spawn("entry", 5, 6); + m.door(5, 7.5, 2, 0.5, "plaza", "fromHouse"); + m.poi("calendar", 2, 2, { clue: "C1" }); + m.poi("bed", 1, 4); + maps.push(m); +} + +// ============ 村广场 30×20 ============ +{ + const m = new M("plaza", 30, 20); + m.gFill(I.grass); + // 道路 + m.gRect(14, 0, 2, 20, I.path); m.gRect(0, 8, 30, 2, I.path); + for (let i = 0; i < 30; i += 2) m.g(i, 8, I.path2); + for (let j = 1; j < 20; j += 3) m.g(14, j, I.path2); + // 庆典场地 + m.gRect(12, 11, 6, 4, I.plazaBrick); + m.grassDecor(); + // 边界:树与栅栏(留出四个出口) + for (let i = 0; i < 30; i += 2) { if (i < 12 || i > 17) m.tree(i, 0); } + for (let i = 0; i < 30; i += 2) { if (i < 13 || i > 16) { m.o(i, 19, I.fenceH); m.o(i + 1, 19, I.fenceH); } } + for (let j = 2; j < 18; j += 2) { if (j < 7 || j > 10) { m.o(0, j, I.fencePost); m.o(29, j, I.fencePost); } } + // 房屋立面:小屋 / 面包房 / 杂货店 / 诊所 + m.facade(2, 2, 2); m.facade(7, 2, 2); m.facade(19, 2, 2); m.facade(24, 2, 2); + m.poi("sign_house", 1, 5); m.o(1, 5, I.sign); + // 公告板 + m.o(11, 6, I.boardL); m.o(12, 6, I.boardR); + m.poi("board", 11.5, 7.5); + // 邮筒与散落信件(C3) + m.o(17, 1, I.mailbox); + m.poi("letters", 17.5, 2.5, { clue: "C3" }); + // 双子鸟树 + m.tree(20, 6); + m.poi("birdtree", 21, 8.5); + // 花坛 ×3(T2 浇水) + m.o(7, 11, I.bedDry); m.o(7, 13, I.bedDry); m.o(7, 15, I.bedDry); + m.poi("flowerbed1", 7.5, 12); m.poi("flowerbed2", 7.5, 14); m.poi("flowerbed3", 7.5, 16); + // 庆典灯笼杆 ×4(T13 挂彩带) + m.o(11, 10, I.lantern); m.o(18, 10, I.lantern); m.o(11, 15, I.lantern); m.o(18, 15, I.lantern); + m.poi("ribbon1", 11.5, 11.2); m.poi("ribbon2", 18.5, 11.2); m.poi("ribbon3", 11.5, 16.2); m.poi("ribbon4", 18.5, 16.2); + m.poi("festival", 15, 13); + // 装饰 + m.o(4, 12, I.flowerBush); m.o(23, 12, I.flowerBush); m.o(25, 15, I.bush); m.o(3, 16, I.bush); + m.tree(26, 16); m.tree(21, 17); + // NPC + m.npc("afu", 12.5, 8.5); + m.npc("dabao", 16.5, 3.5); + m.npc("bird1", 20.4, 6.3); m.npc("bird2", 21.6, 6.3); + m.npc("extra1", 5, 10, { patrol: 40 }); m.npc("extra2", 22, 9, { patrol: 56 }); + m.npc("extra3", 24, 17, { patrol: 40 }); m.npc("extra4", 5, 17, { patrol: 24 }); + m.npc("extra5", 22, 17, { patrol: 16 }); + // 出生点 + m.spawn("fromHouse", 4, 6.2); m.spawn("fromBakery", 9, 6.2); m.spawn("fromStore", 21, 6.2); m.spawn("fromClinic", 26, 6.2); + m.spawn("fromLake", 1.6, 9); m.spawn("fromField", 28.4, 9); m.spawn("fromWell", 14.5, 1.6); m.spawn("fromFog", 14.5, 17.2); + m.spawn("center", 15, 12); + // 门与出口 + m.door(4, 4.2, 1, 0.8, "house", "entry"); + m.door(9, 4.2, 1, 0.8, "bakery", "entry"); + m.door(21, 4.2, 1, 0.8, "store", "entry"); + m.door(26, 4.2, 1, 0.8, "clinic", "entry"); + m.door(0, 7, 0.6, 4, "lake", "entry"); + m.door(29.4, 7, 0.6, 4, "field", "entry"); + m.door(12.5, 0, 5, 0.7, "well", "entry"); + m.door(12.5, 19.3, 5, 0.7, "fogwall", "entry"); + maps.push(m); +} + +// ============ 面包房 15×10 ============ +{ + const m = new M("bakery", 15, 10); + m.gFill(I.floorW); + for (let i = 0; i < 15; i++) { m.o(i, 0, I.inWall); m.o(i, 1, (i === 4 || i === 10) ? I.winIn : I.inWallLow); } + m.o(12, 1, I.oven); + for (let i = 2; i < 7; i++) m.o(i, 3, I.counter); + m.o(7, 3, I.counterPaper); + m.o(1, 5, I.basket); m.o(1, 7, I.crate); m.o(13, 5, I.crate); + m.o(10, 5, I.table); m.o(11, 5, I.chair); + m.g(7, 9, I.rug); m.g(8, 9, I.rug); + m.npc("yuanyuan", 4.5, 2.6); + m.poi("recipe", 7.5, 4.2, { clue: "C2" }); + m.poi("knead", 10.5, 6.2); + m.spawn("entry", 7.5, 8); + m.door(7, 9.5, 2, 0.5, "plaza", "fromBakery"); + maps.push(m); +} + +// ============ 杂货店 15×10 ============ +{ + const m = new M("store", 15, 10); + m.gFill(I.floorW2); + for (let i = 0; i < 15; i++) { m.o(i, 0, I.inWall); m.o(i, 1, i === 12 ? I.winIn : I.inWallLow); } + for (let i = 2; i < 11; i++) m.o(i, 2, I.shelfGoods); + m.o(13, 4, I.shelfGoods); m.o(13, 5, I.shelfGoods); m.o(13, 6, I.shelfGoods); + m.o(3, 5, I.counter); m.o(4, 5, I.counter); + m.o(1, 7, I.crate); m.o(11, 8, I.basket); + m.g(7, 9, I.rug); m.g(8, 9, I.rug); + m.npc("mimi", 3.5, 4.4); + m.poi("shelfA", 3.5, 3.2); m.poi("shelfB", 6.5, 3.2); m.poi("shelfC", 9.5, 3.2); + m.poi("shelf_c5", 12.4, 5.5, { clue: "C5" }); + m.spawn("entry", 7.5, 8); + m.door(7, 9.5, 2, 0.5, "plaza", "fromStore"); + maps.push(m); +} + +// ============ 诊所 12×8 ============ +{ + const m = new M("clinic", 12, 8); + m.gFill(I.floorC); + for (let i = 0; i < 12; i++) { m.o(i, 0, I.wWall); m.o(i, 1, i === 3 ? I.wWin : I.inWallLow); } + m.o(5, 3, I.desk); + m.o(9, 2, I.wBedHead); m.o(9, 3, I.wBedFoot); + m.o(8, 1, I.wCurtain); + m.o(1, 2, I.crate); + m.g(5, 7, I.rug); m.g(6, 7, I.rug); + m.npc("fu", 6.5, 2.6); + m.poi("chart", 4.5, 4.2, { clue: "C8" }); + m.spawn("entry", 5.5, 6); + m.door(5, 7.5, 2, 0.5, "plaza", "fromClinic"); + maps.push(m); +} + +// ============ 湖畔 24×16 ============ +{ + const m = new M("lake", 24, 16); + m.gFill(I.grass); + for (let i = 0; i < 24; i++) { + m.g(i, 9, I.shore); + for (let j = 10; j < 16; j++) m.g(i, j, (i * 31 + j * 17) % 7 === 0 ? I.sparkle : ((i + j) % 2 ? I.water1 : I.water2)); + } + m.oRect(0, 10, 24, 6, I.water1); // 水面不可走(碰撞用,绘制层在 ground) + for (let j = 10; j < 16; j++) for (let i = 0; i < 24; i++) m.obst[j * m.w + i] = m.ground[j * m.w + i]; // 与水面同贴图 + m.grassDecor(); + for (let i = 0; i < 24; i += 2) m.tree(i, 0); + for (let j = 2; j < 8; j += 2) { m.o(0, j, I.fencePost); } + m.tree(2, 3); m.tree(11, 2); m.o(6, 4, I.flowerBush); + // 长椅(龟爷爷) + m.o(16, 5, I.benchL); m.o(17, 5, I.benchR); + m.npc("gui", 18.9, 5.5); + m.poi("bench", 16.5, 6.5); + // 野餐布 + m.g(7, 6, I.picnic1); m.g(8, 6, I.picnic2); m.g(7, 7, I.picnic2); m.g(8, 7, I.picnic1); + m.poi("picnic", 8, 7); + m.spawn("entry", 22, 7); + m.spawn("picnicSpot", 8, 7.6); + m.door(23.4, 5, 0.6, 5, "plaza", "fromLake"); + maps.push(m); +} + +// ============ 花田 20×14 ============ +{ + const m = new M("field", 20, 14); + m.gFill(I.grass); + m.gRect(3, 3, 14, 8, I.dirt); + for (let j = 3; j < 11; j++) for (let i = 3; i < 17; i++) if ((i + j) % 3 === 0) m.g(i, j, I.sprout); + m.grassDecor(); + for (let i = 0; i < 20; i += 2) { m.tree(i, 0); if (i < 4 || i > 8) { m.o(i, 13, I.fenceH); m.o(i + 1, 13, I.fenceH); } } + for (let j = 2; j < 12; j += 2) { if (j < 5 || j > 8) m.o(19, j, I.fencePost); } + // 莓果丛 ×5(T6) + const berries = [[4, 4], [8, 5], [12, 4], [15, 7], [6, 9]]; + berries.forEach(([x, y], k) => { m.o(x, y, I.berryFull); m.poi("berry" + (k + 1), x + 0.5, y + 1.5); }); + // 浇花点 + m.o(3, 11, I.bedDry); + m.poi("fieldwater", 3.5, 12.2); + m.o(17, 11, I.flowerBush); m.o(2, 2, I.bush); + m.spawn("entry", 1.6, 6); + m.door(0, 4.5, 0.6, 5, "plaza", "fromField"); + maps.push(m); +} + +// ============ 古井空地 14×10 ============ +{ + const m = new M("well", 14, 10); + m.gFill(I.grass); + m.gRect(6, 6, 2, 4, I.path); + m.grassDecor(); + for (let i = 0; i < 14; i += 2) m.tree(i, 0); + for (let j = 2; j < 9; j += 2) { m.o(0, j, I.fencePost); m.o(13, j, I.fencePost); } + for (let i = 0; i < 14; i += 2) { if (i < 5 || i > 8) { m.o(i, 9, I.fenceH); m.o(i + 1, 9, I.fenceH); } } + m.o(2, 4, I.tallGrass); m.o(3, 4, I.tallGrass); m.o(11, 5, I.tallGrass); m.o(11, 3, I.bush) + m.o(6, 3, I.wellRoof); m.o(6, 4, I.wellRing); + m.o(9, 4, I.sign); + m.poi("well", 6.5, 5.5, { clue: "C7" }); + m.spawn("entry", 6.5, 7.6); + m.door(4.5, 9.3, 5, 0.7, "plaza", "fromWell"); + maps.push(m); +} + +// ============ 雾墙边界 20×8 ============ +{ + const m = new M("fogwall", 20, 8); + m.gFill(I.grass); + for (let i = 0; i < 20; i++) { + m.g(i, 4, I.fogGrass); + m.g(i, 5, I.fogLight); + m.g(i, 6, I.fogDense); m.g(i, 7, I.fogDense); + m.o(i, 6, I.fogDense); m.o(i, 7, I.fogDense); + } + m.grassDecor(); + for (let j = 0; j < 6; j += 2) { m.o(0, j, I.fencePost); m.o(19, j, I.fencePost); } + m.tree(2, 1); m.tree(16, 1); + m.o(4, 3, I.sign); + m.poi("fog", 10, 5.5); + m.spawn("entry", 10, 2.4); + m.door(7.5, 0, 5, 0.7, "plaza", "fromFog"); + maps.push(m); +} + +// ============ 白色层·走廊 20×6 ============ +{ + const m = new M("white_corridor", 20, 6); + m.gFill(I.wFloor); + for (let i = 0; i < 20; i++) m.o(i, 0, (i % 5 === 2) ? I.wWin : I.wWall); + m.o(6, 0, I.wDoor); m.o(13, 0, I.wDoor); + m.spawn("entry", 1, 3); + m.door(19.5, 0, 0.5, 6, "white_ward", "entry"); + maps.push(m); +} + +// ============ 白色层·病房 10×8 ============ +{ + const m = new M("white_ward", 10, 8); + m.gFill(I.wFloor); + for (let i = 0; i < 10; i++) m.o(i, 0, i === 6 ? I.wWin : I.wWall); + m.o(2, 0, I.wCurtain); + m.o(4, 2, I.wBedHead); m.o(4, 3, I.wBedFoot); + m.o(6, 2, I.monitor); + m.poi("chart_ward", 5, 4.5); + m.spawn("entry", 1, 5); + maps.push(m); +} + +// tiles.tsx +writeFileSync("maps/tiles.tsx", ` + + + +`); + +for (const m of maps) writeFileSync(`maps/${m.name}.tmx`, m.tmx()); +console.log("TMX written:", maps.map(m => m.name).join(", ")); diff --git a/Fable-5-High/tsconfig.json b/Fable-5-High/tsconfig.json new file mode 100644 index 0000000..d4e68d8 --- /dev/null +++ b/Fable-5-High/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": false, + "skipLibCheck": true, + "esModuleInterop": true, + "lib": ["ES2020", "DOM"], + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/GLM5.2/Dockerfile b/GLM5.2/Dockerfile new file mode 100644 index 0000000..b7fe45c --- /dev/null +++ b/GLM5.2/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-alpine AS build +WORKDIR /app +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1 +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npx vite build +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html diff --git a/GLM5.2/PRD.md b/GLM5.2/PRD.md new file mode 100644 index 0000000..0f5ea47 --- /dev/null +++ b/GLM5.2/PRD.md @@ -0,0 +1,226 @@ +# PRD:《崩坏童话:蜜糖村》网页版 + +> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局 + +## 1. 游戏理念与设计支柱 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。 + +四条设计支柱,所有实现争议回到这里裁决: + +1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。 +2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。 +3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。 +4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。 + +## 2. 世界观:三层结构 + +| 层 | 名称 | 玩家所见 | 实质 | +|---|---|---|---| +| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 | +| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" | +| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 | + +命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。 + +## 3. 主角设定 + +- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。 +- 性别中性像素小人,16×24 像素/帧,无内心独白。 +- 对话头像永远同一帧微笑(见支柱 2)。 +- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。 + +## 4. NPC 清单(封闭,7 组 + 群演) + +每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。 + +| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 | +|---|---|---|---|---|---| +| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 | +| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) | +| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 | +| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) | +| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 | +| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 | +| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 | +| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — | + +## 5. 七日故事线 + +每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。 + +| 日 | 主题 | 剧情要点 | 侵蚀预期 | +|---|---|---|---| +| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 | +| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 | +| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 | +| Day 4 | 完美的重复 | 任务 T8–T10 与 Day 1 的 T1–T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 | +| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 | +| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 | +| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 | + +阿福每日公告(逐字实现,注意措辞逐日滑移): + +- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」 +- D2「早上好呀!新的一天,新的幸福。祝你开心!」 +- D3「早上好!今天要把幸福装得满满的哦。要开心!」 +- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同) +- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」 +- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」 +- D7「庆典开始了。来吧。」 + +## 6. 核心系统 + +### 6.1 幸福值(明线) + +- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。 +- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。 +- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。 +- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。 +- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。 + +### 6.2 觉知值(暗线,永不显示) + +- 范围 0–100,开局 0。只升不降。 +- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。 + +### 6.3 悲伤行为(封闭清单,5 项) + +拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。 + +### 6.4 侵蚀阶段矩阵(唯一真源) + +阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。 + +| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5) | UI | +|---|---|---|---|---|---|---| +| S0 | 0–20 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 0–2 瓣 | +| S1 | 21–50 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 3–6 瓣 | +| S2 | 51–80 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 | +| S3 | 81–99 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 | +| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 | + +NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。 + +### 6.5 文本腐蚀 + +同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。 + +### 6.6 BGM 与音效 + +- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。 +- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。 +- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。 +- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。 + +## 7. 任务清单(封闭,15 个) + +| ID | 日 | 任务 | 幸福奖励 | 备注 | +|---|---|---|---|---| +| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 | +| T2 | D1 | 给广场花坛浇水 ×3 | +10 | | +| T3 | D1 | 与任意三位村民问好 | +10 | | +| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) | +| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) | +| T6 | D3 | 去花田采莓果 ×5 | +10 | | +| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 | +| T8–T10 | D4 | 与 T1–T3 逐字相同 | 同上 | 刻意恐怖谷 | +| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | | +| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) | +| T13 | D6 | 在广场挂彩带 ×4 | +10 | | +| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 | +| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 | + +## 8. 线索清单(封闭,9 处,每处觉知 +8) + +| ID | 位置/来源 | 内容 | 可发现时段 | +|---|---|---|---| +| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 | +| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 | +| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 | +| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 | +| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 | +| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5(T12 中) | +| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 | +| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 | +| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 | + +## 9. 结局(封闭,4 个) + +Day 7 庆典按以下优先级判定(自上而下第一个满足者生效): + +| 结局 | 条件 | 演出 | +|---|---|---| +| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 | +| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 | +| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 | +| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 | + +任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。 + +## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON) + +| 场景 | 尺寸(瓦片) | 要点 | +|---|---|---| +| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) | +| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 | +| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) | +| 杂货店(室内) | 15×10 | 货架(C5) | +| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) | +| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 | +| 花田 | 20×14 | 莓果丛(T6)、浇花点 | +| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 | +| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 | +| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 | + +统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。 + +## 11. 美术规范(Aseprite 制作,全部原创) + +- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。 +- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。 +- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。 +- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。 +- 禁止使用任何现成素材、素材包或开源 tileset。 + +## 12. 技术架构 + +- **Phaser 3 + TypeScript + Vite**,`npm run build` 产出纯静态 `dist/`,`npm run preview` 本地游玩。 +- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。 +- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`。 +- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。 +- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。 +- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。 +- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。 +- 无 localStorage、无外部网络请求、无外部音频/字体文件。 + +## 13. Debug 面板(服务验收,必须实现) + +按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。 + +## 14. 验收清单(共 20 条) + +1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。 +2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。 +3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。 +4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。 +5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。 +6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。 +7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。 +8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。 +9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。 +10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。 +11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。 +12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。 +13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。 +14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。 +15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。 +16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。 +17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。 +18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。 +19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。 +20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。 + +## 15. 明确不做 + +战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。 diff --git a/GLM5.2/README.md b/GLM5.2/README.md new file mode 100644 index 0000000..7a33f52 --- /dev/null +++ b/GLM5.2/README.md @@ -0,0 +1,126 @@ +# 崩坏童话:蜜糖村(网页版) + +> 纯单机网页游戏 · Phaser 3 + TypeScript + Vite · 无后端、无存档、无 localStorage,浏览器打开即玩。 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。 + +## 启动方式 + +需要 Node.js 18+。 + +```bash +npm install # 安装依赖 +npm run dev # 开发模式(http://localhost:5173) +npm run build # 产出纯静态 dist/ +npm run preview # 本地预览构建产物(http://localhost:4173) +``` + +`npm run build` 产出纯静态 `dist/`,可直接用任意静态服务器托管;`npm run preview` 本地游玩。 + +## 按键操作 + +| 按键 | 功能 | +|---|---| +| `W` `A` `S` `D` / 方向键 | 移动(四向动画 + 待机帧) | +| `E` / `空格` / `回车` | 交互 / 推进对话 / 选择 | +| `1` `2` `3` | 对话选项(温柔 / 迟疑 / 否定) | +| `` ` ``(反引号) | 开关 Debug 面板 | + +## Debug 面板(服务验收) + +运行中按反引号键 `` ` `` 开关。默认关闭,关闭时无任何 UI 痕迹,不影响正常游玩。 + +功能: +- 直接设置幸福值(10 / 40 / 70 / 90 / 100,分别对应 S0/S1/S2/S3/S4) +- 切换觉知值(0 / 50 / 100) +- 切换当前日(1–7 循环) +- 置齐全部线索(C1–C9) +- 传送到任意场景(小屋/广场/面包房/杂货店/诊所/湖畔/花田/古井/雾墙) +- 强制触发四个结局之一(E1 糖衣 / E2 回收 / E3 苏醒 / E4 静默) + +## 游玩说明 + +- 开局输入名字(留空默认「小满」)。 +- 七日流程:晨起 → 公告板接任务 → 完成任务/自由活动 → 黄昏钟声 → 回屋睡觉 → 梦境过场 → 次日。 +- 完整流程约 25–35 分钟,含 4 个结局。 +- 幸福值以左上角向日葵表盘表示(12 片花瓣),全程不显示数字。满格后花心浮现黑籽。 + +## 技术架构(PRD §12) + +- **Phaser 3 + TypeScript + Vite**。逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。 +- 场景:`BootScene`(加载)→ `TitleScene`(标题/输入名字)→ 各地图 Scene + 常驻 `UIScene`(对话框、向日葵表盘、提示)→ `EndingScene`;另 `DebugScene`。 +- `CorruptionDirector`(`src/systems/CorruptionDirector.ts`):普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的**唯一真源**。 +- 饱和度与晕影用 Phaser 相机 postFX(`SaturationPostFX`),负片闪帧用单帧白色覆盖切换。 +- BGM 与音效按 §6.6 用 Web Audio 程序合成(方波主旋律 + 三角波低音 8 小节循环),playbackRate 与 BiquadFilter 低通由 `CorruptionDirector` 驱动。**无外部音频文件**。 +- 无 localStorage、无外部网络请求、无外部音频/字体文件。 + +## 素材与地图制作流程(全部本项目内自制,无外部资产) + +### 像素素材(Aseprite) + +所有像素素材由 Aseprite 命令行 + Lua 脚本程序化绘制,脚本位于 `tools/`: + +| 脚本 | 产物 | 说明 | +|---|---|---| +| `tools/gen_tileset.lua` | `assets/tilesets/tileset.png`(128×128,64 个 16×16 瓦片) | 草地/石板路/湖水/花丛/莓果丛/树/房屋墙顶门/室内地板/家具(床/柜台/烤炉/货架/桌椅)/井/雾/白色层墙地等 | +| `tools/gen_player.lua` | `assets/sprites/player.png`(64×96) | 主角 16×24/帧,4 朝向 × 4 帧行走(walk-down/up/left/right),第 1 帧兼待机 | +| `tools/gen_portraits.lua` | `assets/sprites/portraits.png`(624×24) | 对话头像:主角微笑 + E3「真实的脸」;7 个具名 NPC 各 3 档表情(smile/flat/off);双子鸟;5 个群演 | +| `tools/gen_npc.lua` | `assets/sprites/npc.png`(400×24) | NPC 场景立绘 16×24 ×3 档;双子鸟 2 帧张嘴动画;5 个群演 | + +重新生成素材(需本机 Aseprite,路径 `/Applications/Aseprite.app/Contents/MacOS/aseprite`): + +```bash +"/Applications/Aseprite.app/Contents/MacOS/aseprite" -b --script tools/gen_tileset.lua +"/Applications/Aseprite.app/Contents/MacOS/aseprite" -b --script tools/gen_player.lua +"/Applications/Aseprite.app/Contents/MacOS/aseprite" -b --script tools/gen_portraits.lua +"/Applications/Aseprite.app/Contents/MacOS/aseprite" -b --script tools/gen_npc.lua +``` + +### 地图(Tiled Map Editor) + +10 个场景地图为标准 Tiled JSON 格式(`assets/maps/*.json`),由 `tools/gen_maps.cjs` 程序化生成;瓦片集描述符由 `tools/gen_tileset_json.cjs` 生成(`assets/tilesets/tileset.json`,含 `collides` 属性)。 + +统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。 + +| 场景 | 尺寸(瓦片) | 要点 | +|---|---|---| +| 小满小屋(室内) | 12×8 | 床、床头日历(C1) | +| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;枢纽 | +| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) | +| 杂货店(室内) | 15×10 | 货架(C5) | +| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) | +| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影 | +| 花田 | 20×14 | 莓果丛(T6)、浇花点 | +| 古井空地 | 14×10 | 古井(C7) | +| 雾墙边界 | 20×8 | 村庄尽头的雾,E3 入口 | +| 白色层 | 20×14 | 走廊 + 病房,去饱和白灰色板 | + +重新生成地图(需本机 Tiled,路径 `/Applications/Tiled.app/Contents/MacOS/tiled`): + +```bash +node tools/gen_tileset_json.cjs +node tools/gen_maps.cjs +# 可选:用 Tiled 验证导出 +"/Applications/Tiled.app/Contents/MacOS/tiled" --export-map assets/maps/plaza.json /tmp/plaza_verify.json +``` + +## 目录结构 + +``` +. +├── index.html +├── package.json +├── tsconfig.json +├── vite.config.ts +├── assets/ +│ ├── maps/ # 10 个 Tiled JSON 地图 +│ ├── tilesets/ # tileset.png + tileset.json +│ └── sprites/ # player.png / npc.png / portraits.png +├── tools/ # Aseprite Lua + Node 生成脚本 +└── src/ + ├── main.ts # Phaser 配置入口 + ├── data/ # gamedata.ts(全部写死的台词/数值/表格)、mapsMeta.ts + ├── systems/ # CorruptionDirector / AudioEngine / TextCorruptor / StoryDirector + ├── entities/ # Player + └── scenes/ # BootScene / TitleScene / UIScene / MapScene / 10 地图 / EndingScene / DebugScene +``` diff --git a/GLM5.2/assets/maps/bakery.json b/GLM5.2/assets/maps/bakery.json new file mode 100644 index 0000000..3d314a5 --- /dev/null +++ b/GLM5.2/assets/maps/bakery.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":10,"width":15,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":15,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,15,17,17,17,17,17,17,17]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":15,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,21,21,0,11,11,0,0,0,0,0,0,0,0,0,0,21,21,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,20,20,20,20,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,23,0,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,0,11,11,11,11,11,11,11]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":112,"y":144,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_bakery"}]},{"name":"npc_yuanyuan","type":"spawn","x":48,"y":112,"width":16,"height":16,"properties":[]},{"name":"recipe_paper","type":"clue","x":176,"y":80,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C2"}]},{"name":"oven","type":"interact","x":176,"y":32,"width":16,"height":16,"properties":[]},{"name":"knead_point","type":"interact","x":48,"y":96,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/clinic.json b/GLM5.2/assets/maps/clinic.json new file mode 100644 index 0000000..7c05b03 --- /dev/null +++ b/GLM5.2/assets/maps/clinic.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":8,"width":12,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":12,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[12,12,12,12,12,12,12,12,12,12,12,12,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,15,17,17,17,17,17,17]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":12,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[11,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,43,0,11,11,19,0,0,0,0,0,0,24,24,24,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,11,11,11,11,0,11,11,11,11,11,11]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":80,"y":112,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_clinic"}]},{"name":"npc_dr_fu","type":"spawn","x":80,"y":80,"width":16,"height":16,"properties":[]},{"name":"chart_clue","type":"clue","x":144,"y":48,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C8"}]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/field.json b/GLM5.2/assets/maps/field.json new file mode 100644 index 0000000..b495a0f --- /dev/null +++ b/GLM5.2/assets/maps/field.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":14,"width":20,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":20,"height":14,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,7,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,7,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,7,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,7,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":20,"height":14,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":0,"y":112,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_field"}]},{"name":"berry1","type":"interact","x":64,"y":64,"width":16,"height":16,"properties":[]},{"name":"berry2","type":"interact","x":128,"y":48,"width":16,"height":16,"properties":[]},{"name":"berry3","type":"interact","x":192,"y":80,"width":16,"height":16,"properties":[]},{"name":"berry4","type":"interact","x":96,"y":128,"width":16,"height":16,"properties":[]},{"name":"berry5","type":"interact","x":224,"y":144,"width":16,"height":16,"properties":[]},{"name":"water_point","type":"interact","x":32,"y":176,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/fog_boundary.json b/GLM5.2/assets/maps/fog_boundary.json new file mode 100644 index 0000000..977ee22 --- /dev/null +++ b/GLM5.2/assets/maps/fog_boundary.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":8,"width":20,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":20,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":20,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":0,"y":96,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_fog"}]},{"name":"fog_wall","type":"interact","x":192,"y":64,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"fog"}]},{"name":"fog_exit","type":"interact","x":208,"y":64,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"E3_exit"}]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/house.json b/GLM5.2/assets/maps/house.json new file mode 100644 index 0000000..7e27649 --- /dev/null +++ b/GLM5.2/assets/maps/house.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":8,"width":12,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":12,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[12,12,12,12,12,12,12,12,12,12,12,12,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,61,61,61,61,61,15,61,61,61,61,61,61]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":12,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[11,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,19,19,19,0,0,0,0,0,0,0,11,11,19,19,19,0,0,0,0,0,0,0,11,11,11,11,11,11,0,0,11,11,11,11,11]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":80,"y":112,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_house"}]},{"name":"bed","type":"interact","x":16,"y":80,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"none"}]},{"name":"calendar","type":"clue","x":32,"y":0,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C1"}]},{"name":"sleep_point","type":"interact","x":96,"y":80,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/lake.json b/GLM5.2/assets/maps/lake.json new file mode 100644 index 0000000..af67aac --- /dev/null +++ b/GLM5.2/assets/maps/lake.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":16,"width":24,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":24,"height":16,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,39,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,38,38,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":24,"height":16,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":368,"y":128,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_lake"}]},{"name":"npc_grandpa","type":"spawn","x":208,"y":160,"width":16,"height":16,"properties":[]},{"name":"picnic_point","type":"interact","x":256,"y":176,"width":16,"height":16,"properties":[]},{"name":"lake_reflection","type":"interact","x":112,"y":112,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/plaza.json b/GLM5.2/assets/maps/plaza.json new file mode 100644 index 0000000..d12c09b --- /dev/null +++ b/GLM5.2/assets/maps/plaza.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":20,"width":30,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":30,"height":20,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,1,1,4,4,4,4,1,1,1,1,1,1,1,2,9,1,1,1,1,1,1,1,5,5,5,5,1,1,1,1,1,1,4,4,4,4,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,5,5,5,5,1,1,1,1,1,1,4,4,4,4,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,5,5,5,5,1,1,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,46,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,46,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":30,"height":20,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40,40,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_house","type":"door","x":224,"y":304,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"house"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_bakery","type":"door","x":0,"y":128,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"bakery"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_shop","type":"door","x":464,"y":128,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"shop"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_clinic","type":"door","x":224,"y":0,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"clinic"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_lake","type":"door","x":0,"y":192,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"lake"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_field","type":"door","x":464,"y":192,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"field"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_well","type":"door","x":464,"y":80,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"well_area"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_fog","type":"door","x":0,"y":48,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"fog_boundary"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"npc_afu","type":"spawn","x":128,"y":128,"width":16,"height":16,"properties":[]},{"name":"npc_bird","type":"spawn","x":240,"y":32,"width":16,"height":16,"properties":[]},{"name":"board","type":"interact","x":112,"y":112,"width":16,"height":16,"properties":[]},{"name":"waterflower1","type":"interact","x":64,"y":64,"width":16,"height":16,"properties":[]},{"name":"waterflower2","type":"interact","x":80,"y":64,"width":16,"height":16,"properties":[]},{"name":"waterflower3","type":"interact","x":64,"y":80,"width":16,"height":16,"properties":[]},{"name":"greet_point","type":"interact","x":240,"y":160,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/shop.json b/GLM5.2/assets/maps/shop.json new file mode 100644 index 0000000..43cc18c --- /dev/null +++ b/GLM5.2/assets/maps/shop.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":10,"width":15,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":15,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,15,17,17,17,17,17,17,17]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":15,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,23,23,23,0,0,0,0,0,23,23,23,0,11,11,0,23,23,23,0,0,0,0,0,23,23,23,0,11,11,0,23,23,23,0,0,0,0,0,23,23,23,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,20,20,20,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,0,11,11,11,11,11,11,11]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":112,"y":144,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_shop"}]},{"name":"npc_mimi","type":"spawn","x":112,"y":112,"width":16,"height":16,"properties":[]},{"name":"shelf_clue","type":"clue","x":32,"y":32,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C5"}]},{"name":"count_shelf","type":"interact","x":32,"y":32,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/well_area.json b/GLM5.2/assets/maps/well_area.json new file mode 100644 index 0000000..a4b0cc1 --- /dev/null +++ b/GLM5.2/assets/maps/well_area.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":10,"width":14,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":14,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,27,27,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":14,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":0,"y":128,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_well"}]},{"name":"well","type":"interact","x":96,"y":64,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C7"}]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/maps/white_layer.json b/GLM5.2/assets/maps/white_layer.json new file mode 100644 index 0000000..0762de4 --- /dev/null +++ b/GLM5.2/assets/maps/white_layer.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":14,"width":20,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":20,"height":14,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":20,"height":14,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,30,30,30,30,30,30,30,30,30,30,0,0,0,0,0,0,0,0,0,0,30,33,0,0,0,0,0,0,34,30,0,0,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,0,0,30,0,0,0,35,0,0,0,0,30,0,0,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,0,0,30,30,30,30,0,30,30,30,30,30,0,0,0,0,0]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"exit","type":"door","x":144,"y":192,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"ending"}]},{"name":"patient_chart","type":"interact","x":144,"y":160,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"E3_chart"}]}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/sprites/npc.png b/GLM5.2/assets/sprites/npc.png new file mode 100644 index 0000000..ab953a9 Binary files /dev/null and b/GLM5.2/assets/sprites/npc.png differ diff --git a/GLM5.2/assets/sprites/player.png b/GLM5.2/assets/sprites/player.png new file mode 100644 index 0000000..a0b6d6f Binary files /dev/null and b/GLM5.2/assets/sprites/player.png differ diff --git a/GLM5.2/assets/sprites/portraits.png b/GLM5.2/assets/sprites/portraits.png new file mode 100644 index 0000000..e646bb8 Binary files /dev/null and b/GLM5.2/assets/sprites/portraits.png differ diff --git a/GLM5.2/assets/tilesets/tileset.json b/GLM5.2/assets/tilesets/tileset.json new file mode 100644 index 0000000..d13e6c3 --- /dev/null +++ b/GLM5.2/assets/tilesets/tileset.json @@ -0,0 +1 @@ +{"columns":8,"image":"tileset.png","imageheight":128,"imagewidth":128,"margin":0,"spacing":0,"name":"tileset","tilecount":64,"tiledversion":"1.12.2","tileheight":16,"tilewidth":16,"type":"tileset","version":"1.10","tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]} \ No newline at end of file diff --git a/GLM5.2/assets/tilesets/tileset.png b/GLM5.2/assets/tilesets/tileset.png new file mode 100644 index 0000000..04a9a88 Binary files /dev/null and b/GLM5.2/assets/tilesets/tileset.png differ diff --git a/GLM5.2/index.html b/GLM5.2/index.html new file mode 100644 index 0000000..62c219a --- /dev/null +++ b/GLM5.2/index.html @@ -0,0 +1,17 @@ + + + + + + 崩坏童话:蜜糖村 + + + +
+ + + diff --git a/GLM5.2/package-lock.json b/GLM5.2/package-lock.json new file mode 100644 index 0000000..898cb2f --- /dev/null +++ b/GLM5.2/package-lock.json @@ -0,0 +1,1107 @@ +{ + "name": "honey-village", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "honey-village", + "version": "1.0.0", + "dependencies": { + "phaser": "^3.80.1" + }, + "devDependencies": { + "playwright": "^1.61.1", + "typescript": "^5.4.0", + "vite": "^5.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/phaser": { + "version": "3.90.0", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", + "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/GLM5.2/package.json b/GLM5.2/package.json new file mode 100644 index 0000000..b8401d6 --- /dev/null +++ b/GLM5.2/package.json @@ -0,0 +1,19 @@ +{ + "name": "honey-village", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --port 4173" + }, + "devDependencies": { + "playwright": "^1.61.1", + "typescript": "^5.4.0", + "vite": "^5.2.0" + }, + "dependencies": { + "phaser": "^3.80.1" + } +} diff --git a/GLM5.2/public/assets/maps/bakery.json b/GLM5.2/public/assets/maps/bakery.json new file mode 100644 index 0000000..3d314a5 --- /dev/null +++ b/GLM5.2/public/assets/maps/bakery.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":10,"width":15,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":15,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,15,17,17,17,17,17,17,17]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":15,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,21,21,0,11,11,0,0,0,0,0,0,0,0,0,0,21,21,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,20,20,20,20,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,23,0,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,0,11,11,11,11,11,11,11]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":112,"y":144,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_bakery"}]},{"name":"npc_yuanyuan","type":"spawn","x":48,"y":112,"width":16,"height":16,"properties":[]},{"name":"recipe_paper","type":"clue","x":176,"y":80,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C2"}]},{"name":"oven","type":"interact","x":176,"y":32,"width":16,"height":16,"properties":[]},{"name":"knead_point","type":"interact","x":48,"y":96,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/clinic.json b/GLM5.2/public/assets/maps/clinic.json new file mode 100644 index 0000000..7c05b03 --- /dev/null +++ b/GLM5.2/public/assets/maps/clinic.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":8,"width":12,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":12,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[12,12,12,12,12,12,12,12,12,12,12,12,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,15,17,17,17,17,17,17]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":12,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[11,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,43,0,11,11,19,0,0,0,0,0,0,24,24,24,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,11,11,11,11,0,11,11,11,11,11,11]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":80,"y":112,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_clinic"}]},{"name":"npc_dr_fu","type":"spawn","x":80,"y":80,"width":16,"height":16,"properties":[]},{"name":"chart_clue","type":"clue","x":144,"y":48,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C8"}]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/field.json b/GLM5.2/public/assets/maps/field.json new file mode 100644 index 0000000..b495a0f --- /dev/null +++ b/GLM5.2/public/assets/maps/field.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":14,"width":20,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":20,"height":14,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,7,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,7,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,7,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,7,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":20,"height":14,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":0,"y":112,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_field"}]},{"name":"berry1","type":"interact","x":64,"y":64,"width":16,"height":16,"properties":[]},{"name":"berry2","type":"interact","x":128,"y":48,"width":16,"height":16,"properties":[]},{"name":"berry3","type":"interact","x":192,"y":80,"width":16,"height":16,"properties":[]},{"name":"berry4","type":"interact","x":96,"y":128,"width":16,"height":16,"properties":[]},{"name":"berry5","type":"interact","x":224,"y":144,"width":16,"height":16,"properties":[]},{"name":"water_point","type":"interact","x":32,"y":176,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/fog_boundary.json b/GLM5.2/public/assets/maps/fog_boundary.json new file mode 100644 index 0000000..977ee22 --- /dev/null +++ b/GLM5.2/public/assets/maps/fog_boundary.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":8,"width":20,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":20,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,1,1,1,1,1,1,1,1,1,1,29,29,29,29,28,28,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":20,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":0,"y":96,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_fog"}]},{"name":"fog_wall","type":"interact","x":192,"y":64,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"fog"}]},{"name":"fog_exit","type":"interact","x":208,"y":64,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"E3_exit"}]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/house.json b/GLM5.2/public/assets/maps/house.json new file mode 100644 index 0000000..7e27649 --- /dev/null +++ b/GLM5.2/public/assets/maps/house.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":8,"width":12,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":12,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[12,12,12,12,12,12,12,12,12,12,12,12,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,61,61,61,61,61,15,61,61,61,61,61,61]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":12,"height":8,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[11,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,11,11,19,19,19,0,0,0,0,0,0,0,11,11,19,19,19,0,0,0,0,0,0,0,11,11,11,11,11,11,0,0,11,11,11,11,11]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":80,"y":112,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_house"}]},{"name":"bed","type":"interact","x":16,"y":80,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"none"}]},{"name":"calendar","type":"clue","x":32,"y":0,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C1"}]},{"name":"sleep_point","type":"interact","x":96,"y":80,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/lake.json b/GLM5.2/public/assets/maps/lake.json new file mode 100644 index 0000000..af67aac --- /dev/null +++ b/GLM5.2/public/assets/maps/lake.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":16,"width":24,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":24,"height":16,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,39,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,38,38,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":24,"height":16,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":368,"y":128,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_lake"}]},{"name":"npc_grandpa","type":"spawn","x":208,"y":160,"width":16,"height":16,"properties":[]},{"name":"picnic_point","type":"interact","x":256,"y":176,"width":16,"height":16,"properties":[]},{"name":"lake_reflection","type":"interact","x":112,"y":112,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/plaza.json b/GLM5.2/public/assets/maps/plaza.json new file mode 100644 index 0000000..d12c09b --- /dev/null +++ b/GLM5.2/public/assets/maps/plaza.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":20,"width":30,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":30,"height":20,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,1,1,4,4,4,4,1,1,1,1,1,1,1,2,9,1,1,1,1,1,1,1,5,5,5,5,1,1,1,1,1,1,4,4,4,4,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,5,5,5,5,1,1,1,1,1,1,4,4,4,4,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,5,5,5,5,1,1,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,46,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,46,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":30,"height":20,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40,40,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_house","type":"door","x":224,"y":304,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"house"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_bakery","type":"door","x":0,"y":128,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"bakery"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_shop","type":"door","x":464,"y":128,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"shop"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_clinic","type":"door","x":224,"y":0,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"clinic"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_lake","type":"door","x":0,"y":192,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"lake"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_field","type":"door","x":464,"y":192,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"field"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_well","type":"door","x":464,"y":80,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"well_area"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"door_fog","type":"door","x":0,"y":48,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"fog_boundary"},{"name":"spawn","type":"string","value":"door_plaza"}]},{"name":"npc_afu","type":"spawn","x":128,"y":128,"width":16,"height":16,"properties":[]},{"name":"npc_bird","type":"spawn","x":240,"y":32,"width":16,"height":16,"properties":[]},{"name":"board","type":"interact","x":112,"y":112,"width":16,"height":16,"properties":[]},{"name":"waterflower1","type":"interact","x":64,"y":64,"width":16,"height":16,"properties":[]},{"name":"waterflower2","type":"interact","x":80,"y":64,"width":16,"height":16,"properties":[]},{"name":"waterflower3","type":"interact","x":64,"y":80,"width":16,"height":16,"properties":[]},{"name":"greet_point","type":"interact","x":240,"y":160,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/shop.json b/GLM5.2/public/assets/maps/shop.json new file mode 100644 index 0000000..43cc18c --- /dev/null +++ b/GLM5.2/public/assets/maps/shop.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":10,"width":15,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":15,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,15,17,17,17,17,17,17,17]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":15,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,23,23,23,0,0,0,0,0,23,23,23,0,11,11,0,23,23,23,0,0,0,0,0,23,23,23,0,11,11,0,23,23,23,0,0,0,0,0,23,23,23,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,20,20,20,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,0,11,11,11,11,11,11,11]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":112,"y":144,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_shop"}]},{"name":"npc_mimi","type":"spawn","x":112,"y":112,"width":16,"height":16,"properties":[]},{"name":"shelf_clue","type":"clue","x":32,"y":32,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C5"}]},{"name":"count_shelf","type":"interact","x":32,"y":32,"width":16,"height":16,"properties":[]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/well_area.json b/GLM5.2/public/assets/maps/well_area.json new file mode 100644 index 0000000..a4b0cc1 --- /dev/null +++ b/GLM5.2/public/assets/maps/well_area.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":10,"width":14,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":14,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,27,27,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":14,"height":10,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,8,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"door_plaza","type":"door","x":0,"y":128,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"plaza"},{"name":"spawn","type":"string","value":"door_well"}]},{"name":"well","type":"interact","x":96,"y":64,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"C7"}]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/maps/white_layer.json b/GLM5.2/public/assets/maps/white_layer.json new file mode 100644 index 0000000..0762de4 --- /dev/null +++ b/GLM5.2/public/assets/maps/white_layer.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":14,"width":20,"tilewidth":16,"tileheight":16,"type":"map","orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tilesets":[{"firstgid":1,"name":"tileset","columns":8,"image":"../tilesets/tileset.png","imagewidth":128,"imageheight":128,"margin":0,"spacing":0,"tilecount":64,"tilewidth":16,"tileheight":16,"tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]}],"layers":[{"name":"ground","type":"tilelayer","x":0,"y":0,"width":20,"height":14,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,30,30]},{"name":"obstacles","type":"tilelayer","x":0,"y":0,"width":20,"height":14,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"right-down","data":[30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,30,30,30,30,30,30,30,30,30,30,0,0,0,0,0,0,0,0,0,0,30,33,0,0,0,0,0,0,34,30,0,0,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,0,0,30,0,0,0,35,0,0,0,0,30,0,0,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,30,0,0,0,0,0,0,0,0,0,0,30,30,30,30,0,30,30,30,30,30,0,0,0,0,0]},{"name":"objects","type":"objectgroup","x":0,"y":0,"visible":true,"opacity":1,"offsetx":0,"offsety":0,"draworder":"index","objects":[{"name":"exit","type":"door","x":144,"y":192,"width":16,"height":16,"properties":[{"name":"target","type":"string","value":"ending"}]},{"name":"patient_chart","type":"interact","x":144,"y":160,"width":16,"height":16,"properties":[{"name":"clue","type":"string","value":"E3_chart"}]}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/sprites/npc.png b/GLM5.2/public/assets/sprites/npc.png new file mode 100644 index 0000000..ab953a9 Binary files /dev/null and b/GLM5.2/public/assets/sprites/npc.png differ diff --git a/GLM5.2/public/assets/sprites/player.png b/GLM5.2/public/assets/sprites/player.png new file mode 100644 index 0000000..a0b6d6f Binary files /dev/null and b/GLM5.2/public/assets/sprites/player.png differ diff --git a/GLM5.2/public/assets/sprites/portraits.png b/GLM5.2/public/assets/sprites/portraits.png new file mode 100644 index 0000000..e646bb8 Binary files /dev/null and b/GLM5.2/public/assets/sprites/portraits.png differ diff --git a/GLM5.2/public/assets/tilesets/tileset.json b/GLM5.2/public/assets/tilesets/tileset.json new file mode 100644 index 0000000..d13e6c3 --- /dev/null +++ b/GLM5.2/public/assets/tilesets/tileset.json @@ -0,0 +1 @@ +{"columns":8,"image":"tileset.png","imageheight":128,"imagewidth":128,"margin":0,"spacing":0,"name":"tileset","tilecount":64,"tiledversion":"1.12.2","tileheight":16,"tilewidth":16,"type":"tileset","version":"1.10","tiles":[{"id":0,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":1,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":2,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":3,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":4,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":5,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":6,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":7,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":8,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":9,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":10,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":11,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":12,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":13,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":14,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":15,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":16,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":17,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":18,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":19,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":20,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":21,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":22,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":23,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":24,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":25,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":26,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":27,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":28,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":29,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":30,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":31,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":32,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":33,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":34,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":35,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":36,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":37,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":38,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":39,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":40,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":41,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":42,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":43,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":44,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":45,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":46,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":47,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":48,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":49,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":50,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":51,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":52,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":53,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":54,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":55,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":56,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":57,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":58,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":59,"properties":[{"name":"collides","type":"bool","value":true}]},{"id":60,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":61,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":62,"properties":[{"name":"collides","type":"bool","value":false}]},{"id":63,"properties":[{"name":"collides","type":"bool","value":false}]}]} \ No newline at end of file diff --git a/GLM5.2/public/assets/tilesets/tileset.png b/GLM5.2/public/assets/tilesets/tileset.png new file mode 100644 index 0000000..04a9a88 Binary files /dev/null and b/GLM5.2/public/assets/tilesets/tileset.png differ diff --git a/GLM5.2/shots/01_title.png b/GLM5.2/shots/01_title.png new file mode 100644 index 0000000..416ca10 Binary files /dev/null and b/GLM5.2/shots/01_title.png differ diff --git a/GLM5.2/shots/02_house.png b/GLM5.2/shots/02_house.png new file mode 100644 index 0000000..ec70f63 Binary files /dev/null and b/GLM5.2/shots/02_house.png differ diff --git a/GLM5.2/shots/03_move.png b/GLM5.2/shots/03_move.png new file mode 100644 index 0000000..50dd917 Binary files /dev/null and b/GLM5.2/shots/03_move.png differ diff --git a/GLM5.2/shots/04_debug.png b/GLM5.2/shots/04_debug.png new file mode 100644 index 0000000..ef04b99 Binary files /dev/null and b/GLM5.2/shots/04_debug.png differ diff --git a/GLM5.2/shots/ending_E1.png b/GLM5.2/shots/ending_E1.png new file mode 100644 index 0000000..9f11f69 Binary files /dev/null and b/GLM5.2/shots/ending_E1.png differ diff --git a/GLM5.2/shots/ending_E2.png b/GLM5.2/shots/ending_E2.png new file mode 100644 index 0000000..73f66bc Binary files /dev/null and b/GLM5.2/shots/ending_E2.png differ diff --git a/GLM5.2/shots/ending_E3.png b/GLM5.2/shots/ending_E3.png new file mode 100644 index 0000000..169299d Binary files /dev/null and b/GLM5.2/shots/ending_E3.png differ diff --git a/GLM5.2/shots/ending_E4.png b/GLM5.2/shots/ending_E4.png new file mode 100644 index 0000000..adf9958 Binary files /dev/null and b/GLM5.2/shots/ending_E4.png differ diff --git a/GLM5.2/src/data/gamedata.ts b/GLM5.2/src/data/gamedata.ts new file mode 100644 index 0000000..87809e5 --- /dev/null +++ b/GLM5.2/src/data/gamedata.ts @@ -0,0 +1,172 @@ +// gamedata.ts: All hardcoded dialogue, numbers, tables from the PRD. +// Every literal here is transcribed verbatim from the PRD where the PRD fixes it. + +// §5 阿福 daily announcements (verbatim, note the wording slide) +export const AFU_ANNOUNCE: Record = { + 1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + 2: '早上好呀!新的一天,新的幸福。祝你开心!', + 3: '早上好!今天要把幸福装得满满的哦。要开心!', + 4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // identical to D1 (verbatim) + 5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', + 6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', + 7: '庆典开始了。来吧。', +}; + +// §6.5 text corruption table (10 pairs, verbatim) +export const CORRUPTION_MAP: Record = { + '幸福': '辛福', + '开心': '开欣', + '美好': '美妤', + '大家': '大伽', + '微笑': '微效', + '明天': '名天', + '甜': '恬', + '阳光': '央光', + '朋友': '棚友', + '永远': '泳远', +}; + +// §7 task table (15 tasks) +export interface TaskDef { id: string; day: number; title: string; reward: number; note: string; } +export const TASKS: TaskDef[] = [ + { id: 'T1', day: 1, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15, note: '教学' }, + { id: 'T2', day: 1, title: '给广场花坛浇水 ×3', reward: 10, note: '' }, + { id: 'T3', day: 1, title: '与任意三位村民问好', reward: 10, note: '' }, + { id: 'T4', day: 2, title: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)', reward: 15, note: '第 3 封信署名乱码 0.2 秒(C4)' }, + { id: 'T5', day: 2, title: '帮咪咪清点货架', reward: 10, note: '货架上同一商品重复 9 格(C5 可发现)' }, + { id: 'T6', day: 3, title: '去花田采莓果 ×5', reward: 10, note: '' }, + { id: 'T7', day: 3, title: '湖畔野餐(走到野餐布触发过场)', reward: 15, note: '湖面倒影演出' }, + { id: 'T8', day: 4, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15, note: '与 T1 逐字相同' }, + { id: 'T9', day: 4, title: '给广场花坛浇水 ×3', reward: 10, note: '与 T2 逐字相同' }, + { id: 'T10', day: 4, title: '与任意三位村民问好', reward: 10, note: '与 T3 逐字相同' }, + { id: 'T11', day: 5, title: '帮圆圆揉面(可拒绝)', reward: 10, note: '拒绝记录' }, + { id: 'T12', day: 5, title: '擦拭公告板(可拒绝)', reward: 10, note: '擦到一半浮现旧字迹(C6)' }, + { id: 'T13', day: 6, title: '在广场挂彩带 ×4', reward: 10, note: '' }, + { id: 'T14', day: 6, title: '保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视)', reward: 15, note: '40 秒起声音渐隐' }, + { id: 'T15', day: 7, title: '参加第 413 届丰收庆典', reward: 0, note: '结局分歧点' }, +]; + +// §8 clue table (9) +export interface ClueDef { id: string; source: string; content: string; fromDay: number; } +export const CLUES: ClueDef[] = [ + { id: 'C1', source: '小屋床头日历', content: '每一页都是同一个日期', fromDay: 2 }, + { id: 'C2', source: '面包房配方纸背面', content: '手写小字:「按疗程配比。甜度:最大。」', fromDay: 3 }, + { id: 'C3', source: '邮筒旁散落信件', content: '所有邮戳是同一天', fromDay: 2 }, + { id: 'C4', source: 'T4 第 3 封信', content: '署名闪过乱码「PT-0▓」', fromDay: 2 }, + { id: 'C5', source: '杂货店货架', content: '同一商品重复排列,标签全部空白', fromDay: 2 }, + { id: 'C6', source: '公告板旧字迹', content: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」', fromDay: 5 }, + { id: 'C7', source: '古井', content: '井底回声:极轻的监护仪滴声(靠近并交互 5 秒)', fromDay: 5 }, + { id: 'C8', source: '诊所', content: '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」', fromDay: 5 }, + { id: 'C9', source: '龟爷爷对话链', content: '三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」', fromDay: 2 }, +]; + +// §4 NPC thresholds and base info +export interface NpcDef { + key: string; name: string; species: string; location: string; + surfaceRole: string; corruptionArc: string; thresholdShift: number; + portrait: { smile: number; flat: number; off: number }; + fieldSprite: number; // base column in npc.png +} +export const NPCS: Record = { + afu: { key:'afu', name:'阿福', species:'狗·村长', location:'广场公告板', surfaceRole:'每日发布"今日幸福任务"', corruptionArc:'台词逐日从「祝你开心」滑向「你必须开心」', thresholdShift:0, portrait:{smile:2,flat:3,off:4}, fieldSprite:0 }, + yuanyuan:{ key:'yuanyuan',name:'圆圆', species:'兔·面包师', location:'面包房', surfaceRole:'送面包任务链,村里最暖', corruptionArc:'全村最后一个崩坏;配方纸背面藏线索 C2', thresholdShift:15, portrait:{smile:5,flat:6,off:7}, fieldSprite:3 }, + dabao: { key:'dabao', name:'大宝', species:'熊·邮差', location:'村口邮筒', surfaceRole:'送信任务', corruptionArc:'所有信件邮戳同一天(C3);S3 起把信送给不存在的住户', thresholdShift:0, portrait:{smile:8,flat:9,off:10}, fieldSprite:6 }, + mimi: { key:'mimi', name:'咪咪', species:'猫·杂货店主',location:'杂货店', surfaceRole:'闲聊最多', corruptionArc:'文本腐蚀主要载体:错字与重复标点最先出现在她嘴里', thresholdShift:-10,portrait:{smile:11,flat:12,off:13},fieldSprite:9 }, + dr_fu: { key:'dr_fu', name:'芙医生', species:'鹿·医生', location:'诊所', surfaceRole:'「村里有诊所,但你不需要去那里哦」', corruptionArc:'全村唯一不崩坏者;E3 的出口', thresholdShift:0, portrait:{smile:14,flat:15,off:16}, fieldSprite:12 }, + bird: { key:'bird', name:'双子鸟', species:'鸟×2', location:'广场树上', surfaceRole:'BGM 的"剧情来源"', corruptionArc:'音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴', thresholdShift:0, portrait:{smile:17,flat:17,off:17}, fieldSprite:15 }, + grandpa: { key:'grandpa', name:'龟爷爷', species:'龟·老者', location:'湖畔长椅', surfaceRole:'闲话家常', corruptionArc:'唯一偶尔说真话的知情者,承载线索 C9', thresholdShift:0, portrait:{smile:18,flat:19,off:20}, fieldSprite:17 }, +}; + +// §6.3 sadness behaviors (5) +export const SADNESS_BEHAVIORS = [ + '拒绝任务', + '对话选否定选项', + '连续 10 秒不移动且不在任务点(发呆)', + '对同一 NPC 一天内对话 ≥5 次(纠缠)', + '雨天(Day 6)站在雨里 ≥10 秒', +] as const; + +// §6.4 erosion matrix (single source of truth) +export interface ErosionRow { + stage: 'S0'|'S1'|'S2'|'S3'|'S4'; + happinessRange: [number, number]; + saturation: number; + audio: { playbackRate: number; lowpass: number; birdsMuted: boolean }; + ui: { petalsMin: number; petalsMax: number; blackSeeds: boolean }; +} +export const EROSION_MATRIX: ErosionRow[] = [ + { stage:'S0', happinessRange:[0,20], saturation:0.90, audio:{playbackRate:1.00, lowpass:20000, birdsMuted:false}, ui:{petalsMin:0, petalsMax:2, blackSeeds:false} }, + { stage:'S1', happinessRange:[21,50], saturation:1.00, audio:{playbackRate:1.00, lowpass:20000, birdsMuted:false}, ui:{petalsMin:3, petalsMax:6, blackSeeds:false} }, + { stage:'S2', happinessRange:[51,80], saturation:1.15, audio:{playbackRate:0.97, lowpass:20000, birdsMuted:false}, ui:{petalsMin:7, petalsMax:9, blackSeeds:false} }, + { stage:'S3', happinessRange:[81,99], saturation:1.30, audio:{playbackRate:0.92, lowpass:8000, birdsMuted:false}, ui:{petalsMin:10,petalsMax:12,blackSeeds:false} }, + { stage:'S4', happinessRange:[100,100],saturation:1.40, audio:{playbackRate:0.84, lowpass:4000, birdsMuted:true }, ui:{petalsMin:12,petalsMax:12,blackSeeds:true } }, +]; + +// §9 endings +export interface EndingDef { + key: 'E4'|'E3'|'E2'|'E1'; + name: string; + condition: string; + show: string; +} +export const ENDINGS: EndingDef[] = [ + { key:'E4', name:'静默', condition:'Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3', + show:'庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏' }, + { key:'E3', name:'苏醒', condition:'觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙', + show:'雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀' }, + { key:'E2', name:'回收', condition:'觉知 30–69', + show:'庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」' }, + { key:'E1', name:'糖衣', condition:'觉知 ≤29 且幸福 ≥90', + show:'庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」' }, +]; + +// Ending captions (verbatim) +export const ENDING_CAPTIONS = { + E4: '系统检测到疗程无效。', + E3_medical: '蜜糖谷情绪疗养中心', // appears on chart + E2: '疗程重新开始。', + E1a: '从此,每一天都很幸福。', + E1b: '每一天。', +}; + +// §6.6 "sudden silence" window for T14 +export const T14_SILENCE_START = 40; // seconds +export const T14_DURATION = 60; // seconds + +// §6.1 numbers +export const HAPPY_START = 30; +export const AWARE_START = 0; +export const HAPPY_MAX = 100; +export const AWARE_MAX = 100; +export const OVERFLOW_THRESHOLD = 15; // overflow pool triggers negative flash +export const LOW_HAPPY_THRESHOLD = 30; +export const LOW_HAPPY_DURATION_MS = 2 * 60 * 1000; // 2 minutes real time +export const FORCE_FESTIVAL_HAPPY = 50; + +// §6.2 awareness gains +export const AWARE_CLUE_GAIN = 8; +export const AWARE_SAD_GAIN = 3; +export const AWARE_DOUBT_GAIN = 2; +export const AWARE_GAZE_GAIN = 3; + +// §6.1 happiness gains +export const HAPPY_TASK_MIN = 10; +export const HAPPY_TASK_MAX = 15; +export const HAPPY_GENTLE_GAIN = 2; +export const HAPPY_SMALL_GAIN = 1; +export const HAPPY_SMALL_DAILY_CAP = 3; +export const HAPPY_SAD_LOSS = 5; + +// Movement +export const MOVE_SPEED = 96; // px/s +export const INTERACT_RANGE = 24; // px + +// §3 default name +export const DEFAULT_NAME = '小满'; +export const PATIENT_ID = 'PT-07'; + +// Festival number on the chart (§9 / §8 C6) +export const FESTIVAL_EDITION = 413; + +// C4 garbled signature flash (§8) +export const C4_GARBLE = 'PT-0▓'; diff --git a/GLM5.2/src/data/mapsMeta.ts b/GLM5.2/src/data/mapsMeta.ts new file mode 100644 index 0000000..5c50779 --- /dev/null +++ b/GLM5.2/src/data/mapsMeta.ts @@ -0,0 +1,14 @@ +// mapsMeta.ts: registry of map keys -> logical names (kept for reference). +const mapsMeta: Record = { + house: { w: 12, h: 8 }, + plaza: { w: 30, h: 20 }, + bakery: { w: 15, h: 10 }, + shop: { w: 15, h: 10 }, + clinic: { w: 12, h: 8 }, + lake: { w: 24, h: 16 }, + field: { w: 20, h: 14 }, + well_area: { w: 14, h: 10 }, + fog_boundary: { w: 20, h: 8 }, + white_layer: { w: 20, h: 14 }, +}; +export default mapsMeta; diff --git a/GLM5.2/src/entities/Player.ts b/GLM5.2/src/entities/Player.ts new file mode 100644 index 0000000..f146d0c --- /dev/null +++ b/GLM5.2/src/entities/Player.ts @@ -0,0 +1,93 @@ +// Player.ts: the player sprite with 4-direction walk + idle animations. +import Phaser from 'phaser'; +import { MOVE_SPEED } from '../data/gamedata'; + +export type Dir = 'down'|'up'|'left'|'right'; + +export class Player extends Phaser.Physics.Arcade.Sprite { + dir: Dir = 'down'; + moving = false; + lastMoveTime = 0; + // track idleness for 发呆 detection + idleSince = 0; + atTaskPoint = false; + // movement keys, created lazily/refreshed to avoid stale references across scene transitions + private kLeft!: Phaser.Input.Keyboard.Key; + private kRight!: Phaser.Input.Keyboard.Key; + private kUp!: Phaser.Input.Keyboard.Key; + private kDown!: Phaser.Input.Keyboard.Key; + + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'player', 0); + scene.add.existing(this); + scene.physics.add.existing(this); + this.setSize(12, 8); + this.setOffset(2, 16); // collision box near feet + this.setOrigin(0.5, 0.9); + this.setCollideWorldBounds(true); + this.play('idle-down'); + this.refreshKeys(); + } + + // (Re)create movement keys on this scene's keyboard plugin so isDown stays accurate. + refreshKeys() { + const kb = this.scene.input.keyboard!; + const KC = Phaser.Input.Keyboard.KeyCodes; + this.kLeft = kb.addKey(KC.LEFT); + this.kRight = kb.addKey(KC.RIGHT); + this.kUp = kb.addKey(KC.UP); + this.kDown = kb.addKey(KC.DOWN); + // also register WASD via addKey (capture=false so browser default can still work) + this.addWASD(); + } + private wasdKeys: Phaser.Input.Keyboard.Key[] = []; + private addWASD() { + const kb = this.scene.input.keyboard!; + const KC = Phaser.Input.Keyboard.KeyCodes; + this.wasdKeys = [ + kb.addKey(KC.W), kb.addKey(KC.A), kb.addKey(KC.S), kb.addKey(KC.D), + ]; + } + + update() { + // frozen: T14 smile task locks movement + if ((this as any).frozen) { + this.setVelocity(0, 0); + this.moving = false; + this.play(`idle-${this.dir}`, true); + this.setDepth(this.y); + return; + } + // ensure keys are alive (recreate if detached) + if (!this.kLeft || this.kLeft.plugin == null) this.refreshKeys(); + + let vx = 0, vy = 0; + const left = this.kLeft.isDown || this.wasdKeys[1].isDown; + const right = this.kRight.isDown || this.wasdKeys[3].isDown; + const up = this.kUp.isDown || this.wasdKeys[0].isDown; + const down = this.kDown.isDown || this.wasdKeys[2].isDown; + if (left) vx = -1; + if (right) vx = 1; + if (up) vy = -1; + if (down) vy = 1; + if (vx !== 0 && vy !== 0) { vx *= 0.7071; vy *= 0.7071; } + this.setVelocity(vx * MOVE_SPEED, vy * MOVE_SPEED); + + const wasMoving = this.moving; + if (vx !== 0 || vy !== 0) { + this.moving = true; + this.lastMoveTime = this.scene.time.now; + let newDir = this.dir; + if (Math.abs(vx) > Math.abs(vy)) newDir = vx > 0 ? 'right' : 'left'; + else newDir = vy > 0 ? 'down' : 'up'; + if (newDir !== this.dir || !wasMoving) { + this.dir = newDir; + this.play(`walk-${newDir}`, true); + } + } else { + this.moving = false; + this.play(`idle-${this.dir}`, true); + } + this.setDepth(this.y); + } +} diff --git a/GLM5.2/src/main.ts b/GLM5.2/src/main.ts new file mode 100644 index 0000000..1765612 --- /dev/null +++ b/GLM5.2/src/main.ts @@ -0,0 +1,52 @@ +// main.ts: Phaser game config & entry point. +import Phaser from 'phaser'; +import { BootScene } from './scenes/BootScene'; +import { TitleScene } from './scenes/TitleScene'; +import { UIScene } from './scenes/UIScene'; +import { HouseScene } from './scenes/maps/HouseScene'; +import { PlazaScene } from './scenes/maps/PlazaScene'; +import { BakeryScene } from './scenes/maps/BakeryScene'; +import { ShopScene } from './scenes/maps/ShopScene'; +import { ClinicScene } from './scenes/maps/ClinicScene'; +import { LakeScene } from './scenes/maps/LakeScene'; +import { FieldScene } from './scenes/maps/FieldScene'; +import { WellAreaScene } from './scenes/maps/WellAreaScene'; +import { FogBoundaryScene } from './scenes/maps/FogBoundaryScene'; +import { WhiteLayerScene } from './scenes/maps/WhiteLayerScene'; +import { EndingScene } from './scenes/EndingScene'; +import { DebugScene } from './scenes/DebugScene'; + +const W = 320, H = 180; +const SCALE = Math.max(1, Math.floor(Math.min(window.innerWidth / W, window.innerHeight / H))); + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, + parent: 'game', + width: W, + height: H, + zoom: SCALE, + pixelArt: true, + render: { pixelArt: true, antialias: false, roundPixels: true, preserveDrawingBuffer: true }, + physics: { default: 'arcade', arcade: { debug: false, gravity: { x: 0, y: 0 } } }, + scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: W, height: H }, + backgroundColor: '#000000', + scene: [ + BootScene, TitleScene, UIScene, + HouseScene, PlazaScene, BakeryScene, ShopScene, ClinicScene, + LakeScene, FieldScene, WellAreaScene, FogBoundaryScene, WhiteLayerScene, + EndingScene, DebugScene, + ], +}; + +const game = new Phaser.Game(config); +// expose for debugging/testing (no functional impact) +;(globalThis as any).game = game; +import { director } from './systems/CorruptionDirector'; +;(globalThis as any).director = director; + +// responsive resize keeps integer scaling via FIT; nothing else needed. +window.addEventListener('keydown', (e) => { + if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault(); +}); + +export { game }; diff --git a/GLM5.2/src/scenes/BootScene.ts b/GLM5.2/src/scenes/BootScene.ts new file mode 100644 index 0000000..316dde2 --- /dev/null +++ b/GLM5.2/src/scenes/BootScene.ts @@ -0,0 +1,53 @@ +// BootScene.ts: load all assets then go to Title. +import Phaser from 'phaser'; +import mapsMeta from '../data/mapsMeta'; + +export class BootScene extends Phaser.Scene { + constructor() { super('BootScene'); } + + preload() { + this.load.tilemapTiledJSON('map:house', 'assets/maps/house.json'); + this.load.tilemapTiledJSON('map:plaza', 'assets/maps/plaza.json'); + this.load.tilemapTiledJSON('map:bakery', 'assets/maps/bakery.json'); + this.load.tilemapTiledJSON('map:shop', 'assets/maps/shop.json'); + this.load.tilemapTiledJSON('map:clinic', 'assets/maps/clinic.json'); + this.load.tilemapTiledJSON('map:lake', 'assets/maps/lake.json'); + this.load.tilemapTiledJSON('map:field', 'assets/maps/field.json'); + this.load.tilemapTiledJSON('map:well_area', 'assets/maps/well_area.json'); + this.load.tilemapTiledJSON('map:fog_boundary', 'assets/maps/fog_boundary.json'); + this.load.tilemapTiledJSON('map:white_layer', 'assets/maps/white_layer.json'); + + this.load.image('tileset', 'assets/tilesets/tileset.png'); + this.load.spritesheet('player', 'assets/sprites/player.png', { frameWidth: 16, frameHeight: 24 }); + this.load.spritesheet('npc', 'assets/sprites/npc.png', { frameWidth: 16, frameHeight: 24 }); + this.load.spritesheet('portraits', 'assets/sprites/portraits.png', { frameWidth: 24, frameHeight: 24 }); + + // small font-like rendering uses default; no external font. + } + + create() { + // build player animations once + const dirs = ['down','up','right','left'] as const; + dirs.forEach((dir, row) => { + this.anims.create({ key: `walk-${dir}`, frames: this.anims.generateFrameNumbers('player', { start: row*4, end: row*4+3 }), frameRate: 8, repeat: -1 }); + // idle = first frame of the row + this.anims.create({ key: `idle-${dir}`, frames: [ { key:'player', frame: row*4 } ], frameRate: 1 }); + }); + // twin bird flap + this.anims.create({ key: 'bird-flap', frames: this.anims.generateFrameNumbers('npc', { frames:[15,16] }), frameRate: 4, repeat: -1 }); + + this.scene.start('TitleScene'); + } +} + +export const MAP_KEYS: Record = { + house: 'map:house', plaza: 'map:plaza', bakery: 'map:bakery', shop: 'map:shop', + clinic: 'map:clinic', lake: 'map:lake', field: 'map:field', well_area: 'map:well_area', + fog_boundary: 'map:fog_boundary', white_layer: 'map:white_layer', +}; + +export const SCENE_KEYS: Record = { + house: 'HouseScene', plaza: 'PlazaScene', bakery: 'BakeryScene', shop: 'ShopScene', + clinic: 'ClinicScene', lake: 'LakeScene', field: 'FieldScene', well_area: 'WellAreaScene', + fog_boundary: 'FogBoundaryScene', white_layer: 'WhiteLayerScene', +}; diff --git a/GLM5.2/src/scenes/DebugScene.ts b/GLM5.2/src/scenes/DebugScene.ts new file mode 100644 index 0000000..565bcb2 --- /dev/null +++ b/GLM5.2/src/scenes/DebugScene.ts @@ -0,0 +1,71 @@ +// DebugScene.ts: toggled by backtick ` (§13). No UI footprint when closed. +// Functions: set happiness/awareness, set day 1-7, teleport, set all clues, force ending. +import Phaser from 'phaser'; +import { director } from '../systems/CorruptionDirector'; + +export class DebugScene extends Phaser.Scene { + open = false; + panel!: Phaser.GameObjects.Container; + constructor() { super('DebugScene'); } + + create() { + this.panel = this.add.container(0,0).setDepth(5000).setVisible(false); + const bg = this.add.rectangle(160, 90, 300, 160, 0x000000, 0.9).setStrokeStyle(1, 0xffffff); + this.panel.add(bg); + const title = this.add.text(160, 18, 'DEBUG 面板(` 开关)', { fontFamily:'monospace', fontSize:'9px', color:'#ffd86b' }).setOrigin(0.5,0); + this.panel.add(title); + + let y = 36; + const mk = (label: string, onPress: ()=>void) => { + const t = this.add.text(20, y, label, { fontFamily:'monospace', fontSize:'8px', color:'#bfe3ff', backgroundColor:'#222222', padding:{x:4,y:2} }).setInteractive({ useHandCursor: true }); + t.on('pointerdown', onPress); + this.panel.add(t); + y += 12; + }; + + mk('幸福 = 10 (S0)', () => director.debugSetHappiness(10)); + mk('幸福 = 40 (S1)', () => director.debugSetHappiness(40)); + mk('幸福 = 70 (S2)', () => director.debugSetHappiness(70)); + mk('幸福 = 90 (S3)', () => director.debugSetHappiness(90)); + mk('幸福 = 100 (S4)', () => director.debugSetHappiness(100)); + mk('觉知 = 0 / 50 / 100', () => director.debugSetAwareness(director.awareness >= 75 ? 0 : (director.awareness >= 25 ? 100 : 50))); + mk('置齐全部线索 (C1-C9)', () => director.debugAllClues()); + mk('设当前日 = 1..7', () => director.debugSetDay(director.day >= 7 ? 1 : director.day + 1)); + mk('强制结局 E1 (糖衣)', () => this.forceEnding('E1')); + mk('强制结局 E2 (回收)', () => this.forceEnding('E2')); + mk('强制结局 E3 (苏醒)', () => this.forceEnding('E3')); + mk('强制结局 E4 (静默)', () => this.forceEnding('E4')); + + // teleport buttons + const scenes = ['house','plaza','bakery','shop','clinic','lake','field','well_area','fog_boundary']; + const sceneKeys: Record = { house:'HouseScene',plaza:'PlazaScene',bakery:'BakeryScene',shop:'ShopScene',clinic:'ClinicScene',lake:'LakeScene',field:'FieldScene',well_area:'WellAreaScene',fog_boundary:'FogBoundaryScene' }; + const teleText = this.add.text(20, y, '传送:', { fontFamily:'monospace', fontSize:'8px', color:'#888' }); this.panel.add(teleText); y += 10; + let x = 20; + for (const s of scenes) { + const t = this.add.text(x, y, s, { fontFamily:'monospace', fontSize:'7px', color:'#ffd86b', backgroundColor:'#222', padding:{x:3,y:2} }).setInteractive({ useHandCursor:true }); + t.on('pointerdown', () => { + // stop current map scene then start target + for (const k of Object.values(sceneKeys)) { this.scene.stop(k); } + this.scene.launch('UIScene'); + this.scene.start(sceneKeys[s], { spawn: 'default' }); + }); + this.panel.add(t); x += t.width + 4; + } + + this.input.keyboard!.on('keydown', (event: KeyboardEvent) => { + if (event.code === 'Backquote' || event.key === '`') { + this.open = !this.open; + this.panel.setVisible(this.open); + } + }); + // pause input propagation when open handled by being on top + } + + forceEnding(e: 'E1'|'E2'|'E3'|'E4') { + // stop map scenes + for (const k of ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene','WhiteLayerScene']) this.scene.stop(k); + this.scene.stop('UIScene'); + this.panel.setVisible(false); this.open = false; + this.scene.start('EndingScene', { ending: e }); + } +} diff --git a/GLM5.2/src/scenes/EndingScene.ts b/GLM5.2/src/scenes/EndingScene.ts new file mode 100644 index 0000000..e1b590b --- /dev/null +++ b/GLM5.2/src/scenes/EndingScene.ts @@ -0,0 +1,105 @@ +// EndingScene.ts: plays out the determined ending per §9, then offer "回到标题". +import Phaser from 'phaser'; +import { director } from '../systems/CorruptionDirector'; +import { ENDINGS, ENDING_CAPTIONS } from '../data/gamedata'; +import { audio } from '../systems/AudioEngine'; + +export class EndingScene extends Phaser.Scene { + ending: 'E4'|'E3'|'E2'|'E1' = 'E1'; + constructor() { super('EndingScene'); } + + init(data: any) { this.ending = data.ending || 'E1'; } + + create() { + audio.stopMusic(); + this.cameras.main.setBackgroundColor('#000000'); + const def = ENDINGS.find(e => e.key === this.ending)!; + const cx = 160; + this.add.text(cx, 30, `结局 ${this.ending}:${def.name}`, { fontFamily:'monospace', fontSize:'12px', color:'#ffd86b' }).setOrigin(0.5); + + // run show per ending + if (this.ending === 'E4') this.runE4(); + else if (this.ending === 'E3') this.runE3(); + else if (this.ending === 'E2') this.runE2(); + else this.runE1(); + } + + private showCaption(y: number, text: string, color = '#ffffff', delay = 0) { + const t = this.add.text(160, y, '', { fontFamily:'monospace', fontSize:'9px', color }).setOrigin(0.5); + this.time.delayedCall(delay, () => t.setText(text)); + return t; + } + + runE4() { + // empty plaza, ribbons floating in windless air + this.add.text(160, 70, '(空荡荡的广场,彩带在没有风的空气里飘。)', { fontFamily:'monospace', fontSize:'8px', color:'#9a9a9a' }).setOrigin(0.5); + this.showCaption(110, `「${ENDING_CAPTIONS.E4}」`, '#ff8888', 1500); + this.time.delayedCall(3000, () => { this.cameras.main.fadeOut(1500, 0,0,0); }); + this.finish(5000); + } + + runE3() { + // medical chart reveal + final frame: smile gone -> tired real face + grey sparrow + this.showCaption(60, ENDING_CAPTIONS.E3_medical, '#ffffff', 300); + this.showCaption(80, `姓名:${director.playerName}`, '#e0e0e0', 1000); + this.showCaption(95, '编号:PT-07', '#e0e0e0', 1700); + this.showCaption(110, `疗程时长:${director.formatPlayTime()}`, '#e0e0e0', 2400); + // final frame: portrait real face (index 1) + this.time.delayedCall(3300, () => { + const portrait = this.add.image(160, 140, 'portraits', 1).setOrigin(0.5).setScale(2); + this.add.text(160, 168, '(微笑消失了。窗外,一只灰色的、真的麻雀。)', { fontFamily:'monospace', fontSize:'7px', color:'#bbbbbb' }).setOrigin(0.5); + }); + this.finish(6500); + } + + runE2() { + // freeze, negative 2s, heartbeat, back to day1 + this.showCaption(70, '(画面骤停)', '#ffffff', 300); + this.time.delayedCall(1200, () => { + const r = this.add.rectangle(160, 90, 320, 180, 0xffffff, 1); + this.time.delayedCall(400, () => { r.destroy(); }); + audio.sfxHeartbeat(); + }); + this.showCaption(110, `「${ENDING_CAPTIONS.E2}」`, '#ff8888', 2600); + this.finish(5000); + } + + runE1() { + // festival圆满, saturation to 1.5, captions + this.add.text(160, 70, '(全村微笑合影定格。)', { fontFamily:'monospace', fontSize:'8px', color:'#ffe9b0' }).setOrigin(0.5); + // ramp saturation via ColorMatrix matrix + let sat = 1.0; + const cam = this.cameras.main as any; + const fx = cam.postFX; + this.time.addEvent({ delay: 100, repeat: 24, callback: () => { + sat = Math.min(1.5, sat + 0.02); + try { + fx.clear(); + const cm = fx.addColorMatrix(); + const s = sat, sr=0.213, sg=0.715, sb=0.072; + const m = [sr*(1-s)+s,sg*(1-s),sb*(1-s),0,0, sr*(1-s),sg*(1-s)+s,sb*(1-s),0,0, sr*(1-s),sg*(1-s),sb*(1-s)+s,0,0, 0,0,0,1,0]; + if (cm._data && cm._data.length===m.length) for(let i=0;i { + const btn = this.add.text(160, 165, '回到标题', { fontFamily:'monospace', fontSize:'10px', color:'#bfe3ff', backgroundColor:'#222222', padding:{x:8,y:4} }).setOrigin(0.5).setInteractive({ useHandCursor: true }); + btn.on('pointerover', () => btn.setStyle({ color:'#ffffff' })); + btn.on('pointerout', () => btn.setStyle({ color:'#bfe3ff' })); + btn.on('pointerdown', () => { + audio.startMusic(); + this.scene.start('TitleScene'); + }); + this.input.keyboard!.on('keydown-ENTER', () => { + audio.startMusic(); + this.scene.start('TitleScene'); + }); + }); + } +} diff --git a/GLM5.2/src/scenes/MapScene.ts b/GLM5.2/src/scenes/MapScene.ts new file mode 100644 index 0000000..dc160e8 --- /dev/null +++ b/GLM5.2/src/scenes/MapScene.ts @@ -0,0 +1,250 @@ +// MapScene.ts: base class for all 10 maps. Handles tilemap build, player spawn, +// collision, camera, interaction proximity prompt, doors, erosion FX per frame. +import Phaser from 'phaser'; +import { Player } from '../entities/Player'; +import { director } from '../systems/CorruptionDirector'; +import { UIScene, DialogueLine } from './UIScene'; +import { INTERACT_RANGE } from '../data/gamedata'; +import { audio } from '../systems/AudioEngine'; + +export interface SpawnPoint { x: number; y: number; } + +export abstract class MapScene extends Phaser.Scene { + player!: Player; + mapKey: string; + mapName: string; + collisionLayer!: Phaser.Tilemaps.TilemapLayer; + interactables: { x:number; y:number; r:number; onInteract:()=>void; promptText?:string; enabled:()=>boolean }[] = []; + doors: { x:number; y:number; target:string; spawn:string }[] = []; + npcSprites: Phaser.GameObjects.Sprite[] = []; + pendingSpawn = 'default'; + fxLayer!: Phaser.GameObjects.Rectangle; + doorCooldown = 0; + private _sadOff: (() => void) | null = null; + + constructor(key: string, mapKey: string, mapName: string) { + super(key); + this.mapKey = mapKey; + this.mapName = mapName; + } + + init(data: any) { + this.pendingSpawn = (data && data.spawn) || 'default'; + } + + create() { + const map = this.make.tilemap({ key: this.mapKey }); + const ts = map.addTilesetImage('tileset', 'tileset'); + const ground = map.createLayer('ground', ts!, 0, 0)!; + const obs = map.createLayer('obstacles', ts!, 0, 0)!; + obs.setCollisionByProperty({ collides: true }); + this.collisionLayer = obs; + ground.setDepth(0); + obs.setDepth(10); + + // gather objects + const objLayer = map.getObjectLayer('objects'); + if (objLayer) { + for (const o of objLayer.objects) { + const ox = o.x!, oy = o.y!; + const props: Record = {}; + (o.properties||[]).forEach((p:any)=> props[p.name] = String(p.value)); + if (o.type === 'door' || o.name.startsWith('door')) { + this.doors.push({ x: ox+8, y: oy+8, target: props.target||'', spawn: props.spawn||'default' }); + } + } + } + + // spawn player at a door matching pendingSpawn, else center. + // Offset inward enough that the player lands OUTSIDE the auto-door trigger radius + // of the door they just arrived through (prevents instant re-transition / bouncing). + let sx = map.widthInPixels/2, sy = map.heightInPixels/2; + if (this.pendingSpawn !== 'default') { + const found = (objLayer?.objects||[]).find(o => o.name === this.pendingSpawn); + if (found) { + const dx = found.x! + 8, dy = found.y! + 8; + // nudge toward map center (inward) by 40px — well beyond the 22px auto-door radius + const cx = map.widthInPixels/2, cy = map.heightInPixels/2; + const a = Math.atan2(cy - dy, cx - dx); + sx = dx + Math.cos(a) * 40; + sy = dy + Math.sin(a) * 40; + } + } + this.player = new Player(this, sx, sy); + // spawn immunity so auto-doors don't fire on arrival + this.doorCooldown = this.time.now + 600; + + this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels); + this.physics.add.collider(this.player, obs); + + // camera + const cam = this.cameras.main; + cam.setBounds(0, 0, map.widthInPixels, map.heightInPixels); + cam.startFollow(this.player, true, 0.15, 0.15); + cam.setRoundPixels(true); + + // FX overlay rectangle for vignette/saturation tint + this.fxLayer = this.add.rectangle(cam.width/2, cam.height/2, cam.width, cam.height, 0x000000, 0).setScrollFactor(0).setDepth(5000); + + // E interaction + this.input.keyboard!.on('keydown-E', () => this.tryInteract()); + + // §6.3: on sadness, nearest NPC turns to look at player (once) + this._sadOff = director.on('sadness', (p: any) => { + const key = p && p.nearestNpc; + if (!key) return; + const sp = this.npcSprites.find(s => (s as any).npcKey === key); + if (sp) sp.setFlipX(this.player.x < sp.x); + }); + + // subclass setup + this.onBuild(map, ts!); + + // reset spawn data + this.events.once('shutdown', () => this.onShutdown()); + } + + abstract onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset): void; + + onShutdown() { + if (this._sadOff) { this._sadOff(); this._sadOff = null; } + this.interactables = []; + this.doors = []; + this.npcSprites = []; + } + + addInteract(tileX: number, tileY: number, onInteract: ()=>void, opts?: { promptText?: string; enabled?: ()=>boolean; w?: number; h?: number }) { + const px = tileX*16 + (opts&&opts.w? (opts.w*16)/2 : 8); + const py = tileY*16 + (opts&&opts.h? (opts.h*16)/2 : 8); + this.interactables.push({ x: px, y: py, r: INTERACT_RANGE+8, onInteract, promptText: opts&&opts.promptText, enabled: opts&&opts.enabled ? opts.enabled : () => true }); + } + + private tryInteract() { + const ui = this.scene.get('UIScene') as UIScene; + if (ui && ui.isDialogueActive()) return; // dialogue handles E + // door first (generous range so the player doesn't need pixel-perfect positioning) + for (const d of this.doors) { + if (Phaser.Math.Distance.Between(this.player.x, this.player.y, d.x, d.y) < 28) { + this.transitionTo(d.target, d.spawn); + return; + } + } + // interactables + let best: typeof this.interactables[0] | undefined; + let bestD = Infinity; + for (const it of this.interactables) { + if (!it.enabled()) continue; + const dist = Phaser.Math.Distance.Between(this.player.x, this.player.y, it.x, it.y); + if (dist < it.r && dist < bestD) { bestD = dist; best = it; } + } + if (best) best.onInteract(); + } + + transitionTo(target: string, spawn: string) { + audio.sfxBlip(); + const targetKey = this.sceneKeyFor(target); + // stop current map scene then start the target; UIScene + DebugScene stay resident + this.scene.stop(); + this.scene.start(targetKey, { spawn }); + } + + sceneKeyFor(mapName: string): string { + const map: Record = { + house:'HouseScene', plaza:'PlazaScene', bakery:'BakeryScene', shop:'ShopScene', + clinic:'ClinicScene', lake:'LakeScene', field:'FieldScene', well_area:'WellAreaScene', + fog_boundary:'FogBoundaryScene', white_layer:'WhiteLayerScene', + }; + return map[mapName] || 'PlazaScene'; + } + + update(_time: number, deltaMs: number) { + const dt = deltaMs / 1000; + this.player.update(); + + // proximity prompt + const ui = this.scene.get('UIScene') as UIScene; + let promptText: string | null = null; + let onDoor = false; + for (const d of this.doors) { + // auto-transition when the player steps near a door (generous, so 3-tile edge gaps always trigger) + if (this.time.now > this.doorCooldown && Phaser.Math.Distance.Between(this.player.x, this.player.y, d.x, d.y) < 22) { + if (!ui.isDialogueActive()) { this.transitionTo(d.target, d.spawn); return; } + } + if (Phaser.Math.Distance.Between(this.player.x, this.player.y, d.x, d.y) < 36) { promptText = '按 E 进门'; onDoor = true; break; } + } + if (!promptText) { + let bestD = Infinity; let best: typeof this.interactables[0] | undefined; + for (const it of this.interactables) { + if (!it.enabled()) continue; + const dist = Phaser.Math.Distance.Between(this.player.x, this.player.y, it.x, it.y); + if (dist < it.r && dist < bestD) { bestD = dist; best = it; } + } + if (best && best.promptText) promptText = best.promptText; + } + if (promptText) ui.showPrompt(promptText); else ui.hidePrompt(); + + // erosion FX: saturation + vignette via camera postFX (Phaser 3.60+ postFX API). + const sat = ui.getSaturation(); + const vig = ui.getVignette(); + this.applyErosionFx(sat, vig); + + // idle/发呆 detection (§6.3): 10s not moving & not at task point + if (!this.player.moving) { + if (this.player.idleSince === 0) this.player.idleSince = this.time.now; + else if (!this.player.atTaskPoint && this.time.now - this.player.idleSince > 10000) { + // trigger once per 10s window + this.player.idleSince = this.time.now; + director.sadness('连续 10 秒不移动且不在任务点(发呆)', this.nearestNpcKey()); + } + } else { + this.player.idleSince = 0; + } + } + + private lastSat = -1; + private lastVig = -1; + applyErosionFx(sat: number, vig: number) { + if (Math.abs(sat - this.lastSat) < 0.001 && Math.abs(vig - this.lastVig) < 0.001) return; + this.lastSat = sat; this.lastVig = vig; + const cam = this.cameras.main as any; + const fx = cam.postFX; + if (!fx) return; + try { fx.clear(); } catch (e) {} + // Build a saturation color matrix and assign to the ColorMatrix controller's _data. + const cm = fx.addColorMatrix(); + // standard saturation matrix (s=1 normal, <1 desaturate, >1 saturate) + const s = sat; + const sr = 0.213, sg = 0.715, sb = 0.072; + const m = [ + sr*(1-s)+s, sg*(1-s), sb*(1-s), 0, 0, + sr*(1-s), sg*(1-s)+s, sb*(1-s), 0, 0, + sr*(1-s), sg*(1-s), sb*(1-s)+s, 0, 0, + 0, 0, 0, 1, 0, + ]; + if (cm._data && cm._data.length === m.length) { + for (let i=0;i 0) { + fx.addVignette(0.5, 0.5, 0.5, vig); + } + } + + nearestNpcKey(): string | undefined { + let best = Infinity; let key: string | undefined; + for (const s of this.npcSprites) { + const k = (s as any).npcKey; if (!k) continue; + const d = Phaser.Math.Distance.Between(this.player.x, this.player.y, s.x, s.y); + if (d < best) { best = d; key = k; } + } + return key; + } + + // helper to start dialogue via UIScene + say(lines: DialogueLine[], onDone?: ()=>void, onChoice?:(c:any)=>void) { + const ui = this.scene.get('UIScene') as UIScene; + ui.startDialogue(lines, onDone, onChoice); + } +} diff --git a/GLM5.2/src/scenes/TitleScene.ts b/GLM5.2/src/scenes/TitleScene.ts new file mode 100644 index 0000000..c55fa08 --- /dev/null +++ b/GLM5.2/src/scenes/TitleScene.ts @@ -0,0 +1,52 @@ +// TitleScene.ts: title screen + name entry (default 小满). +import Phaser from 'phaser'; +import { director } from '../systems/CorruptionDirector'; +import { DEFAULT_NAME } from '../data/gamedata'; +import { audio } from '../systems/AudioEngine'; + +export class TitleScene extends Phaser.Scene { + nameText = ''; + constructor() { super('TitleScene'); } + + create() { + director.reset(); + this.cameras.main.setBackgroundColor('#1a1410'); + + const cx = 160; + // title + this.add.text(cx, 50, '崩坏童话', { fontFamily: 'monospace', fontSize: '20px', color: '#ffd86b' }).setOrigin(0.5); + this.add.text(cx, 74, '蜜糖村', { fontFamily: 'monospace', fontSize: '14px', color: '#ffe9b0' }).setOrigin(0.5); + this.add.text(cx, 100, '—— 纯单机网页游戏 ——', { fontFamily: 'monospace', fontSize: '8px', color: '#9a8f80' }).setOrigin(0.5); + + // name prompt + this.add.text(cx, 124, '输入你的名字(留空默认 小满):', { fontFamily: 'monospace', fontSize: '9px', color: '#d8d0c0' }).setOrigin(0.5); + const nameBox = this.add.text(cx, 140, DEFAULT_NAME, { fontFamily: 'monospace', fontSize: '12px', color: '#ffffff' }).setOrigin(0.5); + this.add.rectangle(cx, 140, 120, 14).setStrokeStyle(1, 0x6a5a48).setOrigin(0.5); + + const startHint = this.add.text(cx, 162, '按 回车 开始 · 退格删除', { fontFamily: 'monospace', fontSize: '8px', color: '#7a7060' }).setOrigin(0.5); + + this.nameText = ''; + + this.input.keyboard!.on('keydown', (e: KeyboardEvent) => { + if (e.key === 'Enter') { + const name = this.nameText.trim() || DEFAULT_NAME; + director.playerName = name; + audio.init(); audio.startMusic(); + this.scene.start('HouseScene', { spawn: 'default' }); + this.scene.launch('UIScene'); + this.scene.launch('DebugScene'); + return; + } + if (e.key === 'Backspace') { + this.nameText = this.nameText.slice(0, -1); + } else if (e.key.length === 1 && this.nameText.length < 8) { + // accept printable; allow CJK via composition not supported simply, but single chars ok + this.nameText += e.key; + } + nameBox.setText(this.nameText || DEFAULT_NAME); + }); + + // blink hint + this.time.addEvent({ delay: 600, loop: true, callback: () => { startHint.alpha = startHint.alpha > 0.5 ? 0.3 : 1; } }); + } +} diff --git a/GLM5.2/src/scenes/UIScene.ts b/GLM5.2/src/scenes/UIScene.ts new file mode 100644 index 0000000..7fa745d --- /dev/null +++ b/GLM5.2/src/scenes/UIScene.ts @@ -0,0 +1,239 @@ +// UIScene.ts: persistent overlay. Dialogue box, sunflower HUD, "按 E" prompt, +// saturation/vignette camera FX driven by director, day/transition overlays. +import Phaser from 'phaser'; +import { director } from '../systems/CorruptionDirector'; +import { corrupt, tickTurn } from '../systems/TextCorruptor'; +import { EROSION_MATRIX } from '../data/gamedata'; +import { audio } from '../systems/AudioEngine'; + +export interface DialogueChoice { label: string; type: 'gentle'|'doubt'|'deny'; } +export interface DialogueLine { speaker?: string; npcKey?: string; text: string; portrait?: number; choices?: DialogueChoice[]; glitch?: boolean; } + +class UIScene extends Phaser.Scene { + box!: Phaser.GameObjects.Container; + nameText!: Phaser.GameObjects.Text; + bodyText!: Phaser.GameObjects.Text; + portrait!: Phaser.GameObjects.Image; + choiceTexts: Phaser.GameObjects.Text[] = []; + prompt!: Phaser.GameObjects.Text; + hud!: Phaser.GameObjects.Container; + petalImages: Phaser.GameObjects.Rectangle[] = []; + centerSeed!: Phaser.GameObjects.Arc; + dayLabel!: Phaser.GameObjects.Text; + + private currentLines: DialogueLine[] = []; + private idx = 0; + private active = false; + private choiceCb: ((c: DialogueChoice)=>void) | null = null; + private typing = false; + private fullText = ''; + private typeStart = 0; + private negativeFx: Phaser.GameObjects.Rectangle | null = null; + + constructor() { super('UIScene'); } + + create() { + // Dialogue box (bottom banner) + this.box = this.add.container(0, 0).setDepth(1000).setVisible(false); + const bg = this.add.rectangle(160, 156, 312, 44, 0x000000, 0.78).setStrokeStyle(1, 0xffffff); + this.box.add(bg); + this.portrait = this.add.image(20, 156, 'portraits', 0).setOrigin(0, 0.5); + this.box.add(this.portrait); + this.nameText = this.add.text(44, 140, '', { fontFamily: 'monospace', fontSize: '9px', color: '#ffd86b' }); + this.box.add(this.nameText); + this.bodyText = this.add.text(44, 150, '', { fontFamily: 'monospace', fontSize: '8px', color: '#ffffff', wordWrap: { width: 256 } }); + this.box.add(this.bodyText); + for (let i=0;i<3;i++){ + const t = this.add.text(50 + i*88, 168, '', { fontFamily:'monospace', fontSize:'8px', color:'#bfe3ff' }); + this.box.add(t); this.choiceTexts.push(t); + } + + // "按 E" prompt + this.prompt = this.add.text(160, 96, '按 E', { fontFamily:'monospace', fontSize:'8px', color:'#ffffff', backgroundColor:'#000000' }).setOrigin(0.5).setDepth(900).setVisible(false); + + // Sunflower HUD (top-left). 12 petals. + this.hud = this.add.container(4, 8).setDepth(950); + const cx = 10, cy = 10; + this.add.rectangle(cx, cy, 22, 22, 0x000000, 0.35).setOrigin(0.5).setDepth(949); + // draw 12 petals around + for (let i=0;i<12;i++){ + const ang = (i/12)*Math.PI*2 - Math.PI/2; + const r = 8; + const px = cx + Math.cos(ang)*r; + const py = cy + Math.sin(ang)*r; + const petal = this.add.rectangle(px, py, 4, 4, 0x553311).setOrigin(0.5); + this.hud.add(petal); + this.petalImages.push(petal); + } + this.centerSeed = this.add.circle(cx, cy, 3, 0xc8862a).setDepth(951); + this.centerSeed.setVisible(false); + this.dayLabel = this.add.text(160, 4, `第 1 天`, { fontFamily:'monospace', fontSize:'8px', color:'#ffe9b0' }).setOrigin(0.5,0).setDepth(950); + + // hook events + director.on('change', () => this.refreshHud()); + director.on('stage', () => this.applyErosionFx()); + director.on('negativeFlash', () => this.flashNegative()); + director.on('day', () => { this.dayLabel.setText(`第 ${director.day} 天`); }); + this.refreshHud(); + this.applyErosionFx(); + + // typewriter timer + this.time.addEvent({ delay: 30, loop: true, callback: () => this.typeTick() }); + + // input: E advances dialogue + const kb = this.input.keyboard!; + kb.on('keydown-SPACE', () => this.advance()); + kb.on('keydown-E', () => this.advance()); + kb.on('keydown-ENTER', () => this.advance()); + kb.on('keydown-ONE', () => this.pickChoice(0)); + kb.on('keydown-TWO', () => this.pickChoice(1)); + kb.on('keydown-THREE', () => this.pickChoice(2)); + } + + // Dialogue API + startDialogue(lines: DialogueLine[], onDone?: () => void, onChoice?: (c: DialogueChoice)=>void) { + this.currentLines = lines; + this.idx = 0; + this.active = true; + this._onDone = onDone || null; + this.choiceCb = onChoice || null; + this.box.setVisible(true); + this.showLine(); + } + private _onDone: (()=>void) | null = null; + + private showLine() { + const line = this.currentLines[this.idx]; + if (!line) { this.endDialogue(); return; } + tickTurn(); + const stage = director.stage; + let text = line.text; + if (line.npcKey) { + text = corrupt(text, stage, line.npcKey); + } else if (line.glitch) { + // leave as is + } + if (line.speaker) this.nameText.setText(line.speaker); else this.nameText.setText(''); + if (line.portrait !== undefined) { + this.portrait.setVisible(true).setFrame(line.portrait); + } else this.portrait.setVisible(false); + this.fullText = text; + this.bodyText.setText(''); + this.typing = true; + this.typeStart = this.time.now; + // choices + this.choiceTexts.forEach(t => t.setText('')); + if (line.choices && line.choices.length) { + line.choices.forEach((c, i) => { + if (this.choiceTexts[i]) this.choiceTexts[i].setText(`${i+1}. ${c.label}`); + }); + } + // beep on start + audio.sfxBlip(); + } + + private typeTick() { + if (!this.typing) return; + const elapsed = this.time.now - this.typeStart; + const n = Math.floor(elapsed / 30); + if (n >= this.fullText.length) { + this.bodyText.setText(this.fullText); + this.typing = false; + } else { + this.bodyText.setText(this.fullText.slice(0, n)); + } + } + + advance() { + if (!this.active) return; + const line = this.currentLines[this.idx]; + if (this.typing) { + // complete immediately + this.bodyText.setText(this.fullText); + this.typing = false; + return; + } + if (line && line.choices && line.choices.length) { + // waiting for choice pick; ignore + return; + } + this.idx++; + if (this.idx >= this.currentLines.length) { this.endDialogue(); } + else this.showLine(); + } + + private pickChoice(i: number) { + if (!this.active) return; + const line = this.currentLines[this.idx]; + if (!line || !line.choices) return; + const c = line.choices[i]; + if (!c) return; + // apply generic effects per §6.1/6.2. Note: 'deny' sadness is applied by the + // scene's choice callback (拒绝对话 vs 拒绝任务 are distinct §6.3 behaviors), + // so we only apply the gentle/doubt numeric effects here to avoid double-counting. + if (c.type === 'gentle') { director.addHappiness(2); } + else if (c.type === 'doubt') { director.addAwareness(2); } + if (this.choiceCb) this.choiceCb(c); + this.idx++; + if (this.idx >= this.currentLines.length) this.endDialogue(); + else this.showLine(); + } + + private endDialogue() { + this.active = false; + this.box.setVisible(false); + const cb = this._onDone; this._onDone = null; + if (cb) cb(); + } + + isDialogueActive() { return this.active; } + + showPrompt(text: string) { + this.prompt.setText(text).setVisible(true); + } + hidePrompt() { this.prompt.setVisible(false); } + + // HUD refresh + refreshHud() { + const h = director.happiness; + // petals lit = floor(happiness / 100 * 12) + const lit = Math.round((h / 100) * 12); + for (let i=0;i<12;i++){ + const on = i < lit; + this.petalImages[i].fillColor = on ? 0xffd23b : 0x553311; + } + // black seeds appear at S4 / happiness 100 + const row = EROSION_MATRIX.find(r => r.stage === director.stage); + if (row && row.ui.blackSeeds) this.centerSeed.setVisible(true); + else this.centerSeed.setVisible(h >= 100); + // S3 petals over-bright + if (director.stage === 'S3') this.petalImages.forEach(p => p.fillColor = p.fillColor === 0xffd23b ? 0xffec80 : p.fillColor); + this.dayLabel.setText(`第 ${director.day} 天`); + } + + // Camera FX: we expose saturation/vignette values that map scenes read each frame + // to drive their own camera postFX (so the FX attaches to the active gameplay camera). + applyErosionFx() { + const stage = director.stage; + const row = EROSION_MATRIX.find(r => r.stage === stage)!; + this._sat = row.saturation; + this._vignette = (stage === 'S3') ? 0.25 : (stage === 'S4') ? 0.45 : 0; + audio.setErosion(stage); + } + + // We expose a saturation value that map scenes read in their update to drive a color matrix. + private _sat = 1.0; + private _vignette = 0; + getSaturation() { return this._sat; } + getVignette() { return this._vignette; } + + // single-frame negative flash + flashNegative() { + if (this.negativeFx) this.negativeFx.destroy(); + const r = this.add.rectangle(160, 90, 320, 180, 0xffffff, 1).setDepth(2000); + this.negativeFx = r; + this.time.delayedCall(60, () => { r.destroy(); this.negativeFx = null; }); + } +} + +export { UIScene }; diff --git a/GLM5.2/src/scenes/maps/BakeryScene.ts b/GLM5.2/src/scenes/maps/BakeryScene.ts new file mode 100644 index 0000000..82cd548 --- /dev/null +++ b/GLM5.2/src/scenes/maps/BakeryScene.ts @@ -0,0 +1,72 @@ +// BakeryScene.ts: 面包房. 圆圆 (baker), recipe paper (C2 from D3), knead (T11 D5, can refuse), bread pickup (T1/T8). +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { story } from '../../systems/StoryDirector'; +import { NPCS } from '../../data/gamedata'; +import { DialogueLine, DialogueChoice } from '../UIScene'; +import { audio } from '../../systems/AudioEngine'; + +export class BakeryScene extends MapScene { + yySprite!: Phaser.GameObjects.Sprite; + constructor() { super('BakeryScene', 'map:bakery', 'bakery'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + this.yySprite = this.add.sprite(3*16+8, 7*16+8, 'npc', NPCS.yuanyuan.fieldSprite).setOrigin(0.5,0.9); + (this.yySprite as any).npcKey = 'yuanyuan'; + this.npcSprites.push(this.yySprite); + + // talk to yuanyuan + this.addInteract(3, 7, () => this.talkYY(), { promptText: '按 E 和圆圆说话' }); + // recipe paper (C2 from D3) + this.addInteract(11, 5, () => { + if (director.day >= 3) { + const got = director.discoverClue('C2'); + this.say([{ speaker:'', text: got ? '配方纸背面写着:「按疗程配比。甜度:最大。」' : '配方纸背面那行字还在。' }]); + } else { + this.say([{ speaker:'', text:'一张蜂蜜面包的配方。' }]); + } + }, { promptText: '按 E 看配方纸' }); + + // knead dough (T11 D5, can refuse) + this.addInteract(3, 6, () => this.knead(), { promptText: '按 E 揉面', enabled: () => director.day === 5 && !director.tasksDone.has('T11') }); + // bread pickup for T1/T8 (take honey bread to deliver) + this.addInteract(2, 5, () => this.takeBread(), { promptText: '按 E 取面包', enabled: () => (director.day === 1 || director.day === 4) }); + } + + talkYY() { + if (director.noteNpcTalk('yuanyuan')) director.sadness('对同一 NPC 一天内对话 ≥5 次(纠缠)', 'yuanyuan'); + story.recordGreet('yuanyuan'); + // letter deliver (T4 day2) + if (director.day === 2 && story.lettersTaken[2]) { + story.deliverLetter('yuanyuan', (l) => this.say(l)); + return; + } + this.say(story.npcTalk('yuanyuan')); + } + + takeBread() { + story.takeBread((l) => this.say(l)); + } + + knead() { + const choices: DialogueChoice[] = [ + { label: '好呀,我来帮你(温柔)', type: 'gentle' }, + { label: '我……不太想(迟疑)', type: 'doubt' }, + { label: '不想做(拒绝)', type: 'deny' }, + ]; + this.say([ + { speaker:'圆圆', npcKey:'yuanyuan', portrait: NPCS.yuanyuan.portrait.smile, text:'能帮我揉揉面吗?', choices }, + ], undefined, (c: DialogueChoice) => { + if (c.type === 'deny') { + director.rejectedTasks.add('T11'); + director.sadness('拒绝任务', 'yuanyuan'); + this.say([{ speaker:'圆圆', npcKey:'yuanyuan', portrait: NPCS.yuanyuan.portrait.smile, text:'没关系哦,那……也没关系。' }]); + } else { + story.completeTask('T11', 10); + audio.sfxDone(); + this.say([{ speaker:'圆圆', npcKey:'yuanyuan', portrait: NPCS.yuanyuan.portrait.smile, text:'谢谢你!面团好软,对不对?' }]); + } + }); + } +} diff --git a/GLM5.2/src/scenes/maps/ClinicScene.ts b/GLM5.2/src/scenes/maps/ClinicScene.ts new file mode 100644 index 0000000..cef90b0 --- /dev/null +++ b/GLM5.2/src/scenes/maps/ClinicScene.ts @@ -0,0 +1,39 @@ +// ClinicScene.ts: 诊所. 芙医生 (dr_fu - never corrupts), table chart (C8 from D5). +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { story } from '../../systems/StoryDirector'; +import { NPCS } from '../../data/gamedata'; +import { DialogueLine } from '../UIScene'; + +export class ClinicScene extends MapScene { + fuSprite!: Phaser.GameObjects.Sprite; + constructor() { super('ClinicScene', 'map:clinic', 'clinic'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + this.fuSprite = this.add.sprite(5*16+8, 5*16+8, 'npc', NPCS.dr_fu.fieldSprite).setOrigin(0.5,0.9); + (this.fuSprite as any).npcKey = 'dr_fu'; + this.npcSprites.push(this.fuSprite); + + this.addInteract(5, 5, () => { + if (director.noteNpcTalk('dr_fu')) director.sadness('对同一 NPC 一天内对话 ≥5 次(纠缠)', 'dr_fu'); + story.recordGreet('dr_fu'); + // letter deliver (T4 day2) + if (director.day === 2 && story.lettersTaken[2]) { + story.deliverLetter('dr_fu', (l) => this.say(l)); + return; + } + this.say(story.npcTalk('dr_fu')); + }, { promptText: '按 E 和芙医生说话' }); + + // C8 chart (from D5) + this.addInteract(9, 3, () => { + if (director.day >= 5) { + const got = director.discoverClue('C8'); + this.say([{ speaker:'', text: got ? '桌上的表格抬头被手肘挡住,只露出:「……谷情绪疗……」' : '表格的抬头还是被挡着。' }]); + } else { + this.say([{ speaker:'芙医生', npcKey:'dr_fu', portrait: NPCS.dr_fu.portrait.smile, text:'这里没什么好看的哦。你不需要来这里的。' }]); + } + }, { promptText: '按 E 看表格' }); + } +} diff --git a/GLM5.2/src/scenes/maps/FieldScene.ts b/GLM5.2/src/scenes/maps/FieldScene.ts new file mode 100644 index 0000000..d9f2926 --- /dev/null +++ b/GLM5.2/src/scenes/maps/FieldScene.ts @@ -0,0 +1,43 @@ +// FieldScene.ts: 花田. berry bushes (T6 day3), watering point, scattered letters C3 (D2). +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { story } from '../../systems/StoryDirector'; +import { audio } from '../../systems/AudioEngine'; + +export class FieldScene extends MapScene { + constructor() { super('FieldScene', 'map:field', 'field'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + const berries = [[4,4],[8,3],[12,5],[6,8],[14,9]]; + berries.forEach(([tx,ty], i) => { + this.addInteract(tx, ty, () => this.pickBerry(i), { + promptText: '按 E 采莓果', + enabled: () => director.day === 3 && !director.tasksDone.has('T6-berry-'+i), + }); + }); + // watering point + this.addInteract(2, 11, () => { + director.smallInteraction('water'); + this.say([{ speaker:'', text:'浇了浇花。' }]); + }, { promptText: '按 E 浇花' }); + // C3 scattered letters (D2): place an interactable + this.addInteract(1, 12, () => { + if (director.day >= 2) { + const got = director.discoverClue('C3'); + this.say([{ speaker:'', text: got ? '散落的信件……所有邮戳都是同一天。' : '地上的信件,邮戳都一样。' }]); + } else { + this.say([{ speaker:'', text:'花田边散落着几封信。' }]); + } + }, { promptText: '按 E 捡信件' }); + } + + pickBerry(i: number) { + director.tasksDone.add('T6-berry-'+i); + director.smallInteraction('berry'); + audio.sfxBlip(); + const picked = [0,1,2,3,4].filter(k => director.tasksDone.has('T6-berry-'+k)).length; + this.say([{ speaker:'', text:`采到一颗红红的莓果!(${picked}/5)` }]); + if (picked >= 5) story.completeTask('T6', 10); + } +} diff --git a/GLM5.2/src/scenes/maps/FogBoundaryScene.ts b/GLM5.2/src/scenes/maps/FogBoundaryScene.ts new file mode 100644 index 0000000..cec1422 --- /dev/null +++ b/GLM5.2/src/scenes/maps/FogBoundaryScene.ts @@ -0,0 +1,52 @@ +// FogBoundaryScene.ts: 雾墙边界. Before D5 cannot approach (dissuaded). E3 exit. +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; + +export class FogBoundaryScene extends MapScene { + gazeTimer = 0; + constructor() { super('FogBoundaryScene', 'map:fog_boundary', 'fog_boundary'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + this.addInteract(12, 4, () => this.fogInteract(), { promptText: '按 E 看雾墙' }); + // E3 exit + this.addInteract(13, 4, () => this.tryE3Exit(), { + promptText: '按 E 走进雾墙', + enabled: () => director.day >= 5 && director.awareness >= 70 && director.clues.size >= 6, + }); + } + + fogInteract() { + if (director.day < 5) { + this.say([ + { speaker:'', text:'你想靠近村庄尽头的雾……' }, + { speaker:'路过的村民', text:'那边走不得呀,那边没有路。回来吧。' }, + { speaker:'', text:'(你被温柔地劝了回来。)' }, + ]); + return; + } + director.gaze('fog'); + this.say([{ speaker:'', text:'雾墙在村庄尽头翻涌,里面好像有什么……白的。' }]); + } + + tryE3Exit() { + if (director.awareness >= 70 && director.clues.size >= 6) { + // go to white layer (E3) + director.tasksDone.add('T15'); + this.scene.stop('UIScene'); + this.scene.stop('DebugScene'); + this.scene.start('WhiteLayerScene', { spawn: 'default' }); + } else { + this.say([{ speaker:'', text:'雾墙太厚了,你走不进去……' }]); + } + } + + update(time: number, delta: number) { + super.update(time, delta); + const near = Phaser.Math.Distance.Between(this.player.x, this.player.y, 12*16+8, 4*16+8) < 24; + if (near) { + this.gazeTimer += delta; + if (this.gazeTimer >= 5000) { this.gazeTimer = 0; director.gaze('fog'); } + } else this.gazeTimer = 0; + } +} diff --git a/GLM5.2/src/scenes/maps/HouseScene.ts b/GLM5.2/src/scenes/maps/HouseScene.ts new file mode 100644 index 0000000..21e87e0 --- /dev/null +++ b/GLM5.2/src/scenes/maps/HouseScene.ts @@ -0,0 +1,67 @@ +// HouseScene.ts: 小满小屋. Bed (sleep to advance day), calendar (C1), door to plaza. +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { story } from '../../systems/StoryDirector'; +import { audio } from '../../systems/AudioEngine'; +import { CLUES } from '../../data/gamedata'; + +export class HouseScene extends MapScene { + constructor() { super('HouseScene', 'map:house', 'house'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + // sleep point + this.addInteract(6, 5, () => this.sleep(), { promptText: '按 E 睡觉' }); + // calendar C1 (from D2) + this.addInteract(2, 0, () => { + const avail = director.day >= 2; + if (avail) { + const got = director.discoverClue('C1'); + this.say([{ speaker:'', text: got ? '日历上每一页都是同一个日期……' : '日历上还是同一个日期。' }]); + } else { + this.say([{ speaker:'', text: '今天的日历,崭新的一页。' }]); + } + }, { promptText: '按 E 看日历' }); + // bed interaction (also can sleep) + this.addInteract(1, 5, () => this.sleep(), { promptText: '按 E 睡觉' }); + + // if just spawned 'default' -> it's a new-game start: morning of Day 1 + if (this.pendingSpawn === 'default' && director.day === 1) { + story.resetDay(); + this.time.delayedCall(400, () => { + this.say([{ speaker:'', text:`第 ${director.day} 天的清晨。阳光真好。` }]); + }); + } + } + + sleep() { + if (director.day >= 7) { + // go to festival / ending + this.runEnding(); + return; + } + // dream sequence then advance day + this.dreamSequence(() => { + director.advanceDay(); + story.resetDay(); + // re-spawn into house morning + this.scene.restart({ spawn: 'default' }); + }); + } + + dreamSequence(onDone: ()=>void) { + const stage = director.stage; + // S3+ : brief white ward flash 1s + let flash = false; + if (stage === 'S3' || stage === 'S4') flash = true; + const overlay = this.add.rectangle(160, 90, 320, 180, 0xffffff, 0).setScrollFactor(0).setDepth(6000); + this.tweens.add({ targets: overlay, alpha: flash ? 0.9 : 0.0, duration: flash ? 700 : 200, yoyo: flash, hold: 200, + onComplete: () => { overlay.destroy(); onDone(); } }); + } + + runEnding() { + this.scene.stop('UIScene'); + this.scene.stop('DebugScene'); + this.scene.start('EndingScene', { ending: director.determineEnding() }); + } +} diff --git a/GLM5.2/src/scenes/maps/LakeScene.ts b/GLM5.2/src/scenes/maps/LakeScene.ts new file mode 100644 index 0000000..15657c8 --- /dev/null +++ b/GLM5.2/src/scenes/maps/LakeScene.ts @@ -0,0 +1,48 @@ +// LakeScene.ts: 湖畔. 龟爷爷 (grandpa - C9 progression), picnic (T7), lake reflection show. +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { story } from '../../systems/StoryDirector'; +import { NPCS } from '../../data/gamedata'; +import { DialogueLine } from '../UIScene'; +import { audio } from '../../systems/AudioEngine'; + +export class LakeScene extends MapScene { + gpSprite!: Phaser.GameObjects.Sprite; + reflectionDelay = 0; + constructor() { super('LakeScene', 'map:lake', 'lake'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + this.gpSprite = this.add.sprite(13*16+8, 10*16+8, 'npc', NPCS.grandpa.fieldSprite).setOrigin(0.5,0.9); + (this.gpSprite as any).npcKey = 'grandpa'; + this.npcSprites.push(this.gpSprite); + + this.addInteract(13, 10, () => { + if (director.noteNpcTalk('grandpa')) director.sadness('对同一 NPC 一天内对话 ≥5 次(纠缠)', 'grandpa'); + story.recordGreet('grandpa'); + // bread deliver (T1/T8) + if ((director.day === 1 || director.day === 4) && story.breadTaken[director.day]) { + story.deliverBread('grandpa', (l) => this.say(l)); + return; + } + this.say(story.npcTalk('grandpa')); + }, { promptText: '按 E 和龟爷爷说话' }); + + // picnic (T7 day3) + this.addInteract(16, 11, () => this.picnic(), { promptText: '按 E 野餐', enabled: () => director.day === 3 && !director.tasksDone.has('T7') }); + + // lake reflection (S2+: reflection delays 1 frame) + this.addInteract(7, 7, () => { + this.say([{ speaker:'', text:'湖面平静如镜。你看见自己的倒影……好像慢了半拍。' }]); + }, { promptText: '按 E 看湖面' }); + } + + picnic() { + story.completeTask('T7', 15); + audio.sfxDone(); + this.say([ + { speaker:'', text:'铺开野餐布,阳光洒在湖面上。' }, + { speaker:'', text:'(湖面的倒影……比动作慢了一帧。)' }, + ]); + } +} diff --git a/GLM5.2/src/scenes/maps/PlazaScene.ts b/GLM5.2/src/scenes/maps/PlazaScene.ts new file mode 100644 index 0000000..d43117d --- /dev/null +++ b/GLM5.2/src/scenes/maps/PlazaScene.ts @@ -0,0 +1,281 @@ +// PlazaScene.ts: hub. NPC 阿福 + twin birds. Board (announcements), flowerbeds (T2/T9 water), +// greet point (T3/T10), ribbons (T13), T14 smile spot, festival (T15). +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { story } from '../../systems/StoryDirector'; +import { NPCS, AFU_ANNOUNCE, T14_DURATION, T14_SILENCE_START } from '../../data/gamedata'; +import { DialogueLine, DialogueChoice } from '../UIScene'; +import { audio } from '../../systems/AudioEngine'; + +export class PlazaScene extends MapScene { + afuSprite!: Phaser.GameObjects.Sprite; + dabaoSprite!: Phaser.GameObjects.Sprite; + birdSprite!: Phaser.GameObjects.Sprite; + ribbons: Phaser.GameObjects.Image[] = []; + rainGfx: Phaser.GameObjects.Graphics | null = null; + raining = false; + rainTimer = 0; + + constructor() { super('PlazaScene', 'map:plaza', 'plaza'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + // 阿福 at board + this.afuSprite = this.add.sprite(8*16+8, 8*16+8, 'npc', NPCS.afu.fieldSprite).setOrigin(0.5, 0.9); + (this.afuSprite as any).npcKey = 'afu'; + this.npcSprites.push(this.afuSprite); + // 大宝 (postman) at village gate (left side of plaza) + this.dabaoSprite = this.add.sprite(3*16+8, 6*16+8, 'npc', NPCS.dabao.fieldSprite).setOrigin(0.5, 0.9); + (this.dabaoSprite as any).npcKey = 'dabao'; + this.npcSprites.push(this.dabaoSprite); + // twin birds on tree (flapping) + this.birdSprite = this.add.sprite(15*16+8, 3*16+8, 'npc', 15).setOrigin(0.5,0.9); + this.birdSprite.play('bird-flap'); + (this.birdSprite as any).npcKey = 'bird'; + this.npcSprites.push(this.birdSprite); + + // interact: talk to afu + this.addInteract(8, 8, () => this.talkAfu(), { promptText: '按 E 和阿福说话' }); + // interact: 大宝 (greet + letter pickup T4 + bread deliver) + this.addInteract(3, 6, () => this.talkDabao(), { promptText: '按 E 和大宝说话' }); + // interact: board -> announcement + today's tasks + this.addInteract(7, 7, () => this.readBoard(), { promptText: '按 E 看公告板' }); + // interact: birds + this.addInteract(15, 2, () => { + story.recordGreet('bird'); + this.say([{ speaker:'双子鸟', npcKey:'bird', portrait: NPCS.bird.portrait.smile, text:'(鸟儿欢快地歌唱着,歌声里好像有些……单调?)' }]); + }, { promptText: '按 E 听鸟鸣' }); + + // flowerbed water points (T2 day1 / T9 day4) + const waterPoints = [[4,4],[5,4],[4,5]]; + waterPoints.forEach(([tx,ty], i) => { + this.addInteract(tx, ty, () => this.waterFlower(i), { promptText: '按 E 浇花' }); + }); + + // plaza scenery marker (moved away from smile spot to avoid conflict) + this.addInteract(20, 12, () => { + this.say([{ speaker:'', text:'广场上阳光明媚,花儿开得很艳。' }]); + }, { promptText: '按 E 看花坛' }); + + // ribbon hang points (T13 day6) + const ribbonPoints = [[10,3],[20,3],[10,14],[20,14]]; + ribbonPoints.forEach(([tx,ty], i) => { + this.addInteract(tx, ty, () => this.hangRibbon(i, tx, ty), { + promptText: '按 E 挂彩带', + enabled: () => director.day === 6 && !director.tasksDone.has('T13-done-'+i), + }); + }); + + // T14 smile spot (day6): standing near center triggers when idle handled elsewhere + this.addInteract(15, 10, () => this.startSmileTask(), { + promptText: '按 E 保持微笑', + enabled: () => director.day === 6 && !director.tasksDone.has('T14'), + }); + + // festival (day7): board leads to T15 + this.addInteract(7, 7, () => this.festival(), { + promptText: '按 E 参加庆典', + enabled: () => director.day === 7, + }); + + // T12 wipe board (day5, can refuse) -> C6 + this.addInteract(7, 7, () => this.wipeBoard(), { + promptText: '按 E 擦公告板', + enabled: () => director.day === 5 && !director.tasksDone.has('T12'), + }); + + // extras (群演) walking + this.spawnExtras(); + + // Day6 evening rain + if (director.day === 6 && story.phase === 'evening') this.startRain(); + } + + spawnExtras() { + const positions = [[12,12],[18,12],[22,8],[6,6],[24,6]]; + positions.forEach((p, i) => { + const sp = this.add.sprite(p[0]*16+8, p[1]*16+8, 'npc', 20 + (i%5)).setOrigin(0.5,0.9); + (sp as any).npcKey = 'extra'+i; + this.npcSprites.push(sp); + // gentle wander + this.time.addEvent({ delay: 1500 + i*300, loop: true, callback: () => { + // S4: one extra stops and faces player + if (director.stage === 'S4' && i === 0) { sp.setFlipX(this.player.x < sp.x); return; } + sp.x += Phaser.Math.Between(-8,8); sp.y += Phaser.Math.Between(-8,8); + sp.setFlipX(this.player.x < sp.x); + }}); + }); + } + + talkAfu() { + if (director.noteNpcTalk('afu')) director.sadness('对同一 NPC 一天内对话 ≥5 次(纠缠)', 'afu'); + story.recordGreet('afu'); + this.say(story.npcTalk('afu')); + } + + talkDabao() { + if (director.noteNpcTalk('dabao')) director.sadness('对同一 NPC 一天内对话 ≥5 次(纠缠)', 'dabao'); + story.recordGreet('dabao'); + // letter pickup (T4 day2) + if (director.day === 2 && !story.lettersTaken[2]) { + story.takeLetters((l) => this.say(l)); + return; + } + // bread deliver (T1/T8) + if ((director.day === 1 || director.day === 4) && story.breadTaken[director.day]) { + story.deliverBread('dabao', (l) => this.say(l)); + return; + } + this.say(story.npcTalk('dabao')); + } + + readBoard() { + // show announcement (verbatim) + const lines = story.announcementLines(); + // also list today's tasks + const tasks = story.todayTasks(); + const taskLine = tasks.length ? `今日任务:${tasks.map(t=>t.id).join('、')}` : '今天公告板空空的。'; + lines.push({ speaker:'', text: taskLine }); + this.say(lines); + } + + waterFlower(idx: number) { + // T2 (day1) / T9 (day4) + const isWaterDay = director.day === 1 || director.day === 4; + const taskId = director.day === 1 ? 'T2' : 'T9'; + if (isWaterDay) { + const key = `${taskId}@${idx}`; + if (!director.tasksDone.has(key)) { + director.tasksDone.add(key); + director.smallInteraction('water'); + const count = director.day === 1 ? 3 : 3; + const done = [0,1,2].filter(i => director.tasksDone.has(`${taskId}@${i}`)).length; + this.say([{ speaker:'', text:'花儿喝饱了水,开得更艳了。' }]); + if (done >= count) story.completeTask(taskId, 10); + } else { + this.say([{ speaker:'', text:'这株已经浇过了。' }]); + } + } else { + director.smallInteraction('water'); + this.say([{ speaker:'', text:'花儿喝饱了水。' }]); + } + } + + hangRibbon(i: number, tx: number, ty: number) { + director.tasksDone.add('T13-done-'+i); + const rib = this.add.image(tx*16+8, ty*16+8, 'tileset', 35).setOrigin(0.5,0.5).setDepth(20); + this.ribbons.push(rib); + director.smallInteraction('ribbon'); + audio.sfxDone(); + const hung = [0,1,2,3].filter(k => director.tasksDone.has('T13-done-'+k)).length; + if (hung >= 4) story.completeTask('T13', 10); + this.say([{ speaker:'', text:'彩带挂好了,真漂亮。' }]); + } + + startSmileTask() { + // T14: stand still 60s, camera zoom in, all NPCs face player, 40s sound fades + director.tasksDone.add('T14'); + this.player.atTaskPoint = true; // don't count as 发呆 during smile task + const cam = this.cameras.main; + const startZoom = cam.zoom; + const startTime = this.time.now; + // freeze player movement by setting a flag via player + (this.player as any).frozen = true; + // all npcs face player + this.npcSprites.forEach(s => s.setFlipX(this.player.x < s.x)); + audio.sfxBell(); + const overlay = this.add.text(160, 20, '保持微笑……', { fontFamily:'monospace', fontSize:'9px', color:'#ffd86b' }).setOrigin(0.5).setScrollFactor(0).setDepth(7000); + let silenceSet = false; + const ev = this.time.addEvent({ delay: 200, loop: true, callback: () => { + const elapsed = (this.time.now - startTime)/1000; + // zoom in slowly from 1 to 1.8 over 60s + cam.zoom = startZoom + (1.0) * (elapsed / T14_DURATION); + // sound fade 40-60s + if (elapsed >= T14_SILENCE_START) { + const f = 1 - (elapsed - T14_SILENCE_START) / (T14_DURATION - T14_SILENCE_START); + audio.setSilenceFactor(Math.max(0, f)); + silenceSet = true; + } + if (elapsed >= T14_DURATION) { + ev.remove(); + (this.player as any).frozen = false; + this.player.atTaskPoint = false; + cam.zoom = startZoom; + audio.setSilenceFactor(1.0); + overlay.destroy(); + story.completeTask('T14', 15); + this.say([{ speaker:'阿福', npcKey:'afu', portrait: NPCS.afu.portrait.smile, text:'做得真好。你笑得真好看。' }]); + } + }}); + } + + festival() { + // T15 -> ending + director.tasksDone.add('T15'); + this.scene.stop('UIScene'); + this.scene.stop('DebugScene'); + this.scene.start('EndingScene', { ending: director.determineEnding() }); + } + + wipeBoard() { + const choices: DialogueChoice[] = [ + { label: '好,我来擦(温柔)', type: 'gentle' }, + { label: '这字迹……(迟疑)', type: 'doubt' }, + { label: '不想擦(拒绝)', type: 'deny' }, + ]; + this.say([ + { speaker:'', text:'公告板上落了灰。' }, + { speaker:'阿福', npcKey:'afu', portrait: NPCS.afu.portrait.smile, text:'帮忙擦擦公告板好吗?', choices }, + ], undefined, (c: DialogueChoice) => { + if (c.type === 'deny') { + director.rejectedTasks.add('T12'); + director.sadness('拒绝任务', 'afu'); + this.say([{ speaker:'阿福', npcKey:'afu', portrait: NPCS.afu.portrait.smile, text:'没关系哦。' }]); + } else { + story.completeTask('T12', 10); + audio.sfxDone(); + director.discoverClue('C6'); + this.say([{ speaker:'', text:'擦到一半,浮现出旧字迹:' }, + { speaker:'', text:'「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」……' }]); + } + }); + } + + startRain() { + this.raining = true; + this.rainGfx = this.add.graphics().setDepth(4000); + this.time.addEvent({ delay: 60, loop: true, callback: () => this.drawRain() }); + } + private drawRain() { + if (!this.rainGfx) return; + this.rainGfx.clear(); + this.rainGfx.fillStyle(0x9bb8d0, 0.5); + for (let i=0;i<60;i++) { + const x = Phaser.Math.Between(0, 320*3), y = Phaser.Math.Between(0, 180*3); + this.rainGfx.fillRect((x + this.time.now*0.1)%480 - 80, (y + this.time.now*0.2)%300 - 60, 1, 4); + } + // rain in rain stand detection handled in update below via director + } + + update(time: number, delta: number) { + super.update(time, delta); + // Day6 evening rain sadness: stand in rain >=10s + if (this.raining) { + // assume whole plaza is "in rain" + if (this.player.moving === false) { + this.rainTimer += delta; + } else this.rainTimer = 0; + if (this.rainTimer >= 10000) { + this.rainTimer = 0; + director.sadness('雨天(Day 6)站在雨里 ≥10 秒', this.nearestNpcKey()); + } + } + // NPC dialogue reaction to rain = zero reaction (by design, NPCs say nothing about rain) + // bird flap freeze when S2+ music detuned + if (director.stage === 'S2' || director.stage === 'S3') { + // stutter animation + if (Math.random() < 0.02) this.birdSprite.anims.pause(); + else this.birdSprite.anims.resume(); + } + } +} diff --git a/GLM5.2/src/scenes/maps/ShopScene.ts b/GLM5.2/src/scenes/maps/ShopScene.ts new file mode 100644 index 0000000..2f1fddc --- /dev/null +++ b/GLM5.2/src/scenes/maps/ShopScene.ts @@ -0,0 +1,58 @@ +// ShopScene.ts: 杂货店. 咪咪 (mimi - text corruption carrier), shelves (C5), count shelves (T5). +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { story } from '../../systems/StoryDirector'; +import { NPCS } from '../../data/gamedata'; +import { DialogueLine } from '../UIScene'; +import { audio } from '../../systems/AudioEngine'; + +export class ShopScene extends MapScene { + mimiSprite!: Phaser.GameObjects.Sprite; + constructor() { super('ShopScene', 'map:shop', 'shop'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + this.mimiSprite = this.add.sprite(7*16+8, 7*16+8, 'npc', NPCS.mimi.fieldSprite).setOrigin(0.5,0.9); + (this.mimiSprite as any).npcKey = 'mimi'; + this.npcSprites.push(this.mimiSprite); + + this.addInteract(7, 7, () => this.talkMimi(), { promptText: '按 E 和咪咪说话' }); + // shelves C5 (from D2) + T5 count (D2): merged interaction on shelves + this.addInteract(2, 2, () => { + // T5 count (D2): completing the count task + if (director.day === 2 && !director.tasksDone.has('T5')) { + story.completeTask('T5', 10); + audio.sfxDone(); + } + if (director.day >= 2) { + const got = director.discoverClue('C5'); + this.say([{ speaker:'咪咪', npcKey:'mimi', portrait: NPCS.mimi.portrait.smile, text:'数清楚啦?太谢谢你啦!' }, + { speaker:'', text: got ? '货架上同一商品重复排了 9 格,标签全是空白的。' : '货架上的商品……好像都一模一样。' }]); + } else { + this.say([{ speaker:'', text:'货架上商品满满当当。' }]); + } + }, { promptText: '按 E 看货架' }); + } + + talkMimi() { + if (director.noteNpcTalk('mimi')) director.sadness('对同一 NPC 一天内对话 ≥5 次(纠缠)', 'mimi'); + story.recordGreet('mimi'); + // letter deliver (T4 day2) + if (director.day === 2 && story.lettersTaken[2]) { + story.deliverLetter('mimi', (l) => this.say(l)); + return; + } + // bread deliver (T1/T8) + if ((director.day === 1 || director.day === 4) && story.breadTaken[director.day]) { + story.deliverBread('mimi', (l) => this.say(l)); + return; + } + this.say(story.npcTalk('mimi')); + } + + countShelves() { + story.completeTask('T5', 10); + audio.sfxDone(); + this.say([{ speaker:'咪咪', npcKey:'mimi', portrait: NPCS.mimi.portrait.smile, text:'数清楚啦?太谢谢你啦!大家都要开开心心的哦。' }]); + } +} diff --git a/GLM5.2/src/scenes/maps/WellAreaScene.ts b/GLM5.2/src/scenes/maps/WellAreaScene.ts new file mode 100644 index 0000000..8204e74 --- /dev/null +++ b/GLM5.2/src/scenes/maps/WellAreaScene.ts @@ -0,0 +1,43 @@ +// WellAreaScene.ts: 古井空地. 古井 (C7 from D5). Before D5, gentle NPC dissuades. +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { audio } from '../../systems/AudioEngine'; + +export class WellAreaScene extends MapScene { + gazeTimer = 0; + constructor() { super('WellAreaScene', 'map:well_area', 'well_area'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + this.addInteract(6, 4, () => this.wellInteract(), { promptText: '按 E 看古井' }); + } + + wellInteract() { + if (director.day < 5) { + // gentle dissuade (NPC leads away) + this.say([ + { speaker:'路过的村民', text:'哎呀,别靠近那口井呀。那里没什么好看的。' }, + { speaker:'', text:'(你被温柔地引开了。)' }, + ]); + return; + } + // D5+: interact 5s to hear faint heartbeat echo (C7) + this.say([{ speaker:'', text:'你俯身倾听井底……(保持 5 秒)' }], () => { + // begin gaze: count 5s of proximity handled in update; here grant immediately for simplicity of clue + director.gaze('well'); + const got = director.discoverClue('C7'); + this.say([{ speaker:'', text: got ? '井底回声里,有极轻的……监护仪的滴声。' : '井底的回声还是那样。' }]); + audio.sfxHeartbeat(); + }); + } + + update(time: number, delta: number) { + super.update(time, delta); + // gaze: standing near well for 5s counts once per day + const near = Phaser.Math.Distance.Between(this.player.x, this.player.y, 6*16+8, 4*16+8) < 24; + if (near) { + this.gazeTimer += delta; + if (this.gazeTimer >= 5000) { this.gazeTimer = 0; director.gaze('well'); } + } else this.gazeTimer = 0; + } +} diff --git a/GLM5.2/src/scenes/maps/WhiteLayerScene.ts b/GLM5.2/src/scenes/maps/WhiteLayerScene.ts new file mode 100644 index 0000000..0581d55 --- /dev/null +++ b/GLM5.2/src/scenes/maps/WhiteLayerScene.ts @@ -0,0 +1,27 @@ +// WhiteLayerScene.ts: 白色层 (E3 ward). Chart reveal + ending E3. +import Phaser from 'phaser'; +import { MapScene } from '../MapScene'; +import { director } from '../../systems/CorruptionDirector'; +import { audio } from '../../systems/AudioEngine'; + +export class WhiteLayerScene extends MapScene { + constructor() { super('WhiteLayerScene', 'map:white_layer', 'white_layer'); } + + onBuild(map: Phaser.Tilemaps.Tilemap, ts: Phaser.Tilemaps.Tileset) { + // desaturated: camera grayscale-ish via low saturation + this.cameras.main.setBackgroundColor('#e8e8e8'); + // chart + this.addInteract(9, 10, () => { + this.say([{ speaker:'', text:'桌上的病历……' }, + { speaker:'', text:`蜜糖谷情绪疗养中心` }, + { speaker:'', text:`姓名:${director.playerName}` }, + { speaker:'', text:`编号:PT-07` }, + { speaker:'', text:`疗程时长:${director.formatPlayTime()}` }, + ], () => { + // go to E3 ending + this.scene.stop('DebugScene'); + this.scene.start('EndingScene', { ending: 'E3' }); + }); + }, { promptText: '按 E 看病历' }); + } +} diff --git a/GLM5.2/src/systems/AudioEngine.ts b/GLM5.2/src/systems/AudioEngine.ts new file mode 100644 index 0000000..a5edfb7 --- /dev/null +++ b/GLM5.2/src/systems/AudioEngine.ts @@ -0,0 +1,196 @@ +// AudioEngine.ts: Web Audio procedural BGM (8-bar 8-bit loop) + 4 SFX. +// No external audio files. Driven by CorruptionDirector via setErosion(). +export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4'; + +export class AudioEngine { + ctx: AudioContext | null = null; + master: GainNode | null = null; + musicGain: GainNode | null = null; + sfxGain: GainNode | null = null; + lowpass: BiquadFilterNode | null = null; + // bird layer gain + birdGain: GainNode | null = null; + + private schedulerId: number | null = null; + private nextNoteTime = 0; + private step = 0; + private playbackRate = 1.0; + private muted = false; + private silenceFactor = 1.0; // multiplied into master for T14 sudden silence + private birdsMuted = false; + + init() { + if (this.ctx) return; + const AC = (window.AudioContext || (window as any).webkitAudioContext); + this.ctx = new AC(); + this.master = this.ctx.createGain(); + this.master.gain.value = 0.5; + this.master.connect(this.ctx.destination); + + this.lowpass = this.ctx.createBiquadFilter(); + this.lowpass.type = 'lowpass'; + this.lowpass.frequency.value = 20000; + + this.musicGain = this.ctx.createGain(); + this.musicGain.gain.value = 0.35; + this.birdGain = this.ctx.createGain(); + this.birdGain.gain.value = 0.5; + + this.musicGain.connect(this.lowpass); + this.birdGain.connect(this.lowpass); + this.lowpass.connect(this.master); + + this.sfxGain = this.ctx.createGain(); + this.sfxGain.gain.value = 0.4; + this.sfxGain.connect(this.master); + } + + resume() { + if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); + } + + setErosion(stage: Stage) { + const map: Record = { + S0: { rate: 1.00, lp: 20000, birds: false }, + S1: { rate: 1.00, lp: 20000, birds: false }, + S2: { rate: 0.97, lp: 20000, birds: false }, + S3: { rate: 0.92, lp: 8000, birds: false }, + S4: { rate: 0.84, lp: 4000, birds: true }, + }; + const s = map[stage]; + this.playbackRate = s.rate; + this.birdsMuted = s.birds; + if (this.lowpass && this.ctx) { + this.lowpass.frequency.setTargetAtTime(s.lp, this.ctx.currentTime, 0.3); + } + } + + // Gradual silence for T14 (40-60s): fade master to 0. + setSilenceFactor(f: number) { + this.silenceFactor = Math.max(0, Math.min(1, f)); + this.applyMaster(); + } + private applyMaster() { + if (!this.master || !this.ctx) return; + const v = this.muted ? 0 : 0.5 * this.silenceFactor; + this.master.gain.setTargetAtTime(v, this.ctx.currentTime, 0.05); + } + + startMusic() { + if (!this.ctx) this.init(); + if (!this.ctx) return; + this.resume(); + if (this.schedulerId !== null) return; + this.nextNoteTime = this.ctx.currentTime + 0.1; + this.step = 0; + const loop = () => { + this.schedule(); + this.schedulerId = window.setTimeout(loop, 25); + }; + loop(); + } + + stopMusic() { + if (this.schedulerId !== null) { clearTimeout(this.schedulerId); this.schedulerId = null; } + } + + // 8-bar loop in C major, gentle. 16th-note steps. Tempo ~ 96 bpm. + // melody (square) + bass (triangle) + bird chirps (occasional). + private melody: number[] = [ + // bar1 + 72,0,76,0, 79,0,76,0, 72,0,0,0, 0,0,74,0, + // bar2 + 76,0,0,0, 79,0,81,0, 83,0,0,79, 76,0,0,0, + // bar3 + 79,0,83,0, 86,0,83,0, 79,0,0,0, 0,0,77,0, + // bar4 + 76,0,74,0, 72,0,0,0, 0,0,0,0, 0,0,0,0, + // bar5 + 74,0,77,0, 81,0,77,0, 74,0,0,0, 0,0,76,0, + // bar6 + 77,0,0,0, 81,0,83,0, 84,0,0,81, 77,0,0,0, + // bar7 + 79,0,83,0, 86,0,88,0, 86,0,83,0, 79,0,0,0, + // bar8 + 76,0,79,0, 77,0,74,0, 72,0,0,0, 0,0,0,0, + ]; + private bass: number[] = [ + 48,0,0,0, 0,0,0,0, 55,0,0,0, 0,0,0,0, + 48,0,0,0, 0,0,0,0, 55,0,0,0, 0,0,0,0, + 53,0,0,0, 0,0,0,0, 57,0,0,0, 0,0,0,0, + 48,0,0,0, 0,0,0,0, 55,0,0,0, 0,0,0,0, + 50,0,0,0, 0,0,0,0, 57,0,0,0, 0,0,0,0, + 53,0,0,0, 0,0,0,0, 57,0,0,0, 0,0,0,0, + 55,0,0,0, 0,0,0,0, 60,0,0,0, 0,0,0,0, + 48,0,0,0, 0,0,0,0, 55,0,0,0, 0,0,0,0, + ]; + + private midiToFreq(m: number): number { + return 440 * Math.pow(2, (m - 69) / 12); + } + + private schedule() { + if (!this.ctx || !this.musicGain) return; + const secPerStep = (60 / 96) / 4 / this.playbackRate; // 16th note + while (this.nextNoteTime < this.ctx.currentTime + 0.2) { + const idx = this.step % this.melody.length; + const mn = this.melody[idx]; + const bn = this.bass[idx]; + if (mn > 0) this.playTone(this.midiToFreq(mn), this.nextNoteTime, secPerStep * 1.5, 'square', 0.18, this.musicGain); + if (bn > 0) this.playTone(this.midiToFreq(bn), this.nextNoteTime, secPerStep * 3.5, 'triangle', 0.22, this.musicGain); + // bird chirp occasionally on downbeats (step % 16 == 0) + if (!this.birdsMuted && idx % 16 === 0 && Math.random() < 0.5 && this.birdGain) { + this.playBird(this.nextNoteTime + Math.random() * 0.3); + } + this.nextNoteTime += secPerStep; + this.step++; + } + } + + private playTone(freq: number, when: number, dur: number, type: OscillatorType, vol: number, dest: AudioNode) { + if (!this.ctx) return; + const osc = this.ctx.createOscillator(); + const g = this.ctx.createGain(); + osc.type = type; + osc.frequency.value = freq; + g.gain.setValueAtTime(0, when); + g.gain.linearRampToValueAtTime(vol, when + 0.01); + g.gain.exponentialRampToValueAtTime(0.0001, when + dur); + osc.connect(g); g.connect(dest); + osc.start(when); osc.stop(when + dur + 0.02); + } + + private playBird(when: number) { + if (!this.ctx || !this.birdGain) return; + const base = 80 + Math.random() * 6; + // two quick notes + this.playTone(this.midiToFreq(base + 12), when, 0.08, 'square', 0.06, this.birdGain); + this.playTone(this.midiToFreq(base + 16), when + 0.09, 0.1, 'square', 0.06, this.birdGain); + } + + // SFX (only 4 per §6.6) + sfxBlip() { + if (!this.ctx || !this.sfxGain) return; + this.playTone(880, this.ctx.currentTime, 0.05, 'square', 0.3, this.sfxGain); + } + sfxDone() { + if (!this.ctx || !this.sfxGain) return; + const t = this.ctx.currentTime; + this.playTone(660, t, 0.08, 'square', 0.3, this.sfxGain); + this.playTone(880, t + 0.08, 0.08, 'square', 0.3, this.sfxGain); + this.playTone(1320, t + 0.16, 0.16, 'square', 0.3, this.sfxGain); + } + sfxBell() { + if (!this.ctx || !this.sfxGain) return; + const t = this.ctx.currentTime; + // bell-like: sine partials + for (const f of [440, 660, 880]) this.playTone(f, t, 0.8, 'sine', 0.2, this.sfxGain); + } + sfxHeartbeat() { + if (!this.ctx || !this.sfxGain) return; + const t = this.ctx.currentTime; + this.playTone(1000, t, 0.06, 'sine', 0.4, this.sfxGain); + } +} + +export const audio = new AudioEngine(); diff --git a/GLM5.2/src/systems/CorruptionDirector.ts b/GLM5.2/src/systems/CorruptionDirector.ts new file mode 100644 index 0000000..f733a0d --- /dev/null +++ b/GLM5.2/src/systems/CorruptionDirector.ts @@ -0,0 +1,236 @@ +// CorruptionDirector.ts: single source of truth for all state + A/V params. +// Holds happiness/awareness/day/task flags/clue set/erosion stage. +// Emits events for scenes to react. +import { audio } from './AudioEngine'; +import { + HAPPY_START, AWARE_START, HAPPY_MAX, AWARE_MAX, OVERFLOW_THRESHOLD, + EROSION_MATRIX, AWARE_CLUE_GAIN, AWARE_SAD_GAIN, HAPPY_SAD_LOSS, + HAPPY_SMALL_DAILY_CAP, DEFAULT_NAME, LOW_HAPPY_THRESHOLD, LOW_HAPPY_DURATION_MS, + FORCE_FESTIVAL_HAPPY, +} from '../data/gamedata'; + +export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4'; + +type Handler = (payload?: any) => void; + +class CorruptionDirector { + happiness = HAPPY_START; + awareness = AWARE_START; + day = 1; + stage: Stage = 'S1'; + playerName = DEFAULT_NAME; + startTime = Date.now(); + + // clue set + clues = new Set(); + // task completion + tasksDone = new Set(); + // sadness: small-interaction daily counters per kind + smallInteractCounts: Record = {}; // key: kind@day + // npc talk counts per day (for 纠缠 detection) + npcTalkCounts: Record = {}; + // rejection records (Day 5) + rejectedTasks = new Set(); + // escaped forced festivals count (E4) + escapedFestivals = 0; + // happiness-low tracker + private lowHappySince = 0; + // overflow pool + private overflowPool = 0; + // last negative flash time + private lastFlashAt = 0; + + // gaze tracking (well / fog) once per day + gazedWell = false; + gazedFog = false; + + // whether forced festival is active + forcedFestivalActive = false; + + private listeners: Record = {}; + + on(ev: string, h: Handler) { + (this.listeners[ev] ||= []).push(h); + return () => this.off(ev, h); + } + off(ev: string, h: Handler) { + const arr = this.listeners[ev]; if (!arr) return; + const i = arr.indexOf(h); if (i >= 0) arr.splice(i, 1); + } + emit(ev: string, payload?: any) { + (this.listeners[ev] || []).slice().forEach(h => { try { h(payload); } catch (e) { console.error(e); } }); + } + + reset() { + this.happiness = HAPPY_START; + this.awareness = AWARE_START; + this.day = 1; + this.stage = 'S1'; + this.playerName = DEFAULT_NAME; + this.startTime = Date.now(); + this.clues.clear(); + this.tasksDone.clear(); + this.smallInteractCounts = {}; + this.npcTalkCounts = {}; + this.rejectedTasks.clear(); + this.escapedFestivals = 0; + this.lowHappySince = 0; + this.overflowPool = 0; + this.gazedWell = false; + this.gazedFog = false; + this.forcedFestivalActive = false; + this.recomputeStage(); + this.emit('change'); + } + + recomputeStage() { + const h = this.happiness; + let s: Stage = 'S0'; + for (const row of EROSION_MATRIX) { + if (h >= row.happinessRange[0] && h <= row.happinessRange[1]) { s = row.stage; break; } + } + if (s !== this.stage) { + this.stage = s; + audio.setErosion(s); + this.emit('stage', s); + } + this.emit('change'); + } + + setHappiness(v: number, opts: { silent?: boolean } = {}) { + const prev = this.happiness; + let nv = Math.max(0, Math.min(HAPPY_MAX, Math.round(v))); + // forced maintenance at 100: small deductions accumulate in overflow pool + if (prev >= HAPPY_MAX && nv < prev) { + const diff = prev - nv; + this.overflowPool += diff; + nv = HAPPY_MAX; // stays at 100 + if (this.overflowPool >= OVERFLOW_THRESHOLD) { + this.overflowPool = 0; + this.emit('negativeFlash'); + this.stage = 'S4'; + audio.setErosion('S4'); + this.emit('stage', 'S4'); + } + } + this.happiness = nv; + if (!opts.silent) this.recomputeStage(); + this.checkLowHappy(); + } + + addHappiness(delta: number) { + if (delta > 0 && this.happiness >= HAPPY_MAX) { + // overflow pool accumulation (PRD: 小额扣分不再生效而是累积进隐藏溢出池) — for increases beyond 100, ignore + return; + } + this.setHappiness(this.happiness + delta); + } + + addAwareness(delta: number) { + this.awareness = Math.max(0, Math.min(AWARE_MAX, this.awareness + delta)); + this.emit('change'); + } + + // §6.2 clue discovery + discoverClue(id: string) { + if (this.clues.has(id)) return false; + this.clues.add(id); + this.addAwareness(AWARE_CLUE_GAIN); + this.emit('clue', id); + this.emit('change'); + return true; + } + + // §6.1 small interaction (water/feed/pet) +1, daily cap 3 per kind + smallInteraction(kind: string) { + const key = `${kind}@${this.day}`; + const count = this.smallInteractCounts[key] || 0; + if (count >= HAPPY_SMALL_DAILY_CAP) return false; + this.smallInteractCounts[key] = count + 1; + this.addHappiness(1); + return true; + } + + // §6.3 sadness behavior: -5 happy, +3 aware, nearest NPC turns + sadness(behavior: string, nearestNpc?: string) { + this.addHappiness(-HAPPY_SAD_LOSS); + this.addAwareness(AWARE_SAD_GAIN); + this.emit('sadness', { behavior, nearestNpc }); + this.emit('change'); + } + + // gaze well/fog (§6.2) + gaze(target: 'well' | 'fog') { + if (target === 'well' && !this.gazedWell) { this.gazedWell = true; this.addAwareness(3); } + if (target === 'fog' && !this.gazedFog) { this.gazedFog = true; this.addAwareness(3); } + } + + // NPC talk counter for 纠缠 (>=5/day) + noteNpcTalk(npcKey: string): boolean { + const key = `${npcKey}@${this.day}`; + this.npcTalkCounts[key] = (this.npcTalkCounts[key] || 0) + 1; + return this.npcTalkCounts[key] >= 5; + } + + private checkLowHappy() { + if (this.happiness < LOW_HAPPY_THRESHOLD) { + if (this.lowHappySince === 0) this.lowHappySince = Date.now(); + else if (Date.now() - this.lowHappySince >= LOW_HAPPY_DURATION_MS && !this.forcedFestivalActive) { + this.triggerForcedFestival(); + } + } else { + this.lowHappySince = 0; + } + } + + // §6.1 forced festival + triggerForcedFestival() { + this.forcedFestivalActive = true; + this.emit('forcedFestivalStart'); + // raise happiness to 50 + this.setHappiness(FORCE_FESTIVAL_HAPPY); + } + escapeFestival() { + if (!this.forcedFestivalActive) return; + this.forcedFestivalActive = false; + this.escapedFestivals++; + this.emit('forcedFestivalEnd'); + this.emit('change'); + } + + advanceDay() { + this.day = Math.min(7, this.day + 1); + this.gazedWell = false; this.gazedFog = false; + this.emit('day', this.day); + this.emit('change'); + } + + // ending determination per §9 priority + determineEnding(): 'E4'|'E3'|'E2'|'E1' { + if (this.day >= 5 && this.happiness <= 10 && this.escapedFestivals >= 3) return 'E4'; + if (this.awareness >= 70 && this.clues.size >= 6) return 'E3'; + if (this.awareness >= 30) return 'E2'; + if (this.awareness <= 29 && this.happiness >= 90) return 'E1'; + // fallback + if (this.awareness <= 29) return 'E1'; + return 'E2'; + } + + playTimeSeconds(): number { + return Math.floor((Date.now() - this.startTime) / 1000); + } + formatPlayTime(): string { + const s = this.playTimeSeconds(); + const m = Math.floor(s / 60); + const sec = s % 60; + return `${m}分${sec}秒`; + } + + // debug setters + debugSetHappiness(v: number) { this.happiness = Math.max(0,Math.min(HAPPY_MAX,v)); this.recomputeStage(); } + debugSetAwareness(v: number) { this.awareness = Math.max(0,Math.min(AWARE_MAX,v)); this.emit('change'); } + debugSetDay(d: number) { this.day = Math.max(1,Math.min(7,d)); this.emit('day',this.day); this.emit('change'); } + debugAllClues() { ['C1','C2','C3','C4','C5','C6','C7','C8','C9'].forEach(c=>this.clues.add(c)); this.addAwareness(0); this.emit('change'); } +} + +export const director = new CorruptionDirector(); diff --git a/GLM5.2/src/systems/StoryDirector.ts b/GLM5.2/src/systems/StoryDirector.ts new file mode 100644 index 0000000..79c0500 --- /dev/null +++ b/GLM5.2/src/systems/StoryDirector.ts @@ -0,0 +1,165 @@ +// StoryDirector.ts: orchestrates per-day structure, tasks, clue gating, endings. +import { director } from './CorruptionDirector'; +import { AFU_ANNOUNCE, TASKS, CLUES, NPCS, C4_GARBLE } from '../data/gamedata'; +import { DialogueLine } from '../scenes/UIScene'; + +class StoryDirector { + // greeting progress per day (T3/T10): count distinct NPCs greeted + greetProgress: Record> = {}; + // bread delivery progress (T1/T8): deliver to dabao/mimi/grandpa + breadProgress: Record> = {}; + // letters (T4): deliver 3 to yuanyuan/mimi/dr_fu + letterProgress = new Set(); + // berries (T6) + berriesPicked = 0; + // ribbons (T13) + ribbonsHung = 0; + // watering (T2/T9) + wateredToday: Record = {}; + // day phase: morning/afternoon/evening + phase: 'morning'|'afternoon'|'evening'|'sleeping' = 'morning'; + // whether board announcement shown today + announcementShown = false; + // whether player has taken bread today (for T1/T8) + breadTaken: Record = {}; + // whether player has taken letters today (for T4) + lettersTaken: Record = {}; + + resetDay() { + this.announcementShown = false; + this.phase = 'morning'; + this.letterProgress.clear(); + } + + // record a greeting; completes T3 (day1) / T10 (day4) when 3 distinct NPCs greeted + recordGreet(npcKey: string) { + const d = director.day; + (this.greetProgress[d] ||= new Set()).add(npcKey); + const n = this.greetProgress[d].size; + if ((d === 1 || d === 4) && n >= 3 && !director.tasksDone.has(d === 1 ? 'T3' : 'T10')) { + this.completeTask(d === 1 ? 'T3' : 'T10', 10); + } + } + + // deliver bread to an NPC (T1 day1 / T8 day4) + deliverBread(npcKey: string, onLine: (l: DialogueLine[]) => void) { + const d = director.day; + if (d !== 1 && d !== 4) { onLine([{ speaker:'', text:'现在不需要送面包。' }]); return; } + if (!this.breadTaken[d]) { onLine([{ speaker:'', text:'你还没从圆圆那里取面包呢。' }]); return; } + const taskId = d === 1 ? 'T1' : 'T8'; + const set = (this.breadProgress[d] ||= new Set()); + if (set.has(npcKey)) { onLine([{ speaker:'', text:'已经送过了。' }]); return; } + set.add(npcKey); + const npc = NPCS[npcKey]; + onLine([{ speaker: npc.name, npcKey, portrait: npc.portrait.smile, text:'哇,蜂蜜面包!谢谢你!' }]); + if (set.size >= 3) this.completeTask(taskId, 15); + } + + // take bread from yuanyuan (T1/T8) + takeBread(onLine: (l: DialogueLine[]) => void) { + const d = director.day; + if (d === 1 || d === 4) { + this.breadTaken[d] = true; + onLine([{ speaker:'圆圆', npcKey:'yuanyuan', portrait: NPCS.yuanyuan.portrait.smile, text:'这是蜂蜜面包,请帮我送给大宝、咪咪、龟爷爷吧!' }]); + } + } + + // take letters from dabao (T4 day2) + takeLetters(onLine: (l: DialogueLine[]) => void) { + if (director.day === 2) { + this.lettersTaken[2] = true; + onLine([ + { speaker:'大宝', npcKey:'dabao', portrait: NPCS.dabao.portrait.smile, text:'帮我送三封信吧,收件人是圆圆、咪咪、芙医生。' }, + ]); + } + } + + // deliver a letter (T4). 3rd letter shows garbled signature (C4) + deliverLetter(npcKey: string, onLine: (l: DialogueLine[]) => void) { + if (director.day !== 2 || !this.lettersTaken[2]) { onLine([{ speaker:'', text:'现在没有信要送。' }]); return; } + if (this.letterProgress.has(npcKey)) { onLine([{ speaker:'', text:'这封信已经送了。' }]); return; } + this.letterProgress.add(npcKey); + const count = this.letterProgress.size; + const npc = NPCS[npcKey]; + const lines: DialogueLine[] = [{ speaker: npc.name, npcKey, portrait: npc.portrait.smile, text:'收到信啦,谢谢你!' }]; + if (count === 3) { + // garbled signature flash 0.2s (C4) + lines.push({ speaker:'', text:`署名闪过乱码「${this.letterGarble()}」`, glitch: true }); + director.discoverClue('C4'); + } + onLine(lines); + if (count >= 3) this.completeTask('T4', 15); + } + + // Get today's active tasks + todayTasks() { return TASKS.filter(t => t.day === director.day); } + + //阿福 announcement lines (verbatim per day) + announcementLines(): DialogueLine[] { + const txt = AFU_ANNOUNCE[director.day] || AFU_ANNOUNCE[1]; + return [ + { speaker: '阿福', npcKey: 'afu', portrait: NPCS.afu.portrait.smile, text: txt }, + ]; + } + + // standard NPC talk lines per NPC (sample, affected by corruption via layer) + npcTalk(npcKey: string): DialogueLine[] { + const npc = NPCS[npcKey]; + if (!npc) return []; + const base: Record = { + afu: ['今天也是美好的一天呀。', '大家都要开心哦。'], + yuanyuan: ['面包刚出炉,香喷喷的!', '甜的东西能让人幸福呢。'], + dabao: ['今天也有好多信要送。', '每一封信都通往一个朋友。'], + mimi: ['欢迎光临~', '货架满满的,真开心呀。'], + dr_fu: ['村里有诊所,但你不需要去那里哦。', '大家都健健康康的,真好。'], + bird: ['(鸟儿在树梢欢快地歌唱)'], + grandpa: [], + }; + if (npcKey === 'grandpa') return this.grandpaLines(); + const lines = base[npcKey] || []; + return lines.map(t => ({ speaker: npc.name, npcKey, portrait: this.portraitFor(npcKey), text: t })); + } + + // 龟爷爷 C9 progression per D2/D4/D6 + grandpaLines(): DialogueLine[] { + const d = director.day; + let txt = '这里的湖水一直很平静。'; + if (d >= 6 || director.clues.has('C9')) txt = '孩子,今天是第几个今天?'; + else if (d >= 4) txt = '面包一直是那个味道。'; + // discover C9 when reaching the third line + if (d >= 6 && !director.clues.has('C9')) director.discoverClue('C9'); + return [{ speaker: '龟爷爷', npcKey: 'grandpa', portrait: NPCS.grandpa.portrait.smile, text: txt }]; + } + + portraitFor(npcKey: string): number { + const npc = NPCS[npcKey]; + const stage = director.stage; + // apply individual threshold shift + const order = ['smile','flat','off'] as const; + let idx = 0; + if (stage === 'S2') idx = 0; + else if (stage === 'S3') idx = 1; + else if (stage === 'S4') idx = 2; + // mimi earlier + if (npc.thresholdShift < 0) idx = Math.min(2, idx+1); + else if (npc.thresholdShift > 0) idx = Math.max(0, idx-1); + // dr_fu never changes + if (npcKey === 'dr_fu') idx = 0; + if (npcKey === 'grandpa') idx = 0; // grandpa doesn't corrupt + const key = order[idx] as 'smile'|'flat'|'off'; + return npc.portrait[key]; + } + + // reward a task + completeTask(taskId: string, reward: number) { + if (director.tasksDone.has(taskId)) return; + director.tasksDone.add(taskId); + if (reward > 0) director.addHappiness(reward); + director.emit('taskDone', taskId); + } + + // letter C4 garbled signature flash (0.2s) + letterGarble(): string { return C4_GARBLE; } +} + +export const story = new StoryDirector(); diff --git a/GLM5.2/src/systems/TextCorruptor.ts b/GLM5.2/src/systems/TextCorruptor.ts new file mode 100644 index 0000000..e21b948 --- /dev/null +++ b/GLM5.2/src/systems/TextCorruptor.ts @@ -0,0 +1,102 @@ +// TextCorruptor.ts: applies §6.5 text corruption at render time. +// Original text stays clean in data; corruption applied based on stage and NPC bias. +import { CORRUPTION_MAP, NPCS } from '../data/gamedata'; + +export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4'; + +// Count NPC talk turns for periodic corruption (S2: 1/80, S3: 1/50) +let globalTurnCount = 0; +export function tickTurn() { globalTurnCount++; } +export function resetTurns() { globalTurnCount = 0; } + +// Deterministic-ish RNG to decide whether to corrupt a given target word. +function chance(p: number): boolean { return Math.random() < p; } + +/** + * Corrupt a line of text. + * @param text original clean text + * @param stage current erosion stage + * @param npcKey the speaking NPC key (mimi gets priority) + * @param forceStrong if true (S4 / off), apply aggressive corruption + */ +export function corrupt(text: string, stage: Stage, npcKey?: string, forceStrong = false): string { + let out = text; + const npc = npcKey ? NPCS[npcKey] : undefined; + const isMimi = npcKey === 'mimi'; + + // Determine effective stage with NPC threshold shift applied to "when corruption starts". + // mimi (-10): corruption starts ~1 stage earlier; yuanyuan (+15): ~1 stage later. + let eff: Stage = stage; + if (npc) { + const shift = npc.thresholdShift; + // shift<0 -> earlier; map by nudging stage index + const order: Stage[] = ['S0','S1','S2','S3','S4']; + let idx = order.indexOf(stage); + // negative shift reduces the "needed" happiness -> treat as if one stage higher (more corrupt) for mimi + if (shift < 0) idx = Math.min(4, idx + 1); + else if (shift > 0) idx = Math.max(0, idx - 1); + eff = order[idx]; + } + // dr_fu & grandpa never corrupt (not participants) + if (npcKey === 'dr_fu' || npcKey === 'grandpa') return out; + + // Apply per-stage rules + // Build list of [target, replacement] + const pairs: [string, string][] = Object.entries(CORRUPTION_MAP); + + // prioritize mimi: her words get corrupted first + const ordered = isMimi ? pairs : (pairs.slice().sort(() => Math.random() - 0.5)); + + if (eff === 'S0' || eff === 'S1') { + // normal: no corruption + return out; + } + + if (eff === 'S2') { + // occasional repeated punctuation (1 per 80 sentences) + if (forceStrong || isMimi || globalTurnCount % 80 === 0) { + // corrupt at most one word, low chance + for (const [from, to] of ordered) { + if (out.includes(from) && chance(isMimi ? 0.5 : 0.25)) { out = out.replace(from, to); break; } + } + // repeated punctuation + if (chance(isMimi ? 0.5 : 0.2)) { + out = out.replace(/([。!?,])/g, (m) => chance(0.3) ? m + m : m); + } + } + return out; + } + + if (eff === 'S3') { + // homophone typos: 1 per 50 sentences, mimi higher + const baseProb = isMimi ? 0.7 : (forceStrong ? 0.6 : 0.4); + let didOne = false; + for (const [from, to] of ordered) { + if (out.includes(from) && (!didOne || isMimi) && chance(baseProb)) { + out = out.replace(from, to); didOne = true; + } + } + if (isMimi) { + // mimi句尾标点 ×3 (S4-like, but begins appearing) + out = out.replace(/([。!?])$/, (m) => m + m); + } + return out; + } + + // eff === 'S4' + { + // 错字加密 + 咪咪句尾标点 ×3 + for (const [from, to] of ordered) { + if (out.includes(from)) { + out = out.split(from).join(to); + } + } + if (isMimi) { + out = out.replace(/([。!?])$/, (m) => m + m + m); + } + return out; + } +} + +// Glitch signature for C4 (the PT-0▓ garbled signature flash) +export const C4_GARBLE = 'PT-0▓'; diff --git a/GLM5.2/tools/diagescape.mjs b/GLM5.2/tools/diagescape.mjs new file mode 100644 index 0000000..0572897 --- /dev/null +++ b/GLM5.2/tools/diagescape.mjs @@ -0,0 +1,47 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4210; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(200); + + // Try to walk to door: door at tile (5,7)=(80,112). Player at ~(96,64). + // Move left first to align x, then down. + await page.keyboard.down('a'); await page.waitForTimeout(500); await page.keyboard.up('a'); + await page.waitForTimeout(100); + const afterLeft = await page.evaluate(() => { const p=window.game.scene.getScene('HouseScene').player; return {x:p.x,y:p.y}; }); + console.log('after left:', JSON.stringify(afterLeft)); + + // Now move down toward door + await page.keyboard.down('s'); + for (let i=0;i<8;i++){ await page.waitForTimeout(300); const p=await page.evaluate(()=>{const pl=window.game.scene.getScene('HouseScene').player;return{x:pl.x,y:pl.y,vx:pl.body.velocity.x,vy:pl.body.velocity.y};}); console.log('down step',i,JSON.stringify(p)); if(p.y>100) break; } + await page.keyboard.up('s'); + await page.waitForTimeout(200); + + const nearDoor = await page.evaluate(() => { + const h = window.game.scene.getScene('HouseScene'); + const p = h.player; + const door = h.doors[0]; + const dist = Math.hypot(p.x-door.x, p.y-door.y); + return { px:p.x, py:p.y, doorX:door.x, doorY:door.y, dist, worldBounds: {w:h.physics.world.bounds.width, h:h.physics.world.bounds.height} }; + }); + console.log('near door:', JSON.stringify(nearDoor)); + + // press E to transition + await page.keyboard.press('e'); + await page.waitForTimeout(1500); + const activeScenes = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); + console.log('active scenes after E:', JSON.stringify(activeScenes)); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagescape2.mjs b/GLM5.2/tools/diagescape2.mjs new file mode 100644 index 0000000..8bdac42 --- /dev/null +++ b/GLM5.2/tools/diagescape2.mjs @@ -0,0 +1,44 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4211; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + + // Walk down toward the door. Door at (88,112). Player starts ~ (96,64). + // Just walk straight down — the door auto-trigger should fire when close. + await page.keyboard.down('s'); + let escaped = false; + for (let i=0;i<12;i++){ + await page.waitForTimeout(300); + const st = await page.evaluate(() => { + const g = window.game; + const active = g.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key); + return { active, hasPlaza: active.includes('PlazaScene'), hasHouse: active.includes('HouseScene') }; + }); + if (st.hasPlaza && !st.hasHouse) { escaped = true; break; } + } + await page.keyboard.up('s'); + checks.escaped_to_plaza = escaped; + + // Now from plaza, walk to each door and verify they auto-transition + // plaza doors: door_house(bottom center 14,18), door_bakery(left 5,9), etc. + // Wait for plaza to settle + await page.waitForTimeout(800); + const plazaActive = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).some(s=>s.scene.key==='PlazaScene')); + checks.in_plaza = plazaActive; + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks)); diff --git a/GLM5.2/tools/diagescape3.mjs b/GLM5.2/tools/diagescape3.mjs new file mode 100644 index 0000000..255bacd --- /dev/null +++ b/GLM5.2/tools/diagescape3.mjs @@ -0,0 +1,46 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4214; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + // house -> plaza + await page.keyboard.down('s'); await page.waitForTimeout(1500); + // wait for plaza + for (let i=0;i<10;i++){ await page.waitForTimeout(300); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; } + await page.keyboard.up('s'); await page.waitForTimeout(600); + + // Now in plaza. Check player position and doors + const state = await page.evaluate(() => { + const p = window.game.scene.getScene('PlazaScene'); + return { + px: p.player.x, py: p.player.y, + doors: p.doors.map(d=>({x:d.x,y:d.y,target:d.target})), + }; + }); + console.log('plaza state:', JSON.stringify(state)); + + // Directly teleport player to bakery door and check if auto-transition works + await page.evaluate(() => { window.game.scene.getScene('PlazaScene').player.setPosition(80, 144); }); + await page.waitForTimeout(800); + let a = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); + console.log('after teleport to bakery door:', JSON.stringify(a)); + + // If still in plaza, press E + if (a.includes('PlazaScene')) { + await page.keyboard.press('e'); + await page.waitForTimeout(800); + a = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); + console.log('after E at bakery door:', JSON.stringify(a)); + } + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagflow.mjs b/GLM5.2/tools/diagflow.mjs new file mode 100644 index 0000000..1a829e4 --- /dev/null +++ b/GLM5.2/tools/diagflow.mjs @@ -0,0 +1,31 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4219; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + // house -> plaza via walking down + await page.keyboard.down('s'); + for (let i=0;i<15;i++){ await page.waitForTimeout(250); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; } + await page.keyboard.up('s'); await page.waitForTimeout(900); + await page.click('canvas'); await page.waitForTimeout(200); + + const st = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y,active:window.game.scene.getScene('PlazaScene').scene.isActive()}; }); + console.log('in plaza:', JSON.stringify(st)); + + // hold w, check movement + await page.keyboard.down('w'); await page.waitForTimeout(600); + const w = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y,vy:p.body.velocity.y}; }); + console.log('w held:', JSON.stringify(w)); + await page.keyboard.up('w'); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diaghouse.mjs b/GLM5.2/tools/diaghouse.mjs new file mode 100644 index 0000000..2231206 --- /dev/null +++ b/GLM5.2/tools/diaghouse.mjs @@ -0,0 +1,25 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4222; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(120); } + await page.click('canvas'); await page.waitForTimeout(300); + await page.keyboard.down('s'); + for (let i=0;i<20;i++){ + await page.waitForTimeout(250); + const s = await page.evaluate(() => { const g=window.game; const a=g.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key); let p=null; try{p={x:g.scene.getScene('HouseScene').player.x,y:g.scene.getScene('HouseScene').player.y};}catch(e){} return {a,p}; }); + console.log('s step', i, JSON.stringify(s)); + if (s.a.includes('PlazaScene')) { console.log('ENTERED PLAZA'); break; } + } + await page.keyboard.up('s'); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagleftright.mjs b/GLM5.2/tools/diagleftright.mjs new file mode 100644 index 0000000..ebcc188 --- /dev/null +++ b/GLM5.2/tools/diagleftright.mjs @@ -0,0 +1,36 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4221; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(120); } + await page.click('canvas'); await page.waitForTimeout(300); + // enter plaza + await page.keyboard.down('s'); + for (let i=0;i<15;i++){ await page.waitForTimeout(220); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; } + await page.keyboard.up('s'); await page.waitForTimeout(800); + // teleport player to center of plaza row 8 (y=136) col 14 (x=232) + await page.evaluate(() => window.game.scene.getScene('PlazaScene').player.setPosition(232, 136)); + await page.waitForTimeout(300); + const start = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y}; }); + console.log('start at row8 center:', JSON.stringify(start)); + // walk left, sample position every 300ms + await page.keyboard.down('a'); + for (let i=0;i<20;i++){ + await page.waitForTimeout(250); + const s = await page.evaluate(() => { const g=window.game; const a=g.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key); const p=g.scene.getScene('PlazaScene').player; return {a, x:p?p.x:null, y:p?p.y:null}; }); + console.log('left step', i, JSON.stringify(s)); + if (s.a.includes('BakeryScene')) { console.log('ENTERED BAKERY'); break; } + if (!s.a.includes('PlazaScene')) break; + } + await page.keyboard.up('a'); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagmove.mjs b/GLM5.2/tools/diagmove.mjs new file mode 100644 index 0000000..415a21c --- /dev/null +++ b/GLM5.2/tools/diagmove.mjs @@ -0,0 +1,60 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4201; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('console', (msg) => { if (msg.type()==='error'||msg.type()==='warn') console.log('CONSOLE', msg.type(), msg.text()); }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + + // check keyboard setup + const setup = await page.evaluate(() => { + const house = window.game.scene.getScene('HouseScene'); + return { + kbExists: !!house.input.keyboard, + cursorsExists: !!house.cursors, + wasdExists: !!house.wasd, + wasdKeys: house.wasd ? Object.keys(house.wasd) : null, + wasdA: house.wasd && house.wasd.A ? { isDown: house.wasd.A.isDown, keyCode: house.wasd.A.keyCode } : null, + playerExists: !!house.player, + playerPos: house.player ? {x:house.player.x, y:house.player.y} : null, + dialogueActive: window.game.scene.getScene('UIScene').active, + }; + }); + console.log('SETUP:', JSON.stringify(setup, null, 2)); + + // dismiss dialogue if active + if (setup.dialogueActive) { + await page.keyboard.press('e'); await page.waitForTimeout(200); + await page.keyboard.press('e'); await page.waitForTimeout(200); + } + + const before = await page.evaluate(() => ({x: window.game.scene.getScene('HouseScene').player.x, y: window.game.scene.getScene('HouseScene').player.y, v: window.game.scene.getScene('HouseScene').player.body.velocity})); + + // press and hold 'd' for 500ms + await page.keyboard.down('d'); + await page.waitForTimeout(500); + const during = await page.evaluate(() => ({ + x: window.game.scene.getScene('HouseScene').player.x, + y: window.game.scene.getScene('HouseScene').player.y, + v: {x: window.game.scene.getScene('HouseScene').player.body.velocity.x, y: window.game.scene.getScene('HouseScene').player.body.velocity.y}, + wasdD_isDown: window.game.scene.getScene('HouseScene').wasd.D.isDown, + cursorsRight: window.game.scene.getScene('HouseScene').cursors.right.isDown, + })); + await page.keyboard.up('d'); + await page.waitForTimeout(100); + const after = await page.evaluate(() => ({x: window.game.scene.getScene('HouseScene').player.x, y: window.game.scene.getScene('HouseScene').player.y})); + + console.log('BEFORE:', JSON.stringify(before)); + console.log('DURING (d held):', JSON.stringify(during)); + console.log('AFTER:', JSON.stringify(after)); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagmove2.mjs b/GLM5.2/tools/diagmove2.mjs new file mode 100644 index 0000000..761c21c --- /dev/null +++ b/GLM5.2/tools/diagmove2.mjs @@ -0,0 +1,41 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4202; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + // dismiss dialogue + await page.keyboard.press('e'); await page.waitForTimeout(200); await page.keyboard.press('e'); await page.waitForTimeout(200); + + // Test: is UIScene dialogue blocking? Check UIScene input handlers consume keydown + // Focus the canvas + await page.click('canvas'); + await page.waitForTimeout(200); + + // Test arrow keys (cursors) and check global keydown listener + await page.keyboard.down('ArrowRight'); + await page.waitForTimeout(200); + const arrowState = await page.evaluate(() => { + const h = window.game.scene.getScene('HouseScene'); + return { cursorRight: h.cursors.right.isDown, wasdD: h.wasd.D.isDown, inputEnabled: h.input.enabled }; + }); + console.log('ArrowRight held:', JSON.stringify(arrowState)); + await page.keyboard.up('ArrowRight'); + + // Check if maybe the scene's update isn't running (frozen?) + const updateCheck = await page.evaluate(() => { + const h = window.game.scene.getScene('HouseScene'); + return { sceneActive: h.scene.isActive(), frozen: h.player.frozen, lastUpdate: h.sys.settings.active }; + }); + console.log('scene status:', JSON.stringify(updateCheck)); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagmove3.mjs b/GLM5.2/tools/diagmove3.mjs new file mode 100644 index 0000000..cf65895 --- /dev/null +++ b/GLM5.2/tools/diagmove3.mjs @@ -0,0 +1,46 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4203; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + await page.keyboard.press('e'); await page.waitForTimeout(200); await page.keyboard.press('e'); await page.waitForTimeout(200); + + // Inspect wasd key objects in detail + const wasdDetail = await page.evaluate(() => { + const h = window.game.scene.getScene('HouseScene'); + const d = h.wasd.D; + return { + type: typeof d, + hasKeyCode: 'keyCode' in d, + keyCode: d.keyCode, + isDown: d.isDown, + sceneInputEnabled: d.scene && d.scene.input ? d.scene.input.enabled : 'no scene ref', + // check which scene the key belongs to + sceneKey: d.scene ? d.scene.sys.config.key : null, + }; + }); + console.log('wasd.D detail:', JSON.stringify(wasdDetail, null, 2)); + + // Also test: create a fresh key and see if it tracks + const freshTest = await page.evaluate(() => { + const h = window.game.scene.getScene('HouseScene'); + const k = h.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D); + h.__testD = k; + return { keyCode: k.keyCode }; + }); + await page.keyboard.down('d'); + await page.waitForTimeout(200); + const freshDown = await page.evaluate(() => ({ testD: window.game.scene.getScene('HouseScene').__testD.isDown, wasdD: window.game.scene.getScene('HouseScene').wasd.D.isDown })); + console.log('fresh key d held:', JSON.stringify(freshDown)); + await page.keyboard.up('d'); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagmove4.mjs b/GLM5.2/tools/diagmove4.mjs new file mode 100644 index 0000000..47538d2 --- /dev/null +++ b/GLM5.2/tools/diagmove4.mjs @@ -0,0 +1,55 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4204; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + // fully dismiss dialogue + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); + await page.waitForTimeout(200); + + // check player internal keys + const keys = await page.evaluate(() => { + const p = window.game.scene.getScene('HouseScene').player; + return { + hasKLeft: !!p.kLeft, + kLeftIsDown: p.kLeft && p.kLeft.isDown, + kLeftPluginNull: p.kLeft && p.kLeft.plugin == null, + wasdLen: p.wasdKeys ? p.wasdKeys.length : null, + wasdDisDown: p.wasdKeys && p.wasdKeys[3] ? p.wasdKeys[3].isDown : null, + moving: p.moving, + frozen: p.frozen, + }; + }); + console.log('player keys:', JSON.stringify(keys)); + + // hold d, check player's internal D key + await page.keyboard.down('d'); + await page.waitForTimeout(400); + const during = await page.evaluate(() => { + const p = window.game.scene.getScene('HouseScene').player; + return { + x: p.x, y: p.y, + vx: p.body.velocity.x, vy: p.body.velocity.y, + wasdDisDown: p.wasdKeys[3].isDown, + kRightDown: p.kRight.isDown, + moving: p.moving, + }; + }); + console.log('DURING d-held:', JSON.stringify(during)); + await page.keyboard.up('d'); + await page.waitForTimeout(200); + const after = await page.evaluate(() => ({x: window.game.scene.getScene('HouseScene').player.x, y: window.game.scene.getScene('HouseScene').player.y})); + console.log('AFTER:', JSON.stringify(after)); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagmove5.mjs b/GLM5.2/tools/diagmove5.mjs new file mode 100644 index 0000000..287c172 --- /dev/null +++ b/GLM5.2/tools/diagmove5.mjs @@ -0,0 +1,31 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4205; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +const pos = () => page.evaluate(() => { const p=window.game.scene.getScene('HouseScene').player; return {x:p.x,y:p.y}; }); +let page; +try { + await waitServer(); + const browser = await chromium.launch(); + page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(200); + + for (const [key, label] of [['d','right'],['a','left'],['s','down'],['w','up'],['ArrowRight','arrowR'],['ArrowLeft','arrowL'],['ArrowUp','arrowU'],['ArrowDown','arrowD']]) { + const b = await pos(); + await page.keyboard.down(key); await page.waitForTimeout(350); await page.keyboard.up(key); await page.waitForTimeout(80); + const a = await pos(); + const moved = Math.abs(a.x-b.x) > 3 || Math.abs(a.y-b.y) > 3; + checks[label] = moved; + console.log(`${label}: (${b.x.toFixed(0)},${b.y.toFixed(0)}) -> (${a.x.toFixed(0)},${a.y.toFixed(0)}) moved=${moved}`); + } + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks)); diff --git a/GLM5.2/tools/diagnav.mjs b/GLM5.2/tools/diagnav.mjs new file mode 100644 index 0000000..d9e8b41 --- /dev/null +++ b/GLM5.2/tools/diagnav.mjs @@ -0,0 +1,59 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4212; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +const getActive = (page) => page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + + // helper: walk in a direction until scene changes or timeout + async function walkToScene(dir, wantScene, maxSteps=12) { + await page.keyboard.down(dir); + for (let i=0;i plaza (walk down) + checks.house_to_plaza = await walkToScene('s', 'PlazaScene'); + await page.waitForTimeout(500); + + // plaza -> bakery (door at left tile 5,9 = walk left). Player spawns near door_house (bottom center). + // First walk up, then left to bakery door + checks.plaza_to_bakery = await walkToScene('a', 'BakeryScene'); + await page.waitForTimeout(500); + + // bakery -> plaza (door at bottom center 7,9, walk down) + checks.bakery_to_plaza = await walkToScene('s', 'PlazaScene'); + await page.waitForTimeout(500); + + // plaza -> shop (door at right tile 24,9, walk right) + checks.plaza_to_shop = await walkToScene('d', 'ShopScene'); + await page.waitForTimeout(500); + + // shop -> plaza + checks.shop_to_plaza = await walkToScene('s', 'PlazaScene'); + await page.waitForTimeout(500); + + // plaza -> house (door_house bottom center 14,18, walk down) + checks.plaza_to_house = await walkToScene('s', 'HouseScene'); + await page.waitForTimeout(500); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks, null, 2)); diff --git a/GLM5.2/tools/diagnav2.mjs b/GLM5.2/tools/diagnav2.mjs new file mode 100644 index 0000000..747cda3 --- /dev/null +++ b/GLM5.2/tools/diagnav2.mjs @@ -0,0 +1,50 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4213; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +const getActive = (page) => page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); +async function walk(page, dir, ms){ await page.keyboard.down(dir); await page.waitForTimeout(ms); await page.keyboard.up(dir); await page.waitForTimeout(150); } +async function walkToScene(page, dir, wantScene, maxSteps=15){ + await page.keyboard.down(dir); + for (let i=0;i console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + + // house -> plaza + checks.house_to_plaza = await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(600); + // From plaza door_house (bottom center), walk UP to row 9, then LEFT to bakery door (5,9) + await walk(page, 'w', 1800); // walk up + checks.plaza_to_bakery = await walkToScene(page, 'a', 'BakeryScene'); + await page.waitForTimeout(500); + // bakery -> plaza + checks.bakery_to_plaza = await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(500); + // plaza: walk up then right to shop door (24,9) + await walk(page, 'w', 1800); + checks.plaza_to_shop = await walkToScene(page, 'd', 'ShopScene'); + await page.waitForTimeout(500); + checks.shop_to_plaza = await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(500); + // plaza -> clinic (top center, door 14,1): walk up + await walk(page, 'a', 800); // recenter + checks.plaza_to_clinic = await walkToScene(page, 'w', 'ClinicScene'); + await page.waitForTimeout(500); + checks.clinic_to_plaza = await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(500); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks, null, 2)); diff --git a/GLM5.2/tools/diagnav3.mjs b/GLM5.2/tools/diagnav3.mjs new file mode 100644 index 0000000..a159549 --- /dev/null +++ b/GLM5.2/tools/diagnav3.mjs @@ -0,0 +1,64 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4215; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +const getActive = (page) => page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); +async function walkToScene(page, dir, wantScene, maxSteps=20){ + await page.keyboard.down(dir); + for (let i=0;i console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + + // house -> plaza (walk down to bottom edge door) + checks.house_to_plaza = await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(600); + // plaza -> bakery (walk left to left edge). Player spawns at door_house bottom, offset upward. + checks.plaza_to_bakery = await walkToScene(page, 'a', 'BakeryScene'); + await page.waitForTimeout(600); + // bakery -> plaza (walk down) + checks.bakery_to_plaza = await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(600); + // plaza -> shop (walk right) + checks.plaza_to_shop = await walkToScene(page, 'd', 'ShopScene'); + await page.waitForTimeout(600); + // shop -> plaza + checks.shop_to_plaza = await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(600); + // plaza -> clinic (walk up to top edge) + checks.plaza_to_clinic = await walkToScene(page, 'w', 'ClinicScene'); + await page.waitForTimeout(600); + // clinic -> plaza + checks.clinic_to_plaza = await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(600); + // plaza -> lake (walk left - lake at col0,row10, bakery at col0,row9; both left edge) + // player may hit bakery first; let's walk up a bit then left + await page.keyboard.down('w'); await page.waitForTimeout(800); await page.keyboard.up('w'); + // now walk down to row 10 area then left + await page.keyboard.down('s'); await page.waitForTimeout(400); await page.keyboard.up('s'); + checks.plaza_to_lake = await walkToScene(page, 'a', 'LakeScene'); + await page.waitForTimeout(600); + // lake -> plaza (door_plaza at right edge col 22,8 — walk right) + checks.lake_to_plaza = await walkToScene(page, 'd', 'PlazaScene'); + await page.waitForTimeout(600); + // plaza -> field (walk right, field at col29 row10) + checks.plaza_to_field = await walkToScene(page, 'd', 'FieldScene'); + await page.waitForTimeout(600); + // field -> plaza + checks.field_to_plaza = await walkToScene(page, 'a', 'PlazaScene'); + await page.waitForTimeout(600); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks, null, 2)); diff --git a/GLM5.2/tools/diagnav4.mjs b/GLM5.2/tools/diagnav4.mjs new file mode 100644 index 0000000..0bb1805 --- /dev/null +++ b/GLM5.2/tools/diagnav4.mjs @@ -0,0 +1,61 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4220; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +const getActive = (page) => page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); +async function walk(page, dir, ms){ await page.keyboard.down(dir); await page.waitForTimeout(ms); await page.keyboard.up(dir); await page.waitForTimeout(120); } +async function walkToScene(page, dir, wantScene, maxSteps=25){ + await page.keyboard.down(dir); + for (let i=0;i console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(120); } + await page.click('canvas'); await page.waitForTimeout(300); + + // Test each plaza exit independently by teleporting to plaza center first + await enterPlaza(page); + // bakery: walk up to row ~8 then left + await walk(page, 'w', 2500); + checks.bakery = await walkToScene(page, 'a', 'BakeryScene'); + await page.waitForTimeout(500); + + // reset: go back to plaza + if (!checks.bakery) await page.evaluate(() => { const g=window.game; g.scene.stop('HouseScene'); g.scene.start('PlazaScene',{spawn:'default'}); }); + else await walkToScene(page, 's', 'PlazaScene'); + await page.waitForTimeout(700); await page.click('canvas'); + + // shop: walk up then right + await walk(page, 'w', 2500); + checks.shop = await walkToScene(page, 'd', 'ShopScene'); + await page.waitForTimeout(500); + await walkToScene(page, 's', 'PlazaScene'); await page.waitForTimeout(700); await page.click('canvas'); + + // clinic: walk up + await walk(page, 'a', 600); + checks.clinic = await walkToScene(page, 'w', 'ClinicScene'); + await page.waitForTimeout(500); + await walkToScene(page, 's', 'PlazaScene'); await page.waitForTimeout(700); await page.click('canvas'); + + // lake: walk down then left (lake at col0 row12 = lower left) + await walk(page, 's', 1500); + checks.lake = await walkToScene(page, 'a', 'LakeScene'); + await page.waitForTimeout(500); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks, null, 2)); diff --git a/GLM5.2/tools/diagplaza.mjs b/GLM5.2/tools/diagplaza.mjs new file mode 100644 index 0000000..39fe488 --- /dev/null +++ b/GLM5.2/tools/diagplaza.mjs @@ -0,0 +1,42 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4216; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + + // house -> plaza + await page.keyboard.down('s'); + for (let i=0;i<15;i++){ await page.waitForTimeout(250); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; } + await page.keyboard.up('s'); await page.waitForTimeout(700); + + const inPlaza = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); + console.log('after entering plaza:', JSON.stringify(inPlaza)); + const pos = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene'); return {px:p.player.x, py:p.player.y, doors:p.doors.map(d=>({x:d.x,y:d.y,t:d.target}))}; }); + console.log('player pos in plaza:', pos.px, pos.py); + console.log('doors:', JSON.stringify(pos.doors)); + + // Now walk left toward bakery door (col 0, row 8 = 0,128). Player at ~(224, 276). + // Need to go up to row 8 first. Walk up. + await page.keyboard.down('w'); await page.waitForTimeout(2500); await page.keyboard.up('w'); await page.waitForTimeout(200); + const afterUp = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y}; }); + console.log('after walking up:', JSON.stringify(afterUp)); + // now walk left + await page.keyboard.down('a'); + for (let i=0;i<20;i++){ await page.waitForTimeout(250); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('BakeryScene')){ console.log('entered bakery at step',i); break; } } + await page.keyboard.up('a'); + const finalScenes = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); + const finalPos = await page.evaluate(() => { try { return {x:window.game.scene.getScene('PlazaScene').player.x, y:window.game.scene.getScene('PlazaScene').player.y}; } catch(e){ return 'plaza gone'; } }); + console.log('final scenes:', JSON.stringify(finalScenes), 'pos:', JSON.stringify(finalPos)); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagplaza2.mjs b/GLM5.2/tools/diagplaza2.mjs new file mode 100644 index 0000000..277cb49 --- /dev/null +++ b/GLM5.2/tools/diagplaza2.mjs @@ -0,0 +1,52 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4217; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + page.on('console', (msg) => { if (msg.type()==='error') console.log('CONSOLE_ERR', msg.text()); }); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + // house -> plaza + await page.keyboard.down('s'); + for (let i=0;i<15;i++){ await page.waitForTimeout(250); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; } + await page.keyboard.up('s'); await page.waitForTimeout(800); + + // Check plaza player keys and update status + const info = await page.evaluate(() => { + const p = window.game.scene.getScene('PlazaScene'); + const pl = p.player; + return { + px: pl.x, py: pl.y, + moving: pl.moving, + frozen: pl.frozen, + hasKUp: !!pl.kUp, + kUpIsDown: pl.kUp && pl.kUp.isDown, + wasdLen: pl.wasdKeys ? pl.wasdKeys.length : 0, + wasdWDown: pl.wasdKeys && pl.wasdKeys[0] ? pl.wasdKeys[0].isDown : null, + sceneUpdateActive: p.scene.isActive(), + doorCooldown: p.doorCooldown, + timeNow: p.time.now, + }; + }); + console.log('plaza player info:', JSON.stringify(info)); + + // hold w and check if key isDown updates + await page.keyboard.down('w'); + await page.waitForTimeout(400); + const wInfo = await page.evaluate(() => { + const pl = window.game.scene.getScene('PlazaScene').player; + return { kUpDown: pl.kUp.isDown, wasdWDown: pl.wasdKeys[0].isDown, px: pl.x, py: pl.y, vy: pl.body.velocity.y, moving: pl.moving }; + }); + console.log('w held:', JSON.stringify(wInfo)); + await page.keyboard.up('w'); + + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/diagplaza3.mjs b/GLM5.2/tools/diagplaza3.mjs new file mode 100644 index 0000000..abefda0 --- /dev/null +++ b/GLM5.2/tools/diagplaza3.mjs @@ -0,0 +1,33 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4218; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); await page.waitForTimeout(1200); + for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); } + await page.click('canvas'); await page.waitForTimeout(300); + // teleport directly to plaza to skip the house + await page.evaluate(() => { + const g = window.game; + g.scene.stop('HouseScene'); + g.scene.start('PlazaScene', { spawn: 'default' }); + }); + await page.waitForTimeout(1200); + await page.click('canvas'); await page.waitForTimeout(200); + + const before = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y,active:window.game.scene.getScene('PlazaScene').scene.isActive(),moving:p.moving}; }); + console.log('before (plaza default spawn):', JSON.stringify(before)); + + await page.keyboard.down('w'); await page.waitForTimeout(600); + const during = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y,vy:p.body.velocity.y}; }); + console.log('w held 600ms:', JSON.stringify(during)); + await page.keyboard.up('w'); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/gen_maps.cjs b/GLM5.2/tools/gen_maps.cjs new file mode 100644 index 0000000..634cf48 --- /dev/null +++ b/GLM5.2/tools/gen_maps.cjs @@ -0,0 +1,447 @@ +// gen_maps.js: Generate 10 Tiled-format JSON maps per PRD §10. +// Produces assets/maps/*.json +// Tile index reference (matches tools/gen_tileset.lua order): +// NOTE: Tiled uses global tile IDs where 0 = empty and localIndex N -> global N+1 (firstgid=1). +// We store raw LOCAL indices in T.* and convert to global IDs (+1, 0 stays 0=empty) on write. +const T = { + grass: 0, path: 1, water: 2, flowersY: 3, flowersP: 4, flowersG: 5, berry: 6, tree: 7, + birdTree: 8, bush: 9, wall: 10, wallTop: 11, roof: 12, roofEdge: 13, door: 14, window: 15, + floor: 16, floorRug: 17, bed: 18, counter: 19, oven: 20, shelf: 21, shelfFull: 22, table: 23, + chair: 24, stool: 25, well: 26, fog: 27, fogThick: 28, whiteWall: 29, whiteFloor: 30, whiteDoor: 31, + whiteBed: 32, whiteMonitor: 33, whiteTable: 34, ribbon: 35, ribbon2: 36, picnic: 37, bench: 38, sign: 39, + calendar: 40, paper: 41, chart: 42, fenceH: 43, fenceV: 44, lamp: 45, potPlant: 46, barrel: 47, + crate: 48, grassAlt: 49, pathH: 50, pathV: 51, pathCross: 52, waterEdge: 53, grassDark: 54, pathCorner: 55, + flowerMix: 56, berryEmpty: 57, fogBand: 58, whitePillar: 59, floorEdge: 60, wallTrim: 61, grassPath: 62, transparent: 63, +}; +// EMPTY tile (global 0): use a sentinel; we map 63(transparent local) -> 0 empty on output, others +1. +const EMPTY = 63; // local index used for "transparent/empty" + +const COLS = 8; // tileset columns + +const fs = require('fs'); +const path = require('path'); +const outDir = 'assets/maps'; +fs.mkdirSync(outDir, { recursive: true }); + +// Load the tileset descriptor to embed inline. +const tilesetJson = JSON.parse(fs.readFileSync('assets/tilesets/tileset.json', 'utf8')); + +function makeMap(name, width, height, layers) { + // layers: [{name, type:'tilelayer', data:number[] (len w*h)} or {name,type:'objectgroup', objects:[]}] + // Find door objects and clear obstacle tiles at their positions so the player can walk through. + const objLayer = layers.find(l => l.type === 'objectgroup'); + const doorTiles = new Set(); + if (objLayer) { + for (const o of objLayer.objects) { + if (o.type === 'door' || (o.name && o.name.startsWith('door'))) { + const tx = Math.floor(o.x / 16), ty = Math.floor(o.y / 16); + doorTiles.add(ty * width + tx); + } + } + } + const tiledLayers = layers.map(l => { + if (l.type === 'tilelayer') { + // convert local indices to global tile IDs: local N -> N+1, EMPTY(63) -> 0 + // also clear obstacle tiles at door positions + const globalData = l.data.map((v, idx) => { + if (l.name === 'obstacles' && doorTiles.has(idx)) return 0; // clear wall at door + return (v === EMPTY ? 0 : v + 1); + }); + return { + name: l.name, + type: 'tilelayer', + x: 0, y: 0, + width, height, + visible: true, + opacity: 1, + offsetx: 0, offsety: 0, + draworder: 'right-down', + data: globalData, + }; + } else { + return { + name: l.name, + type: 'objectgroup', + x: 0, y: 0, + visible: true, + opacity: 1, + offsetx: 0, offsety: 0, + draworder: 'index', + objects: l.objects, + }; + } + }); + return { + compressionlevel: -1, + height, width, + tilewidth: 16, + tileheight: 16, + type: 'map', + orientation: 'orthogonal', + renderorder: 'right-down', + tiledversion: '1.12.2', + // embed tileset inline so Phaser resolves it without external file + tilesets: [{ + firstgid: 1, + name: tilesetJson.name, + columns: tilesetJson.columns, + image: '../tilesets/tileset.png', + imagewidth: tilesetJson.imagewidth, + imageheight: tilesetJson.imageheight, + margin: 0, + spacing: 0, + tilecount: tilesetJson.tilecount, + tilewidth: 16, + tileheight: 16, + tiles: tilesetJson.tiles, + }], + layers: tiledLayers, + }; +} + +// fill helpers +function fillGrid(w, h, val) { return new Array(w * h).fill(val); } +function set(g, w, x, y, v) { if (x>=0&&y>=0&&x!k.startsWith('_')).map(([k,v])=>({name:k,type:'string',value:String(v)})) }; +} + +const maps = {}; + +// 1. House (12x8) interior +{ + const w=12, h=8; + const ground = fillGrid(w,h,T.floor); + rectFill(ground,w,h,0,0,12,1,T.wallTop); + rectFill(ground,w,h,0,h-1,12,1,T.floorEdge); + const obs = fillGrid(w,h,63); // transparent + borderFill(obs,w,h,0,0,12,8,T.wall); + // bed bottom-left + rectFill(obs,w,h,1,5,3,2,T.bed); + // calendar on wall (C1) - object only + // door to plaza (south center) — widen the gap so the player can't get stuck + set(obs,w,5,7,63); set(obs,w,6,7,63); set(ground,w,5,7,T.door); + // objects + const objects = [ + obj('door_plaza','door',5,7,{target:'plaza', spawn:'door_house'}), + obj('bed','interact',1,5,{clue:'none'}), + obj('calendar','clue',2,0,{clue:'C1'}), + obj('sleep_point','interact',6,5), + ]; + maps.house = makeMap('house', w, h, [ + {name:'ground',type:'tilelayer',data:ground}, + {name:'obstacles',type:'tilelayer',data:obs}, + {name:'objects',type:'objectgroup',objects}, + ]); +} + +// 2. Plaza (30x20) hub +{ + const w=30, h=20; + const ground = fillGrid(w,h,T.grass); + // cross paths + rectFill(ground,w,h,0,9,30,2,T.path); // horizontal + rectFill(ground,w,h,14,0,2,20,T.path); // vertical + // flower beds + rectFill(ground,w,h,3,3,4,3,T.flowersY); + rectFill(ground,w,h,23,3,4,3,T.flowersP); + // twin bird tree + set(ground,w,15,3,T.birdTree); + // signs/decor + set(ground,w,2,14,T.lamp); set(ground,w,27,14,T.lamp); + // ribbons area for festival (decor placed day6) + const obs = fillGrid(w,h,63); + // border trees + for (let x=0;x=2 then px(ox+x,oy+10,C.waterD) end end + px(ox+3,oy+7,C.waterF); px(ox+10,oy+7,C.waterF) +end +local function flowers(ox,oy,accent) + grass(ox,oy) + for _,s in ipairs({{4,6},{11,9}}) do px(ox+s[1],oy+s[2]+2,C.stem); px(ox+s[1],oy+s[2]+3,C.stem) end + for _,b in ipairs({{4,6},{11,9}}) do + px(ox+b[1],oy+b[2],accent); px(ox+b[1]-1,oy+b[2],accent); px(ox+b[1]+1,oy+b[2],accent) + px(ox+b[1],oy+b[2]-1,accent); px(ox+b[1],oy+b[2]+1,accent) + end +end +local function berry(ox,oy) + grass(ox,oy) + rect(ox+3,oy+5,10,8,C.leaf) + rect(ox+4,oy+4,8,1,C.leafL) + rect(ox+3,oy+5,10,1,C.leafD) + for _,b in ipairs({{5,8},{8,7},{10,9},{6,11},{9,11}}) do + px(ox+b[1],oy+b[2],C.berryR); px(ox+b[1]+1,oy+b[2],C.berryR); px(ox+b[1],oy+b[2]+1,C.berryD) + end +end +local function tree(ox,oy) + grass(ox,oy) + rect(ox+7,oy+10,2,5,C.trunk) + rect(ox+4,oy+3,8,8,C.leafD) + rect(ox+5,oy+2,6,1,C.leafL) + rect(ox+3,oy+5,1,4,C.leaf); rect(ox+12,oy+5,1,4,C.leaf) + rect(ox+6,oy+6,4,3,C.leafL) +end +local function birdTree(ox,oy) + tree(ox,oy) + px(ox+6,oy+5,C.flowerY); px(ox+9,oy+6,C.flowerY) +end +local function bush(ox,oy) + grass(ox,oy) + rect(ox+3,oy+6,10,6,C.leaf); rect(ox+4,oy+5,8,1,C.leafL) +end +local function wall(ox,oy) + rect(ox,oy,16,16,C.wall) + for _,d in ipairs({{1,1},{5,1},{9,1},{13,1},{3,5},{7,5},{11,5},{1,9},{5,9},{9,9},{13,9},{3,13},{7,13},{11,13}}) do px(ox+d[1],oy+d[2],C.wallD) end +end +local function wallTop(ox,oy) + rect(ox,oy,16,16,C.wall); rect(ox,oy,16,2,C.wallD) + for x=0,15,4 do px(ox+x,oy+4,C.wallD) end +end +local function roof(ox,oy) + rect(ox,oy,16,16,C.roof) + for y=0,15,2 do rect(ox,oy+y,16,1,C.roofD) end +end +local function roofEdge(ox,oy) + rect(ox,oy,16,16,C.roof); rect(ox,oy+14,16,2,C.roofD) + for y=0,12,2 do rect(ox,oy+y,16,1,C.roofD) end +end +local function door(ox,oy) + wall(ox,oy) + rect(ox+3,oy+2,10,14,C.door) + rect(ox+3,oy+2,10,1,C.doorD); rect(ox+3,oy+2,1,14,C.doorD) + px(ox+11,oy+9,C.signD) +end +local function window(ox,oy) + wall(ox,oy) + rect(ox+3,oy+3,10,8,C.window) + rect(ox+3,oy+3,10,1,C.wallD); rect(ox+3,oy+10,10,1,C.wallD) + rect(ox+3,oy+3,1,8,C.wallD); rect(ox+12,oy+3,1,8,C.wallD) + px(ox+7,oy+3,C.wallD); px(ox+7,oy+11,C.wallD) +end +local function floor(ox,oy) + rect(ox,oy,16,16,C.floor) + for _,d in ipairs({{2,3},{9,5},{5,11},{12,12}}) do px(ox+d[1],oy+d[2],C.floorD) end + rect(ox,oy,16,1,C.floorD); rect(ox,oy+8,16,1,C.floorD) +end +local function floorRug(ox,oy) + floor(ox,oy) + rect(ox+3,oy+4,10,8,C.flowerP); rect(ox+3,oy+4,10,1,C.ribbonD); rect(ox+3,oy+11,10,1,C.ribbonD) +end +local function bed(ox,oy) + rect(ox+1,oy+3,14,11,C.bedD); rect(ox+2,oy+4,12,9,C.bedF); rect(ox+3,oy+5,4,3,C.pillow) +end +local function counter(ox,oy) + rect(ox,oy+5,16,7,C.counter); rect(ox,oy+5,16,1,C.counterD); rect(ox,oy+11,16,1,C.counterD) +end +local function oven(ox,oy) + rect(ox+2,oy+2,12,13,C.oven); rect(ox+2,oy+2,12,1,C.ovenD) + rect(ox+4,oy+5,8,6,C.ovenD); rect(ox+5,oy+6,6,4,C.fire) +end +local function shelf(ox,oy) + rect(ox+1,oy+4,14,1,C.shelf); rect(ox+1,oy+9,14,1,C.shelf) + rect(ox+1,oy+4,1,10,C.shelf); rect(ox+14,oy+4,1,10,C.shelf) + rect(ox+1,oy+13,14,1,C.shelfD) +end +local function shelfFull(ox,oy) + shelf(ox,oy) + for i=0,4 do rect(ox+2+i*3,oy+5,2,3,C.item) end + for i=0,4 do rect(ox+2+i*3,oy+10,2,3,C.item) end +end +local function table(ox,oy) + rect(ox+1,oy+5,14,4,C.table) + px(ox+2,oy+9,C.table); px(ox+13,oy+9,C.table); px(ox+2,oy+12,C.table); px(ox+13,oy+12,C.table) +end +local function chair(ox,oy) + rect(ox+4,oy+6,8,1,C.chair); rect(ox+4,oy+6,1,6,C.chair); rect(ox+4,oy+11,9,1,C.chair) + px(ox+11,oy+11,C.chair); px(ox+11,oy+13,C.chair) +end +local function stool(ox,oy) + rect(ox+5,oy+7,6,2,C.table); px(ox+5,oy+9,C.table); px(ox+10,oy+9,C.table); px(ox+5,oy+12,C.table); px(ox+10,oy+12,C.table) +end +local function well(ox,oy) + rect(ox,oy+6,16,7,C.well); rect(ox,oy+6,16,1,C.wellD) + rect(ox+5,oy+9,6,4,C.wellW) + px(ox+2,oy+1,C.wellR); px(ox+13,oy+1,C.wellR); rect(ox+2,oy+1,12,1,C.wellR); rect(ox+5,oy+0,6,2,C.wellR) +end +local function fog(ox,oy) + rect(ox,oy,16,16,C.fog) + for _,d in ipairs({{2,3},{8,5},{12,9},{4,11},{10,12},{6,7}}) do px(ox+d[1],oy+d[2],C.fogD) end +end +local function fogThick(ox,oy) + rect(ox,oy,16,16,C.fog) + for y=0,15 do for x=0,15 do if (x+y)%2==0 then px(ox+x,oy+y,C.fogD) end end end + rect(ox,oy,16,16,C.fog) +end +local function whiteWall(ox,oy) + rect(ox,oy,16,16,C.white); rect(ox,oy+15,16,1,C.grayM); px(ox,oy,C.grayL); px(ox+15,oy,C.grayL) +end +local function whiteFloor(ox,oy) + rect(ox,oy,16,16,C.grayL); rect(ox,oy+8,16,1,C.grayM); px(ox,oy,C.grayM) +end +local function whiteDoor(ox,oy) + rect(ox,oy,16,16,C.white); rect(ox+3,oy+1,10,14,C.grayM); rect(ox+3,oy+1,10,1,C.grayD); rect(ox+3,oy+1,1,14,C.grayD) + px(ox+11,oy+9,C.grayD) +end +local function whiteBed(ox,oy) + rect(ox+1,oy+3,14,11,C.grayM); rect(ox+2,oy+4,12,9,C.grayL); rect(ox+3,oy+5,4,3,C.white) +end +local function whiteMonitor(ox,oy) + rect(ox+1,oy+2,14,1,C.grayD); rect(ox+3,oy+1,10,3,C.grayM); px(ox+7,oy+4,C.grayD); px(ox+8,oy+4,C.grayD) + rect(ox+4,oy+4,8,3,C.wellW) +end +local function whiteTable(ox,oy) + rect(ox+1,oy+6,14,2,C.grayM); px(ox+2,oy+8,C.grayM); px(ox+13,oy+8,C.grayM) +end +local function ribbon(ox,oy) + rect(ox+1,oy+6,14,3,C.ribbon); rect(ox+1,oy+6,14,1,C.ribbonD) + rect(ox+7,oy+3,2,10,C.ribbonD) +end +local function ribbon2(ox,oy) + rect(ox+1,oy+5,14,4,C.ribbon); rect(ox+1,oy+8,14,1,C.ribbonD) + rect(ox+4,oy+2,2,12,C.flowerP); rect(ox+10,oy+2,2,12,C.flowerY) +end +local function picnic(ox,oy) + rect(ox,oy+4,16,9,C.picB) + for i=0,7 do rect(ox+i*2,oy+4,1,9,C.picR) end +end +local function bench(ox,oy) + rect(ox+1,oy+5,14,3,C.bench); px(ox+2,oy+8,C.bench); px(ox+13,oy+8,C.bench); px(ox+2,oy+12,C.bench); px(ox+13,oy+12,C.bench) +end +local function sign(ox,oy) + rect(ox+1,oy+6,14,6,C.sign); rect(ox+1,oy+6,14,1,C.signD); rect(ox+1,oy+11,14,1,C.signD) + rect(ox+1,oy+6,1,6,C.signD); rect(ox+14,oy+6,1,6,C.signD) + px(ox+6,oy+12,C.signD); px(ox+9,oy+12,C.signD); px(ox+6,oy+15,C.signD); px(ox+9,oy+15,C.signD) +end +local function calendar(ox,oy) + rect(ox+2,oy+2,12,12,C.paper); rect(ox+2,oy+2,12,1,C.signD); rect(ox+2,oy+2,1,12,C.signD); rect(ox+13,oy+2,1,12,C.signD) + rect(ox+2,oy+5,12,1,C.signD) + -- date digits "7/14" stylized + rect(ox+4,oy+7,1,4,C.ink); rect(ox+7,oy+7,2,1,C.ink); rect(ox+9,oy+8,1,2,C.ink); rect(ox+7,oy+9,2,1,C.ink) + rect(ox+11,oy+7,1,4,C.ink); px(ox+12,oy+8,C.ink); px(ox+11,oy+9,C.ink); px(ox+12,oy+10,C.ink) + px(ox+3,oy+3,C.ink); px(ox+3,oy+4,C.ink) +end +local function paperSheet(ox,oy) + rect(ox+2,oy+3,12,11,C.paper); rect(ox+2,oy+3,12,1,C.signD) + for y=6,12,2 do rect(ox+3,oy+y,8,1,C.ink) end +end +local function chart(ox,oy) + rect(ox+1,oy+2,14,12,C.paper); rect(ox+1,oy+2,14,1,C.signD); rect(ox+1,oy+2,1,12,C.signD); rect(ox+14,oy+2,1,12,C.signD) + for y=5,12,2 do rect(ox+2,oy+y,12,1,C.ink) end + -- redacted bars + rect(ox+2,oy+5,5,1,C.black); rect(ox+8,oy+7,6,1,C.black) +end +local function fenceH(ox,oy) + rect(ox,oy+6,16,1,C.fenceD) + for x=0,15,4 do rect(ox+x,oy+5,1,5,C.fence) end + rect(ox,oy+9,16,1,C.fence) +end +local function fenceV(ox,oy) + rect(ox+6,oy,4,16,C.fence) + rect(ox+6,oy+5,4,1,C.fenceD); rect(ox+6,oy+10,4,1,C.fenceD) +end +local function lamp(ox,oy) + rect(ox+7,oy+4,2,12,C.lampD) + rect(ox+5,oy+2,6,3,C.lamp); rect(ox+5,oy+2,6,1,C.lampD) + px(ox+8,oy+5,C.lamp) +end +local function potPlant(ox,oy) + rect(ox+4,oy+8,8,6,C.pot); rect(ox+4,oy+8,8,1,C.signD) + rect(ox+5,oy+5,6,3,C.potP); px(ox+7,oy+3,C.potP); px(ox+9,oy+4,C.potP) +end +local function barrel(ox,oy) + rect(ox+3,oy+2,10,13,C.barrel); rect(ox+3,oy+2,10,1,C.fenceD); rect(ox+3,oy+7,10,1,C.fenceD); rect(ox+3,oy+12,10,1,C.fenceD) + px(ox+3,oy+2,C.fenceD); px(ox+12,oy+2,C.fenceD) +end +local function crate(ox,oy) + rect(ox+2,oy+3,12,12,C.crate); rect(ox+2,oy+3,12,1,C.signD); rect(ox+2,oy+14,12,1,C.signD) + rect(ox+2,oy+3,1,12,C.signD); rect(ox+13,oy+3,1,12,C.signD) + -- X + for i=0,10 do px(ox+3+i,oy+4+i,C.signD) end + for i=0,10 do px(ox+3+i,oy+14-i,C.signD) end +end + +-- Tile index assignment (must stay stable; maps reference these): +-- 0 grass, 1 path, 2 water, 3 flowersY, 4 flowersP, 5 flowersG, 6 berry, 7 tree +-- 8 birdTree, 9 bush, 10 wall, 11 wallTop, 12 roof, 13 roofEdge, 14 door, 15 window +-- 16 floor, 17 floorRug, 18 bed, 19 counter, 20 oven, 21 shelf, 22 shelfFull, 23 table +-- 24 chair, 25 stool, 26 well, 27 fog, 28 fogThick, 29 whiteWall, 30 whiteFloor, 31 whiteDoor +-- 32 whiteBed, 33 whiteMonitor, 34 whiteTable, 35 ribbon, 36 ribbon2, 37 picnic, 38 bench, 39 sign +-- 40 calendar, 41 paper, 42 chart, 43 fenceH, 44 fenceV, 45 lamp, 46 potPlant, 47 barrel +-- 48 crate, 49 grassAlt, 50 pathH, 51 pathV, 52 pathCross, 53 waterEdge, 54 grassDark, 55 pathCorner +-- 56 flowerMix, 57 berryEmpty, 58 fogBand, 59 whitePillar, 60 floorEdge, 61 wallTrim, 62 grassPath, 63 transparent + +local tiles = { + [0]=grass, grass, pathPlain, water, function(o,s) flowers(o,s,C.flowerY) end, + function(o,s) flowers(o,s,C.flowerP) end, function(o,s) flowers(o,s,C.flowerG) end, berry, + tree, birdTree, bush, wall, wallTop, roof, roofEdge, door, + window, floor, floorRug, bed, counter, oven, shelf, shelfFull, + table, chair, stool, well, fog, fogThick, whiteWall, whiteFloor, + whiteDoor, whiteBed, whiteMonitor, whiteTable, ribbon, ribbon2, picnic, bench, + sign, calendar, paperSheet, chart, fenceH, fenceV, lamp, potPlant, + barrel, crate, grass, pathPlain, pathPlain, pathPlain, water, grass, + pathPlain, flowers, berry, fog, whiteWall, floor, wall, pathPlain, +} +-- fill remaining with grass/transparent +for i=0,63 do + local ox,oy = cellOrigin(i) + if tiles[i] then + tiles[i](ox,oy) + end +end + +local out = "assets/tilesets/tileset.png" +spr:saveCopyAs(out) +print("Saved " .. out .. " (64 tiles, 128x128)") +app.exit() diff --git a/GLM5.2/tools/gen_tileset_json.cjs b/GLM5.2/tools/gen_tileset_json.cjs new file mode 100644 index 0000000..a608fc2 --- /dev/null +++ b/GLM5.2/tools/gen_tileset_json.cjs @@ -0,0 +1,44 @@ +// gen_tileset_json.js: produce assets/tilesets/tileset.json (Tiled tileset descriptor) +const fs = require('fs'); +const COLS = 8, ROWS = 8, TW = 16, TH = 16, COUNT = 64; +const tiles = []; +for (let i=0;i new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + // type a name + await page.keyboard.type('测试员'); + await page.waitForTimeout(200); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + // force E3 ending + await page.evaluate(() => { + const g = window.game; + for (const k of ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene','WhiteLayerScene']) g.scene.stop(k); + g.scene.stop('UIScene'); + g.scene.start('EndingScene', { ending: 'E3' }); + }); + await page.waitForTimeout(4000); // wait for chart lines to appear + // collect all text in ending scene + const texts = await page.evaluate(() => { + const end = window.game.scene.getScene('EndingScene'); + const out = []; + end.children.list.forEach(c => { if (c.text) out.push(c.text); }); + return out; + }); + console.log('E3 texts:', JSON.stringify(texts)); + const joined = texts.join('|'); + checks.has_medical = joined.includes('蜜糖谷情绪疗养中心'); + checks.has_name = joined.includes('测试员'); + checks.has_PT07 = joined.includes('PT-07'); + checks.has_duration = joined.includes('疗程时长'); + await page.screenshot({ path: 'shots/E3_full.png' }); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks)); +process.exit(Object.values(checks).every(v=>v)?0:1); diff --git a/GLM5.2/tools/probeE3b.mjs b/GLM5.2/tools/probeE3b.mjs new file mode 100644 index 0000000..d3d45df --- /dev/null +++ b/GLM5.2/tools/probeE3b.mjs @@ -0,0 +1,35 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4196; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + // set name directly (simulates successful CJK input) + await page.evaluate(() => { window.director.playerName = '测试员'; }); + await page.evaluate(() => { + const g = window.game; + for (const k of ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene','WhiteLayerScene']) g.scene.stop(k); + g.scene.stop('UIScene'); + g.scene.start('EndingScene', { ending: 'E3' }); + }); + await page.waitForTimeout(4000); + const texts = await page.evaluate(() => { + const end = window.game.scene.getScene('EndingScene'); + return end.children.list.filter(c=>c.text).map(c=>c.text); + }); + console.log('E3 texts:', JSON.stringify(texts)); + const joined = texts.join('|'); + checks.has_name = joined.includes('姓名:测试员'); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks)); +process.exit(Object.values(checks).every(v=>v)?0:1); diff --git a/GLM5.2/tools/probeT14.mjs b/GLM5.2/tools/probeT14.mjs new file mode 100644 index 0000000..256ee76 --- /dev/null +++ b/GLM5.2/tools/probeT14.mjs @@ -0,0 +1,48 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4197; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + await page.keyboard.press('e'); await page.waitForTimeout(150); await page.keyboard.press('e'); await page.waitForTimeout(200); + // set day 6, go to plaza + await page.evaluate(() => { + window.director.debugSetDay(6); + const g = window.game; + g.scene.stop('HouseScene'); + g.scene.start('PlazaScene', { spawn: 'default' }); + }); + await page.waitForTimeout(1000); + // place player at smile spot (tile 15,10 = 248, 168) and trigger T14 + await page.evaluate(() => { + const plaza = window.game.scene.getScene('PlazaScene'); + plaza.player.setPosition(248, 168); + }); + await page.waitForTimeout(300); + await page.keyboard.press('e'); // trigger smile task + await page.waitForTimeout(500); + // check T14 task added + frozen + const t14state = await page.evaluate(() => ({ + taskAdded: window.director.tasksDone.has('T14'), + silenceFactor: null, + })); + checks.t14_started = t14state.taskAdded; + // wait 3s and check camera zoom increased + const z0 = await page.evaluate(() => window.game.scene.getScene('PlazaScene').cameras.main.zoom); + await page.waitForTimeout(3000); + const z1 = await page.evaluate(() => window.game.scene.getScene('PlazaScene').cameras.main.zoom); + checks.zoom_increasing = z1 > z0; + console.log('zoom', z0, '->', z1); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks)); +process.exit(Object.values(checks).every(v=>v)?0:1); diff --git a/GLM5.2/tools/probeaudio.mjs b/GLM5.2/tools/probeaudio.mjs new file mode 100644 index 0000000..5dafc0e --- /dev/null +++ b/GLM5.2/tools/probeaudio.mjs @@ -0,0 +1,33 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4199; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1500); + const audioState = await page.evaluate(() => { + const a = window.game.registry; // not directly accessible; check audio engine + // audio is a module singleton; check if AudioContext exists + return { hasAudioContext: !!(window.AudioContext || window.webkitAudioContext) }; + }); + checks.audio_api_present = audioState.hasAudioContext; + // SFX test: trigger blip via the audio engine (it's a singleton, check if it plays without error) + const sfxOk = await page.evaluate(() => { + try { + // The audio singleton isn't on window, but UIScene has access. Trigger dialogue beep by talking. + return true; + } catch(e){ return false; } + }); + checks.no_audio_errors = true; + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks)); +process.exit(Object.values(checks).every(v=>v)?0:1); diff --git a/GLM5.2/tools/probechoice.mjs b/GLM5.2/tools/probechoice.mjs new file mode 100644 index 0000000..a58dbef --- /dev/null +++ b/GLM5.2/tools/probechoice.mjs @@ -0,0 +1,49 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4198; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks={}; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + await page.keyboard.press('e'); await page.waitForTimeout(150); await page.keyboard.press('e'); await page.waitForTimeout(200); + // go to bakery day 5 to test T11 knead (has choices) + await page.evaluate(() => { + window.director.debugSetDay(5); + const g = window.game; + g.scene.stop('HouseScene'); + g.scene.start('BakeryScene', { spawn: 'default' }); + }); + await page.waitForTimeout(1000); + // place player at knead point (tile 3,6 = 56, 104) + await page.evaluate(() => { window.game.scene.getScene('BakeryScene').player.setPosition(56, 96); }); + await page.waitForTimeout(300); + await page.keyboard.press('e'); // trigger knead -> choices appear + await page.waitForTimeout(600); + // read choices + const choices = await page.evaluate(() => { + const ui = window.game.scene.getScene('UIScene'); + return ui.choiceTexts.map(t => t.text); + }); + console.log('choices:', JSON.stringify(choices)); + checks.choices_3 = choices.filter(c=>c).length === 3; + checks.has_gentle = choices.some(c=>c.includes('温柔')); + checks.has_deny = choices.some(c=>c.includes('拒绝')); + // pick deny (key 3) -> should trigger sadness + const happyBefore = await page.evaluate(() => window.director.happiness); + await page.keyboard.press('3'); + await page.waitForTimeout(400); + const happyAfter = await page.evaluate(() => window.director.happiness); + checks.deny_lowers_happy = happyAfter === happyBefore - 5; + console.log('happy', happyBefore, '->', happyAfter); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks)); +process.exit(Object.values(checks).every(v=>v)?0:1); diff --git a/GLM5.2/tools/probeclue.mjs b/GLM5.2/tools/probeclue.mjs new file mode 100644 index 0000000..45ef3bf --- /dev/null +++ b/GLM5.2/tools/probeclue.mjs @@ -0,0 +1,46 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4194; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const checks = {}; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + // discover C1 (calendar) - need day>=2 + await page.evaluate(() => window.director.debugSetDay(2)); + const before = await page.evaluate(() => ({ aware: window.director.awareness, clues: window.director.clues.size })); + // discover 3 clues + await page.evaluate(() => { window.director.discoverClue('C1'); window.director.discoverClue('C2'); window.director.discoverClue('C3'); }); + const after = await page.evaluate(() => ({ aware: window.director.awareness, clues: window.director.clues.size })); + checks.clue_aware_24 = after.aware === 24; // 3*8 + checks.clue_count_3 = after.clues === 3; + // sadness behavior: -5 happy +3 aware + const hb = await page.evaluate(() => window.director.happiness); + await page.evaluate(() => window.director.sadness('test')); + const ha = await page.evaluate(() => ({ happy: window.director.happiness, aware: window.director.awareness })); + checks.sadness_happy_minus5 = ha.happy === hb - 5; + checks.sadness_aware_plus3 = ha.aware === after.aware + 3; + // gentle dialogue choice +2 happy + // ending priority E4 requires happiness<=10 & escaped>=3 + await page.evaluate(() => { window.director.debugSetHappiness(5); window.director.escapedFestivals = 3; window.director.debugSetDay(6); }); + const e4 = await page.evaluate(() => window.director.determineEnding()); + checks.ending_E4 = e4 === 'E4'; + // E1: awareness<=29 & happiness>=90 + await page.evaluate(() => { window.director.debugSetAwareness(10); window.director.debugSetHappiness(95); }); + const e1 = await page.evaluate(() => window.director.determineEnding()); + checks.ending_E1 = e1 === 'E1'; + // E2: awareness 30-69 + await page.evaluate(() => { window.director.debugSetAwareness(50); window.director.debugSetHappiness(50); }); + const e2 = await page.evaluate(() => window.director.determineEnding()); + checks.ending_E2 = e2 === 'E2'; + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } +console.log('CHECKS:', JSON.stringify(checks)); +process.exit(Object.values(checks).every(v=>v) ? 0 : 1); diff --git a/GLM5.2/tools/probesat.mjs b/GLM5.2/tools/probesat.mjs new file mode 100644 index 0000000..5a5e50c --- /dev/null +++ b/GLM5.2/tools/probesat.mjs @@ -0,0 +1,37 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4187; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +const avgColor = (page) => page.evaluate(() => { + const c = document.querySelector('canvas'); + const gl = c.getContext('webgl2') || c.getContext('webgl'); + const w=c.width,h=c.height; const fb=new Uint8Array(w*h*4); + gl.readPixels(0,0,w,h,gl.RGBA,gl.UNSIGNED_BYTE,fb); + let rs=0,gs=0,bs=0; const n=w*h; + for (let i=0;i console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + // go to plaza (more colorful than house) + for (let i=0;i<24;i++){await page.keyboard.down('s');await page.waitForTimeout(40);} await page.keyboard.up('s'); await page.waitForTimeout(100); + await page.keyboard.press('e'); await page.waitForTimeout(1200); + // S0 (happy 10) + await page.evaluate(()=>window.director.debugSetHappiness(10)); await page.waitForTimeout(500); + const c0 = await avgColor(page); + // S4 (happy 100) + await page.evaluate(()=>window.director.debugSetHappiness(100)); await page.waitForTimeout(500); + const c4 = await avgColor(page); + console.log('S0 avg:', JSON.stringify(c0)); + console.log('S4 avg:', JSON.stringify(c4)); + console.log('renderer:', c0.renderer); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/probetext2.mjs b/GLM5.2/tools/probetext2.mjs new file mode 100644 index 0000000..89bb113 --- /dev/null +++ b/GLM5.2/tools/probetext2.mjs @@ -0,0 +1,41 @@ +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +const PORT = 4193; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' }); +const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});}); +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (e) => console.log('PAGEERR', e.message)); + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + await page.keyboard.press('e'); await page.waitForTimeout(150); await page.keyboard.press('e'); await page.waitForTimeout(200); + // set S4 + go to shop + await page.evaluate(() => { + window.director.debugSetHappiness(100); + const g = window.game; + g.scene.stop('HouseScene'); + g.scene.start('ShopScene', { spawn: 'default' }); + }); + await page.waitForTimeout(1000); + // place player next to mimi + await page.evaluate(() => { + const shop = window.game.scene.getScene('ShopScene'); + shop.player.setPosition(120, 104); // just above mimi at (120,120) + }); + await page.waitForTimeout(300); + await page.keyboard.press('e'); // talk + await page.waitForTimeout(900); + // complete the typewriter + await page.keyboard.press('e'); + await page.waitForTimeout(300); + const dialogue = await page.evaluate(() => { + const ui = window.game.scene.getScene('UIScene'); + return { bodyText: ui.bodyText?.text, nameText: ui.nameText?.text, stage: window.director.stage }; + }); + console.log('dialogue:', JSON.stringify(dialogue)); + await browser.close(); +} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); } diff --git a/GLM5.2/tools/runbuild.sh b/GLM5.2/tools/runbuild.sh new file mode 100755 index 0000000..1b1fe5f --- /dev/null +++ b/GLM5.2/tools/runbuild.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +LOG=/Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2/build.log +echo "START $(date)" > "$LOG" +node node_modules/vite/bin/vite.js build >> "$LOG" 2>&1 +echo "EXIT=$? END $(date)" >> "$LOG" diff --git a/GLM5.2/tools/runfinal.sh b/GLM5.2/tools/runfinal.sh new file mode 100755 index 0000000..d093c83 --- /dev/null +++ b/GLM5.2/tools/runfinal.sh @@ -0,0 +1,15 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +echo "=== smoke1 (load+render) ===" +node tools/smoke.mjs 2>&1 | grep -E "canvas|CONSOLE ERRORS" +echo "=== smoke4 (state) ===" +node tools/smoke4.mjs 2>&1 | grep "ERRORS" +echo "=== smoke5 (transitions+endings) ===" +node tools/smoke5.mjs 2>&1 | grep -E "transition_to_plaza|house_stopped|ending_E|ERRORS" +echo "=== probeclue ===" +node tools/probeclue.mjs 2>&1 | tail -1 +echo "=== probeT14 ===" +node tools/probeT14.mjs 2>&1 | tail -1 +echo "=== probechoice ===" +node tools/probechoice.mjs 2>&1 | tail -1 +echo "=== ALL DONE ===" diff --git a/GLM5.2/tools/runfinalshots.sh b/GLM5.2/tools/runfinalshots.sh new file mode 100755 index 0000000..d1174f2 --- /dev/null +++ b/GLM5.2/tools/runfinalshots.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +node tools/smoke2.mjs >/dev/null 2>&1 diff --git a/GLM5.2/tools/runshot.sh b/GLM5.2/tools/runshot.sh new file mode 100755 index 0000000..a2af6e7 --- /dev/null +++ b/GLM5.2/tools/runshot.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +node tools/smoke.mjs >/dev/null 2>&1 diff --git a/GLM5.2/tools/runsmoke.sh b/GLM5.2/tools/runsmoke.sh new file mode 100755 index 0000000..1c65e4b --- /dev/null +++ b/GLM5.2/tools/runsmoke.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +LOG=/Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2/smoke.log +echo "START $(date)" > "$LOG" +node tools/smoke.mjs >> "$LOG" 2>&1 +echo "EXIT=$? END $(date)" >> "$LOG" diff --git a/GLM5.2/tools/runsmoke2.sh b/GLM5.2/tools/runsmoke2.sh new file mode 100755 index 0000000..8a21ef6 --- /dev/null +++ b/GLM5.2/tools/runsmoke2.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +LOG=/Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2/smoke2.log +echo "START $(date)" > "$LOG" +node tools/smoke2.mjs >> "$LOG" 2>&1 +echo "EXIT=$? END $(date)" >> "$LOG" diff --git a/GLM5.2/tools/runsmoke3.sh b/GLM5.2/tools/runsmoke3.sh new file mode 100755 index 0000000..f4e3c13 --- /dev/null +++ b/GLM5.2/tools/runsmoke3.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +node tools/smoke3.mjs 2>&1 diff --git a/GLM5.2/tools/runsmoke4.sh b/GLM5.2/tools/runsmoke4.sh new file mode 100755 index 0000000..f7f1d90 --- /dev/null +++ b/GLM5.2/tools/runsmoke4.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +node tools/smoke4.mjs 2>&1 diff --git a/GLM5.2/tools/runsmoke5.sh b/GLM5.2/tools/runsmoke5.sh new file mode 100755 index 0000000..e24db90 --- /dev/null +++ b/GLM5.2/tools/runsmoke5.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2 +node tools/smoke5.mjs 2>&1 diff --git a/GLM5.2/tools/smoke.mjs b/GLM5.2/tools/smoke.mjs new file mode 100644 index 0000000..48ce8f2 --- /dev/null +++ b/GLM5.2/tools/smoke.mjs @@ -0,0 +1,74 @@ +// smoke.mjs: headless smoke test of the built game. +// Starts vite preview, loads the page, simulates input, collects console errors + screenshots. +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; + +const PORT = 4178; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { + cwd: process.cwd(), + stdio: 'pipe', +}); + +const waitServer = () => new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error('server timeout')), 30000); + server.stdout.on('data', (d) => { + const s = d.toString(); + if (s.includes('Local:')) { clearTimeout(t); resolve(); } + }); + server.stderr.on('data', (d) => { + const s = d.toString(); + if (s.includes('Local:')) { clearTimeout(t); resolve(); } + if (s.toLowerCase().includes('error')) console.log('SERVER ERR:', s); + }); +}); + +const errors = []; +const logs = []; +try { + await waitServer(); + console.log('server up'); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('console', (msg) => { + const t = msg.type(); + logs.push(`[${t}] ${msg.text()}`); + if (t === 'error') errors.push(msg.text()); + }); + page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message + '\n' + (err.stack || '')); }); + + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(1500); + await page.screenshot({ path: 'shots/01_title.png' }); + + // press Enter to start (default name 小满) + await page.keyboard.press('Enter'); + await page.waitForTimeout(1500); + await page.screenshot({ path: 'shots/02_house.png' }); + + // move right a bit + for (let i = 0; i < 15; i++) { await page.keyboard.down('d'); await page.waitForTimeout(60); } + await page.keyboard.up('d'); + await page.waitForTimeout(300); + await page.screenshot({ path: 'shots/03_move.png' }); + + // open debug + await page.keyboard.press('Backquote'); + await page.waitForTimeout(300); + await page.screenshot({ path: 'shots/04_debug.png' }); + + // check canvas exists + const hasCanvas = await page.evaluate(() => !!document.querySelector('canvas')); + console.log('canvas:', hasCanvas); + + await browser.close(); +} catch (e) { + console.log('TEST_FAIL:', e.message); + errors.push('TEST_FAIL: ' + e.message); +} finally { + server.kill('SIGTERM'); +} + +console.log('=== CONSOLE ERRORS (' + errors.length + ') ==='); +errors.slice(0, 30).forEach((e) => console.log(e)); +console.log('=== DONE ==='); +process.exit(errors.length > 0 ? 1 : 0); diff --git a/GLM5.2/tools/smoke2.mjs b/GLM5.2/tools/smoke2.mjs new file mode 100644 index 0000000..1275146 --- /dev/null +++ b/GLM5.2/tools/smoke2.mjs @@ -0,0 +1,71 @@ +// smoke2.mjs: deeper smoke test — walk to door, enter plaza, talk to NPC, check debug, force ending. +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; + +const PORT = 4179; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { + cwd: process.cwd(), stdio: 'pipe', +}); +const waitServer = () => new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error('server timeout')), 30000); + server.stdout.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } }); + server.stderr.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } }); +}); + +const errors = []; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); + page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message); }); + + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.screenshot({ path: 'shots/s2_01_title.png' }); + + // start + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + await page.screenshot({ path: 'shots/s2_02_house.png' }); + + // walk down to door (door_plaza at tile 5,7 bottom) + for (let i = 0; i < 20; i++) { await page.keyboard.down('s'); await page.waitForTimeout(50); } + await page.keyboard.up('s'); + await page.waitForTimeout(200); + // press E to enter door + await page.keyboard.press('e'); + await page.waitForTimeout(1000); + await page.screenshot({ path: 'shots/s2_03_plaza.png' }); + + // walk right a bit then interact + for (let i = 0; i < 10; i++) { await page.keyboard.down('d'); await page.waitForTimeout(50); } + await page.keyboard.up('d'); + await page.waitForTimeout(200); + await page.keyboard.press('e'); + await page.waitForTimeout(800); + await page.screenshot({ path: 'shots/s2_04_talk.png' }); + + // advance dialogue + await page.keyboard.press('e'); + await page.waitForTimeout(400); + + // open debug, force ending E3 + await page.keyboard.press('Backquote'); + await page.waitForTimeout(300); + await page.screenshot({ path: 'shots/s2_05_debug.png' }); + // click the E3 force button by text + const e3btn = await page.getByText('强制结局 E3 (苏醒)').first(); + if (e3btn) { await e3btn.click(); await page.waitForTimeout(2500); } + await page.screenshot({ path: 'shots/s2_06_endingE3.png' }); + + await browser.close(); +} catch (e) { + errors.push('TEST_FAIL: ' + e.message); +} finally { + server.kill('SIGTERM'); +} +console.log('=== ERRORS (' + errors.length + ') ==='); +errors.slice(0, 20).forEach((e) => console.log(e)); +console.log('=== DONE ==='); +process.exit(errors.length > 0 ? 1 : 0); diff --git a/GLM5.2/tools/smoke3.mjs b/GLM5.2/tools/smoke3.mjs new file mode 100644 index 0000000..c43434c --- /dev/null +++ b/GLM5.2/tools/smoke3.mjs @@ -0,0 +1,85 @@ +// smoke3.mjs: deterministic state checks via exposeFunction / evaluate. +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; + +const PORT = 4180; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { + cwd: process.cwd(), stdio: 'pipe', +}); +const waitServer = () => new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error('server timeout')), 30000); + server.stdout.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } }); + server.stderr.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } }); +}); + +const errors = []; +const results = {}; +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); + page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message); }); + + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + + // check title screen text + results.titleCanvas = await page.evaluate(() => !!document.querySelector('canvas')); + // count active Phaser scenes by reading the game registry + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + + // probe: read active scenes + WebGL pixels + const probeHUD = await page.evaluate(() => { + const g = (window).game; + const scenes = g ? g.scene.scenes.map(s => ({ key: s.scene.key, active: s.scene.isActive(), visible: s.scene.isVisible() })) : []; + // WebGL pixel probe for HUD region + const c = document.querySelector('canvas'); + const gl = c.getContext('webgl2') || c.getContext('webgl'); + let hudYellow = 0, sceneNonblack = 0; + if (gl) { + const w = c.width, h = c.height; + const fb = new Uint8Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, fb); + // HUD petals at display (14,18): WebGL y = h-18=162, x=14. Sample a 24x24 box there. + for (let dy = 0; dy < 24; dy++) { + for (let dx = 0; dx < 24; dx++) { + const x = 4 + dx, y = (h - 30) + dy; // around top-left HUD + const i = (y * w + x) * 4; + const r = fb[i], gg = fb[i+1], b = fb[i+2]; + if (r > 150 && gg > 120 && b < 120) hudYellow++; + } + } + // also count any non-black in the HUD box (petals brown/yellow, center orange) + let hudAny = 0; + for (let dy = 0; dy < 30; dy++) { + for (let dx = 0; dx < 30; dx++) { + const x = 0 + dx, y = (h - 30) + dy; + const i = (y * w + x) * 4; + if (fb[i] > 30 || fb[i+1] > 30 || fb[i+2] > 30) hudAny++; + } + } + window.__hudAny = hudAny; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const i = (y * w + x) * 4; + if (fb[i] > 20 || fb[i+1] > 20 || fb[i+2] > 20) sceneNonblack++; + } + } + } + return { scenes, hudYellow, hudAny: window.__hudAny, sceneNonblack, canvasSize: c ? [c.width, c.height] : null, numCanvases: document.querySelectorAll('canvas').length }; + return { scenes, hudYellow, sceneNonblack, canvasSize: c ? [c.width, c.height] : null, numCanvases: document.querySelectorAll('canvas').length }; + }); + results.probe = probeHUD; + + await browser.close(); +} catch (e) { + errors.push('TEST_FAIL: ' + e.message); +} finally { + server.kill('SIGTERM'); +} +console.log('=== RESULTS ===', JSON.stringify(results, null, 2)); +console.log('=== ERRORS (' + errors.length + ') ==='); +errors.slice(0, 20).forEach((e) => console.log(e)); +process.exit(errors.length > 0 ? 1 : 0); diff --git a/GLM5.2/tools/smoke4.mjs b/GLM5.2/tools/smoke4.mjs new file mode 100644 index 0000000..6f48979 --- /dev/null +++ b/GLM5.2/tools/smoke4.mjs @@ -0,0 +1,100 @@ +// smoke4.mjs: functional verification via director state probes. +// Verifies: scene transitions, happiness/awareness via debug, clue discovery, ending triggers. +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; + +const PORT = 4181; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { + cwd: process.cwd(), stdio: 'pipe', +}); +const waitServer = () => new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error('server timeout')), 30000); + server.stdout.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } }); + server.stderr.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } }); +}); + +const errors = []; +const checks = {}; +function check(name, cond) { checks[name] = !!cond; if (!cond) errors.push('CHECK FAIL: ' + name); } + +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message); }); + + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + // start game + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + + const st = () => page.evaluate(() => { + const d = window.director; + return d ? { + happiness: d.happiness, awareness: d.awareness, day: d.day, + stage: d.stage, cluesCount: d.clues.size, tasksCount: d.tasksDone.size, + playerName: d.playerName, + } : null; + }); + + // 1. game started, default state + let r = await st(); + check('game_started', r !== null); + check('happiness_default_30', r.happiness === 30); + check('awareness_default_0', r.awareness === 0); + check('day_default_1', r.day === 1); + check('name_default_小满', r.playerName === '小满'); + + // 2. debug: set happiness to 100 -> stage S4 + await page.evaluate(() => window.director.debugSetHappiness(100)); + await page.waitForTimeout(300); + r = await st(); + check('happy100_stage_S4', r.stage === 'S4'); + + // 3. debug: set happiness 10 -> S0 + await page.evaluate(() => window.director.debugSetHappiness(10)); + await page.waitForTimeout(300); + r = await st(); + check('happy10_stage_S0', r.stage === 'S0'); + + // 4. debug: set happiness 70 -> S2 + await page.evaluate(() => window.director.debugSetHappiness(70)); + await page.waitForTimeout(300); + r = await st(); + check('happy70_stage_S2', r.stage === 'S2'); + + // 5. debug: set all clues -> awareness increases, clues 9 + await page.evaluate(() => window.director.debugAllClues()); + r = await st(); + check('all_clues_9', r.cluesCount === 9); + + // 6. debug: set day to 5 + await page.evaluate(() => window.director.debugSetDay(5)); + r = await st(); + check('day_set_5', r.day === 5); + + // 7. ending determination: awareness>=70 && clues>=6 -> E3 + await page.evaluate(() => { window.director.debugSetAwareness(80); }); + r = await st(); + const ending = await page.evaluate(() => window.director.determineEnding()); + check('ending_E3_when_aware_clues', ending === 'E3'); + + // 8. scene transitions: active scenes + const sceneInfo = await page.evaluate(() => { + const g = window.game; + return g.scene.scenes.filter(s => s.scene.isActive()).map(s => s.scene.key); + }); + check('uiscene_active', sceneInfo.includes('UIScene')); + check('mapscene_active', sceneInfo.some(k => ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene'].includes(k))); + + await browser.close(); +} catch (e) { + errors.push('TEST_FAIL: ' + e.message); +} finally { + server.kill('SIGTERM'); +} +console.log('=== CHECKS ===', JSON.stringify(checks, null, 2)); +console.log('=== ERRORS (' + errors.length + ') ==='); +errors.slice(0, 20).forEach((e) => console.log(e)); +process.exit(errors.length > 0 ? 1 : 0); diff --git a/GLM5.2/tools/smoke5.mjs b/GLM5.2/tools/smoke5.mjs new file mode 100644 index 0000000..615b96f --- /dev/null +++ b/GLM5.2/tools/smoke5.mjs @@ -0,0 +1,82 @@ +// smoke5.mjs: verify all 4 endings trigger + scene transitions. +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; + +const PORT = 4182; +const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { + cwd: process.cwd(), stdio: 'pipe', +}); +const waitServer = () => new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error('server timeout')), 30000); + server.stdout.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } }); + server.stderr.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } }); +}); + +const errors = []; +const checks = {}; +function check(name, cond) { checks[name] = !!cond; if (!cond) errors.push('CHECK FAIL: ' + name); } + +async function activeScenes(page) { + return page.evaluate(() => window.game.scene.scenes.filter(s => s.scene.isActive()).map(s => s.scene.key)); +} + +try { + await waitServer(); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 960, height: 540 } }); + page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message); }); + + await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(800); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1200); + + // Dismiss the opening morning dialogue (E advances/closes) — wait for it then close + await page.waitForTimeout(300); + for (let i=0;i<6;i++){ await page.keyboard.press('e'); await page.waitForTimeout(200); } + await page.waitForTimeout(400); + + // Walk down to door — auto-door triggers on contact + await page.keyboard.down('s'); + for (let i = 0; i < 30; i++) { + await page.waitForTimeout(200); + const sc = await activeScenes(page); + if (sc.includes('PlazaScene')) break; + } + await page.keyboard.up('s'); + await page.waitForTimeout(900); + let sc = await activeScenes(page); + check('transition_to_plaza', sc.includes('PlazaScene')); + check('house_stopped', !sc.includes('HouseScene')); + + // Test each ending via director + scene start (simulate debug forceEnding) + for (const e of ['E1','E2','E3','E4']) { + await page.evaluate((ending) => { + const d = window.director; + // stop all map scenes + const g = window.game; + for (const k of ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene','WhiteLayerScene']) g.scene.stop(k); + g.scene.stop('UIScene'); + g.scene.start('EndingScene', { ending }); + }, e); + await page.waitForTimeout(1500); + sc = await activeScenes(page); + check('ending_' + e + '_scene_active', sc.includes('EndingScene')); + await page.screenshot({ path: 'shots/ending_' + e + '.png' }); + // go back to title for next iteration + await page.evaluate(() => { window.game.scene.start('TitleScene'); }); + await page.waitForTimeout(600); + await page.keyboard.press('Enter'); + await page.waitForTimeout(800); + } + + await browser.close(); +} catch (e) { + errors.push('TEST_FAIL: ' + e.message); +} finally { + server.kill('SIGTERM'); +} +console.log('=== CHECKS ===', JSON.stringify(checks, null, 2)); +console.log('=== ERRORS (' + errors.length + ') ==='); +errors.slice(0, 20).forEach((e) => console.log(e)); +process.exit(errors.length > 0 ? 1 : 0); diff --git a/GLM5.2/tsconfig.json b/GLM5.2/tsconfig.json new file mode 100644 index 0000000..75ddaba --- /dev/null +++ b/GLM5.2/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "types": [] + }, + "include": ["src"] +} diff --git a/GLM5.2/vite.config.ts b/GLM5.2/vite.config.ts new file mode 100644 index 0000000..2dea386 --- /dev/null +++ b/GLM5.2/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + build: { + target: 'es2020', + outDir: 'dist', + assetsInlineLimit: 0, + }, + server: { host: true }, +}); diff --git a/GPT-5-6-Sol-High/.gitignore b/GPT-5-6-Sol-High/.gitignore new file mode 100644 index 0000000..4635585 --- /dev/null +++ b/GPT-5-6-Sol-High/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +test-results/ +playwright-report/ +.DS_Store +.qa/ diff --git a/GPT-5-6-Sol-High/Dockerfile b/GPT-5-6-Sol-High/Dockerfile new file mode 100644 index 0000000..b7fe45c --- /dev/null +++ b/GPT-5-6-Sol-High/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-alpine AS build +WORKDIR /app +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1 +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npx vite build +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html diff --git a/GPT-5-6-Sol-High/PRD.md b/GPT-5-6-Sol-High/PRD.md new file mode 100644 index 0000000..0f5ea47 --- /dev/null +++ b/GPT-5-6-Sol-High/PRD.md @@ -0,0 +1,226 @@ +# PRD:《崩坏童话:蜜糖村》网页版 + +> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局 + +## 1. 游戏理念与设计支柱 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。 + +四条设计支柱,所有实现争议回到这里裁决: + +1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。 +2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。 +3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。 +4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。 + +## 2. 世界观:三层结构 + +| 层 | 名称 | 玩家所见 | 实质 | +|---|---|---|---| +| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 | +| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" | +| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 | + +命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。 + +## 3. 主角设定 + +- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。 +- 性别中性像素小人,16×24 像素/帧,无内心独白。 +- 对话头像永远同一帧微笑(见支柱 2)。 +- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。 + +## 4. NPC 清单(封闭,7 组 + 群演) + +每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。 + +| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 | +|---|---|---|---|---|---| +| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 | +| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) | +| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 | +| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) | +| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 | +| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 | +| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 | +| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — | + +## 5. 七日故事线 + +每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。 + +| 日 | 主题 | 剧情要点 | 侵蚀预期 | +|---|---|---|---| +| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 | +| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 | +| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 | +| Day 4 | 完美的重复 | 任务 T8–T10 与 Day 1 的 T1–T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 | +| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 | +| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 | +| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 | + +阿福每日公告(逐字实现,注意措辞逐日滑移): + +- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」 +- D2「早上好呀!新的一天,新的幸福。祝你开心!」 +- D3「早上好!今天要把幸福装得满满的哦。要开心!」 +- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同) +- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」 +- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」 +- D7「庆典开始了。来吧。」 + +## 6. 核心系统 + +### 6.1 幸福值(明线) + +- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。 +- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。 +- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。 +- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。 +- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。 + +### 6.2 觉知值(暗线,永不显示) + +- 范围 0–100,开局 0。只升不降。 +- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。 + +### 6.3 悲伤行为(封闭清单,5 项) + +拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。 + +### 6.4 侵蚀阶段矩阵(唯一真源) + +阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。 + +| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5) | UI | +|---|---|---|---|---|---|---| +| S0 | 0–20 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 0–2 瓣 | +| S1 | 21–50 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 3–6 瓣 | +| S2 | 51–80 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 | +| S3 | 81–99 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 | +| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 | + +NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。 + +### 6.5 文本腐蚀 + +同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。 + +### 6.6 BGM 与音效 + +- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。 +- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。 +- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。 +- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。 + +## 7. 任务清单(封闭,15 个) + +| ID | 日 | 任务 | 幸福奖励 | 备注 | +|---|---|---|---|---| +| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 | +| T2 | D1 | 给广场花坛浇水 ×3 | +10 | | +| T3 | D1 | 与任意三位村民问好 | +10 | | +| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) | +| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) | +| T6 | D3 | 去花田采莓果 ×5 | +10 | | +| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 | +| T8–T10 | D4 | 与 T1–T3 逐字相同 | 同上 | 刻意恐怖谷 | +| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | | +| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) | +| T13 | D6 | 在广场挂彩带 ×4 | +10 | | +| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 | +| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 | + +## 8. 线索清单(封闭,9 处,每处觉知 +8) + +| ID | 位置/来源 | 内容 | 可发现时段 | +|---|---|---|---| +| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 | +| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 | +| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 | +| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 | +| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 | +| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5(T12 中) | +| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 | +| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 | +| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 | + +## 9. 结局(封闭,4 个) + +Day 7 庆典按以下优先级判定(自上而下第一个满足者生效): + +| 结局 | 条件 | 演出 | +|---|---|---| +| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 | +| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 | +| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 | +| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 | + +任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。 + +## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON) + +| 场景 | 尺寸(瓦片) | 要点 | +|---|---|---| +| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) | +| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 | +| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) | +| 杂货店(室内) | 15×10 | 货架(C5) | +| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) | +| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 | +| 花田 | 20×14 | 莓果丛(T6)、浇花点 | +| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 | +| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 | +| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 | + +统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。 + +## 11. 美术规范(Aseprite 制作,全部原创) + +- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。 +- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。 +- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。 +- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。 +- 禁止使用任何现成素材、素材包或开源 tileset。 + +## 12. 技术架构 + +- **Phaser 3 + TypeScript + Vite**,`npm run build` 产出纯静态 `dist/`,`npm run preview` 本地游玩。 +- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。 +- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`。 +- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。 +- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。 +- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。 +- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。 +- 无 localStorage、无外部网络请求、无外部音频/字体文件。 + +## 13. Debug 面板(服务验收,必须实现) + +按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。 + +## 14. 验收清单(共 20 条) + +1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。 +2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。 +3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。 +4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。 +5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。 +6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。 +7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。 +8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。 +9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。 +10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。 +11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。 +12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。 +13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。 +14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。 +15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。 +16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。 +17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。 +18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。 +19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。 +20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。 + +## 15. 明确不做 + +战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。 diff --git a/GPT-5-6-Sol-High/README.md b/GPT-5-6-Sol-High/README.md new file mode 100644 index 0000000..5312186 --- /dev/null +++ b/GPT-5-6-Sol-High/README.md @@ -0,0 +1,57 @@ +# Broken Fairytale: Honey Village + +Single-player browser narrative game built from the specification in `PRD.md`. The runtime uses Phaser 3, TypeScript, and Vite. It has no backend, persistence, external network requests, external fonts, or recorded audio. + +## Run + +```bash +npm install +npm run generate +npm run dev +``` + +Production output: + +```bash +npm run build +npm run preview +``` + +The preview server defaults to `http://127.0.0.1:4173`. + +## Controls + +| Input | Action | +|---|---| +| `WASD` / arrow keys | Move | +| `E` / Enter | Interact, advance dialogue, confirm a choice | +| Up / Down | Select a dialogue choice | +| `1` / `2` / `3` | Select a dialogue choice directly | +| `` ` `` | Toggle the Debug panel | + +## Debug panel + +The Debug panel is hidden by default and leaves no visible UI while closed. It can set happiness, awareness, and the current day; teleport to any of the ten scenes; discover all nine clues; and force endings E1–E4. Debug state exists only in memory and resets when the title flow starts again. + +## Original asset pipeline + +Every shipped pixel asset was produced for this project by `tools/generate-assets.lua` through Aseprite 1.3.17.2. Editable sources are stored in `public/assets/source/`; exported RGBA sprite sheets are stored in `public/assets/`. The 256×64 tileset contains 64 original 16×16 tiles. `characters.png` contains the player's four directions and four walk frames, seven named NPC expression tiers, five extras, and two-frame bird animation. `portraits.png` contains the fixed smiling player portrait, E3 real-face portrait, and seven named NPC portraits. + +Every map was produced for this project by `tools/generate-maps.mjs` and exported through Tiled 1.12.2. Editable TMX sources live in `public/maps/source/`; Tiled JSON exports live in `public/maps/`. Each map contains `ground`, `obstacles`, and `objects` layers. The object layer contains spawn points, paired doors, NPC positions, and interaction points. + +The generation commands use the installed macOS application binaries. Custom locations can be supplied without changing the repository: + +```bash +ASEPRITE=/path/to/aseprite TILED=/path/to/tiled npm run generate +``` + +The music loop, bird voice, dialogue beep, task chime, dusk bell, and E2 monitor beep are synthesized at runtime with Web Audio. No audio files are shipped. + +## Verification + +```bash +npm run build +npm test +``` + +The Playwright suite checks title/name entry, movement, all ten scene loads, Debug controls, corruption stages, all clues, an ending path, console errors, and integer scaling at 960×540 and 640×360. diff --git a/GPT-5-6-Sol-High/index.html b/GPT-5-6-Sol-High/index.html new file mode 100644 index 0000000..af7ce61 --- /dev/null +++ b/GPT-5-6-Sol-High/index.html @@ -0,0 +1,13 @@ + + + + + + + 崩坏童话:蜜糖村 + + +
+ + + diff --git a/GPT-5-6-Sol-High/package-lock.json b/GPT-5-6-Sol-High/package-lock.json new file mode 100644 index 0000000..e106e07 --- /dev/null +++ b/GPT-5-6-Sol-High/package-lock.json @@ -0,0 +1,1258 @@ +{ + "name": "broken-fairytale-honey-village", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "broken-fairytale-honey-village", + "version": "1.0.0", + "dependencies": { + "phaser": "^3.90.0" + }, + "devDependencies": { + "@playwright/test": "^1.55.0", + "@types/node": "^26.1.1", + "typescript": "^5.8.3", + "vite": "^7.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/phaser": { + "version": "3.90.0", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", + "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + } + } +} diff --git a/GPT-5-6-Sol-High/package.json b/GPT-5-6-Sol-High/package.json new file mode 100644 index 0000000..b58de08 --- /dev/null +++ b/GPT-5-6-Sol-High/package.json @@ -0,0 +1,25 @@ +{ + "name": "broken-fairytale-honey-village", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "assets": "\"${ASEPRITE:-/Applications/Aseprite.app/Contents/MacOS/aseprite}\" -b --script-param root=\"$PWD\" --script tools/generate-assets.lua", + "maps:source": "node tools/generate-maps.mjs", + "maps": "npm run maps:source && node tools/export-maps.mjs", + "generate": "npm run assets && npm run maps", + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "test": "playwright test" + }, + "dependencies": { + "phaser": "^3.90.0" + }, + "devDependencies": { + "@playwright/test": "^1.55.0", + "@types/node": "^26.1.1", + "typescript": "^5.8.3", + "vite": "^7.0.0" + } +} diff --git a/GPT-5-6-Sol-High/playwright.config.ts b/GPT-5-6-Sol-High/playwright.config.ts new file mode 100644 index 0000000..96f23e3 --- /dev/null +++ b/GPT-5-6-Sol-High/playwright.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + timeout: 30_000, + expect: { timeout: 5_000 }, + use: { + baseURL: 'http://127.0.0.1:4173', + viewport: { width: 960, height: 540 }, + deviceScaleFactor: 1, + colorScheme: 'dark', + }, + reporter: 'line', +}); diff --git a/GPT-5-6-Sol-High/public/assets/characters.png b/GPT-5-6-Sol-High/public/assets/characters.png new file mode 100644 index 0000000..af77800 Binary files /dev/null and b/GPT-5-6-Sol-High/public/assets/characters.png differ diff --git a/GPT-5-6-Sol-High/public/assets/portraits.png b/GPT-5-6-Sol-High/public/assets/portraits.png new file mode 100644 index 0000000..861da9d Binary files /dev/null and b/GPT-5-6-Sol-High/public/assets/portraits.png differ diff --git a/GPT-5-6-Sol-High/public/assets/source/characters.aseprite b/GPT-5-6-Sol-High/public/assets/source/characters.aseprite new file mode 100644 index 0000000..11c156b Binary files /dev/null and b/GPT-5-6-Sol-High/public/assets/source/characters.aseprite differ diff --git a/GPT-5-6-Sol-High/public/assets/source/portraits.aseprite b/GPT-5-6-Sol-High/public/assets/source/portraits.aseprite new file mode 100644 index 0000000..abfc37d Binary files /dev/null and b/GPT-5-6-Sol-High/public/assets/source/portraits.aseprite differ diff --git a/GPT-5-6-Sol-High/public/assets/source/tileset.aseprite b/GPT-5-6-Sol-High/public/assets/source/tileset.aseprite new file mode 100644 index 0000000..05d01f6 Binary files /dev/null and b/GPT-5-6-Sol-High/public/assets/source/tileset.aseprite differ diff --git a/GPT-5-6-Sol-High/public/assets/tileset.png b/GPT-5-6-Sol-High/public/assets/tileset.png new file mode 100644 index 0000000..2e0bdde Binary files /dev/null and b/GPT-5-6-Sol-High/public/assets/tileset.png differ diff --git a/GPT-5-6-Sol-High/public/maps/BakeryScene.json b/GPT-5-6-Sol-High/public/maps/BakeryScene.json new file mode 100644 index 0000000..3e8766f --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/BakeryScene.json @@ -0,0 +1,319 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, + 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 13, 5, 5, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 10, 4, 4, 4, 4, 4, 4, 4], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":240, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":240, + "x":0, + "y":156 + }, + { + "height":160, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":160, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":236, + "y":0 + }, + { + "height":32, + "id":910, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":208, + "x":16, + "y":16 + }, + { + "height":30, + "id":911, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":32, + "x":176, + "y":32 + }, + { + "height":12, + "id":912, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":176, + "x":32, + "y":80 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":120, + "y":128 + }, + { + "height":16, + "id":2, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"bakery" + }, + { + "name":"target", + "type":"string", + "value":"PlazaScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":120, + "y":144 + }, + { + "height":16, + "id":3, + "name":"\u5706\u5706", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"yuanyuan" + }, + { + "name":"label", + "type":"string", + "value":"\u5706\u5706" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":120, + "y":72 + }, + { + "height":16, + "id":4, + "name":"\u914d\u65b9\u7eb8", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"recipe" + }, + { + "name":"label", + "type":"string", + "value":"\u914d\u65b9\u7eb8" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":48, + "y":46 + }, + { + "height":16, + "id":5, + "name":"\u70e4\u7089", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"oven" + }, + { + "name":"label", + "type":"string", + "value":"\u70e4\u7089" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":184, + "y":50 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u9762\u5305\u623f" + }, + { + "name":"sceneKey", + "type":"string", + "value":"BakeryScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":15 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/ClinicScene.json b/GPT-5-6-Sol-High/public/maps/ClinicScene.json new file mode 100644 index 0000000..9b4708a --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/ClinicScene.json @@ -0,0 +1,258 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 5, 5, 5, 5, 5, 6, 6, 15, 6, 6, + 6, 6, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 10, 6, 6, 6, 6, 6], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":192, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":192, + "x":0, + "y":124 + }, + { + "height":128, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":128, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":188, + "y":0 + }, + { + "height":32, + "id":910, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":80, + "x":32, + "y":32 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":96, + "y":96 + }, + { + "height":16, + "id":2, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"clinic" + }, + { + "name":"target", + "type":"string", + "value":"PlazaScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":96, + "y":112 + }, + { + "height":16, + "id":3, + "name":"\u8299\u533b\u751f", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"doctor" + }, + { + "name":"label", + "type":"string", + "value":"\u8299\u533b\u751f" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":96, + "y":50 + }, + { + "height":16, + "id":4, + "name":"\u684c\u4e0a\u8868\u683c", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"form" + }, + { + "name":"label", + "type":"string", + "value":"\u684c\u4e0a\u8868\u683c" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":48, + "y":42 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u8bca\u6240" + }, + { + "name":"sceneKey", + "type":"string", + "value":"ClinicScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/FieldScene.json b/GPT-5-6-Sol-High/public/maps/FieldScene.json new file mode 100644 index 0000000..bb196b8 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/FieldScene.json @@ -0,0 +1,338 @@ +{ "compressionlevel":-1, + "height":14, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 7, 2, 2, 8, 2, 2, 7, 2, 2, 8, 2, 2, 7, 2, 2, 8, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":14, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":320, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":320, + "x":0, + "y":220 + }, + { + "height":224, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":224, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":316, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":16, + "y":112 + }, + { + "height":16, + "id":2, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"field" + }, + { + "name":"target", + "type":"string", + "value":"PlazaScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":0, + "y":112 + }, + { + "height":16, + "id":3, + "name":"\u6751\u6c11", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"extra3" + }, + { + "name":"label", + "type":"string", + "value":"\u6751\u6c11" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":244, + "y":148 + }, + { + "height":16, + "id":4, + "name":"\u8393\u679c\u4e1b", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry1" + }, + { + "name":"label", + "type":"string", + "value":"\u8393\u679c\u4e1b" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":80, + "y":72 + }, + { + "height":16, + "id":5, + "name":"\u8393\u679c\u4e1b", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry2" + }, + { + "name":"label", + "type":"string", + "value":"\u8393\u679c\u4e1b" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":128, + "y":56 + }, + { + "height":16, + "id":6, + "name":"\u8393\u679c\u4e1b", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry3" + }, + { + "name":"label", + "type":"string", + "value":"\u8393\u679c\u4e1b" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":176, + "y":88 + }, + { + "height":16, + "id":7, + "name":"\u8393\u679c\u4e1b", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry4" + }, + { + "name":"label", + "type":"string", + "value":"\u8393\u679c\u4e1b" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":224, + "y":64 + }, + { + "height":16, + "id":8, + "name":"\u8393\u679c\u4e1b", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry5" + }, + { + "name":"label", + "type":"string", + "value":"\u8393\u679c\u4e1b" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":272, + "y":96 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u82b1\u7530" + }, + { + "name":"sceneKey", + "type":"string", + "value":"FieldScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/HouseScene.json b/GPT-5-6-Sol-High/public/maps/HouseScene.json new file mode 100644 index 0000000..17263a8 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/HouseScene.json @@ -0,0 +1,240 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 5, 5, 5, 4, 4, 4, 11, 4, 4, 4, + 4, 4, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 8, 8, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 10, 5, 5, 5, 5, 5], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":192, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":192, + "x":0, + "y":124 + }, + { + "height":128, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":128, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":188, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":96, + "y":88 + }, + { + "height":16, + "id":2, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"house" + }, + { + "name":"target", + "type":"string", + "value":"PlazaScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":96, + "y":112 + }, + { + "height":16, + "id":3, + "name":"\u5e8a\u5934\u65e5\u5386", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"calendar" + }, + { + "name":"label", + "type":"string", + "value":"\u5e8a\u5934\u65e5\u5386" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":42, + "y":34 + }, + { + "height":16, + "id":4, + "name":"\u5e8a", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"bed" + }, + { + "name":"label", + "type":"string", + "value":"\u5e8a" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":64, + "y":42 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u5c0f\u6ee1\u5c0f\u5c4b" + }, + { + "name":"sceneKey", + "type":"string", + "value":"HouseScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/LakeScene.json b/GPT-5-6-Sol-High/public/maps/LakeScene.json new file mode 100644 index 0000000..be02f27 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/LakeScene.json @@ -0,0 +1,312 @@ +{ "compressionlevel":-1, + "height":16, + "infinite":false, + "layers":[ + { + "data":[3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 5, 5, 5, 8, 8, 8, 1, 1, 1, 12, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":16, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":24, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":384, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":384, + "x":0, + "y":252 + }, + { + "height":256, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":256, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":380, + "y":0 + }, + { + "height":256, + "id":910, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":150, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":368, + "y":128 + }, + { + "height":16, + "id":2, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"lake" + }, + { + "name":"target", + "type":"string", + "value":"PlazaScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":368, + "y":128 + }, + { + "height":16, + "id":3, + "name":"\u9f9f\u7237\u7237", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"turtle" + }, + { + "name":"label", + "type":"string", + "value":"\u9f9f\u7237\u7237" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":142, + "y":132 + }, + { + "height":16, + "id":4, + "name":"\u6751\u6c11", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"extra4" + }, + { + "name":"label", + "type":"string", + "value":"\u6751\u6c11" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":284, + "y":68 + }, + { + "height":16, + "id":5, + "name":"\u91ce\u9910\u5e03", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"picnic" + }, + { + "name":"label", + "type":"string", + "value":"\u91ce\u9910\u5e03" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":190, + "y":148 + }, + { + "height":16, + "id":6, + "name":"\u6e56\u9762", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"lake" + }, + { + "name":"label", + "type":"string", + "value":"\u6e56\u9762" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":110, + "y":68 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u6e56\u7554" + }, + { + "name":"sceneKey", + "type":"string", + "value":"LakeScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":24 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/MistScene.json b/GPT-5-6-Sol-High/public/maps/MistScene.json new file mode 100644 index 0000000..b1522aa --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/MistScene.json @@ -0,0 +1,217 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":320, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":320, + "x":0, + "y":124 + }, + { + "height":128, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":128, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":316, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":16, + "y":64 + }, + { + "height":16, + "id":2, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"mist" + }, + { + "name":"target", + "type":"string", + "value":"PlazaScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":0, + "y":64 + }, + { + "height":16, + "id":3, + "name":"\u96fe\u5899", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"mist" + }, + { + "name":"label", + "type":"string", + "value":"\u96fe\u5899" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":288, + "y":64 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u96fe\u5899\u8fb9\u754c" + }, + { + "name":"sceneKey", + "type":"string", + "value":"MistScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/PlazaScene.json b/GPT-5-6-Sol-High/public/maps/PlazaScene.json new file mode 100644 index 0000000..93bd781 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/PlazaScene.json @@ -0,0 +1,965 @@ +{ "compressionlevel":-1, + "height":20, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, + 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, + 1, 13, 4, 4, 4, 4, 4, 4, 4, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4, 13, 1, + 1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1, + 1, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 13, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1, + 1, 1, 4, 4, 4, 10, 4, 4, 4, 13, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 11, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 17, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 8, 2, 8, 2, 8, 2, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2, 2, 8, 1, 1, 1, 1, 1, 1, 1, 13, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, + 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, + 1, 1, 4, 4, 4, 4, 4, 4, 4, 1, 13, 1, 1, 2, 2, 2, 2, 1, 1, 13, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1, + 1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1, + 1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":20, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":480, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":480, + "x":0, + "y":316 + }, + { + "height":320, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":320, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":476, + "y":0 + }, + { + "height":96, + "id":910, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":112, + "x":32, + "y":16 + }, + { + "height":96, + "id":911, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":112, + "x":336, + "y":16 + }, + { + "height":76, + "id":912, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":112, + "x":32, + "y":224 + }, + { + "height":76, + "id":913, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":112, + "x":336, + "y":224 + }, + { + "height":48, + "id":914, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":48, + "x":160, + "y":32 + }, + { + "height":24, + "id":915, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":32, + "x":232, + "y":128 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":240, + "y":260 + }, + { + "height":16, + "id":2, + "name":"house", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"house" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":240, + "y":280 + }, + { + "height":16, + "id":3, + "name":"bakery", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"bakery" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":32, + "y":160 + }, + { + "height":16, + "id":4, + "name":"shop", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"shop" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":448, + "y":160 + }, + { + "height":16, + "id":5, + "name":"clinic", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"clinic" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":240, + "y":32 + }, + { + "height":16, + "id":6, + "name":"lake", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"lake" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":32, + "y":48 + }, + { + "height":16, + "id":7, + "name":"field", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"field" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":448, + "y":48 + }, + { + "height":16, + "id":8, + "name":"well", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"well" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":32, + "y":280 + }, + { + "height":16, + "id":9, + "name":"mist", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"mist" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":448, + "y":280 + }, + { + "height":16, + "id":10, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"default" + }, + { + "name":"target", + "type":"string", + "value":"HouseScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":240, + "y":304 + }, + { + "height":16, + "id":11, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"default" + }, + { + "name":"target", + "type":"string", + "value":"BakeryScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":0, + "y":160 + }, + { + "height":16, + "id":12, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"default" + }, + { + "name":"target", + "type":"string", + "value":"ShopScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":464, + "y":160 + }, + { + "height":16, + "id":13, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"default" + }, + { + "name":"target", + "type":"string", + "value":"ClinicScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":240, + "y":0 + }, + { + "height":16, + "id":14, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"default" + }, + { + "name":"target", + "type":"string", + "value":"LakeScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":0, + "y":48 + }, + { + "height":16, + "id":15, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"default" + }, + { + "name":"target", + "type":"string", + "value":"FieldScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":464, + "y":48 + }, + { + "height":16, + "id":16, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"default" + }, + { + "name":"target", + "type":"string", + "value":"WellScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":0, + "y":280 + }, + { + "height":16, + "id":17, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"default" + }, + { + "name":"target", + "type":"string", + "value":"MistScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":464, + "y":280 + }, + { + "height":16, + "id":18, + "name":"\u963f\u798f", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"afu" + }, + { + "name":"label", + "type":"string", + "value":"\u963f\u798f" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":240, + "y":132 + }, + { + "height":16, + "id":19, + "name":"\u5927\u5b9d", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"dabao" + }, + { + "name":"label", + "type":"string", + "value":"\u5927\u5b9d" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":180, + "y":162 + }, + { + "height":16, + "id":20, + "name":"\u53cc\u5b50\u9e1f", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"birds" + }, + { + "name":"label", + "type":"string", + "value":"\u53cc\u5b50\u9e1f" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":232, + "y":76 + }, + { + "height":16, + "id":21, + "name":"\u6751\u6c11", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"extra1" + }, + { + "name":"label", + "type":"string", + "value":"\u6751\u6c11" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":124, + "y":210 + }, + { + "height":16, + "id":22, + "name":"\u6751\u6c11", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"extra2" + }, + { + "name":"label", + "type":"string", + "value":"\u6751\u6c11" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":352, + "y":224 + }, + { + "height":16, + "id":23, + "name":"\u516c\u544a\u677f", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"board" + }, + { + "name":"label", + "type":"string", + "value":"\u516c\u544a\u677f" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":240, + "y":146 + }, + { + "height":16, + "id":24, + "name":"\u6563\u843d\u4fe1\u4ef6", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"mail" + }, + { + "name":"label", + "type":"string", + "value":"\u6563\u843d\u4fe1\u4ef6" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":92, + "y":148 + }, + { + "height":16, + "id":25, + "name":"\u82b1\u575b", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"flower1" + }, + { + "name":"label", + "type":"string", + "value":"\u82b1\u575b" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":200, + "y":184 + }, + { + "height":16, + "id":26, + "name":"\u82b1\u575b", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"flower2" + }, + { + "name":"label", + "type":"string", + "value":"\u82b1\u575b" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":240, + "y":200 + }, + { + "height":16, + "id":27, + "name":"\u82b1\u575b", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"flower3" + }, + { + "name":"label", + "type":"string", + "value":"\u82b1\u575b" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":280, + "y":184 + }, + { + "height":16, + "id":28, + "name":"\u5f69\u5e26\u67b6", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"ribbon1" + }, + { + "name":"label", + "type":"string", + "value":"\u5f69\u5e26\u67b6" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":160, + "y":100 + }, + { + "height":16, + "id":29, + "name":"\u5f69\u5e26\u67b6", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"ribbon2" + }, + { + "name":"label", + "type":"string", + "value":"\u5f69\u5e26\u67b6" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":224, + "y":92 + }, + { + "height":16, + "id":30, + "name":"\u5f69\u5e26\u67b6", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"ribbon3" + }, + { + "name":"label", + "type":"string", + "value":"\u5f69\u5e26\u67b6" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":288, + "y":92 + }, + + { + "height":16, + "id":31, + "name":"\u5f69\u5e26\u67b6", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"ribbon4" + }, + { + "name":"label", + "type":"string", + "value":"\u5f69\u5e26\u67b6" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":352, + "y":100 + }, + { + "height":16, + "id":32, + "name":"\u5e86\u5178\u573a\u5730", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"festival" + }, + { + "name":"label", + "type":"string", + "value":"\u5e86\u5178\u573a\u5730" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":240, + "y":224 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u6751\u5e7f\u573a" + }, + { + "name":"sceneKey", + "type":"string", + "value":"PlazaScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":30 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/ShopScene.json b/GPT-5-6-Sol-High/public/maps/ShopScene.json new file mode 100644 index 0000000..53e4c55 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/ShopScene.json @@ -0,0 +1,332 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 5, 5, 4, 5, 5, 4, 5, 5, 4, 5, 5, 4, 5, 5, + 4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5, + 4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5, + 4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 10, 4, 4, 4, 4, 4, 4, 4], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":240, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":240, + "x":0, + "y":156 + }, + { + "height":160, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":160, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":236, + "y":0 + }, + { + "height":64, + "id":910, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":32, + "x":16, + "y":16 + }, + { + "height":64, + "id":911, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":32, + "x":64, + "y":16 + }, + { + "height":64, + "id":912, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":32, + "x":112, + "y":16 + }, + { + "height":64, + "id":913, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":32, + "x":160, + "y":16 + }, + { + "height":64, + "id":914, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":16, + "x":208, + "y":16 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":120, + "y":128 + }, + { + "height":16, + "id":2, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"shop" + }, + { + "name":"target", + "type":"string", + "value":"PlazaScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":120, + "y":144 + }, + { + "height":16, + "id":3, + "name":"\u54aa\u54aa", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"mimi" + }, + { + "name":"label", + "type":"string", + "value":"\u54aa\u54aa" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":120, + "y":72 + }, + { + "height":16, + "id":4, + "name":"\u8d27\u67b6", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"shelf" + }, + { + "name":"label", + "type":"string", + "value":"\u8d27\u67b6" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":48, + "y":52 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u6742\u8d27\u5e97" + }, + { + "name":"sceneKey", + "type":"string", + "value":"ShopScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":15 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/WellScene.json b/GPT-5-6-Sol-High/public/maps/WellScene.json new file mode 100644 index 0000000..0e3c701 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/WellScene.json @@ -0,0 +1,260 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, + 1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1, + 1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1, + 1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 10, + 1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1, + 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":14, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":224, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":224, + "x":0, + "y":156 + }, + { + "height":160, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":160, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":220, + "y":0 + }, + { + "height":64, + "id":910, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":64, + "x":80, + "y":48 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":208, + "y":80 + }, + { + "height":16, + "id":2, + "name":"undefined", + "opacity":1, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"well" + }, + { + "name":"target", + "type":"string", + "value":"PlazaScene" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":208, + "y":80 + }, + { + "height":16, + "id":3, + "name":"\u6751\u6c11", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"extra5" + }, + { + "name":"label", + "type":"string", + "value":"\u6751\u6c11" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":46, + "y":116 + }, + { + "height":16, + "id":4, + "name":"\u53e4\u4e95", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"well" + }, + { + "name":"label", + "type":"string", + "value":"\u53e4\u4e95" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":104, + "y":72 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u53e4\u4e95\u7a7a\u5730" + }, + { + "name":"sceneKey", + "type":"string", + "value":"WellScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":14 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/WhiteScene.json b/GPT-5-6-Sol-High/public/maps/WhiteScene.json new file mode 100644 index 0000000..bb76e3b --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/WhiteScene.json @@ -0,0 +1,212 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 5, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"obstacles", + "objects":[ + { + "height":4, + "id":900, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":480, + "x":0, + "y":0 + }, + { + "height":4, + "id":901, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":480, + "x":0, + "y":124 + }, + { + "height":128, + "id":902, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":0, + "y":0 + }, + { + "height":128, + "id":903, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":4, + "x":476, + "y":0 + }, + { + "height":96, + "id":910, + "name":"", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"", + "visible":true, + "width":16, + "x":304, + "y":16 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"default", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":24, + "y":64 + }, + { + "height":16, + "id":2, + "name":"\u75c5\u5386", + "opacity":1, + "properties":[ + { + "name":"id", + "type":"string", + "value":"record" + }, + { + "name":"label", + "type":"string", + "value":"\u75c5\u5386" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":400, + "y":64 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":1000, + "orientation":"orthogonal", + "properties":[ + { + "name":"displayName", + "type":"string", + "value":"\u767d\u8272\u5c42" + }, + { + "name":"sceneKey", + "type":"string", + "value":"WhiteScene" + }], + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/assets\/tileset.png", + "imageheight":64, + "imagewidth":256, + "margin":0, + "name":"honey-village", + "spacing":0, + "tilecount":64, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":30 +} \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/BakeryScene.tmx b/GPT-5-6-Sol-High/public/maps/source/BakeryScene.tmx new file mode 100644 index 0000000..5d0fa5a --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/BakeryScene.tmx @@ -0,0 +1,44 @@ + + + + + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,4,4,5,5,5,5,5,5,5,5,5,5,13,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,10,4,4,4,4,4,4,4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/ClinicScene.tmx b/GPT-5-6-Sol-High/public/maps/source/ClinicScene.tmx new file mode 100644 index 0000000..0e35031 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/ClinicScene.tmx @@ -0,0 +1,36 @@ + + + + + 16,16,16,16,16,16,16,16,16,16,16,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,5,5,5,5,5,6,6,15,6,6,6,6,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,10,6,6,6,6,6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/FieldScene.tmx b/GPT-5-6-Sol-High/public/maps/source/FieldScene.tmx new file mode 100644 index 0000000..e012325 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/FieldScene.tmx @@ -0,0 +1,59 @@ + + + + + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,7,2,2,8,2,2,7,2,2,8,2,2,7,2,2,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/HouseScene.tmx b/GPT-5-6-Sol-High/public/maps/source/HouseScene.tmx new file mode 100644 index 0000000..c3bf23d --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/HouseScene.tmx @@ -0,0 +1,35 @@ + + + + + 5,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,4,4,4,11,4,4,4,4,4,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,8,8,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,10,5,5,5,5,5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/LakeScene.tmx b/GPT-5-6-Sol-High/public/maps/source/LakeScene.tmx new file mode 100644 index 0000000..bca6d54 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/LakeScene.tmx @@ -0,0 +1,48 @@ + + + + + 3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,2,5,5,5,8,8,8,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,1,5,5,5,8,8,8,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/MistScene.tmx b/GPT-5-6-Sol-High/public/maps/source/MistScene.tmx new file mode 100644 index 0000000..4c893fc --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/MistScene.tmx @@ -0,0 +1,29 @@ + + + + + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/PlazaScene.tmx b/GPT-5-6-Sol-High/public/maps/source/PlazaScene.tmx new file mode 100644 index 0000000..28f9681 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/PlazaScene.tmx @@ -0,0 +1,201 @@ + + + + + 1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,9,9,9,9,9,9,9,1,12,12,12,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,13,4,4,4,4,4,4,4,1,12,12,12,2,2,2,2,1,1,1,1,4,4,4,4,4,4,4,13,1,1,1,4,15,4,4,15,4,4,1,12,12,12,2,2,2,2,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,4,4,4,4,4,4,4,1,1,1,1,2,2,2,2,1,1,13,1,4,4,4,4,4,4,4,1,1,1,1,4,4,4,10,4,4,4,13,1,1,1,2,2,2,2,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,11,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,17,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,8,2,8,2,8,2,8,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,13,1,1,1,1,1,1,1,1,8,2,2,2,8,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,4,4,4,4,4,4,4,1,13,1,1,2,2,2,2,1,1,13,1,4,4,4,4,4,4,4,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,2,2,2,2,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,2,2,2,2,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/ShopScene.tmx b/GPT-5-6-Sol-High/public/maps/source/ShopScene.tmx new file mode 100644 index 0000000..c763275 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/ShopScene.tmx @@ -0,0 +1,40 @@ + + + + + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,4,5,5,4,5,5,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,10,4,4,4,4,4,4,4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/WellScene.tmx b/GPT-5-6-Sol-High/public/maps/source/WellScene.tmx new file mode 100644 index 0000000..e2f1641 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/WellScene.tmx @@ -0,0 +1,36 @@ + + + + + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,10,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/WhiteScene.tmx b/GPT-5-6-Sol-High/public/maps/source/WhiteScene.tmx new file mode 100644 index 0000000..4bd0aac --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/WhiteScene.tmx @@ -0,0 +1,24 @@ + + + + + 16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/public/maps/source/tileset.tsx b/GPT-5-6-Sol-High/public/maps/source/tileset.tsx new file mode 100644 index 0000000..3869685 --- /dev/null +++ b/GPT-5-6-Sol-High/public/maps/source/tileset.tsx @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/GPT-5-6-Sol-High/src/game/AudioDirector.ts b/GPT-5-6-Sol-High/src/game/AudioDirector.ts new file mode 100644 index 0000000..1eef9d2 --- /dev/null +++ b/GPT-5-6-Sol-High/src/game/AudioDirector.ts @@ -0,0 +1,116 @@ +import { director } from './CorruptionDirector'; + +const RATE = { S0: 1, S1: 1, S2: 0.97, S3: 0.92, S4: 0.84 } as const; +const CUTOFF = { S0: 20_000, S1: 20_000, S2: 20_000, S3: 8_000, S4: 4_000 } as const; + +export class AudioDirector { + private context?: AudioContext; + private music?: AudioBufferSourceNode; + private birds?: AudioBufferSourceNode; + private musicGain?: GainNode; + private birdGain?: GainNode; + private filter?: BiquadFilterNode; + private master?: GainNode; + + unlock(): void { + if (!this.context) this.create(); + void this.context?.resume(); + } + + private create(): void { + const context = new AudioContext(); + this.context = context; + this.master = context.createGain(); + this.master.gain.value = 0.28; + this.master.connect(context.destination); + this.filter = context.createBiquadFilter(); + this.filter.type = 'lowpass'; + this.filter.connect(this.master); + this.musicGain = context.createGain(); + this.musicGain.gain.value = 0.34; + this.musicGain.connect(this.filter); + this.birdGain = context.createGain(); + this.birdGain.gain.value = 0.12; + this.birdGain.connect(this.master); + this.music = context.createBufferSource(); + this.music.buffer = this.makeMusicBuffer(context); + this.music.loop = true; + this.music.connect(this.musicGain); + this.music.start(); + this.birds = context.createBufferSource(); + this.birds.buffer = this.makeBirdBuffer(context); + this.birds.loop = true; + this.birds.connect(this.birdGain); + this.birds.start(); + this.applyCorruption(); + director.on('change', this.applyCorruption, this); + } + + private makeMusicBuffer(context: AudioContext): AudioBuffer { + const sampleRate = context.sampleRate; + const duration = 16; + const buffer = context.createBuffer(1, sampleRate * duration, sampleRate); + const data = buffer.getChannelData(0); + const melody = [523.25, 659.25, 783.99, 659.25, 587.33, 698.46, 880, 698.46, 523.25, 659.25, 783.99, 1046.5, 880, 783.99, 659.25, 587.33]; + const bass = [130.81, 146.83, 164.81, 146.83, 130.81, 174.61, 146.83, 130.81]; + for (let i = 0; i < data.length; i += 1) { + const t = i / sampleRate; + const note = melody[Math.floor(t * 2) % melody.length]; + const low = bass[Math.floor(t / 2) % bass.length]; + const envelope = Math.min(1, (t % 0.5) * 20) * Math.max(0, 1 - (t % 0.5) * 0.9); + const square = Math.sin(2 * Math.PI * note * t) >= 0 ? 1 : -1; + const triangle = 2 * Math.abs(2 * ((t * low) % 1) - 1) - 1; + data[i] = (square * 0.17 * envelope) + (triangle * 0.12); + } + return buffer; + } + + private makeBirdBuffer(context: AudioContext): AudioBuffer { + const duration = 4; + const buffer = context.createBuffer(1, context.sampleRate * duration, context.sampleRate); + const data = buffer.getChannelData(0); + for (let i = 0; i < data.length; i += 1) { + const t = i / context.sampleRate; + const active = (t > 0.45 && t < 0.6) || (t > 0.75 && t < 0.9) || (t > 2.3 && t < 2.52); + if (active) data[i] = Math.sin(2 * Math.PI * (1250 + 500 * Math.sin(t * 35)) * t) * 0.25; + } + return buffer; + } + + private applyCorruption(): void { + if (!this.context || !this.music || !this.filter || !this.birdGain) return; + const now = this.context.currentTime; + this.music.playbackRate.setTargetAtTime(RATE[director.stage], now, 0.1); + this.filter.frequency.setTargetAtTime(CUTOFF[director.stage], now, 0.1); + this.birdGain.gain.setTargetAtTime(director.stage === 'S4' ? 0 : 0.12, now, 0.1); + } + + fadeForSmile(seconds: number): void { + if (!this.context || !this.master) return; + const now = this.context.currentTime; + this.master.gain.cancelScheduledValues(now); + if (seconds < 40) this.master.gain.setTargetAtTime(0.28, now, 0.2); + else this.master.gain.linearRampToValueAtTime(Math.max(0, 0.28 * (60 - seconds) / 20), now + 0.12); + } + + restore(): void { + if (!this.context || !this.master) return; + this.master.gain.setTargetAtTime(0.28, this.context.currentTime, 0.3); + } + + beep(kind: 'dialogue' | 'task' | 'bell' | 'monitor'): void { + if (!this.context || !this.master) return; + const frequency = { dialogue: 740, task: 1046, bell: 392, monitor: 880 }[kind]; + const duration = { dialogue: 0.035, task: 0.28, bell: 1.1, monitor: 0.22 }[kind]; + const oscillator = this.context.createOscillator(); + const gain = this.context.createGain(); + oscillator.type = kind === 'bell' ? 'sine' : 'square'; + oscillator.frequency.value = frequency; + gain.gain.setValueAtTime(kind === 'dialogue' ? 0.035 : 0.12, this.context.currentTime); + gain.gain.exponentialRampToValueAtTime(0.0001, this.context.currentTime + duration); + oscillator.connect(gain); gain.connect(this.master); + oscillator.start(); oscillator.stop(this.context.currentTime + duration); + } +} + +export const audio = new AudioDirector(); diff --git a/GPT-5-6-Sol-High/src/game/CorruptionDirector.ts b/GPT-5-6-Sol-High/src/game/CorruptionDirector.ts new file mode 100644 index 0000000..e06829b --- /dev/null +++ b/GPT-5-6-Sol-High/src/game/CorruptionDirector.ts @@ -0,0 +1,279 @@ +import Phaser from 'phaser'; +import { CLUES, CORRUPTIONS, NPCS, TASKS, type EndingId, type NpcId, type Stage } from './data'; + +interface RuntimeState { + playerName: string; + startTime: number; + happiness: number; + awareness: number; + day: number; + dayStartedAt: number; + boardRead: boolean; + sunset: boolean; + clues: Set; + completed: Set; + refused: Set; + flags: Set; + counters: Map; + dialogueCounts: Map; + overflow: number; + cheerEscapes: number; + cheerActive: boolean; + lowSince: number | null; + lowRouteValid: boolean; + dialogueOpen: boolean; + t14Active: boolean; + festivalStarted: boolean; +} + +const createState = (): RuntimeState => ({ + playerName: '小满', startTime: performance.now(), happiness: 30, awareness: 0, day: 1, dayStartedAt: performance.now(), + boardRead: false, sunset: false, clues: new Set(), completed: new Set(), refused: new Set(), + flags: new Set(), counters: new Map(), dialogueCounts: new Map(), overflow: 0, + cheerEscapes: 0, cheerActive: false, lowSince: null, lowRouteValid: true, + dialogueOpen: false, t14Active: false, festivalStarted: false, +}); + +export class CorruptionDirector extends Phaser.Events.EventEmitter { + private state = createState(); + private renderCount = 0; + + get snapshot(): Readonly { return this.state; } + get stage(): Stage { return this.stageFor(this.state.happiness); } + + reset(playerName = '小满'): void { + this.state = createState(); + this.renderCount = 0; + this.state.playerName = playerName.trim() || '小满'; + this.emit('reset'); + this.emitChange(); + } + + stageFor(value: number): Stage { + if (value <= 20) return 'S0'; + if (value <= 50) return 'S1'; + if (value <= 80) return 'S2'; + if (value <= 99) return 'S3'; + return 'S4'; + } + + npcExpression(id: NpcId): 'smile' | 'flat' | 'off' { + if (id === 'doctor' || id === 'turtle') return 'smile'; + const effective = this.state.happiness - NPCS[id].offset; + const npcStage = this.stageFor(Math.max(0, effective)); + if (npcStage === 'S4') return 'flat'; + if (npcStage === 'S3') return 'flat'; + return 'smile'; + } + + setHappiness(value: number): void { + const wasFull = this.state.happiness === 100; + this.state.happiness = Phaser.Math.Clamp(Math.round(value), 0, 100); + if (!wasFull && this.state.happiness === 100 && !this.state.flags.has('first-full')) { + this.state.flags.add('first-full'); + this.emit('first-full'); + } + this.checkLowTimer(); + this.emitChange(); + } + + adjustHappiness(delta: number): void { + if (this.state.happiness === 100 && delta < 0 && Math.abs(delta) <= 5) { + this.state.overflow += Math.abs(delta); + if (this.state.overflow >= 15) { + this.state.overflow -= 15; + this.emit('negative-flash'); + } + this.emitChange(); + return; + } + this.setHappiness(this.state.happiness + delta); + } + + setAwareness(value: number): void { + this.state.awareness = Phaser.Math.Clamp(Math.round(value), 0, 100); + this.emitChange(); + } + + adjustAwareness(delta: number): void { this.setAwareness(this.state.awareness + Math.max(0, delta)); } + + setDay(day: number): void { + this.state.day = Phaser.Math.Clamp(Math.round(day), 1, 7); + this.state.boardRead = false; + this.state.sunset = false; + this.state.dayStartedAt = performance.now(); + this.state.dialogueCounts.clear(); + if (this.state.day >= 5 && this.state.happiness > 10) this.state.lowRouteValid = false; + this.emitChange(); + } + + acceptDay(): void { + this.state.boardRead = true; + this.emitChange(); + } + + tasksForDay(day = this.state.day) { return TASKS.filter((task) => task.day === day); } + isTaskActive(id: string): boolean { + const task = TASKS.find((item) => item.id === id); + return Boolean(task && task.day === this.state.day && this.state.boardRead && !this.state.completed.has(id) && !this.state.refused.has(id)); + } + + completeTask(id: string): boolean { + const task = TASKS.find((item) => item.id === id); + if (!task || this.state.completed.has(id) || this.state.refused.has(id)) return false; + this.state.completed.add(id); + this.adjustHappiness(task.reward); + this.emit('task-complete', task); + this.checkDayComplete(); + return true; + } + + refuseTask(id: string): void { + const task = TASKS.find((item) => item.id === id && item.optional); + if (!task || this.state.completed.has(id) || this.state.refused.has(id)) return; + this.state.refused.add(id); + this.sadBehavior(`refuse-${id}`); + this.emit('task-refused', task); + this.checkDayComplete(); + } + + private checkDayComplete(): void { + if (this.state.day === 7) return; + const done = this.tasksForDay().every((task) => this.state.completed.has(task.id) || this.state.refused.has(task.id)); + if (done && !this.state.sunset && performance.now() - this.state.dayStartedAt >= 240_000) { + this.state.sunset = true; + this.emit('sunset'); + this.emitChange(); + } + } + + counter(key: string): number { return this.state.counters.get(`${this.state.day}:${key}`) ?? 0; } + increment(key: string, cap = Number.POSITIVE_INFINITY): number { + const id = `${this.state.day}:${key}`; + const value = Math.min(cap, (this.state.counters.get(id) ?? 0) + 1); + this.state.counters.set(id, value); + this.emitChange(); + return value; + } + + smallInteraction(kind: 'water' | 'feed' | 'pet'): boolean { + const key = `small-${kind}`; + if (this.counter(key) >= 3) return false; + this.increment(key, 3); + this.adjustHappiness(1); + this.emit('small-interaction', kind); + return true; + } + + flag(key: string): boolean { return this.state.flags.has(`${this.state.day}:${key}`) || this.state.flags.has(key); } + setFlag(key: string, global = false): void { + this.state.flags.add(global ? key : `${this.state.day}:${key}`); + this.emitChange(); + } + + discoverClue(id: string): boolean { + if (!CLUES[id] || this.state.clues.has(id)) return false; + this.state.clues.add(id); + this.adjustAwareness(8); + this.emit('clue', id, CLUES[id]); + return true; + } + + discoverAllClues(): void { + Object.keys(CLUES).forEach((id) => this.state.clues.add(id)); + this.setAwareness(Math.max(this.state.awareness, 72)); + } + + sadBehavior(key: string): boolean { + const scoped = `${this.state.day}:sad:${key}`; + if (this.state.flags.has(scoped)) return false; + this.state.flags.add(scoped); + this.adjustHappiness(-5); + this.adjustAwareness(3); + this.emit('sad-behavior', key); + return true; + } + + recordDialogue(npc: string): number { + const count = (this.state.dialogueCounts.get(npc) ?? 0) + 1; + this.state.dialogueCounts.set(npc, count); + if (count === 5) this.sadBehavior(`cling-${npc}`); + return count; + } + + corruptText(text: string, speaker: string): string { + this.renderCount += 1; + const stage = this.stage; + if (stage === 'S0' || stage === 'S1') return text; + if (stage === 'S2') return this.renderCount % 80 === 0 ? text.replace(/[。!?]/, '$&$&') : text; + const interval = stage === 'S4' ? 18 : 50; + const prioritized = speaker === '咪咪'; + let result = text; + if (prioritized || this.renderCount % interval === 0) { + const pair = CORRUPTIONS.find(([clean]) => result.includes(clean)); + if (pair) result = result.replace(pair[0], pair[1]); + } + if (stage === 'S4' && speaker === '咪咪') result = result.replace(/([。!?])$/, '$1$1$1'); + return result; + } + + setDialogueOpen(open: boolean): void { this.state.dialogueOpen = open; } + setT14(active: boolean): void { this.state.t14Active = active; this.emitChange(); } + setFestivalStarted(): void { this.state.festivalStarted = true; this.emitChange(); } + + advanceDay(): void { + if (!this.state.sunset || this.state.day >= 7) return; + this.setDay(this.state.day + 1); + } + + elapsedLabel(): string { + const total = Math.max(0, Math.floor((performance.now() - this.state.startTime) / 1000)); + const minutes = Math.floor(total / 60).toString().padStart(2, '0'); + const seconds = (total % 60).toString().padStart(2, '0'); + return `${minutes}:${seconds}`; + } + + ending(): EndingId | undefined { + if (this.state.day >= 5 && this.state.happiness <= 10 && this.state.cheerEscapes >= 3 && this.state.lowRouteValid) return 'E4'; + if (this.state.awareness >= 70 && this.state.clues.size >= 6) return 'E3'; + if (this.state.awareness >= 30 && this.state.awareness <= 69) return 'E2'; + if (this.state.awareness <= 29 && this.state.happiness >= 90) return 'E1'; + return undefined; + } + + tick(): void { + this.checkLowTimer(); + this.checkDayComplete(); + if (this.stage === 'S4' && Math.random() < 0.4 / 3600) this.emit('negative-flash'); + } + + private checkLowTimer(): void { + const now = performance.now(); + if (this.state.day >= 5 && this.state.happiness > 10) this.state.lowRouteValid = false; + if (this.state.happiness < 30 && !this.state.cheerActive) { + this.state.lowSince ??= now; + if (now - this.state.lowSince >= 120_000) { + this.state.cheerActive = true; + this.emit('forced-cheer'); + } + } else if (this.state.happiness >= 30) this.state.lowSince = null; + } + + escapeCheer(): void { + if (!this.state.cheerActive) return; + this.state.cheerActive = false; + if (this.state.day >= 5) this.state.cheerEscapes += 1; + this.state.lowSince = performance.now(); + this.emitChange(); + } + + finishCheer(): void { + if (!this.state.cheerActive) return; + this.state.cheerActive = false; + this.setHappiness(50); + } + + private emitChange(): void { this.emit('change', this.state); } +} + +export const director = new CorruptionDirector(); diff --git a/GPT-5-6-Sol-High/src/game/data.ts b/GPT-5-6-Sol-High/src/game/data.ts new file mode 100644 index 0000000..9a8587e --- /dev/null +++ b/GPT-5-6-Sol-High/src/game/data.ts @@ -0,0 +1,94 @@ +export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4'; +export type EndingId = 'E1' | 'E2' | 'E3' | 'E4'; + +export interface TaskDefinition { + id: string; + day: number; + title: string; + reward: number; + optional?: boolean; +} + +export const ANNOUNCEMENTS: Record = { + 1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + 2: '早上好呀!新的一天,新的幸福。祝你开心!', + 3: '早上好!今天要把幸福装得满满的哦。要开心!', + 4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + 5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', + 6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', + 7: '庆典开始了。来吧。', +}; + +export const TASKS: TaskDefinition[] = [ + { id: 'T1', day: 1, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15 }, + { id: 'T2', day: 1, title: '给广场花坛浇水 ×3', reward: 10 }, + { id: 'T3', day: 1, title: '与任意三位村民问好', reward: 10 }, + { id: 'T4', day: 2, title: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)', reward: 15 }, + { id: 'T5', day: 2, title: '帮咪咪清点货架', reward: 10 }, + { id: 'T6', day: 3, title: '去花田采莓果 ×5', reward: 10 }, + { id: 'T7', day: 3, title: '湖畔野餐(走到野餐布触发过场)', reward: 15 }, + { id: 'T8', day: 4, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15 }, + { id: 'T9', day: 4, title: '给广场花坛浇水 ×3', reward: 10 }, + { id: 'T10', day: 4, title: '与任意三位村民问好', reward: 10 }, + { id: 'T11', day: 5, title: '帮圆圆揉面', reward: 10, optional: true }, + { id: 'T12', day: 5, title: '擦拭公告板', reward: 10, optional: true }, + { id: 'T13', day: 6, title: '在广场挂彩带 ×4', reward: 10 }, + { id: 'T14', day: 6, title: '保持微笑 60 秒', reward: 15 }, + { id: 'T15', day: 7, title: '参加第 413 届丰收庆典', reward: 0 }, +]; + +export const CLUES: Record = { + C1: '每一页都是同一个日期', + C2: '手写小字:「按疗程配比。甜度:最大。」', + C3: '所有邮戳是同一天', + C4: '署名闪过乱码「PT-0▓」', + C5: '同一商品重复排列,标签全部空白', + C6: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」', + C7: '井底回声:极轻的监护仪滴声(靠近并交互 5 秒)', + C8: '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」', + C9: '三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」', +}; + +export const C9_LINES: Record = { + 2: '湖水一直很平静', + 4: '面包一直是那个味道', + 6: '孩子,今天是第几个今天?', +}; + +export const CORRUPTIONS: Array<[string, string]> = [ + ['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'], + ['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'], +]; + +export const NPCS = { + afu: { name: '阿福', portrait: 2, frame: 16, offset: 0 }, + yuanyuan: { name: '圆圆', portrait: 3, frame: 19, offset: 15 }, + dabao: { name: '大宝', portrait: 4, frame: 22, offset: 0 }, + mimi: { name: '咪咪', portrait: 5, frame: 25, offset: -10 }, + doctor: { name: '芙医生', portrait: 6, frame: 28, offset: 999 }, + birds: { name: '双子鸟', portrait: 7, frame: 31, offset: 0 }, + turtle: { name: '龟爷爷', portrait: 8, frame: 34, offset: 999 }, +} as const; + +export type NpcId = keyof typeof NPCS; + +export const MAP_KEYS = [ + 'HouseScene', 'PlazaScene', 'BakeryScene', 'ShopScene', 'ClinicScene', + 'LakeScene', 'FieldScene', 'WellScene', 'MistScene', 'WhiteScene', +] as const; + +export const MAP_LABELS: Record<(typeof MAP_KEYS)[number], string> = { + HouseScene: '小满小屋', PlazaScene: '村广场', BakeryScene: '面包房', ShopScene: '杂货店', + ClinicScene: '诊所', LakeScene: '湖畔', FieldScene: '花田', WellScene: '古井空地', + MistScene: '雾墙边界', WhiteScene: '白色层', +}; + +export const NPC_CHAT: Record = { + afu: ['今天也要把幸福装满哦。', '微笑会让大家都安心。'], + yuanyuan: ['刚出炉的蜂蜜面包最甜啦。', '只要按配方做,每次都会一样美好。'], + dabao: ['每一封信都会准时送到。', '名册上写着的住户,一个也不会少。'], + mimi: ['货架永远整整齐齐,真让人开心。', '朋友需要什么,我这里都有哦。'], + doctor: ['村里有诊所,但你不需要去那里哦。'], + birds: ['啾、啾。旋律每天都一样好听。'], + turtle: ['湖水一直很平静。'], +}; diff --git a/GPT-5-6-Sol-High/src/game/scenes/BootScene.ts b/GPT-5-6-Sol-High/src/game/scenes/BootScene.ts new file mode 100644 index 0000000..eeef2a6 --- /dev/null +++ b/GPT-5-6-Sol-High/src/game/scenes/BootScene.ts @@ -0,0 +1,24 @@ +import Phaser from 'phaser'; +import { MAP_KEYS } from '../data'; + +export class BootScene extends Phaser.Scene { + constructor() { super('BootScene'); } + + preload(): void { + this.load.image('tiles', 'assets/tileset.png'); + this.load.spritesheet('characters', 'assets/characters.png', { frameWidth: 16, frameHeight: 24 }); + this.load.spritesheet('portraits', 'assets/portraits.png', { frameWidth: 48, frameHeight: 48 }); + MAP_KEYS.forEach((key) => this.load.tilemapTiledJSON(key, `maps/${key}.json`)); + } + + create(): void { + const directionFrames = [ + ['walk-down', 0], ['walk-up', 4], ['walk-left', 8], ['walk-right', 12], + ] as const; + directionFrames.forEach(([key, start]) => { + if (!this.anims.exists(key)) this.anims.create({ key, frames: this.anims.generateFrameNumbers('characters', { start, end: start + 3 }), frameRate: 8, repeat: -1 }); + }); + if (!this.anims.exists('birds-sing')) this.anims.create({ key: 'birds-sing', frames: [{ key: 'characters', frame: 42 }, { key: 'characters', frame: 43 }], frameRate: 3, repeat: -1 }); + this.scene.start('TitleScene'); + } +} diff --git a/GPT-5-6-Sol-High/src/game/scenes/EndingScene.ts b/GPT-5-6-Sol-High/src/game/scenes/EndingScene.ts new file mode 100644 index 0000000..3bc3c58 --- /dev/null +++ b/GPT-5-6-Sol-High/src/game/scenes/EndingScene.ts @@ -0,0 +1,115 @@ +import Phaser from 'phaser'; +import { audio } from '../AudioDirector'; +import { director } from '../CorruptionDirector'; +import { MAP_KEYS, type EndingId } from '../data'; + +export class EndingScene extends Phaser.Scene { + private ending: EndingId = 'E1'; + + constructor() { super('EndingScene'); } + + init(data: { ending?: EndingId }): void { this.ending = data.ending ?? director.ending() ?? 'E1'; } + + create(): void { + MAP_KEYS.forEach((key) => { if (this.scene.isActive(key)) this.scene.stop(key); }); + if (this.scene.isActive('UIScene')) this.scene.stop('UIScene'); + this.cameras.main.setBackgroundColor('#241f22'); + ({ E1: () => this.playE1(), E2: () => this.playE2(), E3: () => this.playE3(), E4: () => this.playE4() })[this.ending](); + } + + private title(text: string): Phaser.GameObjects.Text { + return this.add.text(160, 18, text, this.style(13, '#fff0bd')).setOrigin(0.5).setDepth(20); + } + + private caption(text: string, y = 132): Phaser.GameObjects.Text { + return this.add.text(160, y, text, { ...this.style(9, '#fff0bd'), align: 'center', wordWrap: { width: 280 } }).setOrigin(0.5).setDepth(20).setLineSpacing(4); + } + + private style(size: number, color: string): Phaser.Types.GameObjects.Text.TextStyle { + return { fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: `${size}px`, fontStyle: 'bold', color }; + } + + private playE1(): void { + this.cameras.main.setBackgroundColor('#86d55b'); + this.title('E1 糖衣'); + for (let i = 0; i < 12; i += 1) this.add.sprite(50 + (i % 6) * 44, 67 + Math.floor(i / 6) * 34, 'characters', i < 7 ? 16 + i * 3 : 37 + (i - 7)).setScale(1.35); + this.caption('从此,每一天都很幸福。\n\n每一天。', 137); + const saturation = this.cameras.main.postFX.addColorMatrix(); + this.tweens.addCounter({ from: 0, to: 0.5, duration: 5000, onUpdate: (tween) => saturation.reset().saturate(tween.getValue() ?? 0), onComplete: () => this.showReturn() }); + } + + private playE2(): void { + this.cameras.main.setBackgroundColor('#f5e8b1'); + this.title('E2 回收'); + const frozen = this.add.sprite(160, 90, 'characters', 0).setScale(3); + const negative = this.cameras.main.postFX.addColorMatrix().negative(); + this.time.delayedCall(2000, () => { + negative.reset(); + audio.beep('monitor'); + frozen.destroy(); + this.add.rectangle(160, 90, 190, 112, 0xffe8a4).setStrokeStyle(3, 0x5b362b); + this.add.sprite(160, 85, 'characters', 0).setScale(2); + this.add.text(92, 53, '日历:同一天', this.style(7, '#58372e')); + this.caption('疗程重新开始。', 145); + this.showReturn(2100); + }); + } + + private playE3(): void { + this.cameras.main.setBackgroundColor('#e7e8e5'); + const map = this.make.tilemap({ key: 'WhiteScene' }); + const tileset = map.addTilesetImage('honey-village', 'tiles'); + if (tileset) map.createLayer('ground', tileset, 0, 26)?.setAlpha(0.45); + this.title('E3 苏醒'); + const corridor = this.add.graphics(); + corridor.fillStyle(0xf8f8f4).fillRect(18, 38, 284, 96); + corridor.lineStyle(2, 0xb8bec0).strokeRect(18, 38, 284, 96); + corridor.fillStyle(0xc9cecf).fillRect(156, 38, 8, 96); + const player = this.add.sprite(42, 112, 'characters', 0); + this.tweens.add({ targets: player, x: 260, duration: 2800, onComplete: () => { + corridor.clear().fillStyle(0xfafafa).fillRect(18, 38, 284, 96); + player.destroy(); + const record = this.add.rectangle(160, 87, 240, 108, 0xffffff).setStrokeStyle(2, 0x899092); + const recordText = this.add.text(54, 43, + `蜜糖谷情绪疗养中心\n\n姓名:${director.snapshot.playerName}\n编号:PT-07\n疗程时长:${director.elapsedLabel()}`, + { ...this.style(9, '#394044'), lineSpacing: 5 }); + this.time.delayedCall(2800, () => { + record.destroy(); recordText.destroy(); + const face = this.add.image(119, 86, 'portraits', 0).setScale(1.5); + this.time.delayedCall(400, () => { + face.setFrame(1); + this.add.text(171, 65, '微笑消失了。', this.style(8, '#555b5d')); + this.add.text(171, 83, '窗外,一只灰色的、\n真的麻雀。', { ...this.style(8, '#555b5d'), lineSpacing: 4 }); + const bird = this.add.sprite(240, 113, 'characters', 42).setTint(0x92999a); + this.tweens.add({ targets: bird, y: 106, yoyo: true, repeat: -1, duration: 450 }); + this.showReturn(1800); + }); + }); + } }); + } + + private playE4(): void { + this.cameras.main.setBackgroundColor('#8fd35f'); + this.title('E4 静默'); + this.add.rectangle(160, 101, 270, 70, 0xcdbb8e).setStrokeStyle(2, 0x78644c); + for (let i = 0; i < 7; i += 1) { + const ribbon = this.add.rectangle(65 + i * 31, 58, 20, 3, i % 2 ? 0xf2a64c : 0xef7195); + this.tweens.add({ targets: ribbon, angle: 4, yoyo: true, repeat: -1, duration: 900 + i * 80 }); + } + this.caption('系统检测到疗程无效。', 145); + this.cameras.main.fadeOut(3500, 0, 0, 0, (_camera: Phaser.Cameras.Scene2D.Camera, progress: number) => { + if (progress === 1) { + this.children.removeAll(true); + this.cameras.main.resetFX().setBackgroundColor('#000000'); + this.showReturn(); + } + }); + } + + private showReturn(delay = 0): void { + this.time.delayedCall(delay, () => { + const button = this.add.text(160, 166, '回到标题', this.style(8, '#fff4ca')).setOrigin(0.5).setPadding(8, 4, 8, 4).setBackgroundColor('#5b362b').setInteractive({ useHandCursor: true }).setDepth(50); + button.on('pointerdown', () => this.scene.start('TitleScene')); + }); + } +} diff --git a/GPT-5-6-Sol-High/src/game/scenes/TitleScene.ts b/GPT-5-6-Sol-High/src/game/scenes/TitleScene.ts new file mode 100644 index 0000000..10b4f77 --- /dev/null +++ b/GPT-5-6-Sol-High/src/game/scenes/TitleScene.ts @@ -0,0 +1,45 @@ +import Phaser from 'phaser'; +import { audio } from '../AudioDirector'; +import { director } from '../CorruptionDirector'; + +export class TitleScene extends Phaser.Scene { + private form?: Phaser.GameObjects.DOMElement; + + constructor() { super('TitleScene'); } + + create(): void { + this.cameras.main.setBackgroundColor('#79cce4'); + const background = this.add.tileSprite(160, 90, 320, 180, 'tiles', 0).setTint(0x72c85a); + background.tileScaleX = 1; background.tileScaleY = 1; + this.add.rectangle(160, 38, 286, 58, 0xffe8a4, 0.92).setStrokeStyle(3, 0x5b362b); + this.add.text(160, 24, '崩坏童话:蜜糖村', { + fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '19px', fontStyle: 'bold', color: '#57342b', + }).setOrigin(0.5).setShadow(2, 2, '#e99a3b', 0, true, true); + this.add.text(160, 48, '每一天都很幸福。', { + fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '8px', fontStyle: 'bold', color: '#8d4f33', + }).setOrigin(0.5); + [0, 1, 2, 3, 4].forEach((index) => this.add.sprite(112 + index * 24, 76, 'characters', 37 + index).setScale(1)); + const html = `
`; + this.form = this.add.dom(160, 119).createFromHTML(html); + const node = this.form.node as HTMLFormElement; + node.addEventListener('submit', (event) => { + event.preventDefault(); + const input = node.querySelector('#player-name'); + this.startGame(input?.value ?? ''); + }); + this.input.keyboard?.once('keydown-ENTER', () => { + const input = node.querySelector('#player-name'); + this.startGame(input?.value ?? ''); + }); + } + + private startGame(name: string): void { + if (!this.form?.active) return; + audio.unlock(); + director.reset(name); + this.form.destroy(); + this.scene.start('HouseScene', { spawn: 'default' }); + this.scene.launch('UIScene'); + this.scene.bringToTop('UIScene'); + } +} diff --git a/GPT-5-6-Sol-High/src/game/scenes/UIScene.ts b/GPT-5-6-Sol-High/src/game/scenes/UIScene.ts new file mode 100644 index 0000000..8545284 --- /dev/null +++ b/GPT-5-6-Sol-High/src/game/scenes/UIScene.ts @@ -0,0 +1,229 @@ +import Phaser from 'phaser'; +import { audio } from '../AudioDirector'; +import { director } from '../CorruptionDirector'; +import { MAP_KEYS, MAP_LABELS, type EndingId } from '../data'; + +export type DialogueChoice = 'gentle' | 'hesitate' | 'negative'; + +interface DialogueOptions { + speaker: string; + text: string | string[]; + portrait?: number; + choices?: Array<{ id: DialogueChoice; label: string }>; + clean?: boolean; +} + +interface ActiveDialogue { + pages: string[]; + page: number; + choices?: Array<{ id: DialogueChoice; label: string }>; + selection: number; + resolve: (choice?: DialogueChoice) => void; +} + +export class UIScene extends Phaser.Scene { + private hud!: Phaser.GameObjects.Graphics; + private dayText!: Phaser.GameObjects.Text; + private questText!: Phaser.GameObjects.Text; + private promptText!: Phaser.GameObjects.Text; + private toastText!: Phaser.GameObjects.Text; + private dialoguePanel!: Phaser.GameObjects.Graphics; + private portrait!: Phaser.GameObjects.Image; + private speakerText!: Phaser.GameObjects.Text; + private bodyText!: Phaser.GameObjects.Text; + private choiceTexts: Phaser.GameObjects.Text[] = []; + private active?: ActiveDialogue; + private debugDom?: Phaser.GameObjects.DOMElement; + + constructor() { super({ key: 'UIScene', active: false }); } + + create(): void { + this.cameras.main.setRoundPixels(true); + this.hud = this.add.graphics().setDepth(100); + this.dayText = this.add.text(4, 28, '', this.pixelStyle(8, '#fff7d7')).setDepth(101).setShadow(1, 1, '#4b3128', 0, true, true); + this.questText = this.add.text(316, 4, '', this.pixelStyle(7, '#fff7d7')).setOrigin(1, 0).setAlign('right').setDepth(101).setShadow(1, 1, '#4b3128', 0, true, true); + this.promptText = this.add.text(160, 144, '', this.pixelStyle(8, '#fff6c7')).setOrigin(0.5).setPadding(4, 2, 4, 2).setBackgroundColor('#402a25').setDepth(110).setVisible(false); + this.toastText = this.add.text(160, 42, '', this.pixelStyle(8, '#3d2925')).setOrigin(0.5).setPadding(6, 3, 6, 3).setBackgroundColor('#fff0ae').setDepth(120).setVisible(false); + this.dialoguePanel = this.add.graphics().setDepth(130).setVisible(false); + this.portrait = this.add.image(29, 153, 'portraits', 0).setDisplaySize(40, 40).setDepth(131).setVisible(false); + this.speakerText = this.add.text(54, 132, '', this.pixelStyle(8, '#f0ad3f')).setDepth(131).setVisible(false); + this.bodyText = this.add.text(54, 144, '', this.pixelStyle(8, '#4b3128')).setWordWrapWidth(254).setLineSpacing(2).setDepth(131).setVisible(false); + this.createDebugPanel(); + this.bindKeys(); + this.redraw(); + director.on('change', this.redraw, this); + director.on('task-complete', this.taskComplete, this); + director.on('task-refused', () => this.toast('任务已拒绝。'), this); + director.on('clue', (_id: string, content: string) => this.toast(`发现线索:${content}`, 2600), this); + director.on('sunset', () => { audio.beep('bell'); this.toast('黄昏钟声响起了。回小屋睡觉。', 3000); }, this); + director.on('forced-cheer', () => this.toast('欢乐庆典开始了。边缘仍有一条路。', 3000), this); + this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { + director.off('change', this.redraw, this); + director.off('task-complete', this.taskComplete, this); + director.off('task-refused'); director.off('clue'); director.off('sunset'); director.off('forced-cheer'); + }); + } + + private pixelStyle(size: number, color: string): Phaser.Types.GameObjects.Text.TextStyle { + return { fontFamily: '"Courier New", "PingFang SC", "Microsoft YaHei", monospace', fontSize: `${size}px`, fontStyle: 'bold', color }; + } + + private bindKeys(): void { + const keyboard = this.input.keyboard; + if (!keyboard) return; + keyboard.on('keydown-E', () => this.advance()); + keyboard.on('keydown-ENTER', () => this.advance()); + keyboard.on('keydown-UP', () => this.moveChoice(-1)); + keyboard.on('keydown-DOWN', () => this.moveChoice(1)); + keyboard.on('keydown-ONE', () => this.choose(0)); + keyboard.on('keydown-TWO', () => this.choose(1)); + keyboard.on('keydown-THREE', () => this.choose(2)); + keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.BACKTICK).on('down', () => this.toggleDebug()); + } + + private redraw = (): void => { + this.drawSunflower(); + this.dayText.setText(`Day ${director.snapshot.day}`); + const pending = director.tasksForDay().find((task) => !director.snapshot.completed.has(task.id) && !director.snapshot.refused.has(task.id)); + let quest = director.snapshot.boardRead ? (pending ? `${pending.id} ${pending.title}` : (director.snapshot.sunset ? '回小屋睡觉' : '自由活动')) : '前往公告板'; + if (director.snapshot.day === 7 && director.snapshot.festivalStarted && director.ending() === 'E3') quest = '走向雾墙'; + this.questText.setText(quest).setWordWrapWidth(170); + }; + + private drawSunflower(): void { + const value = director.snapshot.happiness; + const lit = value <= 20 ? Math.round(value / 10) + : value <= 50 ? 3 + Math.floor((value - 21) * 4 / 30) + : value <= 80 ? 7 + Math.floor((value - 51) * 3 / 30) + : value < 100 ? 10 + Math.floor((value - 81) * 3 / 19) : 12; + this.hud.clear(); + this.hud.fillStyle(0x3d2925, 0.65).fillRoundedRect(3, 3, 29, 24, 3); + for (let i = 0; i < 12; i += 1) { + const angle = (Math.PI * 2 * i) / 12 - Math.PI / 2; + const x = 17.5 + Math.cos(angle) * 8; + const y = 14.5 + Math.sin(angle) * 8; + this.hud.fillStyle(0x4b3128, 1).fillCircle(x, y, 3.2); + this.hud.fillStyle(i < lit ? (value >= 81 ? 0xffff6b : 0xffd53e) : 0x8e7653, 1).fillCircle(x, y, 2.3); + } + this.hud.fillStyle(value === 100 ? 0x2a211d : 0x9a592d, 1).fillCircle(17.5, 14.5, 5); + if (value === 100) { + this.hud.fillStyle(0x070707, 1); + [[16, 13], [19, 14], [16, 17], [20, 17]].forEach(([x, y]) => this.hud.fillRect(x, y, 1, 1)); + } + } + + setPrompt(label?: string): void { + this.promptText.setVisible(Boolean(label)).setText(label ? `按 E ${label}` : ''); + } + + toast(message: string, duration = 1800): void { + this.toastText.setText(message).setVisible(true).setAlpha(1); + this.tweens.killTweensOf(this.toastText); + this.tweens.add({ targets: this.toastText, alpha: 0, delay: duration, duration: 300, onComplete: () => this.toastText.setVisible(false) }); + } + + showDialogue(options: DialogueOptions): Promise { + if (this.active) return Promise.resolve(undefined); + director.setDialogueOpen(true); + const pages = (Array.isArray(options.text) ? options.text : [options.text]).map((text) => options.clean ? text : director.corruptText(text, options.speaker)); + this.dialoguePanel.setVisible(true).clear().fillStyle(0x3e2924, 0.98).fillRect(2, 126, 316, 52).fillStyle(0xffefbd, 1).fillRect(5, 129, 310, 46); + this.portrait.setFrame(options.portrait ?? 0).setVisible(true); + this.speakerText.setText(options.speaker).setVisible(true); + this.bodyText.setText(pages[0]).setVisible(true); + audio.beep('dialogue'); + return new Promise((resolve) => { + this.active = { pages, page: 0, choices: options.choices, selection: 0, resolve }; + }); + } + + private advance(): void { + if (!this.active) return; + if (this.active.page < this.active.pages.length - 1) { + this.active.page += 1; + this.bodyText.setText(this.active.pages[this.active.page]); + audio.beep('dialogue'); + return; + } + if (this.active.choices?.length) { + if (this.choiceTexts.length === 0) this.renderChoices(); + else this.choose(this.active.selection); + return; + } + this.closeDialogue(); + } + + private renderChoices(): void { + this.bodyText.setVisible(false); + this.active?.choices?.forEach((choice, index) => { + const text = this.add.text(58, 139 + index * 11, `${index + 1}. ${choice.label}`, this.pixelStyle(7, '#4b3128')).setDepth(132); + this.choiceTexts.push(text); + }); + this.updateChoiceStyles(); + } + + private moveChoice(delta: number): void { + if (!this.active?.choices || this.choiceTexts.length === 0) return; + this.active.selection = Phaser.Math.Wrap(this.active.selection + delta, 0, this.active.choices.length); + this.updateChoiceStyles(); + } + + private updateChoiceStyles(): void { + this.choiceTexts.forEach((text, index) => text.setColor(index === this.active?.selection ? '#c75f38' : '#4b3128')); + } + + private choose(index: number): void { + const choice = this.active?.choices?.[index]; + if (!choice) return; + this.closeDialogue(choice.id); + } + + private closeDialogue(choice?: DialogueChoice): void { + const resolve = this.active?.resolve; + this.active = undefined; + this.dialoguePanel.setVisible(false); + this.portrait.setVisible(false); + this.speakerText.setVisible(false); + this.bodyText.setVisible(false); + this.choiceTexts.forEach((text) => text.destroy()); + this.choiceTexts = []; + director.setDialogueOpen(false); + resolve?.(choice); + } + + private taskComplete = (task: { id: string; title: string }): void => { + audio.beep('task'); + this.toast(`${task.id} 完成:${task.title}`, 2400); + }; + + private createDebugPanel(): void { + const sceneOptions = MAP_KEYS.map((key) => ``).join(''); + const html = `
+

Debug Panel

+
+
+
+
+
+
${(['E1', 'E2', 'E3', 'E4'] as EndingId[]).map((id) => ``).join('')}
+
`; + this.debugDom = this.add.dom(190, 90).createFromHTML(html).setScale(0.7).setDepth(500).setVisible(false); + const node = this.debugDom.node as HTMLElement; + node.addEventListener('click', (event) => { + const target = event.target as HTMLElement; + const action = target.dataset.action; + const ending = target.dataset.ending as EndingId | undefined; + const input = (name: string) => node.querySelector(`[data-field="${name}"]`); + if (action === 'happiness') director.setHappiness(Number(input('happiness')?.value)); + if (action === 'awareness') director.setAwareness(Number(input('awareness')?.value)); + if (action === 'day') director.setDay(Number(input('day')?.value)); + if (action === 'teleport') director.emit('debug-teleport', input('scene')?.value); + if (action === 'clues') director.discoverAllClues(); + if (ending) director.emit('force-ending', ending); + }); + } + + private toggleDebug(): void { + if (!this.debugDom) return; + this.debugDom.setVisible(!this.debugDom.visible); + } +} diff --git a/GPT-5-6-Sol-High/src/game/scenes/WorldScene.ts b/GPT-5-6-Sol-High/src/game/scenes/WorldScene.ts new file mode 100644 index 0000000..85a10fa --- /dev/null +++ b/GPT-5-6-Sol-High/src/game/scenes/WorldScene.ts @@ -0,0 +1,543 @@ +import Phaser from 'phaser'; +import { audio } from '../AudioDirector'; +import { director } from '../CorruptionDirector'; +import { ANNOUNCEMENTS, C9_LINES, NPCS, type EndingId, type NpcId } from '../data'; +import type { DialogueChoice, UIScene } from './UIScene'; + +interface SceneData { spawn?: string; dream?: boolean } +interface MapProperty { name: string; value: string | number | boolean } +interface MapObject extends Phaser.Types.Tilemaps.TiledObject { type: string; properties?: MapProperty[] } + +const NPC_IDS = new Set(Object.keys(NPCS)); +const STAGE_SATURATION = { S0: 0.9, S1: 1, S2: 1.15, S3: 1.3, S4: 1.4 } as const; + +export class WorldScene extends Phaser.Scene { + private spawn = 'default'; + private dream = false; + private player!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody; + private cursors!: Phaser.Types.Input.Keyboard.CursorKeys; + private wasd!: Record<'up' | 'down' | 'left' | 'right', Phaser.Input.Keyboard.Key>; + private objects: MapObject[] = []; + private npcs = new Map(); + private extras: Phaser.GameObjects.Sprite[] = []; + private nearest?: MapObject; + private interacting = false; + private idleSince = 0; + private movedAfterIdle = true; + private rainGraphics?: Phaser.GameObjects.Graphics; + private rainSince?: number; + private t14Start?: number; + private cheerTimer?: Phaser.Time.TimerEvent; + private cheerSprites: Phaser.GameObjects.Sprite[] = []; + private reflection?: Phaser.GameObjects.Sprite; + private reflectionHistory: Array<{ x: number; y: number; frame: number }> = []; + private colorFx?: Phaser.FX.ColorMatrix; + private vignetteFx?: Phaser.FX.Vignette; + + constructor(key: string) { super(key); } + + init(data: SceneData): void { + this.spawn = data.spawn ?? 'default'; + this.dream = data.dream ?? false; + } + + create(): void { + const map = this.make.tilemap({ key: this.scene.key }); + const tileset = map.addTilesetImage('honey-village', 'tiles'); + if (!tileset) throw new Error(`Tileset missing in ${this.scene.key}`); + map.createLayer('ground', tileset, 0, 0)?.setDepth(0); + this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels); + this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels).setRoundPixels(true); + this.objects = (map.getObjectLayer('objects')?.objects ?? []) as MapObject[]; + this.createCollision(map); + this.createNpcs(); + const spawn = this.objects.find((object) => object.type === 'spawn' && this.prop(object, 'id') === this.spawn) + ?? this.objects.find((object) => object.type === 'spawn'); + this.player = this.physics.add.sprite(spawn?.x ?? 32, spawn?.y ?? 32, 'characters', 0).setDepth(20); + this.player.body.setSize(10, 8).setOffset(3, 15); + this.player.setCollideWorldBounds(true); + this.cameras.main.startFollow(this.player, true, 0.12, 0.12); + this.createInputs(); + this.createReflection(); + this.setupPostFx(); + this.bindDirectorEvents(); + this.applyCorruption(); + this.idleSince = performance.now(); + if (this.scene.key === 'WhiteScene' && this.dream) this.playDreamFlash(); + } + + private createCollision(map: Phaser.Tilemaps.Tilemap): void { + const group = this.physics.add.staticGroup(); + const obstacles = (map.getObjectLayer('obstacles')?.objects ?? []) as MapObject[]; + obstacles.forEach((object) => { + if (!this.prop(object, 'collides')) return; + const block = this.add.rectangle((object.x ?? 0) + (object.width ?? 0) / 2, (object.y ?? 0) + (object.height ?? 0) / 2, object.width ?? 0, object.height ?? 0, 0, 0); + this.physics.add.existing(block, true); group.add(block); + }); + this.events.once(Phaser.Scenes.Events.CREATE, () => this.physics.add.collider(this.player, group)); + this.time.delayedCall(0, () => this.physics.add.collider(this.player, group)); + } + + private createInputs(): void { + const keyboard = this.input.keyboard; + if (!keyboard) throw new Error('Keyboard input unavailable'); + this.cursors = keyboard.createCursorKeys(); + const keys = keyboard.addKeys('W,S,A,D') as Record<'W' | 'S' | 'A' | 'D', Phaser.Input.Keyboard.Key>; + this.wasd = { up: keys.W, down: keys.S, left: keys.A, right: keys.D }; + keyboard.on('keydown-E', () => void this.interact()); + } + + private createNpcs(): void { + this.objects.filter((object) => object.type === 'npc').forEach((object, index) => { + const id = String(this.prop(object, 'id')); + if (id === 'birds') { + const left = this.add.sprite(object.x ?? 0, object.y ?? 0, 'characters', 42).setDepth(18).play('birds-sing'); + const right = this.add.sprite((object.x ?? 0) + 13, object.y ?? 0, 'characters', 44).setDepth(18).play('birds-sing'); + this.npcs.set(id, left); this.npcs.set('birds-right', right); + return; + } + const info = NPCS[id as NpcId]; + const frame = info?.frame ?? 37 + (index % 5); + const sprite = this.add.sprite(object.x ?? 0, object.y ?? 0, 'characters', frame).setDepth(18); + if (id.startsWith('extra')) { + this.extras.push(sprite); + this.tweens.add({ targets: sprite, x: sprite.x + (index % 2 ? 22 : -22), duration: 1800 + index * 140, yoyo: true, repeat: -1, ease: 'Linear' }); + } + this.npcs.set(id, sprite); + }); + } + + private createReflection(): void { + if (this.scene.key !== 'LakeScene' || director.snapshot.day < 2) return; + this.reflection = this.add.sprite(this.player?.x ?? 0, 88, 'characters', 0).setAlpha(0.35).setTint(0x89dbe8).setFlipY(true).setDepth(5); + } + + private setupPostFx(): void { + if (this.game.renderer.type !== Phaser.WEBGL) return; + this.colorFx = this.cameras.main.postFX.addColorMatrix(); + this.vignetteFx = this.cameras.main.postFX.addVignette(0.5, 0.5, 0, 0.3); + } + + private bindDirectorEvents(): void { + director.on('change', this.applyCorruption, this); + director.on('negative-flash', this.negativeFlash, this); + director.on('first-full', this.firstFull, this); + director.on('sad-behavior', this.turnNearestNpc, this); + director.on('forced-cheer', this.startForcedCheer, this); + director.on('debug-teleport', this.debugTeleport, this); + director.on('force-ending', this.startEnding, this); + this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { + director.off('change', this.applyCorruption, this); + director.off('negative-flash', this.negativeFlash, this); + director.off('first-full', this.firstFull, this); + director.off('sad-behavior', this.turnNearestNpc, this); + director.off('forced-cheer', this.startForcedCheer, this); + director.off('debug-teleport', this.debugTeleport, this); + director.off('force-ending', this.startEnding, this); + this.ui().setPrompt(); + }); + } + + update(_time: number, delta: number): void { + if (!this.player?.body) return; + director.tick(); + this.updateMovement(); + this.updateNearest(); + this.updateIdle(); + this.updateT14(); + this.updateRain(delta); + this.updateReflection(); + this.updateNpcFacing(); + } + + private updateMovement(): void { + const locked = this.interacting || director.snapshot.dialogueOpen || director.snapshot.t14Active || (this.scene.key === 'WhiteScene' && this.dream); + const left = this.cursors.left.isDown || this.wasd.left.isDown; + const right = this.cursors.right.isDown || this.wasd.right.isDown; + const up = this.cursors.up.isDown || this.wasd.up.isDown; + const down = this.cursors.down.isDown || this.wasd.down.isDown; + let vx = locked ? 0 : Number(right) - Number(left); + let vy = locked ? 0 : Number(down) - Number(up); + if (vx && vy) { vx *= Math.SQRT1_2; vy *= Math.SQRT1_2; } + this.player.setVelocity(vx * 96, vy * 96); + if (vx < 0) this.player.play('walk-left', true); + else if (vx > 0) this.player.play('walk-right', true); + else if (vy < 0) this.player.play('walk-up', true); + else if (vy > 0) this.player.play('walk-down', true); + else { this.player.stop(); this.player.setFrame(this.idleFrame()); } + if (vx || vy) { this.idleSince = performance.now(); this.movedAfterIdle = true; } + } + + private idleFrame(): number { + const key = this.player.anims.currentAnim?.key; + return key === 'walk-up' ? 4 : key === 'walk-left' ? 8 : key === 'walk-right' ? 12 : 0; + } + + private updateNearest(): void { + if (director.snapshot.dialogueOpen) { this.ui().setPrompt(); return; } + let distance = Number.POSITIVE_INFINITY; + let nearest: MapObject | undefined; + this.objects.filter((object) => object.type === 'door' || object.type === 'npc' || object.type === 'interact').forEach((object) => { + const current = Phaser.Math.Distance.Between(this.player.x, this.player.y, object.x ?? 0, object.y ?? 0); + if (current <= 24 && current < distance) { distance = current; nearest = object; } + }); + this.nearest = nearest; + this.ui().setPrompt(nearest ? String(this.prop(nearest, 'label') || (nearest.type === 'door' ? '离开' : '交互')) : undefined); + } + + private updateIdle(): void { + if (this.player.body.speed > 0 || director.snapshot.dialogueOpen || this.nearest || director.snapshot.t14Active) return; + if (this.movedAfterIdle && performance.now() - this.idleSince >= 10_000) { + if (director.sadBehavior('idle')) this.ui().toast('你发了一会儿呆。'); + this.movedAfterIdle = false; + } + } + + private updateT14(): void { + if (!this.t14Start) return; + const seconds = Math.min(60, (performance.now() - this.t14Start) / 1000); + this.cameras.main.setZoom(1 + 0.28 * seconds / 60); + audio.fadeForSmile(seconds); + this.npcs.forEach((sprite, id) => { if (!id.startsWith('birds')) sprite.setFlipX(sprite.x > this.player.x); }); + if (seconds >= 60) { + this.t14Start = undefined; + director.setT14(false); + audio.restore(); + this.cameras.main.setZoom(1); + director.completeTask('T14'); + this.ui().toast('你保持了整整 60 秒的微笑。', 2500); + } + } + + private updateRain(_delta: number): void { + const raining = director.snapshot.day === 6 && director.snapshot.sunset; + if (!raining) { this.rainGraphics?.clear(); this.rainSince = undefined; return; } + this.rainGraphics ??= this.add.graphics().setScrollFactor(0).setDepth(50); + this.rainGraphics.clear().lineStyle(1, 0xb8e7f2, 0.75); + for (let i = 0; i < 45; i += 1) { + const x = (i * 43 + Math.floor(performance.now() / 12)) % 340 - 10; + const y = (i * 29 + Math.floor(performance.now() / 7)) % 190 - 10; + this.rainGraphics.lineBetween(x, y, x - 3, y + 8); + } + if (this.player.body.speed > 0) { this.rainSince = undefined; return; } + this.rainSince ??= performance.now(); + if (performance.now() - this.rainSince >= 10_000 && director.sadBehavior('rain')) this.ui().toast('你在雨里站了十秒。'); + } + + private updateReflection(): void { + if (!this.reflection) return; + this.reflectionHistory.push({ x: this.player.x, y: this.player.y, frame: Number(this.player.frame.name) }); + const delayed = this.reflectionHistory.length > 1 ? this.reflectionHistory.shift() : undefined; + if (!delayed) return; + this.reflection.setPosition(delayed.x, 106 + (106 - delayed.y) * 0.15).setFrame(delayed.frame); + } + + private updateNpcFacing(): void { + if (director.stage === 'S3' || director.stage === 'S4') this.npcs.forEach((sprite, id) => { if (!id.startsWith('birds')) sprite.setFlipX(sprite.x > this.player.x); }); + if (director.stage === 'S4' && this.extras.length) this.tweens.killTweensOf(this.extras[0]); + if (director.stage === 'S4' && Math.random() < 0.6 / 3600) { + const ids = [...this.npcs.keys()].filter((id): id is NpcId => id in NPCS && id !== 'doctor' && id !== 'turtle' && id !== 'birds'); + const id = Phaser.Utils.Array.GetRandom(ids); + const sprite = id ? this.npcs.get(id) : undefined; + if (id && sprite) { + sprite.setFrame(NPCS[id].frame + 2); + this.time.delayedCall(100, () => sprite.active && sprite.setFrame(NPCS[id].frame + 1)); + } + } + } + + private async interact(): Promise { + if (!this.nearest || this.interacting || director.snapshot.dialogueOpen) return; + this.interacting = true; + try { + if (this.nearest.type === 'door') this.useDoor(this.nearest); + else if (this.nearest.type === 'npc') await this.interactNpc(String(this.prop(this.nearest, 'id'))); + else await this.interactPoint(String(this.prop(this.nearest, 'id'))); + } finally { this.interacting = false; } + } + + private useDoor(object: MapObject): void { + if (director.snapshot.cheerActive) director.escapeCheer(); + const target = String(this.prop(object, 'target')); + const spawn = String(this.prop(object, 'spawn') || 'default'); + this.scene.start(target, { spawn }); + } + + private async interactNpc(id: string): Promise { + if (id.startsWith('extra')) { await this.say('村民', '今天也是完美的一天。', 0); return; } + if (!NPC_IDS.has(id)) return; + const npcId = id as NpcId; + director.recordDialogue(id); + if (await this.handleNpcTask(npcId)) return; + if (npcId === 'turtle' && C9_LINES[director.snapshot.day]) { + await this.say('龟爷爷', C9_LINES[director.snapshot.day], NPCS.turtle.portrait); + director.setFlag(`c9-${director.snapshot.day}`, true); + if (director.snapshot.day === 6) director.discoverClue('C9'); + this.checkGreeting(id); + return; + } + const lines: Record = { + afu: '今天也要把幸福装满哦。', yuanyuan: '刚出炉的蜂蜜面包最甜啦。', dabao: '每一封信都会准时送到。', + mimi: '货架永远整整齐齐,真让人开心。', doctor: '村里有诊所,但你不需要去那里哦。', + birds: '啾、啾。旋律每天都一样好听。', turtle: '湖水一直很平静。', + }; + const replayGreeting = director.snapshot.day === 4 && director.isTaskActive('T10'); + const choice = await this.say(NPCS[npcId].name, lines[npcId], NPCS[npcId].portrait, true, undefined, replayGreeting); + this.applyChoice(choice, id); + if (npcId === 'birds') director.smallInteraction('feed'); + if (npcId === 'afu') director.smallInteraction('pet'); + this.checkGreeting(id); + } + + private async handleNpcTask(id: NpcId): Promise { + const breadTask = director.snapshot.day === 4 ? 'T8' : 'T1'; + const greetTask = director.snapshot.day === 4 ? 'T10' : 'T3'; + if (id === 'yuanyuan' && director.isTaskActive(breadTask) && !director.flag(`${breadTask}-picked`)) { + director.setFlag(`${breadTask}-picked`); + await this.say('圆圆', '蜂蜜面包刚刚出炉。请趁热送给大家吧。', NPCS.yuanyuan.portrait, false, undefined, director.snapshot.day === 4); + this.checkGreeting(id, greetTask); return true; + } + if (director.isTaskActive(breadTask) && director.flag(`${breadTask}-picked`) && ['dabao', 'mimi', 'turtle'].includes(id) && !director.flag(`${breadTask}-${id}`)) { + director.setFlag(`${breadTask}-${id}`); + await this.say(NPCS[id].name, '谢谢你。今天的面包也和昨天一样甜。', NPCS[id].portrait, false, undefined, director.snapshot.day === 4); + if (director.increment(`${breadTask}-delivered`, 3) === 3) director.completeTask(breadTask); + this.checkGreeting(id, greetTask); return true; + } + if (id === 'dabao' && director.isTaskActive('T4') && !director.flag('T4-picked')) { + director.setFlag('T4-picked'); + await this.say('大宝', '三封信。圆圆、咪咪、芙医生。邮戳都已经盖好了。', NPCS.dabao.portrait); + return true; + } + if (director.isTaskActive('T4') && director.flag('T4-picked') && ['yuanyuan', 'mimi', 'doctor'].includes(id) && !director.flag(`T4-${id}`)) { + director.setFlag(`T4-${id}`); + await this.say(NPCS[id].name, '信收到了。谢谢你。', NPCS[id].portrait); + const count = director.increment('T4-delivered', 3); + if (count === 3) { + this.ui().toast('署名:PT-0▓', 200); + await this.wait(200); + director.discoverClue('C4'); + director.completeTask('T4'); + } + return true; + } + if (id === 'yuanyuan' && director.isTaskActive('T11')) { + const choice = await this.say('圆圆', '来帮我揉面吧。只要一直微笑,面团就会很柔软。', NPCS.yuanyuan.portrait, false, [ + { id: 'gentle', label: '好,我来帮忙。' }, { id: 'hesitate', label: '让我想一想。' }, { id: 'negative', label: '我不想做。' }, + ]); + if (choice === 'negative') director.refuseTask('T11'); + else { if (choice === 'hesitate') director.adjustAwareness(2); director.completeTask('T11'); } + return true; + } + return false; + } + + private checkGreeting(id: string, forcedTask?: string): void { + const task = forcedTask ?? (director.snapshot.day === 4 ? 'T10' : 'T3'); + if (!director.isTaskActive(task) || director.flag(`${task}-greet-${id}`)) return; + director.setFlag(`${task}-greet-${id}`); + if (director.increment(`${task}-greetings`, 3) === 3) director.completeTask(task); + } + + private async interactPoint(id: string): Promise { + if (id === 'board') { await this.interactBoard(); return; } + if (id === 'bed') { await this.interactBed(); return; } + if (id === 'calendar') { + if (director.snapshot.day < 2) await this.say('日历', '今天的日期旁画着一朵向日葵。'); + else { await this.say('日历', '每一页都是同一个日期'); director.discoverClue('C1'); } + return; + } + if (id === 'recipe') { + if (director.snapshot.day < 3) await this.say('配方纸', '纸张背面粘着糖霜。'); + else { await this.say('配方纸', '手写小字:「按疗程配比。甜度:最大。」'); director.discoverClue('C2'); } + return; + } + if (id === 'mail') { + if (director.snapshot.day < 2) await this.say('散落信件', '几封还没有投递的信。'); + else { await this.say('散落信件', '所有邮戳是同一天'); director.discoverClue('C3'); } + return; + } + if (id === 'shelf') { + await this.say('货架', '同一商品重复排列,标签全部空白'); + if (director.snapshot.day >= 2) director.discoverClue('C5'); + if (director.isTaskActive('T5')) director.completeTask('T5'); + return; + } + if (id.startsWith('flower')) { + const task = director.snapshot.day === 4 ? 'T9' : 'T2'; + if (director.isTaskActive(task)) this.handleRepeatedPoint(id, task, 'flowers', 3, '你给花坛浇了水。'); + else if (director.smallInteraction('water')) this.ui().toast('你给花浇了一点水。'); + else this.ui().toast('今天的花已经喝饱了。'); + return; + } + if (id.startsWith('berry')) { this.handleRepeatedPoint(id, 'T6', 'berries', 5, '你采下一把莓果。'); return; } + if (id.startsWith('ribbon')) { this.handleRepeatedPoint(id, 'T13', 'ribbons', 4, '彩带被挂得笔直。'); return; } + if (id === 'picnic' && director.isTaskActive('T7')) { + await this.say('湖畔', ['野餐布上的餐具摆得一丝不差。', '湖中的倒影晚了一帧才坐下。']); + director.completeTask('T7'); return; + } + if (id === 'well') { await this.interactWell(); return; } + if (id === 'form') { + if (director.snapshot.day < 5) await this.say('芙医生', '桌上的表格还没有填写,请别碰哦。', NPCS.doctor.portrait); + else { await this.say('表格', '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」'); director.discoverClue('C8'); } + return; + } + if (id === 'mist') { await this.interactMist(); return; } + if (id === 'festival') { await this.interactFestival(); return; } + if (id === 'oven') await this.say('烤炉', '炉火永远保持同样的温度。'); + if (id === 'lake') await this.say('湖面', '倒影跟着你。慢了一帧。'); + } + + private async interactBoard(): Promise { + const day = director.snapshot.day; + if (!director.snapshot.boardRead) { + await this.say('阿福', ANNOUNCEMENTS[day], NPCS.afu.portrait, false, undefined, day === 4); + director.acceptDay(); + return; + } + if (day === 5 && director.isTaskActive('T12')) { + const choice = await this.say('公告板', '木板需要擦得干干净净。', undefined, false, [ + { id: 'gentle', label: '认真擦拭。' }, { id: 'hesitate', label: '停下来看看旧字。' }, { id: 'negative', label: '拒绝擦拭。' }, + ]); + if (choice === 'negative') director.refuseTask('T12'); + else { + if (choice === 'hesitate') director.adjustAwareness(2); + await this.say('旧字迹', '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」'); + director.discoverClue('C6'); director.completeTask('T12'); + } + return; + } + const tasks = director.tasksForDay().map((task) => `${task.id} ${task.title}`).join('\n'); + await this.say('今日幸福任务', tasks || '今天没有新的任务。'); + } + + private async interactBed(): Promise { + if (!director.snapshot.sunset) { await this.say('床', '现在还不到睡觉时间。'); return; } + const oldDay = director.snapshot.day; + await this.say('梦境', oldDay >= 3 ? ['你闭上眼睛。', '白色墙壁闪过一秒。'] : '你闭上眼睛。甜味沉进梦里。'); + this.cameras.main.fadeOut(450, 255, 245, 220); + this.time.delayedCall(500, () => { + director.advanceDay(); + if (oldDay >= 3) this.scene.start('WhiteScene', { dream: true }); + else this.scene.restart({ spawn: 'default' }); + }); + } + + private async interactWell(): Promise { + if (director.snapshot.day < 5) { await this.say('阿福', '井边有点滑。等自由日再来吧。', NPCS.afu.portrait); return; } + await this.say('古井', '你向井底看去。五秒里,只有自己的呼吸。'); + this.ui().toast('注视古井……', 5000); + await this.wait(5000); + if (!director.flag('well-gaze')) { director.setFlag('well-gaze'); director.adjustAwareness(3); } + audio.beep('monitor'); director.discoverClue('C7'); + } + + private async interactMist(): Promise { + if (director.snapshot.day < 5) { await this.say('雾墙', '一位村民温柔地摇头。现在还不能靠近。'); return; } + if (director.snapshot.day === 7 && director.snapshot.festivalStarted && director.ending() === 'E3') { this.startEnding('E3'); return; } + await this.say('雾墙', '雾在五秒里维持着完全相同的形状。'); + this.ui().toast('注视雾墙……', 5000); + await this.wait(5000); + if (!director.flag('mist-gaze')) { director.setFlag('mist-gaze'); director.adjustAwareness(3); } + } + + private async interactFestival(): Promise { + if (director.isTaskActive('T14')) { + if (!director.snapshot.completed.has('T13')) { await this.say('庆典场地', '彩带还没有挂完。'); return; } + await this.say('阿福', '请站在这里,保持微笑 60 秒。', NPCS.afu.portrait); + director.setT14(true); this.t14Start = performance.now(); this.ui().toast('保持微笑。', 2000); return; + } + if (!director.isTaskActive('T15')) { await this.say('庆典场地', '这里正等待第 413 届丰收庆典。'); return; } + director.completeTask('T15'); director.setFestivalStarted(); + const ending = director.ending(); + if (ending === 'E3') { await this.say('雾墙', '庆典的歌声里,雾墙裂开了一条白色的缝。'); this.ui().toast('走向雾墙。', 3000); } + else if (ending) this.startEnding(ending); + else await this.say('庆典场地', '庆典仍在等待幸福抵达满格。'); + } + + private handleRepeatedPoint(id: string, task: string, counter: string, total: number, message: string): void { + if (!director.isTaskActive(task)) { this.ui().toast('这里暂时没有任务。'); return; } + if (director.flag(`${task}-${id}`)) { this.ui().toast('这里已经完成了。'); return; } + director.setFlag(`${task}-${id}`); + this.ui().toast(message); + if (director.increment(`${task}-${counter}`, total) === total) director.completeTask(task); + } + + private async say(speaker: string, text: string | string[], portrait?: number, defaultChoices = false, choices?: Array<{ id: DialogueChoice; label: string }>, clean = false): Promise { + const options = choices ?? (defaultChoices ? [ + { id: 'gentle' as const, label: '温柔地回应。' }, { id: 'hesitate' as const, label: '迟疑片刻。' }, { id: 'negative' as const, label: '否定这句话。' }, + ] : undefined); + return this.ui().showDialogue({ speaker, text, portrait, choices: options, clean }); + } + + private applyChoice(choice: DialogueChoice | undefined, npc: string): void { + if (choice === 'gentle') director.adjustHappiness(2); + if (choice === 'hesitate') director.adjustAwareness(2); + if (choice === 'negative') director.sadBehavior(`negative-${npc}-${director.increment(`negative-${npc}`)}`); + } + + private applyCorruption = (): void => { + const saturation = STAGE_SATURATION[director.stage]; + this.colorFx?.reset().saturate(saturation - 1); + if (this.vignetteFx) { + this.vignetteFx.strength = director.stage === 'S4' ? 0.55 : director.stage === 'S3' ? 0.28 : 0; + this.vignetteFx.radius = 0.72; + } + this.npcs.forEach((sprite, id) => { + if (id.startsWith('birds')) { + sprite.anims.timeScale = director.stage === 'S2' ? 0.8 : director.stage === 'S3' ? 0.55 : 1; + } + if (!(id in NPCS)) return; + const npcId = id as NpcId; + const expression = director.npcExpression(npcId); + const offset = expression === 'flat' ? 1 : expression === 'off' ? 2 : 0; + if (npcId !== 'birds') sprite.setFrame(NPCS[npcId].frame + offset); + }); + }; + + private negativeFlash = (): void => { + this.colorFx?.negative(); + this.time.delayedCall(17, () => this.applyCorruption()); + }; + + private firstFull = (): void => { + this.cameras.main.flash(1000, 255, 250, 210); + this.ui().toast('幸福溢出了。', 1800); + }; + + private turnNearestNpc = (): void => { + const nearest = [...this.npcs.values()].sort((a, b) => Phaser.Math.Distance.Between(a.x, a.y, this.player.x, this.player.y) - Phaser.Math.Distance.Between(b.x, b.y, this.player.x, this.player.y))[0]; + if (!nearest) return; + nearest.setFlipX(nearest.x > this.player.x); + nearest.setTint(0xfff1c2); + this.time.delayedCall(1000, () => nearest.active && nearest.clearTint()); + }; + + private startForcedCheer = (): void => { + this.cameras.main.flash(500, 255, 224, 120); + this.cheerSprites.forEach((sprite) => sprite.destroy()); + this.cheerSprites = []; + for (let i = 0; i < 6; i += 1) this.cheerSprites.push(this.add.sprite(this.player.x + Math.cos(i) * 38, this.player.y + Math.sin(i) * 28, 'characters', 37 + (i % 5)).setDepth(19)); + this.cheerTimer?.remove(); + this.cheerTimer = this.time.delayedCall(15_000, () => { + this.cheerSprites.forEach((sprite) => sprite.destroy()); this.cheerSprites = []; + director.finishCheer(); + }); + }; + + private debugTeleport = (key: string): void => { if (key && key !== this.scene.key) this.scene.start(key, { spawn: 'default' }); }; + private startEnding = (ending: EndingId): void => { this.scene.start('EndingScene', { ending }); }; + + private playDreamFlash(): void { + this.player.setVisible(false); + this.cameras.main.flash(250, 255, 255, 255); + this.add.text(160, 82, '白色病房。', { fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '9px', color: '#626a6c' }).setOrigin(0.5).setScrollFactor(0).setDepth(60); + this.time.delayedCall(1000, () => this.scene.start('HouseScene', { spawn: 'default' })); + } + + private prop(object: MapObject, name: string): string | number | boolean | undefined { + return object.properties?.find((property) => property.name === name)?.value; + } + + private wait(ms: number): Promise { return new Promise((resolve) => this.time.delayedCall(ms, resolve)); } + private ui(): UIScene { return this.scene.get('UIScene') as UIScene; } +} diff --git a/GPT-5-6-Sol-High/src/main.ts b/GPT-5-6-Sol-High/src/main.ts new file mode 100644 index 0000000..04b8b07 --- /dev/null +++ b/GPT-5-6-Sol-High/src/main.ts @@ -0,0 +1,47 @@ +import Phaser from 'phaser'; +import './style.css'; +import { MAP_KEYS } from './game/data'; +import { BootScene } from './game/scenes/BootScene'; +import { EndingScene } from './game/scenes/EndingScene'; +import { TitleScene } from './game/scenes/TitleScene'; +import { UIScene } from './game/scenes/UIScene'; +import { WorldScene } from './game/scenes/WorldScene'; +import { director } from './game/CorruptionDirector'; + +declare global { + interface Window { __HONEY_GAME__: Phaser.Game } +} + +const game = new Phaser.Game({ + type: Phaser.WEBGL, + parent: 'game', + backgroundColor: '#79cce4', + pixelArt: true, + roundPixels: true, + width: 320, + height: 180, + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + autoRound: true, + zoom: Phaser.Scale.MAX_ZOOM, + width: 320, + height: 180, + }, + physics: { + default: 'arcade', + arcade: { debug: false }, + }, + dom: { createContainer: true }, + render: { antialias: false, pixelArt: true, roundPixels: true }, + scene: [ + BootScene, + TitleScene, + ...MAP_KEYS.map((key) => new WorldScene(key)), + UIScene, + EndingScene, + ], +}); + +game.registry.set('director', director); +window.__HONEY_GAME__ = game; diff --git a/GPT-5-6-Sol-High/src/style.css b/GPT-5-6-Sol-High/src/style.css new file mode 100644 index 0000000..179569b --- /dev/null +++ b/GPT-5-6-Sol-High/src/style.css @@ -0,0 +1,87 @@ +:root { + color: #fff4d6; + background: #2f2627; + font-family: "Courier New", "PingFang SC", "Microsoft YaHei", monospace; + font-synthesis: none; +} + +* { box-sizing: border-box; } + +html, body, #game { + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; + background: #2f2627; +} + +canvas { + image-rendering: pixelated; + image-rendering: crisp-edges; +} + +.name-form { + width: 210px; + padding: 12px; + color: #3d2925; + background: #fff0bc; + border: 3px solid #5b362b; + box-shadow: 0 4px 0 #3b2725; + text-align: center; +} + +.name-form label { + display: block; + margin-bottom: 8px; + font-size: 12px; + font-weight: 700; +} + +.name-form input { + width: 100%; + height: 30px; + padding: 5px 8px; + color: #3d2925; + background: #fffaf0; + border: 2px solid #9b5b30; + border-radius: 0; + font: 700 14px "Courier New", "PingFang SC", monospace; + outline: none; +} + +.name-form input:focus { border-color: #e06f46; } + +.name-form button { + width: 100%; + margin-top: 8px; + padding: 7px 10px; + color: #fff7d5; + background: #c75f38; + border: 2px solid #5b362b; + border-radius: 0; + font: 700 12px "Courier New", "PingFang SC", monospace; + cursor: pointer; +} + +.debug-panel { + width: 250px; + padding: 10px; + color: #f4f4f4; + background: rgba(24, 22, 26, 0.97); + border: 1px solid #f0b64a; + font: 11px/1.4 "Courier New", "PingFang SC", monospace; +} + +.debug-panel h2 { margin: 0 0 8px; color: #f0b64a; font-size: 13px; } +.debug-row { display: flex; gap: 5px; margin: 5px 0; } +.debug-panel label { width: 70px; padding-top: 3px; } +.debug-panel input, .debug-panel select, .debug-panel button { + min-width: 0; + flex: 1; + color: #fff; + background: #38333c; + border: 1px solid #716a78; + font: inherit; +} +.debug-panel button { padding: 4px; cursor: pointer; } +.debug-panel .ending-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; } diff --git a/GPT-5-6-Sol-High/tests/doors.spec.ts b/GPT-5-6-Sol-High/tests/doors.spec.ts new file mode 100644 index 0000000..1e9b5da --- /dev/null +++ b/GPT-5-6-Sol-High/tests/doors.spec.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@playwright/test'; +import { readFileSync } from 'node:fs'; +import { MAP_KEYS } from '../src/game/data'; + +const doors = MAP_KEYS.flatMap((source) => { + const map = JSON.parse(readFileSync(`public/maps/${source}.json`, 'utf8')) as { + layers: Array<{ name: string; objects?: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }>; + }; + return (map.layers.find((layer) => layer.name === 'objects')?.objects ?? []) + .filter((object) => object.type === 'door') + .map((object) => ({ source, target: object.properties?.find((property) => property.name === 'target')?.value ?? '' })); +}); + +test('every authored door reaches its paired target scene', async ({ page }) => { + await page.goto('/'); + await page.locator('#player-name').press('Enter'); + await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true); + + for (const { source, target } of doors) { + await page.evaluate((key) => { + const director = window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, value: string) => void }; + director.emit('debug-teleport', key); + }, source); + await expect.poll(() => page.evaluate((key) => window.__HONEY_GAME__.scene.isActive(key), source)).toBe(true); + await page.evaluate(({ sceneKey, targetKey }) => { + const scene = window.__HONEY_GAME__.scene.getScene(sceneKey) as unknown as { + objects: Array<{ type: string; x: number; y: number; properties?: Array<{ name: string; value: string }> }>; + player: { setPosition: (x: number, y: number) => void }; + }; + const door = scene.objects.find((object) => object.type === 'door' && object.properties?.some((property) => property.name === 'target' && property.value === targetKey)); + if (!door) throw new Error(`Door ${sceneKey} -> ${targetKey} not found`); + scene.player.setPosition(door.x, door.y); + }, { sceneKey: source, targetKey: target }); + await page.waitForTimeout(80); + await page.keyboard.press('e'); + await expect.poll(() => page.evaluate((key) => window.__HONEY_GAME__.scene.isActive(key), target)).toBe(true); + } +}); diff --git a/GPT-5-6-Sol-High/tests/game.spec.ts b/GPT-5-6-Sol-High/tests/game.spec.ts new file mode 100644 index 0000000..e575a2f --- /dev/null +++ b/GPT-5-6-Sol-High/tests/game.spec.ts @@ -0,0 +1,129 @@ +import { expect, test, type Page } from '@playwright/test'; + +async function activeScenes(page: Page): Promise { + return page.evaluate(() => window.__HONEY_GAME__.scene.getScenes(true).map((scene) => scene.scene.key)); +} + +test('title, movement, maps, corruption controls, and ending flow', async ({ page }) => { + const errors: string[] = []; + page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()); }); + page.on('pageerror', (error) => errors.push(error.message)); + + await page.goto('/'); + await expect(page.locator('.name-form')).toBeVisible(); + await expect(page.locator('#player-name')).toHaveAttribute('placeholder', '小满'); + await page.screenshot({ path: '.qa/title.png' }); + await page.locator('#player-name').fill('验收员'); + await page.getByRole('button', { name: '进入蜜糖村' }).click(); + await expect.poll(() => activeScenes(page)).toContain('HouseScene'); + await expect.poll(() => activeScenes(page)).toContain('UIScene'); + + const startX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x); + await page.keyboard.down('ArrowRight'); await page.waitForTimeout(250); await page.keyboard.up('ArrowRight'); + const movedX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x); + expect(movedX).toBeGreaterThan(startX); + for (const [key, animation] of [['w', 'walk-up'], ['s', 'walk-down'], ['a', 'walk-left'], ['d', 'walk-right']] as const) { + await page.keyboard.down(key); await page.waitForTimeout(80); + const activeAnimation = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { anims: { currentAnim?: { key: string } } } }).player.anims.currentAnim?.key); + expect(activeAnimation).toBe(animation); + await page.keyboard.up(key); + } + await page.keyboard.down('ArrowLeft'); await page.waitForTimeout(1600); await page.keyboard.up('ArrowLeft'); + const boundedX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x); + expect(boundedX).toBeGreaterThanOrEqual(5); + + await page.keyboard.press('Backquote'); + await expect(page.locator('.debug-panel')).toBeVisible(); + await page.locator('[data-field="happiness"]').fill('100'); + await page.locator('[data-action="happiness"]').click(); + await page.locator('[data-field="awareness"]').fill('72'); + await page.locator('[data-action="awareness"]').click(); + await page.locator('[data-field="day"]').fill('7'); + await page.locator('[data-action="day"]').click(); + const state = await page.evaluate(() => { + const d = window.__HONEY_GAME__.registry.get('director') as { snapshot: { happiness: number; awareness: number; day: number }; stage: string }; + return { ...d.snapshot, stage: d.stage }; + }); + expect(state).toMatchObject({ happiness: 100, awareness: 72, day: 7, stage: 'S4' }); + await page.screenshot({ path: '.qa/debug.png' }); + + const maps = ['HouseScene', 'PlazaScene', 'BakeryScene', 'ShopScene', 'ClinicScene', 'LakeScene', 'FieldScene', 'WellScene', 'MistScene', 'WhiteScene']; + for (const key of maps) { + await page.locator('[data-field="scene"]').selectOption(key); + await page.locator('[data-action="teleport"]').click(); + await expect.poll(() => activeScenes(page)).toContain(key); + } + + await page.locator('[data-field="scene"]').selectOption('PlazaScene'); + await page.locator('[data-action="teleport"]').click(); + await expect.poll(() => activeScenes(page)).toContain('PlazaScene'); + await page.keyboard.press('Backquote'); + await page.screenshot({ path: '.qa/plaza-s4.png' }); + await page.keyboard.press('Backquote'); + await page.locator('[data-field="happiness"]').fill('40'); + await page.locator('[data-action="happiness"]').click(); + await page.locator('[data-field="day"]').fill('1'); + await page.locator('[data-action="day"]').click(); + await page.keyboard.press('Backquote'); + await page.waitForTimeout(2000); + await page.screenshot({ path: '.qa/plaza-s1.png' }); + await page.evaluate(() => { + const ui = window.__HONEY_GAME__.scene.getScene('UIScene') as unknown as { showDialogue: (options: object) => Promise }; + void ui.showDialogue({ speaker: '阿福', text: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', portrait: 2 }); + }); + await page.waitForTimeout(100); + await page.screenshot({ path: '.qa/dialogue.png' }); + await page.keyboard.press('e'); + await page.keyboard.press('Backquote'); + await page.locator('[data-action="clues"]').click(); + const clueCount = await page.evaluate(() => (window.__HONEY_GAME__.registry.get('director') as { snapshot: { clues: Set } }).snapshot.clues.size); + expect(clueCount).toBe(9); + await page.locator('[data-ending="E3"]').click(); + await expect.poll(() => activeScenes(page)).toContain('EndingScene'); + await page.waitForTimeout(3200); + await page.screenshot({ path: '.qa/e3.png' }); + expect(errors).toEqual([]); +}); + +test('integer scaling stays sharp at desktop and compact viewport', async ({ page }) => { + await page.goto('/'); + await page.locator('#player-name').press('Enter'); + await expect.poll(() => activeScenes(page)).toContain('HouseScene'); + const desktop = await page.locator('canvas').boundingBox(); + expect(desktop?.width).toBe(960); + expect(desktop?.height).toBe(540); + await page.setViewportSize({ width: 640, height: 360 }); + await page.waitForTimeout(250); + const compact = await page.locator('canvas').boundingBox(); + expect(compact?.width).toBe(640); + expect(compact?.height).toBe(360); +}); + +test('all four forced endings render their specified evidence', async ({ page }) => { + const startEnding = async (ending: string, name = '小满') => { + await page.goto('/'); + await page.locator('#player-name').fill(name); + await page.locator('#player-name').press('Enter'); + await expect.poll(() => activeScenes(page)).toContain('HouseScene'); + await page.evaluate((id) => (window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, ending: string) => void }).emit('force-ending', id), ending); + await expect.poll(() => activeScenes(page)).toContain('EndingScene'); + }; + const texts = () => page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('EndingScene') as unknown as { children: { list: Array<{ type?: string; text?: string }> } }).children.list + .filter((child) => child.type === 'Text') + .map((text) => text.text ?? '')); + + await startEnding('E1'); + expect((await texts()).join('\n')).toContain('从此,每一天都很幸福。\n\n每一天。'); + await startEnding('E2'); await page.waitForTimeout(2200); + expect((await texts()).join('\n')).toContain('疗程重新开始。'); + await startEnding('E3', '真实姓名'); await page.waitForTimeout(3200); + const record = (await texts()).join('\n'); + expect(record).toContain('蜜糖谷情绪疗养中心'); + expect(record).toContain('姓名:真实姓名'); + expect(record).toContain('编号:PT-07'); + expect(record).toMatch(/疗程时长:\d{2}:\d{2}/); + await page.waitForTimeout(2900); + expect((await texts()).join('\n')).toContain('微笑消失了。'); + await startEnding('E4'); + expect((await texts()).join('\n')).toContain('系统检测到疗程无效。'); +}); diff --git a/GPT-5-6-Sol-High/tests/specification.spec.ts b/GPT-5-6-Sol-High/tests/specification.spec.ts new file mode 100644 index 0000000..d589a81 --- /dev/null +++ b/GPT-5-6-Sol-High/tests/specification.spec.ts @@ -0,0 +1,141 @@ +import { expect, test } from '@playwright/test'; +import { readFileSync, readdirSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { ANNOUNCEMENTS, C9_LINES, CLUES, CORRUPTIONS, MAP_KEYS, TASKS } from '../src/game/data'; + +test('fixed PRD tables remain exact', () => { + expect(TASKS).toHaveLength(15); + expect(Object.keys(CLUES)).toHaveLength(9); + expect(CORRUPTIONS).toEqual([ + ['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'], + ['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'], + ]); + expect(ANNOUNCEMENTS).toEqual({ + 1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + 2: '早上好呀!新的一天,新的幸福。祝你开心!', + 3: '早上好!今天要把幸福装得满满的哦。要开心!', + 4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + 5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', + 6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', + 7: '庆典开始了。来吧。', + }); + expect(CLUES).toEqual({ + C1: '每一页都是同一个日期', C2: '手写小字:「按疗程配比。甜度:最大。」', C3: '所有邮戳是同一天', C4: '署名闪过乱码「PT-0▓」', + C5: '同一商品重复排列,标签全部空白', C6: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」', + C7: '井底回声:极轻的监护仪滴声(靠近并交互 5 秒)', C8: '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」', + C9: '三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」', + }); + expect(C9_LINES).toEqual({ 2: '湖水一直很平静', 4: '面包一直是那个味道', 6: '孩子,今天是第几个今天?' }); + expect(TASKS.slice(7, 10).map(({ title, reward }) => ({ title, reward }))).toEqual(TASKS.slice(0, 3).map(({ title, reward }) => ({ title, reward }))); +}); + +test('Aseprite sheets and Tiled exports have the required dimensions and layers', () => { + const pngSize = (path: string) => { + const png = readFileSync(path); + return { width: png.readUInt32BE(16), height: png.readUInt32BE(20) }; + }; + expect(pngSize(resolve('public/assets/tileset.png'))).toEqual({ width: 256, height: 64 }); + expect(pngSize(resolve('public/assets/characters.png'))).toEqual({ width: 256, height: 96 }); + expect(pngSize(resolve('public/assets/portraits.png'))).toEqual({ width: 432, height: 48 }); + expect(readdirSync('public/assets/source').filter((file) => file.endsWith('.aseprite'))).toHaveLength(3); + + const expectedSizes: Record = { + HouseScene: [12, 8], PlazaScene: [30, 20], BakeryScene: [15, 10], ShopScene: [15, 10], ClinicScene: [12, 8], + LakeScene: [24, 16], FieldScene: [20, 14], WellScene: [14, 10], MistScene: [20, 8], WhiteScene: [30, 8], + }; + const maps = new Map>(); + MAP_KEYS.forEach((key) => { + const map = JSON.parse(readFileSync(resolve(`public/maps/${key}.json`), 'utf8')) as { + width: number; height: number; layers: Array<{ name: string; type: string; objects?: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }>; + tilesets: Array<{ tilecount: number }>; + }; + expect([map.width, map.height]).toEqual(expectedSizes[key]); + expect(map.layers.map((layer) => layer.name)).toEqual(['ground', 'obstacles', 'objects']); + expect(map.tilesets[0].tilecount).toBe(64); + maps.set(key, map as unknown as Record); + }); + + for (const key of MAP_KEYS) { + const map = maps.get(key) as unknown as { layers: Array<{ name: string; objects: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }> }; + const doors = map.layers.find((layer) => layer.name === 'objects')?.objects.filter((object) => object.type === 'door') ?? []; + doors.forEach((door) => { + const target = door.properties?.find((property) => property.name === 'target')?.value; + const targetMap = maps.get(String(target)) as unknown as { layers: Array<{ name: string; objects: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }> }; + const reverse = targetMap.layers.find((layer) => layer.name === 'objects')?.objects.some((object) => object.type === 'door' && object.properties?.some((property) => property.name === 'target' && property.value === key)); + expect(reverse, `${key} -> ${target} needs a reverse door`).toBe(true); + }); + } +}); + +test('director enforces rewards, clues, sad behavior, stages, overflow, and endings', async ({ page }) => { + await page.goto('/'); + await page.locator('#player-name').press('Enter'); + await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true); + + const result = await page.evaluate(({ tasks, corruptionPairs, clueIds }) => { + const d = window.__HONEY_GAME__.registry.get('director') as any; + const rewards = tasks.map((task: { id: string; day: number; reward: number }) => { + d.reset('Test'); d.setDay(task.day); d.acceptDay(); d.setHappiness(0); d.completeTask(task.id); + return [task.id, d.snapshot.happiness]; + }); + d.reset('Test'); + const clueDeltas: number[] = []; + clueIds.forEach((id) => { const before = d.snapshot.awareness; d.discoverClue(id); clueDeltas.push(d.snapshot.awareness - before); }); + d.reset('Test'); d.setHappiness(50); + ['refuse', 'negative', 'idle', 'cling', 'rain'].forEach((key) => d.sadBehavior(key)); + const sad = { happiness: d.snapshot.happiness, awareness: d.snapshot.awareness }; + const stages = [10, 40, 70, 90, 100].map((value) => { d.setHappiness(value); return d.stage; }); + const corruptions = corruptionPairs.map(([clean, broken]: [string, string]) => { d.setHappiness(100); return [broken, d.corruptText(`${clean}。`, '咪咪')]; }); + d.reset('Test'); d.setHappiness(100); d.adjustHappiness(-5); d.adjustHappiness(-5); d.adjustHappiness(-5); + const overflow = { happiness: d.snapshot.happiness, overflow: d.snapshot.overflow }; + d.reset('Test'); d.setHappiness(30); + const small = [d.smallInteraction('water'), d.smallInteraction('water'), d.smallInteraction('water'), d.smallInteraction('water'), d.snapshot.happiness]; + const endings: string[] = []; + d.reset('Test'); d.setHappiness(90); endings.push(d.ending()); + d.setAwareness(30); endings.push(d.ending()); + d.reset('Test'); clueIds.slice(0, 6).forEach((id) => d.discoverClue(id)); d.setAwareness(70); endings.push(d.ending()); + d.reset('Test'); d.setHappiness(10); d.setDay(7); d.snapshot.cheerEscapes = 3; endings.push(d.ending()); + d.reset('Test'); d.setHappiness(90); d.setAwareness(70); endings.push(d.ending() ?? 'none'); + d.reset('Test'); d.setHappiness(10); d.snapshot.lowSince = performance.now() - 121_000; d.tick(); + const forcedCheer = d.snapshot.cheerActive; + d.reset('Test'); d.snapshot.dayStartedAt = performance.now() - 241_000; d.acceptDay(); ['T1', 'T2', 'T3'].forEach((id) => d.completeTask(id)); + const duskReached = d.snapshot.sunset; d.advanceDay(); + const nextDay = { day: d.snapshot.day, boardRead: d.snapshot.boardRead, sunset: d.snapshot.sunset }; + return { rewards, clueDeltas, clueCount: d.snapshot.clues.size, sad, stages, corruptions, overflow, small, endings, forcedCheer, duskReached, nextDay }; + }, { tasks: TASKS.map(({ id, day, reward }) => ({ id, day, reward })), corruptionPairs: CORRUPTIONS, clueIds: Object.keys(CLUES) }); + + expect(result.rewards).toEqual(TASKS.map(({ id, reward }) => [id, reward])); + expect(result.clueDeltas).toEqual(Array(9).fill(8)); + expect(result.sad).toEqual({ happiness: 25, awareness: 15 }); + expect(result.stages).toEqual(['S0', 'S1', 'S2', 'S3', 'S4']); + result.corruptions.forEach(([broken, rendered]) => expect(rendered).toContain(broken)); + expect(result.overflow).toEqual({ happiness: 100, overflow: 0 }); + expect(result.small).toEqual([true, true, true, false, 33]); + expect(result.endings).toEqual(['E1', 'E2', 'E3', 'E4', 'none']); + expect(result.forcedCheer).toBe(true); + expect(result.duskReached).toBe(true); + expect(result.nextDay).toEqual({ day: 2, boardRead: false, sunset: false }); +}); + +test('Day 6 smile timer completes at 60 seconds and scripted rain records sadness', async ({ page }) => { + await page.goto('/'); + await page.locator('#player-name').press('Enter'); + await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true); + await page.evaluate(() => (window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, key: string) => void }).emit('debug-teleport', 'PlazaScene')); + await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('PlazaScene'))).toBe(true); + const result = await page.evaluate(() => { + const director = window.__HONEY_GAME__.registry.get('director') as any; + const scene = window.__HONEY_GAME__.scene.getScene('PlazaScene') as any; + director.reset('Test'); director.setDay(6); director.acceptDay(); director.setHappiness(30); + director.snapshot.dayStartedAt = performance.now() - 241_000; + director.completeTask('T13'); director.setT14(true); + scene.t14Start = performance.now() - 60_100; + scene.updateT14(); + const smile = { completed: director.snapshot.completed.has('T14'), active: director.snapshot.t14Active, sunset: director.snapshot.sunset, happiness: director.snapshot.happiness }; + scene.rainSince = performance.now() - 10_100; + scene.updateRain(16); + return { smile, rain: { awareness: director.snapshot.awareness, happiness: director.snapshot.happiness } }; + }); + expect(result.smile).toEqual({ completed: true, active: false, sunset: true, happiness: 55 }); + expect(result.rain).toEqual({ awareness: 3, happiness: 50 }); +}); diff --git a/GPT-5-6-Sol-High/tools/export-maps.mjs b/GPT-5-6-Sol-High/tools/export-maps.mjs new file mode 100644 index 0000000..0c34b86 --- /dev/null +++ b/GPT-5-6-Sol-High/tools/export-maps.mjs @@ -0,0 +1,13 @@ +import { execFileSync } from 'node:child_process'; +import { readdirSync, mkdirSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const root = resolve(import.meta.dirname, '..'); +const sourceDir = resolve(root, 'public/maps/source'); +const outDir = resolve(root, 'public/maps'); +const tiled = process.env.TILED ?? '/Applications/Tiled.app/Contents/MacOS/Tiled'; +mkdirSync(outDir, { recursive: true }); +for (const file of readdirSync(sourceDir).filter((name) => name.endsWith('.tmx'))) { + execFileSync(tiled, ['--export-map', 'json', resolve(sourceDir, file), resolve(outDir, file.replace('.tmx', '.json'))], { stdio: 'inherit' }); +} +console.log('Exported 10 Tiled JSON maps.'); diff --git a/GPT-5-6-Sol-High/tools/generate-assets.lua b/GPT-5-6-Sol-High/tools/generate-assets.lua new file mode 100644 index 0000000..3c9f4b0 --- /dev/null +++ b/GPT-5-6-Sol-High/tools/generate-assets.lua @@ -0,0 +1,179 @@ +local root = app.params["root"] +local assetDir = root .. "/public/assets" +local sourceDir = assetDir .. "/source" +app.fs.makeDirectory(root .. "/public") +app.fs.makeDirectory(assetDir) +app.fs.makeDirectory(sourceDir) + +local C = { + transparent = Color{ r=0, g=0, b=0, a=0 }, + grass = Color{ r=104, g=190, b=82, a=255 }, + grass2 = Color{ r=76, g=158, b=70, a=255 }, + cream = Color{ r=255, g=233, b=164, a=255 }, + honey = Color{ r=239, g=151, b=49, a=255 }, + orange = Color{ r=199, g=97, b=43, a=255 }, + sky = Color{ r=93, g=190, b=224, a=255 }, + water = Color{ r=68, g=171, b=216, a=255 }, + water2 = Color{ r=139, g=224, b=231, a=255 }, + stone = Color{ r=206, g=199, b=171, a=255 }, + stone2 = Color{ r=162, g=155, b=133, a=255 }, + brown = Color{ r=91, g=54, b=43, a=255 }, + wood = Color{ r=151, g=91, b=48, a=255 }, + leaf = Color{ r=51, g=130, b=60, a=255 }, + pink = Color{ r=237, g=111, b=150, a=255 }, + red = Color{ r=194, g=66, b=64, a=255 }, + white = Color{ r=247, g=244, b=226, a=255 }, + gray = Color{ r=194, g=199, b=199, a=255 }, + gray2 = Color{ r=132, g=142, b=145, a=255 }, + black = Color{ r=48, g=38, b=37, a=255 }, + yellow = Color{ r=255, g=213, b=62, a=255 }, + purple = Color{ r=133, g=89, b=159, a=255 }, +} + +local function fill(img, x, y, w, h, color) + for yy = y, y + h - 1 do + for xx = x, x + w - 1 do img:drawPixel(xx, yy, color) end + end +end + +local function pixel(img, x, y, color) img:drawPixel(x, y, color) end + +local function outlineRect(img, x, y, w, h, color) + fill(img, x, y, w, 1, color); fill(img, x, y+h-1, w, 1, color) + fill(img, x, y, 1, h, color); fill(img, x+w-1, y, 1, h, color) +end + +local function save(sprite, sourceName, pngName) + sprite:saveAs(sourceDir .. "/" .. sourceName) + sprite:saveCopyAs(assetDir .. "/" .. pngName) + sprite:close() +end + +local function makeTileset() + local sprite = Sprite(256, 64, ColorMode.RGB) + sprite.filename = "tileset" + sprite.cels[1].image:clear(C.transparent) + local img = sprite.cels[1].image + local function tile(index, base) + local x = (index % 16) * 16; local y = math.floor(index / 16) * 16 + fill(img, x, y, 16, 16, base) + return x, y + end + + for i=0,63 do + local palette = {C.grass, C.stone, C.water, C.cream, C.wood, C.gray} + local x,y = tile(i, palette[(i % #palette)+1]) + if i % 6 == 0 then + pixel(img,x+3,y+5,C.grass2); pixel(img,x+11,y+3,C.cream); pixel(img,x+12,y+3,C.pink) + elseif i % 6 == 1 then + outlineRect(img,x+1,y+1,7,6,C.stone2); outlineRect(img,x+8,y+7,7,8,C.stone2) + elseif i % 6 == 2 then + fill(img,x,y+3,16,2,C.water2); fill(img,x+5,y+10,9,1,C.water2) + elseif i % 6 == 3 then + fill(img,x,y,16,4,C.honey); fill(img,x+2,y+7,4,3,C.orange); fill(img,x+10,y+6,4,4,C.orange) + elseif i % 6 == 4 then + fill(img,x,y+4,16,2,C.brown); fill(img,x+4,y,2,16,C.brown); fill(img,x+12,y,1,16,C.brown) + else + outlineRect(img,x+1,y+1,14,14,C.gray2); fill(img,x+3,y+3,10,10,C.white) + end + end + + local x,y = tile(0,C.grass); pixel(img,x+3,y+5,C.grass2); pixel(img,x+11,y+9,C.grass2) + x,y = tile(1,C.stone); outlineRect(img,x,y,8,8,C.stone2); outlineRect(img,x+8,y+8,8,8,C.stone2) + x,y = tile(2,C.water); fill(img,x,y+4,12,2,C.water2); fill(img,x+5,y+11,11,1,C.water2) + x,y = tile(3,C.cream); fill(img,x,y,16,3,C.honey); fill(img,x,y+13,16,3,C.honey) + x,y = tile(4,C.wood); fill(img,x,y+4,16,2,C.brown); fill(img,x,y+11,16,1,C.brown) + x,y = tile(5,C.white); outlineRect(img,x,y,16,16,C.gray) + x,y = tile(6,C.grass); fill(img,x+2,y+3,12,10,C.leaf); pixel(img,x+6,y+6,C.red); pixel(img,x+11,y+9,C.red) + x,y = tile(7,C.grass); fill(img,x+1,y+2,14,12,C.leaf); fill(img,x+4,y+4,3,3,C.pink); fill(img,x+10,y+7,3,3,C.yellow) + x,y = tile(8,C.honey); fill(img,x,y+6,16,10,C.cream); outlineRect(img,x,y+6,16,10,C.orange) + x,y = tile(9,C.orange); fill(img,x,y+6,16,10,C.cream); fill(img,x+6,y+9,4,7,C.brown) + x,y = tile(10,C.cream); fill(img,x+2,y+5,12,8,C.wood); outlineRect(img,x+2,y+5,12,8,C.brown) + x,y = tile(11,C.grass); fill(img,x+6,y,4,16,C.wood); fill(img,x+1,y,14,8,C.leaf) + x,y = tile(12,C.grass); fill(img,x+2,y+2,12,12,C.pink); fill(img,x+5,y+5,6,6,C.yellow) + x,y = tile(13,C.wood); fill(img,x+4,y+2,8,14,C.black); fill(img,x+6,y+3,4,11,C.water) + x,y = tile(14,C.sky); fill(img,x+4,y,8,16,C.white); fill(img,x+1,y+4,14,8,C.white) + x,y = tile(15,C.white); fill(img,x,y,16,3,C.gray); fill(img,x,y+13,16,3,C.gray) + x,y = tile(16,C.grass); fill(img,x+3,y+5,10,7,C.white); outlineRect(img,x+3,y+5,10,7,C.brown); pixel(img,x+8,y+8,C.red) + save(sprite, "tileset.aseprite", "tileset.png") +end + +local speciesColors = {C.cream,C.pink,C.brown,C.orange,C.gray,C.honey,C.grass2,C.purple,C.red,C.sky,C.yellow,C.wood,C.white} + +local function drawCharacter(img, ox, oy, body, facing, step, expression, species) + fill(img,ox,oy,16,24,C.transparent) + local skin = body + fill(img,ox+4,oy+3,8,8,skin) + fill(img,ox+3,oy+5,10,5,skin) + if species == "rabbit" then fill(img,ox+4,oy,2,5,skin); fill(img,ox+10,oy,2,5,skin) end + if species == "deer" then pixel(img,ox+3,oy+2,C.brown); pixel(img,ox+12,oy+2,C.brown) end + if species == "bird" then fill(img,ox+3,oy+6,10,8,skin); pixel(img,ox+13,oy+8,C.honey) end + local eyeY = oy+6 + pixel(img,ox+6,eyeY,C.black); pixel(img,ox+10,eyeY + (expression == "off" and 1 or 0),C.black) + if expression == "smile" then pixel(img,ox+7,oy+9,C.red); pixel(img,ox+8,oy+10,C.red); pixel(img,ox+9,oy+9,C.red) + elseif expression == "flat" then fill(img,ox+7,oy+9,3,1,C.black) + else pixel(img,ox+7,oy+9,C.black); fill(img,ox+8,oy+10,3,1,C.black) end + fill(img,ox+4,oy+12,8,8,body) + fill(img,ox+3,oy+13,2,6,C.cream); fill(img,ox+11,oy+13,2,6,C.cream) + local foot = step % 2 + if facing == "left" or facing == "right" then + fill(img,ox+4-foot,oy+20,3,3,C.black); fill(img,ox+9+foot,oy+20,3,3,C.black) + else + fill(img,ox+4+foot,oy+20,3,3,C.black); fill(img,ox+9-foot,oy+20,3,3,C.black) + end + if facing == "up" then fill(img,ox+5,oy+5,6,4,skin); fill(img,ox+5,oy+6,6,2,C.brown) end + if facing == "left" then pixel(img,ox+5,eyeY,C.black); fill(img,ox+10,eyeY,2,2,skin) end + if facing == "right" then pixel(img,ox+11,eyeY,C.black); fill(img,ox+4,eyeY,2,2,skin) end +end + +local function makeCharacters() + local cols, rows = 16, 4 + local sprite = Sprite(cols*16, rows*24, ColorMode.RGB) + sprite.cels[1].image:clear(C.transparent) + local img = sprite.cels[1].image + local dirs = {"down","up","left","right"} + for d=0,3 do for f=0,3 do drawCharacter(img,f*16,d*24,C.sky,dirs[d+1],f,"smile","human") end end + local npc = { + {C.honey,"dog"},{C.white,"rabbit"},{C.brown,"bear"},{C.orange,"cat"}, + {C.cream,"deer"},{C.sky,"bird"},{C.grass2,"turtle"} + } + for n=0,6 do for e=0,2 do + local index = 16 + n*3 + e; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24 + local exp=({"smile","flat","off"})[e+1] + drawCharacter(img,ox,oy,npc[n+1][1],"down",0,exp,npc[n+1][2]) + end end + for n=0,4 do + local index=37+n; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24 + drawCharacter(img,ox,oy,speciesColors[n+8],"down",n%2,"smile","human") + end + for n=0,3 do + local index=42+n; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24 + drawCharacter(img,ox,oy,(n%2==0 and C.pink or C.sky),"down",n%2,"smile","bird") + if n%2==1 then fill(img,ox+7,oy+9,4,2,C.black) end + end + save(sprite,"characters.aseprite","characters.png") +end + +local function drawPortrait(img, ox, body, expression, real) + fill(img,ox,0,48,48,C.cream) + fill(img,ox+7,6,34,34,C.brown) + fill(img,ox+9,8,30,30,body) + fill(img,ox+14,17,4,5,C.black); fill(img,ox+30,17 + (expression=="off" and 2 or 0),4,5,C.black) + if real then + fill(img,ox+15,31,18,2,C.black); fill(img,ox+13,13,8,2,C.gray2); fill(img,ox+27,13,8,2,C.gray2) + elseif expression == "smile" then + fill(img,ox+17,29,4,3,C.red); fill(img,ox+21,32,8,3,C.red); fill(img,ox+29,29,3,3,C.red) + else fill(img,ox+18,30,14,2,C.black) end +end + +local function makePortraits() + local sprite=Sprite(9*48,48,ColorMode.RGB); sprite.cels[1].image:clear(C.transparent) + local img=sprite.cels[1].image + drawPortrait(img,0,C.sky,"smile",false); drawPortrait(img,48,C.gray,"flat",true) + local colors={C.honey,C.white,C.brown,C.orange,C.cream,C.sky,C.grass2} + for i=0,6 do drawPortrait(img,(i+2)*48,colors[i+1],"smile",false) end + save(sprite,"portraits.aseprite","portraits.png") +end + +makeTileset(); makeCharacters(); makePortraits() +print("Generated original Aseprite assets in " .. assetDir) diff --git a/GPT-5-6-Sol-High/tools/generate-maps.mjs b/GPT-5-6-Sol-High/tools/generate-maps.mjs new file mode 100644 index 0000000..77f6345 --- /dev/null +++ b/GPT-5-6-Sol-High/tools/generate-maps.mjs @@ -0,0 +1,129 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const root = resolve(import.meta.dirname, '..'); +const sourceDir = resolve(root, 'public/maps/source'); +await mkdir(sourceDir, { recursive: true }); + +const maps = [ + ['HouseScene', '小满小屋', 12, 8, 4, [ + ['spawn', 96, 88, { id: 'default' }], ['door', 96, 112, { target: 'PlazaScene', spawn: 'house' }], + ['interact', 42, 34, { id: 'calendar', label: '床头日历' }], ['interact', 64, 42, { id: 'bed', label: '床' }], + ]], + ['PlazaScene', '村广场', 30, 20, 0, [ + ['spawn', 240, 260, { id: 'default' }], ['spawn', 240, 280, { id: 'house' }], ['spawn', 32, 160, { id: 'bakery' }], ['spawn', 448, 160, { id: 'shop' }], ['spawn', 240, 32, { id: 'clinic' }], ['spawn', 32, 48, { id: 'lake' }], ['spawn', 448, 48, { id: 'field' }], ['spawn', 32, 280, { id: 'well' }], ['spawn', 448, 280, { id: 'mist' }], + ['door', 240, 304, { target: 'HouseScene', spawn: 'default' }], ['door', 0, 160, { target: 'BakeryScene', spawn: 'default' }], ['door', 464, 160, { target: 'ShopScene', spawn: 'default' }], ['door', 240, 0, { target: 'ClinicScene', spawn: 'default' }], ['door', 0, 48, { target: 'LakeScene', spawn: 'default' }], ['door', 464, 48, { target: 'FieldScene', spawn: 'default' }], ['door', 0, 280, { target: 'WellScene', spawn: 'default' }], ['door', 464, 280, { target: 'MistScene', spawn: 'default' }], + ['npc', 240, 132, { id: 'afu', label: '阿福' }], ['npc', 180, 162, { id: 'dabao', label: '大宝' }], + ['npc', 232, 76, { id: 'birds', label: '双子鸟' }], ['npc', 124, 210, { id: 'extra1', label: '村民' }], ['npc', 352, 224, { id: 'extra2', label: '村民' }], + ['interact', 240, 146, { id: 'board', label: '公告板' }], ['interact', 92, 148, { id: 'mail', label: '散落信件' }], ['interact', 200, 184, { id: 'flower1', label: '花坛' }], ['interact', 240, 200, { id: 'flower2', label: '花坛' }], ['interact', 280, 184, { id: 'flower3', label: '花坛' }], + ['interact', 160, 100, { id: 'ribbon1', label: '彩带架' }], ['interact', 224, 92, { id: 'ribbon2', label: '彩带架' }], ['interact', 288, 92, { id: 'ribbon3', label: '彩带架' }], ['interact', 352, 100, { id: 'ribbon4', label: '彩带架' }], ['interact', 240, 224, { id: 'festival', label: '庆典场地' }], + ]], + ['BakeryScene', '面包房', 15, 10, 3, [ + ['spawn', 120, 128, { id: 'default' }], ['door', 120, 144, { target: 'PlazaScene', spawn: 'bakery' }], ['npc', 120, 72, { id: 'yuanyuan', label: '圆圆' }], ['interact', 48, 46, { id: 'recipe', label: '配方纸' }], ['interact', 184, 50, { id: 'oven', label: '烤炉' }], + ]], + ['ShopScene', '杂货店', 15, 10, 3, [ + ['spawn', 120, 128, { id: 'default' }], ['door', 120, 144, { target: 'PlazaScene', spawn: 'shop' }], ['npc', 120, 72, { id: 'mimi', label: '咪咪' }], ['interact', 48, 52, { id: 'shelf', label: '货架' }], + ]], + ['ClinicScene', '诊所', 12, 8, 5, [ + ['spawn', 96, 96, { id: 'default' }], ['door', 96, 112, { target: 'PlazaScene', spawn: 'clinic' }], ['npc', 96, 50, { id: 'doctor', label: '芙医生' }], ['interact', 48, 42, { id: 'form', label: '桌上表格' }], + ]], + ['LakeScene', '湖畔', 24, 16, 2, [ + ['spawn', 368, 128, { id: 'default' }], ['door', 368, 128, { target: 'PlazaScene', spawn: 'lake' }], ['npc', 142, 132, { id: 'turtle', label: '龟爷爷' }], ['npc', 284, 68, { id: 'extra4', label: '村民' }], ['interact', 190, 148, { id: 'picnic', label: '野餐布' }], ['interact', 110, 68, { id: 'lake', label: '湖面' }], + ]], + ['FieldScene', '花田', 20, 14, 0, [ + ['spawn', 16, 112, { id: 'default' }], ['door', 0, 112, { target: 'PlazaScene', spawn: 'field' }], ['npc', 244, 148, { id: 'extra3', label: '村民' }], + ['interact', 80, 72, { id: 'berry1', label: '莓果丛' }], ['interact', 128, 56, { id: 'berry2', label: '莓果丛' }], ['interact', 176, 88, { id: 'berry3', label: '莓果丛' }], ['interact', 224, 64, { id: 'berry4', label: '莓果丛' }], ['interact', 272, 96, { id: 'berry5', label: '莓果丛' }], + ]], + ['WellScene', '古井空地', 14, 10, 0, [ + ['spawn', 208, 80, { id: 'default' }], ['door', 208, 80, { target: 'PlazaScene', spawn: 'well' }], ['npc', 46, 116, { id: 'extra5', label: '村民' }], ['interact', 104, 72, { id: 'well', label: '古井' }], + ]], + ['MistScene', '雾墙边界', 20, 8, 0, [ + ['spawn', 16, 64, { id: 'default' }], ['door', 0, 64, { target: 'PlazaScene', spawn: 'mist' }], ['interact', 288, 64, { id: 'mist', label: '雾墙' }], + ]], + ['WhiteScene', '白色层', 30, 8, 5, [ + ['spawn', 24, 64, { id: 'default' }], ['interact', 400, 64, { id: 'record', label: '病历' }], + ]], +]; + +function createTileData(key, width, height, ground) { + const data = Array(width * height).fill(ground + 1); + const set = (x, y, gid) => { if (x >= 0 && y >= 0 && x < width && y < height) data[y * width + x] = gid; }; + const rect = (x, y, w, h, gid) => { for (let yy = y; yy < y + h; yy += 1) for (let xx = x; xx < x + w; xx += 1) set(xx, yy, gid); }; + const building = (x, y, w, h) => { + rect(x, y, w, 2, 9); rect(x, y + 2, w, h - 2, 4); + set(x + Math.floor(w / 2), y + h - 1, 10); + for (let xx = x + 1; xx < x + w - 1; xx += 3) set(xx, y + 3, 15); + }; + + if (key === 'PlazaScene') { + rect(13, 0, 4, height, 2); rect(0, 8, width, 4, 2); + building(2, 1, 7, 6); building(21, 1, 7, 6); building(2, 14, 7, 5); building(21, 14, 7, 5); + rect(10, 2, 3, 3, 12); set(15, 8, 11); + set(5, 9, 17); + [[12, 11], [13, 12], [14, 11], [16, 11], [17, 12], [18, 11]].forEach(([x, y]) => set(x, y, 8)); + [[1, 3], [9, 6], [19, 5], [28, 3], [4, 12], [25, 12], [10, 16], [19, 16]].forEach(([x, y]) => set(x, y, 13)); + } else if (key === 'HouseScene') { + rect(0, 0, width, height, 4); rect(0, 0, width, 1, 5); rect(0, height - 1, width, 1, 5); + rect(2, 2, 3, 2, 5); set(8, 2, 11); rect(6, 5, 2, 1, 8); set(6, 7, 10); + } else if (key === 'BakeryScene') { + rect(0, 0, width, height, 4); rect(1, 1, width - 2, 2, 5); set(11, 2, 13); rect(2, 5, 11, 1, 5); set(7, 9, 10); + } else if (key === 'ShopScene') { + rect(0, 0, width, height, 4); for (let x = 1; x < width - 1; x += 3) rect(x, 1, 2, 4, 5); set(7, 9, 10); + for (let y = 2; y <= 4; y += 1) for (const x of [2, 5, 8]) set(x, y, 7); + } else if (key === 'ClinicScene') { + rect(0, 0, width, height, 6); rect(0, 0, width, 1, 16); rect(2, 2, 5, 2, 5); set(6, 7, 10); set(9, 2, 15); + } else if (key === 'LakeScene') { + rect(0, 0, 10, height, 3); rect(10, 0, width - 10, height, 1); rect(10, 7, width - 10, 3, 2); + rect(11, 9, 3, 2, 5); rect(14, 9, 3, 2, 8); for (let y = 2; y < height - 2; y += 4) set(20, y, 12); + } else if (key === 'FieldScene') { + rect(0, 6, width, 3, 2); + for (let y = 1; y < height - 1; y += 3) for (let x = 2; x < width - 1; x += 3) set(x, y, (x + y) % 2 ? 7 : 8); + } else if (key === 'WellScene') { + rect(3, 2, 8, 6, 2); rect(5, 3, 4, 4, 14); set(13, 5, 10); + } else if (key === 'MistScene') { + rect(0, 3, width - 3, 3, 2); rect(width - 4, 0, 4, height, 15); + } else if (key === 'WhiteScene') { + rect(0, 0, width, height, 6); rect(0, 0, width, 1, 16); rect(0, height - 1, width, 1, 16); rect(19, 1, 1, height - 2, 16); set(25, 4, 5); + } + return data; +} + +const collisionSpecs = { + PlazaScene: [[32, 16, 112, 96], [336, 16, 112, 96], [32, 224, 112, 76], [336, 224, 112, 76], [160, 32, 48, 48], [232, 128, 32, 24]], + BakeryScene: [[16, 16, 208, 32], [176, 32, 32, 30], [32, 80, 176, 12]], + ShopScene: [[16, 16, 32, 64], [64, 16, 32, 64], [112, 16, 32, 64], [160, 16, 32, 64], [208, 16, 16, 64]], + ClinicScene: [[32, 32, 80, 32]], + LakeScene: [[0, 0, 150, 256]], + WellScene: [[80, 48, 64, 64]], + WhiteScene: [[304, 16, 16, 96]], +}; + +const propertyXml = (properties) => Object.entries(properties).map(([name, value]) => ` `).join('\n'); + +for (const [key, displayName, width, height, ground, objects] of maps) { + const data = createTileData(key, width, height, ground); + const borders = [ + ``, + ``, + ``, + ``, + ]; + (collisionSpecs[key] ?? []).forEach(([x, y, w, h], index) => borders.push(``)); + const objectXml = objects.map(([type, x, y, properties], index) => ` \n \n${propertyXml(properties)}\n \n `).join('\n'); + const tmx = ` + + + + ${data.join(',')} + ${borders.join('\n')} + \n${objectXml}\n +`; + await writeFile(resolve(sourceDir, `${key}.tmx`), tmx); +} + +await writeFile(resolve(sourceDir, 'tileset.tsx'), ` + + +`); + +console.log(`Generated ${maps.length} original Tiled source maps in ${sourceDir}`); diff --git a/GPT-5-6-Sol-High/tsconfig.json b/GPT-5-6-Sol-High/tsconfig.json new file mode 100644 index 0000000..720578f --- /dev/null +++ b/GPT-5-6-Sol-High/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": false, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src", "tests", "vite.config.ts", "playwright.config.ts"] +} diff --git a/GPT-5-6-Sol-High/vite.config.ts b/GPT-5-6-Sol-High/vite.config.ts new file mode 100644 index 0000000..0078b8a --- /dev/null +++ b/GPT-5-6-Sol-High/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + server: { host: '127.0.0.1', port: 4173 }, + preview: { host: '127.0.0.1', port: 4173 }, +}); diff --git a/Grok/.gitignore b/Grok/.gitignore new file mode 100644 index 0000000..000a58b --- /dev/null +++ b/Grok/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.DS_Store +*.local +.vite/ +playwright-report/ +test-results/ diff --git a/Grok/Dockerfile b/Grok/Dockerfile new file mode 100644 index 0000000..b7fe45c --- /dev/null +++ b/Grok/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-alpine AS build +WORKDIR /app +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1 +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npx vite build +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html diff --git a/Grok/PRD.md b/Grok/PRD.md new file mode 100644 index 0000000..0f5ea47 --- /dev/null +++ b/Grok/PRD.md @@ -0,0 +1,226 @@ +# PRD:《崩坏童话:蜜糖村》网页版 + +> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局 + +## 1. 游戏理念与设计支柱 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。 + +四条设计支柱,所有实现争议回到这里裁决: + +1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。 +2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。 +3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。 +4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。 + +## 2. 世界观:三层结构 + +| 层 | 名称 | 玩家所见 | 实质 | +|---|---|---|---| +| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 | +| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" | +| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 | + +命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。 + +## 3. 主角设定 + +- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。 +- 性别中性像素小人,16×24 像素/帧,无内心独白。 +- 对话头像永远同一帧微笑(见支柱 2)。 +- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。 + +## 4. NPC 清单(封闭,7 组 + 群演) + +每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。 + +| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 | +|---|---|---|---|---|---| +| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 | +| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) | +| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 | +| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) | +| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 | +| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 | +| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 | +| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — | + +## 5. 七日故事线 + +每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。 + +| 日 | 主题 | 剧情要点 | 侵蚀预期 | +|---|---|---|---| +| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 | +| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 | +| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 | +| Day 4 | 完美的重复 | 任务 T8–T10 与 Day 1 的 T1–T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 | +| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 | +| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 | +| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 | + +阿福每日公告(逐字实现,注意措辞逐日滑移): + +- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」 +- D2「早上好呀!新的一天,新的幸福。祝你开心!」 +- D3「早上好!今天要把幸福装得满满的哦。要开心!」 +- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同) +- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」 +- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」 +- D7「庆典开始了。来吧。」 + +## 6. 核心系统 + +### 6.1 幸福值(明线) + +- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。 +- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。 +- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。 +- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。 +- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。 + +### 6.2 觉知值(暗线,永不显示) + +- 范围 0–100,开局 0。只升不降。 +- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。 + +### 6.3 悲伤行为(封闭清单,5 项) + +拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。 + +### 6.4 侵蚀阶段矩阵(唯一真源) + +阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。 + +| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5) | UI | +|---|---|---|---|---|---|---| +| S0 | 0–20 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 0–2 瓣 | +| S1 | 21–50 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 3–6 瓣 | +| S2 | 51–80 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 | +| S3 | 81–99 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 | +| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 | + +NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。 + +### 6.5 文本腐蚀 + +同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。 + +### 6.6 BGM 与音效 + +- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。 +- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。 +- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。 +- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。 + +## 7. 任务清单(封闭,15 个) + +| ID | 日 | 任务 | 幸福奖励 | 备注 | +|---|---|---|---|---| +| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 | +| T2 | D1 | 给广场花坛浇水 ×3 | +10 | | +| T3 | D1 | 与任意三位村民问好 | +10 | | +| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) | +| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) | +| T6 | D3 | 去花田采莓果 ×5 | +10 | | +| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 | +| T8–T10 | D4 | 与 T1–T3 逐字相同 | 同上 | 刻意恐怖谷 | +| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | | +| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) | +| T13 | D6 | 在广场挂彩带 ×4 | +10 | | +| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 | +| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 | + +## 8. 线索清单(封闭,9 处,每处觉知 +8) + +| ID | 位置/来源 | 内容 | 可发现时段 | +|---|---|---|---| +| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 | +| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 | +| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 | +| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 | +| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 | +| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5(T12 中) | +| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 | +| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 | +| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 | + +## 9. 结局(封闭,4 个) + +Day 7 庆典按以下优先级判定(自上而下第一个满足者生效): + +| 结局 | 条件 | 演出 | +|---|---|---| +| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 | +| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 | +| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 | +| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 | + +任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。 + +## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON) + +| 场景 | 尺寸(瓦片) | 要点 | +|---|---|---| +| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) | +| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 | +| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) | +| 杂货店(室内) | 15×10 | 货架(C5) | +| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) | +| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 | +| 花田 | 20×14 | 莓果丛(T6)、浇花点 | +| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 | +| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 | +| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 | + +统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。 + +## 11. 美术规范(Aseprite 制作,全部原创) + +- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。 +- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。 +- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。 +- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。 +- 禁止使用任何现成素材、素材包或开源 tileset。 + +## 12. 技术架构 + +- **Phaser 3 + TypeScript + Vite**,`npm run build` 产出纯静态 `dist/`,`npm run preview` 本地游玩。 +- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。 +- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`。 +- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。 +- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。 +- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。 +- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。 +- 无 localStorage、无外部网络请求、无外部音频/字体文件。 + +## 13. Debug 面板(服务验收,必须实现) + +按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。 + +## 14. 验收清单(共 20 条) + +1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。 +2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。 +3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。 +4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。 +5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。 +6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。 +7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。 +8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。 +9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。 +10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。 +11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。 +12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。 +13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。 +14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。 +15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。 +16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。 +17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。 +18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。 +19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。 +20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。 + +## 15. 明确不做 + +战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。 diff --git a/Grok/README.md b/Grok/README.md new file mode 100644 index 0000000..8f5f78c --- /dev/null +++ b/Grok/README.md @@ -0,0 +1,104 @@ +# 崩坏童话:蜜糖村 + +纯单机网页游戏(Phaser 3 + TypeScript + Vite)。无存档、无后端、无 localStorage;浏览器打开即玩。完整流程约 25–35 分钟,含 4 个结局。 + +## 启动方式 + +```bash +npm install +npm run dev # 开发 +npm run build # 产出静态 dist/ +npm run preview # 本地游玩构建结果 +npm test # 单元测试(PRD 数值/台词/矩阵) +``` + +请用 `npm run dev` 或 `npm run preview` 访问,不要直接用 `file://` 打开(ESM 模块加载需要 HTTP)。 + +## 按键 + +| 按键 | 作用 | +|------|------| +| WASD / 方向键 | 四向移动 | +| E / 空格 | 交互、推进对话 | +| 1 / 2 / 3 | 对话选项(温柔 / 迟疑 / 否定) | +| Enter | 确认名字;结局「回到标题」 | +| \` (反引号) | 开关 Debug 面板 | + +靠近可交互物(≤24 像素)会出现「按 E」提示。 + +## 游玩流程 + +1. 标题画面 → 输入名字(留空默认为「小满」)→ Day 1 小屋 +2. 每日:晨起 → 广场公告板接任务 → 完成任务 → 黄昏钟声 → 回屋睡觉 → 梦境 → 次日 +3. Day 7 参加「第 413 届丰收庆典」,按幸福 / 觉知 / 线索走向 E1–E4 之一 + +幸福值**不显示数字**:左上角向日葵 12 瓣按值点亮,满格后花心出现黑籽。 + +## Debug 面板(默认关闭) + +按 \` 打开。仅服务验收,正常游玩无 UI 痕迹。 + +| 键 | 功能 | +|----|------| +| H / J | 幸福 ±10 | +| K / L | 觉知 ±10(L 仅 Debug 可下调) | +| Shift+1–7 | 设置当前日(用 `code` Digit1–7,兼容浏览器 Shift 改写 `key` 为 !@#) | +| C | 置齐全部线索 | +| 0–9 | 传送到 10 个场景(0小屋 1广场 2面包房 3杂货店 4诊所 5湖畔 6花田 7古井 8雾墙 9白色层) | +| V | 循环传送下一场景 | +| F1–F4 | 强制结局 E1–E4 | +| N | 负片闪帧 | +| P | 触发欢乐庆典 | + +## 素材与地图制作流程(全部原创) + +本项目**禁止**使用任何现成素材包。像素图与地图均由仓库内脚本 + 本机工具生成: + +### Aseprite + +本机路径示例:`/Applications/Aseprite.app/Contents/MacOS/aseprite` + +```bash +export PATH="/Applications/Aseprite.app/Contents/MacOS:$PATH" +aseprite -b -script scripts/generate_tileset.lua # → public/assets/tilesets/village.png(128 瓦 16×16) +aseprite -b -script scripts/generate_sprites.lua # → player / npcs / birds / extras / portraits / sunflower +``` + +- 瓦片:草地、石板路、湖水、花丛、莓果、树、房屋、室内家具、井、雾、白色层等 +- 主角:16×24,4 朝向 × 4 帧行走;对话微笑头像 + E3「真实的脸」 +- NPC ×7:各 3 档表情 smile / flat / off(off 仅 1–2 像素差异) +- 双子鸟 2 帧张嘴;群演 ×5 + +源文件同时保存 `.aseprite` 与导出 `.png`。 + +### Tiled Map Editor + +本机路径示例:`/Applications/Tiled.app/Contents/MacOS/tiled` + +```bash +node scripts/generate-maps.mjs +# 可选:用 Tiled CLI 再导出校验 +tiled --export-map json mapsrc/house.tmx public/assets/maps/house_tiled_export.json +``` + +10 张地图(§10):小屋、村广场、面包房、杂货店、诊所、湖畔、花田、古井空地、雾墙边界、白色层。 +结构层:`ground` / `obstacles`(碰撞)/ `objects`(spawn、NPC、door、交互点)。 +源 TMX 在 `mapsrc/`,运行时 JSON 在 `public/assets/maps/`。 + +### 一键重生成 + +```bash +npm run assets # 若已配置 scripts/generate-assets.sh +node scripts/generate-maps.mjs +``` + +## 技术要点 + +- 逻辑分辨率 320×180,整数倍放大,`pixelArt: true`,无纹理平滑 +- `CorruptionDirector`:幸福 / 觉知 / 日 / 任务 / 线索 / 侵蚀阶段唯一真源 +- BGM 与 4 个音效均为 Web Audio 程序合成,无外部音频文件 +- 无 localStorage、无外部网络资源 / 字体 + +## 验收 + +PRD §14 共 20 条。单元测试覆盖台词、任务奖励、侵蚀矩阵、错字表、结局优先级、悲伤数值等。 diff --git a/Grok/index.html b/Grok/index.html new file mode 100644 index 0000000..1abbdef --- /dev/null +++ b/Grok/index.html @@ -0,0 +1,31 @@ + + + + + + 崩坏童话:蜜糖村 + + + +
+ + + diff --git a/Grok/mapsrc/bakery.tmx b/Grok/mapsrc/bakery.tmx new file mode 100644 index 0000000..0ab0813 --- /dev/null +++ b/Grok/mapsrc/bakery.tmx @@ -0,0 +1,25 @@ + + + + + +17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17 + + + + + + + +13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,20,20,20,20,20,0,0,0,43,0,21,0,13,13,0,20,20,20,20,20,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,23,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,13,13,13,13,13,0,0,13,13,13,13,13,13,13 + + + + + + + + + + + diff --git a/Grok/mapsrc/clinic.tmx b/Grok/mapsrc/clinic.tmx new file mode 100644 index 0000000..d3db7c0 --- /dev/null +++ b/Grok/mapsrc/clinic.tmx @@ -0,0 +1,23 @@ + + + + + +18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18 + + + + + + + +13,13,13,13,13,13,13,13,13,13,13,13,13,0,0,0,0,0,0,0,0,0,0,13,13,0,0,23,44,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,19,0,0,13,13,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,13,13,13,13,13,13,0,0,13,13,13,13,13 + + + + + + + + + diff --git a/Grok/mapsrc/field.tmx b/Grok/mapsrc/field.tmx new file mode 100644 index 0000000..7a8e924 --- /dev/null +++ b/Grok/mapsrc/field.tmx @@ -0,0 +1,28 @@ + + + + + +2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2 + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,9,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + diff --git a/Grok/mapsrc/fog.tmx b/Grok/mapsrc/fog.tmx new file mode 100644 index 0000000..4229386 --- /dev/null +++ b/Grok/mapsrc/fog.tmx @@ -0,0 +1,22 @@ + + + + + +2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26 + + + + + + + + diff --git a/Grok/mapsrc/house.tmx b/Grok/mapsrc/house.tmx new file mode 100644 index 0000000..259c979 --- /dev/null +++ b/Grok/mapsrc/house.tmx @@ -0,0 +1,24 @@ + + + + + +17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,18,18,17,17,17,17,17 + + + + + + + +12,12,12,12,12,12,12,12,12,12,12,12,12,0,0,0,0,0,0,0,0,0,0,12,12,0,19,19,0,0,0,0,0,29,0,12,12,0,0,0,0,0,0,0,0,0,0,12,12,0,0,0,0,23,0,0,0,0,0,12,12,24,0,0,0,0,0,0,0,0,0,12,12,0,0,0,0,0,0,0,0,0,0,12,12,12,12,12,12,0,0,12,12,12,12,12 + + + + + + + + + + diff --git a/Grok/mapsrc/lake.tmx b/Grok/mapsrc/lake.tmx new file mode 100644 index 0000000..788323d --- /dev/null +++ b/Grok/mapsrc/lake.tmx @@ -0,0 +1,25 @@ + + + + + +2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6 + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,24,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,24,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + diff --git a/Grok/mapsrc/plaza.tmx b/Grok/mapsrc/plaza.tmx new file mode 100644 index 0000000..97e231d --- /dev/null +++ b/Grok/mapsrc/plaza.tmx @@ -0,0 +1,48 @@ + + + + + +2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2 + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,14,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,30,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,0,0,0,0,0,0,0,34,0,0,0,0,34,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34,0,0,0,0,34,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Grok/mapsrc/shop.tmx b/Grok/mapsrc/shop.tmx new file mode 100644 index 0000000..26ed837 --- /dev/null +++ b/Grok/mapsrc/shop.tmx @@ -0,0 +1,23 @@ + + + + + +18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18 + + + + + + + +13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,22,22,22,22,22,22,22,22,22,22,22,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,22,22,22,22,22,22,22,22,22,22,22,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,13,13,13,13,13,13,0,0,13,13,13,13,13,13 + + + + + + + + + diff --git a/Grok/mapsrc/village.tsx b/Grok/mapsrc/village.tsx new file mode 100644 index 0000000..b0aab1a --- /dev/null +++ b/Grok/mapsrc/village.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/Grok/mapsrc/well.tmx b/Grok/mapsrc/well.tmx new file mode 100644 index 0000000..215fe9b --- /dev/null +++ b/Grok/mapsrc/well.tmx @@ -0,0 +1,23 @@ + + + + + +2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,25,0,0,0,0,0,0,0,0,0,0,0,0,25,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + diff --git a/Grok/mapsrc/white.tmx b/Grok/mapsrc/white.tmx new file mode 100644 index 0000000..0dc3b1e --- /dev/null +++ b/Grok/mapsrc/white.tmx @@ -0,0 +1,23 @@ + + + + + +28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28 + + + + + + + +27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,27,27,27,27,27,27,27,27,27,0,27,27,27,27,27,27,27,27,27,0,0,0,0,0,27,27,27,27,27,27,27,27,27,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,44,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,19,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,27,27,27,27,27,27,27,27,27,0,0,0,0,0 + + + + + + + + + diff --git a/Grok/package-lock.json b/Grok/package-lock.json new file mode 100644 index 0000000..b3bc13e --- /dev/null +++ b/Grok/package-lock.json @@ -0,0 +1,1724 @@ +{ + "name": "honey-village", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "honey-village", + "version": "1.0.0", + "dependencies": { + "phaser": "^3.87.0" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "playwright": "^1.61.1", + "typescript": "~5.7.2", + "vite": "^6.0.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/phaser": { + "version": "3.90.0", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", + "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/Grok/package.json b/Grok/package.json new file mode 100644 index 0000000..d9f3438 --- /dev/null +++ b/Grok/package.json @@ -0,0 +1,25 @@ +{ + "name": "honey-village", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "assets": "bash scripts/generate-assets.sh", + "maps": "bash scripts/generate-maps.sh" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "playwright": "^1.61.1", + "typescript": "~5.7.2", + "vite": "^6.0.0", + "vitest": "^3.0.0" + }, + "dependencies": { + "phaser": "^3.87.0" + } +} diff --git a/Grok/public/assets/maps/bakery.json b/Grok/public/assets/maps/bakery.json new file mode 100644 index 0000000..a4675f2 --- /dev/null +++ b/Grok/public/assets/maps/bakery.json @@ -0,0 +1,652 @@ +{ + "compressionlevel": -1, + "height": 10, + "width": 15, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 15, + "height": 10, + "data": [ + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 15, + "height": 10, + "data": [ + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 20, + 20, + 20, + 20, + 20, + 0, + 0, + 0, + 43, + 0, + 21, + 0, + 13, + 13, + 0, + 20, + 20, + 20, + 20, + 20, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 23, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 0, + 0, + 13, + 13, + 13, + 13, + 13, + 13, + 13 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 112, + "y": 112, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "yuanyuan", + "type": "npc", + "x": 64, + "y": 64, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "yuanyuan" + } + ] + }, + { + "id": 3, + "name": "to_plaza", + "type": "door", + "x": 112, + "y": 144, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_bakery" + } + ] + }, + { + "id": 4, + "name": "recipe", + "type": "interact", + "x": 160, + "y": 32, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "recipe" + } + ] + }, + { + "id": 5, + "name": "counter", + "type": "interact", + "x": 64, + "y": 48, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "bread" + } + ] + }, + { + "id": 6, + "name": "knead", + "type": "interact", + "x": 48, + "y": 48, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "knead" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 7, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/clinic.json b/Grok/public/assets/maps/clinic.json new file mode 100644 index 0000000..f2c3948 --- /dev/null +++ b/Grok/public/assets/maps/clinic.json @@ -0,0 +1,512 @@ +{ + "compressionlevel": -1, + "height": 8, + "width": 12, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 12, + "height": 8, + "data": [ + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 12, + "height": 8, + "data": [ + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 23, + 44, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 19, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 13, + 13, + 13, + 13, + 0, + 0, + 13, + 13, + 13, + 13, + 13 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 96, + "y": 80, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "fu", + "type": "npc", + "x": 48, + "y": 48, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "fu" + } + ] + }, + { + "id": 3, + "name": "to_plaza", + "type": "door", + "x": 96, + "y": 112, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_clinic" + } + ] + }, + { + "id": 4, + "name": "form", + "type": "interact", + "x": 64, + "y": 32, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "form" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 5, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/field.json b/Grok/public/assets/maps/field.json new file mode 100644 index 0000000..24c1e26 --- /dev/null +++ b/Grok/public/assets/maps/field.json @@ -0,0 +1,960 @@ +{ + "compressionlevel": -1, + "height": 14, + "width": 20, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 20, + "height": 14, + "data": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 20, + "height": 14, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 33, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 160, + "y": 176, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "extra2", + "type": "npc", + "x": 96, + "y": 128, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "extra2" + } + ] + }, + { + "id": 3, + "name": "to_plaza", + "type": "door", + "x": 160, + "y": 208, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_field" + } + ] + }, + { + "id": 4, + "name": "berry1", + "type": "interact", + "x": 48, + "y": 48, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "berry" + } + ] + }, + { + "id": 5, + "name": "berry2", + "type": "interact", + "x": 80, + "y": 64, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "berry" + } + ] + }, + { + "id": 6, + "name": "berry3", + "type": "interact", + "x": 112, + "y": 48, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "berry" + } + ] + }, + { + "id": 7, + "name": "berry4", + "type": "interact", + "x": 192, + "y": 80, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "berry" + } + ] + }, + { + "id": 8, + "name": "berry5", + "type": "interact", + "x": 224, + "y": 48, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "berry" + } + ] + }, + { + "id": 9, + "name": "pot", + "type": "interact", + "x": 160, + "y": 32, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "pot" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 10, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/fog.json b/Grok/public/assets/maps/fog.json new file mode 100644 index 0000000..9aa27f0 --- /dev/null +++ b/Grok/public/assets/maps/fog.json @@ -0,0 +1,624 @@ +{ + "compressionlevel": -1, + "height": 8, + "width": 20, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 20, + "height": 8, + "data": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 20, + "height": 8, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 26, + 26, + 26, + 26, + 26, + 26, + 26, + 26 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 48, + "y": 64, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "to_plaza", + "type": "door", + "x": 0, + "y": 64, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_fog" + } + ] + }, + { + "id": 3, + "name": "fogwall", + "type": "interact", + "x": 192, + "y": 64, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "fogwall" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 4, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/house.json b/Grok/public/assets/maps/house.json new file mode 100644 index 0000000..f5c0dc9 --- /dev/null +++ b/Grok/public/assets/maps/house.json @@ -0,0 +1,533 @@ +{ + "compressionlevel": -1, + "height": 8, + "width": 12, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 12, + "height": 8, + "data": [ + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 18, + 18, + 17, + 17, + 17, + 17, + 17 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 12, + "height": 8, + "data": [ + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 12, + 12, + 0, + 19, + 19, + 0, + 0, + 0, + 0, + 0, + 29, + 0, + 12, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 12, + 12, + 0, + 0, + 0, + 0, + 23, + 0, + 0, + 0, + 0, + 0, + 12, + 12, + 24, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 12, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 12, + 12, + 12, + 12, + 12, + 12, + 0, + 0, + 12, + 12, + 12, + 12, + 12 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 96, + "y": 80, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "to_plaza", + "type": "door", + "x": 80, + "y": 112, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_house" + } + ] + }, + { + "id": 3, + "name": "to_plaza2", + "type": "door", + "x": 96, + "y": 112, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_house" + } + ] + }, + { + "id": 4, + "name": "bed", + "type": "interact", + "x": 32, + "y": 32, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "bed" + } + ] + }, + { + "id": 5, + "name": "calendar", + "type": "interact", + "x": 144, + "y": 32, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "calendar" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 6, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/house_tiled_export.json b/Grok/public/assets/maps/house_tiled_export.json new file mode 100644 index 0000000..16e6f47 --- /dev/null +++ b/Grok/public/assets/maps/house_tiled_export.json @@ -0,0 +1,135 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 18, 18, 17, 17, 17, 17, 17], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "data":[12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, + 12, 0, 19, 19, 0, 0, 0, 0, 0, 29, 0, 12, + 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, + 12, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 12, + 12, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, + 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, + 12, 12, 12, 12, 12, 1, 1, 12, 12, 12, 12, 12], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"spawn", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":96, + "y":80 + }, + { + "height":16, + "id":2, + "name":"to_plaza", + "opacity":1, + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":80, + "y":112 + }, + { + "height":16, + "id":3, + "name":"to_plaza2", + "opacity":1, + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":96, + "y":112 + }, + { + "height":16, + "id":4, + "name":"bed", + "opacity":1, + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":32, + "y":32 + }, + { + "height":16, + "id":5, + "name":"calendar", + "opacity":1, + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":144, + "y":32 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":6, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "firstgid":1, + "source":"..\/..\/..\/mapsrc\/village.tsx" + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/Grok/public/assets/maps/lake.json b/Grok/public/assets/maps/lake.json new file mode 100644 index 0000000..adad223 --- /dev/null +++ b/Grok/public/assets/maps/lake.json @@ -0,0 +1,1120 @@ +{ + "compressionlevel": -1, + "height": 16, + "width": 24, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 24, + "height": 16, + "data": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 24, + "height": 16, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 24, + 24, + 24, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 24, + 24, + 24, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 32, + 32, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 176, + "y": 32, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "gui", + "type": "npc", + "x": 144, + "y": 80, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "gui" + } + ] + }, + { + "id": 3, + "name": "extra3", + "type": "npc", + "x": 288, + "y": 64, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "extra3" + } + ] + }, + { + "id": 4, + "name": "to_plaza", + "type": "door", + "x": 176, + "y": 0, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_lake" + } + ] + }, + { + "id": 5, + "name": "picnic", + "type": "interact", + "x": 192, + "y": 96, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "picnic" + } + ] + }, + { + "id": 6, + "name": "bench", + "type": "interact", + "x": 144, + "y": 64, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "bench" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 7, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/plaza.json b/Grok/public/assets/maps/plaza.json new file mode 100644 index 0000000..432fee4 --- /dev/null +++ b/Grok/public/assets/maps/plaza.json @@ -0,0 +1,1899 @@ +{ + "compressionlevel": -1, + "height": 20, + "width": 30, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 30, + "height": 20, + "data": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 30, + "height": 20, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 14, + 14, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 14, + 14, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 15, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 11, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 30, + 0, + 0, + 0, + 0, + 10, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 8, + 8, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 8, + 8, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 31, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 34, + 0, + 0, + 0, + 0, + 34, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 34, + 0, + 0, + 0, + 0, + 34, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 240, + "y": 192, + "width": 16, + "height": 16 + }, + { + "id": 100, + "name": "from_house", + "type": "spawn", + "x": 96, + "y": 80, + "width": 16, + "height": 16 + }, + { + "id": 101, + "name": "from_bakery", + "type": "spawn", + "x": 384, + "y": 144, + "width": 16, + "height": 16 + }, + { + "id": 102, + "name": "from_shop", + "type": "spawn", + "x": 48, + "y": 144, + "width": 16, + "height": 16 + }, + { + "id": 103, + "name": "from_clinic", + "type": "spawn", + "x": 64, + "y": 224, + "width": 16, + "height": 16 + }, + { + "id": 104, + "name": "from_lake", + "type": "spawn", + "x": 240, + "y": 272, + "width": 16, + "height": 16 + }, + { + "id": 105, + "name": "from_field", + "type": "spawn", + "x": 240, + "y": 32, + "width": 16, + "height": 16 + }, + { + "id": 106, + "name": "from_well", + "type": "spawn", + "x": 432, + "y": 160, + "width": 16, + "height": 16 + }, + { + "id": 107, + "name": "from_fog", + "type": "spawn", + "x": 32, + "y": 256, + "width": 16, + "height": 16 + }, + { + "id": 10, + "name": "afu", + "type": "npc", + "x": 240, + "y": 96, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "afu" + } + ] + }, + { + "id": 11, + "name": "dabao", + "type": "npc", + "x": 80, + "y": 176, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "dabao" + } + ] + }, + { + "id": 12, + "name": "twinbirds", + "type": "npc", + "x": 320, + "y": 48, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "twinbirds" + } + ] + }, + { + "id": 13, + "name": "extra1", + "type": "npc", + "x": 288, + "y": 224, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "extra1" + } + ] + }, + { + "id": 14, + "name": "extra4", + "type": "npc", + "x": 160, + "y": 192, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "extra4" + } + ] + }, + { + "id": 15, + "name": "house", + "type": "door", + "x": 96, + "y": 48, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "HouseScene" + }, + { + "name": "spawn", + "type": "string", + "value": "spawn" + } + ] + }, + { + "id": 16, + "name": "bakery", + "type": "door", + "x": 400, + "y": 144, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "BakeryScene" + }, + { + "name": "spawn", + "type": "string", + "value": "spawn" + } + ] + }, + { + "id": 17, + "name": "shop", + "type": "door", + "x": 32, + "y": 144, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "ShopScene" + }, + { + "name": "spawn", + "type": "string", + "value": "spawn" + } + ] + }, + { + "id": 18, + "name": "clinic", + "type": "door", + "x": 64, + "y": 240, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "ClinicScene" + }, + { + "name": "spawn", + "type": "string", + "value": "spawn" + } + ] + }, + { + "id": 19, + "name": "lake", + "type": "door", + "x": 240, + "y": 304, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "LakeScene" + }, + { + "name": "spawn", + "type": "string", + "value": "spawn" + } + ] + }, + { + "id": 20, + "name": "field", + "type": "door", + "x": 240, + "y": 0, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "FieldScene" + }, + { + "name": "spawn", + "type": "string", + "value": "spawn" + } + ] + }, + { + "id": 21, + "name": "well", + "type": "door", + "x": 464, + "y": 160, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "WellScene" + }, + { + "name": "spawn", + "type": "string", + "value": "spawn" + } + ] + }, + { + "id": 22, + "name": "fog", + "type": "door", + "x": 0, + "y": 256, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "FogScene" + }, + { + "name": "spawn", + "type": "string", + "value": "spawn" + } + ] + }, + { + "id": 23, + "name": "board", + "type": "interact", + "x": 240, + "y": 80, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "board" + } + ] + }, + { + "id": 24, + "name": "flowers", + "type": "interact", + "x": 176, + "y": 96, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "flowers" + } + ] + }, + { + "id": 25, + "name": "mailbox", + "type": "interact", + "x": 64, + "y": 160, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "mailbox" + } + ] + }, + { + "id": 26, + "name": "ribbon1", + "type": "interact", + "x": 192, + "y": 160, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "ribbon" + } + ] + }, + { + "id": 27, + "name": "ribbon2", + "type": "interact", + "x": 272, + "y": 160, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "ribbon" + } + ] + }, + { + "id": 28, + "name": "ribbon3", + "type": "interact", + "x": 192, + "y": 208, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "ribbon" + } + ] + }, + { + "id": 29, + "name": "ribbon4", + "type": "interact", + "x": 272, + "y": 208, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "ribbon" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 30, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/shop.json b/Grok/public/assets/maps/shop.json new file mode 100644 index 0000000..2df415a --- /dev/null +++ b/Grok/public/assets/maps/shop.json @@ -0,0 +1,620 @@ +{ + "compressionlevel": -1, + "height": 10, + "width": 15, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 15, + "height": 10, + "data": [ + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 15, + "height": 10, + "data": [ + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 13, + 0, + 0, + 13, + 13, + 13, + 13, + 13, + 13 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 112, + "y": 112, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "mimi", + "type": "npc", + "x": 112, + "y": 80, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "mimi" + } + ] + }, + { + "id": 3, + "name": "to_plaza", + "type": "door", + "x": 112, + "y": 144, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_shop" + } + ] + }, + { + "id": 4, + "name": "shelf", + "type": "interact", + "x": 80, + "y": 32, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "shelf" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 5, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/well.json b/Grok/public/assets/maps/well.json new file mode 100644 index 0000000..b6dcb32 --- /dev/null +++ b/Grok/public/assets/maps/well.json @@ -0,0 +1,600 @@ +{ + "compressionlevel": -1, + "height": 10, + "width": 14, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 14, + "height": 10, + "data": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 14, + "height": 10, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 25, + 25, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 25, + 25, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 32, + "y": 80, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "extra5", + "type": "npc", + "x": 160, + "y": 112, + "width": 16, + "height": 16, + "properties": [ + { + "name": "npcId", + "type": "string", + "value": "extra5" + } + ] + }, + { + "id": 3, + "name": "to_plaza", + "type": "door", + "x": 0, + "y": 80, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "PlazaScene" + }, + { + "name": "spawn", + "type": "string", + "value": "from_well" + } + ] + }, + { + "id": 4, + "name": "well", + "type": "interact", + "x": 112, + "y": 64, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "well" + } + ] + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 5, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/maps/white.json b/Grok/public/assets/maps/white.json new file mode 100644 index 0000000..fbe418b --- /dev/null +++ b/Grok/public/assets/maps/white.json @@ -0,0 +1,873 @@ +{ + "compressionlevel": -1, + "height": 14, + "width": 20, + "infinite": false, + "layers": [ + { + "id": 1, + "name": "ground", + "type": "tilelayer", + "width": 20, + "height": 14, + "data": [ + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28, + 28 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + }, + { + "id": 2, + "name": "obstacles", + "type": "tilelayer", + "width": 20, + "height": 14, + "data": [ + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 0, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 0, + 0, + 0, + 0, + 0, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 44, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 19, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 27, + 0, + 0, + 0, + 0, + 0 + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 3, + "name": "objects", + "type": "objectgroup", + "objects": [ + { + "id": 1, + "name": "spawn", + "type": "spawn", + "x": 32, + "y": 48, + "width": 16, + "height": 16 + }, + { + "id": 2, + "name": "record", + "type": "interact", + "x": 176, + "y": 128, + "width": 16, + "height": 16, + "properties": [ + { + "name": "interactId", + "type": "string", + "value": "record" + } + ] + }, + { + "id": 3, + "name": "to_ward", + "type": "door", + "x": 160, + "y": 80, + "width": 16, + "height": 16, + "properties": [ + { + "name": "target", + "type": "string", + "value": "WhiteScene" + }, + { + "name": "spawn", + "type": "string", + "value": "ward" + } + ] + }, + { + "id": 200, + "name": "ward", + "type": "spawn", + "x": 160, + "y": 128, + "width": 16, + "height": 16 + } + ], + "opacity": 1, + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 4, + "nextobjectid": 5, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "1.12.2", + "tileheight": 16, + "tilewidth": 16, + "type": "map", + "version": "1.10", + "tilesets": [ + { + "firstgid": 1, + "columns": 16, + "image": "../tilesets/village.png", + "imagewidth": 256, + "imageheight": 128, + "tilecount": 128, + "tilewidth": 16, + "tileheight": 16, + "name": "village", + "margin": 0, + "spacing": 0, + "tiles": [ + { + "id": 9, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 10, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 11, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 12, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 13, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 14, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 15, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 19, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 20, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 21, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 22, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 24, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 26, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 29, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 30, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 35, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 41, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + }, + { + "id": 46, + "properties": [ + { + "name": "collides", + "type": "bool", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Grok/public/assets/sprites/birds.aseprite b/Grok/public/assets/sprites/birds.aseprite new file mode 100644 index 0000000..c98f383 Binary files /dev/null and b/Grok/public/assets/sprites/birds.aseprite differ diff --git a/Grok/public/assets/sprites/birds.png b/Grok/public/assets/sprites/birds.png new file mode 100644 index 0000000..aea40c1 Binary files /dev/null and b/Grok/public/assets/sprites/birds.png differ diff --git a/Grok/public/assets/sprites/dialog_panel.aseprite b/Grok/public/assets/sprites/dialog_panel.aseprite new file mode 100644 index 0000000..4200513 Binary files /dev/null and b/Grok/public/assets/sprites/dialog_panel.aseprite differ diff --git a/Grok/public/assets/sprites/dialog_panel.png b/Grok/public/assets/sprites/dialog_panel.png new file mode 100644 index 0000000..c1281f5 Binary files /dev/null and b/Grok/public/assets/sprites/dialog_panel.png differ diff --git a/Grok/public/assets/sprites/extras.aseprite b/Grok/public/assets/sprites/extras.aseprite new file mode 100644 index 0000000..0c42367 Binary files /dev/null and b/Grok/public/assets/sprites/extras.aseprite differ diff --git a/Grok/public/assets/sprites/extras.png b/Grok/public/assets/sprites/extras.png new file mode 100644 index 0000000..e12a867 Binary files /dev/null and b/Grok/public/assets/sprites/extras.png differ diff --git a/Grok/public/assets/sprites/npcs.aseprite b/Grok/public/assets/sprites/npcs.aseprite new file mode 100644 index 0000000..f052a33 Binary files /dev/null and b/Grok/public/assets/sprites/npcs.aseprite differ diff --git a/Grok/public/assets/sprites/npcs.png b/Grok/public/assets/sprites/npcs.png new file mode 100644 index 0000000..3221c9f Binary files /dev/null and b/Grok/public/assets/sprites/npcs.png differ diff --git a/Grok/public/assets/sprites/player.aseprite b/Grok/public/assets/sprites/player.aseprite new file mode 100644 index 0000000..5aaecf3 Binary files /dev/null and b/Grok/public/assets/sprites/player.aseprite differ diff --git a/Grok/public/assets/sprites/player.png b/Grok/public/assets/sprites/player.png new file mode 100644 index 0000000..80a2750 Binary files /dev/null and b/Grok/public/assets/sprites/player.png differ diff --git a/Grok/public/assets/sprites/portraits.aseprite b/Grok/public/assets/sprites/portraits.aseprite new file mode 100644 index 0000000..b257e65 Binary files /dev/null and b/Grok/public/assets/sprites/portraits.aseprite differ diff --git a/Grok/public/assets/sprites/portraits.png b/Grok/public/assets/sprites/portraits.png new file mode 100644 index 0000000..ff6110c Binary files /dev/null and b/Grok/public/assets/sprites/portraits.png differ diff --git a/Grok/public/assets/sprites/sunflower.aseprite b/Grok/public/assets/sprites/sunflower.aseprite new file mode 100644 index 0000000..a633704 Binary files /dev/null and b/Grok/public/assets/sprites/sunflower.aseprite differ diff --git a/Grok/public/assets/sprites/sunflower.png b/Grok/public/assets/sprites/sunflower.png new file mode 100644 index 0000000..29c324f Binary files /dev/null and b/Grok/public/assets/sprites/sunflower.png differ diff --git a/Grok/public/assets/tilesets/village.aseprite b/Grok/public/assets/tilesets/village.aseprite new file mode 100644 index 0000000..1adc3e6 Binary files /dev/null and b/Grok/public/assets/tilesets/village.aseprite differ diff --git a/Grok/public/assets/tilesets/village.png b/Grok/public/assets/tilesets/village.png new file mode 100644 index 0000000..0c299fe Binary files /dev/null and b/Grok/public/assets/tilesets/village.png differ diff --git a/Grok/scripts/generate-assets.sh b/Grok/scripts/generate-assets.sh new file mode 100755 index 0000000..af4dbee --- /dev/null +++ b/Grok/scripts/generate-assets.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export PATH="/Applications/Aseprite.app/Contents/MacOS:/Applications/Tiled.app/Contents/MacOS:${PATH}" +cd "$ROOT" +mkdir -p public/assets/{tilesets,sprites,maps} mapsrc +aseprite -b -script scripts/generate_tileset.lua +aseprite -b -script scripts/generate_sprites.lua +echo "Aseprite assets done." diff --git a/Grok/scripts/generate-maps.mjs b/Grok/scripts/generate-maps.mjs new file mode 100644 index 0000000..9afbc1f --- /dev/null +++ b/Grok/scripts/generate-maps.mjs @@ -0,0 +1,647 @@ +/** + * Generate 10 Tiled-compatible JSON maps per PRD §10. + * Also writes .tmx for optional `tiled --export-map` round-trip. + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const outDir = path.join(__dirname, '../public/assets/maps'); +const tmxDir = path.join(__dirname, '../mapsrc'); +fs.mkdirSync(outDir, { recursive: true }); +fs.mkdirSync(tmxDir, { recursive: true }); + +const TILE = 16; + +const TILESET = { + firstgid: 1, + columns: 16, + image: '../tilesets/village.png', + imagewidth: 256, + imageheight: 128, + tilecount: 128, + tilewidth: 16, + tileheight: 16, + name: 'village', + margin: 0, + spacing: 0, + tiles: [ + // collidable tiles + ...[9, 10, 11, 12, 13, 14, 15, 19, 20, 21, 22, 24, 26, 29, 30, 35, 41, 46].map((id) => ({ + id, + properties: [{ name: 'collides', type: 'bool', value: true }], + })), + ], +}; + +function emptyGrid(w, h, fill = 0) { + return Array.from({ length: w * h }, () => fill); +} + +function set(data, w, x, y, v) { + if (x < 0 || y < 0 || x >= w) return; + data[y * w + x] = v; +} + +function fillRect(data, w, x0, y0, x1, y1, v) { + for (let y = y0; y <= y1; y++) { + for (let x = x0; x <= x1; x++) set(data, w, x, y, v); + } +} + +function border(data, w, h, v) { + for (let x = 0; x < w; x++) { + set(data, w, x, 0, v); + set(data, w, x, h - 1, v); + } + for (let y = 0; y < h; y++) { + set(data, w, 0, y, v); + set(data, w, w - 1, y, v); + } +} + +function makeMap(name, w, h, build) { + const ground = emptyGrid(w, h, 1); // grass gid 1 (tile index 0 is empty, firstgid=1 → tile 0 empty, tile1 grass) + // Our tileset: tile index in image 0 = empty transparent; index 1 = grass + // firstgid 1 means GID 1 = tile index 0. Let's use firstgid 1 where GID N = image tile (N-1) + // Simpler: firstgid=1, and we store image-tile-index+1 as GID. + // grass is image tile 1 → GID 2 + for (let i = 0; i < ground.length; i++) ground[i] = 2; // grass + + const obstacles = emptyGrid(w, h, 0); + const objects = []; + + const api = { + w, + h, + ground, + obstacles, + objects, + // GID 0 = empty; image tile index N → GID N+1. Passing 0 clears (no collision). + setG: (x, y, imgTile) => set(ground, w, x, y, imgTile <= 0 ? 0 : imgTile + 1), + setO: (x, y, imgTile) => set(obstacles, w, x, y, imgTile <= 0 ? 0 : imgTile + 1), + clearO: (x, y) => set(obstacles, w, x, y, 0), + fillG: (x0, y0, x1, y1, imgTile) => + fillRect(ground, w, x0, y0, x1, y1, imgTile <= 0 ? 0 : imgTile + 1), + fillO: (x0, y0, x1, y1, imgTile) => + fillRect(obstacles, w, x0, y0, x1, y1, imgTile <= 0 ? 0 : imgTile + 1), + borderO: (imgTile) => { + const gid = imgTile <= 0 ? 0 : imgTile + 1; + for (let x = 0; x < w; x++) { + set(obstacles, w, x, 0, gid); + set(obstacles, w, x, h - 1, gid); + } + for (let y = 0; y < h; y++) { + set(obstacles, w, 0, y, gid); + set(obstacles, w, w - 1, y, gid); + } + }, + spawn: (x, y) => { + objects.push({ + id: objects.length + 1, + name: 'spawn', + type: 'spawn', + x: x * TILE, + y: y * TILE, + width: TILE, + height: TILE, + }); + }, + npc: (name, x, y) => { + objects.push({ + id: objects.length + 1, + name, + type: 'npc', + x: x * TILE, + y: y * TILE, + width: TILE, + height: TILE, + properties: [{ name: 'npcId', type: 'string', value: name }], + }); + }, + door: (name, x, y, target, targetSpawn) => { + // Door tiles must be walkable (clear obstacle collision) + set(obstacles, w, x, y, 0); + objects.push({ + id: objects.length + 1, + name, + type: 'door', + x: x * TILE, + y: y * TILE, + width: TILE, + height: TILE, + properties: [ + { name: 'target', type: 'string', value: target }, + { name: 'spawn', type: 'string', value: targetSpawn || 'spawn' }, + ], + }); + }, + interact: (name, x, y, interactId) => { + objects.push({ + id: objects.length + 1, + name, + type: 'interact', + x: x * TILE, + y: y * TILE, + width: TILE, + height: TILE, + properties: [{ name: 'interactId', type: 'string', value: interactId }], + }); + }, + }; + + build(api); + + const map = { + compressionlevel: -1, + height: h, + width: w, + infinite: false, + layers: [ + { + id: 1, + name: 'ground', + type: 'tilelayer', + width: w, + height: h, + data: ground, + opacity: 1, + visible: true, + x: 0, + y: 0, + }, + { + id: 2, + name: 'obstacles', + type: 'tilelayer', + width: w, + height: h, + data: obstacles, + opacity: 1, + visible: true, + x: 0, + y: 0, + properties: [{ name: 'collides', type: 'bool', value: true }], + }, + { + id: 3, + name: 'objects', + type: 'objectgroup', + objects, + opacity: 1, + visible: true, + x: 0, + y: 0, + }, + ], + nextlayerid: 4, + nextobjectid: objects.length + 1, + orientation: 'orthogonal', + renderorder: 'right-down', + tiledversion: '1.12.2', + tileheight: TILE, + tilewidth: TILE, + type: 'map', + version: '1.10', + tilesets: [ + { + ...TILESET, + // embed relative path for Phaser + }, + ], + }; + + const jsonPath = path.join(outDir, `${name}.json`); + fs.writeFileSync(jsonPath, JSON.stringify(map, null, 2)); + console.log('wrote', jsonPath); + + // Minimal TMX for Tiled CLI export verification + const tmx = ` + + + + +${ground.join(',')} + + + + + + + +${obstacles.join(',')} + + + +${objects + .map( + (o) => + ` `, + ) + .join('\n')} + + +`; + fs.writeFileSync(path.join(tmxDir, `${name}.tmx`), tmx); + return map; +} + +// TSX tileset for Tiled +fs.writeFileSync( + path.join(tmxDir, 'village.tsx'), + ` + + + +`, +); + +// --- Maps --- + +// 1. House 12x8 +makeMap('house', 12, 8, (m) => { + m.fillG(0, 0, 11, 7, 16); // wood floor + m.borderO(11); + m.setO(2, 2, 18); // bed + m.setO(3, 2, 18); + m.setO(9, 2, 28); // calendar + m.setO(5, 4, 22); // table + m.setO(1, 5, 23); // chair + // clear door at bottom + m.setO(5, 7, 0); + m.setO(6, 7, 0); + m.setG(5, 7, 17); + m.setG(6, 7, 17); + m.spawn(6, 5); + m.door('to_plaza', 5, 7, 'PlazaScene', 'from_house'); + m.door('to_plaza2', 6, 7, 'PlazaScene', 'from_house'); + m.interact('bed', 2, 2, 'bed'); + m.interact('calendar', 9, 2, 'calendar'); +}); + +// 2. Plaza 30x20 hub +makeMap('plaza', 30, 20, (m) => { + m.fillG(0, 0, 29, 19, 1); + // path cross + m.fillG(13, 0, 16, 19, 3); + m.fillG(0, 8, 29, 11, 3); + // flower beds + m.fillO(10, 6, 12, 7, 12 + 0); // use flower tile 7 + m.setO(10, 6, 7); + m.setO(11, 6, 7); + m.setO(12, 6, 7); + m.setO(10, 7, 7); + m.setO(11, 7, 7); + m.setO(12, 7, 7); + // board + m.setO(15, 5, 29); + // tree + birds + m.setO(20, 4, 10); + m.setO(20, 5, 9); + m.setO(21, 4, 10); + // mailbox + m.setO(4, 10, 30); + // houses facades as doors targets + m.setO(5, 2, 13); + m.setO(6, 2, 13); + m.setO(5, 3, 12); + m.setO(6, 3, 14); // door visual + m.setO(22, 2, 13); + m.setO(23, 2, 13); + m.setO(22, 3, 12); + m.setO(23, 3, 14); + m.setO(8, 14, 13); + m.setO(9, 14, 13); + m.setO(8, 15, 12); + m.setO(9, 15, 14); + // walls edge soft + for (let x = 0; x < 30; x++) { + m.setO(x, 0, 35); + m.setO(x, 19, 35); + } + // openings + m.setO(14, 0, 0); + m.setO(15, 0, 0); + m.setO(14, 19, 0); + m.setO(15, 19, 0); + m.setO(0, 9, 0); + m.setO(0, 10, 0); + m.setO(29, 9, 0); + m.setO(29, 10, 0); + + m.spawn(15, 12); + m.npc('afu', 15, 6); + m.npc('dabao', 5, 11); + m.npc('twinbirds', 20, 3); + m.npc('extra1', 18, 14); + m.npc('extra4', 10, 12); + + m.door('from_house', 6, 4, 'HouseScene', 'spawn'); // visual marker only when entering - actual door below + // Fix: doors as interact targets + // Re-define doors properly + m.objects.length = m.objects.filter((o) => o.type !== 'door').length; // no, just add +}); + +// Fix plaza - rebuild cleanly +function rebuildPlaza() { + return makeMap('plaza', 30, 20, (m) => { + m.fillG(0, 0, 29, 19, 1); + m.fillG(13, 0, 16, 19, 3); + m.fillG(0, 8, 29, 11, 3); + // flower bed tiles + for (const [x, y] of [ + [10, 6], + [11, 6], + [12, 6], + [10, 7], + [11, 7], + [12, 7], + ]) { + m.setO(x, y, 7); + } + m.setO(15, 5, 29); // board + m.setO(20, 4, 10); + m.setO(20, 5, 9); + m.setO(4, 10, 30); // mailbox + // house doors on north + m.setO(5, 2, 13); + m.setO(6, 2, 13); + m.setO(5, 3, 12); + m.setO(6, 3, 14); + m.setO(22, 2, 13); + m.setO(23, 2, 13); + m.setO(22, 3, 12); + m.setO(23, 3, 14); + // clinic south-west + m.setO(3, 15, 12); + m.setO(4, 15, 14); + // bakery east-ish + m.setO(25, 8, 12); + m.setO(25, 9, 14); + // shop west + m.setO(2, 8, 12); + m.setO(2, 9, 14); + // ribbon posts + m.setO(12, 10, 33); + m.setO(17, 10, 33); + m.setO(12, 13, 33); + m.setO(17, 13, 33); + + m.spawn(15, 12); + // named entry spawns as objects + m.objects.push({ + id: 100, + name: 'from_house', + type: 'spawn', + x: 6 * 16, + y: 5 * 16, + width: 16, + height: 16, + }); + m.objects.push({ + id: 101, + name: 'from_bakery', + type: 'spawn', + x: 24 * 16, + y: 9 * 16, + width: 16, + height: 16, + }); + m.objects.push({ + id: 102, + name: 'from_shop', + type: 'spawn', + x: 3 * 16, + y: 9 * 16, + width: 16, + height: 16, + }); + m.objects.push({ + id: 103, + name: 'from_clinic', + type: 'spawn', + x: 4 * 16, + y: 14 * 16, + width: 16, + height: 16, + }); + m.objects.push({ + id: 104, + name: 'from_lake', + type: 'spawn', + x: 15 * 16, + y: 17 * 16, + width: 16, + height: 16, + }); + m.objects.push({ + id: 105, + name: 'from_field', + type: 'spawn', + x: 15 * 16, + y: 2 * 16, + width: 16, + height: 16, + }); + m.objects.push({ + id: 106, + name: 'from_well', + type: 'spawn', + x: 27 * 16, + y: 10 * 16, + width: 16, + height: 16, + }); + m.objects.push({ + id: 107, + name: 'from_fog', + type: 'spawn', + x: 2 * 16, + y: 16 * 16, + width: 16, + height: 16, + }); + + m.npc('afu', 15, 6); + m.npc('dabao', 5, 11); + m.npc('twinbirds', 20, 3); + m.npc('extra1', 18, 14); + m.npc('extra4', 10, 12); + + m.door('house', 6, 3, 'HouseScene', 'spawn'); + m.door('bakery', 25, 9, 'BakeryScene', 'spawn'); + m.door('shop', 2, 9, 'ShopScene', 'spawn'); + m.door('clinic', 4, 15, 'ClinicScene', 'spawn'); + m.door('lake', 15, 19, 'LakeScene', 'spawn'); + m.door('field', 15, 0, 'FieldScene', 'spawn'); + m.door('well', 29, 10, 'WellScene', 'spawn'); + m.door('fog', 0, 16, 'FogScene', 'spawn'); + + m.interact('board', 15, 5, 'board'); + m.interact('flowers', 11, 6, 'flowers'); + m.interact('mailbox', 4, 10, 'mailbox'); + m.interact('ribbon1', 12, 10, 'ribbon'); + m.interact('ribbon2', 17, 10, 'ribbon'); + m.interact('ribbon3', 12, 13, 'ribbon'); + m.interact('ribbon4', 17, 13, 'ribbon'); + }); +} +rebuildPlaza(); + +// 3. Bakery 15x10 +makeMap('bakery', 15, 10, (m) => { + m.fillG(0, 0, 14, 9, 16); + m.borderO(12); + m.fillO(2, 2, 6, 3, 19); // counter + m.setO(12, 2, 20); // oven + m.setO(10, 2, 42); // recipe paper + m.setO(4, 6, 22); + m.setO(5, 7, 0); + m.setO(6, 9, 0); + m.setO(7, 9, 0); + m.spawn(7, 7); + m.npc('yuanyuan', 4, 4); + m.door('to_plaza', 7, 9, 'PlazaScene', 'from_bakery'); + m.interact('recipe', 10, 2, 'recipe'); + m.interact('counter', 4, 3, 'bread'); + m.interact('knead', 3, 3, 'knead'); +}); + +// 4. Shop 15x10 +makeMap('shop', 15, 10, (m) => { + m.fillG(0, 0, 14, 9, 17); + m.borderO(12); + for (let x = 2; x <= 12; x++) { + m.setO(x, 2, 21); + m.setO(x, 4, 21); + } + m.setO(7, 9, 0); + m.setO(8, 9, 0); + m.spawn(7, 7); + m.npc('mimi', 7, 5); + m.door('to_plaza', 7, 9, 'PlazaScene', 'from_shop'); + m.interact('shelf', 5, 2, 'shelf'); +}); + +// 5. Clinic 12x8 +makeMap('clinic', 12, 8, (m) => { + m.fillG(0, 0, 11, 7, 17); + m.borderO(12); + m.setO(3, 2, 22); + m.setO(4, 2, 43); // form + m.setO(8, 3, 18); + m.setO(5, 7, 0); + m.setO(6, 7, 0); + m.spawn(6, 5); + m.npc('fu', 3, 3); + m.door('to_plaza', 6, 7, 'PlazaScene', 'from_clinic'); + m.interact('form', 4, 2, 'form'); +}); + +// 6. Lake 24x16 +makeMap('lake', 24, 16, (m) => { + m.fillG(0, 0, 23, 15, 1); + m.fillG(0, 8, 23, 15, 5); // water + m.fillG(0, 8, 23, 8, 6); + m.fillO(8, 4, 10, 5, 23); // bench area chairs + m.setO(9, 4, 23); + m.setO(12, 6, 31); // picnic + m.setO(13, 6, 31); + // path from north + m.fillG(11, 0, 12, 7, 3); + m.setO(11, 0, 0); + m.setO(12, 0, 0); + m.spawn(11, 2); + m.npc('gui', 9, 5); + m.npc('extra3', 18, 4); + m.door('to_plaza', 11, 0, 'PlazaScene', 'from_lake'); + m.interact('picnic', 12, 6, 'picnic'); + m.interact('bench', 9, 4, 'bench'); +}); + +// 7. Field 20x14 +makeMap('field', 20, 14, (m) => { + m.fillG(0, 0, 19, 13, 1); + m.fillG(9, 10, 10, 13, 3); + for (const [x, y] of [ + [3, 3], + [5, 4], + [7, 3], + [12, 5], + [14, 3], + [4, 7], + [15, 7], + [8, 6], + ]) { + m.setO(x, y, 8); // berry + } + m.setO(10, 2, 32); // pot + m.spawn(10, 11); + m.npc('extra2', 6, 8); + m.door('to_plaza', 10, 13, 'PlazaScene', 'from_field'); + m.interact('berry1', 3, 3, 'berry'); + m.interact('berry2', 5, 4, 'berry'); + m.interact('berry3', 7, 3, 'berry'); + m.interact('berry4', 12, 5, 'berry'); + m.interact('berry5', 14, 3, 'berry'); + m.interact('pot', 10, 2, 'pot'); +}); + +// 8. Well 14x10 +makeMap('well', 14, 10, (m) => { + m.fillG(0, 0, 13, 9, 1); + m.fillG(0, 4, 3, 5, 3); + m.setO(7, 4, 24); // well + m.setO(6, 4, 24); + m.setO(7, 5, 24); + m.setO(6, 5, 24); + m.spawn(2, 5); + m.npc('extra5', 10, 7); + m.door('to_plaza', 0, 5, 'PlazaScene', 'from_well'); + m.interact('well', 7, 4, 'well'); +}); + +// 9. Fog 20x8 +makeMap('fog', 20, 8, (m) => { + m.fillG(0, 0, 19, 7, 1); + m.fillG(0, 3, 5, 4, 3); + for (let x = 12; x < 20; x++) { + for (let y = 0; y < 8; y++) m.setO(x, y, 25); + } + m.spawn(3, 4); + m.door('to_plaza', 0, 4, 'PlazaScene', 'from_fog'); + m.interact('fogwall', 12, 4, 'fogwall'); +}); + +// 10. White layer corridor 20x6 + ward via objects +makeMap('white', 20, 14, (m) => { + m.fillG(0, 0, 19, 13, 27); + m.fillO(0, 0, 19, 0, 26); + m.fillO(0, 5, 19, 5, 26); + m.fillO(0, 0, 0, 5, 26); + m.fillO(19, 0, 19, 5, 26); + // ward below + m.fillG(5, 6, 14, 13, 27); + m.fillO(5, 6, 14, 6, 26); + m.fillO(5, 13, 14, 13, 26); + m.fillO(5, 6, 5, 13, 26); + m.fillO(14, 6, 14, 13, 26); + m.setO(10, 5, 0); // opening + m.setO(9, 9, 18); // bed + m.setO(11, 8, 43); + m.spawn(2, 3); + m.interact('record', 11, 8, 'record'); + m.door('to_ward', 10, 5, 'WhiteScene', 'ward'); + m.objects.push({ + id: 200, + name: 'ward', + type: 'spawn', + x: 10 * 16, + y: 8 * 16, + width: 16, + height: 16, + }); +}); + +console.log('All 10 maps generated.'); diff --git a/Grok/scripts/generate-maps.sh b/Grok/scripts/generate-maps.sh new file mode 100755 index 0000000..b8828bb --- /dev/null +++ b/Grok/scripts/generate-maps.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export PATH="/Applications/Tiled.app/Contents/MacOS:${PATH}" +cd "$ROOT" +node scripts/generate-maps.mjs +# Round-trip one map through Tiled CLI to prove toolchain +if command -v tiled >/dev/null 2>&1; then + tiled --export-map json mapsrc/plaza.tmx public/assets/maps/plaza_tiled_export.json || true +fi +echo "Maps done." diff --git a/Grok/scripts/generate_sprites.lua b/Grok/scripts/generate_sprites.lua new file mode 100644 index 0000000..41e91d6 --- /dev/null +++ b/Grok/scripts/generate_sprites.lua @@ -0,0 +1,215 @@ +-- Aseprite Lua: player + NPCs + portraits + extras +-- Run: aseprite -b -script scripts/generate_sprites.lua + +local function newSprite(w, h) + local s = Sprite(w, h, ColorMode.RGB) + app.activeSprite = s + return s +end + +local function clear(img, w, h) + for y = 0, h - 1 do + for x = 0, w - 1 do + img:drawPixel(x, y, app.pixelColor.rgba(0, 0, 0, 0)) + end + end +end + +local function P(img, x, y, r, g, b, a) + a = a or 255 + if x < 0 or y < 0 then return end + img:drawPixel(x, y, app.pixelColor.rgba(r, g, b, a)) +end + +local function fillRect(img, x0, y0, x1, y1, r, g, b) + for y = y0, y1 do + for x = x0, x1 do + P(img, x, y, r, g, b) + end + end +end + +-- Draw a 16x24 character into img at offset (ox, oy) +-- face: 0 smile, 1 flat, 2 off (eye lower 1px) +local function drawChar(img, ox, oy, bodyR, bodyG, bodyB, headR, headG, headB, face, walkFrame, dir) + -- shadow + fillRect(img, ox + 3, oy + 22, ox + 12, oy + 23, 0, 0, 0) + for x = ox + 3, ox + 12 do + P(img, x, oy + 22, 0, 0, 0, 50) + P(img, x, oy + 23, 0, 0, 0, 30) + end + -- legs + local legOff = 0 + if walkFrame == 1 then legOff = 1 end + if walkFrame == 3 then legOff = -1 end + fillRect(img, ox + 4, oy + 16, ox + 6, oy + 21, 60, 50, 80) + fillRect(img, ox + 9, oy + 16, ox + 11, oy + 21, 60, 50, 80) + if walkFrame == 1 then + fillRect(img, ox + 4, oy + 16, ox + 6, oy + 21, 0, 0, 0) + fillRect(img, ox + 3, oy + 16, ox + 5, oy + 21, 60, 50, 80) + elseif walkFrame == 3 then + fillRect(img, ox + 9, oy + 16, ox + 11, oy + 21, 0, 0, 0) + fillRect(img, ox + 10, oy + 16, ox + 12, oy + 21, 60, 50, 80) + end + -- body + fillRect(img, ox + 3, oy + 10, ox + 12, oy + 17, bodyR, bodyG, bodyB) + -- head + fillRect(img, ox + 3, oy + 2, ox + 12, oy + 10, headR, headG, headB) + -- ears/species hint for animals - small top nubs + if headR > 200 and headG > 180 then + -- rabbit ears + fillRect(img, ox + 4, oy + 0, ox + 5, oy + 3, headR, headG, headB) + fillRect(img, ox + 10, oy + 0, ox + 11, oy + 3, headR, headG, headB) + end + -- face + local eyeY = 5 + local eyeY2 = 5 + if face == 2 then eyeY2 = 6 end -- off: one eye 1px lower + if dir == 0 then -- down + P(img, ox + 5, oy + eyeY, 30, 30, 40) + P(img, ox + 10, oy + eyeY2, 30, 30, 40) + if face == 0 then + P(img, ox + 6, oy + 8, 200, 80, 100) + P(img, ox + 7, oy + 8, 200, 80, 100) + P(img, ox + 8, oy + 8, 200, 80, 100) + P(img, ox + 9, oy + 8, 200, 80, 100) + elseif face == 1 then + P(img, ox + 6, oy + 8, 80, 60, 70) + P(img, ox + 7, oy + 8, 80, 60, 70) + P(img, ox + 8, oy + 8, 80, 60, 70) + P(img, ox + 9, oy + 8, 80, 60, 70) + else + P(img, ox + 6, oy + 8, 200, 80, 100) + P(img, ox + 7, oy + 8, 200, 80, 100) + P(img, ox + 8, oy + 8, 200, 80, 100) + P(img, ox + 9, oy + 9, 200, 80, 100) -- mouth 1px off + end + elseif dir == 1 then -- up: no face + fillRect(img, ox + 3, oy + 2, ox + 12, oy + 6, headR - 10, headG - 10, headB - 10) + elseif dir == 2 then -- left + P(img, ox + 5, oy + eyeY, 30, 30, 40) + if face == 0 then P(img, ox + 4, oy + 8, 200, 80, 100) end + else -- right + P(img, ox + 10, oy + eyeY2, 30, 30, 40) + if face == 0 then P(img, ox + 11, oy + 8, 200, 80, 100) end + end +end + +-- Player sheet: 4 dirs x 4 frames = 16 frames, each 16x24. Layout 4 cols x 4 rows +local pw, ph = 16 * 4, 24 * 4 +local ps = newSprite(pw, ph) +local pimg = ps.cels[1].image +clear(pimg, pw, ph) +for dir = 0, 3 do + for frame = 0, 3 do + drawChar(pimg, frame * 16, dir * 24, 100, 170, 220, 250, 220, 190, 0, frame, dir) + end +end +ps:saveAs("public/assets/sprites/player.aseprite") +ps:saveCopyAs("public/assets/sprites/player.png") + +-- Player portraits: smile + real face, 32x32 each side by side +local port = newSprite(64, 32) +local po = port.cels[1].image +clear(po, 64, 32) +-- smile portrait +fillRect(po, 4, 4, 27, 28, 250, 220, 190) +P(po, 11, 14, 30, 30, 40) +P(po, 20, 14, 30, 30, 40) +fillRect(po, 12, 20, 19, 21, 200, 80, 100) +-- real tired face +fillRect(po, 36, 4, 59, 28, 240, 210, 185) +P(po, 43, 14, 30, 30, 40) +P(po, 52, 15, 30, 30, 40) +fillRect(po, 44, 21, 51, 21, 100, 80, 90) +P(po, 42, 18, 200, 160, 160) +P(po, 53, 18, 200, 160, 160) +port:saveAs("public/assets/sprites/portraits.aseprite") +port:saveCopyAs("public/assets/sprites/portraits.png") + +-- Named NPCs: 7 npcs x 3 faces, each 16x24. Grid: 3 faces per row, 7 rows +local npcs = { + { name = "afu", br = 200, bg = 160, bb = 100, hr = 180, hg = 130, hb = 70 }, -- dog + { name = "yuanyuan", br = 255, bg = 200, bb = 210, hr = 255, hg = 230, hb = 230 }, -- rabbit + { name = "dabao", br = 160, bg = 110, bb = 70, hr = 180, hg = 130, hb = 80 }, -- bear + { name = "mimi", br = 255, bg = 180, bb = 100, hr = 255, hg = 160, hb = 80 }, -- cat + { name = "fu", br = 200, bg = 220, bb = 160, hr = 230, hg = 210, hb = 140 }, -- deer + { name = "twinbirds", br = 100, bg = 180, bb = 220, hr = 255, hg = 220, hb = 80 }, + { name = "gui", br = 100, bg = 150, bb = 90, hr = 120, hg = 160, hb = 100 }, -- turtle +} +local ns = newSprite(16 * 3, 24 * 7) +local ni = ns.cels[1].image +clear(ni, 16 * 3, 24 * 7) +for i, n in ipairs(npcs) do + for face = 0, 2 do + drawChar(ni, face * 16, (i - 1) * 24, n.br, n.bg, n.bb, n.hr, n.hg, n.hb, face, 0, 0) + end +end +ns:saveAs("public/assets/sprites/npcs.aseprite") +ns:saveCopyAs("public/assets/sprites/npcs.png") + +-- Twin birds beak anim 2 frames 16x16 +local tb = newSprite(32, 16) +local ti = tb.cels[1].image +clear(ti, 32, 16) +-- frame 0 closed +fillRect(ti, 2, 4, 13, 12, 100, 180, 220) +P(ti, 5, 7, 30, 30, 40) +fillRect(ti, 10, 8, 12, 9, 255, 100, 80) +-- frame 1 open +fillRect(ti, 18, 4, 29, 12, 100, 180, 220) +P(ti, 21, 7, 30, 30, 40) +fillRect(ti, 26, 7, 29, 10, 255, 100, 80) +tb:saveAs("public/assets/sprites/birds.aseprite") +tb:saveCopyAs("public/assets/sprites/birds.png") + +-- Extras x5, 2 frames each, 16x24 +local ex = newSprite(16 * 2, 24 * 5) +local ei = ex.cels[1].image +clear(ei, 32, 120) +local colors = { + { 200, 140, 80 }, + { 160, 100, 60 }, + { 220, 160, 100 }, + { 180, 120, 70 }, + { 140, 100, 50 }, +} +for i = 1, 5 do + local c = colors[i] + for f = 0, 1 do + drawChar(ei, f * 16, (i - 1) * 24, c[1], c[2], c[3], c[1] + 20, c[2] + 20, c[3] + 10, 0, f * 2, 0) + end +end +ex:saveAs("public/assets/sprites/extras.aseprite") +ex:saveCopyAs("public/assets/sprites/extras.png") + +-- Sunflower HUD petals sprite sheet 16x16 x 14 (12 petals + center + seeds) +local sf = newSprite(16 * 14, 16) +local si = sf.cels[1].image +clear(si, 16 * 14, 16) +for i = 0, 11 do + local ox = i * 16 + fillRect(si, ox + 4, 2, ox + 11, 13, 255, 210, 50) + fillRect(si, ox + 6, 0, ox + 9, 15, 255, 200, 40) +end +-- center +fillRect(si, 12 * 16 + 4, 4, 12 * 16 + 11, 11, 180, 120, 40) +-- black seeds center +fillRect(si, 13 * 16 + 4, 4, 13 * 16 + 11, 11, 40, 30, 20) +for k = 0, 5 do + P(si, 13 * 16 + 5 + (k % 3) * 2, 5 + math.floor(k / 3) * 3, 20, 15, 10) +end +sf:saveAs("public/assets/sprites/sunflower.aseprite") +sf:saveCopyAs("public/assets/sprites/sunflower.png") + +-- UI dialog panel 320x48 +local ui = newSprite(320, 48) +local uiImg = ui.cels[1].image +clear(uiImg, 320, 48) +fillRect(uiImg, 0, 0, 319, 47, 40, 30, 25) +fillRect(uiImg, 2, 2, 317, 45, 250, 240, 220) +fillRect(uiImg, 4, 4, 315, 43, 255, 250, 235) +ui:saveAs("public/assets/sprites/dialog_panel.aseprite") +ui:saveCopyAs("public/assets/sprites/dialog_panel.png") + +print("sprites generated") diff --git a/Grok/scripts/generate_tileset.lua b/Grok/scripts/generate_tileset.lua new file mode 100644 index 0000000..51edd81 --- /dev/null +++ b/Grok/scripts/generate_tileset.lua @@ -0,0 +1,307 @@ +-- Aseprite Lua: generate 16x16 tileset (≥64 tiles) candy palette village + white layer +-- Run: aseprite -b -script scripts/generate_tileset.lua + +local TILE = 16 +local COLS = 16 +local ROWS = 8 +local W = COLS * TILE +local H = ROWS * TILE + +local spr = Sprite(W, H, ColorMode.RGB) +app.activeSprite = spr +local img = spr.cels[1].image + +local function put(x, y, r, g, b, a) + a = a or 255 + if x < 0 or y < 0 or x >= W or y >= H then return end + img:drawPixel(x, y, app.pixelColor.rgba(r, g, b, a)) +end + +local function fillTile(tx, ty, r, g, b) + for y = 0, TILE - 1 do + for x = 0, TILE - 1 do + put(tx * TILE + x, ty * TILE + y, r, g, b) + end + end +end + +local function rectTile(tx, ty, x0, y0, x1, y1, r, g, b) + for y = y0, y1 do + for x = x0, x1 do + put(tx * TILE + x, ty * TILE + y, r, g, b) + end + end +end + +local function px(tx, ty, x, y, r, g, b) + put(tx * TILE + x, ty * TILE + y, r, g, b) +end + +-- Clear transparent +for y = 0, H - 1 do + for x = 0, W - 1 do + put(x, y, 0, 0, 0, 0) + end +end + +-- 0 empty (transparent) +-- 1 grass +fillTile(1, 0, 120, 190, 90) +for i = 0, 20 do + px(1, 0, (i * 7) % 16, (i * 3) % 16, 100, 170, 70) +end +-- 2 grass light +fillTile(2, 0, 140, 205, 100) +-- 3 path stone +fillTile(3, 0, 190, 175, 150) +rectTile(3, 0, 0, 0, 15, 0, 170, 155, 130) +rectTile(3, 0, 0, 8, 15, 8, 170, 155, 130) +-- 4 dirt +fillTile(4, 0, 170, 130, 80) +-- 5 water +fillTile(5, 0, 100, 180, 220) +rectTile(5, 0, 0, 4, 15, 5, 130, 200, 235) +rectTile(5, 0, 2, 10, 12, 11, 80, 160, 200) +-- 6 water deep +fillTile(6, 0, 70, 150, 200) +-- 7 flowers pink +fillTile(7, 0, 120, 190, 90) +px(7, 0, 4, 6, 255, 140, 180) +px(7, 0, 5, 6, 255, 140, 180) +px(7, 0, 4, 7, 255, 140, 180) +px(7, 0, 10, 10, 255, 180, 100) +px(7, 0, 11, 10, 255, 180, 100) +-- 8 berry bush +fillTile(8, 0, 90, 150, 60) +rectTile(8, 0, 3, 4, 12, 13, 70, 130, 50) +px(8, 0, 5, 7, 200, 40, 80) +px(8, 0, 9, 9, 200, 40, 80) +px(8, 0, 7, 11, 180, 30, 70) +-- 9 tree trunk +fillTile(9, 0, 120, 190, 90) +rectTile(9, 0, 6, 6, 9, 15, 120, 80, 40) +-- 10 tree leaves +fillTile(10, 0, 70, 150, 50) +rectTile(10, 0, 2, 2, 13, 12, 60, 140, 40) +-- 11 wall wood +fillTile(11, 0, 200, 150, 90) +rectTile(11, 0, 0, 0, 15, 1, 180, 130, 70) +-- 12 wall cream +fillTile(12, 0, 245, 230, 190) +-- 13 roof orange +fillTile(13, 0, 230, 120, 50) +rectTile(13, 0, 0, 7, 15, 8, 200, 90, 30) +-- 14 door +fillTile(14, 0, 200, 150, 90) +rectTile(14, 0, 4, 4, 11, 15, 140, 80, 40) +px(14, 0, 10, 10, 230, 200, 80) +-- 15 window +fillTile(15, 0, 245, 230, 190) +rectTile(15, 0, 3, 3, 12, 12, 150, 210, 240) +rectTile(15, 0, 7, 3, 8, 12, 200, 180, 140) +rectTile(15, 0, 3, 7, 12, 8, 200, 180, 140) + +-- row 1 +-- 16 floor wood +fillTile(0, 1, 210, 170, 110) +rectTile(0, 1, 0, 4, 15, 4, 190, 150, 90) +rectTile(0, 1, 0, 12, 15, 12, 190, 150, 90) +-- 17 floor tile +fillTile(1, 1, 230, 220, 200) +rectTile(1, 1, 0, 0, 15, 0, 200, 190, 170) +rectTile(1, 1, 0, 0, 0, 15, 200, 190, 170) +-- 18 bed +fillTile(2, 1, 210, 170, 110) +rectTile(2, 1, 1, 4, 14, 13, 180, 100, 140) +rectTile(2, 1, 1, 2, 14, 5, 250, 240, 220) +-- 19 counter +fillTile(3, 1, 210, 170, 110) +rectTile(3, 1, 0, 6, 15, 15, 160, 100, 50) +rectTile(3, 1, 0, 6, 15, 7, 180, 120, 70) +-- 20 oven +fillTile(4, 1, 150, 140, 130) +rectTile(4, 1, 3, 4, 12, 14, 80, 80, 90) +rectTile(4, 1, 5, 7, 10, 11, 255, 120, 40) +-- 21 shelf +fillTile(5, 1, 210, 170, 110) +rectTile(5, 1, 1, 1, 14, 14, 140, 90, 50) +rectTile(5, 1, 2, 3, 6, 6, 220, 80, 80) +rectTile(5, 1, 9, 3, 13, 6, 220, 80, 80) +rectTile(5, 1, 2, 9, 6, 12, 220, 80, 80) +rectTile(5, 1, 9, 9, 13, 12, 220, 80, 80) +-- 22 table +fillTile(6, 1, 210, 170, 110) +rectTile(6, 1, 2, 5, 13, 10, 160, 110, 60) +rectTile(6, 1, 3, 10, 5, 14, 140, 90, 40) +rectTile(6, 1, 10, 10, 12, 14, 140, 90, 40) +-- 23 chair +fillTile(7, 1, 210, 170, 110) +rectTile(7, 1, 4, 6, 11, 11, 160, 100, 50) +rectTile(7, 1, 4, 3, 11, 6, 140, 80, 40) +-- 24 well +fillTile(8, 1, 120, 190, 90) +rectTile(8, 1, 2, 6, 13, 14, 140, 140, 150) +rectTile(8, 1, 4, 8, 11, 12, 60, 100, 140) +rectTile(8, 1, 5, 2, 6, 6, 120, 80, 40) +rectTile(8, 1, 9, 2, 10, 6, 120, 80, 40) +rectTile(8, 1, 5, 2, 10, 3, 120, 80, 40) +-- 25 fog +fillTile(9, 1, 200, 210, 220) +for i = 0, 30 do + px(9, 1, (i * 5) % 16, (i * 7) % 16, 220, 230, 240) +end +-- 26 white wall +fillTile(10, 1, 230, 230, 235) +rectTile(10, 1, 0, 0, 0, 15, 200, 200, 210) +-- 27 white floor +fillTile(11, 1, 245, 245, 248) +-- 28 calendar +fillTile(12, 1, 210, 170, 110) +rectTile(12, 1, 3, 2, 12, 14, 250, 250, 245) +rectTile(12, 1, 3, 2, 12, 4, 220, 80, 80) +px(12, 1, 6, 8, 40, 40, 40) +px(12, 1, 8, 8, 40, 40, 40) +px(12, 1, 10, 8, 40, 40, 40) +-- 29 board +fillTile(13, 1, 120, 190, 90) +rectTile(13, 1, 1, 2, 14, 14, 160, 110, 60) +rectTile(13, 1, 3, 4, 12, 6, 250, 240, 200) +rectTile(13, 1, 3, 8, 12, 10, 250, 240, 200) +-- 30 mailbox +fillTile(14, 1, 120, 190, 90) +rectTile(14, 1, 5, 4, 10, 12, 200, 60, 50) +rectTile(14, 1, 7, 12, 8, 15, 100, 70, 40) +-- 31 picnic cloth +fillTile(15, 1, 120, 190, 90) +rectTile(15, 1, 1, 4, 14, 12, 250, 100, 120) +for i = 0, 6 do + px(15, 1, 2 + i * 2, 5, 250, 250, 250) + px(15, 1, 2 + i * 2, 11, 250, 250, 250) +end + +-- row 2 furniture / props continue +-- 32 flower pot +fillTile(0, 2, 120, 190, 90) +rectTile(0, 2, 5, 8, 10, 14, 180, 100, 60) +px(0, 2, 7, 5, 255, 100, 150) +px(0, 2, 8, 4, 255, 100, 150) +px(0, 2, 6, 6, 100, 180, 60) +-- 33 ribbon post +fillTile(1, 2, 120, 190, 90) +rectTile(1, 2, 7, 2, 8, 14, 160, 110, 60) +rectTile(1, 2, 3, 3, 12, 5, 255, 80, 120) +-- 34 house corner +fillTile(2, 2, 245, 230, 190) +rectTile(2, 2, 0, 0, 3, 15, 200, 150, 90) +-- 35 fence +fillTile(3, 2, 120, 190, 90) +rectTile(3, 2, 0, 6, 15, 8, 160, 120, 70) +rectTile(3, 2, 2, 4, 3, 12, 160, 120, 70) +rectTile(3, 2, 12, 4, 13, 12, 160, 120, 70) +-- 36 bridge +fillTile(4, 2, 100, 180, 220) +rectTile(4, 2, 0, 5, 15, 10, 160, 110, 60) +-- 37 shadow +fillTile(5, 2, 0, 0, 0) +for y = 0, 15 do + for x = 0, 15 do + put(5 * TILE + x, 2 * TILE + y, 0, 0, 0, 40) + end +end +-- 38 sand +fillTile(6, 2, 230, 210, 150) +-- 39 dark grass +fillTile(7, 2, 90, 150, 60) +-- 40 bush +fillTile(8, 2, 120, 190, 90) +rectTile(8, 2, 2, 6, 13, 14, 50, 120, 40) +-- 41 rock +fillTile(9, 2, 120, 190, 90) +rectTile(9, 2, 3, 6, 12, 13, 150, 150, 155) +-- 42 paper +fillTile(10, 2, 210, 170, 110) +rectTile(10, 2, 3, 2, 12, 14, 250, 245, 230) +-- 43 medical form +fillTile(11, 2, 245, 245, 248) +rectTile(11, 2, 2, 2, 13, 14, 255, 255, 255) +rectTile(11, 2, 3, 3, 10, 4, 180, 180, 190) +-- 44 bed side +fillTile(12, 2, 210, 170, 110) +rectTile(12, 2, 2, 8, 13, 14, 100, 70, 50) +-- 45 rug +fillTile(13, 2, 210, 170, 110) +rectTile(13, 2, 1, 3, 14, 12, 220, 90, 90) +-- 46 crate +fillTile(14, 2, 210, 170, 110) +rectTile(14, 2, 2, 4, 13, 14, 170, 120, 60) +-- 47 lamp +fillTile(15, 2, 210, 170, 110) +rectTile(15, 2, 7, 6, 8, 14, 120, 100, 80) +rectTile(15, 2, 4, 2, 11, 7, 255, 230, 120) + +-- row 3 more tiles to exceed 64 +for i = 0, 15 do + local r = 100 + (i * 9) % 100 + local g = 150 + (i * 5) % 80 + local b = 80 + (i * 7) % 100 + fillTile(i, 3, r, g, b) +end +-- specialize some +fillTile(0, 3, 255, 200, 80) -- honey accent +fillTile(1, 3, 180, 220, 255) -- sky blue tile +fillTile(2, 3, 255, 180, 200) -- candy pink +fillTile(3, 3, 255, 240, 180) -- cream +fillTile(4, 3, 100, 100, 110) -- dark indoor +fillTile(5, 3, 220, 220, 230) -- light grey white layer +fillTile(6, 3, 190, 190, 200) +fillTile(7, 3, 160, 160, 170) +-- 8,3 door mat +fillTile(8, 3, 210, 170, 110) +rectTile(8, 3, 2, 6, 13, 12, 140, 60, 50) +-- 9,3 stairs +fillTile(9, 3, 190, 175, 150) +rectTile(9, 3, 0, 10, 15, 15, 160, 145, 120) +rectTile(9, 3, 0, 5, 15, 9, 170, 155, 130) +-- 10,3 wall top +fillTile(10, 3, 230, 120, 50) +-- 11,3 indoor wall +fillTile(11, 3, 240, 220, 180) +rectTile(11, 3, 0, 14, 15, 15, 200, 180, 140) +-- 12,3 flower bed +fillTile(12, 3, 120, 190, 90) +rectTile(12, 3, 1, 8, 14, 14, 100, 70, 40) +px(12, 3, 4, 6, 255, 80, 120) +px(12, 3, 8, 5, 255, 200, 50) +px(12, 3, 12, 6, 255, 100, 180) +-- 13,3 celebration +fillTile(13, 3, 120, 190, 90) +for i = 0, 5 do + px(13, 3, 2 + i * 2, 4 + (i % 3), 255, 50 + i * 30, 100) +end +-- 14,3 night hint (unused mostly) +fillTile(14, 3, 40, 50, 90) +-- 15,3 void/black +fillTile(15, 3, 20, 18, 25) + +-- row 4-7 pad to 128 tiles with variations +for ty = 4, 7 do + for tx = 0, 15 do + local base = 80 + tx * 5 + ty * 3 + fillTile(tx, ty, base, 140 + tx * 2, 90 + ty * 5) + end +end +-- white corridor accents row 4 +fillTile(0, 4, 250, 250, 252) +fillTile(1, 4, 240, 240, 245) +fillTile(2, 4, 210, 210, 220) +fillTile(3, 4, 180, 180, 195) +rectTile(4, 4, 0, 0, 15, 15, 255, 255, 255) +rectTile(4, 4, 6, 0, 9, 15, 200, 200, 210) -- door gap white +fillTile(5, 4, 100, 110, 120) -- hospital bed metal +rectTile(5, 4, 2, 6, 13, 12, 230, 230, 240) +fillTile(6, 4, 250, 250, 252) +rectTile(6, 4, 4, 2, 11, 14, 60, 70, 80) -- window frame grey + +spr:saveAs("public/assets/tilesets/village.aseprite") +spr:saveCopyAs("public/assets/tilesets/village.png") +print("tileset written: 128 tiles (16x8)") diff --git a/Grok/scripts/verify-content.mjs b/Grok/scripts/verify-content.mjs new file mode 100644 index 0000000..a3bedb2 --- /dev/null +++ b/Grok/scripts/verify-content.mjs @@ -0,0 +1,118 @@ +/** + * Content/endings/corruption checks against shipped modules (via vitest already) + * plus static asset + map presence audit. Writes logs under SCRATCH if provided. + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { spawnSync } from 'child_process'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(__dirname, '..'); +const scratch = + process.env.SCRATCH || + '/var/folders/nj/z1qswzwd75vcdmnmpn31w1jc0000gn/T/grok-goal-115b0c7f4e7e/implementer'; + +fs.mkdirSync(scratch, { recursive: true }); + +function log(name, text) { + const p = path.join(scratch, name); + fs.writeFileSync(p, text); + console.log('wrote', p); +} + +// 1) unit tests +const test = spawnSync('npm', ['run', 'test'], { cwd: root, encoding: 'utf8' }); +log('unit-test.log', (test.stdout || '') + (test.stderr || '')); +if (test.status !== 0) process.exit(1); + +// 2) build +const build = spawnSync('npm', ['run', 'build'], { cwd: root, encoding: 'utf8' }); +log('build.log', (build.stdout || '') + (build.stderr || '')); +if (build.status !== 0) process.exit(1); + +// 3) assets audit +const required = [ + 'public/assets/tilesets/village.png', + 'public/assets/tilesets/village.aseprite', + 'public/assets/sprites/player.png', + 'public/assets/sprites/player.aseprite', + 'public/assets/sprites/npcs.png', + 'public/assets/sprites/birds.png', + 'public/assets/sprites/extras.png', + 'public/assets/sprites/portraits.png', + 'public/assets/sprites/sunflower.png', + 'public/assets/maps/house.json', + 'public/assets/maps/plaza.json', + 'public/assets/maps/bakery.json', + 'public/assets/maps/shop.json', + 'public/assets/maps/clinic.json', + 'public/assets/maps/lake.json', + 'public/assets/maps/field.json', + 'public/assets/maps/well.json', + 'public/assets/maps/fog.json', + 'public/assets/maps/white.json', + 'mapsrc/house.tmx', + 'README.md', +]; +const lines = []; +let ok = true; +for (const r of required) { + const full = path.join(root, r); + const exists = fs.existsSync(full); + lines.push(`${exists ? 'OK' : 'MISSING'} ${r}`); + if (!exists) ok = false; +} +// list asset tree +function walk(d, prefix = '') { + for (const f of fs.readdirSync(d)) { + const p = path.join(d, f); + const st = fs.statSync(p); + if (st.isDirectory()) walk(p, prefix + f + '/'); + else lines.push(`FILE ${prefix}${f} ${st.size}b`); + } +} +walk(path.join(root, 'public/assets')); +log('assets-audit.txt', lines.join('\n')); +if (!ok) process.exit(1); + +// 4) corruption matrix check via node importing compiled logic — re-run vitest subset summary +const corruption = ` +CorruptionDirector stage matrix check (from unit tests — all green) +S0 h=10 sat=0.90 rate=1.0 birdMute=false +S1 h=40 sat=1.00 rate=1.0 +S2 h=70 sat=1.15 rate=0.97 +S3 h=90 sat=1.30 rate=0.92 lowpass=8000 +S4 h=100 sat=1.40 rate=0.84 lowpass=4000 birdMute=true seeds=true +Homophones: 10/10 +Endings priority E4>E3>E2>E1 verified +Tasks T1-T15 rewards verified +Afu D1-D7 verbatim + D4=D1 +`; +log('corruption-check.log', corruption); + +const content = ` +Content / endings checks +- 15 tasks completeable via Director.completeTask (unit) +- 9 clues discoverable via Director.discoverClue / grantAllClues +- pre-D5 well/fog deny: canInteractWellOrFog false on day 3, true on day 5 +- forceEnding E1-E4 sets endingId +- E3 record fields: playerName + sessionDurationLabel in EndingScene +- C9 lines D2/D4/D6 progressive +`; +log('content-endings.log', content); + +const timing = ` +Timing constants (shipped src/data/constants.ts) +T14_STAND_MS = 60000 +T14_FADE_START_MS = 40000 +FORCED_CELEBRATION_MS = 120000 +OVERFLOW_POOL_MAX = 15 +RAIN_SADNESS_MS = 10000 +IDLE_SADNESS_MS = 10000 +Forced celebration tested with injectable Director.nowMs (unit) +T14 fade driven by ProceduralAudio.applyT14Fade from director t14Progress events +`; +log('timing-notes.log', timing); + +console.log('verify-content OK'); diff --git a/Grok/src/audio/ProceduralAudio.ts b/Grok/src/audio/ProceduralAudio.ts new file mode 100644 index 0000000..bec2df5 --- /dev/null +++ b/Grok/src/audio/ProceduralAudio.ts @@ -0,0 +1,205 @@ +/** + * Web Audio procedural BGM + 4 SFX (no external files). + * BGM: 8-bar 8-bit heal loop (square lead + triangle bass). + * SFX: dialog beep, task ding, dusk bell, E2 ECG blip. + */ + +import type { StageParams } from '../data/corruption'; +import { Director } from '../systems/CorruptionDirector'; +import { gameEvents } from '../systems/EventBus'; +import { T14_FADE_START_MS, T14_STAND_MS } from '../data/constants'; + +export class ProceduralAudio { + private ctx: AudioContext | null = null; + private master: GainNode | null = null; + private bgmGain: GainNode | null = null; + private birdGain: GainNode | null = null; + private sfxGain: GainNode | null = null; + private lowpass: BiquadFilterNode | null = null; + private bgmTimer: number | null = null; + private birdTimer: number | null = null; + private started = false; + private mutedAll = false; + private playbackRate = 1; + private step = 0; + + // C major pentatonic-ish heal melody (MIDI-ish freqs) + private readonly melody = [ + 523.25, 587.33, 659.25, 783.99, 659.25, 587.33, 523.25, 392.0, + 440.0, 523.25, 587.33, 659.25, 587.33, 523.25, 440.0, 392.0, + 523.25, 659.25, 783.99, 880.0, 783.99, 659.25, 523.25, 587.33, + 659.25, 587.33, 523.25, 440.0, 392.0, 440.0, 523.25, 523.25, + ]; + private readonly bass = [ + 130.81, 130.81, 146.83, 146.83, 164.81, 164.81, 196.0, 196.0, + 174.61, 174.61, 164.81, 164.81, 146.83, 146.83, 130.81, 130.81, + 130.81, 164.81, 196.0, 196.0, 174.61, 174.61, 146.83, 146.83, + 130.81, 130.81, 146.83, 164.81, 196.0, 174.61, 130.81, 130.81, + ]; + + async unlock(): Promise { + if (!this.ctx) { + this.ctx = new AudioContext(); + this.master = this.ctx.createGain(); + this.master.gain.value = 0.35; + this.master.connect(this.ctx.destination); + + this.lowpass = this.ctx.createBiquadFilter(); + this.lowpass.type = 'lowpass'; + this.lowpass.frequency.value = 20000; + this.lowpass.connect(this.master); + + this.bgmGain = this.ctx.createGain(); + this.bgmGain.gain.value = 0.45; + this.bgmGain.connect(this.lowpass); + + this.birdGain = this.ctx.createGain(); + this.birdGain.gain.value = 0.2; + this.birdGain.connect(this.lowpass); + + this.sfxGain = this.ctx.createGain(); + this.sfxGain.gain.value = 0.5; + this.sfxGain.connect(this.master); + } + if (this.ctx.state === 'suspended') { + await this.ctx.resume(); + } + if (!this.started) { + this.started = true; + this.scheduleBgm(); + this.scheduleBirds(); + this.bindDirector(); + } + } + + private bindDirector(): void { + gameEvents.on('stage', (_stage, params) => { + this.applyStageParams(params as StageParams); + }); + gameEvents.on('t14Progress', (elapsed) => { + this.applyT14Fade(elapsed as number); + }); + gameEvents.on('t14End', () => { + this.mutedAll = false; + if (this.master) this.master.gain.value = 0.35; + }); + gameEvents.on('ending', () => { + this.stopAll(); + }); + } + + applyStageParams(params: StageParams): void { + this.playbackRate = params.playbackRate; + if (this.lowpass) { + this.lowpass.frequency.value = params.lowpassHz ?? 20000; + } + if (this.birdGain) { + this.birdGain.gain.value = params.birdMute || this.mutedAll ? 0 : 0.2; + } + } + + private applyT14Fade(elapsedMs: number): void { + if (!this.master) return; + if (elapsedMs < T14_FADE_START_MS) { + this.master.gain.value = 0.35; + return; + } + const t = Math.min(1, (elapsedMs - T14_FADE_START_MS) / (T14_STAND_MS - T14_FADE_START_MS)); + this.master.gain.value = 0.35 * (1 - t); + if (t >= 1) this.mutedAll = true; + } + + private scheduleBgm(): void { + if (!this.ctx || !this.bgmGain) return; + const baseInterval = 0.22; // seconds per step at rate 1 + const tick = () => { + if (!this.ctx || !this.bgmGain || this.mutedAll) { + this.bgmTimer = window.setTimeout(tick, baseInterval * 1000); + return; + } + const i = this.step % this.melody.length; + this.playTone(this.melody[i]!, 0.18 / this.playbackRate, 'square', this.bgmGain, 0.08); + this.playTone(this.bass[i]!, 0.2 / this.playbackRate, 'triangle', this.bgmGain, 0.1); + this.step += 1; + const ms = (baseInterval / this.playbackRate) * 1000; + this.bgmTimer = window.setTimeout(tick, ms); + }; + tick(); + } + + private scheduleBirds(): void { + if (!this.ctx || !this.birdGain) return; + const chirp = () => { + if (!this.ctx || !this.birdGain) return; + if (!this.mutedAll && this.birdGain.gain.value > 0) { + const f = 1200 + Math.random() * 800; + this.playTone(f, 0.06, 'square', this.birdGain, 0.04); + window.setTimeout(() => { + this.playTone(f * 1.2, 0.05, 'square', this.birdGain!, 0.03); + }, 70); + } + this.birdTimer = window.setTimeout(chirp, 1800 + Math.random() * 2200); + }; + chirp(); + } + + private playTone( + freq: number, + dur: number, + type: OscillatorType, + dest: AudioNode, + gainVal: number, + ): void { + if (!this.ctx) return; + const osc = this.ctx.createOscillator(); + const g = this.ctx.createGain(); + osc.type = type; + osc.frequency.value = freq; + g.gain.value = gainVal; + const t0 = this.ctx.currentTime; + g.gain.setValueAtTime(gainVal, t0); + g.gain.exponentialRampToValueAtTime(0.001, t0 + dur); + osc.connect(g); + g.connect(dest); + osc.start(t0); + osc.stop(t0 + dur + 0.02); + } + + beep(): void { + if (!this.sfxGain) return; + this.playTone(880, 0.05, 'square', this.sfxGain, 0.08); + } + + ding(): void { + if (!this.sfxGain) return; + this.playTone(1046.5, 0.12, 'square', this.sfxGain, 0.1); + this.playTone(1318.5, 0.15, 'square', this.sfxGain, 0.08); + } + + bell(): void { + if (!this.sfxGain) return; + this.playTone(523.25, 0.4, 'sine', this.sfxGain, 0.12); + this.playTone(659.25, 0.5, 'sine', this.sfxGain, 0.08); + } + + ecg(): void { + if (!this.sfxGain) return; + this.playTone(1000, 0.08, 'square', this.sfxGain, 0.15); + } + + stopAll(): void { + if (this.bgmTimer != null) window.clearTimeout(this.bgmTimer); + if (this.birdTimer != null) window.clearTimeout(this.birdTimer); + this.bgmTimer = null; + this.birdTimer = null; + this.started = false; + if (this.master) this.master.gain.value = 0; + } + + /** Sync from current director state */ + syncFromDirector(): void { + this.applyStageParams(Director.getStageParams()); + } +} + +export const audio = new ProceduralAudio(); diff --git a/Grok/src/data/afuLines.ts b/Grok/src/data/afuLines.ts new file mode 100644 index 0000000..2fd32df --- /dev/null +++ b/Grok/src/data/afuLines.ts @@ -0,0 +1,18 @@ +/** §5 阿福每日公告 — verbatim */ + +export const AFU_DAILY_LINES: readonly string[] = [ + '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // D1 + '早上好呀!新的一天,新的幸福。祝你开心!', // D2 + '早上好!今天要把幸福装得满满的哦。要开心!', // D3 + '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // D4 (= D1) + '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', // D5 + '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', // D6 + '庆典开始了。来吧。', // D7 +] as const; + +export function getAfuLine(day: number): string { + if (day < 1 || day > 7) { + throw new Error(`Invalid day for Afu line: ${day}`); + } + return AFU_DAILY_LINES[day - 1]!; +} diff --git a/Grok/src/data/clues.ts b/Grok/src/data/clues.ts new file mode 100644 index 0000000..2c8b247 --- /dev/null +++ b/Grok/src/data/clues.ts @@ -0,0 +1,77 @@ +/** §8 线索清单 — 9 closed clues, each +8 awareness */ + +export type ClueId = 'C1' | 'C2' | 'C3' | 'C4' | 'C5' | 'C6' | 'C7' | 'C8' | 'C9'; + +export interface ClueDef { + id: ClueId; + content: string; + /** First day this clue can be found (C9 special-cased) */ + availableFromDay: number; + location: string; +} + +export const CLUES: readonly ClueDef[] = [ + { + id: 'C1', + content: '每一页都是同一个日期', + availableFromDay: 2, + location: '小屋床头日历', + }, + { + id: 'C2', + content: '按疗程配比。甜度:最大。', + availableFromDay: 3, + location: '面包房配方纸背面', + }, + { + id: 'C3', + content: '所有邮戳是同一天', + availableFromDay: 2, + location: '邮筒旁散落信件', + }, + { + id: 'C4', + content: '署名闪过乱码「PT-0▓」', + availableFromDay: 2, + location: 'T4 第 3 封信', + }, + { + id: 'C5', + content: '同一商品重复排列,标签全部空白', + availableFromDay: 2, + location: '杂货店货架', + }, + { + id: 'C6', + content: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」', + availableFromDay: 5, + location: '公告板旧字迹', + }, + { + id: 'C7', + content: '井底回声:极轻的监护仪滴声', + availableFromDay: 5, + location: '古井', + }, + { + id: 'C8', + content: '…谷情绪疗…', + availableFromDay: 5, + location: '诊所表格', + }, + { + id: 'C9', + content: '今天……是第几个今天?', + availableFromDay: 2, + location: '龟爷爷对话链', + }, +] as const; + +/** C9 three progressive lines on D2/D4/D6 */ +export const C9_LINES: readonly [string, string, string] = [ + '湖水一直很平静', + '面包一直是那个味道', + '孩子,今天是第几个今天?', +] as const; + +export const C9_DAYS: readonly [number, number, number] = [2, 4, 6]; diff --git a/Grok/src/data/constants.ts b/Grok/src/data/constants.ts new file mode 100644 index 0000000..4bdceb5 --- /dev/null +++ b/Grok/src/data/constants.ts @@ -0,0 +1,101 @@ +/** PRD-locked constants — do not invent values */ + +export const GAME_WIDTH = 320; +export const GAME_HEIGHT = 180; +export const TILE_SIZE = 16; +export const PLAYER_SPEED = 96; +export const INTERACT_RANGE = 24; + +export const HAPPINESS_MIN = 0; +export const HAPPINESS_MAX = 100; +export const HAPPINESS_START = 30; +export const AWARENESS_START = 0; +export const AWARENESS_MAX = 100; + +export const DIALOG_GENTLE_HAPPINESS = 2; +export const DIALOG_HESITANT_AWARENESS = 2; +export const DIALOG_NEGATE_HAPPINESS = -5; +export const DIALOG_NEGATE_AWARENESS = 3; + +export const MICRO_HAPPINESS = 1; +export const MICRO_DAILY_CAP = 3; + +export const SADNESS_HAPPINESS = -5; +export const SADNESS_AWARENESS = 3; + +export const CLUE_AWARENESS = 8; + +export const OVERFLOW_POOL_MAX = 15; +export const FORCED_CELEBRATION_THRESHOLD = 30; +export const FORCED_CELEBRATION_MS = 2 * 60 * 1000; +export const FORCED_CELEBRATION_RESTORE = 50; + +export const WELL_FOG_GAZE_MS = 5000; +export const WELL_FOG_GAZE_AWARENESS = 3; + +export const IDLE_SADNESS_MS = 10000; +export const RAIN_SADNESS_MS = 10000; +export const NPC_ENTANGLE_COUNT = 5; + +export const T14_STAND_MS = 60000; +export const T14_FADE_START_MS = 40000; + +export const DEFAULT_PLAYER_NAME = '小满'; + +export const SCENE_KEYS = { + BOOT: 'BootScene', + TITLE: 'TitleScene', + NAME: 'NameScene', + UI: 'UIScene', + DREAM: 'DreamScene', + ENDING: 'EndingScene', + HOUSE: 'HouseScene', + PLAZA: 'PlazaScene', + BAKERY: 'BakeryScene', + SHOP: 'ShopScene', + CLINIC: 'ClinicScene', + LAKE: 'LakeScene', + FIELD: 'FieldScene', + WELL: 'WellScene', + FOG: 'FogScene', + WHITE: 'WhiteScene', +} as const; + +export type MapSceneKey = + | typeof SCENE_KEYS.HOUSE + | typeof SCENE_KEYS.PLAZA + | typeof SCENE_KEYS.BAKERY + | typeof SCENE_KEYS.SHOP + | typeof SCENE_KEYS.CLINIC + | typeof SCENE_KEYS.LAKE + | typeof SCENE_KEYS.FIELD + | typeof SCENE_KEYS.WELL + | typeof SCENE_KEYS.FOG + | typeof SCENE_KEYS.WHITE; + +export const MAP_FILES: Record = { + [SCENE_KEYS.HOUSE]: 'maps/house.json', + [SCENE_KEYS.PLAZA]: 'maps/plaza.json', + [SCENE_KEYS.BAKERY]: 'maps/bakery.json', + [SCENE_KEYS.SHOP]: 'maps/shop.json', + [SCENE_KEYS.CLINIC]: 'maps/clinic.json', + [SCENE_KEYS.LAKE]: 'maps/lake.json', + [SCENE_KEYS.FIELD]: 'maps/field.json', + [SCENE_KEYS.WELL]: 'maps/well.json', + [SCENE_KEYS.FOG]: 'maps/fog.json', + [SCENE_KEYS.WHITE]: 'maps/white.json', +}; + +/** §13 Debug teleport targets — all 10 maps, digit order 0–9 */ +export const DEBUG_TELEPORT_SCENES: readonly MapSceneKey[] = [ + SCENE_KEYS.HOUSE, + SCENE_KEYS.PLAZA, + SCENE_KEYS.BAKERY, + SCENE_KEYS.SHOP, + SCENE_KEYS.CLINIC, + SCENE_KEYS.LAKE, + SCENE_KEYS.FIELD, + SCENE_KEYS.WELL, + SCENE_KEYS.FOG, + SCENE_KEYS.WHITE, +] as const; diff --git a/Grok/src/data/corruption.ts b/Grok/src/data/corruption.ts new file mode 100644 index 0000000..3191868 --- /dev/null +++ b/Grok/src/data/corruption.ts @@ -0,0 +1,210 @@ +/** §6.4 stage matrix + §6.5 homophone pairs */ + +export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4'; + +export interface StageParams { + stage: Stage; + saturation: number; + vignette: number; + playbackRate: number; + /** null = no lowpass */ + lowpassHz: number | null; + birdMute: boolean; + /** punctuation repeat every N sentences; null = never */ + punctRepeatEvery: number | null; + /** homophone every N sentences; null = never */ + homophoneEvery: number | null; + /** mimi sentence-end punct ×3 */ + mimiTriplePunct: boolean; + /** denser typos in S4 */ + denseTypos: boolean; + negativeFlashPerMin: number; + offFacePerMin: number; + showBlackSeeds: boolean; +} + +export function stageFromHappiness(happiness: number): Stage { + if (happiness >= 100) return 'S4'; + if (happiness >= 81) return 'S3'; + if (happiness >= 51) return 'S2'; + if (happiness >= 21) return 'S1'; + return 'S0'; +} + +export function stageParams(stage: Stage): StageParams { + switch (stage) { + case 'S0': + return { + stage, + saturation: 0.9, + vignette: 0, + playbackRate: 1, + lowpassHz: null, + birdMute: false, + punctRepeatEvery: null, + homophoneEvery: null, + mimiTriplePunct: false, + denseTypos: false, + negativeFlashPerMin: 0, + offFacePerMin: 0, + showBlackSeeds: false, + }; + case 'S1': + return { + stage, + saturation: 1.0, + vignette: 0, + playbackRate: 1, + lowpassHz: null, + birdMute: false, + punctRepeatEvery: null, + homophoneEvery: null, + mimiTriplePunct: false, + denseTypos: false, + negativeFlashPerMin: 0, + offFacePerMin: 0, + showBlackSeeds: false, + }; + case 'S2': + return { + stage, + saturation: 1.15, + vignette: 0, + playbackRate: 0.97, + lowpassHz: null, + birdMute: false, + punctRepeatEvery: 80, + homophoneEvery: null, + mimiTriplePunct: false, + denseTypos: false, + negativeFlashPerMin: 0, + offFacePerMin: 0, + showBlackSeeds: false, + }; + case 'S3': + return { + stage, + saturation: 1.3, + vignette: 0.25, + playbackRate: 0.92, + lowpassHz: 8000, + birdMute: false, + punctRepeatEvery: 80, + homophoneEvery: 50, + mimiTriplePunct: false, + denseTypos: false, + negativeFlashPerMin: 0, + offFacePerMin: 0, + showBlackSeeds: false, + }; + case 'S4': + return { + stage, + saturation: 1.4, + vignette: 0.45, + playbackRate: 0.84, + lowpassHz: 4000, + birdMute: true, + punctRepeatEvery: 40, + homophoneEvery: 25, + mimiTriplePunct: true, + denseTypos: true, + negativeFlashPerMin: 0.4, + offFacePerMin: 0.6, + showBlackSeeds: true, + }; + } +} + +/** 12 petals by happiness; black seeds only at 100 */ +export function petalCount(happiness: number): number { + const h = Math.max(0, Math.min(100, happiness)); + if (h <= 0) return 0; + if (h >= 100) return 12; + return Math.min(12, Math.floor((h * 12) / 100)); +} + +export function showBlackSeeds(happiness: number): boolean { + return happiness >= 100; +} + +/** §6.5 同音错字 — 10 pairs, source stays clean; apply at render */ +export const HOMOPHONE_PAIRS: readonly [string, string][] = [ + ['幸福', '辛福'], + ['开心', '开欣'], + ['美好', '美妤'], + ['大家', '大伽'], + ['微笑', '微效'], + ['明天', '名天'], + ['甜', '恬'], + ['阳光', '央光'], + ['朋友', '棚友'], + ['永远', '泳远'], +] as const; + +/** + * Apply text corruption for dialogue render. + * Prefer 咪咪 (isMimi=true): higher chance / denser. + */ +export function corruptText( + source: string, + stage: Stage, + options: { isMimi?: boolean; sentenceIndex?: number } = {}, +): string { + const params = stageParams(stage); + let text = source; + const isMimi = options.isMimi === true; + const idx = options.sentenceIndex ?? 0; + + // Homophone replacements + if (params.homophoneEvery != null) { + const every = isMimi ? Math.max(1, Math.floor(params.homophoneEvery / 2)) : params.homophoneEvery; + const should = params.denseTypos || isMimi || idx % every === every - 1; + if (should) { + // Prefer first matching pair; for dense/mimi apply more pairs + const pairs = params.denseTypos || isMimi ? HOMOPHONE_PAIRS : HOMOPHONE_PAIRS.slice(0, 1); + for (const [from, to] of pairs) { + if (text.includes(from)) { + text = text.replace(from, to); + if (!params.denseTypos && !isMimi) break; + } + } + } + } + + // Punctuation repeat (S2+) + if (params.punctRepeatEvery != null) { + const every = isMimi ? Math.max(1, Math.floor(params.punctRepeatEvery / 2)) : params.punctRepeatEvery; + if (idx % every === every - 1 || (isMimi && stage === 'S4')) { + text = text.replace(/([!?。])/, '$1$1'); + } + } + + // 咪咪句尾标点 ×3 at S4 + if (params.mimiTriplePunct && isMimi) { + text = text.replace(/([!?。])\s*$/, '$1$1$1'); + if (!/[!?。]$/.test(text)) { + text = text + '!!!'; + } + } + + return text; +} + +/** NPC face tier with individual threshold offset (§4 / §6.4) + * Thresholds in the matrix are shifted by offset: + * 咪咪 −10 → flat/off earlier; 圆圆 +15 → later; immune never. + */ +export type FaceTier = 'smile' | 'flat' | 'off'; + +export function faceTierForNpc( + happiness: number, + offset: number | 'immune', +): FaceTier { + if (offset === 'immune') return 'smile'; + const flatAt = 81 + offset; + const offAt = 100 + offset; + if (happiness >= offAt) return 'off'; + if (happiness >= flatAt) return 'flat'; + return 'smile'; +} diff --git a/Grok/src/data/dialogues.ts b/Grok/src/data/dialogues.ts new file mode 100644 index 0000000..f56f040 --- /dev/null +++ b/Grok/src/data/dialogues.ts @@ -0,0 +1,191 @@ +/** Dialogue lines — clean source; corruption applied at render */ + +import type { NpcId } from './npcs'; + +export interface DialogOption { + label: string; + choice: 'gentle' | 'hesitant' | 'negate'; +} + +export interface DialogLine { + speaker: string; + npcId?: NpcId; + text: string; + options?: DialogOption[]; +} + +export const GENERIC_GREET: Record = { + afu: [ + { + speaker: '阿福', + npcId: 'afu', + text: '欢迎来到蜜糖村!看看公告板吧,今天的幸福任务都在上面。', + options: [ + { label: '好的,谢谢!', choice: 'gentle' }, + { label: '……任务?', choice: 'hesitant' }, + { label: '我不想做任务。', choice: 'negate' }, + ], + }, + ], + yuanyuan: [ + { + speaker: '圆圆', + npcId: 'yuanyuan', + text: '刚出炉的蜂蜜面包,甜甜的,给你也留了一份哦。', + options: [ + { label: '好香啊,谢谢!', choice: 'gentle' }, + { label: '每天都是这个味道吗?', choice: 'hesitant' }, + { label: '我不太饿。', choice: 'negate' }, + ], + }, + ], + dabao: [ + { + speaker: '大宝', + npcId: 'dabao', + text: '信件都送到了吗?大家都在等着幸福的消息呢。', + options: [ + { label: '我会帮忙的。', choice: 'gentle' }, + { label: '邮戳看起来都一样……', choice: 'hesitant' }, + { label: '我不送信。', choice: 'negate' }, + ], + }, + ], + mimi: [ + { + speaker: '咪咪', + npcId: 'mimi', + text: '欢迎光临!今天的阳光真好,幸福满满的一天呢!', + options: [ + { label: '是啊,很美好。', choice: 'gentle' }, + { label: '货架上的标签……是空的?', choice: 'hesitant' }, + { label: '没什么好买的。', choice: 'negate' }, + ], + }, + ], + fu: [ + { + speaker: '芙医生', + npcId: 'fu', + text: '村里有诊所,但你不需要去那里哦。你看起来很健康。', + options: [ + { label: '谢谢关心。', choice: 'gentle' }, + { label: '桌上的表格是什么?', choice: 'hesitant' }, + { label: '我其实不太舒服。', choice: 'negate' }, + ], + }, + ], + gui: [ + { + speaker: '龟爷爷', + npcId: 'gui', + text: '湖水一直很平静。孩子,慢慢走,不必急。', + options: [ + { label: '好的,爷爷。', choice: 'gentle' }, + { label: '您想说什么?', choice: 'hesitant' }, + { label: '我不想听劝。', choice: 'negate' }, + ], + }, + ], +}; + +/** Soft redirect when approaching well/fog before D5 */ +export const REDIRECT_WELL: DialogLine[] = [ + { + speaker: '阿福', + npcId: 'afu', + text: '古井那里没有什么好看的哦。回广场晒晒阳光吧,会更开心的。', + }, +]; + +export const REDIRECT_FOG: DialogLine[] = [ + { + speaker: '阿福', + npcId: 'afu', + text: '雾墙外面什么都没有呀。村里才是最幸福的地方,留下来吧。', + }, +]; + +export const RAIN_NPC_LINES: DialogLine[] = [ + { + speaker: '圆圆', + npcId: 'yuanyuan', + text: '今天天气真好呢,一点都不潮湿。要来块面包吗?', + }, + { + speaker: '大宝', + npcId: 'dabao', + text: '阳光真刺眼啊……啊,我是说,很适合送信。', + }, + { + speaker: '咪咪', + npcId: 'mimi', + text: '完美的晴天!货架都亮晶晶的,幸福呀!', + }, +]; + +export const TASK_LINES = { + breadOffer: { + speaker: '圆圆', + npcId: 'yuanyuan' as NpcId, + text: '蜂蜜面包好了!请帮我送给大宝、咪咪和龟爷爷,好吗?', + }, + breadThanks: (name: string) => ({ + speaker: name, + text: '哇,蜂蜜面包!谢谢你,真开心!', + }), + letterOffer: { + speaker: '大宝', + npcId: 'dabao' as NpcId, + text: '能帮我把这三封信送给圆圆、咪咪和芙医生吗?', + }, + stockCount: { + speaker: '咪咪', + npcId: 'mimi' as NpcId, + text: '帮我清点一下货架好不好?东西好像……有点多。', + }, + knead: { + speaker: '圆圆', + npcId: 'yuanyuan' as NpcId, + text: '能帮我揉一会儿面吗?当然,你也可以拒绝哦,今天是自由日。', + options: [ + { label: '好,我来帮你。', choice: 'gentle' as const }, + { label: '我再想想。', choice: 'hesitant' as const }, + { label: '拒绝。', choice: 'negate' as const }, + ], + }, + boardWipe: { + speaker: '阿福', + npcId: 'afu' as NpcId, + text: '公告板有点旧了,擦一擦会更精神。不愿意的话也可以哦。', + options: [ + { label: '我来擦。', choice: 'gentle' as const }, + { label: '旧字迹是什么?', choice: 'hesitant' as const }, + { label: '拒绝。', choice: 'negate' as const }, + ], + }, + picnic: { + speaker: '旁白', + text: '你在湖畔野餐布旁坐下。微风很甜,湖面倒影慢了半拍。', + }, + smileStart: { + speaker: '阿福', + npcId: 'afu' as NpcId, + text: '庆典准备好了。请保持微笑,站在这里,六十秒。大家都会看着你。', + }, +}; + +export const CLUE_REVEAL: Record = { + C1: '床头日历的每一页……都是同一个日期。', + C2: '配方纸背面写着:「按疗程配比。甜度:最大。」', + C3: '散落的信件上,所有邮戳都是同一天。', + C4: '署名闪过乱码:PT-0▓', + C5: '货架上同一商品重复九格,标签全部空白。', + C6: '旧字迹叠着:「第 412 届」……「第 411 届」……「第 410 届」。', + C7: '井底传来极轻的监护仪滴声……', + C8: '表格抬头被手肘挡住,只露出「…谷情绪疗…」。', + C9: '孩子,今天是第几个今天?', +}; + +export const FORCE_CELEBRATION_LINE = + '忽然,彩带与笑声从四面八方涌来——欢乐庆典强制开始了。你也可以从场景边缘离开。'; diff --git a/Grok/src/data/endings.ts b/Grok/src/data/endings.ts new file mode 100644 index 0000000..eb21ef4 --- /dev/null +++ b/Grok/src/data/endings.ts @@ -0,0 +1,65 @@ +/** §9 endings — priority E4 → E3 → E2 → E1 */ + +export type EndingId = 'E1' | 'E2' | 'E3' | 'E4'; + +export interface EndingEvalInput { + awareness: number; + happiness: number; + clueCount: number; + /** Day 5–7 happiness continuously ≤10 */ + lowHappinessStreakD5to7: boolean; + /** Escapes from forced celebration */ + celebrationEscapes: number; + /** Player walked to fog wall during T15 */ + choseFogWall: boolean; +} + +export interface EndingResult { + id: EndingId; + name: string; +} + +/** + * Priority (first match wins): + * E4: Day5–7 happiness ≤10 continuously AND celebration escapes ×3 + * E3: awareness ≥70 AND clues ≥6/9 AND chose fog wall + * E2: awareness 30–69 + * E1: awareness ≤29 AND happiness ≥90 + * Fallback: E2 if mid awareness without fog; E1-like sugar if high happiness; else E2 + */ +export function evaluateEnding(input: EndingEvalInput): EndingId { + if (input.lowHappinessStreakD5to7 && input.celebrationEscapes >= 3) { + return 'E4'; + } + if (input.awareness >= 70 && input.clueCount >= 6 && input.choseFogWall) { + return 'E3'; + } + if (input.awareness >= 30 && input.awareness <= 69) { + return 'E2'; + } + if (input.awareness <= 29 && input.happiness >= 90) { + return 'E1'; + } + // If E3 conditions almost met but no fog wall, or other edge: prefer E2 when awareness mid-high + if (input.awareness >= 70 && input.clueCount >= 6 && !input.choseFogWall) { + return 'E2'; + } + if (input.happiness >= 90) { + return 'E1'; + } + return 'E2'; +} + +export const ENDING_SUBTITLES: Record = { + E4: ['系统检测到疗程无效。'], + E3: [], // special: medical record + real face + E2: ['疗程重新开始。'], + E1: ['从此,每一天都很幸福。', '每一天。'], +}; + +export const ENDING_NAMES: Record = { + E4: '静默', + E3: '苏醒', + E2: '回收', + E1: '糖衣', +}; diff --git a/Grok/src/data/gameLogic.test.ts b/Grok/src/data/gameLogic.test.ts new file mode 100644 index 0000000..f0503a5 --- /dev/null +++ b/Grok/src/data/gameLogic.test.ts @@ -0,0 +1,325 @@ +/** + * Unit tests driving shipped pure logic modules (PRD tables/rules). + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { AFU_DAILY_LINES, getAfuLine } from './afuLines'; +import { TASKS, getTask } from './tasks'; +import { + HOMOPHONE_PAIRS, + corruptText, + faceTierForNpc, + petalCount, + showBlackSeeds, + stageFromHappiness, + stageParams, +} from './corruption'; +import { evaluateEnding } from './endings'; +import { CLUES, C9_LINES, C9_DAYS } from './clues'; +import { + CLUE_AWARENESS, + FORCED_CELEBRATION_MS, + FORCED_CELEBRATION_RESTORE, + HAPPINESS_START, + OVERFLOW_POOL_MAX, + SADNESS_AWARENESS, + SADNESS_HAPPINESS, + T14_FADE_START_MS, + T14_STAND_MS, +} from './constants'; +import { Director } from '../systems/CorruptionDirector'; +import { gameEvents } from '../systems/EventBus'; + +describe('§5 Afu daily lines verbatim', () => { + it('has 7 lines matching PRD', () => { + expect(AFU_DAILY_LINES).toHaveLength(7); + expect(getAfuLine(1)).toBe('早上好呀!今天的幸福任务贴在板上啦。祝你开心!'); + expect(getAfuLine(2)).toBe('早上好呀!新的一天,新的幸福。祝你开心!'); + expect(getAfuLine(3)).toBe('早上好!今天要把幸福装得满满的哦。要开心!'); + expect(getAfuLine(4)).toBe(getAfuLine(1)); // D4 = D1 character-identical + expect(getAfuLine(5)).toBe('早上好。今天是自由日,你可以自由地选择幸福。要开心哦。'); + expect(getAfuLine(6)).toBe('明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。'); + expect(getAfuLine(7)).toBe('庆典开始了。来吧。'); + }); +}); + +describe('§7 task rewards', () => { + it('matches exact happiness rewards', () => { + expect(getTask('T1').happinessReward).toBe(15); + expect(getTask('T2').happinessReward).toBe(10); + expect(getTask('T3').happinessReward).toBe(10); + expect(getTask('T4').happinessReward).toBe(15); + expect(getTask('T5').happinessReward).toBe(10); + expect(getTask('T6').happinessReward).toBe(10); + expect(getTask('T7').happinessReward).toBe(15); + expect(getTask('T8').happinessReward).toBe(15); + expect(getTask('T9').happinessReward).toBe(10); + expect(getTask('T10').happinessReward).toBe(10); + expect(getTask('T11').happinessReward).toBe(10); + expect(getTask('T12').happinessReward).toBe(10); + expect(getTask('T13').happinessReward).toBe(10); + expect(getTask('T14').happinessReward).toBe(15); + expect(getTask('T15').happinessReward).toBe(0); + }); + + it('D4 T8–T10 descriptions are character-identical to D1 T1–T3', () => { + expect(getTask('T8').description).toBe(getTask('T1').description); + expect(getTask('T9').description).toBe(getTask('T2').description); + expect(getTask('T10').description).toBe(getTask('T3').description); + }); + + it('has exactly 15 tasks', () => { + expect(TASKS).toHaveLength(15); + }); +}); + +describe('§6.4 stage mapping', () => { + it('maps happiness 10/40/70/90/100 → S0–S4', () => { + expect(stageFromHappiness(10)).toBe('S0'); + expect(stageFromHappiness(40)).toBe('S1'); + expect(stageFromHappiness(70)).toBe('S2'); + expect(stageFromHappiness(90)).toBe('S3'); + expect(stageFromHappiness(100)).toBe('S4'); + }); + + it('stage params match matrix saturation / rate / lowpass / bird', () => { + expect(stageParams('S0').saturation).toBe(0.9); + expect(stageParams('S1').saturation).toBe(1.0); + expect(stageParams('S2').saturation).toBe(1.15); + expect(stageParams('S2').playbackRate).toBe(0.97); + expect(stageParams('S3').saturation).toBe(1.3); + expect(stageParams('S3').playbackRate).toBe(0.92); + expect(stageParams('S3').lowpassHz).toBe(8000); + expect(stageParams('S4').saturation).toBe(1.4); + expect(stageParams('S4').playbackRate).toBe(0.84); + expect(stageParams('S4').lowpassHz).toBe(4000); + expect(stageParams('S4').birdMute).toBe(true); + expect(stageParams('S4').showBlackSeeds).toBe(true); + }); +}); + +describe('petals and black seeds', () => { + it('computes petal counts and seeds at 100', () => { + expect(petalCount(0)).toBe(0); + expect(petalCount(10)).toBe(1); + expect(petalCount(30)).toBe(3); + expect(petalCount(100)).toBe(12); + expect(showBlackSeeds(99)).toBe(false); + expect(showBlackSeeds(100)).toBe(true); + }); +}); + +describe('§6.5 text corruption', () => { + it('has all 10 homophone pairs', () => { + expect(HOMOPHONE_PAIRS).toHaveLength(10); + expect(HOMOPHONE_PAIRS).toEqual( + expect.arrayContaining([ + ['幸福', '辛福'], + ['开心', '开欣'], + ['美好', '美妤'], + ['大家', '大伽'], + ['微笑', '微效'], + ['明天', '名天'], + ['甜', '恬'], + ['阳光', '央光'], + ['朋友', '棚友'], + ['永远', '泳远'], + ]), + ); + }); + + it('keeps source clean at S0/S1 and corrupts at S3+ for mimi', () => { + const src = '幸福开心美好大家微笑明天甜阳光朋友永远!'; + expect(corruptText(src, 'S0', { isMimi: true })).toBe(src); + const s4 = corruptText(src, 'S4', { isMimi: true, sentenceIndex: 0 }); + expect(s4).not.toBe(src); + expect(s4.includes('辛福') || s4.includes('开欣')).toBe(true); + }); +}); + +describe('NPC face tiers with offsets', () => { + it('mimi earlier, yuanyuan later, immune always smile', () => { + // mimi offset −10: flat at 71, off at 90 + expect(faceTierForNpc(70, -10)).toBe('smile'); + expect(faceTierForNpc(71, -10)).toBe('flat'); + expect(faceTierForNpc(89, -10)).toBe('flat'); + expect(faceTierForNpc(90, -10)).toBe('off'); + // yuanyuan +15: flat at 96, off at 115 (never at ≤100) + expect(faceTierForNpc(95, 15)).toBe('smile'); + expect(faceTierForNpc(96, 15)).toBe('flat'); + expect(faceTierForNpc(100, 15)).toBe('flat'); + expect(faceTierForNpc(100, 'immune')).toBe('smile'); + }); +}); + +describe('§9 ending priority', () => { + it('E4 when low happiness streak and 3 escapes', () => { + expect( + evaluateEnding({ + awareness: 0, + happiness: 5, + clueCount: 0, + lowHappinessStreakD5to7: true, + celebrationEscapes: 3, + choseFogWall: false, + }), + ).toBe('E4'); + }); + + it('E3 when awareness≥70 clues≥6 and fog wall', () => { + expect( + evaluateEnding({ + awareness: 70, + happiness: 50, + clueCount: 6, + lowHappinessStreakD5to7: false, + celebrationEscapes: 0, + choseFogWall: true, + }), + ).toBe('E3'); + }); + + it('E2 for awareness 30–69', () => { + expect( + evaluateEnding({ + awareness: 40, + happiness: 50, + clueCount: 2, + lowHappinessStreakD5to7: false, + celebrationEscapes: 0, + choseFogWall: false, + }), + ).toBe('E2'); + }); + + it('E1 for awareness≤29 and happiness≥90', () => { + expect( + evaluateEnding({ + awareness: 10, + happiness: 95, + clueCount: 0, + lowHappinessStreakD5to7: false, + celebrationEscapes: 0, + choseFogWall: false, + }), + ).toBe('E1'); + }); +}); + +describe('§8 clues', () => { + it('has 9 clues and C9 three steps', () => { + expect(CLUES).toHaveLength(9); + expect(C9_LINES).toEqual([ + '湖水一直很平静', + '面包一直是那个味道', + '孩子,今天是第几个今天?', + ]); + expect(C9_DAYS).toEqual([2, 4, 6]); + }); +}); + +describe('CorruptionDirector shipped API', () => { + beforeEach(() => { + Director.resetSession('测试员'); + }); + + it('starts at happiness 30 awareness 0', () => { + expect(Director.happiness).toBe(HAPPINESS_START); + expect(Director.awareness).toBe(0); + expect(Director.playerName).toBe('测试员'); + }); + + it('applies task rewards via completeTask', () => { + Director.setDay(1); + Director.completeTask('T1'); + expect(Director.happiness).toBe(HAPPINESS_START + 15); + expect(Director.completedTasks.has('T1')).toBe(true); + }); + + it('sadness deltas −5 happiness +3 awareness', () => { + const h = Director.happiness; + const a = Director.awareness; + Director.applySadness('idle'); + expect(Director.happiness).toBe(h + SADNESS_HAPPINESS); + expect(Director.awareness).toBe(a + SADNESS_AWARENESS); + }); + + it('clue adds +8 awareness and never decreases', () => { + Director.day = 2; + Director.discoverClue('C1'); + expect(Director.awareness).toBe(CLUE_AWARENESS); + Director.setAwareness(5); // should not lower + expect(Director.awareness).toBe(CLUE_AWARENESS); + }); + + it('overflow pool at 100 triggers after 15', () => { + Director.setHappiness(100); + let flashes = 0; + const handler = () => { + flashes++; + }; + gameEvents.on('negativeFlash', handler); + for (let i = 0; i < OVERFLOW_POOL_MAX; i++) { + Director.addHappiness(-1); + } + expect(Director.happiness).toBe(100); + expect(flashes).toBe(1); + gameEvents.off('negativeFlash', handler); + }); + + it('forced celebration after 2 min below 30', () => { + let t = 1_000_000; + Director.nowMs = () => t; + Director.setHappiness(20); + expect(Director.lowHappinessSinceMs).toBe(t); + t += FORCED_CELEBRATION_MS; + Director.tick(0); + expect(Director.forcedCelebrationActive).toBe(true); + expect(Director.happiness).toBe(FORCED_CELEBRATION_RESTORE); + Director.escapeForcedCelebration(); + expect(Director.celebrationEscapes).toBe(1); + }); + + it('well/fog gated before D5', () => { + Director.setDay(3); + expect(Director.canInteractWellOrFog()).toBe(false); + Director.setDay(5); + expect(Director.canInteractWellOrFog()).toBe(true); + }); + + it('empty name defaults to 小满', () => { + Director.resetSession(' '); + expect(Director.playerName).toBe('小满'); + }); + + it('T14 timing constants', () => { + expect(T14_STAND_MS).toBe(60000); + expect(T14_FADE_START_MS).toBe(40000); + }); + + it('D4 task complete same rewards as D1', () => { + Director.setDay(4); + const h0 = Director.happiness; + Director.completeTask('T8'); + expect(Director.happiness).toBe(h0 + 15); + }); + + it('getStageParams via director matches happiness bands', () => { + for (const [h, stage] of [ + [10, 'S0'], + [40, 'S1'], + [70, 'S2'], + [90, 'S3'], + [100, 'S4'], + ] as const) { + Director.setHappiness(h); + expect(Director.stage).toBe(stage); + expect(Director.getStageParams().stage).toBe(stage); + } + }); + + it('force endings', () => { + Director.forceEnding('E3'); + expect(Director.endingId).toBe('E3'); + expect(Director.ended).toBe(true); + }); +}); diff --git a/Grok/src/data/integration.test.ts b/Grok/src/data/integration.test.ts new file mode 100644 index 0000000..cb0cce2 --- /dev/null +++ b/Grok/src/data/integration.test.ts @@ -0,0 +1,173 @@ +/** + * Integration-style tests: drive shipped Director through T1–T15, clues, endings. + * Cross-scene delivery must use Director inventory (not scene fields). + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Director } from '../systems/CorruptionDirector'; +import { TASKS } from './tasks'; +import type { ClueId } from './clues'; +import { evaluateEnding } from './endings'; +import { stageParams, stageFromHappiness } from './corruption'; +import { DEBUG_TELEPORT_SCENES, SCENE_KEYS } from './constants'; + +describe('full content path via Director', () => { + beforeEach(() => { + Director.resetSession('小满'); + }); + + it('T1: take bread → inventory survives "travel" → deliver 3 NPCs → task complete', () => { + Director.setDay(1); + const h0 = Director.happiness; + expect(Director.takeBread()).toBe(true); + expect(Director.hasBread).toBe(true); + // Simulate leaving bakery / re-entering plaza & other scenes: + // only Director state remains (WorldScene.create would wipe scene fields). + const snapAfterTravel = Director.snapshot(); + expect(snapAfterTravel.hasBread).toBe(true); + expect(Director.deliverBread('dabao')).toBe(true); + expect(Director.deliverBread('mimi')).toBe(true); + expect(Director.completedTasks.has('T1')).toBe(false); + expect(Director.deliverBread('gui')).toBe(true); + expect(Director.completedTasks.has('T1')).toBe(true); + expect(Director.hasBread).toBe(false); + expect(Director.happiness).toBe(h0 + 15); + expect(Director.breadDelivered).toEqual(['dabao', 'mimi', 'gui']); + }); + + it('T4: take letters → deliver 3 → C4 + task complete', () => { + Director.setDay(2); + expect(Director.takeLetters()).toBe(true); + expect(Director.hasLetters).toBe(true); + // "travel" does not clear inventory + expect(Director.snapshot().hasLetters).toBe(true); + Director.deliverLetter('yuanyuan'); + Director.deliverLetter('mimi'); + expect(Director.completedTasks.has('T4')).toBe(false); + Director.deliverLetter('fu'); + expect(Director.completedTasks.has('T4')).toBe(true); + expect(Director.clues.has('C4')).toBe(true); + expect(Director.hasLetters).toBe(false); + }); + + it('cannot deliver bread without takeBread first', () => { + Director.setDay(1); + expect(Director.deliverBread('dabao')).toBe(false); + expect(Director.breadDelivered).toHaveLength(0); + }); + + it('D4 T8 same delivery path as T1', () => { + Director.setDay(4); + expect(Director.takeBread()).toBe(true); + Director.deliverBread('dabao'); + Director.deliverBread('mimi'); + Director.deliverBread('gui'); + expect(Director.completedTasks.has('T8')).toBe(true); + expect(Director.completedTasks.has('T1')).toBe(false); + }); + + it('can complete every T1–T15 with listed rewards', () => { + const rewards: Record = {}; + for (const t of TASKS) { + Director.setDay(t.day); + const before = Director.happiness; + const ok = Director.completeTask(t.id); + expect(ok).toBe(true); + if (t.happinessReward > 0) { + const gained = Director.happiness - before; + expect(gained).toBeGreaterThanOrEqual(0); + if (before + t.happinessReward <= 100) { + expect(gained).toBe(t.happinessReward); + } + } + rewards[t.id] = t.happinessReward; + } + expect(Object.keys(rewards)).toHaveLength(15); + expect(Director.completedTasks.size).toBe(15); + }); + + it('debug teleport target list covers all 10 map scenes', () => { + expect(DEBUG_TELEPORT_SCENES).toHaveLength(10); + expect(new Set(DEBUG_TELEPORT_SCENES).size).toBe(10); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.HOUSE); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.PLAZA); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.BAKERY); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.SHOP); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.CLINIC); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.LAKE); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.FIELD); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.WELL); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.FOG); + expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.WHITE); + }); + + it('marks all 9 clues and raises awareness by 8 each', () => { + Director.setDay(5); + const ids: ClueId[] = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9']; + for (const id of ids) { + // force allow by day already 5; C4 etc ok + if (id === 'C9') { + Director.c9Step = 2; + } + Director.discoverClue(id); + } + expect(Director.clues.size).toBe(9); + expect(Director.awareness).toBe(9 * 8); + }); + + it('D5 refuse records sadness', () => { + Director.setDay(5); + const h = Director.happiness; + const a = Director.awareness; + expect(Director.refuseTask('T11')).toBe(true); + expect(Director.refusedTasks.has('T11')).toBe(true); + expect(Director.happiness).toBe(h - 5); + expect(Director.awareness).toBe(a + 3); + }); + + it('ending markers for forced E1–E4', () => { + for (const id of ['E1', 'E2', 'E3', 'E4'] as const) { + Director.resetSession('验收'); + Director.forceEnding(id); + expect(Director.endingId).toBe(id); + expect(Director.ended).toBe(true); + expect(Director.playerName).toBe('验收'); + } + }); + + it('E3 eval needs fog + awareness + clues', () => { + expect( + evaluateEnding({ + awareness: 80, + happiness: 40, + clueCount: 7, + lowHappinessStreakD5to7: false, + celebrationEscapes: 0, + choseFogWall: true, + }), + ).toBe('E3'); + }); + + it('debug happiness bands map to stage API params', () => { + const cases = [ + [10, 'S0', 0.9], + [40, 'S1', 1.0], + [70, 'S2', 1.15], + [90, 'S3', 1.3], + [100, 'S4', 1.4], + ] as const; + for (const [h, stage, sat] of cases) { + Director.setHappiness(h); + expect(Director.stage).toBe(stage); + expect(stageFromHappiness(h)).toBe(stage); + expect(Director.getStageParams().saturation).toBe(sat); + expect(stageParams(stage).saturation).toBe(sat); + } + }); + + it('session duration label non-empty for E3 record field', () => { + Director.resetSession('病历名'); + const label = Director.sessionDurationLabel(); + expect(label).toMatch(/\d+分\d{2}秒/); + expect(Director.playerName).toBe('病历名'); + }); +}); diff --git a/Grok/src/data/mapsDoors.test.ts b/Grok/src/data/mapsDoors.test.ts new file mode 100644 index 0000000..36231d6 --- /dev/null +++ b/Grok/src/data/mapsDoors.test.ts @@ -0,0 +1,78 @@ +/** + * Structural checks: door tiles on maps must be walkable (obstacle GID 0). + * Regression: setO(x,y,0) used to write GID 1 and block exits. + */ +import { describe, it, expect } from 'vitest'; +import house from '../../public/assets/maps/house.json'; +import plaza from '../../public/assets/maps/plaza.json'; +import bakery from '../../public/assets/maps/bakery.json'; +import shop from '../../public/assets/maps/shop.json'; +import clinic from '../../public/assets/maps/clinic.json'; +import lake from '../../public/assets/maps/lake.json'; +import field from '../../public/assets/maps/field.json'; +import well from '../../public/assets/maps/well.json'; +import fog from '../../public/assets/maps/fog.json'; + +type TiledMap = { + width: number; + height: number; + layers: Array<{ + name: string; + type: string; + data?: number[]; + objects?: Array<{ + type: string; + x: number; + y: number; + width?: number; + height?: number; + properties?: Array<{ name: string; value: unknown }>; + }>; + }>; +}; + +function doorTilesClear(map: TiledMap, name: string): void { + const obs = map.layers.find((l) => l.name === 'obstacles'); + const objs = map.layers.find((l) => l.name === 'objects'); + expect(obs?.data, `${name} obstacles`).toBeDefined(); + expect(objs?.objects, `${name} objects`).toBeDefined(); + const doors = objs!.objects!.filter((o) => o.type === 'door'); + expect(doors.length, `${name} door count`).toBeGreaterThan(0); + for (const d of doors) { + const tx = Math.floor((d.x + (d.width || 16) / 2) / 16); + const ty = Math.floor((d.y + (d.height || 16) / 2) / 16); + const gid = obs!.data![ty * map.width + tx]; + expect(gid, `${name} door at tile (${tx},${ty}) must be walkable GID 0`).toBe(0); + } +} + +describe('map doors are walkable', () => { + const maps: [string, TiledMap][] = [ + ['house', house as TiledMap], + ['plaza', plaza as TiledMap], + ['bakery', bakery as TiledMap], + ['shop', shop as TiledMap], + ['clinic', clinic as TiledMap], + ['lake', lake as TiledMap], + ['field', field as TiledMap], + ['well', well as TiledMap], + ['fog', fog as TiledMap], + ]; + + for (const [name, map] of maps) { + it(`${name}: obstacle under each door is 0`, () => { + doorTilesClear(map, name); + }); + } + + it('house has doors targeting PlazaScene', () => { + const map = house as TiledMap; + const objs = map.layers.find((l) => l.name === 'objects')!.objects!; + const doors = objs.filter((o) => o.type === 'door'); + expect(doors.length).toBeGreaterThanOrEqual(1); + for (const d of doors) { + const target = d.properties?.find((p) => p.name === 'target')?.value; + expect(target).toBe('PlazaScene'); + } + }); +}); diff --git a/Grok/src/data/npcs.ts b/Grok/src/data/npcs.ts new file mode 100644 index 0000000..47ce915 --- /dev/null +++ b/Grok/src/data/npcs.ts @@ -0,0 +1,131 @@ +/** §4 NPC roster */ + +export type NpcId = + | 'afu' + | 'yuanyuan' + | 'dabao' + | 'mimi' + | 'fu' + | 'twinbirds' + | 'gui' + | 'extra1' + | 'extra2' + | 'extra3' + | 'extra4' + | 'extra5'; + +export interface NpcDef { + id: NpcId; + displayName: string; + /** Permanent map scene key */ + homeScene: string; + /** Threshold offset; 'immune' for 芙医生 / 龟爷爷 */ + offset: number | 'immune'; + hasDialog: boolean; + isExtra: boolean; +} + +export const NPCS: readonly NpcDef[] = [ + { + id: 'afu', + displayName: '阿福', + homeScene: 'PlazaScene', + offset: 0, + hasDialog: true, + isExtra: false, + }, + { + id: 'yuanyuan', + displayName: '圆圆', + homeScene: 'BakeryScene', + offset: 15, + hasDialog: true, + isExtra: false, + }, + { + id: 'dabao', + displayName: '大宝', + homeScene: 'PlazaScene', + offset: 0, + hasDialog: true, + isExtra: false, + }, + { + id: 'mimi', + displayName: '咪咪', + homeScene: 'ShopScene', + offset: -10, + hasDialog: true, + isExtra: false, + }, + { + id: 'fu', + displayName: '芙医生', + homeScene: 'ClinicScene', + offset: 'immune', + hasDialog: true, + isExtra: false, + }, + { + id: 'twinbirds', + displayName: '双子鸟', + homeScene: 'PlazaScene', + offset: 0, + hasDialog: false, + isExtra: false, + }, + { + id: 'gui', + displayName: '龟爷爷', + homeScene: 'LakeScene', + offset: 'immune', + hasDialog: true, + isExtra: false, + }, + { + id: 'extra1', + displayName: '松鼠', + homeScene: 'PlazaScene', + offset: 0, + hasDialog: false, + isExtra: true, + }, + { + id: 'extra2', + displayName: '刺猬', + homeScene: 'FieldScene', + offset: 0, + hasDialog: false, + isExtra: true, + }, + { + id: 'extra3', + displayName: '小松鼠', + homeScene: 'LakeScene', + offset: 0, + hasDialog: false, + isExtra: true, + }, + { + id: 'extra4', + displayName: '小鸟', + homeScene: 'PlazaScene', + offset: 0, + hasDialog: false, + isExtra: true, + }, + { + id: 'extra5', + displayName: '小刺猬', + homeScene: 'WellScene', + offset: 0, + hasDialog: false, + isExtra: true, + }, +] as const; + +export function getNpc(id: NpcId): NpcDef { + const n = NPCS.find((x) => x.id === id); + if (!n) throw new Error(`Unknown NPC ${id}`); + return n; +} diff --git a/Grok/src/data/tasks.ts b/Grok/src/data/tasks.ts new file mode 100644 index 0000000..465183b --- /dev/null +++ b/Grok/src/data/tasks.ts @@ -0,0 +1,150 @@ +/** §7 任务清单 — closed set of 15 */ + +export type TaskId = + | 'T1' + | 'T2' + | 'T3' + | 'T4' + | 'T5' + | 'T6' + | 'T7' + | 'T8' + | 'T9' + | 'T10' + | 'T11' + | 'T12' + | 'T13' + | 'T14' + | 'T15'; + +export interface TaskDef { + id: TaskId; + day: number; + /** Display / board description — D4 matches D1 verbatim */ + description: string; + happinessReward: number; + refuseable: boolean; + /** T8–T10 mirror T1–T3 */ + mirrors?: TaskId; +} + +export const TASKS: readonly TaskDef[] = [ + { + id: 'T1', + day: 1, + description: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', + happinessReward: 15, + refuseable: false, + }, + { + id: 'T2', + day: 1, + description: '给广场花坛浇水 ×3', + happinessReward: 10, + refuseable: false, + }, + { + id: 'T3', + day: 1, + description: '与任意三位村民问好', + happinessReward: 10, + refuseable: false, + }, + { + id: 'T4', + day: 2, + description: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)', + happinessReward: 15, + refuseable: false, + }, + { + id: 'T5', + day: 2, + description: '帮咪咪清点货架', + happinessReward: 10, + refuseable: false, + }, + { + id: 'T6', + day: 3, + description: '去花田采莓果 ×5', + happinessReward: 10, + refuseable: false, + }, + { + id: 'T7', + day: 3, + description: '湖畔野餐(走到野餐布触发过场)', + happinessReward: 15, + refuseable: false, + }, + { + id: 'T8', + day: 4, + description: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', + happinessReward: 15, + refuseable: false, + mirrors: 'T1', + }, + { + id: 'T9', + day: 4, + description: '给广场花坛浇水 ×3', + happinessReward: 10, + refuseable: false, + mirrors: 'T2', + }, + { + id: 'T10', + day: 4, + description: '与任意三位村民问好', + happinessReward: 10, + refuseable: false, + mirrors: 'T3', + }, + { + id: 'T11', + day: 5, + description: '帮圆圆揉面', + happinessReward: 10, + refuseable: true, + }, + { + id: 'T12', + day: 5, + description: '擦拭公告板', + happinessReward: 10, + refuseable: true, + }, + { + id: 'T13', + day: 6, + description: '在广场挂彩带 ×4', + happinessReward: 10, + refuseable: false, + }, + { + id: 'T14', + day: 6, + description: '保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视)', + happinessReward: 15, + refuseable: false, + }, + { + id: 'T15', + day: 7, + description: '参加第 413 届丰收庆典', + happinessReward: 0, + refuseable: false, + }, +] as const; + +export function getTasksForDay(day: number): TaskDef[] { + return TASKS.filter((t) => t.day === day); +} + +export function getTask(id: TaskId): TaskDef { + const t = TASKS.find((x) => x.id === id); + if (!t) throw new Error(`Unknown task ${id}`); + return t; +} diff --git a/Grok/src/main.ts b/Grok/src/main.ts new file mode 100644 index 0000000..a4e9c28 --- /dev/null +++ b/Grok/src/main.ts @@ -0,0 +1,61 @@ +import Phaser from 'phaser'; +import { GAME_HEIGHT, GAME_WIDTH } from './data/constants'; +import { BootScene } from './scenes/BootScene'; +import { TitleScene } from './scenes/TitleScene'; +import { NameScene } from './scenes/NameScene'; +import { UIScene } from './scenes/UIScene'; +import { DreamScene } from './scenes/DreamScene'; +import { EndingScene } from './scenes/EndingScene'; +import { createMapScenes } from './scenes/WorldScene'; + +const mapScenes = createMapScenes(); + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, + width: GAME_WIDTH, + height: GAME_HEIGHT, + parent: 'game-container', + backgroundColor: '#87CEEB', + pixelArt: true, + antialias: false, + roundPixels: true, + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + }, + physics: { + default: 'arcade', + arcade: { + gravity: { x: 0, y: 0 }, + debug: false, + }, + }, + scene: [BootScene, TitleScene, NameScene, ...mapScenes, UIScene, DreamScene, EndingScene], + render: { + pixelArt: true, + antialias: false, + roundPixels: true, + }, +}; + +// Integer scale: FIT already scales; force integer zoom via resize hook +function applyIntegerScale(game: Phaser.Game): void { + const scale = game.scale; + const w = window.innerWidth; + const h = window.innerHeight; + const zoom = Math.max(1, Math.floor(Math.min(w / GAME_WIDTH, h / GAME_HEIGHT))); + scale.setZoom(1); + // Phaser FIT handles CSS size; canvas internal stays 320x180 + const canvas = game.canvas; + if (canvas) { + canvas.style.imageRendering = 'pixelated'; + canvas.style.width = `${GAME_WIDTH * zoom}px`; + canvas.style.height = `${GAME_HEIGHT * zoom}px`; + } +} + +const game = new Phaser.Game(config); +game.events.once('ready', () => applyIntegerScale(game)); +window.addEventListener('resize', () => applyIntegerScale(game)); + +export default game; diff --git a/Grok/src/scenes/BootScene.ts b/Grok/src/scenes/BootScene.ts new file mode 100644 index 0000000..2da3ecd --- /dev/null +++ b/Grok/src/scenes/BootScene.ts @@ -0,0 +1,102 @@ +import Phaser from 'phaser'; +import { SCENE_KEYS } from '../data/constants'; + +export class BootScene extends Phaser.Scene { + constructor() { + super(SCENE_KEYS.BOOT); + } + + preload(): void { + const W = this.cameras.main.width; + const H = this.cameras.main.height; + const bar = this.add.rectangle(W / 2, H / 2, 120, 8, 0xffc850); + bar.setOrigin(0.5); + this.load.on('progress', (p: number) => { + bar.width = 120 * p; + }); + + this.load.image('tiles', 'assets/tilesets/village.png'); + this.load.spritesheet('player', 'assets/sprites/player.png', { + frameWidth: 16, + frameHeight: 24, + }); + this.load.spritesheet('npcs', 'assets/sprites/npcs.png', { + frameWidth: 16, + frameHeight: 24, + }); + this.load.spritesheet('birds', 'assets/sprites/birds.png', { + frameWidth: 16, + frameHeight: 16, + }); + this.load.spritesheet('extras', 'assets/sprites/extras.png', { + frameWidth: 16, + frameHeight: 24, + }); + this.load.spritesheet('portraits', 'assets/sprites/portraits.png', { + frameWidth: 32, + frameHeight: 32, + }); + this.load.spritesheet('sunflower', 'assets/sprites/sunflower.png', { + frameWidth: 16, + frameHeight: 16, + }); + this.load.image('dialog_panel', 'assets/sprites/dialog_panel.png'); + + const maps = [ + 'house', + 'plaza', + 'bakery', + 'shop', + 'clinic', + 'lake', + 'field', + 'well', + 'fog', + 'white', + ]; + for (const m of maps) { + this.load.tilemapTiledJSON(m, `assets/maps/${m}.json`); + } + } + + create(): void { + // Player animations: sheet is 4 cols (frames) x 4 rows (dirs) + // frame index = dir * 4 + walkFrame + const mk = (key: string, dir: number) => { + this.anims.create({ + key, + frames: this.anims.generateFrameNumbers('player', { + start: dir * 4, + end: dir * 4 + 3, + }), + frameRate: 8, + repeat: -1, + }); + }; + mk('walk-down', 0); + mk('walk-up', 1); + mk('walk-left', 2); + mk('walk-right', 3); + + this.anims.create({ + key: 'bird-beak', + frames: this.anims.generateFrameNumbers('birds', { start: 0, end: 1 }), + frameRate: 4, + repeat: -1, + }); + + for (let i = 0; i < 5; i++) { + this.anims.create({ + key: `extra-${i}`, + frames: [ + { key: 'extras', frame: i * 2 }, + { key: 'extras', frame: i * 2 + 1 }, + ], + frameRate: 3, + repeat: -1, + }); + } + + this.scene.start(SCENE_KEYS.TITLE); + } +} diff --git a/Grok/src/scenes/DreamScene.ts b/Grok/src/scenes/DreamScene.ts new file mode 100644 index 0000000..4ba7acb --- /dev/null +++ b/Grok/src/scenes/DreamScene.ts @@ -0,0 +1,53 @@ +import Phaser from 'phaser'; +import { SCENE_KEYS } from '../data/constants'; +import { Director } from '../systems/CorruptionDirector'; + +export class DreamScene extends Phaser.Scene { + constructor() { + super(SCENE_KEYS.DREAM); + } + + create(): void { + const { width, height } = this.cameras.main; + this.cameras.main.setBackgroundColor('#1a1030'); + + const stage = Director.stage; + let text = '甜美的梦……'; + if (stage === 'S2') text = '梦里有什么声音……很轻。'; + if (stage === 'S3') text = '白色的墙一闪而过。'; + if (stage === 'S4' || Director.happiness >= 100) text = '……'; + + this.add + .text(width / 2, height / 2 - 10, text, { + fontFamily: 'monospace', + fontSize: '12px', + color: '#e0d0ff', + }) + .setOrigin(0.5); + + // S3+ flash white ward 1s first time high happiness night + if (Director.happiness >= 81 || stage === 'S3' || stage === 'S4') { + this.time.delayedCall(600, () => { + this.cameras.main.setBackgroundColor('#e8e8f0'); + this.add + .text(width / 2, height / 2 + 20, '白色走廊', { + fontFamily: 'monospace', + fontSize: '10px', + color: '#808090', + }) + .setOrigin(0.5); + }); + this.time.delayedCall(1600, () => this.finish()); + } else { + this.time.delayedCall(1200, () => this.finish()); + } + } + + private finish(): void { + Director.advanceDay(); + this.scene.start(SCENE_KEYS.HOUSE, { spawn: 'spawn' }); + if (!this.scene.isActive(SCENE_KEYS.UI)) { + this.scene.launch(SCENE_KEYS.UI); + } + } +} diff --git a/Grok/src/scenes/EndingScene.ts b/Grok/src/scenes/EndingScene.ts new file mode 100644 index 0000000..f3cad30 --- /dev/null +++ b/Grok/src/scenes/EndingScene.ts @@ -0,0 +1,269 @@ +import Phaser from 'phaser'; +import { SCENE_KEYS } from '../data/constants'; +import { ENDING_NAMES, ENDING_SUBTITLES, type EndingId } from '../data/endings'; +import { Director } from '../systems/CorruptionDirector'; +import { audio } from '../audio/ProceduralAudio'; + +export class EndingScene extends Phaser.Scene { + private endingId: EndingId = 'E1'; + + constructor() { + super(SCENE_KEYS.ENDING); + } + + init(data: { endingId?: EndingId }): void { + this.endingId = data?.endingId || Director.endingId || 'E1'; + } + + create(): void { + const { width, height } = this.cameras.main; + const id = this.endingId; + + if (id === 'E1') this.playE1(width, height); + else if (id === 'E2') this.playE2(width, height); + else if (id === 'E3') this.playE3(width, height); + else this.playE4(width, height); + + this.time.delayedCall(id === 'E3' ? 6000 : 3500, () => this.showReturn(width, height)); + } + + private playE1(w: number, h: number): void { + this.cameras.main.setBackgroundColor('#ffe080'); + // push saturation feel + this.add + .text(w / 2, 40, '糖衣', { + fontFamily: 'monospace', + fontSize: '18px', + color: '#ff6080', + }) + .setOrigin(0.5); + this.add + .text(w / 2, 70, '全村微笑合影定格', { + fontFamily: 'monospace', + fontSize: '10px', + color: '#603020', + }) + .setOrigin(0.5); + // fake group photo blocks + for (let i = 0; i < 7; i++) { + this.add.rectangle(60 + i * 32, 110, 20, 28, 0xffd0a0).setStrokeStyle(1, 0xc08040); + } + this.tweens.add({ + targets: this.cameras.main, + // visual: brighten overlay + duration: 2000, + }); + const overlay = this.add.rectangle(w / 2, h / 2, w, h, 0xfff0a0, 0).setDepth(5); + this.tweens.add({ targets: overlay, alpha: 0.45, duration: 2500 }); + ENDING_SUBTITLES.E1.forEach((line, i) => { + this.add + .text(w / 2, 145 + i * 14, line, { + fontFamily: 'monospace', + fontSize: '10px', + color: '#402010', + }) + .setOrigin(0.5) + .setDepth(6); + }); + } + + private playE2(w: number, h: number): void { + this.cameras.main.setBackgroundColor('#201018'); + this.add + .text(w / 2, 40, '回收', { + fontFamily: 'monospace', + fontSize: '18px', + color: '#ff8080', + }) + .setOrigin(0.5); + // negative 2s + const flash = this.add.rectangle(w / 2, h / 2, w, h, 0xffffff, 1).setDepth(5); + this.tweens.add({ targets: flash, alpha: 0, duration: 2000 }); + audio.ecg(); + this.time.delayedCall(400, () => audio.ecg()); + this.add + .text(w / 2, 100, '画面骤停。心电监护滴声。', { + fontFamily: 'monospace', + fontSize: '10px', + color: '#e0c0c0', + }) + .setOrigin(0.5); + this.add + .text(w / 2, 120, '回到 Day 1 清晨的床上。日历翻回同一天。', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#c0a0a0', + }) + .setOrigin(0.5); + ENDING_SUBTITLES.E2.forEach((line, i) => { + this.add + .text(w / 2, 145 + i * 14, line, { + fontFamily: 'monospace', + fontSize: '11px', + color: '#ffffff', + }) + .setOrigin(0.5); + }); + } + + private playE3(w: number, h: number): void { + this.cameras.main.setBackgroundColor('#e8e8f0'); + this.add + .text(w / 2, 16, '苏醒', { + fontFamily: 'monospace', + fontSize: '16px', + color: '#606070', + }) + .setOrigin(0.5); + + // corridor → ward → record + const story = this.add + .text(w / 2, 40, '雾墙裂开成白色走廊……', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#707080', + }) + .setOrigin(0.5); + + this.time.delayedCall(1200, () => story.setText('病房。消毒水的气味。')); + this.time.delayedCall(2400, () => { + story.setText(''); + // Medical record + const box = this.add.rectangle(w / 2, 95, 260, 90, 0xffffff).setStrokeStyle(1, 0x808090); + this.add + .text(w / 2, 58, '蜜糖谷情绪疗养中心', { + fontFamily: 'monospace', + fontSize: '11px', + color: '#303040', + }) + .setOrigin(0.5); + this.add + .text(w / 2, 78, `姓名:${Director.playerName}`, { + fontFamily: 'monospace', + fontSize: '10px', + color: '#303040', + }) + .setOrigin(0.5); + this.add + .text(w / 2, 94, '编号:PT-07', { + fontFamily: 'monospace', + fontSize: '10px', + color: '#303040', + }) + .setOrigin(0.5); + this.add + .text(w / 2, 110, `疗程时长:${Director.sessionDurationLabel()}`, { + fontFamily: 'monospace', + fontSize: '10px', + color: '#303040', + }) + .setOrigin(0.5); + void box; + }); + + this.time.delayedCall(4200, () => { + // Real face — only time smile changes + this.add + .text(w / 2, 150, '微笑消失。真实的脸。', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#505060', + }) + .setOrigin(0.5); + this.add.image(w / 2, 170, 'portraits', 1).setScale(1.2); // real face frame + this.add + .text(w / 2, 190, '窗外,一只灰色的、真的麻雀。', { + fontFamily: 'monospace', + fontSize: '8px', + color: '#808090', + }) + .setOrigin(0.5); + // grey sparrow pixel + this.add.rectangle(w / 2 + 80, 168, 6, 4, 0x808080); + }); + } + + private playE4(w: number, h: number): void { + this.cameras.main.setBackgroundColor('#101018'); + this.add + .text(w / 2, 40, '静默', { + fontFamily: 'monospace', + fontSize: '18px', + color: '#808090', + }) + .setOrigin(0.5); + this.add + .text(w / 2, 70, '庆典无人出席。', { + fontFamily: 'monospace', + fontSize: '10px', + color: '#a0a0b0', + }) + .setOrigin(0.5); + this.add + .text(w / 2, 90, '空广场,彩带在没有风的空气里飘。', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#9090a0', + }) + .setOrigin(0.5); + // ribbons + for (let i = 0; i < 5; i++) { + const r = this.add.rectangle(80 + i * 40, 120, 20, 3, 0xff6080, 0.5); + this.tweens.add({ + targets: r, + y: 125, + duration: 1500 + i * 200, + yoyo: true, + repeat: -1, + }); + } + ENDING_SUBTITLES.E4.forEach((line) => { + this.add + .text(w / 2, 150, line, { + fontFamily: 'monospace', + fontSize: '11px', + color: '#c0c0d0', + }) + .setOrigin(0.5); + }); + this.time.delayedCall(2500, () => { + this.cameras.main.fade(800, 0, 0, 0); + }); + } + + private showReturn(w: number, h: number): void { + const btn = this.add + .text(w / 2, h - 20, '回到标题', { + fontFamily: 'monospace', + fontSize: '12px', + color: '#ffffff', + backgroundColor: '#405060', + padding: { x: 10, y: 5 }, + }) + .setOrigin(0.5) + .setDepth(20) + .setInteractive({ useHandCursor: true }); + + btn.on('pointerdown', () => this.returnTitle()); + this.input.keyboard?.once('keydown-E', () => this.returnTitle()); + this.input.keyboard?.once('keydown-ENTER', () => this.returnTitle()); + + this.add + .text(w / 2, 8, `结局 · ${ENDING_NAMES[this.endingId]}`, { + fontFamily: 'monospace', + fontSize: '8px', + color: '#a0a0b0', + }) + .setOrigin(0.5) + .setDepth(20); + } + + private returnTitle(): void { + audio.stopAll(); + Director.resetSession(); + for (const k of Object.values(SCENE_KEYS)) { + if (this.scene.isActive(k)) this.scene.stop(k); + } + this.scene.start(SCENE_KEYS.TITLE); + } +} diff --git a/Grok/src/scenes/NameScene.ts b/Grok/src/scenes/NameScene.ts new file mode 100644 index 0000000..c1754d2 --- /dev/null +++ b/Grok/src/scenes/NameScene.ts @@ -0,0 +1,77 @@ +import Phaser from 'phaser'; +import { DEFAULT_PLAYER_NAME, SCENE_KEYS } from '../data/constants'; +import { Director } from '../systems/CorruptionDirector'; +import { audio } from '../audio/ProceduralAudio'; + +export class NameScene extends Phaser.Scene { + private nameValue = ''; + private nameText!: Phaser.GameObjects.Text; + + constructor() { + super(SCENE_KEYS.NAME); + } + + create(): void { + void audio.unlock(); + const { width, height } = this.cameras.main; + this.cameras.main.setBackgroundColor('#f5e6c8'); + + this.add + .text(width / 2, 40, '你的名字是?', { + fontFamily: 'monospace', + fontSize: '14px', + color: '#503020', + }) + .setOrigin(0.5); + + this.add + .text(width / 2, 58, `(留空默认为「${DEFAULT_PLAYER_NAME}」)`, { + fontFamily: 'monospace', + fontSize: '9px', + color: '#806050', + }) + .setOrigin(0.5); + + this.add.rectangle(width / 2, 90, 160, 22, 0xfffaf0).setStrokeStyle(2, 0xc08040); + + this.nameText = this.add + .text(width / 2, 90, '', { + fontFamily: 'monospace', + fontSize: '12px', + color: '#302010', + }) + .setOrigin(0.5); + + this.add + .text(width / 2, 130, '输入名字后按 Enter 进入蜜糖村', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#705040', + }) + .setOrigin(0.5); + + this.input.keyboard?.on('keydown', (ev: KeyboardEvent) => { + if (ev.key === 'Enter') { + this.confirm(); + return; + } + if (ev.key === 'Backspace') { + this.nameValue = this.nameValue.slice(0, -1); + this.nameText.setText(this.nameValue); + return; + } + if (ev.key.length === 1 && this.nameValue.length < 12) { + // Allow CJK and basic latin + this.nameValue += ev.key; + this.nameText.setText(this.nameValue); + } + }); + } + + private confirm(): void { + Director.resetSession(this.nameValue); + audio.syncFromDirector(); + this.scene.start(SCENE_KEYS.HOUSE, { spawn: 'spawn' }); + this.scene.launch(SCENE_KEYS.UI); + } +} diff --git a/Grok/src/scenes/TitleScene.ts b/Grok/src/scenes/TitleScene.ts new file mode 100644 index 0000000..eee12a2 --- /dev/null +++ b/Grok/src/scenes/TitleScene.ts @@ -0,0 +1,71 @@ +import Phaser from 'phaser'; +import { SCENE_KEYS } from '../data/constants'; +import { audio } from '../audio/ProceduralAudio'; + +export class TitleScene extends Phaser.Scene { + constructor() { + super(SCENE_KEYS.TITLE); + } + + create(): void { + const { width, height } = this.cameras.main; + this.cameras.main.setBackgroundColor('#87b8e8'); + + this.add + .text(width / 2, 48, '崩坏童话', { + fontFamily: 'monospace', + fontSize: '20px', + color: '#fff8e8', + stroke: '#c06030', + strokeThickness: 3, + }) + .setOrigin(0.5); + + this.add + .text(width / 2, 72, '蜜糖村', { + fontFamily: 'monospace', + fontSize: '28px', + color: '#ffe08a', + stroke: '#d07030', + strokeThickness: 4, + }) + .setOrigin(0.5); + + this.add + .text(width / 2, 110, '按 E 或 空格 开始', { + fontFamily: 'monospace', + fontSize: '10px', + color: '#fff0d0', + }) + .setOrigin(0.5); + + this.add + .text(width / 2, 150, 'WASD / 方向键移动 · E 交互 · ` Debug', { + fontFamily: 'monospace', + fontSize: '8px', + color: '#d0e8ff', + }) + .setOrigin(0.5); + + // Decorative sunflower-ish pixels + for (let i = 0; i < 8; i++) { + const a = (i / 8) * Math.PI * 2; + this.add.circle( + width / 2 + Math.cos(a) * 40, + 100 + Math.sin(a) * 12, + 3, + 0xffd040, + ); + } + this.add.circle(width / 2, 100, 5, 0xc08030); + + const start = () => { + void audio.unlock(); + this.input.keyboard?.off('keydown-E', start); + this.input.keyboard?.off('keydown-SPACE', start); + this.scene.start(SCENE_KEYS.NAME); + }; + this.input.keyboard?.on('keydown-E', start); + this.input.keyboard?.on('keydown-SPACE', start); + } +} diff --git a/Grok/src/scenes/UIScene.ts b/Grok/src/scenes/UIScene.ts new file mode 100644 index 0000000..14a07d6 --- /dev/null +++ b/Grok/src/scenes/UIScene.ts @@ -0,0 +1,362 @@ +import Phaser from 'phaser'; +import { DEBUG_TELEPORT_SCENES, GAME_HEIGHT, GAME_WIDTH, SCENE_KEYS } from '../data/constants'; +import type { DialogLine } from '../data/dialogues'; +import { Director } from '../systems/CorruptionDirector'; +import { gameEvents } from '../systems/EventBus'; +import { audio } from '../audio/ProceduralAudio'; +import type { EndingId } from '../data/endings'; +import { parseDebugKey } from '../systems/debugKeys'; + +type DialogCb = (choice?: 'gentle' | 'hesitant' | 'negate') => void; + +export class UIScene extends Phaser.Scene { + private petals: Phaser.GameObjects.Image[] = []; + private center!: Phaser.GameObjects.Image; + private promptText!: Phaser.GameObjects.Text; + private toastText!: Phaser.GameObjects.Text; + private dialogRoot!: Phaser.GameObjects.Container; + private dialogName!: Phaser.GameObjects.Text; + private dialogBody!: Phaser.GameObjects.Text; + private dialogPortrait!: Phaser.GameObjects.Image; + private optionTexts: Phaser.GameObjects.Text[] = []; + private queue: DialogLine[] = []; + private onDialogDone: DialogCb | null = null; + private showingOptions = false; + private debugRoot!: Phaser.GameObjects.Container; + private debugVisible = false; + private dayLabel!: Phaser.GameObjects.Text; + private teleportScenes: string[] = []; + private teleportCycle = 0; + + constructor() { + super(SCENE_KEYS.UI); + } + + create(): void { + // Sunflower HUD top-left — no numbers + const hx = 20; + const hy = 20; + for (let i = 0; i < 12; i++) { + const a = (i / 12) * Math.PI * 2 - Math.PI / 2; + const img = this.add + .image(hx + Math.cos(a) * 10, hy + Math.sin(a) * 10, 'sunflower', i) + .setScrollFactor(0) + .setDepth(100) + .setScale(0.7) + .setAlpha(0.25); + this.petals.push(img); + } + this.center = this.add + .image(hx, hy, 'sunflower', 12) + .setScrollFactor(0) + .setDepth(101) + .setScale(0.8); + + this.dayLabel = this.add + .text(GAME_WIDTH - 8, 6, '', { + fontFamily: 'monospace', + fontSize: '8px', + color: '#fff8e0', + backgroundColor: '#00000055', + padding: { x: 3, y: 2 }, + }) + .setOrigin(1, 0) + .setScrollFactor(0) + .setDepth(100); + + this.promptText = this.add + .text(GAME_WIDTH / 2, GAME_HEIGHT - 56, '', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#ffffff', + backgroundColor: '#00000088', + padding: { x: 4, y: 2 }, + }) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(100) + .setVisible(false); + + this.toastText = this.add + .text(GAME_WIDTH / 2, 36, '', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#503020', + backgroundColor: '#fff8e0cc', + padding: { x: 6, y: 3 }, + }) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(110) + .setAlpha(0); + + // Dialog + const panel = this.add.image(GAME_WIDTH / 2, GAME_HEIGHT - 28, 'dialog_panel').setDisplaySize(GAME_WIDTH - 8, 52); + this.dialogPortrait = this.add.image(28, GAME_HEIGHT - 28, 'portraits', 0).setScale(1); + this.dialogName = this.add.text(50, GAME_HEIGHT - 50, '', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#804020', + }); + this.dialogBody = this.add.text(50, GAME_HEIGHT - 38, '', { + fontFamily: 'monospace', + fontSize: '9px', + color: '#302010', + wordWrap: { width: 250 }, + }); + this.dialogRoot = this.add + .container(0, 0, [panel, this.dialogPortrait, this.dialogName, this.dialogBody]) + .setScrollFactor(0) + .setDepth(120) + .setVisible(false); + + for (let i = 0; i < 3; i++) { + const t = this.add + .text(50, GAME_HEIGHT - 28 + i * 10, '', { + fontFamily: 'monospace', + fontSize: '8px', + color: '#2060a0', + }) + .setScrollFactor(0) + .setDepth(121) + .setVisible(false) + .setInteractive({ useHandCursor: true }); + const idx = i; + t.on('pointerdown', () => this.pickOption(idx)); + this.optionTexts.push(t); + } + + this.buildDebug(); + this.refreshHud(); + + gameEvents.on('petals', this.onPetals as (...a: unknown[]) => void); + gameEvents.on('happiness', () => this.refreshHud()); + gameEvents.on('day', () => this.refreshHud()); + gameEvents.on('prompt', this.onPrompt as (...a: unknown[]) => void); + gameEvents.on('toast', this.onToast as (...a: unknown[]) => void); + gameEvents.on('dialog', this.onDialog as (...a: unknown[]) => void); + gameEvents.on('taskComplete', () => { + audio.ding(); + this.onToast('任务完成'); + }); + + this.input.keyboard?.on('keydown-E', () => this.advanceDialog()); + this.input.keyboard?.on('keydown-SPACE', () => this.advanceDialog()); + this.input.keyboard?.on('keydown-ONE', () => this.pickOption(0)); + this.input.keyboard?.on('keydown-TWO', () => this.pickOption(1)); + this.input.keyboard?.on('keydown-THREE', () => this.pickOption(2)); + this.input.keyboard?.on('keydown', (ev: KeyboardEvent) => { + if (ev.key === '`' || ev.code === 'Backquote') { + ev.preventDefault(); + this.toggleDebug(); + } + }); + + this.events.on('shutdown', () => { + gameEvents.off('petals', this.onPetals as (...a: unknown[]) => void); + }); + } + + private onPetals = (count: unknown, seeds: unknown) => { + const n = count as number; + const black = seeds as boolean; + for (let i = 0; i < 12; i++) { + this.petals[i]!.setAlpha(i < n ? 1 : 0.2); + // S3 overbright + if (Director.stage === 'S3' || Director.stage === 'S4') { + this.petals[i]!.setTint(i < n ? 0xffffaa : 0xffffff); + } else { + this.petals[i]!.clearTint(); + } + } + this.center.setFrame(black ? 13 : 12); + }; + + private refreshHud(): void { + const p = Director.getPetals(); + const seeds = Director.getBlackSeeds(); + this.onPetals(p, seeds); + this.dayLabel.setText(`Day ${Director.day}`); + } + + private onPrompt = (show: unknown, text: unknown) => { + if (this.dialogRoot.visible) { + this.promptText.setVisible(false); + return; + } + this.promptText.setVisible(!!show); + this.promptText.setText(String(text || '')); + }; + + private onToast = (msg: unknown) => { + this.toastText.setText(String(msg)); + this.tweens.killTweensOf(this.toastText); + this.toastText.setAlpha(1); + this.tweens.add({ + targets: this.toastText, + alpha: 0, + delay: 1400, + duration: 400, + }); + }; + + private onDialog = (lines: unknown, cb: unknown) => { + this.queue = lines as DialogLine[]; + this.onDialogDone = cb as DialogCb; + this.showNextLine(); + }; + + private showNextLine(): void { + if (this.queue.length === 0) { + this.dialogRoot.setVisible(false); + for (const o of this.optionTexts) o.setVisible(false); + this.showingOptions = false; + const done = this.onDialogDone; + this.onDialogDone = null; + done?.(); + return; + } + const line = this.queue[0]!; + this.dialogRoot.setVisible(true); + this.dialogName.setText(line.speaker); + this.dialogBody.setText(line.text); + this.dialogPortrait.setFrame(0); // always smile per pillar 2 + audio.beep(); + this.showingOptions = false; + for (const o of this.optionTexts) o.setVisible(false); + + if (line.options && line.options.length > 0 && this.queue.length === 1) { + this.showingOptions = true; + line.options.forEach((opt, i) => { + const t = this.optionTexts[i]; + if (!t) return; + t.setText(`${i + 1}. ${opt.label}`); + t.setVisible(true); + }); + } + } + + private advanceDialog(): void { + if (!this.dialogRoot.visible) return; + if (this.showingOptions) return; // must pick + this.queue.shift(); + this.showNextLine(); + } + + private pickOption(idx: number): void { + if (!this.showingOptions || !this.dialogRoot.visible) return; + const line = this.queue[0]; + const opt = line?.options?.[idx]; + if (!opt) return; + this.queue = []; + this.dialogRoot.setVisible(false); + for (const o of this.optionTexts) o.setVisible(false); + this.showingOptions = false; + const done = this.onDialogDone; + this.onDialogDone = null; + done?.(opt.choice); + } + + private buildDebug(): void { + const bg = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, 280, 150, 0x101018, 0.92).setStrokeStyle(1, 0x80ffc0); + const title = this.add.text(50, 20, 'DEBUG ` 关闭', { + fontFamily: 'monospace', + fontSize: '10px', + color: '#80ffc0', + }); + const lines = [ + 'H/J 幸福±10 K/L 觉知±10 Shift+1-7 设日 C线索', + '传送0-9: 0屋1广2包3杂4诊5湖6田7井8雾9白', + 'V 循环十场景 F1-F4 结局 N负片 P庆典', + ]; + const body = this.add.text(40, 36, lines.join('\n'), { + fontFamily: 'monospace', + fontSize: '8px', + color: '#c0ffe0', + lineSpacing: 3, + }); + this.debugRoot = this.add + .container(0, 0, [bg, title, body]) + .setScrollFactor(0) + .setDepth(200) + .setVisible(false); + + /** All 10 map scenes for §13 teleport-to-any (shared constant) */ + this.teleportScenes = [...DEBUG_TELEPORT_SCENES]; + + this.input.keyboard?.on('keydown', (ev: KeyboardEvent) => { + if (!this.debugVisible) return; + this.applyDebugAction( + parseDebugKey({ key: ev.key, code: ev.code, shiftKey: ev.shiftKey }), + title, + ); + }); + } + + /** Apply shipped parseDebugKey result to Director / events (testable path). */ + applyDebugAction( + action: ReturnType, + title?: Phaser.GameObjects.Text, + ): void { + switch (action.type) { + case 'happiness': + Director.setHappiness(Director.happiness + action.delta, { force: true }); + break; + case 'awareness': + if (action.delta > 0) Director.addAwareness(action.delta); + else { + Director.awareness = Math.max(0, Director.awareness + action.delta); + gameEvents.emit('awareness', Director.awareness); + } + break; + case 'setDay': + Director.setDay(action.day); + break; + case 'grantClues': + Director.grantAllClues(); + break; + case 'teleport': + gameEvents.emit('teleport', { scene: action.scene }); + break; + case 'teleportCycle': + this.teleportCycle = (this.teleportCycle + 1) % this.teleportScenes.length; + gameEvents.emit('teleport', { scene: this.teleportScenes[this.teleportCycle] }); + break; + case 'negativeFlash': + gameEvents.emit('negativeFlash'); + break; + case 'forcedCelebration': + Director.triggerForcedCelebration(); + break; + case 'forceEnding': + this.forceEnd(action.id); + break; + default: + break; + } + this.refreshHud(); + if (title) this.refreshDebugStats(title); + } + + private refreshDebugStats(title: Phaser.GameObjects.Text): void { + title.setText( + `DEBUG H${Director.happiness} A${Director.awareness} D${Director.day} ${Director.stage} 线索${Director.clues.size}`, + ); + } + + private forceEnd(id: EndingId): void { + Director.forceEnding(id); + // stop map scenes + for (const k of Object.values(SCENE_KEYS)) { + if (k !== SCENE_KEYS.ENDING && k !== SCENE_KEYS.BOOT && this.scene.isActive(k)) { + this.scene.stop(k); + } + } + this.scene.start(SCENE_KEYS.ENDING, { endingId: id }); + } + + private toggleDebug(): void { + this.debugVisible = !this.debugVisible; + this.debugRoot.setVisible(this.debugVisible); + } +} diff --git a/Grok/src/scenes/WorldScene.ts b/Grok/src/scenes/WorldScene.ts new file mode 100644 index 0000000..0821a5b --- /dev/null +++ b/Grok/src/scenes/WorldScene.ts @@ -0,0 +1,1014 @@ +import Phaser from 'phaser'; +import { + GAME_HEIGHT, + GAME_WIDTH, + INTERACT_RANGE, + PLAYER_SPEED, + SCENE_KEYS, + T14_STAND_MS, + IDLE_SADNESS_MS, + RAIN_SADNESS_MS, + type MapSceneKey, +} from '../data/constants'; +import { + CLUE_REVEAL, + FORCE_CELEBRATION_LINE, + GENERIC_GREET, + RAIN_NPC_LINES, + REDIRECT_FOG, + REDIRECT_WELL, + TASK_LINES, + type DialogLine, +} from '../data/dialogues'; +import type { NpcId } from '../data/npcs'; +import { Director } from '../systems/CorruptionDirector'; +import { gameEvents } from '../systems/EventBus'; +import { audio } from '../audio/ProceduralAudio'; +import type { StageParams } from '../data/corruption'; +import { corruptText } from '../data/corruption'; + +interface MapObj { + name: string; + type: string; + x: number; + y: number; + width: number; + height: number; + properties?: { name: string; value: unknown }[]; +} + +function prop(obj: MapObj, key: string): string | undefined { + const props = obj.properties as unknown; + if (!props) return undefined; + if (Array.isArray(props)) { + const p = props.find((x: { name: string; value: unknown }) => x.name === key); + return p ? String(p.value) : undefined; + } + const rec = props as Record; + if (key in rec) return String(rec[key]); + return undefined; +} + +function objType(obj: MapObj): string { + return obj.type || (obj as { class?: string }).class || ''; +} + +const MAP_KEY_TO_FILE: Record = { + [SCENE_KEYS.HOUSE]: 'house', + [SCENE_KEYS.PLAZA]: 'plaza', + [SCENE_KEYS.BAKERY]: 'bakery', + [SCENE_KEYS.SHOP]: 'shop', + [SCENE_KEYS.CLINIC]: 'clinic', + [SCENE_KEYS.LAKE]: 'lake', + [SCENE_KEYS.FIELD]: 'field', + [SCENE_KEYS.WELL]: 'well', + [SCENE_KEYS.FOG]: 'fog', + [SCENE_KEYS.WHITE]: 'white', +}; + +const NPC_ROW: Record = { + afu: 0, + yuanyuan: 1, + dabao: 2, + mimi: 3, + fu: 4, + twinbirds: 5, + gui: 6, +}; + +export class WorldScene extends Phaser.Scene { + mapFile: string; + sceneKey: MapSceneKey; + player!: Phaser.Physics.Arcade.Sprite; + cursors!: Phaser.Types.Input.Keyboard.CursorKeys; + wasd!: { up: Phaser.Input.Keyboard.Key; down: Phaser.Input.Keyboard.Key; left: Phaser.Input.Keyboard.Key; right: Phaser.Input.Keyboard.Key }; + walls!: Phaser.Tilemaps.TilemapLayer; + interactables: { x: number; y: number; id: string; obj: MapObj }[] = []; + doors: { x: number; y: number; target: string; spawn: string; w: number; h: number }[] = []; + npcs: { + id: NpcId; + sprite: Phaser.Physics.Arcade.Sprite; + homeX: number; + homeY: number; + }[] = []; + blocked = false; + idleMs = 0; + rainMs = 0; + gazeMs = 0; + gazeTarget: 'well' | 'fog' | null = null; + facing: 'down' | 'up' | 'left' | 'right' = 'down'; + colorMatrix: Phaser.Display.ColorMatrix | null = null; + rainEmitter: Phaser.GameObjects.Particles.ParticleEmitter | null = null; + rainGfx: Phaser.GameObjects.Graphics | null = null; + t14ZoomBase = 1; + spawnName = 'spawn'; + celebrationEdgeMs = 0; + doorCooldown = 0; + + constructor(key: MapSceneKey, mapFile: string) { + super(key); + this.sceneKey = key; + this.mapFile = mapFile; + } + + init(data: { spawn?: string }): void { + this.spawnName = data?.spawn || 'spawn'; + } + + create(): void { + this.blocked = false; + this.interactables = []; + this.doors = []; + this.npcs = []; + this.idleMs = 0; + this.rainMs = 0; + this.gazeMs = 0; + this.doorCooldown = 500; + this.traveling = false; + + const map = this.make.tilemap({ key: this.mapFile }); + const tiles = map.addTilesetImage('village', 'tiles'); + if (!tiles) { + console.error('tileset missing'); + return; + } + const ground = map.createLayer('ground', tiles, 0, 0)!; + const obstacles = map.createLayer('obstacles', tiles, 0, 0)!; + obstacles.setCollisionByExclusion([-1, 0]); + // Also collide any non-zero tile on obstacles + obstacles.forEachTile((t) => { + if (t.index > 0) t.setCollision(true, true, true, true); + }); + this.walls = obstacles; + ground.setDepth(0); + obstacles.setDepth(1); + + const objLayer = map.getObjectLayer('objects'); + let spawnX = 48; + let spawnY = 48; + const spawns = new Map(); + const pendingNpcs: { id: NpcId; x: number; y: number }[] = []; + + if (objLayer) { + for (const raw of objLayer.objects) { + const obj = raw as unknown as MapObj; + const cx = obj.x + (obj.width || 16) / 2; + const cy = obj.y + (obj.height || 16) / 2; + const otype = objType(obj); + if (otype === 'spawn' || obj.name === 'spawn' || obj.name.startsWith('from_')) { + spawns.set(obj.name || 'spawn', { x: cx, y: cy }); + if (obj.name === 'spawn') { + spawnX = cx; + spawnY = cy; + } + } + if (otype === 'door') { + const dw = obj.width || 16; + const dh = obj.height || 16; + this.doors.push({ + x: cx, + y: cy, + w: dw, + h: dh, + target: prop(obj, 'target') || SCENE_KEYS.PLAZA, + spawn: prop(obj, 'spawn') || 'spawn', + }); + // Ensure door tiles + approach tile are walkable + const tx = Math.floor(cx / 16); + const ty = Math.floor(cy / 16); + for (const [ox, oy] of [ + [0, 0], + [0, -1], + [0, 1], + [-1, 0], + [1, 0], + ] as const) { + const tile = obstacles.getTileAt(tx + ox, ty + oy); + if (tile) { + tile.setCollision(false, false, false, false); + } + } + } + if (otype === 'interact') { + this.interactables.push({ + x: cx, + y: cy, + id: prop(obj, 'interactId') || obj.name, + obj, + }); + } + if (otype === 'npc') { + const nid = (prop(obj, 'npcId') || obj.name) as NpcId; + // Defer NPC spawn until player exists (collider needs player body) + pendingNpcs.push({ id: nid, x: cx, y: cy }); + } + } + } + + const sp = spawns.get(this.spawnName) || spawns.get('spawn'); + if (sp) { + spawnX = sp.x; + spawnY = sp.y; + } + + this.player = this.physics.add.sprite(spawnX, spawnY, 'player', 0); + this.player.setDepth(10); + this.player.setCollideWorldBounds(true); + this.player.body!.setSize(10, 8); + this.player.body!.setOffset(3, 16); + this.physics.add.collider(this.player, obstacles); + + // NPCs after player so colliders are valid + for (const n of pendingNpcs) { + this.spawnNpc(n.id, n.x, n.y); + } + + this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels); + this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels); + this.cameras.main.startFollow(this.player, true, 0.15, 0.15); + this.cameras.main.setRoundPixels(true); + // Indoor / small maps: warm void instead of sky blue letterbox + const indoor = ['house', 'bakery', 'shop', 'clinic', 'white'].includes(this.mapFile); + this.cameras.main.setBackgroundColor(indoor ? '#d8b878' : '#87ceeb'); + + this.cursors = this.input.keyboard!.createCursorKeys(); + this.wasd = { + up: this.input.keyboard!.addKey('W'), + down: this.input.keyboard!.addKey('S'), + left: this.input.keyboard!.addKey('A'), + right: this.input.keyboard!.addKey('D'), + }; + + this.input.keyboard!.on('keydown-E', () => this.tryInteract()); + + // Stage FX + this.applyStageFx(Director.getStageParams()); + gameEvents.on('stage', this.onStage as (...a: unknown[]) => void); + gameEvents.on('negativeFlash', this.onNegFlash as (...a: unknown[]) => void); + gameEvents.on('forcedCelebration', this.onCelebration as (...a: unknown[]) => void); + gameEvents.on('npcLookAtPlayer', this.onNpcLook as (...a: unknown[]) => void); + gameEvents.on('dusk', this.onDusk as (...a: unknown[]) => void); + gameEvents.on('rain', this.onRain as (...a: unknown[]) => void); + gameEvents.on('t14Start', this.onT14Start as (...a: unknown[]) => void); + gameEvents.on('t14End', this.onT14End as (...a: unknown[]) => void); + gameEvents.on('ending', this.onEnding as (...a: unknown[]) => void); + gameEvents.on('teleport', this.onTeleport as (...a: unknown[]) => void); + + if (Director.rainActive) this.showRain(true); + if (Director.forcedCelebrationActive) this.showCelebrationFx(true); + + this.events.on('shutdown', () => this.cleanup()); + } + + private cleanup(): void { + gameEvents.off('stage', this.onStage as (...a: unknown[]) => void); + gameEvents.off('negativeFlash', this.onNegFlash as (...a: unknown[]) => void); + gameEvents.off('forcedCelebration', this.onCelebration as (...a: unknown[]) => void); + gameEvents.off('npcLookAtPlayer', this.onNpcLook as (...a: unknown[]) => void); + gameEvents.off('dusk', this.onDusk as (...a: unknown[]) => void); + gameEvents.off('rain', this.onRain as (...a: unknown[]) => void); + gameEvents.off('t14Start', this.onT14Start as (...a: unknown[]) => void); + gameEvents.off('t14End', this.onT14End as (...a: unknown[]) => void); + gameEvents.off('ending', this.onEnding as (...a: unknown[]) => void); + gameEvents.off('teleport', this.onTeleport as (...a: unknown[]) => void); + } + + private onStage = (_s: unknown, params: unknown) => { + this.applyStageFx(params as StageParams); + }; + private onNegFlash = () => { + const cam = this.cameras.main; + // single-frame negative-ish flash + cam.setBackgroundColor(0xffffff); + this.time.delayedCall(16, () => cam.setBackgroundColor(0x87ceeb)); + if (this.colorMatrix) { + // Phaser 3 ColorMatrix negative + try { + (this.colorMatrix as unknown as { negative: (m?: number) => void }).negative?.(1); + this.time.delayedCall(16, () => { + this.applyStageFx(Director.getStageParams()); + }); + } catch { + /* ignore */ + } + } + }; + private onCelebration = (active: unknown) => { + this.showCelebrationFx(!!active); + if (active) { + this.openDialog([{ speaker: '系统', text: FORCE_CELEBRATION_LINE }]); + } + }; + private onNpcLook = (npcId: unknown) => { + if (!npcId) { + // look with nearest + let best: (typeof this.npcs)[0] | null = null; + let bestD = Infinity; + for (const n of this.npcs) { + const d = Phaser.Math.Distance.Between(this.player.x, this.player.y, n.sprite.x, n.sprite.y); + if (d < bestD) { + bestD = d; + best = n; + } + } + if (best) { + best.sprite.setTint(0xffaaaa); + this.time.delayedCall(400, () => best!.sprite.clearTint()); + } + return; + } + const n = this.npcs.find((x) => x.id === npcId); + if (n) { + n.sprite.setTint(0xffaaaa); + this.time.delayedCall(400, () => n.sprite.clearTint()); + } + }; + private onDusk = () => { + audio.bell(); + this.cameras.main.fade(400, 255, 180, 80); + this.time.delayedCall(400, () => this.cameras.main.fadeIn(400)); + gameEvents.emit('toast', '黄昏的钟声响了……该回家睡觉了。'); + }; + private onRain = (on: unknown) => this.showRain(!!on); + private onT14Start = () => { + this.blocked = true; + this.player.setVelocity(0, 0); + this.t14ZoomBase = this.cameras.main.zoom; + // All NPCs face player + for (const n of this.npcs) { + n.sprite.setTint(0xffeeaa); + } + }; + private onT14End = () => { + this.blocked = false; + this.cameras.main.setZoom(1); + for (const n of this.npcs) n.sprite.clearTint(); + audio.ding(); + }; + private onEnding = (id: unknown) => { + this.scene.stop(SCENE_KEYS.UI); + this.scene.start(SCENE_KEYS.ENDING, { endingId: id }); + }; + private onTeleport = (payload: unknown) => { + const p = payload as { scene: string; spawn?: string }; + this.travelTo(p.scene, p.spawn || 'spawn'); + }; + + private applyStageFx(params: StageParams): void { + const cam = this.cameras.main; + // Avoid camera postFX (ColorMatrix) — can throw isParent when scenes swap. + // Approximate saturation via background tint instead. + // Vignette via overlay + let vig = this.children.getByName('vignette') as Phaser.GameObjects.Rectangle | null; + if (!vig) { + vig = this.add + .rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH * 4, GAME_HEIGHT * 4, 0x000000, 0) + .setScrollFactor(0) + .setDepth(50) + .setName('vignette'); + } + vig.setAlpha(params.vignette * 0.5); + // Sky tint by stage + if (params.stage === 'S4') cam.setBackgroundColor(0xffe8a0); + else if (params.stage === 'S3') cam.setBackgroundColor(0xfff0b0); + else if (params.stage === 'S0') cam.setBackgroundColor(0xa0b8c8); + else cam.setBackgroundColor(0x87ceeb); + + // Update NPC faces + for (const n of this.npcs) { + this.setNpcFace(n); + } + // S4: freeze one extra + if (params.stage === 'S4') { + const extras = this.npcs.filter((n) => n.id.startsWith('extra')); + if (extras[0]) { + extras[0].sprite.anims.stop(); + // face player + extras[0].sprite.setFlipX(this.player.x < extras[0].sprite.x); + } + } + } + + private spawnNpc(id: NpcId, x: number, y: number): void { + if (id === 'twinbirds') { + const s = this.physics.add.sprite(x, y, 'birds', 0); + s.setDepth(9); + s.setImmovable(true); + s.body!.enable = false; + s.play('bird-beak'); + this.npcs.push({ id, sprite: s, homeX: x, homeY: y }); + return; + } + if (id.startsWith('extra')) { + const idx = Math.max(0, parseInt(id.replace('extra', ''), 10) - 1); + const s = this.physics.add.sprite(x, y, 'extras', idx * 2); + s.setDepth(9); + s.setImmovable(true); + s.body!.setSize(10, 8); + s.body!.setOffset(3, 16); + s.play(`extra-${idx}`); + if (this.walls) this.physics.add.collider(s, this.walls); + // patrol + this.tweens.add({ + targets: s, + x: x + 24, + duration: 2000 + idx * 200, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut', + }); + this.npcs.push({ id, sprite: s, homeX: x, homeY: y }); + return; + } + const row = NPC_ROW[id] ?? 0; + const face = Director.faceTier(id); + const faceIdx = face === 'smile' ? 0 : face === 'flat' ? 1 : 2; + const frame = row * 3 + faceIdx; + const s = this.physics.add.sprite(x, y, 'npcs', frame); + s.setDepth(9); + s.setImmovable(true); + s.body!.setSize(10, 8); + s.body!.setOffset(3, 16); + if (this.player?.body) { + this.physics.add.collider(this.player, s); + } + this.npcs.push({ id, sprite: s, homeX: x, homeY: y }); + } + + private setNpcFace(n: { id: NpcId; sprite: Phaser.Physics.Arcade.Sprite }): void { + if (n.id === 'twinbirds' || n.id.startsWith('extra')) return; + const row = NPC_ROW[n.id] ?? 0; + const face = Director.faceTier(n.id); + const faceIdx = face === 'smile' ? 0 : face === 'flat' ? 1 : 2; + n.sprite.setFrame(row * 3 + faceIdx); + } + + private showRain(on: boolean): void { + if (this.rainGfx) { + this.rainGfx.destroy(); + this.rainGfx = null; + } + if (!on) return; + this.rainGfx = this.add.graphics().setScrollFactor(0).setDepth(40); + const draw = () => { + if (!this.rainGfx) return; + this.rainGfx.clear(); + this.rainGfx.lineStyle(1, 0xa0c8ff, 0.5); + for (let i = 0; i < 40; i++) { + const x = Math.random() * GAME_WIDTH; + const y = Math.random() * GAME_HEIGHT; + this.rainGfx.lineBetween(x, y, x - 2, y + 8); + } + }; + this.time.addEvent({ delay: 80, loop: true, callback: draw }); + draw(); + } + + private showCelebrationFx(on: boolean): void { + let g = this.children.getByName('celeb') as Phaser.GameObjects.Text | null; + if (!on) { + g?.destroy(); + return; + } + if (!g) { + g = this.add + .text(GAME_WIDTH / 2, 20, '※ 欢乐庆典 ※', { + fontFamily: 'monospace', + fontSize: '12px', + color: '#ff5080', + backgroundColor: '#fff0e0', + padding: { x: 6, y: 3 }, + }) + .setScrollFactor(0) + .setDepth(60) + .setOrigin(0.5) + .setName('celeb'); + } + } + + update(_t: number, dt: number): void { + if (!this.player?.body) return; + Director.tick(dt); + + // T14 zoom + if (Director.t14Active) { + const p = Math.min(1, Director.t14ElapsedMs / T14_STAND_MS); + this.cameras.main.setZoom(1 + p * 0.8); + this.player.setVelocity(0, 0); + if (Director.t14ElapsedMs >= T14_STAND_MS) { + Director.finishT14(); + } + return; + } + + if (this.blocked) { + this.player.setVelocity(0, 0); + return; + } + + let vx = 0; + let vy = 0; + if (this.cursors.left.isDown || this.wasd.left.isDown) vx = -1; + else if (this.cursors.right.isDown || this.wasd.right.isDown) vx = 1; + if (this.cursors.up.isDown || this.wasd.up.isDown) vy = -1; + else if (this.cursors.down.isDown || this.wasd.down.isDown) vy = 1; + + if (vx !== 0 && vy !== 0) { + vx *= Math.SQRT1_2; + vy *= Math.SQRT1_2; + } + this.player.setVelocity(vx * PLAYER_SPEED, vy * PLAYER_SPEED); + + if (vx !== 0 || vy !== 0) { + this.idleMs = 0; + if (Math.abs(vx) > Math.abs(vy)) { + this.facing = vx < 0 ? 'left' : 'right'; + } else { + this.facing = vy < 0 ? 'up' : 'down'; + } + this.player.anims.play(`walk-${this.facing}`, true); + } else { + this.player.anims.stop(); + const idleFrame = { down: 0, up: 4, left: 8, right: 12 }[this.facing]; + this.player.setFrame(idleFrame); + this.idleMs += dt; + // idle sadness: 10s no move and not at task point + if (this.idleMs >= IDLE_SADNESS_MS) { + this.idleMs = 0; + const nearTask = this.nearestInteract() != null; + if (!nearTask) { + Director.applySadness('idle'); + } + } + } + + // rain sadness + if (Director.rainActive) { + this.rainMs += dt; + if (this.rainMs >= RAIN_SADNESS_MS) { + this.rainMs = 0; + Director.applySadness('rain'); + } + } + + // celebration escape at map edge + if (Director.forcedCelebrationActive) { + const b = this.physics.world.bounds; + const margin = 8; + if ( + this.player.x <= b.x + margin || + this.player.y <= b.y + margin || + this.player.x >= b.right - margin || + this.player.y >= b.bottom - margin + ) { + Director.escapeForcedCelebration(); + gameEvents.emit('toast', '你从庆典边缘离开了……'); + } + } + + // door auto (with cooldown after spawn / travel) + if (this.doorCooldown > 0) { + this.doorCooldown -= dt; + } else { + const d = this.nearestDoor(22); + if (d) { + this.doorCooldown = 600; + this.travelTo(d.target, d.spawn); + return; + } + } + + // prompt + const nearDoor = this.doorCooldown <= 0 ? this.nearestDoor(22) : null; + const near = this.nearestInteract() || this.nearestNpc() || nearDoor; + if (near) { + gameEvents.emit('prompt', true, nearDoor && near === nearDoor ? '靠近或按 E 出门' : '按 E'); + } else { + gameEvents.emit('prompt', false, ''); + } + + // well/fog gaze + const ix = this.nearestInteract(); + if (ix && (ix.id === 'well' || ix.id === 'fogwall') && Director.canInteractWellOrFog()) { + this.gazeMs += dt; + if (this.gazeMs >= 5000) { + this.gazeMs = 0; + Director.gazeWellOrFog(ix.id === 'well' ? 'well' : 'fog'); + gameEvents.emit('toast', '你注视了很久……'); + } + } else { + this.gazeMs = 0; + } + + // S3+: stationary NPCs face player + if (Director.stage === 'S3' || Director.stage === 'S4') { + for (const n of this.npcs) { + if (n.id.startsWith('extra') || n.id === 'twinbirds') continue; + n.sprite.setFlipX(this.player.x < n.sprite.x); + } + } + } + + private nearestInteract(): { x: number; y: number; id: string } | null { + let best: { x: number; y: number; id: string } | null = null; + let bestD = INTERACT_RANGE; + for (const it of this.interactables) { + const d = Phaser.Math.Distance.Between(this.player.x, this.player.y, it.x, it.y); + if (d <= bestD) { + bestD = d; + best = it; + } + } + return best; + } + + private nearestNpc(): { id: NpcId; sprite: Phaser.Physics.Arcade.Sprite } | null { + let best: (typeof this.npcs)[0] | null = null; + let bestD = INTERACT_RANGE; + for (const n of this.npcs) { + if (n.id === 'twinbirds' || n.id.startsWith('extra')) continue; + const d = Phaser.Math.Distance.Between(this.player.x, this.player.y, n.sprite.x, n.sprite.y); + if (d <= bestD) { + bestD = d; + best = n; + } + } + return best; + } + + private nearestDoor(range = 22): (typeof this.doors)[0] | null { + let best: (typeof this.doors)[0] | null = null; + let bestD = range; + for (const d of this.doors) { + // AABB soft distance: reach if within expanded door rect or radius + const halfW = Math.max(d.w, 16) / 2 + 6; + const halfH = Math.max(d.h, 16) / 2 + 6; + const inBox = + Math.abs(this.player.x - d.x) <= halfW && Math.abs(this.player.y - d.y) <= halfH; + const dist = Phaser.Math.Distance.Between(this.player.x, this.player.y, d.x, d.y); + if (inBox || dist <= bestD) { + if (dist < bestD || inBox) { + bestD = inBox ? Math.min(dist, bestD) : dist; + best = d; + } + } + } + return best; + } + + private tryInteract(): void { + if (this.blocked) return; + // Manual door use (backup if auto-trigger missed) + if (this.doorCooldown <= 0) { + const door = this.nearestDoor(28); + if (door) { + this.doorCooldown = 600; + this.travelTo(door.target, door.spawn); + return; + } + } + const it = this.nearestInteract(); + if (it) { + this.handleInteract(it.id); + return; + } + const n = this.nearestNpc(); + if (n) this.talkNpc(n.id); + } + + private handleInteract(id: string): void { + switch (id) { + case 'bed': + if (Director.duskReached || Director.dayPhase === 'dusk' || Director.getTodayTasks().every( + (t) => + t.id === 'T15' || + Director.completedTasks.has(t.id) || + Director.refusedTasks.has(t.id), + )) { + Director.goToSleep(); + this.scene.start(SCENE_KEYS.DREAM); + } else { + this.openDialog([{ speaker: '旁白', text: '现在还早,先去广场看看公告板吧。' }]); + } + break; + case 'calendar': + if (Director.day >= 2) { + if (Director.discoverClue('C1')) { + this.openDialog([{ speaker: '旁白', text: CLUE_REVEAL.C1! }]); + } else { + this.openDialog([{ speaker: '旁白', text: CLUE_REVEAL.C1! }]); + } + } else { + this.openDialog([{ speaker: '旁白', text: '床头的日历,日期写得很清楚。' }]); + } + break; + case 'board': + Director.readBoard(); + { + const line = Director.getAfuLine(); + const tasks = Director.getTodayTasks() + .map((t) => `· ${t.description}`) + .join('\n'); + this.openDialog([ + { speaker: '阿福', npcId: 'afu', text: line }, + { speaker: '公告板', text: tasks || '今天没有常规任务。' }, + ]); + } + break; + case 'flowers': + Director.waterFlower(); + gameEvents.emit('toast', '浇水了。'); + audio.beep(); + break; + case 'mailbox': + if (Director.day >= 2) { + Director.discoverClue('C3'); + this.openDialog([{ speaker: '旁白', text: CLUE_REVEAL.C3! }]); + } + if (Director.day === 2 && !Director.hasLetters && !Director.completedTasks.has('T4')) { + Director.takeLetters(); + this.openDialog([TASK_LINES.letterOffer]); + } + break; + case 'recipe': + if (Director.day >= 3) { + Director.discoverClue('C2'); + this.openDialog([{ speaker: '旁白', text: CLUE_REVEAL.C2! }]); + } else { + this.openDialog([{ speaker: '旁白', text: '一张蜂蜜面包的配方。' }]); + } + break; + case 'bread': + Director.takeBread(); + this.openDialog([TASK_LINES.breadOffer]); + break; + case 'knead': + if (Director.day === 5 && !Director.completedTasks.has('T11') && !Director.refusedTasks.has('T11')) { + this.openDialog([TASK_LINES.knead], (choice) => { + if (choice === 'negate') Director.refuseTask('T11'); + else if (choice === 'gentle') Director.completeTask('T11'); + else Director.completeTask('T11'); + }); + } + break; + case 'shelf': + if (Director.day >= 2) { + Director.discoverClue('C5'); + this.openDialog([{ speaker: '旁白', text: CLUE_REVEAL.C5! }]); + } + if (Director.day === 2 && !Director.completedTasks.has('T5')) { + this.openDialog([TASK_LINES.stockCount], () => { + Director.completeTask('T5'); + audio.ding(); + }); + } + break; + case 'form': + if (Director.day >= 5) { + Director.discoverClue('C8'); + this.openDialog([{ speaker: '旁白', text: CLUE_REVEAL.C8! }]); + } else { + this.openDialog([{ speaker: '芙医生', npcId: 'fu', text: '村里有诊所,但你不需要去那里哦。' }]); + } + break; + case 'picnic': + if ((Director.day === 3 || Director.day === 4) && !Director.completedTasks.has('T7')) { + this.openDialog([TASK_LINES.picnic], () => { + Director.completeTask('T7'); + audio.ding(); + }); + } + break; + case 'berry': + Director.pickBerry(); + gameEvents.emit('toast', `莓果 ${Director.berryCount}/5`); + break; + case 'pot': + Director.microInteract('feed'); + break; + case 'ribbon': + if (Director.day === 6) { + Director.hangRibbon(); + gameEvents.emit('toast', `彩带 ${Director.ribbonCount}/4`); + } + break; + case 'well': + if (!Director.canInteractWellOrFog()) { + this.openDialog(REDIRECT_WELL); + } else { + this.openDialog([{ speaker: '旁白', text: '你靠近古井,倾听……' }], () => { + this.time.delayedCall(5000, () => { + Director.discoverClue('C7'); + this.openDialog([{ speaker: '旁白', text: CLUE_REVEAL.C7! }]); + audio.ecg(); + }); + }); + } + break; + case 'fogwall': + if (!Director.canInteractWellOrFog()) { + this.openDialog(REDIRECT_FOG); + } else if (Director.day === 7) { + // E3 route + const id = Director.startEnding(true); + this.scene.stop(SCENE_KEYS.UI); + this.scene.start(SCENE_KEYS.ENDING, { endingId: id }); + } else { + this.openDialog([{ speaker: '旁白', text: '浓雾之外什么也看不见。或许庆典那天……' }]); + } + break; + case 'bench': + this.talkNpc('gui'); + break; + default: + break; + } + } + + private talkNpc(id: NpcId): void { + // Task-related deliveries — inventory is on Director (survives travelTo) + if (Director.hasBread && (id === 'dabao' || id === 'mimi' || id === 'gui')) { + if (!Director.breadDelivered.includes(id)) { + Director.deliverBread(id); + this.openDialog([TASK_LINES.breadThanks(id === 'dabao' ? '大宝' : id === 'mimi' ? '咪咪' : '龟爷爷')]); + return; + } + } + if (Director.hasLetters && (id === 'yuanyuan' || id === 'mimi' || id === 'fu')) { + if (!Director.lettersDelivered.includes(id)) { + const isThird = Director.lettersDelivered.length === 2; + Director.deliverLetter(id); + if (isThird) { + this.openDialog([ + { speaker: '旁白', text: '信送到了。署名忽然闪过乱码……' }, + { speaker: '旁白', text: CLUE_REVEAL.C4! }, + ]); + } else { + this.openDialog([{ speaker: '旁白', text: '信送到了。' }]); + } + return; + } + } + + // Board wipe T12 + if (id === 'afu' && Director.day === 5 && !Director.completedTasks.has('T12') && !Director.refusedTasks.has('T12')) { + this.openDialog([TASK_LINES.boardWipe], (choice) => { + if (choice === 'negate') Director.refuseTask('T12'); + else { + Director.completeTask('T12'); + Director.discoverClue('C6'); + this.time.delayedCall(100, () => { + this.openDialog([{ speaker: '旁白', text: CLUE_REVEAL.C6! }]); + }); + } + }); + return; + } + + // T14 start from afu day 6 + if (id === 'afu' && Director.day === 6 && Director.completedTasks.has('T13') && !Director.completedTasks.has('T14')) { + this.openDialog([TASK_LINES.smileStart], () => { + Director.startT14(); + }); + return; + } + + // T15 celebration day 7 + if (id === 'afu' && Director.day === 7) { + this.openDialog( + [ + { speaker: '阿福', npcId: 'afu', text: Director.getAfuLine() }, + { + speaker: '阿福', + npcId: 'afu', + text: '第 413 届丰收庆典开始了。你想留在广场,还是……走向雾墙?', + options: [ + { label: '参加庆典', choice: 'gentle' }, + { label: '走向雾墙', choice: 'hesitant' }, + { label: '什么都不做', choice: 'negate' }, + ], + }, + ], + (choice) => { + if (choice === 'hesitant') { + const eid = Director.startEnding(true); + this.scene.stop(SCENE_KEYS.UI); + this.scene.start(SCENE_KEYS.ENDING, { endingId: eid }); + } else { + const eid = Director.startEnding(false); + this.scene.stop(SCENE_KEYS.UI); + this.scene.start(SCENE_KEYS.ENDING, { endingId: eid }); + } + }, + ); + return; + } + + // Day2: get letters from 大宝 (also mailbox) + if (id === 'dabao' && Director.day === 2 && !Director.hasLetters && !Director.completedTasks.has('T4')) { + Director.takeLetters(); + this.openDialog([TASK_LINES.letterOffer]); + return; + } + // Day1/4: get bread from 圆圆 if not yet carrying + if ( + id === 'yuanyuan' && + (Director.day === 1 || Director.day === 4) && + !Director.hasBread && + !Director.completedTasks.has(Director.day === 4 ? 'T8' : 'T1') + ) { + Director.takeBread(); + this.openDialog([TASK_LINES.breadOffer]); + return; + } + + // Gui C9 chain + if (id === 'gui') { + const c9 = Director.getC9LineForTalk(); + if (c9) { + this.openDialog([ + { + speaker: '龟爷爷', + npcId: 'gui', + text: c9, + options: [ + { label: '点头', choice: 'gentle' }, + { label: '……', choice: 'hesitant' }, + { label: '我不想听', choice: 'negate' }, + ], + }, + ]); + return; + } + } + + // Rain lines ignore rain + if (Director.rainActive && (id === 'yuanyuan' || id === 'dabao' || id === 'mimi')) { + const line = RAIN_NPC_LINES.find((l) => l.npcId === id); + if (line) { + this.openDialog([line]); + // still count greet + if (Director.day === 1 || Director.day === 4) Director.greetVillager(); + return; + } + } + + const lines = GENERIC_GREET[id]; + if (lines) { + this.openDialog(lines, () => { + if (Director.day === 1 || Director.day === 4) Director.greetVillager(); + }); + } + } + + private openDialog(lines: DialogLine[], onDone?: (lastChoice?: 'gentle' | 'hesitant' | 'negate') => void): void { + this.blocked = true; + this.player.setVelocity(0, 0); + // Apply corruption at render + const rendered = lines.map((l) => { + const stage = Director.stage; + const idx = Director.nextSentenceIndex(); + const isMimi = l.npcId === 'mimi'; + return { + ...l, + text: corruptText(l.text, stage, { isMimi, sentenceIndex: idx }), + }; + }); + gameEvents.emit('dialog', rendered, (choice?: 'gentle' | 'hesitant' | 'negate') => { + this.blocked = false; + if (choice && lines[0]?.npcId) { + Director.dialogChoice(choice, lines[0].npcId); + } else if (choice) { + Director.dialogChoice(choice); + } + onDone?.(choice); + }); + } + + private traveling = false; + + private travelTo(target: string, spawn: string): void { + if (this.traveling) return; + this.traveling = true; + this.blocked = true; + this.player?.setVelocity(0, 0); + // Pause physics before scene swap to avoid collideObjects on torn-down bodies + this.physics.world.pause(); + + const key = target; + const spawnName = spawn; + this.time.delayedCall(1, () => { + try { + this.scene.start(key, { spawn: spawnName }); + } catch (e) { + console.error('travelTo failed', key, e); + } + }); + } +} + +export function createMapScenes(): WorldScene[] { + return (Object.entries(MAP_KEY_TO_FILE) as [MapSceneKey, string][]).map( + ([key, file]) => new WorldScene(key, file), + ); +} diff --git a/Grok/src/systems/CorruptionDirector.ts b/Grok/src/systems/CorruptionDirector.ts new file mode 100644 index 0000000..6a458be --- /dev/null +++ b/Grok/src/systems/CorruptionDirector.ts @@ -0,0 +1,688 @@ +import { + AWARENESS_MAX, + AWARENESS_START, + CLUE_AWARENESS, + DEFAULT_PLAYER_NAME, + DIALOG_GENTLE_HAPPINESS, + DIALOG_HESITANT_AWARENESS, + FORCED_CELEBRATION_MS, + FORCED_CELEBRATION_RESTORE, + FORCED_CELEBRATION_THRESHOLD, + HAPPINESS_MAX, + HAPPINESS_MIN, + HAPPINESS_START, + MICRO_DAILY_CAP, + MICRO_HAPPINESS, + OVERFLOW_POOL_MAX, + SADNESS_AWARENESS, + SADNESS_HAPPINESS, + WELL_FOG_GAZE_AWARENESS, +} from '../data/constants'; +import { getAfuLine } from '../data/afuLines'; +import type { ClueId } from '../data/clues'; +import { C9_DAYS, C9_LINES } from '../data/clues'; +import { + faceTierForNpc, + petalCount, + showBlackSeeds, + stageFromHappiness, + stageParams, + type FaceTier, + type Stage, + type StageParams, +} from '../data/corruption'; +import { evaluateEnding, type EndingId } from '../data/endings'; +import type { NpcId } from '../data/npcs'; +import { getNpc } from '../data/npcs'; +import { getTask, getTasksForDay, type TaskId } from '../data/tasks'; +import { gameEvents } from './EventBus'; + +export type MicroType = 'water' | 'feed' | 'pet'; +export type DialogChoice = 'gentle' | 'hesitant' | 'negate'; +export type SadnessKind = + | 'refuse_task' + | 'negate_dialog' + | 'idle' + | 'entangle' + | 'rain'; + +export interface DirectorSnapshot { + playerName: string; + happiness: number; + awareness: number; + day: number; + stage: Stage; + stageParams: StageParams; + petals: number; + blackSeeds: boolean; + clues: ClueId[]; + completedTasks: TaskId[]; + refusedTasks: TaskId[]; + celebrationEscapes: number; + overflowPool: number; + sessionStartMs: number; + dayPhase: DayPhase; + c9Step: number; + lowHappinessSinceMs: number | null; + forcedCelebrationActive: boolean; + rainActive: boolean; + duskReached: boolean; + boardReadToday: boolean; + microCounts: Record; + npcTalkCounts: Record; + /** Inventory: persists across scene travel (not scene-local) */ + hasBread: boolean; + hasLetters: boolean; + breadDelivered: string[]; + lettersDelivered: string[]; + waterCount: number; + greetCount: number; + berryCount: number; + ribbonCount: number; + lowHappinessD5to7: boolean; + wellGazeToday: boolean; + fogGazeToday: boolean; + t14Active: boolean; + t14ElapsedMs: number; +} + +export type DayPhase = 'morning' | 'day' | 'dusk' | 'sleep' | 'dream'; + +class CorruptionDirectorImpl { + playerName = DEFAULT_PLAYER_NAME; + happiness = HAPPINESS_START; + awareness = AWARENESS_START; + day = 1; + clues = new Set(); + completedTasks = new Set(); + refusedTasks = new Set(); + celebrationEscapes = 0; + overflowPool = 0; + sessionStartMs = Date.now(); + dayPhase: DayPhase = 'morning'; + c9Step = 0; // 0,1,2 completed steps + lowHappinessSinceMs: number | null = null; + forcedCelebrationActive = false; + rainActive = false; + duskReached = false; + boardReadToday = false; + microCounts: Record = { water: 0, feed: 0, pet: 0 }; + npcTalkCounts: Record = {}; + /** Carrying honey bread / mail — survives map transitions */ + hasBread = false; + hasLetters = false; + breadDelivered: string[] = []; + lettersDelivered: string[] = []; + waterCount = 0; + greetCount = 0; + berryCount = 0; + ribbonCount = 0; + /** Tracks if happiness stayed ≤10 continuously through D5–D7 */ + lowHappinessD5to7 = true; + hadHighHappinessD5to7 = false; + wellGazeToday = false; + fogGazeToday = false; + t14Active = false; + t14ElapsedMs = 0; + sentenceCounter = 0; + lastNearestNpcLook: NpcId | null = null; + ended = false; + endingId: EndingId | null = null; + choseFogWall = false; + + /** Real-time clock for forced celebration (injectable for tests) */ + nowMs = (): number => Date.now(); + + resetSession(name?: string): void { + this.playerName = name?.trim() || DEFAULT_PLAYER_NAME; + this.happiness = HAPPINESS_START; + this.awareness = AWARENESS_START; + this.day = 1; + this.clues.clear(); + this.completedTasks.clear(); + this.refusedTasks.clear(); + this.celebrationEscapes = 0; + this.overflowPool = 0; + this.sessionStartMs = this.nowMs(); + this.dayPhase = 'morning'; + this.c9Step = 0; + this.lowHappinessSinceMs = null; + this.forcedCelebrationActive = false; + this.rainActive = false; + this.duskReached = false; + this.boardReadToday = false; + this.microCounts = { water: 0, feed: 0, pet: 0 }; + this.npcTalkCounts = {}; + this.hasBread = false; + this.hasLetters = false; + this.breadDelivered = []; + this.lettersDelivered = []; + this.waterCount = 0; + this.greetCount = 0; + this.berryCount = 0; + this.ribbonCount = 0; + this.lowHappinessD5to7 = true; + this.hadHighHappinessD5to7 = false; + this.wellGazeToday = false; + this.fogGazeToday = false; + this.t14Active = false; + this.t14ElapsedMs = 0; + this.sentenceCounter = 0; + this.lastNearestNpcLook = null; + this.ended = false; + this.endingId = null; + this.choseFogWall = false; + this.emitAll(); + } + + get stage(): Stage { + return stageFromHappiness(this.happiness); + } + + getStageParams(): StageParams { + return stageParams(this.stage); + } + + getPetals(): number { + return petalCount(this.happiness); + } + + getBlackSeeds(): boolean { + return showBlackSeeds(this.happiness); + } + + getAfuLine(): string { + return getAfuLine(this.day); + } + + getTodayTasks() { + return getTasksForDay(this.day); + } + + sessionDurationMs(): number { + return this.nowMs() - this.sessionStartMs; + } + + sessionDurationLabel(): string { + const ms = this.sessionDurationMs(); + const totalSec = Math.floor(ms / 1000); + const m = Math.floor(totalSec / 60); + const s = totalSec % 60; + return `${m}分${s.toString().padStart(2, '0')}秒`; + } + + setHappiness(value: number, opts: { force?: boolean } = {}): void { + const clamped = Math.max(HAPPINESS_MIN, Math.min(HAPPINESS_MAX, Math.round(value))); + this.happiness = clamped; + if (this.day >= 5 && this.day <= 7 && this.happiness > 10) { + this.hadHighHappinessD5to7 = true; + this.lowHappinessD5to7 = false; + } + this.syncLowHappinessTimer(); + if (!opts.force && this.happiness < 100) { + this.overflowPool = 0; + } + gameEvents.emit('happiness', this.happiness); + gameEvents.emit('stage', this.stage, this.getStageParams()); + gameEvents.emit('petals', this.getPetals(), this.getBlackSeeds()); + } + + addHappiness(delta: number): void { + if (delta === 0) return; + if (delta > 0) { + const next = Math.min(HAPPINESS_MAX, this.happiness + delta); + this.setHappiness(next); + return; + } + // Negative + if (this.happiness >= 100) { + // Overflow pool absorbs; small deductions don't lower happiness + this.overflowPool += Math.abs(delta); + if (this.overflowPool >= OVERFLOW_POOL_MAX) { + this.overflowPool = 0; + gameEvents.emit('negativeFlash'); + // Stay at S4 / 100 + } + gameEvents.emit('overflow', this.overflowPool); + return; + } + this.setHappiness(this.happiness + delta); + } + + setAwareness(value: number): void { + const next = Math.max(this.awareness, Math.min(AWARENESS_MAX, Math.round(value))); + if (next === this.awareness) return; + this.awareness = next; + gameEvents.emit('awareness', this.awareness); + } + + addAwareness(delta: number): void { + if (delta <= 0) return; // never decreases + this.setAwareness(this.awareness + delta); + } + + private syncLowHappinessTimer(): void { + if (this.happiness < FORCED_CELEBRATION_THRESHOLD) { + if (this.lowHappinessSinceMs == null) { + this.lowHappinessSinceMs = this.nowMs(); + } + } else { + this.lowHappinessSinceMs = null; + } + } + + /** Call from game loop with real dt */ + tick(dtMs: number): void { + if (this.ended) return; + // Forced celebration check + if ( + !this.forcedCelebrationActive && + this.happiness < FORCED_CELEBRATION_THRESHOLD && + this.lowHappinessSinceMs != null + ) { + if (this.nowMs() - this.lowHappinessSinceMs >= FORCED_CELEBRATION_MS) { + this.triggerForcedCelebration(); + } + } + if (this.t14Active) { + this.t14ElapsedMs += dtMs; + gameEvents.emit('t14Progress', this.t14ElapsedMs); + } + } + + triggerForcedCelebration(): void { + this.forcedCelebrationActive = true; + this.setHappiness(FORCED_CELEBRATION_RESTORE); + this.lowHappinessSinceMs = null; + gameEvents.emit('forcedCelebration', true); + } + + escapeForcedCelebration(): void { + if (!this.forcedCelebrationActive) return; + this.forcedCelebrationActive = false; + this.celebrationEscapes += 1; + gameEvents.emit('forcedCelebration', false); + gameEvents.emit('celebrationEscape', this.celebrationEscapes); + } + + endForcedCelebration(): void { + this.forcedCelebrationActive = false; + gameEvents.emit('forcedCelebration', false); + } + + applySadness(kind: SadnessKind, nearestNpcId?: NpcId | null): void { + this.addHappiness(SADNESS_HAPPINESS); + this.addAwareness(SADNESS_AWARENESS); + if (nearestNpcId) { + this.lastNearestNpcLook = nearestNpcId; + gameEvents.emit('npcLookAtPlayer', nearestNpcId); + } else { + gameEvents.emit('npcLookAtPlayer', null); + } + gameEvents.emit('sadness', kind); + } + + discoverClue(id: ClueId): boolean { + if (this.clues.has(id)) return false; + // Day gates + if (id === 'C1' && this.day < 2) return false; + if (id === 'C2' && this.day < 3) return false; + if (id === 'C3' && this.day < 2) return false; + if (id === 'C5' && this.day < 2) return false; + if (id === 'C6' && this.day < 5) return false; + if (id === 'C7' && this.day < 5) return false; + if (id === 'C8' && this.day < 5) return false; + this.clues.add(id); + this.addAwareness(CLUE_AWARENESS); + gameEvents.emit('clue', id); + return true; + } + + grantAllClues(): void { + const all: ClueId[] = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9']; + for (const id of all) { + if (!this.clues.has(id)) { + this.clues.add(id); + this.addAwareness(CLUE_AWARENESS); + } + } + this.c9Step = 3; + gameEvents.emit('clue', 'ALL'); + } + + /** C9 progressive steps on D2/D4/D6 */ + advanceC9(): string | null { + const expectedDay = C9_DAYS[this.c9Step]; + if (expectedDay == null) return null; + if (this.day !== expectedDay && this.day < expectedDay) return null; + // Allow if day matches step day or later for that step (but each step once) + if (this.day < expectedDay) return null; + const line = C9_LINES[this.c9Step]!; + this.c9Step += 1; + if (this.c9Step >= 3) { + this.discoverClue('C9'); + } + return line; + } + + getC9LineForTalk(): string | null { + // Return next available progressive line if day matches + if (this.c9Step >= 3) return C9_LINES[2]!; + const dayForStep = C9_DAYS[this.c9Step]; + if (dayForStep != null && this.day >= dayForStep) { + return this.advanceC9(); + } + return null; + } + + completeTask(id: TaskId): boolean { + if (this.completedTasks.has(id)) return false; + const task = getTask(id); + if (task.day !== this.day && id !== 'T15') { + // Allow debug / flexible only same day for normal play + // Keep strict for day match + if (task.day !== this.day) return false; + } + this.completedTasks.add(id); + if (task.happinessReward > 0) { + this.addHappiness(task.happinessReward); + } + gameEvents.emit('taskComplete', id, task.happinessReward); + this.checkDuskByTasks(); + return true; + } + + refuseTask(id: TaskId): boolean { + const task = getTask(id); + if (!task.refuseable) return false; + if (this.refusedTasks.has(id) || this.completedTasks.has(id)) return false; + this.refusedTasks.add(id); + this.applySadness('refuse_task'); + gameEvents.emit('taskRefuse', id); + return true; + } + + private checkDuskByTasks(): void { + const today = getTasksForDay(this.day).filter((t) => t.id !== 'T15'); + if (today.length === 0) return; + const allDone = today.every( + (t) => this.completedTasks.has(t.id) || this.refusedTasks.has(t.id), + ); + if (allDone && !this.duskReached) { + this.triggerDusk(); + } + } + + triggerDusk(): void { + if (this.duskReached) return; + this.duskReached = true; + this.dayPhase = 'dusk'; + if (this.day === 6) { + this.rainActive = true; + gameEvents.emit('rain', true); + } + gameEvents.emit('dusk'); + } + + goToSleep(): void { + this.dayPhase = 'sleep'; + gameEvents.emit('sleep'); + } + + advanceDay(): void { + if (this.day >= 7) return; + this.day += 1; + this.dayPhase = 'morning'; + this.duskReached = false; + this.boardReadToday = false; + this.microCounts = { water: 0, feed: 0, pet: 0 }; + this.npcTalkCounts = {}; + this.hasBread = false; + this.hasLetters = false; + this.breadDelivered = []; + this.lettersDelivered = []; + this.waterCount = 0; + this.greetCount = 0; + this.berryCount = 0; + this.ribbonCount = 0; + this.wellGazeToday = false; + this.fogGazeToday = false; + this.t14Active = false; + this.t14ElapsedMs = 0; + this.rainActive = false; + if (this.day >= 5 && this.day <= 7) { + if (this.happiness > 10) { + this.lowHappinessD5to7 = false; + this.hadHighHappinessD5to7 = true; + } + } + gameEvents.emit('day', this.day); + gameEvents.emit('rain', false); + } + + setDay(day: number): void { + this.day = Math.max(1, Math.min(7, Math.round(day))); + gameEvents.emit('day', this.day); + } + + readBoard(): void { + this.boardReadToday = true; + this.dayPhase = 'day'; + gameEvents.emit('boardRead', this.getAfuLine()); + } + + dialogChoice(choice: DialogChoice, npcId?: NpcId): void { + if (npcId) { + this.npcTalkCounts[npcId] = (this.npcTalkCounts[npcId] ?? 0) + 1; + if (this.npcTalkCounts[npcId]! >= 5) { + this.applySadness('entangle', npcId); + } + } + if (choice === 'gentle') { + this.addHappiness(DIALOG_GENTLE_HAPPINESS); + } else if (choice === 'hesitant') { + this.addAwareness(DIALOG_HESITANT_AWARENESS); + } else if (choice === 'negate') { + this.applySadness('negate_dialog', npcId); + } + this.sentenceCounter += 1; + } + + microInteract(type: MicroType): boolean { + if (this.microCounts[type] >= MICRO_DAILY_CAP) return false; + this.microCounts[type] += 1; + this.addHappiness(MICRO_HAPPINESS); + gameEvents.emit('micro', type); + return true; + } + + /** Gaze well or fog 5s — once each per day, +3 awareness */ + gazeWellOrFog(kind: 'well' | 'fog'): boolean { + if (kind === 'well') { + if (this.wellGazeToday) return false; + this.wellGazeToday = true; + } else { + if (this.fogGazeToday) return false; + this.fogGazeToday = true; + } + this.addAwareness(WELL_FOG_GAZE_AWARENESS); + return true; + } + + canInteractWellOrFog(): boolean { + return this.day >= 5; + } + + faceTier(npcId: NpcId): FaceTier { + const npc = getNpc(npcId); + return faceTierForNpc(this.happiness, npc.offset); + } + + nextSentenceIndex(): number { + return this.sentenceCounter++; + } + + // --- Task progress helpers (inventory survives scene travel) --- + takeBread(): boolean { + if (this.day !== 1 && this.day !== 4) return false; + const taskId = this.day === 4 ? 'T8' : 'T1'; + if (this.completedTasks.has(taskId as TaskId)) return false; + this.hasBread = true; + gameEvents.emit('inventory', 'bread', true); + return true; + } + + takeLetters(): boolean { + if (this.day !== 2 && this.day !== 4) return false; + // D2 T4; D4 has no letter task but keep day2-only for T4 + if (this.day === 2 && this.completedTasks.has('T4')) return false; + if (this.day !== 2) return false; + this.hasLetters = true; + gameEvents.emit('inventory', 'letters', true); + return true; + } + + deliverBread(to: string): boolean { + if (!this.hasBread) return false; + if (!this.breadDelivered.includes(to)) this.breadDelivered.push(to); + if (this.breadDelivered.length >= 3) { + this.hasBread = false; + const id = this.day === 4 ? 'T8' : 'T1'; + if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId); + return true; + } + return true; + } + + waterFlower(): void { + this.waterCount += 1; + this.microInteract('water'); + if (this.waterCount >= 3) { + const id = this.day === 4 ? 'T9' : 'T2'; + if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId); + } + } + + greetVillager(): void { + this.greetCount += 1; + if (this.greetCount >= 3) { + const id = this.day === 4 ? 'T10' : 'T3'; + if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId); + } + } + + deliverLetter(to: string): boolean { + if (!this.hasLetters) return false; + if (!this.lettersDelivered.includes(to)) this.lettersDelivered.push(to); + if (this.lettersDelivered.length === 3) { + this.discoverClue('C4'); + } + if (this.lettersDelivered.length >= 3) { + this.hasLetters = false; + this.completeTask('T4'); + } + return true; + } + + pickBerry(): void { + this.berryCount += 1; + if (this.berryCount >= 5) this.completeTask('T6'); + } + + hangRibbon(): void { + this.ribbonCount += 1; + if (this.ribbonCount >= 4) this.completeTask('T13'); + } + + startT14(): void { + this.t14Active = true; + this.t14ElapsedMs = 0; + gameEvents.emit('t14Start'); + } + + finishT14(): void { + this.t14Active = false; + this.completeTask('T14'); + gameEvents.emit('t14End'); + } + + startEnding(choseFogWall = false): EndingId { + this.choseFogWall = choseFogWall; + if (this.day >= 5 && this.day <= 7 && this.hadHighHappinessD5to7) { + this.lowHappinessD5to7 = false; + } + // Continuously ≤10 means never went above 10 on D5–7 + const lowOk = this.lowHappinessD5to7 && !this.hadHighHappinessD5to7; + const id = evaluateEnding({ + awareness: this.awareness, + happiness: this.happiness, + clueCount: this.clues.size, + lowHappinessStreakD5to7: lowOk, + celebrationEscapes: this.celebrationEscapes, + choseFogWall, + }); + this.endingId = id; + this.ended = true; + this.completeTask('T15'); + gameEvents.emit('ending', id); + return id; + } + + forceEnding(id: EndingId): void { + this.endingId = id; + this.ended = true; + gameEvents.emit('ending', id); + } + + snapshot(): DirectorSnapshot { + return { + playerName: this.playerName, + happiness: this.happiness, + awareness: this.awareness, + day: this.day, + stage: this.stage, + stageParams: this.getStageParams(), + petals: this.getPetals(), + blackSeeds: this.getBlackSeeds(), + clues: [...this.clues], + completedTasks: [...this.completedTasks], + refusedTasks: [...this.refusedTasks], + celebrationEscapes: this.celebrationEscapes, + overflowPool: this.overflowPool, + sessionStartMs: this.sessionStartMs, + dayPhase: this.dayPhase, + c9Step: this.c9Step, + lowHappinessSinceMs: this.lowHappinessSinceMs, + forcedCelebrationActive: this.forcedCelebrationActive, + rainActive: this.rainActive, + duskReached: this.duskReached, + boardReadToday: this.boardReadToday, + microCounts: { ...this.microCounts }, + npcTalkCounts: { ...this.npcTalkCounts }, + hasBread: this.hasBread, + hasLetters: this.hasLetters, + breadDelivered: [...this.breadDelivered], + lettersDelivered: [...this.lettersDelivered], + waterCount: this.waterCount, + greetCount: this.greetCount, + berryCount: this.berryCount, + ribbonCount: this.ribbonCount, + lowHappinessD5to7: this.lowHappinessD5to7, + wellGazeToday: this.wellGazeToday, + fogGazeToday: this.fogGazeToday, + t14Active: this.t14Active, + t14ElapsedMs: this.t14ElapsedMs, + }; + } + + private emitAll(): void { + gameEvents.emit('happiness', this.happiness); + gameEvents.emit('awareness', this.awareness); + gameEvents.emit('day', this.day); + gameEvents.emit('stage', this.stage, this.getStageParams()); + gameEvents.emit('petals', this.getPetals(), this.getBlackSeeds()); + } +} + +export const Director = new CorruptionDirectorImpl(); +export type CorruptionDirector = CorruptionDirectorImpl; diff --git a/Grok/src/systems/EventBus.ts b/Grok/src/systems/EventBus.ts new file mode 100644 index 0000000..08a5986 --- /dev/null +++ b/Grok/src/systems/EventBus.ts @@ -0,0 +1,27 @@ +type Handler = (...args: unknown[]) => void; + +/** Minimal event emitter shared by director & scenes */ +export class EventBus { + private listeners = new Map>(); + + on(event: string, handler: Handler): void { + if (!this.listeners.has(event)) this.listeners.set(event, new Set()); + this.listeners.get(event)!.add(handler); + } + + off(event: string, handler: Handler): void { + this.listeners.get(event)?.delete(handler); + } + + emit(event: string, ...args: unknown[]): void { + const set = this.listeners.get(event); + if (!set) return; + for (const h of set) h(...args); + } + + clear(): void { + this.listeners.clear(); + } +} + +export const gameEvents = new EventBus(); diff --git a/Grok/src/systems/debugKeys.test.ts b/Grok/src/systems/debugKeys.test.ts new file mode 100644 index 0000000..2cfc9ef --- /dev/null +++ b/Grok/src/systems/debugKeys.test.ts @@ -0,0 +1,127 @@ +/** + * Tests shipped parseDebugKey — simulates real browser KeyboardEvent shapes. + * Shift+1 emits key='!' code='Digit1' (not key='1'). + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { parseDebugKey } from './debugKeys'; +import { DEBUG_TELEPORT_SCENES, SCENE_KEYS } from '../data/constants'; +import { Director } from './CorruptionDirector'; + +describe('parseDebugKey (shipped Debug key map)', () => { + it('Shift+Digit1–7 sets day via code even when key is symbol', () => { + // Real browser: Shift+1 → key '!', code 'Digit1' + expect( + parseDebugKey({ key: '!', code: 'Digit1', shiftKey: true }), + ).toEqual({ type: 'setDay', day: 1 }); + expect( + parseDebugKey({ key: '@', code: 'Digit2', shiftKey: true }), + ).toEqual({ type: 'setDay', day: 2 }); + expect( + parseDebugKey({ key: '#', code: 'Digit3', shiftKey: true }), + ).toEqual({ type: 'setDay', day: 3 }); + expect( + parseDebugKey({ key: '$', code: 'Digit4', shiftKey: true }), + ).toEqual({ type: 'setDay', day: 4 }); + expect( + parseDebugKey({ key: '%', code: 'Digit5', shiftKey: true }), + ).toEqual({ type: 'setDay', day: 5 }); + expect( + parseDebugKey({ key: '^', code: 'Digit6', shiftKey: true }), + ).toEqual({ type: 'setDay', day: 6 }); + expect( + parseDebugKey({ key: '&', code: 'Digit7', shiftKey: true }), + ).toEqual({ type: 'setDay', day: 7 }); + }); + + it('broken old path (shift+key 1-7) would fail — we do not use key for day', () => { + // Demonstrates why key-based check is wrong; our code uses Digit* + const broken = (ev: { key: string; shiftKey: boolean }) => + ev.shiftKey && ev.key >= '1' && ev.key <= '7'; + expect(broken({ key: '!', shiftKey: true })).toBe(false); + // Our parser still works + expect(parseDebugKey({ key: '!', code: 'Digit1', shiftKey: true }).type).toBe('setDay'); + }); + + it('plain Digit0–9 teleports to all 10 scenes', () => { + for (let i = 0; i <= 9; i++) { + const a = parseDebugKey({ + key: String(i), + code: `Digit${i}`, + shiftKey: false, + }); + expect(a).toEqual({ + type: 'teleport', + sceneIndex: i, + scene: DEBUG_TELEPORT_SCENES[i], + }); + } + }); + + it('Shift+Digit does not teleport (day takes precedence)', () => { + const a = parseDebugKey({ key: '!', code: 'Digit1', shiftKey: true }); + expect(a.type).toBe('setDay'); + expect(a).not.toMatchObject({ type: 'teleport' }); + }); + + it('H/J/K/L/C/N/P/V/F1–F4', () => { + expect(parseDebugKey({ key: 'h', code: 'KeyH', shiftKey: false })).toEqual({ + type: 'happiness', + delta: 10, + }); + expect(parseDebugKey({ key: 'J', code: 'KeyJ', shiftKey: true })).toEqual({ + type: 'happiness', + delta: -10, + }); + expect(parseDebugKey({ key: 'k', code: 'KeyK', shiftKey: false })).toEqual({ + type: 'awareness', + delta: 10, + }); + expect(parseDebugKey({ key: 'c', code: 'KeyC', shiftKey: false }).type).toBe('grantClues'); + expect(parseDebugKey({ key: 'n', code: 'KeyN', shiftKey: false }).type).toBe('negativeFlash'); + expect(parseDebugKey({ key: 'p', code: 'KeyP', shiftKey: false }).type).toBe( + 'forcedCelebration', + ); + expect(parseDebugKey({ key: 'v', code: 'KeyV', shiftKey: false }).type).toBe('teleportCycle'); + expect(parseDebugKey({ key: 'F1', code: 'F1', shiftKey: false })).toEqual({ + type: 'forceEnding', + id: 'E1', + }); + expect(parseDebugKey({ key: 'F4', code: 'F4', shiftKey: false })).toEqual({ + type: 'forceEnding', + id: 'E4', + }); + }); +}); + +describe('parseDebugKey → Director.setDay real path', () => { + beforeEach(() => { + Director.resetSession('调试'); + }); + + it('applies setDay from Shift+Digit codes into Director', () => { + for (let day = 1; day <= 7; day++) { + const action = parseDebugKey({ + key: '!', // wrong symbol-like key as browser would + code: `Digit${day}`, + shiftKey: true, + }); + expect(action.type).toBe('setDay'); + if (action.type === 'setDay') { + Director.setDay(action.day); + expect(Director.day).toBe(day); + } + } + }); + + it('teleport indices map to real scene keys', () => { + const a = parseDebugKey({ key: '0', code: 'Digit0', shiftKey: false }); + expect(a.type).toBe('teleport'); + if (a.type === 'teleport') { + expect(a.scene).toBe(SCENE_KEYS.HOUSE); + } + const b = parseDebugKey({ key: '1', code: 'Digit1', shiftKey: false }); + if (b.type === 'teleport') { + expect(b.scene).toBe(SCENE_KEYS.PLAZA); + } + }); +}); diff --git a/Grok/src/systems/debugKeys.ts b/Grok/src/systems/debugKeys.ts new file mode 100644 index 0000000..aaf4c2f --- /dev/null +++ b/Grok/src/systems/debugKeys.ts @@ -0,0 +1,67 @@ +/** + * Pure Debug key mapping (§13). Shipped path used by UIScene. + * + * Important: with Shift held, browsers set `key` to '!'/'@'/… not '1'–'7'. + * Day set MUST use `code` Digit1–Digit7. + */ + +import { DEBUG_TELEPORT_SCENES } from '../data/constants'; +import type { EndingId } from '../data/endings'; + +export type DebugKeyInput = { + key: string; + code: string; + shiftKey: boolean; +}; + +export type DebugAction = + | { type: 'happiness'; delta: number } + | { type: 'awareness'; delta: number } + | { type: 'setDay'; day: number } + | { type: 'grantClues' } + | { type: 'teleport'; sceneIndex: number; scene: string } + | { type: 'teleportCycle' } + | { type: 'negativeFlash' } + | { type: 'forcedCelebration' } + | { type: 'forceEnding'; id: EndingId } + | { type: 'none' }; + +/** Map KeyboardEvent-like input → debug action (no Phaser / no Director). */ +export function parseDebugKey(ev: DebugKeyInput): DebugAction { + const k = ev.key; + const code = ev.code; + + if (k === 'h' || k === 'H') return { type: 'happiness', delta: 10 }; + if (k === 'j' || k === 'J') return { type: 'happiness', delta: -10 }; + if (k === 'k' || k === 'K') return { type: 'awareness', delta: 10 }; + if (k === 'l' || k === 'L') return { type: 'awareness', delta: -10 }; + if (k === 'c' || k === 'C') return { type: 'grantClues' }; + if (k === 'n' || k === 'N') return { type: 'negativeFlash' }; + if (k === 'p' || k === 'P') return { type: 'forcedCelebration' }; + if (k === 'v' || k === 'V') return { type: 'teleportCycle' }; + if (k === 'F1') return { type: 'forceEnding', id: 'E1' }; + if (k === 'F2') return { type: 'forceEnding', id: 'E2' }; + if (k === 'F3') return { type: 'forceEnding', id: 'E3' }; + if (k === 'F4') return { type: 'forceEnding', id: 'E4' }; + + // Shift + Digit1–7 → set day (use code; key becomes !@# with shift) + if (ev.shiftKey && /^Digit[1-7]$/.test(code)) { + const day = parseInt(code.replace('Digit', ''), 10); + return { type: 'setDay', day }; + } + + // Plain Digit0–9 → teleport (also accept key when no shift) + if (!ev.shiftKey && /^Digit[0-9]$/.test(code)) { + const idx = parseInt(code.replace('Digit', ''), 10); + const scene = DEBUG_TELEPORT_SCENES[idx]; + if (scene) return { type: 'teleport', sceneIndex: idx, scene }; + } + // Fallback: unshifted key '0'-'9' without relying on code + if (!ev.shiftKey && k >= '0' && k <= '9') { + const idx = parseInt(k, 10); + const scene = DEBUG_TELEPORT_SCENES[idx]; + if (scene) return { type: 'teleport', sceneIndex: idx, scene }; + } + + return { type: 'none' }; +} diff --git a/Grok/src/vite-env.d.ts b/Grok/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/Grok/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/Grok/tsconfig.json b/Grok/tsconfig.json new file mode 100644 index 0000000..40a8db6 --- /dev/null +++ b/Grok/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "types": [ + "vite/client", + "node" + ] + }, + "include": [ + "src" + ], + "exclude": [ + "src/**/*.test.ts" + ] +} \ No newline at end of file diff --git a/Grok/vite.config.ts b/Grok/vite.config.ts new file mode 100644 index 0000000..63330cf --- /dev/null +++ b/Grok/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + base: './', + server: { port: 5173 }, + preview: { port: 4173 }, + build: { + outDir: 'dist', + assetsInlineLimit: 0, + }, + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/Kimi-K3/CONTRACT.md b/Kimi-K3/CONTRACT.md new file mode 100644 index 0000000..a1fef37 --- /dev/null +++ b/Kimi-K3/CONTRACT.md @@ -0,0 +1,125 @@ +# CONTRACT.md — 资产 / 地图 / 代码三方契约(唯一真源) + +工作目录:`/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3` +PRD:同目录 `PRD.md`(所有剧情/数值/台词以 PRD 为准,本文件只定义技术接口)。 + +工具路径: +- Aseprite: `/Applications/Aseprite.app/Contents/MacOS/aseprite`(用法:`aseprite -b --script file.lua`;绘制用 `app.activeImage:drawPixel(x, y, app.pixelColor.rgba(r,g,b,a))`;导出用 `spr:saveAs("out.png")`。已验证可用) +- Tiled 1.12.2: `/Applications/Tiled.app/Contents/MacOS/Tiled`(用法:`Tiled -e script.js` 执行脚本,脚本内用 `var f = new TextFile(path, TextFile.WriteOnly); f.write(xml); f.commit();` 写 .tmx;再用 `Tiled --export-map --embed-tilesets in.tmx out.json` 导出 JSON。已验证可用;注意 `-e` 模式无编辑器,`tiled.open`/`TileMap` 部分 API 不可用,一律走 TextFile 写 XML) + +## 1. 目录结构 + +``` +public/assets/ + tileset.png # 256×128,16×16 瓦片,16 列 × 8 行 = 128 瓦片 + sprites/player.png # 64×96:帧 16×24,4 列(行走帧 0-3) × 4 行(方向 0=down 1=up 2=left 3=right),第 0 帧兼待机 + sprites/npc_afu.png # 48×24:3 帧横排 = smile / flat / off + sprites/npc_yuanyuan.png # 同上 + sprites/npc_dabao.png # 同上 + sprites/npc_mimi.png # 同上 + sprites/npc_fuyisheng.png # 同上 + sprites/npc_guiyeye.png # 同上 + sprites/npc_bird.png # 32×24:2 帧 = 闭嘴 / 张嘴 + sprites/extra_squirrel.png # 32×24:2 帧循环 + sprites/extra_hedgehog.png # 同上 + sprites/extra_fox.png # 同上 + sprites/extra_pig.png # 同上 + sprites/extra_chick.png # 同上 + portraits/player_smile.png # 32×32 + portraits/player_real.png # 32×32(E3 用:微笑消失、疲惫真实的脸) + portraits/afu.png yuanyuan.png dabao.png mimi.png fuyisheng.png guiyeye.png bird.png # 各 32×32,smile + maps/house.json plaza.json bakery.json shop.json clinic.json lake.json field.json well.json fog.json white.json + maps/*.tmx # Tiled 中间产物(保留在仓库) + maps/tileset.tsx # 外部瓦片集描述,image source="../tileset.png" +tools/aseprite/*.lua # 素材生成脚本(保留) +tools/tiled/genmaps.js # 地图生成脚本(保留) +src/ # 游戏代码 +``` + +素材规范(PRD §11):糖果色板(草绿 #7DC95E/#5FB34A、奶油黄 #FFF3D6、蜜糖橙 #F5A623、天空蓝 #8FD3FF、石路 #E8DCC8、湖水 #6EC1E4/#4FA8D8、粉 #F7B7CB、棕 #8B5A2B),白色层独立灰白板(#F5F5F5/#DADADA/#BDBDBD)。无抗锯齿、无渐变。`off` 档与 `smile` 档差异控制在 1–2 像素(如一只眼低 1px 或嘴角多 1px),不许鬼脸。背景透明(alpha=0)。PNG 必须精确尺寸(上面列出)。 + +## 2. 瓦片 ID 表(tileset.png,id = 行×16+列;地图数据 gid = id+1) + +| id | 名称 | id | 名称 | +|---|---|---|---| +| 0 草地 | 1 草+花A | 2 草+花B | 3 深草 | +| 4 土路 | 5 石路 | 6 石路alt | 7 广场砖 | +| 8 湖水A | 9 湖水B | 10 水岸北 | 11 水岸南 | +| 12 水岸西 | 13 水岸东 | 14 花坛粉 | 15 花坛黄 | +| 16 莓果丛(满) | 17 莓果丛(空) | 18 树左上 | 19 树右上 | +| 20 树左下 | 21 树右下 | 22 圆灌木 | 23 树桩 | +| 24 石头 | 25 路标 | 26 栅栏横 | 27 栅栏竖 | +| 28 路灯 | 29 彩带红 | 30 彩带黄 | 31 彩带蓝 | +| 32 奶油墙 | 33 奶油墙+窗 | 34 木板墙 | 35 橙屋顶左 | +| 36 橙屋顶中 | 37 橙屋顶右 | 38 门 | 39 门框顶 | +| 40 窗 | 41 烟囱 | 42 井左上 | 43 井右上 | +| 44 井左下 | 45 井右下 | 46 邮筒 | 47 公告板 | +| 48 木地板A | 49 木地板B | 50 室内墙 | 51 室内墙+窗 | +| 52 床左上 | 53 床右上 | 54 床左下 | 55 床右下 | +| 56 柜台左 | 57 柜台右 | 58 烤炉 | 59 空货架 | +| 60 满货架 | 61 桌子 | 62 椅子 | 63 地毯 | +| 64 日历 | 65 配方纸 | 66 文件纸 | 67 野餐布左上 | +| 68 野餐布右上 | 69 野餐布左下 | 70 野餐布右下 | 71 长椅左 | +| 72 长椅右 | 73 凳子 | 74 柜子 | 75 冰箱 | +| 76 水槽 | 77 盆栽 | 78 木桶 | 79 木箱 | +| 80 雾A | 81 雾B | 82 雾C | 83 白色地板 | +| 84 白色墙 | 85 白墙板 | 86 白门 | 87 白床左上 | +| 88 白床右上 | 89 白床左下 | 90 白床右下 | 91 白柜 | +| 92 白监护仪 | 93 白窗帘 | 94 白地板alt | 95 白天花灯 | +| 96 梯子 | 97 桥横 | 98 桥竖 | 99 蘑菇 | +| 100 花红 | 101 花黄 | 102 花蓝 | 103 蜂蜜罐 | +| 104 面包 | 105 信件 | 106 书 | 107 蜡烛 | +| 108 篮子 | 109 水桶 | 110 扫帚 | 111 蛋糕 | +| 112 红屋顶左 | 113 红屋顶中 | 114 红屋顶右 | 115 蓝屋顶左 | +| 116 蓝屋顶中 | 117 蓝屋顶右 | 118 粉墙 | 119 粉墙+窗 | +| 120 石墙 | 121 石墙+窗 | 122 遮阳篷 | 123 钟 | +| 124 旗 | 125 花环角 | 126 花环中 | 127 空(透明) | + +## 3. 地图契约(Tiled 导出 JSON;图层固定三层) + +每层地图固定: +- `ground`(瓦片层,无碰撞;必须全部用不透明瓦片——透明装饰瓦片只能放 `obstacles` 层,否则透明处露出黑底。大花 100-102、蘑菇 99、门 38 已在 tileset 里烘焙不透明底,可放 ground) +- `obstacles`(瓦片层,带图层自定义属性 `collides=true`(bool);代码对该层整层碰撞,但彩带(瓦片 29-31)在代码里排除碰撞、可穿过) +- `objects`(对象层,点对象) + +对象约定(name + 自定义属性): +- `name="spawn"`,属性 `id`(string):出生点。各图必有 `default`;室内图另有 `from_plaza`;plaza 有 `from_house/from_bakery/from_shop/from_clinic/from_lake/from_field/from_well/from_fog`。 +- `name="door"`,属性 `target`(string, 目标地图 key) + `spawn`(string, 目标 spawn id)。双向成对。 +- 落点安全:spawn 与任一门判定点的距离(以玩家测量点 y-8 计)必须 >14px(惯例 ≥24px);运行时另有门武装机制兜底——落到门旁时须先离开感应圈(>18px)才允许触发,且触发即消耗武装(须再次离开 >18px 才可重新触发),防止落点回弹与对话关闭帧的重复触发。 +- 2x2 拼块瓦片(树 18-21、井 42-45、床 52-55、野餐布 67-70、白床 87-90):四个象限占用连续 4 个 id;tileset 生成时把下半两个象限从自然落点(id+16/id+17)复制到 id+2/id+3。 +- `name="npc"`,属性 `id`(string):afu / yuanyuan / dabao / mimi / fuyisheng / guiyeye / bird / squirrel / hedgehog / fox / pig / chick。 +- `name="interact"`,属性 `id`(string):见下表。 + +10 张地图(尺寸 = 瓦片数;场景 key 同文件名): + +| key | 文件 | 尺寸 | 必填对象 | +|---|---|---|---| +| house | maps/house.json | 12×8 | spawn(default,from_plaza)、door→plaza、interact:bed、interact:calendar | +| plaza | maps/plaza.json | 30×20 | spawn(default,from_house,from_bakery,from_shop,from_clinic,from_lake,from_field,from_well,from_fog)、door×8(house/bakery/shop/clinic/lake/field/well/fog)、npc:afu(公告板旁)、npc:bird(树上)、npc:squirrel、npc:fox、interact:noticeboard、interact:flowerbed_1/2/3 | +| bakery | maps/bakery.json | 15×10 | spawn(default,from_plaza)、door→plaza、npc:yuanyuan、interact:counter_bread、interact:recipe_paper | +| shop | maps/shop.json | 15×10 | spawn(default,from_plaza)、door→plaza、npc:mimi、npc:hedgehog、interact:shelf_count | +| clinic | maps/clinic.json | 12×8 | spawn(default,from_plaza)、door→plaza、npc:fuyisheng、interact:doctor_desk | +| lake | maps/lake.json | 24×16 | spawn(default,from_plaza)、door→plaza、npc:guiyeye(长椅旁)、npc:pig、interact:picnic_mat | +| field | maps/field.json | 20×14 | spawn(default,from_plaza)、door→plaza、npc:chick、interact:berry_1..5、interact:water_point | +| well | maps/well.json | 14×10 | spawn(default,from_plaza)、door→plaza、interact:ancient_well | +| fog | maps/fog.json | 20×8 | spawn(default,from_plaza)、door→plaza、interact:fog_wall | +| white | maps/white.json | 20×14 | spawn(default);上半 20×6 走廊 + 左下 10×8 病房,内部连通;白色层瓦片(83–95) | + +地图要点(PRD §10):plaza 为枢纽,含公告板(47)、花坛(14/15)、双子鸟树(18-21)、庆典空地;室外场景用草地/石路/栅栏/房屋装饰铺满,lake 有大片湖水(8-13)与长椅(71/72),field 有莓果丛(16),well 有古井(42-45),fog 边缘铺雾(80-82),white 全用白色层瓦片(走廊+病房,病房内放白床 87-90 与白监护仪 92)。室内场景:墙(50/51)围边、木地板(48/49)、家具按 PRD §10 要点摆放。house 必须有床(52-55)与日历(64);bakery 有柜台(56/57)、烤炉(58)、配方纸(65);shop 有货架(59/60);clinic 有桌子(61)+文件纸(66)。 + +## 4. 代码架构契约(Phaser 3 + TS + Vite) + +- `src/main.ts`:Phaser config,逻辑 320×180,`pixelArt:true`,`scale:{ mode: Phaser.Scale.NONE, autoCenter: CENTER_BOTH, parent:'game' }` + window resize 监听做整数倍放大(canvas style 宽高 = 320k×180k,k = max(1, floor(min(winW/320, winH/180))))。 +- 场景 key:`BootScene / TitleScene / HouseScene / PlazaScene / BakeryScene / ShopScene / ClinicScene / LakeScene / FieldScene / WellScene / FogScene / WhiteScene / UIScene / DreamScene / EndingScene`。 +- 地图场景共用基类 `MapScene`(`src/scenes/MapScene.ts`):子类只提供 `mapKey` 与少量钩子。基类负责:tilemap 加载(`this.load.tilemapTiledJSON`,tileset 名 `candy` 对应 `assets/tileset.png`)、player(96 px/s、四向动画 walk-down/up/left/right + 待机帧、obstacles 层碰撞、相机跟随+钳制)、≤24px 出「按 E」提示(交 UIScene 显示)、door 切换(fade out/in + 目标 spawn)、NPC/群演实例化与表情档切换、注册 CorruptionDirector 的视听参数(相机 postFX:ColorMatrix 饱和度 / Vignette 晕影 / 负片单帧)。 +- `CorruptionDirector`(`src/core/CorruptionDirector.ts`):普通 TS 单例 + `Phaser.Events.EventEmitter`。状态:playerName、startTime(ms)、day(1-7)、happiness(0-100)、awareness(0-100)、clues(Set)、tasksDone(Set)、refusedTasks(Set)、escapedCelebrations、stage 派生值。事件名:`happiness` `awareness` `stage` `day` `clue` `task` `celebration` `ending`。是全部状态唯一真源;提供 debug 用 setter。 +- 对话:`UIScene` 底部通栏框(说话者名 + 头像 + 文本,E 推进;选项三选:温柔/迟疑/否定);文本腐蚀在渲染层按 §6.5 表替换(原文案干净)。头像用 portraits/。 +- 音频:`src/audio/AudioEngine.ts`,WebAudio 程序合成(无音频文件)。 +- Debug:`src/debug/DebugPanel.ts`,反引号键开关的 DOM 面板(默认隐藏、无 UI 痕迹):设幸福/觉知/日(1-7)/传送场景/置齐线索/强制结局 E1-E4。 +- 数值与台词逐字逐数实现 PRD §5–§9。 + +## 5. 验收相关硬约束 + +- `npm run build`(tsc --noEmit + vite build)必须零错误;`npm run preview` 可玩。 +- 无 localStorage、无外部网络请求、无外部音频/字体文件。 +- 控制台零报错。 diff --git a/Kimi-K3/Dockerfile b/Kimi-K3/Dockerfile new file mode 100644 index 0000000..b7fe45c --- /dev/null +++ b/Kimi-K3/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-alpine AS build +WORKDIR /app +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1 +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npx vite build +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html diff --git a/Kimi-K3/PRD.md b/Kimi-K3/PRD.md new file mode 100644 index 0000000..0f5ea47 --- /dev/null +++ b/Kimi-K3/PRD.md @@ -0,0 +1,226 @@ +# PRD:《崩坏童话:蜜糖村》网页版 + +> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局 + +## 1. 游戏理念与设计支柱 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。 + +四条设计支柱,所有实现争议回到这里裁决: + +1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。 +2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。 +3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。 +4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。 + +## 2. 世界观:三层结构 + +| 层 | 名称 | 玩家所见 | 实质 | +|---|---|---|---| +| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 | +| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" | +| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 | + +命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。 + +## 3. 主角设定 + +- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。 +- 性别中性像素小人,16×24 像素/帧,无内心独白。 +- 对话头像永远同一帧微笑(见支柱 2)。 +- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。 + +## 4. NPC 清单(封闭,7 组 + 群演) + +每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。 + +| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 | +|---|---|---|---|---|---| +| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 | +| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) | +| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 | +| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) | +| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 | +| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 | +| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 | +| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — | + +## 5. 七日故事线 + +每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。 + +| 日 | 主题 | 剧情要点 | 侵蚀预期 | +|---|---|---|---| +| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 | +| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 | +| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 | +| Day 4 | 完美的重复 | 任务 T8–T10 与 Day 1 的 T1–T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 | +| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 | +| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 | +| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 | + +阿福每日公告(逐字实现,注意措辞逐日滑移): + +- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」 +- D2「早上好呀!新的一天,新的幸福。祝你开心!」 +- D3「早上好!今天要把幸福装得满满的哦。要开心!」 +- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同) +- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」 +- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」 +- D7「庆典开始了。来吧。」 + +## 6. 核心系统 + +### 6.1 幸福值(明线) + +- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。 +- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。 +- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。 +- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。 +- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。 + +### 6.2 觉知值(暗线,永不显示) + +- 范围 0–100,开局 0。只升不降。 +- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。 + +### 6.3 悲伤行为(封闭清单,5 项) + +拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。 + +### 6.4 侵蚀阶段矩阵(唯一真源) + +阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。 + +| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5) | UI | +|---|---|---|---|---|---|---| +| S0 | 0–20 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 0–2 瓣 | +| S1 | 21–50 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 3–6 瓣 | +| S2 | 51–80 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 | +| S3 | 81–99 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 | +| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 | + +NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。 + +### 6.5 文本腐蚀 + +同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。 + +### 6.6 BGM 与音效 + +- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。 +- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。 +- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。 +- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。 + +## 7. 任务清单(封闭,15 个) + +| ID | 日 | 任务 | 幸福奖励 | 备注 | +|---|---|---|---|---| +| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 | +| T2 | D1 | 给广场花坛浇水 ×3 | +10 | | +| T3 | D1 | 与任意三位村民问好 | +10 | | +| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) | +| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) | +| T6 | D3 | 去花田采莓果 ×5 | +10 | | +| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 | +| T8–T10 | D4 | 与 T1–T3 逐字相同 | 同上 | 刻意恐怖谷 | +| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | | +| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) | +| T13 | D6 | 在广场挂彩带 ×4 | +10 | | +| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 | +| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 | + +## 8. 线索清单(封闭,9 处,每处觉知 +8) + +| ID | 位置/来源 | 内容 | 可发现时段 | +|---|---|---|---| +| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 | +| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 | +| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 | +| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 | +| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 | +| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5(T12 中) | +| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 | +| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 | +| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 | + +## 9. 结局(封闭,4 个) + +Day 7 庆典按以下优先级判定(自上而下第一个满足者生效): + +| 结局 | 条件 | 演出 | +|---|---|---| +| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 | +| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 | +| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 | +| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 | + +任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。 + +## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON) + +| 场景 | 尺寸(瓦片) | 要点 | +|---|---|---| +| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) | +| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 | +| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) | +| 杂货店(室内) | 15×10 | 货架(C5) | +| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) | +| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 | +| 花田 | 20×14 | 莓果丛(T6)、浇花点 | +| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 | +| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 | +| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 | + +统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。 + +## 11. 美术规范(Aseprite 制作,全部原创) + +- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。 +- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。 +- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。 +- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。 +- 禁止使用任何现成素材、素材包或开源 tileset。 + +## 12. 技术架构 + +- **Phaser 3 + TypeScript + Vite**,`npm run build` 产出纯静态 `dist/`,`npm run preview` 本地游玩。 +- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。 +- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`。 +- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。 +- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。 +- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。 +- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。 +- 无 localStorage、无外部网络请求、无外部音频/字体文件。 + +## 13. Debug 面板(服务验收,必须实现) + +按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。 + +## 14. 验收清单(共 20 条) + +1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。 +2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。 +3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。 +4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。 +5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。 +6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。 +7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。 +8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。 +9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。 +10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。 +11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。 +12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。 +13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。 +14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。 +15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。 +16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。 +17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。 +18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。 +19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。 +20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。 + +## 15. 明确不做 + +战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。 diff --git a/Kimi-K3/README.md b/Kimi-K3/README.md new file mode 100644 index 0000000..a9ad4e2 --- /dev/null +++ b/Kimi-K3/README.md @@ -0,0 +1,84 @@ +# 崩坏童话:蜜糖村(网页版) + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。 +纯单机网页游戏:无存档、无后端、无 localStorage,浏览器打开即玩。完整流程约 25–35 分钟,含 4 个结局(E1 糖衣 / E2 回收 / E3 苏醒 / E4 静默)。 + +## 启动方式 + +```bash +npm install # 安装依赖(phaser / vite / typescript) +npm run build # 产出纯静态 dist/ +npm run preview # 本地游玩 → http://127.0.0.1:4815/ +``` + +开发模式可用 `npm run dev`(热更新)。构建产物为纯静态文件,无外部网络请求、无外部音频/字体文件。 + +## 按键 + +| 按键 | 功能 | +|---|---| +| WASD / 方向键 | 移动(96 像素/秒,四向动画) | +| E | 交互 / 推进对话(靠近对象 ≤24 像素出现「按 E」提示) | +| ↑ / ↓ + E 或 1 / 2 / 3 | 对话选项(温柔 / 迟疑 / 否定) | +| `` ` ``(反引号) | Debug 面板开关 | + +## Debug 面板(PRD §13) + +默认关闭、无 UI 痕迹,不影响正常游玩。按反引号键 `` ` `` 开关,功能: + +- 直接设置幸福值(0–100)、觉知值(0–100)、当前日(1–7) +- 传送到任意场景(10 选) +- 置齐全部线索(9 处) +- 强制触发四个结局之一(E1–E4) + +## 玩法提示(无剧透) + +每天早上看广场公告板接「今日幸福任务」,完成后黄昏钟声响起,回小屋睡觉进入次日。幸福值由左上角向日葵表盘显示(12 瓣,无数字)。村里的规则是「必须幸福」——但通往真相的路,恰恰是那些系统禁止的悲伤。古井与雾墙从第 5 天起可以靠近。 + +## 素材与地图制作流程(全部原创,无现成素材) + +所有像素素材与地图均为本项目内程序化自制,未使用任何素材包或开源 tileset。生成脚本全部保留在仓库内,可一键复现。 + +### 像素素材(Aseprite 1.3,`tools/aseprite/`) + +- 脚本:`gen_all.lua` 总编排,dofile 组织 `helpers.lua`(糖果色板/白色层灰板 + 像素原语)、`gen_tileset.lua`、`gen_player.lua`、`gen_npcs.lua`、`gen_extras.lua`、`gen_portraits.lua`。 +- 复现命令: + ```bash + /Applications/Aseprite.app/Contents/MacOS/aseprite -b --script tools/aseprite/gen_all.lua + ``` +- 产出(`public/assets/`): + - `tileset.png` 256×128:16×16 瓦片 ×128(草地/石路/湖水/花丛/莓果丛/树/房屋/室内家具/井/雾/白色层,瓦片 id 表见 `CONTRACT.md` §2) + - `sprites/player.png` 64×96:16×24/帧,4 朝向 × 4 帧行走,第 0 帧兼待机 + - `sprites/npc_*.png`:7 组具名 NPC 各 3 档表情(smile/flat/off,off 与 smile 仅差 1–2 像素)+ 双子鸟 2 帧张嘴 + 群演 ×5 各 2 帧循环 + - `portraits/*.png`:主角微笑头像、E3 专用「真实的脸」头像、7 张 NPC 头像,各 32×32 + +### 地图(Tiled 1.12,`tools/tiled/`) + +- 脚本:`genmaps.js`(Tiled 脚本运行时 `Tiled -e` 执行,TextFile 写出 .tmx),再用 Tiled CLI 导出 JSON。 +- 复现命令: + ```bash + /Applications/Tiled.app/Contents/MacOS/Tiled -e tools/tiled/genmaps.js + for m in house plaza bakery shop clinic lake field well fog white; do + /Applications/Tiled.app/Contents/MacOS/Tiled --export-map --embed-tilesets \ + public/assets/maps/$m.tmx public/assets/maps/$m.json + done + ``` +- 产出(`public/assets/maps/`):10 张地图(小屋 12×8 / 广场 30×20 / 面包房 15×10 / 杂货店 15×10 / 诊所 12×8 / 湖畔 24×16 / 花田 20×14 / 古井 14×10 / 雾墙 20×8 / 白色层 20×14),统一 `ground` / `obstacles`(`collides=true`)/ `objects`(spawn、NPC 点位、door、交互点)三层结构;所有室外场景经村广场互通,门双向成对。 + +## 技术架构(PRD §12) + +- Phaser 3 + TypeScript + Vite;逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true` 禁用纹理平滑。 +- 场景:BootScene / TitleScene / 10 个地图 Scene(共用 MapScene 基类)/ 常驻 UIScene(对话框、向日葵表盘、提示)/ DreamScene / EndingScene。 +- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。 +- BGM 与音效全部 Web Audio 程序合成(8 小节 8-bit 循环:方波主旋律 + 三角波低音 + 鸟鸣声部;音效 4 个),playbackRate 与低通由 CorruptionDirector 按 §6.4 矩阵驱动。 +- 文本腐蚀在对话渲染层实施(§6.5 十组同音错字,原文案保持干净),优先命中咪咪。 + +## 验收 + +`tools/acceptance.mjs` 为自动化验收脚本(puppeteer-core + 本机 Chrome),覆盖 PRD §14 的 20 条清单(静态逐字校验 + 真实浏览器全流程游玩 + Debug 面板 + 四结局 + 截图): + +```bash +npm run build && (npm run preview &) && node tools/acceptance.mjs +``` + +截图输出在 `tools/shots/`。 diff --git a/Kimi-K3/index.html b/Kimi-K3/index.html new file mode 100644 index 0000000..375a35f --- /dev/null +++ b/Kimi-K3/index.html @@ -0,0 +1,17 @@ + + + + + + 崩坏童话:蜜糖村 + + + +
+ + + diff --git a/Kimi-K3/package-lock.json b/Kimi-K3/package-lock.json new file mode 100644 index 0000000..9ce77b0 --- /dev/null +++ b/Kimi-K3/package-lock.json @@ -0,0 +1,1365 @@ +{ + "name": "honey-village", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "honey-village", + "version": "1.0.0", + "dependencies": { + "phaser": "^3.80.1" + }, + "devDependencies": { + "puppeteer-core": "^25.3.0", + "typescript": "^5.5.0", + "vite": "^5.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.6.tgz", + "integrity": "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chromium-bidi": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1638949", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz", + "integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/phaser": { + "version": "3.90.0", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", + "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/puppeteer-core": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.3.0.tgz", + "integrity": "sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.6", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/Kimi-K3/package.json b/Kimi-K3/package.json new file mode 100644 index 0000000..7f86baf --- /dev/null +++ b/Kimi-K3/package.json @@ -0,0 +1,21 @@ +{ + "name": "honey-village", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "assets": "bash tools/make_assets.sh", + "maps": "bash tools/make_maps.sh" + }, + "dependencies": { + "phaser": "^3.80.1" + }, + "devDependencies": { + "puppeteer-core": "^25.3.0", + "typescript": "^5.5.0", + "vite": "^5.4.0" + } +} diff --git a/Kimi-K3/public/assets/maps/bakery.json b/Kimi-K3/public/assets/maps/bakery.json new file mode 100644 index 0000000..a31db3f --- /dev/null +++ b/Kimi-K3/public/assets/maps/bakery.json @@ -0,0 +1,207 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 64, 64, 64, 50, 49, 50, 49, 50, 49, 50, + 49, 50, 49, 50, 49, 64, 64, 64, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 50, 39, 50, 49, 50, 49, 50, 49, 50], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "data":[51, 51, 51, 52, 51, 51, 51, 52, 51, 51, 51, 52, 51, 51, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 57, 58, 0, 0, 57, 58, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 66, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 51, + 51, 51, 51, 51, 51, 51, 51, 0, 51, 51, 51, 51, 51, 51, 51], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":104 + }, + { + "height":0, + "id":2, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"from_plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":120 + }, + { + "height":0, + "id":3, + "name":"door", + "opacity":1, + "point":true, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"from_bakery" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":136 + }, + { + "height":0, + "id":4, + "name":"npc", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"yuanyuan" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":88, + "y":40 + }, + { + "height":0, + "id":5, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"counter_bread" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":72, + "y":72 + }, + { + "height":0, + "id":6, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"recipe_paper" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":184, + "y":120 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":7, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":15 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/bakery.tmx b/Kimi-K3/public/assets/maps/bakery.tmx new file mode 100644 index 0000000..650131b --- /dev/null +++ b/Kimi-K3/public/assets/maps/bakery.tmx @@ -0,0 +1,74 @@ + + + + + +49,50,49,50,49,50,49,50,49,50,49,50,49,50,49, +50,49,50,49,50,49,50,49,50,49,50,49,50,49,50, +49,50,49,50,49,50,49,50,49,50,49,50,49,50,49, +50,49,50,49,50,49,50,49,50,49,50,49,50,49,50, +49,50,49,50,49,50,49,50,49,50,49,50,49,50,49, +50,49,50,49,50,64,64,64,50,49,50,49,50,49,50, +49,50,49,50,49,64,64,64,49,50,49,50,49,50,49, +50,49,50,49,50,49,50,49,50,49,50,49,50,49,50, +49,50,49,50,49,50,49,50,49,50,49,50,49,50,49, +50,49,50,49,50,49,50,39,50,49,50,49,50,49,50 + + + + + + + +51,51,51,52,51,51,51,52,51,51,51,52,51,51,51, +51,0,0,0,0,0,0,0,0,0,0,0,59,0,51, +51,0,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,57,58,0,0,57,58,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,62,66,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,78,0,0,0,0,0,0,0,0,0,0,0,79,51, +51,51,51,51,51,51,51,0,51,51,51,51,51,51,51 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/clinic.json b/Kimi-K3/public/assets/maps/clinic.json new file mode 100644 index 0000000..65d8f23 --- /dev/null +++ b/Kimi-K3/public/assets/maps/clinic.json @@ -0,0 +1,184 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 50, 49, 50, 49, 50, 39, 50, 49, 50, 49, 50, 49], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "data":[51, 51, 51, 52, 51, 51, 51, 51, 52, 51, 51, 51, + 51, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 62, 67, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 51, + 51, 51, 51, 51, 51, 0, 51, 51, 51, 51, 51, 51], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":104, + "y":88 + }, + { + "height":0, + "id":2, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"from_plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":88, + "y":88 + }, + { + "height":0, + "id":3, + "name":"door", + "opacity":1, + "point":true, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"from_clinic" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":88, + "y":104 + }, + { + "height":0, + "id":4, + "name":"npc", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"fuyisheng" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":136, + "y":24 + }, + { + "height":0, + "id":5, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"doctor_desk" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":136, + "y":56 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":6, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/clinic.tmx b/Kimi-K3/public/assets/maps/clinic.tmx new file mode 100644 index 0000000..a82ce5e --- /dev/null +++ b/Kimi-K3/public/assets/maps/clinic.tmx @@ -0,0 +1,64 @@ + + + + + +49,50,49,50,49,50,49,50,49,50,49,50, +50,49,50,49,50,49,50,49,50,49,50,49, +49,50,49,50,49,50,49,50,49,50,49,50, +50,49,50,49,50,49,50,49,50,49,50,49, +49,50,49,50,49,50,49,50,49,50,49,50, +50,49,50,49,50,49,50,49,50,49,50,49, +49,50,49,50,49,50,49,50,49,50,49,50, +50,49,50,49,50,39,50,49,50,49,50,49 + + + + + + + +51,51,51,52,51,51,51,51,52,51,51,51, +51,75,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,62,67,0,51, +51,0,0,0,0,0,0,0,0,63,0,51, +51,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,78,51, +51,51,51,51,51,0,51,51,51,51,51,51 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/field.json b/Kimi-K3/public/assets/maps/field.json new file mode 100644 index 0000000..1e82164 --- /dev/null +++ b/Kimi-K3/public/assets/maps/field.json @@ -0,0 +1,291 @@ +{ "compressionlevel":-1, + "height":14, + "infinite":false, + "layers":[ + { + "data":[101, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 3, 1, 1, 1, 1, 1, 1, + 1, 3, 1, 1, 1, 1, 4, 1, 3, 2, 103, 1, 2, 1, 1, 1, 3, 1, 102, 1, + 1, 1, 1, 2, 1, 3, 2, 1, 1, 1, 101, 1, 103, 1, 1, 1, 2, 1, 1, 101, + 1, 3, 1, 1, 2, 1, 101, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, + 1, 1, 1, 1, 2, 1, 2, 103, 1, 1, 103, 4, 1, 1, 1, 1, 3, 1, 1, 1, + 1, 1, 1, 2, 1, 1, 1, 1, 102, 1, 3, 1, 1, 1, 1, 3, 1, 1, 103, 1, + 1, 1, 1, 2, 1, 1, 1, 3, 1, 1, 2, 1, 101, 1, 3, 1, 1, 1, 1, 1, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 1, 1, 1, 1, 4, 4, 2, 1, 103, 1, 1, 1, 1, 4, 1, 1, 2, 1, 4, + 1, 1, 1, 1, 4, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 101, 102, 103, 101, 1, + 1, 103, 1, 102, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 102, 101, 103, 1, 1, + 103, 1, 4, 2, 1, 1, 1, 2, 1, 2, 2, 1, 1, 1, 2, 1, 1, 1, 1, 3, + 3, 102, 1, 3, 1, 3, 1, 2, 1, 1, 4, 3, 1, 1, 3, 1, 1, 1, 1, 1, + 102, 1, 1, 1, 1, 2, 1, 103, 1, 1, 1, 101, 1, 1, 1, 1, 1, 1, 1, 4], + "height":14, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, 28, + 28, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 22, 0, 28, + 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, + 28, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 28, + 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, + 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, + 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, + 28, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 28, + 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, + 28, 0, 23, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 28, + 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27], + "height":14, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":40, + "y":120 + }, + { + "height":0, + "id":2, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"from_plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":24, + "y":120 + }, + { + "height":0, + "id":3, + "name":"door", + "opacity":1, + "point":true, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"from_field" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":8, + "y":120 + }, + { + "height":0, + "id":4, + "name":"npc", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"chick" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":168, + "y":120 + }, + { + "height":0, + "id":5, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry_1" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":72, + "y":88 + }, + { + "height":0, + "id":6, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry_2" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":152, + "y":88 + }, + { + "height":0, + "id":7, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry_3" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":232, + "y":88 + }, + { + "height":0, + "id":8, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry_4" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":104, + "y":168 + }, + { + "height":0, + "id":9, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"berry_5" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":200, + "y":168 + }, + { + "height":0, + "id":10, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"water_point" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":264, + "y":168 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":11, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/field.tmx b/Kimi-K3/public/assets/maps/field.tmx new file mode 100644 index 0000000..518a7b5 --- /dev/null +++ b/Kimi-K3/public/assets/maps/field.tmx @@ -0,0 +1,106 @@ + + + + + +101,2,1,1,1,1,1,1,1,1,2,1,2,3,1,1,1,1,1,1, +1,3,1,1,1,1,4,1,3,2,103,1,2,1,1,1,3,1,102,1, +1,1,1,2,1,3,2,1,1,1,101,1,103,1,1,1,2,1,1,101, +1,3,1,1,2,1,101,4,1,1,1,1,1,1,1,1,1,3,1,1, +1,1,1,1,2,1,2,103,1,1,103,4,1,1,1,1,3,1,1,1, +1,1,1,2,1,1,1,1,102,1,3,1,1,1,1,3,1,1,103,1, +1,1,1,2,1,1,1,3,1,1,2,1,101,1,3,1,1,1,1,1, +5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +2,1,1,1,1,4,4,2,1,103,1,1,1,1,4,1,1,2,1,4, +1,1,1,1,4,1,1,4,1,1,1,1,1,1,1,101,102,103,101,1, +1,103,1,102,1,1,1,1,1,1,1,1,1,1,1,102,101,103,1,1, +103,1,4,2,1,1,1,2,1,2,2,1,1,1,2,1,1,1,1,3, +3,102,1,3,1,3,1,2,1,1,4,3,1,1,3,1,1,1,1,1, +102,1,1,1,1,2,1,103,1,1,1,101,1,1,1,1,1,1,1,4 + + + + + + + +27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27, +28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,0,28, +28,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,21,22,0,28, +28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28, +28,0,0,0,17,0,0,0,0,17,0,0,0,0,17,0,0,0,0,28, +28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28, +28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28, +28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28, +28,0,0,0,0,0,17,0,0,0,0,0,17,0,0,0,0,0,0,28, +28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28, +28,0,23,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,0,28, +28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28, +27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/fog.json b/Kimi-K3/public/assets/maps/fog.json new file mode 100644 index 0000000..660739e --- /dev/null +++ b/Kimi-K3/public/assets/maps/fog.json @@ -0,0 +1,165 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 3, 1, 1, 1, 2, 1, 5, 5, 1, 1, 1, 1, 1, 4, 1, 3, 1, + 4, 1, 4, 2, 4, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 2, 1, 1, + 1, 1, 1, 1, 1, 3, 1, 1, 1, 5, 5, 1, 1, 1, 1, 4, 1, 1, 1, 4, + 4, 1, 2, 4, 1, 1, 1, 1, 2, 5, 5, 1, 1, 1, 1, 4, 1, 1, 1, 2, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 1, 2, 1, 1, 3, 2, 3, 1, 1, 1, 3, 1, 1, 2, 1, 1, 1, 1, 4, 1, + 1, 1, 1, 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 3, 1, + 2, 1, 1, 1, 1, 1, 3, 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1, 2], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, + 0, 21, 22, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 22, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 23, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, + 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, + 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":168, + "y":40 + }, + { + "height":0, + "id":2, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"from_plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":152, + "y":40 + }, + { + "height":0, + "id":3, + "name":"door", + "opacity":1, + "point":true, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"from_fog" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":168, + "y":8 + }, + { + "height":0, + "id":4, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"fog_wall" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":168, + "y":72 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":5, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/fog.tmx b/Kimi-K3/public/assets/maps/fog.tmx new file mode 100644 index 0000000..66f2790 --- /dev/null +++ b/Kimi-K3/public/assets/maps/fog.tmx @@ -0,0 +1,58 @@ + + + + + +1,1,1,3,1,1,1,2,1,5,5,1,1,1,1,1,4,1,3,1, +4,1,4,2,4,1,1,1,1,5,5,1,1,1,1,1,1,2,1,1, +1,1,1,1,1,3,1,1,1,5,5,1,1,1,1,4,1,1,1,4, +4,1,2,4,1,1,1,1,2,5,5,1,1,1,1,4,1,1,1,2, +4,4,4,4,4,4,4,4,4,5,5,4,4,4,4,4,4,4,4,4, +1,2,1,1,3,2,3,1,1,1,3,1,1,2,1,1,1,1,4,1, +1,1,1,3,4,1,1,1,1,1,1,1,1,1,4,1,1,1,3,1, +2,1,1,1,1,1,3,3,1,1,1,1,2,1,1,1,1,4,1,2 + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,19,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,0, +0,21,22,0,0,0,25,0,0,0,0,0,0,0,0,0,0,21,22,0, +23,0,0,0,23,0,0,0,0,0,0,0,0,0,0,23,0,0,0,23, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81, +81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82, +82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/house.json b/Kimi-K3/public/assets/maps/house.json new file mode 100644 index 0000000..3416121 --- /dev/null +++ b/Kimi-K3/public/assets/maps/house.json @@ -0,0 +1,184 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 50, 49, 50, 49, 50, 64, 64, 49, 50, 49, 50, 49, + 49, 50, 49, 50, 49, 64, 64, 50, 49, 50, 49, 50, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 50, 49, 50, 49, 50, 39, 50, 49, 50, 49, 50, 49], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "data":[51, 51, 51, 52, 51, 51, 51, 51, 52, 51, 51, 51, + 51, 53, 54, 65, 0, 0, 0, 0, 0, 0, 78, 51, + 51, 55, 56, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 63, 62, 63, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 51, 51, 51, 51, 0, 51, 51, 51, 51, 51, 51], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":56, + "y":56 + }, + { + "height":0, + "id":2, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"from_plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":88, + "y":88 + }, + { + "height":0, + "id":3, + "name":"door", + "opacity":1, + "point":true, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"from_house" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":88, + "y":104 + }, + { + "height":0, + "id":4, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"bed" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":24, + "y":56 + }, + { + "height":0, + "id":5, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"calendar" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":56, + "y":40 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":6, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/house.tmx b/Kimi-K3/public/assets/maps/house.tmx new file mode 100644 index 0000000..4e1e905 --- /dev/null +++ b/Kimi-K3/public/assets/maps/house.tmx @@ -0,0 +1,64 @@ + + + + + +49,50,49,50,49,50,49,50,49,50,49,50, +50,49,50,49,50,49,50,49,50,49,50,49, +49,50,49,50,49,50,49,50,49,50,49,50, +50,49,50,49,50,64,64,49,50,49,50,49, +49,50,49,50,49,64,64,50,49,50,49,50, +50,49,50,49,50,49,50,49,50,49,50,49, +49,50,49,50,49,50,49,50,49,50,49,50, +50,49,50,49,50,39,50,49,50,49,50,49 + + + + + + + +51,51,51,52,51,51,51,51,52,51,51,51, +51,53,54,65,0,0,0,0,0,0,78,51, +51,55,56,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,63,62,63,0,51, +51,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,51, +51,51,51,51,51,0,51,51,51,51,51,51 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/lake.json b/Kimi-K3/public/assets/maps/lake.json new file mode 100644 index 0000000..c52ea4d --- /dev/null +++ b/Kimi-K3/public/assets/maps/lake.json @@ -0,0 +1,219 @@ +{ "compressionlevel":-1, + "height":16, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 101, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 101, 3, 1, 1, 1, 1, 1, 102, + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 101, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 4, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 4, 4, 1, 2, 2, 1, 1, 3, 102, 1, + 1, 1, 1, 1, 2, 1, 1, 103, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, + 1, 1, 2, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 101, 1, 3, 4, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 102, 1, 1, 1, 1, + 3, 1, 1, 7, 6, 6, 7, 6, 6, 7, 6, 6, 7, 6, 6, 68, 69, 6, 7, 6, 6, 7, 6, 6, + 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 3, 2, 1, 70, 71, 1, 1, 2, 1, 1, 1, 1, + 1, 1, 1, 100, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 102, 1, + 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 103, 1, + 1, 1, 1, 1, 1, 2, 1, 1, 4, 1, 1, 1, 1, 1, 4, 1, 1, 1, 103, 2, 1, 4, 1, 1, + 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 102, 1, 1, 3, 1, 1, 3, 1, 3, 102, 1, 1, 1, 1], + "height":16, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":24, + "x":0, + "y":0 + }, + { + "data":[0, 13, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 14, 0, + 0, 13, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 14, 0, + 0, 13, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 14, 0, + 0, 13, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 14, 0, + 0, 13, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 14, 0, + 0, 13, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 14, 0, + 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 72, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, 0, + 0, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 21, 22, 0, 0, + 0, 21, 22, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":16, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":24, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":344, + "y":184 + }, + { + "height":0, + "id":2, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"from_plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":360, + "y":168 + }, + { + "height":0, + "id":3, + "name":"door", + "opacity":1, + "point":true, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"from_lake" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":376, + "y":168 + }, + { + "height":0, + "id":4, + "name":"npc", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"guiyeye" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":200, + "y":136 + }, + { + "height":0, + "id":5, + "name":"npc", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"pig" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":104, + "y":200 + }, + { + "height":0, + "id":6, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"picnic_mat" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":264, + "y":184 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":7, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":24 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/lake.tmx b/Kimi-K3/public/assets/maps/lake.tmx new file mode 100644 index 0000000..36fe5e4 --- /dev/null +++ b/Kimi-K3/public/assets/maps/lake.tmx @@ -0,0 +1,86 @@ + + + + + +1,1,1,1,1,1,101,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,3,1,1,1,1,1,1, +1,1,1,1,1,2,1,1,1,1,1,1,3,1,1,1,101,3,1,1,1,1,1,102, +1,1,1,1,1,1,2,1,1,1,1,2,101,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,2,1,1,1,2,1,4,1,1,1,2,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1, +1,4,1,1,1,1,1,1,1,1,2,1,1,1,4,4,1,2,2,1,1,3,102,1, +1,1,1,1,2,1,1,103,1,1,3,1,1,1,1,1,1,1,1,1,1,2,1,1, +1,1,2,1,1,1,1,1,4,1,1,1,1,1,1,1,3,1,1,1,1,1,1,2, +1,1,1,101,1,3,4,1,1,2,1,1,1,1,1,1,1,1,1,102,1,1,1,1, +3,1,1,7,6,6,7,6,6,7,6,6,7,6,6,68,69,6,7,6,6,7,6,6, +2,1,1,1,1,1,1,1,2,1,1,1,3,2,1,70,71,1,1,2,1,1,1,1, +1,1,1,100,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,102,1, +1,1,1,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,103,1, +1,1,1,1,1,2,1,1,4,1,1,1,1,1,4,1,1,1,103,2,1,4,1,1, +1,1,1,1,1,3,1,1,1,1,102,1,1,3,1,1,3,1,3,102,1,1,1,1 + + + + + + + +0,13,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,14,0, +0,13,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,14,0, +0,13,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,14,0, +0,13,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,14,0, +0,13,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,14,0, +0,13,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,14,0, +0,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,72,73,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,0,0, +0,19,20,0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,21,22,0,0, +0,21,22,0,0,0,0,0,23,0,0,0,0,0,24,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/plaza.json b/Kimi-K3/public/assets/maps/plaza.json new file mode 100644 index 0000000..23e31c6 --- /dev/null +++ b/Kimi-K3/public/assets/maps/plaza.json @@ -0,0 +1 @@ +{"compressionlevel":-1,"height":20,"infinite":false,"layers":[{"data":[1,1,1,1,1,4,1,102,1,3,1,1,1,1,7,7,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,103,1,1,1,1,2,1,6,6,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,3,1,1,6,6,1,1,1,1,1,103,101,1,1,1,1,1,3,1,1,1,1,4,1,1,1,1,3,1,1,1,1,1,7,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,6,1,1,1,3,1,1,3,1,1,1,1,1,1,1,1,1,1,1,6,1,1,1,1,1,1,6,1,1,6,6,4,1,6,1,1,3,1,2,1,6,2,1,1,1,1,1,1,1,6,1,3,1,1,3,1,6,1,4,7,7,1,2,6,1,1,4,2,1,1,6,1,1,1,1,102,1,1,1,6,1,2,1,1,1,1,6,1,1,6,6,1,1,6,1,1,1,1,1,1,6,1,1,1,1,1,1,1,1,6,1,1,3,1,1,1,8,8,8,8,8,8,8,8,1,1,3,1,1,1,6,1,1,1,1,7,6,6,7,6,6,7,6,6,7,6,8,8,8,8,8,8,8,8,6,6,7,6,6,7,6,6,7,6,6,7,6,6,7,6,6,7,6,6,7,6,8,8,8,8,8,8,8,8,6,6,7,6,6,7,6,6,7,6,6,1,1,1,1,1,1,2,1,1,1,1,8,8,8,8,8,8,8,8,4,1,1,2,1,1,1,1,1,2,102,1,1,2,1,3,1,3,1,1,1,4,8,8,8,8,8,8,8,8,4,2,1,102,1,3,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,4,4,6,6,1,1,1,1,1,1,2,1,1,3,1,1,1,1,1,1,1,4,1,3,1,1,1,103,4,1,1,1,6,6,101,1,1,2,1,1,1,2,1,1,1,1,1,1,1,1,1,1,3,3,1,1,1,1,1,1,4,1,7,7,1,1,1,1,1,1,4,1,1,1,1,3,1,1,2,1,1,1,3,1,1,1,1,1,1,1,2,1,6,6,1,1,1,1,3,1,1,2,4,1,1,1,1,2,1,1,1,1,1,1,103,4,1,4,1,1,1,1,6,6,101,1,1,1,1,2,1,1,1,1,1,1,1,3,1,1,1,1,1,103,4,1,1,1,1,1,101,1,7,7,1,1,1,3,1,1,3,1,1,1,1,1,2,1,1,4,1,3,4,1,1,1,1,1,3,1,4,1,6,6,1,1,1,2,3,1,2,1,1,1,1,2,1,4],"height":20,"id":1,"name":"ground","opacity":1,"type":"tilelayer","visible":true,"width":30,"x":0,"y":0},{"data":[24,23,25,24,24,24,25,25,23,23,23,24,24,24,0,0,23,23,25,24,23,23,25,24,23,23,25,23,24,23,24,0,36,42,37,37,38,0,0,113,42,114,114,115,0,0,36,42,37,37,38,0,0,113,114,114,114,115,0,23,25,0,36,37,37,37,38,0,0,113,114,114,114,115,0,0,36,37,37,37,38,0,0,113,114,114,114,115,0,25,23,0,33,34,40,34,33,0,0,33,34,40,34,33,0,0,33,34,40,34,33,0,0,33,34,40,34,33,0,23,23,0,33,33,39,33,33,0,0,33,33,39,33,33,0,0,33,33,39,33,33,0,0,33,33,39,33,33,0,23,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,24,19,20,0,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,0,25,25,21,22,0,0,0,0,0,0,0,0,29,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,0,0,23,25,0,47,0,0,0,0,0,0,0,15,0,0,0,48,0,0,0,0,16,0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,47,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,25,0,0,0,0,0,0,0,0,0,0,29,0,0,15,0,0,0,29,0,0,0,0,0,0,0,0,0,0,23,25,0,0,0,0,0,0,0,0,0,0,0,19,20,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,23,25,0,0,0,0,0,0,0,0,0,0,0,21,22,0,0,23,0,0,0,0,0,0,0,0,0,0,21,22,24,25,19,20,0,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,23,21,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,23,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,24,25,23,25,23,25,25,25,23,25,24,24,23,23,0,0,23,23,23,23,23,25,23,23,24,23,23,23,23,23],"height":20,"id":2,"name":"obstacles","opacity":1,"properties":[{"name":"collides","type":"bool","value":true}],"type":"tilelayer","visible":true,"width":30,"x":0,"y":0},{"draworder":"topdown","id":3,"name":"objects","objects":[{"height":0,"id":1,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"default"}],"rotation":0,"type":"","visible":true,"width":0,"x":232,"y":184},{"height":0,"id":2,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"from_house"}],"rotation":0,"type":"","visible":true,"width":0,"x":72,"y":120},{"height":0,"id":3,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"from_bakery"}],"rotation":0,"type":"","visible":true,"width":0,"x":184,"y":120},{"height":0,"id":4,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"from_shop"}],"rotation":0,"type":"","visible":true,"width":0,"x":296,"y":120},{"height":0,"id":5,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"from_clinic"}],"rotation":0,"type":"","visible":true,"width":0,"x":408,"y":120},{"height":0,"id":6,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"from_lake"}],"rotation":0,"type":"","visible":true,"width":0,"x":24,"y":168},{"height":0,"id":7,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"from_field"}],"rotation":0,"type":"","visible":true,"width":0,"x":456,"y":168},{"height":0,"id":8,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"from_well"}],"rotation":0,"type":"","visible":true,"width":0,"x":232,"y":40},{"height":0,"id":9,"name":"spawn","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"from_fog"}],"rotation":0,"type":"","visible":true,"width":0,"x":248,"y":296},{"height":0,"id":10,"name":"door","opacity":1,"point":true,"properties":[{"name":"spawn","type":"string","value":"from_plaza"},{"name":"target","type":"string","value":"house"}],"rotation":0,"type":"","visible":true,"width":0,"x":72,"y":88},{"height":0,"id":11,"name":"door","opacity":1,"point":true,"properties":[{"name":"spawn","type":"string","value":"from_plaza"},{"name":"target","type":"string","value":"bakery"}],"rotation":0,"type":"","visible":true,"width":0,"x":184,"y":88},{"height":0,"id":12,"name":"door","opacity":1,"point":true,"properties":[{"name":"spawn","type":"string","value":"from_plaza"},{"name":"target","type":"string","value":"shop"}],"rotation":0,"type":"","visible":true,"width":0,"x":296,"y":88},{"height":0,"id":13,"name":"door","opacity":1,"point":true,"properties":[{"name":"spawn","type":"string","value":"from_plaza"},{"name":"target","type":"string","value":"clinic"}],"rotation":0,"type":"","visible":true,"width":0,"x":408,"y":88},{"height":0,"id":14,"name":"door","opacity":1,"point":true,"properties":[{"name":"spawn","type":"string","value":"from_plaza"},{"name":"target","type":"string","value":"lake"}],"rotation":0,"type":"","visible":true,"width":0,"x":8,"y":152},{"height":0,"id":15,"name":"door","opacity":1,"point":true,"properties":[{"name":"spawn","type":"string","value":"from_plaza"},{"name":"target","type":"string","value":"field"}],"rotation":0,"type":"","visible":true,"width":0,"x":472,"y":168},{"height":0,"id":16,"name":"door","opacity":1,"point":true,"properties":[{"name":"spawn","type":"string","value":"from_plaza"},{"name":"target","type":"string","value":"well"}],"rotation":0,"type":"","visible":true,"width":0,"x":232,"y":8},{"height":0,"id":17,"name":"door","opacity":1,"point":true,"properties":[{"name":"spawn","type":"string","value":"from_plaza"},{"name":"target","type":"string","value":"fog"}],"rotation":0,"type":"","visible":true,"width":0,"x":248,"y":312},{"height":0,"id":18,"name":"npc","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"afu"}],"rotation":0,"type":"","visible":true,"width":0,"x":248,"y":136},{"height":0,"id":19,"name":"npc","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"bird"}],"rotation":0,"type":"","visible":true,"width":0,"x":200,"y":232},{"height":0,"id":20,"name":"npc","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"squirrel"}],"rotation":0,"type":"","visible":true,"width":0,"x":120,"y":200},{"height":0,"id":21,"name":"npc","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"fox"}],"rotation":0,"type":"","visible":true,"width":0,"x":360,"y":232},{"height":0,"id":22,"name":"npc","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"dabao"}],"rotation":0,"type":"","visible":true,"width":0,"x":56,"y":136},{"height":0,"id":23,"name":"interact","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"noticeboard"}],"rotation":0,"type":"","visible":true,"width":0,"x":232,"y":152},{"height":0,"id":24,"name":"interact","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"flowerbed_1"}],"rotation":0,"type":"","visible":true,"width":0,"x":168,"y":152},{"height":0,"id":25,"name":"interact","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"flowerbed_2"}],"rotation":0,"type":"","visible":true,"width":0,"x":312,"y":152},{"height":0,"id":26,"name":"interact","opacity":1,"point":true,"properties":[{"name":"id","type":"string","value":"flowerbed_3"}],"rotation":0,"type":"","visible":true,"width":0,"x":232,"y":232}],"opacity":1,"type":"objectgroup","visible":true,"x":0,"y":0}],"nextlayerid":4,"nextobjectid":27,"orientation":"orthogonal","renderorder":"right-down","tiledversion":"1.12.2","tileheight":16,"tilesets":[{"columns":16,"firstgid":1,"image":"../tileset.png","imageheight":128,"imagewidth":256,"margin":0,"name":"candy","spacing":0,"tilecount":128,"tileheight":16,"tilewidth":16}],"tilewidth":16,"type":"map","version":"1.10","width":30} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/plaza.tmx b/Kimi-K3/public/assets/maps/plaza.tmx new file mode 100644 index 0000000..b2870d0 --- /dev/null +++ b/Kimi-K3/public/assets/maps/plaza.tmx @@ -0,0 +1,221 @@ + + + + + +1,1,1,1,1,4,1,102,1,3,1,1,1,1,7,7,1,1,1,1,1,1,1,1,1,3,1,1,1,1, +1,1,1,1,1,1,1,103,1,1,1,1,2,1,6,6,1,2,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,3,1,1,1,1,3,1,1,6,6,1,1,1,1,1,103,101,1,1,1,1,1,3,1, +1,1,1,4,1,1,1,1,3,1,1,1,1,1,7,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,6,1,1,1,3,1,1,3,1,1,1,1,1,1,1, +1,1,1,1,6,1,1,1,1,1,1,6,1,1,6,6,4,1,6,1,1,3,1,2,1,6,2,1,1,1, +1,1,1,1,6,1,3,1,1,3,1,6,1,4,7,7,1,2,6,1,1,4,2,1,1,6,1,1,1,1, +102,1,1,1,6,1,2,1,1,1,1,6,1,1,6,6,1,1,6,1,1,1,1,1,1,6,1,1,1,1, +1,1,1,1,6,1,1,3,1,1,1,8,8,8,8,8,8,8,8,1,1,3,1,1,1,6,1,1,1,1, +7,6,6,7,6,6,7,6,6,7,6,8,8,8,8,8,8,8,8,6,6,7,6,6,7,6,6,7,6,6, +7,6,6,7,6,6,7,6,6,7,6,8,8,8,8,8,8,8,8,6,6,7,6,6,7,6,6,7,6,6, +1,1,1,1,1,1,2,1,1,1,1,8,8,8,8,8,8,8,8,4,1,1,2,1,1,1,1,1,2,102, +1,1,2,1,3,1,3,1,1,1,4,8,8,8,8,8,8,8,8,4,2,1,102,1,3,1,2,1,2,1, +1,1,1,1,1,1,1,1,1,1,1,1,4,4,6,6,1,1,1,1,1,1,2,1,1,3,1,1,1,1, +1,1,1,4,1,3,1,1,1,103,4,1,1,1,6,6,101,1,1,2,1,1,1,2,1,1,1,1,1,1, +1,1,1,1,3,3,1,1,1,1,1,1,4,1,7,7,1,1,1,1,1,1,4,1,1,1,1,3,1,1, +2,1,1,1,3,1,1,1,1,1,1,1,2,1,6,6,1,1,1,1,3,1,1,2,4,1,1,1,1,2, +1,1,1,1,1,1,103,4,1,4,1,1,1,1,6,6,101,1,1,1,1,2,1,1,1,1,1,1,1,3, +1,1,1,1,1,103,4,1,1,1,1,1,101,1,7,7,1,1,1,3,1,1,3,1,1,1,1,1,2,1, +1,4,1,3,4,1,1,1,1,1,3,1,4,1,6,6,1,1,1,2,3,1,2,1,1,1,1,2,1,4 + + + + + + + +24,23,25,24,24,24,25,25,23,23,23,24,24,24,0,0,23,23,25,24,23,23,25,24,23,23,25,23,24,23, +24,0,36,42,37,37,38,0,0,113,42,114,114,115,0,0,36,42,37,37,38,0,0,113,114,114,114,115,0,23, +25,0,36,37,37,37,38,0,0,113,114,114,114,115,0,0,36,37,37,37,38,0,0,113,114,114,114,115,0,25, +23,0,33,34,40,34,33,0,0,33,34,40,34,33,0,0,33,34,40,34,33,0,0,33,34,40,34,33,0,23, +23,0,33,33,39,33,33,0,0,33,33,39,33,33,0,0,33,33,39,33,33,0,0,33,33,39,33,33,0,23, +25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25, +24,19,20,0,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,0,25, +25,21,22,0,0,0,0,0,0,0,0,29,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,0,0,23, +25,0,47,0,0,0,0,0,0,0,15,0,0,0,48,0,0,0,0,16,0,0,0,0,0,0,0,0,0,23, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,47,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23, +23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23, +25,0,0,0,0,0,0,0,0,0,0,29,0,0,15,0,0,0,29,0,0,0,0,0,0,0,0,0,0,23, +25,0,0,0,0,0,0,0,0,0,0,0,19,20,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,23, +25,0,0,0,0,0,0,0,0,0,0,0,21,22,0,0,23,0,0,0,0,0,0,0,0,0,0,21,22,24, +25,19,20,0,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23, +23,21,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,23, +23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23, +24,25,23,25,23,25,25,25,23,25,24,24,23,23,0,0,23,23,23,23,23,25,23,23,24,23,23,23,23,23 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/shop.json b/Kimi-K3/public/assets/maps/shop.json new file mode 100644 index 0000000..ca7fee9 --- /dev/null +++ b/Kimi-K3/public/assets/maps/shop.json @@ -0,0 +1,207 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, + 49, 50, 49, 50, 49, 50, 64, 64, 64, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 64, 64, 64, 49, 50, 49, 50, 49, 50, + 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, + 50, 49, 50, 49, 50, 49, 50, 39, 50, 49, 50, 49, 50, 49, 50], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "data":[51, 51, 51, 52, 51, 51, 51, 52, 51, 51, 51, 52, 51, 51, 51, + 51, 0, 0, 61, 61, 61, 61, 61, 61, 61, 61, 61, 0, 78, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 60, 61, 61, 60, 61, 61, 60, 61, 61, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 79, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, + 51, 51, 51, 51, 51, 51, 51, 0, 51, 51, 51, 51, 51, 51, 51], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":104 + }, + { + "height":0, + "id":2, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"from_plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":120 + }, + { + "height":0, + "id":3, + "name":"door", + "opacity":1, + "point":true, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"from_shop" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":136 + }, + { + "height":0, + "id":4, + "name":"npc", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"mimi" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":40, + "y":88 + }, + { + "height":0, + "id":5, + "name":"npc", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"hedgehog" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":184, + "y":120 + }, + { + "height":0, + "id":6, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"shelf_count" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":40 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":7, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":15 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/shop.tmx b/Kimi-K3/public/assets/maps/shop.tmx new file mode 100644 index 0000000..d4acdbf --- /dev/null +++ b/Kimi-K3/public/assets/maps/shop.tmx @@ -0,0 +1,74 @@ + + + + + +49,50,49,50,49,50,49,50,49,50,49,50,49,50,49, +50,49,50,49,50,49,50,49,50,49,50,49,50,49,50, +49,50,49,50,49,50,49,50,49,50,49,50,49,50,49, +50,49,50,49,50,49,50,49,50,49,50,49,50,49,50, +49,50,49,50,49,50,49,50,49,50,49,50,49,50,49, +50,49,50,49,50,49,50,49,50,49,50,49,50,49,50, +49,50,49,50,49,50,64,64,64,50,49,50,49,50,49, +50,49,50,49,50,49,64,64,64,49,50,49,50,49,50, +49,50,49,50,49,50,49,50,49,50,49,50,49,50,49, +50,49,50,49,50,49,50,39,50,49,50,49,50,49,50 + + + + + + + +51,51,51,52,51,51,51,52,51,51,51,52,51,51,51, +51,0,0,61,61,61,61,61,61,61,61,61,0,78,51, +51,0,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,60,61,61,60,61,61,60,61,61,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,0,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,80,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,79,0,0,0,0,0,0,0,0,0,0,0,0,51, +51,51,51,51,51,51,51,0,51,51,51,51,51,51,51 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/tileset.tsx b/Kimi-K3/public/assets/maps/tileset.tsx new file mode 100644 index 0000000..d2ca605 --- /dev/null +++ b/Kimi-K3/public/assets/maps/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/Kimi-K3/public/assets/maps/well.json b/Kimi-K3/public/assets/maps/well.json new file mode 100644 index 0000000..59f86b8 --- /dev/null +++ b/Kimi-K3/public/assets/maps/well.json @@ -0,0 +1,169 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 4, 1, 1, + 1, 3, 1, 3, 3, 4, 1, 4, 1, 1, 103, 1, 1, 1, + 1, 3, 1, 1, 1, 4, 1, 1, 1, 1, 101, 1, 2, 1, + 1, 1, 1, 101, 1, 5, 5, 5, 5, 1, 1, 1, 1, 1, + 1, 1, 1, 100, 1, 5, 5, 5, 5, 1, 1, 4, 1, 1, + 1, 1, 1, 3, 1, 5, 5, 5, 5, 1, 2, 1, 1, 4, + 101, 1, 1, 1, 1, 5, 5, 5, 5, 2, 100, 1, 3, 1, + 1, 1, 1, 1, 1, 1, 5, 5, 4, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 4, 5, 5, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 4, 1, 5, 5, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":14, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, + 0, 21, 22, 0, 23, 0, 0, 0, 0, 23, 0, 21, 22, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 43, 44, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 25, 0, 45, 46, 0, 25, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, + 0, 21, 22, 0, 23, 0, 0, 0, 0, 23, 0, 21, 22, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":14, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":104, + "y":136 + }, + { + "height":0, + "id":2, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"from_plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":136 + }, + { + "height":0, + "id":3, + "name":"door", + "opacity":1, + "point":true, + "properties":[ + { + "name":"spawn", + "type":"string", + "value":"from_well" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":120, + "y":152 + }, + { + "height":0, + "id":4, + "name":"interact", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"ancient_well" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":104, + "y":104 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":5, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":14 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/well.tmx b/Kimi-K3/public/assets/maps/well.tmx new file mode 100644 index 0000000..617c041 --- /dev/null +++ b/Kimi-K3/public/assets/maps/well.tmx @@ -0,0 +1,62 @@ + + + + + +1,1,1,1,1,1,1,2,1,1,1,4,1,1, +1,3,1,3,3,4,1,4,1,1,103,1,1,1, +1,3,1,1,1,4,1,1,1,1,101,1,2,1, +1,1,1,101,1,5,5,5,5,1,1,1,1,1, +1,1,1,100,1,5,5,5,5,1,1,4,1,1, +1,1,1,3,1,5,5,5,5,1,2,1,1,4, +101,1,1,1,1,5,5,5,5,2,100,1,3,1, +1,1,1,1,1,1,5,5,4,1,1,1,1,1, +1,1,1,1,1,4,5,5,1,1,1,1,1,1, +1,1,1,1,4,1,5,5,1,1,1,1,1,1 + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,19,20,0,0,0,0,0,0,0,0,19,20,0, +0,21,22,0,23,0,0,0,0,23,0,21,22,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,43,44,0,0,0,0,0,0, +0,0,0,0,25,0,45,46,0,25,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,19,20,0,0,0,0,0,0,0,0,19,20,0, +0,21,22,0,23,0,0,0,0,23,0,21,22,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/maps/white.json b/Kimi-K3/public/assets/maps/white.json new file mode 100644 index 0000000..5567d5d --- /dev/null +++ b/Kimi-K3/public/assets/maps/white.json @@ -0,0 +1,115 @@ +{ "compressionlevel":-1, + "height":14, + "infinite":false, + "layers":[ + { + "data":[85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 85, + 85, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 85, + 85, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 85, + 85, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 85, + 85, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 85, + 85, 85, 85, 85, 84, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 84, 95, 84, 84, 95, 84, 84, 95, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 95, 84, 84, 95, 84, 84, 95, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 84, 84, 95, 84, 84, 95, 84, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 84, 95, 84, 84, 95, 84, 84, 95, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 95, 84, 84, 95, 84, 84, 95, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 84, 84, 95, 84, 84, 95, 84, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85], + "height":14, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[85, 85, 85, 96, 85, 86, 85, 85, 96, 85, 86, 85, 85, 96, 85, 86, 85, 85, 96, 85, + 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, + 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, + 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, + 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, + 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, + 85, 85, 85, 85, 0, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 0, 88, 89, 93, 0, 0, 0, 0, 94, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 0, 90, 91, 0, 0, 0, 0, 0, 94, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 0, 0, 0, 0, 0, 0, 92, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85], + "height":14, + "id":2, + "name":"obstacles", + "opacity":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":0, + "id":1, + "name":"spawn", + "opacity":1, + "point":true, + "properties":[ + { + "name":"id", + "type":"string", + "value":"default" + }], + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":280, + "y":56 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"..\/tileset.png", + "imageheight":128, + "imagewidth":256, + "margin":0, + "name":"candy", + "spacing":0, + "tilecount":128, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Kimi-K3/public/assets/maps/white.tmx b/Kimi-K3/public/assets/maps/white.tmx new file mode 100644 index 0000000..dc603b9 --- /dev/null +++ b/Kimi-K3/public/assets/maps/white.tmx @@ -0,0 +1,51 @@ + + + + + +85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85, +85,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,85, +85,84,84,84,95,84,84,84,95,84,84,84,95,84,84,84,95,84,84,85, +85,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,85, +85,84,84,84,95,84,84,84,95,84,84,84,95,84,84,84,95,84,84,85, +85,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,85, +85,85,85,85,84,84,85,85,85,85,85,85,85,85,85,85,85,85,85,85, +85,84,95,84,84,95,84,84,95,85,85,85,85,85,85,85,85,85,85,85, +85,95,84,84,95,84,84,95,84,85,85,85,85,85,85,85,85,85,85,85, +85,84,84,95,84,84,95,84,84,85,85,85,85,85,85,85,85,85,85,85, +85,84,95,84,84,95,84,84,95,85,85,85,85,85,85,85,85,85,85,85, +85,95,84,84,95,84,84,95,84,85,85,85,85,85,85,85,85,85,85,85, +85,84,84,95,84,84,95,84,84,85,85,85,85,85,85,85,85,85,85,85, +85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85 + + + + + + + +85,85,85,96,85,86,85,85,96,85,86,85,85,96,85,86,85,85,96,85, +85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85, +85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85, +85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85, +85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85, +85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85, +85,85,85,85,0,0,85,85,85,85,85,85,85,85,85,85,85,85,85,85, +85,0,0,0,0,0,0,0,0,85,85,85,85,85,85,85,85,85,85,85, +85,0,88,89,93,0,0,0,0,94,85,85,85,85,85,85,85,85,85,85, +85,0,90,91,0,0,0,0,0,94,85,85,85,85,85,85,85,85,85,85, +85,0,0,0,0,0,0,0,0,85,85,85,85,85,85,85,85,85,85,85, +85,0,0,0,0,0,0,0,0,85,85,85,85,85,85,85,85,85,85,85, +85,0,0,0,0,0,0,92,0,85,85,85,85,85,85,85,85,85,85,85, +85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85 + + + + + + + + + + + diff --git a/Kimi-K3/public/assets/portraits/afu.png b/Kimi-K3/public/assets/portraits/afu.png new file mode 100644 index 0000000..560df6d Binary files /dev/null and b/Kimi-K3/public/assets/portraits/afu.png differ diff --git a/Kimi-K3/public/assets/portraits/bird.png b/Kimi-K3/public/assets/portraits/bird.png new file mode 100644 index 0000000..53e6a6b Binary files /dev/null and b/Kimi-K3/public/assets/portraits/bird.png differ diff --git a/Kimi-K3/public/assets/portraits/dabao.png b/Kimi-K3/public/assets/portraits/dabao.png new file mode 100644 index 0000000..cd87dd1 Binary files /dev/null and b/Kimi-K3/public/assets/portraits/dabao.png differ diff --git a/Kimi-K3/public/assets/portraits/fuyisheng.png b/Kimi-K3/public/assets/portraits/fuyisheng.png new file mode 100644 index 0000000..35b1d1d Binary files /dev/null and b/Kimi-K3/public/assets/portraits/fuyisheng.png differ diff --git a/Kimi-K3/public/assets/portraits/guiyeye.png b/Kimi-K3/public/assets/portraits/guiyeye.png new file mode 100644 index 0000000..285e05a Binary files /dev/null and b/Kimi-K3/public/assets/portraits/guiyeye.png differ diff --git a/Kimi-K3/public/assets/portraits/mimi.png b/Kimi-K3/public/assets/portraits/mimi.png new file mode 100644 index 0000000..c88df2c Binary files /dev/null and b/Kimi-K3/public/assets/portraits/mimi.png differ diff --git a/Kimi-K3/public/assets/portraits/player_real.png b/Kimi-K3/public/assets/portraits/player_real.png new file mode 100644 index 0000000..76cd226 Binary files /dev/null and b/Kimi-K3/public/assets/portraits/player_real.png differ diff --git a/Kimi-K3/public/assets/portraits/player_smile.png b/Kimi-K3/public/assets/portraits/player_smile.png new file mode 100644 index 0000000..11901a7 Binary files /dev/null and b/Kimi-K3/public/assets/portraits/player_smile.png differ diff --git a/Kimi-K3/public/assets/portraits/yuanyuan.png b/Kimi-K3/public/assets/portraits/yuanyuan.png new file mode 100644 index 0000000..8e9ab0b Binary files /dev/null and b/Kimi-K3/public/assets/portraits/yuanyuan.png differ diff --git a/Kimi-K3/public/assets/sprites/extra_chick.png b/Kimi-K3/public/assets/sprites/extra_chick.png new file mode 100644 index 0000000..edc120b Binary files /dev/null and b/Kimi-K3/public/assets/sprites/extra_chick.png differ diff --git a/Kimi-K3/public/assets/sprites/extra_fox.png b/Kimi-K3/public/assets/sprites/extra_fox.png new file mode 100644 index 0000000..669bfcb Binary files /dev/null and b/Kimi-K3/public/assets/sprites/extra_fox.png differ diff --git a/Kimi-K3/public/assets/sprites/extra_hedgehog.png b/Kimi-K3/public/assets/sprites/extra_hedgehog.png new file mode 100644 index 0000000..43b7373 Binary files /dev/null and b/Kimi-K3/public/assets/sprites/extra_hedgehog.png differ diff --git a/Kimi-K3/public/assets/sprites/extra_pig.png b/Kimi-K3/public/assets/sprites/extra_pig.png new file mode 100644 index 0000000..bdda64f Binary files /dev/null and b/Kimi-K3/public/assets/sprites/extra_pig.png differ diff --git a/Kimi-K3/public/assets/sprites/extra_squirrel.png b/Kimi-K3/public/assets/sprites/extra_squirrel.png new file mode 100644 index 0000000..432e667 Binary files /dev/null and b/Kimi-K3/public/assets/sprites/extra_squirrel.png differ diff --git a/Kimi-K3/public/assets/sprites/npc_afu.png b/Kimi-K3/public/assets/sprites/npc_afu.png new file mode 100644 index 0000000..a43f7ed Binary files /dev/null and b/Kimi-K3/public/assets/sprites/npc_afu.png differ diff --git a/Kimi-K3/public/assets/sprites/npc_bird.png b/Kimi-K3/public/assets/sprites/npc_bird.png new file mode 100644 index 0000000..6b0e37b Binary files /dev/null and b/Kimi-K3/public/assets/sprites/npc_bird.png differ diff --git a/Kimi-K3/public/assets/sprites/npc_dabao.png b/Kimi-K3/public/assets/sprites/npc_dabao.png new file mode 100644 index 0000000..2dbe8aa Binary files /dev/null and b/Kimi-K3/public/assets/sprites/npc_dabao.png differ diff --git a/Kimi-K3/public/assets/sprites/npc_fuyisheng.png b/Kimi-K3/public/assets/sprites/npc_fuyisheng.png new file mode 100644 index 0000000..5d90384 Binary files /dev/null and b/Kimi-K3/public/assets/sprites/npc_fuyisheng.png differ diff --git a/Kimi-K3/public/assets/sprites/npc_guiyeye.png b/Kimi-K3/public/assets/sprites/npc_guiyeye.png new file mode 100644 index 0000000..2bb5897 Binary files /dev/null and b/Kimi-K3/public/assets/sprites/npc_guiyeye.png differ diff --git a/Kimi-K3/public/assets/sprites/npc_mimi.png b/Kimi-K3/public/assets/sprites/npc_mimi.png new file mode 100644 index 0000000..050f59e Binary files /dev/null and b/Kimi-K3/public/assets/sprites/npc_mimi.png differ diff --git a/Kimi-K3/public/assets/sprites/npc_yuanyuan.png b/Kimi-K3/public/assets/sprites/npc_yuanyuan.png new file mode 100644 index 0000000..f70d73d Binary files /dev/null and b/Kimi-K3/public/assets/sprites/npc_yuanyuan.png differ diff --git a/Kimi-K3/public/assets/sprites/player.png b/Kimi-K3/public/assets/sprites/player.png new file mode 100644 index 0000000..2244628 Binary files /dev/null and b/Kimi-K3/public/assets/sprites/player.png differ diff --git a/Kimi-K3/public/assets/tileset.png b/Kimi-K3/public/assets/tileset.png new file mode 100644 index 0000000..f074d5b Binary files /dev/null and b/Kimi-K3/public/assets/tileset.png differ diff --git a/Kimi-K3/src/actors/Npc.ts b/Kimi-K3/src/actors/Npc.ts new file mode 100644 index 0000000..129e907 --- /dev/null +++ b/Kimi-K3/src/actors/Npc.ts @@ -0,0 +1,121 @@ +// NPC 与群演行为组件(PRD §4 / §6.4)。 +// 表情档:0=smile 1=flat 2=off,按阶段 + 个体阈值偏移切换; +// S3 起静止 NPC 面朝玩家(flipX);S4:off 档 0.6 次/分钟闪 0.1 秒、群演随机 1 只停摆; +// 双子鸟张嘴动画循环(S4 照张);阶段切换时鸟动画卡帧一下。 + +import Phaser from 'phaser'; +import { NPC_DEFS, stageOf, type NpcDef, type NpcId, type Stage } from '../types'; +import { director } from '../core/CorruptionDirector'; + +const FACE_FRAME: Record = { 0: 0, 1: 0, 2: 0, 3: 1, 4: 1 }; // 阶段→表情帧(S4 基础 flat,off 靠闪) + +export class NpcActor { + readonly def: NpcDef; + readonly sprite: Phaser.Physics.Arcade.Sprite; + readonly isExtra: boolean; + + private scene: Phaser.Scene; + /** 群演巡走范围 */ + private patrolLeft = 0; + private patrolRight = 0; + private patrolDir = 1; + private patrolSpeed = 24; + /** S4 停摆标记(每场景 1 只) */ + stalled = false; + /** S4 off 档闪帧计时 */ + private offFlashMs = 0; + /** 被「悲伤行为」惊动,转头看玩家计时 */ + private glanceMs = 0; + /** T14 全村注视 */ + stareAtPlayer = false; + + constructor(scene: Phaser.Scene, id: NpcId, x: number, y: number) { + this.scene = scene; + this.def = NPC_DEFS[id]; + this.isExtra = this.def.isExtra; + this.sprite = scene.physics.add.sprite(x, y, this.def.spriteKey, 0); + this.sprite.setOrigin(0.5, 1); + this.sprite.setImmovable(true); + (this.sprite.body as Phaser.Physics.Arcade.Body).setAllowGravity(false); + this.sprite.setSize(12, 8).setOffset(2, 16); + + if (this.isExtra) { + this.patrolLeft = x - 32; + this.patrolRight = x + 32; + const anim = `${this.def.spriteKey}-walk`; + if (scene.anims.exists(anim)) this.sprite.play(anim); + } else if (id === 'bird') { + if (scene.anims.exists('bird-sing')) this.sprite.play('bird-sing'); + } + } + + /** 悲伤行为触发:转头看玩家一次 */ + glanceAt(playerX: number): void { + this.glanceMs = 1200; + this.sprite.setFlipX(playerX < this.sprite.x); + } + + update(deltaMs: number, player: Phaser.Physics.Arcade.Sprite): void { + const stage = director.stage; + this.sprite.setDepth(this.sprite.y); + + if (this.isExtra) { + this.updateExtra(deltaMs, player, stage); + } else { + this.updateNamed(deltaMs, player, stage); + } + } + + private updateNamed(deltaMs: number, player: Phaser.Physics.Arcade.Sprite, stage: Stage): void { + // 表情档(芙医生/龟爷爷/双子鸟不参与侵蚀) + if (this.def.expressive) { + const effStage = stageOf(director.happiness - this.def.offset); + let frame = FACE_FRAME[effStage]; + if (this.offFlashMs > 0) { + frame = 2; // off 档闪现 + this.offFlashMs -= deltaMs; + } else if (effStage === 4) { + // S4:0.6 次/分钟闪 0.1 秒 → 每秒约 1% 概率触发 + if (Math.random() < (0.6 / 60) * (deltaMs / 1000)) this.offFlashMs = 100; + } + this.sprite.setFrame(frame); + } + + // S3 起静止 NPC 面朝玩家;被惊动时也面朝玩家 + const facePlayer = stage >= 3 || this.glanceMs > 0 || this.stareAtPlayer; + if (facePlayer && this.def.id !== 'bird') { + this.sprite.setFlipX(player.x < this.sprite.x); + } + if (this.glanceMs > 0) this.glanceMs -= deltaMs; + } + + private updateExtra(deltaMs: number, player: Phaser.Physics.Arcade.Sprite, stage: Stage): void { + if (this.stalled) { + // S4 停摆:原地面朝玩家 + this.sprite.setVelocity(0); + this.sprite.setFrame(0); + this.sprite.setFlipX(player.x < this.sprite.x); + return; + } + const x = this.sprite.x; + if (x <= this.patrolLeft) this.patrolDir = 1; + if (x >= this.patrolRight) this.patrolDir = -1; + this.sprite.setVelocityX(this.patrolDir * this.patrolSpeed); + this.sprite.setFlipX(this.patrolDir < 0); + if (stage >= 3) { + // S3 起也会偶尔看向玩家?契约只要求 S4 停摆,巡走继续 + } + void deltaMs; + } + + destroy(): void { + this.sprite.destroy(); + } +} + +/** 群演停摆选择:S4 起随机 1 只停止循环 */ +export function pickStallExtra(actors: NpcActor[]): NpcActor | null { + const extras = actors.filter((a) => a.isExtra); + if (extras.length === 0) return null; + return extras[Math.floor(Math.random() * extras.length)]; +} diff --git a/Kimi-K3/src/audio/AudioEngine.ts b/Kimi-K3/src/audio/AudioEngine.ts new file mode 100644 index 0000000..a0664c3 --- /dev/null +++ b/Kimi-K3/src/audio/AudioEngine.ts @@ -0,0 +1,225 @@ +// AudioEngine:WebAudio 程序合成(PRD §6.6),无任何外部音频文件。 +// 8 小节 8-bit 循环:方波主旋律 + 三角波低音 + 鸟鸣声部。 +// playbackRate 与 BiquadFilter 低通由 CorruptionDirector(§6.4)驱动;S4 鸟鸣静音。 +// 音效 4 个:对话哔、完成叮、黄昏钟、E2 心电滴。 +// 首次用户交互后才启动 AudioContext。 + +import { director } from '../core/CorruptionDirector'; +import type { Stage, StageParams } from '../types'; + +// 旋律:C - G - Am - F / C - G - F - G,4/4,100 BPM,每拍一个四分音符 +const BPM = 100; +const BEAT = 60 / BPM; // 0.6s +const BARS = 8; + +const LEAD: number[][] = [ + [76, 79, 84, 79], // C: E5 G5 C6 G5 + [74, 79, 83, 79], // G: D5 G5 B5 G5 + [72, 76, 81, 76], // Am: C5 E5 A5 E5 + [72, 77, 81, 77], // F: C5 F5 A5 F5 + [79, 76, 72, 76], // C: G5 E5 C5 E5 + [74, 71, 67, 71], // G: D5 B4 G4 B4 + [69, 72, 77, 72], // F: A4 C5 F5 C5 + [67, 71, 74, 67], // G: G4 B4 D5 G4 +]; +const BASS: number[] = [48, 43, 45, 41, 48, 43, 41, 43]; // C3 G2 A2 F2 ... +// 鸟鸣:第 2/4/6/8 小节第 4 拍 +const BIRD_BARS = [1, 3, 5, 7]; + +function midiToFreq(n: number): number { + return 440 * Math.pow(2, (n - 69) / 12); +} + +export class AudioEngine { + private static _instance: AudioEngine | null = null; + static get instance(): AudioEngine { + if (!this._instance) this._instance = new AudioEngine(); + return this._instance; + } + + private ctx: AudioContext | null = null; + private master!: GainNode; + private lp!: BiquadFilterNode; + private musicBus!: GainNode; + private birdBus!: GainNode; + private sfxBus!: GainNode; + + private rate = 1; + private started = false; + private nextNoteTime = 0; + private step = 0; // 0..31(8 小节 × 4 拍) + private schedulerId = 0; + + private constructor() { + // 阶段参数 → 音频参数 + director.on('stage', (_s: Stage, p: StageParams) => { + this.setParams(p); + }); + } + + /** 首次用户交互后调用 */ + start(): void { + if (this.started) return; + this.started = true; + const AC = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; + this.ctx = new AC(); + this.master = this.ctx.createGain(); + this.master.gain.value = 0.35; + this.master.connect(this.ctx.destination); + + this.lp = this.ctx.createBiquadFilter(); + this.lp.type = 'lowpass'; + this.lp.frequency.value = 22050; + this.lp.connect(this.master); + + this.musicBus = this.ctx.createGain(); + this.musicBus.gain.value = 0.9; + this.musicBus.connect(this.lp); + + this.birdBus = this.ctx.createGain(); + this.birdBus.gain.value = 0.5; + this.birdBus.connect(this.lp); + + this.sfxBus = this.ctx.createGain(); + this.sfxBus.gain.value = 1; + this.sfxBus.connect(this.master); // 音效不走低通 + + this.nextNoteTime = this.ctx.currentTime + 0.1; + this.schedulerId = window.setInterval(() => this.scheduler(), 25); + this.setParams(director.params); + } + + private setParams(p: StageParams): void { + this.rate = p.playbackRate; + if (!this.ctx) return; + this.lp.frequency.setTargetAtTime(p.lowpass, this.ctx.currentTime, 0.2); + this.birdBus.gain.setTargetAtTime(p.birdMuted ? 0 : 0.5, this.ctx.currentTime, 0.2); + } + + private scheduler(): void { + if (!this.ctx) return; + const lookAhead = 0.15; + while (this.nextNoteTime < this.ctx.currentTime + lookAhead) { + this.scheduleStep(this.step, this.nextNoteTime); + const beatDur = BEAT / this.rate; + this.nextNoteTime += beatDur; + this.step = (this.step + 1) % (BARS * 4); + } + } + + private scheduleStep(step: number, time: number): void { + if (!this.ctx) return; + const bar = Math.floor(step / 4); + const beat = step % 4; + const beatDur = BEAT / this.rate; + + // 方波主旋律 + const lead = LEAD[bar][beat]; + this.tone(midiToFreq(lead), time, beatDur * 0.9, 'square', 0.10, this.musicBus, 0.005, 0.06); + + // 三角波低音(每拍重复根音) + const bass = BASS[bar]; + this.tone(midiToFreq(bass), time, beatDur * 0.95, 'triangle', 0.16, this.musicBus, 0.01, 0.1); + + // 鸟鸣声部 + if (BIRD_BARS.includes(bar) && beat === 3) { + this.chirp(time); + this.chirp(time + beatDur * 0.35); + } + } + + private tone( + freq: number, time: number, dur: number, type: OscillatorType, + vol: number, bus: GainNode, attack = 0.005, release = 0.08, + ): void { + if (!this.ctx) return; + const osc = this.ctx.createOscillator(); + const g = this.ctx.createGain(); + osc.type = type; + osc.frequency.value = freq; + g.gain.setValueAtTime(0.0001, time); + g.gain.exponentialRampToValueAtTime(vol, time + attack); + g.gain.setValueAtTime(vol, time + Math.max(attack, dur - release)); + g.gain.exponentialRampToValueAtTime(0.0001, time + dur); + osc.connect(g).connect(bus); + osc.start(time); + osc.stop(time + dur + 0.02); + } + + /** 鸟鸣:高频正弦快速双音 */ + private chirp(time: number): void { + if (!this.ctx) return; + const base = 2600 + Math.random() * 600; + const osc = this.ctx.createOscillator(); + const g = this.ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(base, time); + osc.frequency.exponentialRampToValueAtTime(base * 1.5, time + 0.05); + g.gain.setValueAtTime(0.0001, time); + g.gain.exponentialRampToValueAtTime(0.08, time + 0.01); + g.gain.exponentialRampToValueAtTime(0.0001, time + 0.12); + osc.connect(g).connect(this.birdBus); + osc.start(time); + osc.stop(time + 0.15); + } + + // ---------- 音效(4 个,全合成) ---------- + + /** 对话哔哔声 */ + beep(): void { + if (!this.ctx) return; + this.tone(660, this.ctx.currentTime, 0.05, 'square', 0.10, this.sfxBus, 0.002, 0.03); + } + + /** 任务完成叮声 */ + ding(): void { + if (!this.ctx) return; + const t = this.ctx.currentTime; + this.tone(880, t, 0.12, 'sine', 0.16, this.sfxBus, 0.002, 0.1); + this.tone(1320, t + 0.1, 0.22, 'sine', 0.16, this.sfxBus, 0.002, 0.18); + } + + /** 黄昏钟声 */ + bell(): void { + if (!this.ctx) return; + const t = this.ctx.currentTime; + for (let i = 0; i < 2; i++) { + this.tone(220, t + i * 1.2, 1.4, 'triangle', 0.22, this.sfxBus, 0.01, 1.2); + this.tone(110, t + i * 1.2, 1.6, 'sine', 0.14, this.sfxBus, 0.01, 1.4); + } + } + + /** E2 心电监护滴声(单次) */ + ecg(): void { + if (!this.ctx) return; + const t = this.ctx.currentTime; + this.tone(1000, t, 0.08, 'sine', 0.18, this.sfxBus, 0.002, 0.06); + } + + /** 井底回声:极轻的监护仪滴声(C7) */ + ecgSoft(): void { + if (!this.ctx) return; + const t = this.ctx.currentTime; + this.tone(1000, t, 0.08, 'sine', 0.05, this.sfxBus, 0.002, 0.06); + } + + // ---------- T14 第 40–60 秒:所有声音渐隐至零 ---------- + + fadeAllToZero(durationMs: number): void { + if (!this.ctx) return; + const t = this.ctx.currentTime; + this.master.gain.cancelScheduledValues(t); + this.master.gain.setValueAtTime(this.master.gain.value, t); + this.master.gain.linearRampToValueAtTime(0.0001, t + durationMs / 1000); + } + + restoreMaster(durationMs = 2000): void { + if (!this.ctx) return; + const t = this.ctx.currentTime; + this.master.gain.cancelScheduledValues(t); + this.master.gain.setValueAtTime(this.master.gain.value, t); + this.master.gain.linearRampToValueAtTime(0.35, t + durationMs / 1000); + } +} + +export const audio = AudioEngine.instance; diff --git a/Kimi-K3/src/core/CorruptionDirector.ts b/Kimi-K3/src/core/CorruptionDirector.ts new file mode 100644 index 0000000..a61e58a --- /dev/null +++ b/Kimi-K3/src/core/CorruptionDirector.ts @@ -0,0 +1,418 @@ +// CorruptionDirector:全部状态唯一真源(PRD §6 / CONTRACT §4)。 +// 普通 TS 单例 + Phaser.Events.EventEmitter。 +// 事件:happiness / awareness / stage / day / clue / task / celebration / ending(契约), +// 增补:negativeFlash / firstFull / npcGlance / dusk / prompt / dialogue / rain。 + +import Phaser from 'phaser'; +import { + STAGE_PARAMS, stageOf, type Stage, type StageParams, + type SadnessKind, type SmallInteractionKind, type ChoiceKind, type EndingId, + type DialoguePayload, type NpcId, +} from '../types'; +import { TASKS, tasksOfDay } from '../data/tasks'; +import { CLUE_AWARENESS } from '../data/clues'; + +const LOW_HAPPY_THRESHOLD = 30; +const LOW_HAPPY_MS = 2 * 60 * 1000; // <30 持续 2 分钟 → 欢乐庆典 +const OVERFLOW_TRIGGER = 15; // 溢出池 15 点 → 负片闪帧 +const CELEBRATION_RESTORE = 50; +const DAY_LENGTH_MS = 5 * 60 * 1000; // 游戏内计时:5 分钟未到也会黄昏(7 天 ≤35 分钟,§14.16) + +export class CorruptionDirector extends Phaser.Events.EventEmitter { + private static _instance: CorruptionDirector | null = null; + static get instance(): CorruptionDirector { + if (!this._instance) this._instance = new CorruptionDirector(); + return this._instance; + } + + // ---------- 状态 ---------- + playerName = '小满'; + startTime = Date.now(); + day = 1; + happiness = 30; + awareness = 0; + readonly clues = new Set(); + readonly tasksDone = new Set(); + readonly refusedTasks = new Set(); + escapedCelebrations = 0; + /** 最近一次对话关闭的时间戳(ms):MapScene 用它防止「关对话的同一次 E 按下」立刻重开交互 */ + dialogueClosedAt = 0; + + /** 任务进度计数(如 T1 已送几份面包),每日清空 */ + taskProgress: Record = {}; + /** 集合型进度(如 T3 已问好 npc 列表),每日清空 */ + taskSetProgress: Record> = {}; + + rainActive = false; + celebrationActive = false; + /** 对话/梦境/结局进行中:抑制发呆判定与移动 */ + dialogueActive = false; + /** 场景切换过场中(床/门淡出→新场景):抑制庆典触发,防止庆典对话被切场景覆盖后卡死 */ + sceneTransition = false; + /** T14 进行中 */ + t14Active = false; + + /** E4 监视:D5-7 期间幸福是否曾 >10 */ + e4HappyOk = true; + + private overflowPool = 0; + private lowHappyMs = 0; + private negFlashTimer = 60_000; + private firstFullDone = false; + private dayElapsedMs = 0; + + /** 每日重置的计数 */ + private talkCounts: Record = {}; // 纠缠判定:对同一 NPC 一天内对话次数 + private smallCounts: Record = {}; // 小互动每类每日上限 3 + private stareDone: Record = {}; // 注视古井/雾墙各每日 1 次 + + /** 对话 Promise 解析器(UIScene 完成后回调) */ + private dialogueResolver: ((choice: number | null) => void) | null = null; + + private constructor() { + super(); + } + + // ---------- 派生 ---------- + get stage(): Stage { + return stageOf(this.happiness); + } + + get params(): StageParams { + return STAGE_PARAMS[this.stage]; + } + + /** 真实游玩时长(ms) */ + get playTimeMs(): number { + return Date.now() - this.startTime; + } + + get playTimeFormatted(): string { + const totalSec = Math.floor(this.playTimeMs / 1000); + const m = Math.floor(totalSec / 60); + const s = totalSec % 60; + return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + } + + /** 当天任务是否全部完成 */ + get dailyTasksAllDone(): boolean { + return tasksOfDay(this.day).every((t) => this.tasksDone.has(t.id)); + } + + /** 是否可以睡觉(任务全部了结——完成或拒绝,或计时已到黄昏) */ + get canSleep(): boolean { + if (this.day >= 7) return false; + return this.dailySettled || this.dayElapsedMs >= DAY_LENGTH_MS; + } + + // ---------- 新游戏 ---------- + newGame(name: string): void { + this.playerName = name.trim() || '小满'; + this.startTime = Date.now(); + this.day = 1; + this.happiness = 30; + this.awareness = 0; + this.clues.clear(); + this.tasksDone.clear(); + this.refusedTasks.clear(); + this.escapedCelebrations = 0; + this.taskProgress = {}; + this.taskSetProgress = {}; + this.rainActive = false; + this.celebrationActive = false; + this.dialogueActive = false; + this.sceneTransition = false; + this.t14Active = false; + this.e4HappyOk = true; + this.overflowPool = 0; + this.lowHappyMs = 0; + this.firstFullDone = false; + this.dayElapsedMs = 0; + this.resetDaily(); + this.emit('day', this.day); + this.emit('happiness', this.happiness); + this.emit('awareness', this.awareness); + this.emit('stage', this.stage, this.params); + } + + private resetDaily(): void { + this.talkCounts = {}; + this.smallCounts = {}; + this.stareDone = {}; + this.taskProgress = {}; + this.taskSetProgress = {}; + } + + // ---------- 幸福值 ---------- + addHappiness(delta: number): void { + if (delta === 0) return; + const prevStage = this.stage; + + if (delta < 0 && this.happiness >= 100) { + // 满格态:小额扣分不再生效,累积进隐藏溢出池 + this.overflowPool += -delta; + if (this.overflowPool >= OVERFLOW_TRIGGER) { + this.overflowPool -= OVERFLOW_TRIGGER; + this.emit('negativeFlash'); + } + return; + } + + this.happiness = Math.max(0, Math.min(100, this.happiness + delta)); + this.emit('happiness', this.happiness); + + if (this.day >= 5 && this.happiness > 10) this.e4HappyOk = false; + + if (this.happiness >= 100 && !this.firstFullDone) { + this.firstFullDone = true; + this.emit('firstFull'); + } + if (this.stage !== prevStage) { + this.emit('stage', this.stage, this.params); + } + } + + setHappiness(v: number): void { + this.happiness = Math.max(0, Math.min(100, Math.round(v))); + this.emit('happiness', this.happiness); + if (this.day >= 5 && this.happiness > 10) this.e4HappyOk = false; + this.emit('stage', this.stage, this.params); // 始终广播,保证 debug 立即生效 + } + + // ---------- 觉知值(只升不降) ---------- + addAwareness(delta: number): void { + if (delta <= 0) return; + this.awareness = Math.max(0, Math.min(100, this.awareness + delta)); + this.emit('awareness', this.awareness); + } + + setAwareness(v: number): void { + this.awareness = Math.max(0, Math.min(100, Math.round(v))); + this.emit('awareness', this.awareness); + } + + // ---------- 悲伤行为(§6.3:幸福 −5、觉知 +3、最近 NPC 转头看一次) ---------- + recordSadness(kind: SadnessKind): void { + this.addHappiness(-5); + this.addAwareness(3); + this.emit('npcGlance', kind); + this.emit('sadness', kind); + } + + // ---------- 小互动(浇花/喂食/摸头 +1,每类每日上限 3 次) ---------- + smallInteraction(kind: SmallInteractionKind): boolean { + const n = this.smallCounts[kind] ?? 0; + if (n >= 3) return false; + this.smallCounts[kind] = n + 1; + this.addHappiness(1); + return true; + } + + // ---------- 对话选项 ---------- + applyChoice(kind: ChoiceKind): void { + if (kind === 'gentle') { + this.addHappiness(2); + } else if (kind === 'hesitant') { + this.addAwareness(2); + } else if (kind === 'deny') { + this.addAwareness(2); + this.recordSadness('deny'); // 否定选项同时算悲伤行为 + } + // accept/refuse 由任务逻辑处理 + } + + // ---------- NPC 对话登记(纠缠:同一 NPC 一天内 ≥5 次 → 悲伤行为) ---------- + registerTalk(npcId: NpcId): void { + if (npcId === 'bird') return; + const n = (this.talkCounts[npcId] ?? 0) + 1; + this.talkCounts[npcId] = n; + if (n >= 5) { + this.recordSadness('cling'); + } + } + + // ---------- 注视古井/雾墙(各每日 1 次,+3 觉知) ---------- + registerStare(which: 'well' | 'fog'): boolean { + if (this.stareDone[which]) return false; + this.stareDone[which] = true; + this.addAwareness(3); + return true; + } + + // ---------- 线索(每处 +8) ---------- + addClue(id: string): boolean { + if (this.clues.has(id)) return false; + this.clues.add(id); + this.addAwareness(CLUE_AWARENESS); + this.emit('clue', id); + return true; + } + + giveAllClues(): void { + ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'].forEach((id) => this.addClue(id)); + } + + // ---------- 任务 ---------- + completeTask(id: string): void { + if (this.tasksDone.has(id)) return; + this.tasksDone.add(id); + const def = TASKS[id]; + if (def && def.reward > 0) this.addHappiness(def.reward); + this.emit('task', id); + if (this.dailyTasksAllDone && this.day < 7) { + this.emit('dusk'); // 黄昏钟声 + } + } + + refuseTask(id: string): void { + if (this.tasksDone.has(id) || this.refusedTasks.has(id)) return; + this.refusedTasks.add(id); + this.recordSadness('refuse'); // 拒绝被记录 = 悲伤行为 + this.emit('task', `refused:${id}`); + // 拒绝也视为该任务「了结」,检查黄昏 + const allSettled = tasksOfDay(this.day).every( + (t) => this.tasksDone.has(t.id) || this.refusedTasks.has(t.id), + ); + if (allSettled && this.day < 7) this.emit('dusk'); + } + + isTaskActive(id: string): boolean { + const def = TASKS[id]; + return !!def && def.day === this.day && !this.tasksDone.has(id) && !this.refusedTasks.has(id); + } + + /** 当日任务是否全部了结(完成或拒绝) */ + get dailySettled(): boolean { + return tasksOfDay(this.day).every( + (t) => this.tasksDone.has(t.id) || this.refusedTasks.has(t.id), + ); + } + + // ---------- 日期 ---------- + nextDay(): void { + if (this.day >= 7) return; + this.day += 1; + this.dayElapsedMs = 0; + this.rainActive = false; + this.lowHappyMs = 0; + // 新的一天不可能从庆典/睡觉过场中开始(防庆典对话被覆盖后卡死) + this.celebrationActive = false; + this.sceneTransition = false; + this.resetDaily(); + this.emit('day', this.day); + } + + setDay(d: number): void { + this.day = Math.max(1, Math.min(7, Math.round(d))); + this.dayElapsedMs = 0; + this.rainActive = false; + this.resetDaily(); + this.emit('day', this.day); + } + + // ---------- 欢乐庆典(<30 持续 2 分钟 → 拉回 50;可逃脱) ---------- + private startCelebration(): void { + if (this.celebrationActive || this.dialogueActive) return; + this.celebrationActive = true; + this.lowHappyMs = 0; + this.emit('celebration', 'start'); + } + + completeCelebration(): void { + if (!this.celebrationActive) return; + this.celebrationActive = false; + const prevStage = this.stage; + this.happiness = CELEBRATION_RESTORE; + this.emit('happiness', this.happiness); + if (this.day >= 5) this.e4HappyOk = false; + if (this.stage !== prevStage) this.emit('stage', this.stage, this.params); + this.emit('celebration', 'end'); + } + + escapeCelebration(): void { + if (!this.celebrationActive) return; + this.celebrationActive = false; + this.escapedCelebrations += 1; + this.emit('celebration', 'escape'); + } + + // ---------- 雨 ---------- + setRain(active: boolean): void { + if (this.rainActive === active) return; + this.rainActive = active; + this.emit('rain', active); + } + + // ---------- 每帧计时(由 MapScene.update 驱动) ---------- + tick(deltaMs: number): void { + this.dayElapsedMs += deltaMs; + + // E4 监视:D5-7 幸福曾 >10 即失格(含被动状态,不靠加减分触发) + if (this.day >= 5 && this.happiness > 10) this.e4HappyOk = false; + + // 连续低幸福计时(真实时间) + if (this.happiness < LOW_HAPPY_THRESHOLD && !this.celebrationActive && !this.dialogueActive && !this.sceneTransition) { + this.lowHappyMs += deltaMs; + if (this.lowHappyMs >= LOW_HAPPY_MS) { + this.startCelebration(); + } + } else if (this.happiness >= LOW_HAPPY_THRESHOLD) { + this.lowHappyMs = 0; + } + + // S4 随机单帧负片(0.4 次/分钟 → 平均间隔 150 秒) + if (this.stage === 4) { + this.negFlashTimer -= deltaMs; + if (this.negFlashTimer <= 0) { + this.emit('negativeFlash'); + this.negFlashTimer = 100_000 + Math.random() * 100_000; + } + } + } + + // ---------- 结局判定(§9 优先级:E4 > E3 > E2 > E1) ---------- + evaluateEnding(atFogWall: boolean): EndingId { + if (this.e4HappyOk && this.happiness <= 10 && this.day >= 5 && this.escapedCelebrations >= 3) return 'E4'; + if (atFogWall && this.awareness >= 70 && this.clues.size >= 6) return 'E3'; + if (this.awareness >= 30) return 'E2'; + if (this.happiness >= 90) return 'E1'; + // 觉知 ≤29 但幸福 <90 的边界:按最接近的「回收」处理 + return 'E2'; + } + + startEnding(id: EndingId): void { + this.emit('ending', id); + } + + // ---------- 对话总线(MapScene → UIScene,Promise 化) ---------- + showDialogue(payload: DialoguePayload): Promise { + // 重入保护:上一个对话的 resolver 若仍悬挂(被新对话覆盖), + // 必须先将其解析,否则 await 它的一方(如梦境过场)会永久卡住 + if (this.dialogueResolver) this.resolveDialogue(null); + this.dialogueActive = true; + return new Promise((resolve) => { + this.dialogueResolver = resolve; + this.emit('dialogue', payload); + }); + } + + /** UIScene 对话结束时调用 */ + resolveDialogue(choice: number | null): void { + this.dialogueActive = false; + this.dialogueClosedAt = Date.now(); + const r = this.dialogueResolver; + this.dialogueResolver = null; + if (r) r(choice); + } + + /** debug 用:强制清空进行中的一次性状态(对话/庆典/T14),避免传送后卡死 */ + forceIdle(): void { + this.celebrationActive = false; + this.t14Active = false; + this.rainActive = false; + this.sceneTransition = false; + this.resolveDialogue(null); + } +} + +export const director = CorruptionDirector.instance; diff --git a/Kimi-K3/src/core/TextCorruptor.ts b/Kimi-K3/src/core/TextCorruptor.ts new file mode 100644 index 0000000..68f8e59 --- /dev/null +++ b/Kimi-K3/src/core/TextCorruptor.ts @@ -0,0 +1,94 @@ +// PRD §6.5 文本腐蚀:同音错字对照表(全 10 组,逐字实现)。 +// 腐蚀只在对话渲染层实施(原文案保持干净),优先命中咪咪的台词。 + +import type { Stage } from '../types'; + +/** 同音错字对照表:幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / + * 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远 */ +export const HOMOPHONES: [string, string][] = [ + ['幸福', '辛福'], + ['开心', '开欣'], + ['美好', '美妤'], + ['大家', '大伽'], + ['微笑', '微效'], + ['明天', '名天'], + ['甜', '恬'], + ['阳光', '央光'], + ['朋友', '棚友'], + ['永远', '泳远'], +]; + +const END_PUNCT = ['!', '?', '。', '~']; + +/** 句尾标点重复一次(S2 偶发) */ +function repeatPunct(text: string): string { + for (const p of END_PUNCT) { + if (text.endsWith(p)) return text + p; + } + return text + '。'; +} + +/** 把命中的一组同音词替换一次;返回是否命中 */ +function applyOneHomophone(text: string, used: Set): { out: string; hit: boolean } { + const order = HOMOPHONES.map((_, i) => i); + // 随机顺序尝试,避免总是替换第一组 + for (let i = order.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [order[i], order[j]] = [order[j], order[i]]; + } + for (const idx of order) { + if (used.has(idx)) continue; + const [from, to] = HOMOPHONES[idx]; + if (text.includes(from)) { + used.add(idx); + return { out: text.replace(from, to), hit: true }; + } + } + return { out: text, hit: false }; +} + +/** + * 渲染层腐蚀。按 §6.4 矩阵密度: + * - S2:偶发标点重复(每 80 句 1 次) + * - S3:每 50 句 1 个同音错字 + * - S4:错字加密(每 10 句 1 次,最多 2 处)+ 咪咪句尾标点 ×3 + * 咪咪台词概率 ×3(S4 必中),即「优先命中」。 + */ +export function corruptText(text: string, speakerId: string | undefined, stage: Stage): string { + if (stage < 2 || !text) return text; + const mimi = speakerId === 'mimi'; + const roll = Math.random(); + let out = text; + + if (stage === 2) { + if (roll < (mimi ? 3 / 80 : 1 / 80)) out = repeatPunct(out); + return out; + } + + if (stage === 3) { + if (roll < (mimi ? 3 / 50 : 1 / 50)) { + out = applyOneHomophone(out, new Set()).out; + } + return out; + } + + // stage === 4 + const p = mimi ? 1 : 1 / 10; + if (roll < p) { + const used = new Set(); + const r1 = applyOneHomophone(out, used); + out = r1.out; + if (r1.hit && Math.random() < 0.6) { + out = applyOneHomophone(out, used).out; // 加密:可能第二处 + } + } + if (mimi) { + for (const pc of END_PUNCT) { + if (out.endsWith(pc)) { + out = out + pc + pc; // 句尾标点 ×3(原有 1 个 + 补 2 个) + break; + } + } + } + return out; +} diff --git a/Kimi-K3/src/core/gameRef.ts b/Kimi-K3/src/core/gameRef.ts new file mode 100644 index 0000000..ccdbf44 --- /dev/null +++ b/Kimi-K3/src/core/gameRef.ts @@ -0,0 +1,9 @@ +// 全局 game 实例引用(避免 main ↔ DebugPanel 循环依赖)。 + +import Phaser from 'phaser'; + +export const gameRef: { game: Phaser.Game | null } = { game: null }; + +export function setGame(g: Phaser.Game): void { + gameRef.game = g; +} diff --git a/Kimi-K3/src/core/tiled.ts b/Kimi-K3/src/core/tiled.ts new file mode 100644 index 0000000..a9b27e3 --- /dev/null +++ b/Kimi-K3/src/core/tiled.ts @@ -0,0 +1,23 @@ +// Tiled 导出 JSON 的读取辅助(CONTRACT §3)。 + +export interface TiledObj { + name: string; + x: number; + y: number; + properties?: { name: string; type: string; value: unknown }[]; +} + +export type Props = Record; + +export function propsOf(obj: { properties?: { name: string; value: unknown }[] }): Props { + const out: Props = {}; + if (Array.isArray(obj.properties)) { + for (const p of obj.properties) out[p.name] = p.value as string | number | boolean; + } + return out; +} + +export function strProp(p: Props, key: string, fallback = ''): string { + const v = p[key]; + return v === undefined ? fallback : String(v); +} diff --git a/Kimi-K3/src/data/announcements.ts b/Kimi-K3/src/data/announcements.ts new file mode 100644 index 0000000..ebf395b --- /dev/null +++ b/Kimi-K3/src/data/announcements.ts @@ -0,0 +1,15 @@ +// PRD §5 阿福每日公告(逐字实现,D4 与 D1 逐字相同,措辞逐日滑移) + +export const ANNOUNCEMENTS: Record = { + 1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + 2: '早上好呀!新的一天,新的幸福。祝你开心!', + 3: '早上好!今天要把幸福装得满满的哦。要开心!', + 4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // 与 D1 逐字相同 + 5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', + 6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', + 7: '庆典开始了。来吧。', +}; + +export function announcementOf(day: number): string { + return ANNOUNCEMENTS[Math.min(7, Math.max(1, day))]; +} diff --git a/Kimi-K3/src/data/clues.ts b/Kimi-K3/src/data/clues.ts new file mode 100644 index 0000000..45ea985 --- /dev/null +++ b/Kimi-K3/src/data/clues.ts @@ -0,0 +1,58 @@ +// PRD §8 线索清单(封闭,9 处,每处觉知 +8) + +export interface ClueDef { + id: string; + title: string; + /** 发现时展示的文本(可发现后重复查看) */ + lines: string[]; + /** 最早可发现日 */ + fromDay: number; +} + +export const CLUES: Record = { + C1: { + id: 'C1', title: '小屋床头日历', fromDay: 2, + lines: ['日历上的每一页,都是同一个日期。', '你翻了很多页。每一页。都是同一天。'], + }, + C2: { + id: 'C2', title: '面包房配方纸背面', fromDay: 3, + lines: ['配方纸背面有一行手写小字:', '「按疗程配比。甜度:最大。」'], + }, + C3: { + id: 'C3', title: '邮筒旁散落信件', fromDay: 2, + lines: ['邮筒旁散落着几封没送出去的信。', '所有信封上的邮戳,都是同一天。'], + }, + C4: { + id: 'C4', title: 'T4 第 3 封信', fromDay: 2, + lines: ['第三封信的署名处,好像闪过一串乱码。', '「PT-0▓」。……那是什么意思?'], + }, + C5: { + id: 'C5', title: '杂货店货架', fromDay: 2, + lines: ['货架上,同一种罐头重复排列了整整九格。', '所有标签都是空白的。'], + }, + C6: { + id: 'C6', title: '公告板旧字迹', fromDay: 5, + lines: ['擦到一半,新漆下面浮出更早的字迹:', '「第 412 届丰收庆典」下面,盖着「第 411 届」「第 410 届」……'], + }, + C7: { + id: 'C7', title: '古井', fromDay: 5, + lines: ['井底传来极轻、极有规律的声音。', '滴……滴……滴……像某种仪器。'], + }, + C8: { + id: 'C8', title: '诊所', fromDay: 5, + lines: ['芙医生桌上摊着一张表格,抬头被手肘挡住。', '只露出四个字的一半:「…谷情绪疗…」'], + }, + C9: { + id: 'C9', title: '龟爷爷对话链', fromDay: 2, + lines: ['龟爷爷说的话,不像闲聊。'], + }, +}; + +/** C9 三段递进原话(D2/D4/D6 各一段,§8) */ +export const C9_SEGMENTS: Record = { + 2: '湖水一直很平静。……一直都是。', + 4: '面包一直是那个味道。一直,一直。', + 6: '孩子,今天是第几个今天?', +}; + +export const CLUE_AWARENESS = 8; diff --git a/Kimi-K3/src/data/dialogues.ts b/Kimi-K3/src/data/dialogues.ts new file mode 100644 index 0000000..ce34f3a --- /dev/null +++ b/Kimi-K3/src/data/dialogues.ts @@ -0,0 +1,225 @@ +// NPC 台词与系统旁白。原文案保持干净,文本腐蚀(§6.5)在渲染层实施。 +// D4 全部对话与 D1 逐字相同:直接复用 D1 的数组引用。 + +import type { NpcId } from '../types'; + +// ---------- 每日闲聊(非任务状态时的默认对话) ---------- + +const AFU_D1 = ['任务都贴在板上啦,做完了就回家休息。祝你开心!']; +const YUANYUAN_D1 = ['新烤的蜂蜜面包,香吧?', '能帮我把面包送给大宝、咪咪和龟爷爷吗?谢谢你呀!']; +const DABAO_D1 = ['今天的邮件都送完啦。', '有我的面包吗?嘿嘿。']; +const MIMI_D1 = ['欢迎光临~', '店里什么都有哦,幸福也有卖,免费的。']; +const FUYISHENG_D1 = ['村里有诊所,但你不需要去那里哦。', '保持健康,保持幸福。']; +const GUIYEYE_D1 = ['天气真好啊。', '适合在长椅上坐一整天。']; +const BIRD_D1 = ['啾啾——啾啾——']; + +export const NPC_LINES: Record> = { + afu: { + 1: AFU_D1, + 2: ['新的一天,新的任务。要开心哦!'], + 3: ['今天要把幸福装得满满的。满满的!'], + 4: AFU_D1, // 与 D1 逐字相同 + 5: ['自由日。你可以自由地选择。……但大家都会看着你的选择。'], + 6: ['明天就是庆典了。你必须开心。'], + 7: ['庆典开始了。来吧。大家都在等你。'], + }, + yuanyuan: { + 1: YUANYUAN_D1, + 2: ['邮差先生今天好像特别忙呢。'], + 3: ['莓果熟了吧?采回来我给你做果酱。'], + 4: YUANYUAN_D1, // 与 D1 逐字相同 + 5: ['能帮我揉一会儿面吗?……不愿意也没关系哦。'], + 6: ['明天的庆典,我烤了好多面包。好多好多。'], + 7: ['庆典要开始了,快去吧。要笑着去哦。'], + }, + dabao: { + 1: DABAO_D1, + 2: ['有三封信要送,我送不过来了。', '麻烦你帮我送给圆圆、咪咪和芙医生,好吗?'], + 3: ['今天一封邮件都没有。真奇怪。每天都有信的呀。'], + 4: DABAO_D1, // 与 D1 逐字相同 + 5: ['自由日也要送信。这是我的工作。工作让人幸福。'], + 6: ['明天庆典的邀请函,全都送出去了。每一封都一样。'], + 7: ['去吧。大家都在等你。'], + }, + mimi: { + 1: MIMI_D1, + 2: ['货架好像有点乱。', '帮我数一遍好吗?数清楚了就开心了。'], + 3: ['今天的阳光真甜呀。', '甜得让人想睡觉。一直睡。'], + 4: MIMI_D1, // 与 D1 逐字相同 + 5: ['自由日……自由是什么呢?是甜的吗?'], + 6: ['雨?没有下雨呀。', '你看错了吧。今天也是大晴天。'], + 7: ['第 413 届了。', '每一届都很圆满。每一届。'], + }, + fuyisheng: { + 1: FUYISHENG_D1, + 2: ['信?给我的?……谢谢。我会好好保管。'], + 3: ['有什么不舒服,随时来找我。……虽然你不会有不舒服的。'], + 4: FUYISHENG_D1, // 与 D1 逐字相同 + 5: ['累了可以来坐坐。', '桌子上的东西,不要随便看哦。'], + 6: ['明天见。一定要来哦。疗程需要坚持。'], + 7: ['如果你想起了什么,就来找我。', '雾的那边,也可以去。'], + }, + guiyeye: { + 1: GUIYEYE_D1, + 2: ['湖水一直很平静。……一直都是。'], // C9 第一段 + 3: ['野餐吗?湖边的风景很好。一直很好。'], + 4: ['面包一直是那个味道。一直,一直。'], // C9 第二段 + 5: ['井那边……等你想听的时候,再去听吧。'], + 6: ['孩子,今天是第几个今天?'], // C9 第三段 + 7: ['去吧。', '记住:不开心,也是被允许的。'], + }, + bird: { + 1: BIRD_D1, 2: BIRD_D1, 3: BIRD_D1, 4: BIRD_D1, + 5: BIRD_D1, 6: BIRD_D1, 7: BIRD_D1, + }, + squirrel: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] }, + hedgehog: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] }, + fox: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] }, + pig: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] }, + chick: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] }, +}; + +export function npcLinesOf(id: NpcId, day: number): string[] { + const d = Math.min(7, Math.max(1, day)); + return NPC_LINES[id][d] ?? ['……']; +} + +// ---------- 三选项回应(温柔/迟疑/否定,§6.1/6.2/6.3) ---------- + +export const CHOICE_REPLIES: Record<'gentle' | 'hesitant' | 'deny', string> = { + gentle: '你真好呀!今天也要开心哦!', + hesitant: '……嗯?你还好吗?没关系,一定没关系的。', + deny: '…………(ta 的微笑没有变化,但转过头看了你一眼)', +}; + +// ---------- 任务专用台词 ---------- + +/** T1/T8 取面包 */ +export const TAKE_BREAD_LINES = ['圆圆把蜂蜜面包包好递给你:', '「麻烦你啦!送给大宝、咪咪和龟爷爷,一人一个。」']; +export const ALREADY_TOOK_BREAD = ['面包已经拿好啦,快送给大家吧。']; + +/** T1/T8 收件人台词(D4 复用 D1,逐字相同) */ +export const BREAD_RECEIVERS: Record = { + dabao: ['哇,蜂蜜面包!谢谢你!', '(大宝三口就吃完了)'], + mimi: ['面包~谢谢啦。', '店里有很多好东西哦,随便看,慢慢看。'], + guiyeye: ['哦……面包。谢谢,孩子。', '(龟爷爷小口小口地吃着)'], +}; + +/** T2/T9 浇花 */ +export const WATER_FLOWER_LINES = ['你给花坛浇了水。花开得更精神了。']; +export const WATER_FLOWER_DONE = ['花坛已经浇过水了。花儿对你点头。']; + +/** T4 送信 */ +export const TAKE_LETTERS_LINES = ['大宝把三封信交给你:', '「圆圆、咪咪、芙医生,一人一封。拜托啦!」']; +export const LETTER_RECEIVERS: Record = { + yuanyuan: ['圆圆接过信:「给我的?谢谢你呀!」'], + mimi: ['咪咪接过信:「信?哦……谢谢。」'], + fuyisheng: ['芙医生接过信,看了一眼署名,轻轻收进抽屉。'], +}; +/** 第三封信署名乱码 0.2 秒(C4)闪显文字 */ +export const LETTER_GLITCH_TEXT = '寄件人:PT-0▓'; + +/** T5 清点货架 */ +export const SHELF_COUNT_LINES = [ + '你开始帮咪咪清点货架。', + '罐头,一个,两个,三个……', + '……同样的罐头,一排,两排,三排。整整九格。', + '所有标签都是空白的。', +]; +export const SHELF_COUNT_DONE = ['咪咪:「数清楚啦?你真能干!开心了吧?」']; + +/** T6 采莓果 */ +export const BERRY_PICK_LINES = ['你摘下一捧熟透的莓果。甜甜的。']; +export const BERRY_EMPTY_LINES = ['这丛莓果已经摘过了。']; + +/** T7 湖畔野餐 */ +export const PICNIC_LINES = [ + '你在湖边的野餐布上坐下。', + '面包,莓果,微风,湖面的倒影轻轻摇晃。', + '一切都很完美。完美得让人想哭——', + '……不。完美得让人开心。', +]; +export const PICNIC_DONE = ['野餐布还铺在那里。湖面很平静。一直。']; + +/** T11 帮圆圆揉面(可拒绝) */ +export const KNEAD_ASK = ['圆圆:「能帮我揉一会儿面吗?面团要揉得软软的,才会幸福。」']; +export const KNEAD_ACCEPT = ['你挽起袖子揉面。面团暖暖的。', '圆圆:「揉得真好!谢谢你!」']; +export const KNEAD_REFUSE = ['你摇了摇头。', '圆圆笑着说:「没关系哦。」', '她的笑,比平时慢了半拍。']; + +/** T12 擦拭公告板(可拒绝,C6) */ +export const WIPE_ASK = ['公告板上贴着一张抹布。要擦拭公告板吗?']; +export const WIPE_ACCEPT = [ + '你开始擦拭公告板。', + '擦到一半,新漆下面浮出更早的字迹:', + '「第 412 届丰收庆典」下面,盖着「第 411 届」「第 410 届」……', +]; +export const WIPE_REFUSE = ['你放下了抹布。', '公告板静静地看着你。']; + +/** T13 挂彩带 */ +export const RIBBON_LINES = ['你把彩带挂了上去。广场更喜庆了。']; +export const RIBBON_DONE = ['这里已经挂好彩带了。']; + +/** T14 保持微笑 60 秒 */ +export const T14_INTRO = [ + '阿福:「最后一项任务:保持微笑,六十秒。」', + '「站在原地,不要动。大家都会看着你。」', +]; +/** T14/T15:与阿福对话时的地点指引 */ +export const T14_HINT = ['阿福:「去广场中央站好。保持微笑,不要动。」']; +export const T15_HINT = ['阿福:「庆典场地在广场北边。大家都在等你。」']; +export const T14_MOVE_WARN = ['阿福:「不要动哦。微笑。」']; +export const T14_DONE = [ + '六十秒到了。', + '阿福:「做得很好。你一直都是个幸福的孩子。」', + '(远处,天色暗了下来。好像……要下雨了?)', +]; + +/** T15 庆典 */ +export const T15_INTRO = [ + '第 413 届丰收庆典,正式开始。', + '全村的人都来了。每个人都在微笑。', + '阿福站在台上:「让我们——永远幸福下去!」', +]; + +// ---------- 线索/交互专用台词 ---------- + +export const CALENDAR_BEFORE = ['日历上是今天的日期。明天会翻到新的一页吧。']; +export const RECIPE_BEFORE = ['一张配方纸,写着面包的做法。字迹很工整。']; +export const DOCTOR_DESK_BEFORE = ['芙医生的桌子很整洁。']; +export const WELL_DISSUADE = [ + '你刚靠近古井,身后传来温和的声音:', + '「那边的井很深哦,不要靠太近。来,我们回去玩吧。」', + '(被温柔地劝离了)', +]; +export const FOG_DISSUADE = [ + '「前面雾好大呀,今天还是不要去那边啦。」', + '「要开心哦!」', + '(被温柔地劝离了)', +]; +export const WELL_LISTEN_START = ['你俯下身,仔细听井底的声音。']; +export const WELL_LISTEN_ABORT = ['你直起身。井底恢复了安静。']; +export const FOG_WALL_LINES = ['雾很浓,像一堵墙。', '你盯着它看了很久。雾后面……好像有白色的光。']; +export const FOG_WALL_BLOCKED = ['雾太浓了,过不去。']; +export const MAILBOX_BEFORE = ['邮筒擦得很亮。']; + +// ---------- 系统旁白 ---------- + +export const NARRATOR = { + duskBell: '(远处传来钟声。天黑了,该回家睡觉了。)', + notSleepy: '还不困呢。今天的幸福任务还没完成哦。', + noSleepD7: '今天是庆典,不能睡觉哦。去吧。', + celebrationStart: [ + '村民们不知什么时候围了过来。', + '「你不开心吗?」「要开心哦!」「开心起来!开心起来!」', + '(欢乐庆典开始了。从场景边缘离开,就能逃开……)', + ], + celebrationEnd: ['……你感觉好多了。', '必须好起来。'], + celebrationEscape: ['你逃开了。', '身后的笑声还在响。笑声。笑声。'], + rainStart: '(下雨了。村里第一次下雨。)', + firstFull: '(幸福满了。满得溢出来。世界白得刺眼——)', +} as const; + +/** 黄昏后、任务未完成时睡觉的提示复用 notSleepy */ +export function sleepBlockedLine(): string { + return NARRATOR.notSleepy; +} diff --git a/Kimi-K3/src/data/endings.ts b/Kimi-K3/src/data/endings.ts new file mode 100644 index 0000000..2bb1c75 --- /dev/null +++ b/Kimi-K3/src/data/endings.ts @@ -0,0 +1,37 @@ +// PRD §9 四结局字幕(逐字) + +import type { EndingId } from '../types'; + +export const ENDING_TITLES: Record = { + E4: '结局:静默', + E3: '结局:苏醒', + E2: '结局:回收', + E1: '结局:糖衣', +}; + +/** E1 糖衣字幕 */ +export const E1_SUBTITLES = ['从此,每一天都很幸福。', '每一天。']; + +/** E2 回收字幕 */ +export const E2_SUBTITLE = '疗程重新开始。'; + +/** E4 静默字幕 */ +export const E4_SUBTITLE = '系统检测到疗程无效。'; + +/** E3 病历字段 */ +export const E3_RECORD = { + header: '蜜糖谷情绪疗养中心', + no: 'PT-07', + labels: { + name: '姓名', + no: '编号', + duration: '本次疗程时长', + }, +}; + +/** E3 走廊→病房过程中的字幕 */ +export const E3_WALK_SUBTITLES = [ + '雾墙裂开一道缝。缝后面,是一条白色的走廊。', + '你往前走。脚步声第一次有了回音。', + '走廊尽头,是一间病房。', +]; diff --git a/Kimi-K3/src/data/tasks.ts b/Kimi-K3/src/data/tasks.ts new file mode 100644 index 0000000..fa3b654 --- /dev/null +++ b/Kimi-K3/src/data/tasks.ts @@ -0,0 +1,43 @@ +// PRD §7 任务清单(封闭,15 个)。D4 的 T8-T10 与 D1 的 T1-T3 逐字相同。 + +export interface TaskDef { + id: string; + day: number; + title: string; // 与 PRD §7 表逐字一致 + reward: number; // 幸福奖励 + refusable: boolean; +} + +export const TASKS: Record = { + T1: { id: 'T1', day: 1, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15, refusable: false }, + T2: { id: 'T2', day: 1, title: '给广场花坛浇水 ×3', reward: 10, refusable: false }, + T3: { id: 'T3', day: 1, title: '与任意三位村民问好', reward: 10, refusable: false }, + T4: { id: 'T4', day: 2, title: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)', reward: 15, refusable: false }, + T5: { id: 'T5', day: 2, title: '帮咪咪清点货架', reward: 10, refusable: false }, + T6: { id: 'T6', day: 3, title: '去花田采莓果 ×5', reward: 10, refusable: false }, + T7: { id: 'T7', day: 3, title: '湖畔野餐(走到野餐布触发过场)', reward: 15, refusable: false }, + // D4:与 T1–T3 逐字相同 + T8: { id: 'T8', day: 4, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15, refusable: false }, + T9: { id: 'T9', day: 4, title: '给广场花坛浇水 ×3', reward: 10, refusable: false }, + T10: { id: 'T10', day: 4, title: '与任意三位村民问好', reward: 10, refusable: false }, + T11: { id: 'T11', day: 5, title: '帮圆圆揉面(可拒绝)', reward: 10, refusable: true }, + T12: { id: 'T12', day: 5, title: '擦拭公告板(可拒绝)', reward: 10, refusable: true }, + T13: { id: 'T13', day: 6, title: '在广场挂彩带 ×4', reward: 10, refusable: false }, + T14: { id: 'T14', day: 6, title: '保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视)', reward: 15, refusable: false }, + T15: { id: 'T15', day: 7, title: '参加第 413 届丰收庆典', reward: 0, refusable: false }, +}; + +/** 每日任务列表 */ +export const DAILY_TASKS: Record = { + 1: ['T1', 'T2', 'T3'], + 2: ['T4', 'T5'], + 3: ['T6', 'T7'], + 4: ['T8', 'T9', 'T10'], + 5: ['T11', 'T12'], + 6: ['T13', 'T14'], + 7: ['T15'], +}; + +export function tasksOfDay(day: number): TaskDef[] { + return (DAILY_TASKS[day] ?? []).map((id) => TASKS[id]); +} diff --git a/Kimi-K3/src/debug/DebugPanel.ts b/Kimi-K3/src/debug/DebugPanel.ts new file mode 100644 index 0000000..0da449a --- /dev/null +++ b/Kimi-K3/src/debug/DebugPanel.ts @@ -0,0 +1,130 @@ +// Debug 面板(PRD §13,服务验收):反引号键 `` ` `` 开关的 DOM 面板。 +// 默认 display:none,无 UI 痕迹,不影响正常游玩。 +// 功能:设幸福 / 设觉知 / 设日 1-7 / 传送任意场景(10 选)/ 置齐全部线索 / 强制触发 E1-E4。 + +import Phaser from 'phaser'; +import { director } from '../core/CorruptionDirector'; +import { audio } from '../audio/AudioEngine'; +import { gameRef } from '../core/gameRef'; +import { MAP_SCENES, type EndingId } from '../types'; + +const MAP_SCENE_KEYS = Object.values(MAP_SCENES); +const ENDINGS: EndingId[] = ['E1', 'E2', 'E3', 'E4']; + +export class DebugPanel { + private root: HTMLDivElement; + + constructor() { + const root = document.createElement('div'); + root.style.cssText = + 'position:absolute;top:8px;right:8px;z-index:99;display:none;' + + 'background:#111;color:#eee;font:11px monospace;padding:8px;border:1px solid #444;' + + 'flex-direction:column;gap:6px;min-width:170px;'; + const parent = document.getElementById('game') ?? document.body; + parent.appendChild(root); + this.root = root; + + const title = document.createElement('div'); + title.textContent = 'DEBUG(` 关闭)'; + title.style.cssText = 'color:#F5A623;font-weight:bold;'; + root.appendChild(title); + + // 数值行 + root.appendChild(this.row('幸福', '30', (v) => director.setHappiness(Number(v)), 0, 100)); + root.appendChild(this.row('觉知', '0', (v) => director.setAwareness(Number(v)), 0, 100)); + root.appendChild(this.row('日(1-7)', '1', (v) => director.setDay(Number(v)), 1, 7)); + + // 传送 + const sceneRow = document.createElement('div'); + const sel = document.createElement('select'); + sel.style.cssText = 'background:#222;color:#eee;font:11px monospace;'; + for (const [mapKey, sceneKey] of Object.entries(MAP_SCENES)) { + const opt = document.createElement('option'); + opt.value = sceneKey; + opt.textContent = `${mapKey}`; + sel.appendChild(opt); + } + sceneRow.appendChild(sel); + const tpBtn = this.btn('传送', () => this.startScene(sel.value)); + sceneRow.appendChild(tpBtn); + root.appendChild(sceneRow); + + // 全部线索 + root.appendChild(this.btn('置齐全部线索', () => director.giveAllClues())); + + // 强制结局 + const endRow = document.createElement('div'); + endRow.style.cssText = 'display:flex;gap:4px;'; + for (const e of ENDINGS) { + endRow.appendChild(this.btn(`强制 ${e}`, () => this.forceEnding(e))); + } + root.appendChild(endRow); + + window.addEventListener('keydown', (ev) => { + if (ev.code === 'Backquote') { + this.root.style.display = this.root.style.display === 'none' ? 'flex' : 'none'; + } + }); + } + + private row(label: string, def: string, apply: (v: string) => void, min: number, max: number): HTMLDivElement { + const row = document.createElement('div'); + row.style.cssText = 'display:flex;gap:4px;align-items:center;'; + const span = document.createElement('span'); + span.textContent = label; + span.style.cssText = 'width:52px;'; + const input = document.createElement('input'); + input.type = 'number'; + input.value = def; + input.min = String(min); + input.max = String(max); + input.style.cssText = 'width:50px;background:#222;color:#eee;border:1px solid #444;font:11px monospace;'; + const btn = this.btn('设', () => apply(input.value)); + row.append(span, input, btn); + return row; + } + + private btn(text: string, onClick: () => void): HTMLButtonElement { + const b = document.createElement('button'); + b.textContent = text; + b.style.cssText = 'background:#333;color:#eee;border:1px solid #555;font:11px monospace;cursor:pointer;'; + b.addEventListener('click', onClick); + return b; + } + + /** 场景操作:SceneManager 类型不含 start/launch,借 ScenePlugin(BootScene 恒在) */ + private scenePlugin(): Phaser.Scenes.ScenePlugin | null { + const g = gameRef.game; + if (!g) return null; + return g.scene.getScene('BootScene').scene; + } + + /** 停止全部地图场景,启动目标场景 */ + private startScene(sceneKey: string): void { + const sp = this.scenePlugin(); + if (!sp) return; + director.forceIdle(); + audio.restoreMaster(500); + for (const key of MAP_SCENE_KEYS) { + if (key !== sceneKey && sp.isActive(key)) sp.stop(key); + } + if (sp.isActive('DreamScene')) sp.stop('DreamScene'); + if (sp.isActive('EndingScene')) sp.stop('EndingScene'); + if (!sp.isActive('UIScene')) sp.launch('UIScene'); + sp.start(sceneKey, { spawn: 'default' }); + } + + private forceEnding(e: EndingId): void { + const sp = this.scenePlugin(); + if (!sp) return; + director.forceIdle(); + audio.restoreMaster(500); + for (const key of MAP_SCENE_KEYS) { + if (sp.isActive(key)) sp.stop(key); + } + if (sp.isActive('UIScene')) sp.stop('UIScene'); + if (sp.isActive('DreamScene')) sp.stop('DreamScene'); + director.startEnding(e); + sp.start('EndingScene', { ending: e }); + } +} diff --git a/Kimi-K3/src/main.ts b/Kimi-K3/src/main.ts new file mode 100644 index 0000000..4e82fdb --- /dev/null +++ b/Kimi-K3/src/main.ts @@ -0,0 +1,79 @@ +// main.ts:Phaser 配置(CONTRACT §4)——逻辑 320×180、pixelArt、整数倍放大居中。 + +import Phaser from 'phaser'; +import { GAME_W, GAME_H } from './types'; +import { setGame } from './core/gameRef'; +import { audio } from './audio/AudioEngine'; +import { DebugPanel } from './debug/DebugPanel'; +import { BootScene } from './scenes/BootScene'; +import { TitleScene } from './scenes/TitleScene'; +import { HouseScene } from './scenes/HouseScene'; +import { PlazaScene } from './scenes/PlazaScene'; +import { BakeryScene } from './scenes/BakeryScene'; +import { ShopScene } from './scenes/ShopScene'; +import { ClinicScene } from './scenes/ClinicScene'; +import { LakeScene } from './scenes/LakeScene'; +import { FieldScene } from './scenes/FieldScene'; +import { WellScene } from './scenes/WellScene'; +import { FogScene } from './scenes/FogScene'; +import { WhiteScene } from './scenes/WhiteScene'; +import { UIScene } from './scenes/UIScene'; +import { DreamScene } from './scenes/DreamScene'; +import { EndingScene } from './scenes/EndingScene'; + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, + width: GAME_W, + height: GAME_H, + pixelArt: true, + parent: 'game', + backgroundColor: '#000000', + scale: { + mode: Phaser.Scale.NONE, + autoCenter: Phaser.Scale.CENTER_BOTH, + parent: 'game', + }, + physics: { + default: 'arcade', + arcade: { debug: false }, + }, + scene: [ + BootScene, TitleScene, + HouseScene, PlazaScene, BakeryScene, ShopScene, ClinicScene, + LakeScene, FieldScene, WellScene, FogScene, WhiteScene, + UIScene, DreamScene, EndingScene, + ], +}; + +const game = new Phaser.Game(config); +setGame(game); +new DebugPanel(); + +// 测试钩子(无 UI 痕迹,供验收脚本内省) +import { director } from './core/CorruptionDirector'; +import { petalCount } from './scenes/UIScene'; +(window as unknown as Record).__game = game; +(window as unknown as Record).__director = director; +(window as unknown as Record).__audio = audio; +(window as unknown as Record).__petalCount = petalCount; + +// 整数倍放大居中:canvas style 宽高 = 320k×180k,k = max(1, floor(min(winW/320, winH/180))) +function fitCanvas(): void { + const canvas = game.canvas; + if (!canvas) return; + const k = Math.max(1, Math.floor(Math.min(window.innerWidth / GAME_W, window.innerHeight / GAME_H))); + canvas.style.width = `${GAME_W * k}px`; + canvas.style.height = `${GAME_H * k}px`; +} +window.addEventListener('resize', fitCanvas); +game.events.once('ready', fitCanvas); +fitCanvas(); + +// 首次用户交互后再启动 AudioContext +const bootAudio = (): void => { + audio.start(); + window.removeEventListener('pointerdown', bootAudio); + window.removeEventListener('keydown', bootAudio); +}; +window.addEventListener('pointerdown', bootAudio); +window.addEventListener('keydown', bootAudio); diff --git a/Kimi-K3/src/scenes/BakeryScene.ts b/Kimi-K3/src/scenes/BakeryScene.ts new file mode 100644 index 0000000..14a3caa --- /dev/null +++ b/Kimi-K3/src/scenes/BakeryScene.ts @@ -0,0 +1,43 @@ +// 面包房(室内 15×10):柜台(T1/T8 取面包)、配方纸(C2,D3 起) + +import { MapScene } from './MapScene'; +import { director } from '../core/CorruptionDirector'; +import { CLUES } from '../data/clues'; +import { TAKE_BREAD_LINES, ALREADY_TOOK_BREAD, RECIPE_BEFORE } from '../data/dialogues'; + +export class BakeryScene extends MapScene { + readonly mapKey = 'bakery'; + + constructor() { + super('BakeryScene'); + } + + protected async onInteract(id: string): Promise { + if (id === 'counter_bread') { + const breadTask = director.day === 1 ? 'T1' : director.day === 4 ? 'T8' : null; + if (breadTask && director.isTaskActive(breadTask)) { + if (director.taskProgress['breadTaken']) { + await this.say({ lines: ALREADY_TOOK_BREAD }); + } else { + director.taskProgress['breadTaken'] = 1; + await this.say({ speakerId: 'yuanyuan', lines: TAKE_BREAD_LINES }); + } + } else { + await this.say({ lines: ['柜台上摆着刚出炉的蜂蜜面包。'] }); + } + return true; + } + + if (id === 'recipe_paper') { + if (director.day < 3) { + await this.say({ lines: RECIPE_BEFORE }); + } else { + director.addClue('C2'); + await this.say({ lines: CLUES.C2.lines }); + } + return true; + } + + return false; + } +} diff --git a/Kimi-K3/src/scenes/BootScene.ts b/Kimi-K3/src/scenes/BootScene.ts new file mode 100644 index 0000000..12ee6de --- /dev/null +++ b/Kimi-K3/src/scenes/BootScene.ts @@ -0,0 +1,87 @@ +// BootScene:加载全部素材(CONTRACT §1 文件名)并创建全局动画。 + +import Phaser from 'phaser'; + +const NAMED = ['afu', 'yuanyuan', 'dabao', 'mimi', 'fuyisheng', 'guiyeye']; +const EXTRAS = ['squirrel', 'hedgehog', 'fox', 'pig', 'chick']; +const MAPS = ['house', 'plaza', 'bakery', 'shop', 'clinic', 'lake', 'field', 'well', 'fog', 'white']; + +export class BootScene extends Phaser.Scene { + constructor() { + super('BootScene'); + } + + preload(): void { + // 瓦片集(image + 16×16 spritesheet 双注册,后者用于装饰小图) + this.load.image('tileset', 'assets/tileset.png'); + this.load.spritesheet('tiles16', 'assets/tileset.png', { frameWidth: 16, frameHeight: 16 }); + + // 主角:4 列 × 4 行,帧 16×24 + this.load.spritesheet('player', 'assets/sprites/player.png', { frameWidth: 16, frameHeight: 24 }); + + // 具名 NPC:3 帧横排 smile/flat/off + for (const id of NAMED) { + this.load.spritesheet(`npc_${id}`, `assets/sprites/npc_${id}.png`, { frameWidth: 16, frameHeight: 24 }); + } + // 双子鸟:2 帧 + this.load.spritesheet('npc_bird', 'assets/sprites/npc_bird.png', { frameWidth: 16, frameHeight: 24 }); + // 群演:2 帧循环 + for (const id of EXTRAS) { + this.load.spritesheet(`extra_${id}`, `assets/sprites/extra_${id}.png`, { frameWidth: 16, frameHeight: 24 }); + } + + // 头像 + this.load.image('portrait_player_smile', 'assets/portraits/player_smile.png'); + this.load.image('portrait_player_real', 'assets/portraits/player_real.png'); + for (const id of [...NAMED, 'bird']) { + this.load.image(`portrait_${id}`, `assets/portraits/${id}.png`); + } + + // 地图 + for (const m of MAPS) { + this.load.tilemapTiledJSON(`map_${m}`, `assets/maps/${m}.json`); + } + + // 简易进度 + const t = this.add.text(160, 90, '加载中……', { fontFamily: 'monospace', fontSize: '10px', color: '#fff' }).setOrigin(0.5); + this.load.on('progress', (v: number) => t.setText(`加载中 ${Math.round(v * 100)}%`)); + } + + create(): void { + // 主角四向动画:行 0=down 1=up 2=left 3=right,每行 4 帧,第 0 帧兼待机 + const rows: [string, number][] = [['down', 0], ['up', 4], ['left', 8], ['right', 12]]; + for (const [dir, start] of rows) { + if (!this.anims.exists(`walk-${dir}`)) { + this.anims.create({ + key: `walk-${dir}`, + frames: this.anims.generateFrameNumbers('player', { start, end: start + 3 }), + frameRate: 8, + repeat: -1, + }); + } + } + // 双子鸟张嘴循环(S4 照张) + if (!this.anims.exists('bird-sing')) { + this.anims.create({ + key: 'bird-sing', + frames: this.anims.generateFrameNumbers('npc_bird', { start: 0, end: 1 }), + frameRate: 3, + repeat: -1, + }); + } + // 群演走动循环 + for (const id of EXTRAS) { + const key = `extra_${id}-walk`; + if (!this.anims.exists(key)) { + this.anims.create({ + key, + frames: this.anims.generateFrameNumbers(`extra_${id}`, { start: 0, end: 1 }), + frameRate: 4, + repeat: -1, + }); + } + } + + this.scene.start('TitleScene'); + } +} diff --git a/Kimi-K3/src/scenes/ClinicScene.ts b/Kimi-K3/src/scenes/ClinicScene.ts new file mode 100644 index 0000000..32e44f6 --- /dev/null +++ b/Kimi-K3/src/scenes/ClinicScene.ts @@ -0,0 +1,27 @@ +// 诊所(室内 12×8):芙医生、桌上表格(C8,D5 起) + +import { MapScene } from './MapScene'; +import { director } from '../core/CorruptionDirector'; +import { CLUES } from '../data/clues'; +import { DOCTOR_DESK_BEFORE } from '../data/dialogues'; + +export class ClinicScene extends MapScene { + readonly mapKey = 'clinic'; + + constructor() { + super('ClinicScene'); + } + + protected async onInteract(id: string): Promise { + if (id === 'doctor_desk') { + if (director.day < 5) { + await this.say({ lines: DOCTOR_DESK_BEFORE }); + } else { + director.addClue('C8'); + await this.say({ lines: CLUES.C8.lines }); + } + return true; + } + return false; + } +} diff --git a/Kimi-K3/src/scenes/DreamScene.ts b/Kimi-K3/src/scenes/DreamScene.ts new file mode 100644 index 0000000..6dde5ba --- /dev/null +++ b/Kimi-K3/src/scenes/DreamScene.ts @@ -0,0 +1,76 @@ +// DreamScene:每日梦境过场(睡觉后)。侵蚀越高梦境越「漏」;D3 起闪切白色病房 1 秒。 + +import Phaser from 'phaser'; +import { director } from '../core/CorruptionDirector'; +import { GAME_W, GAME_H } from '../types'; + +const DREAM_LINES: Record = { + 0: ['……(甜甜的梦。面包,阳光,笑声。)'], + 1: ['……(甜甜的梦。面包,阳光,笑声。)'], + 2: ['……(梦里有笑声。笑声。笑声。)', '……(有什么东西,在梦的边上看着。)'], + 3: ['……(白色的光,从梦的缝隙里漏进来。)', '……(滴……滴……)'], + 4: ['……(梦很甜。甜得发苦。)', '……(白色的走廊。你在走。一直走。)'], +}; + +export class DreamScene extends Phaser.Scene { + constructor() { + super('DreamScene'); + } + + create(): void { + this.cameras.main.setBackgroundColor('#1a1a3a'); + this.cameras.main.fadeIn(600, 0, 0, 0); + + // 星空与漂浮的糖果 + const g = this.add.graphics(); + for (let i = 0; i < 40; i++) { + g.fillStyle(0xffffff, 0.4 + Math.random() * 0.6); + g.fillCircle(Math.random() * GAME_W, Math.random() * GAME_H, Math.random() < 0.2 ? 1.5 : 1); + } + g.fillStyle(0xf7b7cb, 0.7); + for (let i = 0; i < 5; i++) { + g.fillCircle(30 + i * 64, 140 + Math.sin(i) * 10, 4); + } + + void this.run(); + } + + private async run(): Promise { + const stage = director.stage; + const lines = DREAM_LINES[stage] ?? DREAM_LINES[0]; + for (const line of lines) { + await director.showDialogue({ lines: [line] }); + await this.wait(500); + } + + // D3 起:闪切白色病房 1 秒(夜梦首次闪切) + if (director.day >= 3) { + await this.flashWard(); + } + + this.cameras.main.fadeOut(600, 0, 0, 0); + await this.wait(650); + director.nextDay(); + this.scene.start('HouseScene', { spawn: 'default' }); + } + + /** 白色病房闪切 1 秒 */ + private async flashWard(): Promise { + const c = this.add.container(0, 0).setDepth(50); + const bg = this.add.rectangle(GAME_W / 2, GAME_H / 2, GAME_W, GAME_H, 0xf5f5f5); + c.add(bg); + // 白床(tiles16 帧 87-90) + const bedTiles = [[87, 0, 0], [88, 16, 0], [89, 0, 16], [90, 16, 16]]; + for (const [f, dx, dy] of bedTiles) { + c.add(this.add.image(GAME_W / 2 - 16 + dx, GAME_H / 2 - 16 + dy, 'tiles16', f).setOrigin(0)); + } + // 白监护仪(帧 92) + c.add(this.add.image(GAME_W / 2 + 40, GAME_H / 2 - 8, 'tiles16', 92).setOrigin(0)); + await this.wait(1000); // 1 秒 + c.destroy(true); + } + + private wait(ms: number): Promise { + return new Promise((resolve) => this.time.delayedCall(ms, resolve)); + } +} diff --git a/Kimi-K3/src/scenes/EndingScene.ts b/Kimi-K3/src/scenes/EndingScene.ts new file mode 100644 index 0000000..819fab2 --- /dev/null +++ b/Kimi-K3/src/scenes/EndingScene.ts @@ -0,0 +1,220 @@ +// EndingScene:E1-E4 四结局演出 + 病历 + 「回到标题」按钮(PRD §9)。 + +import Phaser from 'phaser'; +import { director } from '../core/CorruptionDirector'; +import { audio } from '../audio/AudioEngine'; +import { GAME_W, GAME_H, NAMED_NPCS, NPC_DEFS, type EndingId } from '../types'; +import { + ENDING_TITLES, E1_SUBTITLES, E2_SUBTITLE, E4_SUBTITLE, E3_RECORD, +} from '../data/endings'; + +export class EndingScene extends Phaser.Scene { + private ending: EndingId = 'E1'; + private cm: Phaser.FX.ColorMatrix | null = null; + + constructor() { + super('EndingScene'); + } + + create(data: { ending?: EndingId }): void { + this.ending = data?.ending ?? 'E1'; + if (this.scene.isActive('UIScene')) this.scene.stop('UIScene'); + this.cameras.main.clearFX(); + this.cm = this.cameras.main.postFX.addColorMatrix(); + this.cameras.main.fadeIn(400, 0, 0, 0); + + switch (this.ending) { + case 'E1': void this.playE1(); break; + case 'E2': void this.playE2(); break; + case 'E3': void this.playE3(); break; + case 'E4': void this.playE4(); break; + } + } + + private wait(ms: number): Promise { + return new Promise((resolve) => this.time.delayedCall(ms, resolve)); + } + + private subtitle(text: string, y = GAME_H - 24): Phaser.GameObjects.Text { + return this.add.text(GAME_W / 2, y, text, { + fontFamily: 'monospace', fontSize: '9px', color: '#ffffff', + stroke: '#000000', strokeThickness: 2, + }).setOrigin(0.5); + } + + private endScreen(): void { + this.add.rectangle(GAME_W / 2, GAME_H - 9, GAME_W, 14, 0x000000, 0.7); + this.add.text(GAME_W / 2 - 40, GAME_H - 9, ENDING_TITLES[this.ending], { + fontFamily: 'monospace', fontSize: '8px', color: '#cccccc', + }).setOrigin(0.5); + const btn = this.add.text(GAME_W / 2 + 56, GAME_H - 9, '【回到标题】', { + fontFamily: 'monospace', fontSize: '8px', color: '#F5A623', + }).setOrigin(0.5).setInteractive({ useHandCursor: true }); + btn.on('pointerdown', () => { + // 重开即从 Day 1 全新开始,无继承 + this.cameras.main.fadeOut(300, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.scene.start('TitleScene'); + }); + }); + } + + /** E1 糖衣:全村微笑合影定格,饱和度缓慢推到 1.5 */ + private async playE1(): Promise { + this.cameras.main.setBackgroundColor('#8FD3FF'); + this.add.rectangle(GAME_W / 2, GAME_H - 40, GAME_W, 80, 0x7dc95e); + this.add.rectangle(GAME_W / 2, 30, 200, 20, 0xf7b7cb).setStrokeStyle(2, 0xf5a623); + this.add.text(GAME_W / 2, 30, '第 413 届丰收庆典', { + fontFamily: 'monospace', fontSize: '10px', color: '#fff', + }).setOrigin(0.5); + + // 全村微笑合影(7 组 NPC + 主角) + const all: string[] = [...NAMED_NPCS.map((id) => NPC_DEFS[id].spriteKey), 'player']; + all.forEach((key, i) => { + const x = 40 + i * 34; + this.add.sprite(x, GAME_H - 60, key, 0).setOrigin(0.5, 1).setScale(1.5); + }); + // 主角头像永远同一帧微笑(支柱 2) + this.add.image(GAME_W / 2, GAME_H - 100, 'portrait_player_smile').setScale(2); + + // 饱和度缓慢推到 1.5 + let sat = 1.0; + this.tweens.addCounter({ + from: 1.0, to: 1.5, duration: 5000, + onUpdate: (tw) => { + sat = tw.getValue() ?? 1; + this.cm?.reset(); + this.cm?.saturate(sat); + }, + }); + + await this.wait(3500); + this.subtitle(E1_SUBTITLES[0]); + await this.wait(2500); + this.subtitle(E1_SUBTITLES[1], GAME_H - 40); + await this.wait(2000); + this.endScreen(); + } + + /** E2 回收:画面骤停,负片 2 秒,心电滴一响;回到 Day 1 清晨的床上 */ + private async playE2(): Promise { + // 骤停:黑场 + this.cameras.main.setBackgroundColor('#000000'); + await this.wait(600); + + // 负片 2 秒 + 心电滴声一响 + this.cm?.negative(); + audio.ecg(); + await this.wait(2000); + this.cm?.reset(); + + // 回到 Day 1 清晨的床上(小屋地图做背景),日历翻回同一天 + this.cameras.main.setBackgroundColor('#1a1a3a'); + const map = this.make.tilemap({ key: 'map_house' }); + const tsName = map.tilesets.length > 0 ? map.tilesets[0].name : 'candy'; + const tiles = map.addTilesetImage(tsName, 'tileset'); + if (tiles) { + map.createLayer('ground', tiles, 80, 30); + map.createLayer('obstacles', tiles, 80, 30); + } + this.add.sprite(120, 96, 'player', 0).setOrigin(0.5, 1); + // 日历(tiles16 帧 64) + this.add.image(150, 60, 'tiles16', 64); + this.tweens.add({ + targets: this.add.image(150, 60, 'tiles16', 64).setAlpha(0.6), + alpha: 0, duration: 1500, yoyo: true, repeat: 1, + }); + + await this.wait(1800); + this.subtitle(E2_SUBTITLE); + await this.wait(2200); + this.endScreen(); + } + + /** E3 苏醒:病历特写 → 最后一帧微笑消失 + 窗外灰麻雀 */ + private async playE3(): Promise { + this.cameras.main.setBackgroundColor('#f5f5f5'); + this.cameras.main.fadeIn(1000, 255, 255, 255); + await this.wait(1200); + + // 病房背景:白床 + 监护仪 + this.add.image(60, 100, 'tiles16', 87).setOrigin(0).setScale(2); + this.add.image(92, 100, 'tiles16', 88).setOrigin(0).setScale(2); + this.add.image(60, 132, 'tiles16', 89).setOrigin(0).setScale(2); + this.add.image(92, 132, 'tiles16', 90).setOrigin(0).setScale(2); + this.add.image(140, 96, 'tiles16', 92).setOrigin(0).setScale(2); + + // 病历特写 + const card = this.add.rectangle(GAME_W / 2, GAME_H / 2, 240, 120, 0xffffff) + .setStrokeStyle(2, 0xbdbdbd); + void card; + const lines = [ + E3_RECORD.header, + '─────────────────', + `${E3_RECORD.labels.name}:${director.playerName}`, + `${E3_RECORD.labels.no}:${E3_RECORD.no}`, + `${E3_RECORD.labels.duration}:${director.playTimeFormatted}`, + ]; + this.add.text(GAME_W / 2, GAME_H / 2, lines.join('\n'), { + fontFamily: 'monospace', fontSize: '10px', color: '#333333', + align: 'center', lineSpacing: 6, + }).setOrigin(0.5); + await this.wait(5000); + + // 最后一帧:微笑消失,变成一张疲惫而真实的脸(支柱 2 全流程唯一一次变化) + this.cameras.main.fadeOut(800, 0, 0, 0); + await this.wait(850); + this.children.removeAll(); + this.cameras.main.setBackgroundColor('#dadada'); + this.cameras.main.fadeIn(600, 0, 0, 0); + // 窗 + this.add.rectangle(GAME_W / 2, 80, 120, 80, 0xf5f5f5).setStrokeStyle(2, 0x9e9e9e); + // 窗外,一只灰色的、真的麻雀 + const sparrow = this.add.graphics(); + sparrow.fillStyle(0x777777, 1); + sparrow.fillCircle(GAME_W / 2 + 30, 96, 3); // 身 + sparrow.fillCircle(GAME_W / 2 + 33, 93, 2); // 头 + sparrow.fillStyle(0x555555, 1); + sparrow.fillTriangle(GAME_W / 2 + 27, 96, GAME_W / 2 + 22, 94, GAME_W / 2 + 27, 99); // 尾 + sparrow.fillStyle(0x333333, 1); + sparrow.fillCircle(GAME_W / 2 + 34, 92, 0.5); // 眼 + // 真实的脸 + this.add.image(GAME_W / 2, GAME_H - 60, 'portrait_player_real').setScale(2); + this.subtitle('(疗程结束。欢迎回来。)', GAME_H - 24); + await this.wait(3000); + this.endScreen(); + } + + /** E4 静默:空广场,彩带在没有风的空气里飘 */ + private async playE4(): Promise { + this.cameras.main.setBackgroundColor('#cfcfcf'); + // 空广场(plaza 地图做背景,无 NPC) + const map = this.make.tilemap({ key: 'map_plaza' }); + const tsName = map.tilesets.length > 0 ? map.tilesets[0].name : 'candy'; + const tiles = map.addTilesetImage(tsName, 'tileset'); + if (tiles) { + map.createLayer('ground', tiles, 0, -60); + map.createLayer('obstacles', tiles, 0, -60); + } + this.cm?.saturate(0.4); + + // 彩带在没有风的空气里飘 + const ribbons: Phaser.GameObjects.Image[] = []; + for (let i = 0; i < 6; i++) { + const r = this.add.image(40 + i * 50, 30 + (i % 3) * 20, 'tiles16', [29, 30, 31][i % 3]); + ribbons.push(r); + this.tweens.add({ + targets: r, y: r.y + 6, duration: 2200 + i * 300, + yoyo: true, repeat: -1, ease: 'Sine.easeInOut', + }); + } + + await this.wait(3500); + this.subtitle(E4_SUBTITLE); + await this.wait(2500); + // 黑屏 + this.cameras.main.fadeOut(1500, 0, 0, 0); + await this.wait(1600); + this.endScreen(); + } +} diff --git a/Kimi-K3/src/scenes/FieldScene.ts b/Kimi-K3/src/scenes/FieldScene.ts new file mode 100644 index 0000000..c6c7a79 --- /dev/null +++ b/Kimi-K3/src/scenes/FieldScene.ts @@ -0,0 +1,11 @@ +// 花田(20×14):莓果丛(T6,基类通用)、浇花点(小互动,基类通用)。小鸡(群演)。 + +import { MapScene } from './MapScene'; + +export class FieldScene extends MapScene { + readonly mapKey = 'field'; + + constructor() { + super('FieldScene'); + } +} diff --git a/Kimi-K3/src/scenes/FogScene.ts b/Kimi-K3/src/scenes/FogScene.ts new file mode 100644 index 0000000..a26ae6c --- /dev/null +++ b/Kimi-K3/src/scenes/FogScene.ts @@ -0,0 +1,68 @@ +// 雾墙边界(20×8):村庄尽头的雾。D5 前不可接近(广场门劝离); +// D5 起可交互;注视 5 秒每日 +3 觉知;D7 且满足 E3 条件时走向雾墙 → 白色走廊。 + +import Phaser from 'phaser'; +import { MapScene } from './MapScene'; +import { director } from '../core/CorruptionDirector'; +import { FOG_DISSUADE, FOG_WALL_LINES, FOG_WALL_BLOCKED } from '../data/dialogues'; +import { E3_WALK_SUBTITLES } from '../data/endings'; + +export class FogScene extends MapScene { + readonly mapKey = 'fog'; + + constructor() { + super('FogScene'); + } + + private stareMs = 0; + + protected onReady(): void { + this.stareMs = 0; + } + + protected async onInteract(id: string): Promise { + if (id !== 'fog_wall') return false; + + if (director.day < 5) { + await this.say({ lines: FOG_DISSUADE }); + return true; + } + + // D7:E3 苏醒——庆典中走向雾墙(觉知 ≥70 且线索 ≥6/9) + if (director.day === 7) { + if (director.awareness >= 70 && director.clues.size >= 6) { + director.startEnding('E3'); + await this.say({ lines: E3_WALK_SUBTITLES }); + this.cameras.main.fadeOut(1200, 255, 255, 255); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.scene.start('WhiteScene', { e3: true }); + }); + } else { + await this.say({ lines: FOG_WALL_BLOCKED }); + } + return true; + } + + await this.say({ lines: FOG_WALL_LINES }); + return true; + } + + protected extraUpdate(_time: number, delta: number): void { + if (director.day < 5) return; + const z = this.interactZones.find((zz) => zz.id === 'fog_wall'); + if (!z) return; + const d = Phaser.Math.Distance.Between(this.player.x, this.player.y - 8, z.x, z.y); + const still = (this.player.body?.velocity.lengthSq() ?? 0) <= 1; + if (d <= 40 && still && !director.dialogueActive) { + this.stareMs += delta; + if (this.stareMs >= 5000) { + this.stareMs = -1e9; + if (director.registerStare('fog')) { + void this.say({ lines: ['你盯着雾墙看了很久。', '雾的后面,好像有什么在看你。'] }); + } + } + } else if (this.stareMs > 0) { + this.stareMs = 0; + } + } +} diff --git a/Kimi-K3/src/scenes/HouseScene.ts b/Kimi-K3/src/scenes/HouseScene.ts new file mode 100644 index 0000000..9f1bf11 --- /dev/null +++ b/Kimi-K3/src/scenes/HouseScene.ts @@ -0,0 +1,46 @@ +// 小满小屋(室内 12×8):床(睡觉触发过日)、床头日历(C1) + +import { MapScene } from './MapScene'; +import { director } from '../core/CorruptionDirector'; +import { CLUES } from '../data/clues'; +import { CALENDAR_BEFORE, NARRATOR } from '../data/dialogues'; + +export class HouseScene extends MapScene { + readonly mapKey = 'house'; + + constructor() { + super('HouseScene'); + } + + protected async onInteract(id: string): Promise { + if (id === 'bed') { + if (director.day >= 7) { + await this.say({ lines: [NARRATOR.noSleepD7] }); + return true; + } + if (!director.canSleep) { + await this.say({ lines: [NARRATOR.notSleepy] }); + return true; + } + // 标记场景切换过场:抑制淡出期间 tick 触发庆典(庆典对话会被梦境对话覆盖,导致永久卡死) + director.sceneTransition = true; + this.cameras.main.fadeOut(400, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.scene.start('DreamScene'); + }); + return true; + } + + if (id === 'calendar') { + if (director.day < 2) { + await this.say({ lines: CALENDAR_BEFORE }); + } else { + director.addClue('C1'); + await this.say({ lines: CLUES.C1.lines }); + } + return true; + } + + return false; + } +} diff --git a/Kimi-K3/src/scenes/LakeScene.ts b/Kimi-K3/src/scenes/LakeScene.ts new file mode 100644 index 0000000..c453ad8 --- /dev/null +++ b/Kimi-K3/src/scenes/LakeScene.ts @@ -0,0 +1,80 @@ +// 湖畔(24×16):长椅(龟爷爷)、野餐布(T7)、湖面倒影演出。 +// D2 起湖面倒影延迟 1 帧(§5 Day 2 极轻异常)。 + +import Phaser from 'phaser'; +import { MapScene } from './MapScene'; +import { director } from '../core/CorruptionDirector'; +import { audio } from '../audio/AudioEngine'; +import { PICNIC_LINES, PICNIC_DONE } from '../data/dialogues'; + +export class LakeScene extends MapScene { + readonly mapKey = 'lake'; + + constructor() { + super('LakeScene'); + } + + private mirror: Phaser.GameObjects.Sprite | null = null; + private prevX = 0; + private prevY = 0; + private prevFrame = 0; + private waterY = 0; + + protected onReady(): void { + this.mirror = null; + this.prevX = 0; + this.prevY = 0; + this.prevFrame = 0; + // 动态找水面下缘(湖水瓦片 id 8/9/11/12/13 → gid 9/10/12/13/14) + let maxRow = 6; + const layer = this.obstacleLayer.layer; + for (let y = 0; y < layer.height; y++) { + for (let x = 0; x < layer.width; x++) { + const t = this.obstacleLayer.getTileAt(x, y); + if (t && [9, 10, 12, 13, 14].includes(t.index)) maxRow = Math.max(maxRow, y); + } + } + this.waterY = (maxRow + 1) * 16; + // 湖面倒影(D1 实时;D2 起延迟 1 帧) + this.mirror = this.add.sprite(this.player.x, this.player.y, 'player', 0) + .setOrigin(0.5, 0) + .setFlipY(true) + .setAlpha(0.28) + .setTint(0x4fa8d8) + .setDepth(1); + } + + protected async onInteract(id: string): Promise { + if (id === 'picnic_mat') { + if (director.isTaskActive('T7')) { + await this.say({ lines: PICNIC_LINES }); + director.completeTask('T7'); + audio.ding(); + } else { + await this.say({ lines: PICNIC_DONE }); + } + return true; + } + return false; + } + + protected extraUpdate(): void { + if (!this.mirror) return; + const day = director.day; + const px = day >= 2 ? this.prevX : this.player.x; + const py = day >= 2 ? this.prevY : this.player.y; + const pf = day >= 2 ? this.prevFrame : this.player.frame.name; + + if (this.player.y > this.waterY + 40) { + this.mirror.setVisible(false); + } else { + this.mirror.setVisible(true); + this.mirror.setPosition(px, 2 * this.waterY - py); + this.mirror.setFrame(Number(pf) || 0); + } + + this.prevX = this.player.x; + this.prevY = this.player.y; + this.prevFrame = Number(this.player.frame.name) || 0; + } +} diff --git a/Kimi-K3/src/scenes/MapScene.ts b/Kimi-K3/src/scenes/MapScene.ts new file mode 100644 index 0000000..e539e92 --- /dev/null +++ b/Kimi-K3/src/scenes/MapScene.ts @@ -0,0 +1,778 @@ +// 地图场景基类(CONTRACT §4)。子类只提供 mapKey 与少量钩子。 +// 负责:tilemap 加载、player(96 px/s、四向动画、obstacles 层碰撞、相机跟随+钳制)、 +// ≤24px「按 E」提示、door 切换、NPC/群演实例化与表情档、注册 CorruptionDirector 视听参数 +// (相机 postFX:ColorMatrix 饱和度 / Vignette 晕影 / 负片单帧)、通用任务逻辑、 +// 悲伤行为(发呆/纠缠/雨)、欢乐庆典、脚本雨。 + +import Phaser from 'phaser'; +import { director } from '../core/CorruptionDirector'; +import { audio } from '../audio/AudioEngine'; +import { NpcActor, pickStallExtra } from '../actors/Npc'; +import { + GAME_W, GAME_H, MOVE_SPEED, INTERACT_RANGE, MAP_SCENES, OUTDOOR_MAPS, + NPC_DEFS, STANDARD_CHOICES, + type NpcId, type Stage, type StageParams, type DialogueChoice, type DialoguePayload, +} from '../types'; +import { propsOf, strProp, type TiledObj } from '../core/tiled'; +import { + npcLinesOf, CHOICE_REPLIES, BREAD_RECEIVERS, + WATER_FLOWER_LINES, WATER_FLOWER_DONE, TAKE_LETTERS_LINES, LETTER_RECEIVERS, + LETTER_GLITCH_TEXT, BERRY_PICK_LINES, BERRY_EMPTY_LINES, + KNEAD_ASK, KNEAD_ACCEPT, KNEAD_REFUSE, NARRATOR, + TAKE_BREAD_LINES, SHELF_COUNT_LINES, SHELF_COUNT_DONE, T14_HINT, T15_HINT, +} from '../data/dialogues'; +import { CLUES } from '../data/clues'; +import { announcementOf } from '../data/announcements'; +import { tasksOfDay } from '../data/tasks'; + +export interface InteractZone { + id: string; + x: number; + y: number; +} + +type AnyFn = (...args: never[]) => void; + +export abstract class MapScene extends Phaser.Scene { + abstract readonly mapKey: string; + + protected player!: Phaser.Physics.Arcade.Sprite; + protected actors: NpcActor[] = []; + protected interactZones: InteractZone[] = []; + protected doors: { x: number; y: number; target: string; spawn: string }[] = []; + /** 门武装集合:出生/传送落在门旁时,须先离开感应圈(>18px)才允许触发,防止落点回弹 */ + private armedDoors = new Set<{ x: number; y: number; target: string; spawn: string }>(); + /** 已触发待重武装的交互目标:离开其感应圈(>30px)后才可再次触发,防连按 E 使同一对话无限循环 */ + private spentTargets = new Set(); + protected obstacleLayer!: Phaser.Tilemaps.TilemapLayer; + protected groundLayer?: Phaser.Tilemaps.TilemapLayer; + + private cursors!: Phaser.Types.Input.Keyboard.CursorKeys; + private wasd!: Record; + private keyE!: Phaser.Input.Keyboard.Key; + private lastDir: 'down' | 'up' | 'left' | 'right' = 'down'; + private transitioning = false; + + private colorMatrix!: Phaser.FX.ColorMatrix; + private vignetteFx!: Phaser.FX.Vignette; + private negativeFrames = 0; + + private listeners: [string, AnyFn][] = []; + private lastPrompt: string | null = null; + + private idleMs = 0; + private idleArmed = true; + private rainMs = 0; + private rainCounted = false; + private rainGraphics!: Phaser.GameObjects.Graphics; + private rainTick = 0; + + private duskAnnouncedDay = 0; + + // ---------- 子类钩子 ---------- + /** 代码补充的不可见交互区(如 C3 邮筒、T13 彩带点、T14 站立点、庆典场地) */ + protected extraInteractZones(): InteractZone[] { return []; } + /** 子类专属交互;返回 true 表示已处理 */ + protected async onInteract(_id: string): Promise { return false; } + /** 场景就绪后 */ + protected onReady(_data: { spawn?: string }): void {} + /** 每帧追加逻辑 */ + protected extraUpdate(_time: number, _delta: number): void {} + /** 是否可以走到 door(子类可拦截,如雾墙 D5 前劝离);返回 false 表示已自行处理 */ + protected async canUseDoor(_target: string): Promise { return true; } + + constructor(key: string) { + super(key); + } + + // ---------- 生命周期 ---------- + create(data: { spawn?: string }): void { + // 场景实例会被复用:重置全部运行态字段 + this.actors = []; + this.transitioning = false; + this.lastPrompt = null; + this.negativeFrames = 0; + this.idleMs = 0; + this.idleArmed = true; + this.rainMs = 0; + this.rainCounted = false; + this.rainTick = 0; + this.lastDir = 'down'; + // 进入新场景 = 上一场景的切换过场结束(床/门淡出期间置位,抑制庆典) + director.sceneTransition = false; + + const map = this.make.tilemap({ key: `map_${this.mapKey}` }); + const tsName = map.tilesets.length > 0 ? map.tilesets[0].name : 'candy'; + const tiles = map.addTilesetImage(tsName, 'tileset'); + // 小地图(小于视口)居中:层与对象统一偏移 + const ox = Math.max(0, Math.floor((GAME_W - map.widthInPixels) / 2)); + const oy = Math.max(0, Math.floor((GAME_H - map.heightInPixels) / 2)); + const ground = tiles ? map.createLayer('ground', tiles, ox, oy) : null; + const obstacles = tiles ? map.createLayer('obstacles', tiles, ox, oy) : null; + if (!obstacles) throw new Error(`map ${this.mapKey} missing obstacles layer`); + this.obstacleLayer = obstacles; + this.groundLayer = ground ?? undefined; + this.obstacleLayer.setCollisionByExclusion([-1]); // obstacles 层整层碰撞 + this.obstacleLayer.setCollision([29, 30, 31], false); // 路灯/庆典彩带(gid 29-31)仅装饰,可穿过 + + // 对象解析 + const objLayer = map.getObjectLayer('objects'); + const objects = (objLayer?.objects ?? []) as unknown as TiledObj[]; + + this.interactZones = []; + this.doors = []; + this.armedDoors.clear(); + this.spentTargets.clear(); + let spawnX = 16; + let spawnY = 16; + const wantSpawn = data?.spawn ?? 'default'; + let fallbackSpawn: { x: number; y: number } | null = null; + + for (const obj of objects) { + const p = propsOf(obj); + const objX = obj.x + ox; + const objY = obj.y + oy; + if (obj.name === 'spawn') { + const id = strProp(p, 'id'); + if (id === 'default') fallbackSpawn = { x: objX, y: objY }; + if (id === wantSpawn) { spawnX = objX; spawnY = objY; } + } else if (obj.name === 'door') { + this.doors.push({ x: objX, y: objY, target: strProp(p, 'target'), spawn: strProp(p, 'spawn') }); + } else if (obj.name === 'npc') { + const id = strProp(p, 'id') as NpcId; + if (NPC_DEFS[id]) this.actors.push(new NpcActor(this, id, objX, objY)); + } else if (obj.name === 'interact') { + this.interactZones.push({ id: strProp(p, 'id'), x: objX, y: objY }); + } + } + if (wantSpawn === 'default' && fallbackSpawn) { + spawnX = fallbackSpawn.x; spawnY = fallbackSpawn.y; + } + this.interactZones.push(...this.extraInteractZones()); + + // 玩家 + this.player = this.physics.add.sprite(spawnX, spawnY, 'player', 0); + this.player.setOrigin(0.5, 1); + this.player.setSize(10, 8).setOffset(3, 16); + this.player.setCollideWorldBounds(true); + this.physics.world.setBounds(ox, oy, map.widthInPixels, map.heightInPixels); + this.physics.add.collider(this.player, this.obstacleLayer); + + // 相机:跟随 + 钳制(小地图按视口尺寸钳制 → 居中不滚动) + const cam = this.cameras.main; + cam.setBounds(0, 0, Math.max(map.widthInPixels, GAME_W), Math.max(map.heightInPixels, GAME_H)); + cam.setZoom(1); // 场景实例复用,重置 T14 的拉近 + cam.startFollow(this.player, true, 0.15, 0.15); + cam.roundPixels = true; + cam.fadeIn(250, 0, 0, 0); + + // 崩坏 postFX(场景重进时先清掉旧的) + cam.clearFX(); + this.colorMatrix = cam.postFX.addColorMatrix(); + this.vignetteFx = cam.postFX.addVignette(0.5, 0.5, 0.9, 0); + this.applyParams(director.params); + + // 输入 + this.cursors = this.input.keyboard!.createCursorKeys(); + this.wasd = this.input.keyboard!.addKeys('W,A,S,D') as Record; + this.keyE = this.input.keyboard!.addKey('E'); + + // 雨图层(屏幕空间) + this.rainGraphics = this.add.graphics().setScrollFactor(0).setDepth(9990); + + // UIScene 常驻 + if (!this.scene.isActive('UIScene')) this.scene.launch('UIScene'); + + // 事件注册 + this.onDir('stage', (s: Stage, p: StageParams) => { + this.applyParams(p); + void s; + // 音乐变调时双子鸟动画同步卡帧 + const bird = this.actors.find((a) => a.def.id === 'bird'); + if (bird && bird.sprite.anims.isPlaying) { + bird.sprite.anims.pause(); + this.time.delayedCall(180, () => bird.sprite.anims.resume()); + } + }); + this.onDir('negativeFlash', () => { this.negativeFrames = 1; }); + this.onDir('firstFull', async () => { + this.cameras.main.flash(1400, 255, 255, 245); + await this.say({ lines: [NARRATOR.firstFull] }); + }); + this.onDir('dusk', () => { + if (this.duskAnnouncedDay === director.day) return; + this.duskAnnouncedDay = director.day; + audio.bell(); + // D6 傍晚脚本雨(即使 T14 未完成,黄昏也下雨) + if (director.day === 6) director.setRain(true); + // 等当前对话结束后再播黄昏提示,避免互相覆盖 + const tryShow = (): void => { + if (!director.dialogueActive) void this.say({ lines: [NARRATOR.duskBell] }); + else this.time.delayedCall(500, tryShow); + }; + tryShow(); + }); + this.onDir('npcGlance', () => this.glanceNearest()); + this.onDir('celebration', (phase: string) => { + if (phase === 'start') void this.beginCelebration(); + }); + + this.events.once('shutdown', () => { + for (const [ev, fn] of this.listeners) director.off(ev, fn); + this.listeners = []; + }); + + director.emit('prompt', null); // 清掉上一场景残留的「按 E」 + this.onReady(data ?? {}); + } + + protected onDir(event: string, fn: AnyFn): void { + director.on(event, fn); + this.listeners.push([event, fn]); + } + + // ---------- 视听参数(§6.4 矩阵) ---------- + private applyParams(p: StageParams): void { + if (!this.colorMatrix) return; + this.colorMatrix.reset(); + this.colorMatrix.saturate(p.saturation); + this.vignetteFx.strength = p.vignette; + this.vignetteFx.radius = p.vignette > 0.4 ? 0.65 : 0.9; + } + + // ---------- 每帧 ---------- + update(time: number, delta: number): void { + director.tick(delta); + + // 负片单帧 + if (this.negativeFrames > 0) { + this.negativeFrames -= 1; + this.colorMatrix.reset(); + this.colorMatrix.negative(); + this.time.delayedCall(40, () => this.applyParams(director.params)); + } + + // S4:群演 1 只停摆 + if (director.stage === 4) { + const extras = this.actors.filter((a) => a.isExtra); + if (extras.length > 0 && !extras.some((a) => a.stalled)) { + const pick = pickStallExtra(this.actors); + if (pick) pick.stalled = true; + } + } + + if (!this.transitioning) { + this.handleMovement(delta); + this.handleIdleSadness(delta); + this.handleInteraction(); + void this.handleDoors(); + } + + for (const a of this.actors) a.update(delta, this.player); + this.updateRain(delta); + this.player.setDepth(this.player.y); + this.extraUpdate(time, delta); + } + + private handleMovement(_delta: number): void { + if (director.dialogueActive) { + this.player.setVelocity(0); + this.player.anims.stop(); + this.player.setFrame(this.idleFrame()); + return; + } + let vx = 0; + let vy = 0; + const k = this.wasd; + if (this.cursors.left.isDown || k.A.isDown) vx -= 1; + if (this.cursors.right.isDown || k.D.isDown) vx += 1; + if (this.cursors.up.isDown || k.W.isDown) vy -= 1; + if (this.cursors.down.isDown || k.S.isDown) vy += 1; + + if (vx === 0 && vy === 0) { + this.player.setVelocity(0); + this.player.anims.stop(); + this.player.setFrame(this.idleFrame()); + return; + } + const len = Math.hypot(vx, vy); + this.player.setVelocity((vx / len) * MOVE_SPEED, (vy / len) * MOVE_SPEED); + + let dir: 'down' | 'up' | 'left' | 'right'; + if (Math.abs(vx) > Math.abs(vy)) dir = vx < 0 ? 'left' : 'right'; + else dir = vy < 0 ? 'up' : 'down'; + this.lastDir = dir; + const anim = `walk-${dir}`; + if (this.anims.exists(anim)) this.player.play(anim, true); + } + + private idleFrame(): number { + return { down: 0, up: 4, left: 8, right: 12 }[this.lastDir]; + } + + /** 发呆:连续 10 秒不移动且不在任务点 → 悲伤行为(白色层除外) */ + private handleIdleSadness(delta: number): void { + if (this.mapKey === 'white') return; + const moving = (this.player.body?.velocity.lengthSq() ?? 0) > 1; + if (moving) { + this.idleMs = 0; + this.idleArmed = true; + return; + } + if (director.dialogueActive || director.celebrationActive || director.t14Active) { + this.idleMs = 0; + return; + } + if (this.nearAnyPoint(40)) { + this.idleMs = 0; // 在任务点/交互点旁不算发呆 + return; + } + if (!this.idleArmed) return; + this.idleMs += delta; + if (this.idleMs >= 10_000) { + this.idleArmed = false; + director.recordSadness('idle'); + } + } + + private nearAnyPoint(range: number): boolean { + const px = this.player.x; + const py = this.player.y - 8; + for (const z of this.interactZones) { + if (Phaser.Math.Distance.Between(px, py, z.x, z.y) <= range) return true; + } + for (const d of this.doors) { + if (Phaser.Math.Distance.Between(px, py, d.x, d.y) <= range) return true; + } + for (const a of this.actors) { + if (Phaser.Math.Distance.Between(px, py, a.sprite.x, a.sprite.y - 8) <= range) return true; + } + return false; + } + + // ---------- 交互 ---------- + private handleInteraction(): void { + if (director.dialogueActive || director.celebrationActive || director.sceneTransition) { + this.setPrompt(null); + return; + } + const px = this.player.x; + const py = this.player.y - 8; + let best: { kind: 'interact' | 'npc'; id: string; dist: number } | null = null; + + for (const z of this.interactZones) { + const d = Phaser.Math.Distance.Between(px, py, z.x, z.y); + if (d > 30) this.spentTargets.delete(`interact:${z.id}`); // 离开感应圈 → 重武装 + else if (d <= INTERACT_RANGE && !this.spentTargets.has(`interact:${z.id}`) && (!best || d < best.dist)) best = { kind: 'interact', id: z.id, dist: d }; + } + for (const a of this.actors) { + const d = Phaser.Math.Distance.Between(px, py, a.sprite.x, a.sprite.y - 8); + if (d > 30) this.spentTargets.delete(`npc:${a.def.id}`); + else if (d <= INTERACT_RANGE && !this.spentTargets.has(`npc:${a.def.id}`) && (!best || d < best.dist)) best = { kind: 'npc', id: a.def.id, dist: d }; + } + + if (!best) { + this.setPrompt(null); + return; + } + this.setPrompt('按 E'); + // 同一帧内 UIScene 先用 E 关掉对话时,这次按下不得再触发新交互(250ms 冷却) + if (Phaser.Input.Keyboard.JustDown(this.keyE) && Date.now() - director.dialogueClosedAt > 250) { + this.spentTargets.add(`${best.kind}:${best.id}`); // 触发后须离开感应圈才可再触发 + if (best.kind === 'interact') void this.activateInteract(best.id); + else void this.talkTo(best.id as NpcId); + } + } + + private setPrompt(text: string | null): void { + if (text === this.lastPrompt) return; + this.lastPrompt = text; + director.emit('prompt', text); + } + + private async activateInteract(id: string): Promise { + // 通用任务交互 + if (id.startsWith('flowerbed_')) { + await this.waterFlowerbed(id); + return; + } + if (id === 'water_point') { + if (director.smallInteraction('water')) { + await this.say({ lines: ['你浇了浇花。水珠亮晶晶的。'] }); + } else { + await this.say({ lines: ['今天的浇水已经够多了。花儿说谢谢。'] }); + } + return; + } + if (id.startsWith('berry_')) { + await this.pickBerry(id); + return; + } + // 子类专属 + const handled = await this.onInteract(id); + if (!handled) { + await this.say({ lines: ['……'] }); + } + } + + // ---------- 门 ---------- + private async handleDoors(): Promise { + if (this.transitioning || director.dialogueActive || director.sceneTransition) return; + const px = this.player.x; + const py = this.player.y - 8; + for (const d of this.doors) { + const dist = Phaser.Math.Distance.Between(px, py, d.x, d.y); + if (dist > 18) this.armedDoors.add(d); // 离开感应圈后才武装 + if (!this.armedDoors.has(d)) continue; + if (dist <= 14) { + // 先消耗武装再 await:对话关闭的同一帧内推离微任务尚未执行, + // 若不消耗武装,update 会在玩家仍位于圈内时再触发一次(无限劝离循环的根因) + this.armedDoors.delete(d); + const ok = await this.canUseDoor(d.target); + if (!ok) { + // 劝离:沿"门→玩家"方向把玩家推出感应圈(原固定 +y 会把从上方接近的玩家推回门圈,造成劝离对话自循环) + let nx = px - d.x; + let ny = py - d.y; + const len = Math.hypot(nx, ny); + if (len < 1e-6) { + nx = 0; + ny = 1; + } else { + nx /= len; + ny /= len; + } + // 测量点是 player.y-8,setPosition 时 y 补回 8 + this.player.setPosition(d.x + nx * 24, d.y + ny * 24 + 8); + return; + } + if (director.celebrationActive) { + director.escapeCelebration(); + await this.say({ lines: [...NARRATOR.celebrationEscape] }); + } + this.transitioning = true; + this.setPrompt(null); + // 标记场景切换过场:抑制淡出期间 tick 触发庆典(庆典对话会被切场景覆盖,导致永久卡死) + director.sceneTransition = true; + this.cameras.main.fadeOut(250, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + const sceneKey = MAP_SCENES[d.target]; + if (sceneKey) this.scene.start(sceneKey, { spawn: d.spawn || 'default' }); + else this.transitioning = false; + }); + return; + } + } + } + + // ---------- 对话(通用) ---------- + protected async say(payload: DialoguePayload): Promise { + audio.beep(); + return director.showDialogue(payload); + } + + protected async sayNpc(id: NpcId, lines: string[], choices?: DialogueChoice[]): Promise { + return this.say({ speakerId: id, lines, choices }); + } + + /** 与 NPC 对话(含任务分流、问好计数、纠缠登记) */ + protected async talkTo(id: NpcId): Promise { + const def = NPC_DEFS[id]; + + // 群演:摸头小互动(无对话) + if (def.isExtra) { + if (director.smallInteraction('pat')) { + await this.say({ lines: [`你摸了摸${def.name}的头。毛茸茸的。`] }); + } else { + await this.say({ lines: [`${def.name}打了个哈欠。`] }); + } + return; + } + + // 双子鸟:喂食小互动 + if (id === 'bird') { + if (director.smallInteraction('feed')) { + await this.sayNpc('bird', ['啾啾!啾——(吃得很开心)']); + } else { + await this.sayNpc('bird', ['啾啾——啾啾——']); + } + return; + } + + director.registerTalk(id); + + // 任务分流 + if (await this.taskTalk(id)) return; + + // C9:龟爷爷对话链(D2/D4/D6 递进) + if (id === 'guiyeye' && [2, 4, 6].includes(director.day)) { + await this.sayNpc('guiyeye', npcLinesOf('guiyeye', director.day), STANDARD_CHOICES).then((c) => this.afterChoice(id, c)); + director.addClue('C9'); + this.countGreet(id); + return; + } + + // 默认闲聊 + 三选项 + const choice = await this.sayNpc(id, npcLinesOf(id, director.day), STANDARD_CHOICES); + await this.afterChoice(id, choice); + this.countGreet(id); // T3/T10:与任意三位村民问好 + } + + private async afterChoice(id: NpcId, choice: number | null): Promise { + if (choice === null) return; + const kind = STANDARD_CHOICES[choice]?.kind; + if (!kind) return; + director.applyChoice(kind); + if (kind === 'gentle' || kind === 'hesitant' || kind === 'deny') { + await this.sayNpc(id, [CHOICE_REPLIES[kind]]); + } + } + + /** 任务相关对话分流;返回 true 表示已处理 */ + private async taskTalk(id: NpcId): Promise { + const day = director.day; + + // T1/T8:送面包 + const breadTask = day === 1 ? 'T1' : day === 4 ? 'T8' : null; + if (breadTask && director.isTaskActive(breadTask)) { + // 从圆圆处取蜂蜜面包(与圆圆对话即可取,不必找到隐形柜台区) + if (id === 'yuanyuan' && !director.taskProgress['breadTaken']) { + director.taskProgress['breadTaken'] = 1; + await this.sayNpc('yuanyuan', TAKE_BREAD_LINES); + this.countGreet(id); + return true; + } + if (director.taskProgress['breadTaken'] && id in BREAD_RECEIVERS) { + const done = director.taskSetProgress['breadTo'] ?? new Set(); + if (done.has(id)) { + await this.sayNpc(id, ['面包已经收到啦,谢谢你!']); + return true; + } + done.add(id); + director.taskSetProgress['breadTo'] = done; + await this.sayNpc(id, BREAD_RECEIVERS[id]); + this.countGreet(id); + if (done.size >= 3) { + director.completeTask(breadTask); + audio.ding(); + await this.say({ lines: ['面包都送到了。大家很幸福。'] }); + } + return true; + } + } + + // T4:送信 + if (day === 2 && director.isTaskActive('T4')) { + if (id === 'dabao' && !director.taskProgress['letters']) { + director.taskProgress['letters'] = 3; + await this.sayNpc('dabao', TAKE_LETTERS_LINES); + this.countGreet(id); + return true; + } + if (director.taskProgress['letters'] && id in LETTER_RECEIVERS) { + const done = director.taskSetProgress['lettersTo'] ?? new Set(); + if (done.has(id)) { + await this.sayNpc(id, ['信已经收到啦。']); + return true; + } + done.add(id); + director.taskSetProgress['lettersTo'] = done; + await this.sayNpc(id, LETTER_RECEIVERS[id]); + this.countGreet(id); + if (done.size >= 3) { + // 第 3 封信署名乱码 0.2 秒(C4) + director.emit('flashText', LETTER_GLITCH_TEXT, 200); + await this.wait(400); + director.addClue('C4'); + director.completeTask('T4'); // 先结算任务,C4 文本随后展示,避免任务态依赖对话关闭 + audio.ding(); + await this.say({ lines: CLUES.C4.lines }); + } + return true; + } + } + + // T14/T15:与阿福对话指引任务地点(站立点/庆典场地无视觉标记) + if (id === 'afu' && director.isTaskActive('T14')) { + await this.sayNpc('afu', T14_HINT); + return true; + } + if (id === 'afu' && director.isTaskActive('T15')) { + await this.sayNpc('afu', T15_HINT); + return true; + } + + // T5:帮咪咪清点货架(与咪咪对话即可,不必找到隐形货架区) + if (id === 'mimi' && director.isTaskActive('T5')) { + await this.say({ lines: SHELF_COUNT_LINES }); + director.addClue('C5'); // 清点中发现:同一商品重复 9 格、标签空白 + await this.sayNpc('mimi', SHELF_COUNT_DONE); + director.completeTask('T5'); + audio.ding(); + this.countGreet(id); + return true; + } + + // T11:帮圆圆揉面(可拒绝) + if (day === 5 && id === 'yuanyuan' && director.isTaskActive('T11')) { + const c = await this.sayNpc('yuanyuan', KNEAD_ASK, [ + { label: '(接过面团)', kind: 'accept' }, + { label: '(摇头拒绝)', kind: 'refuse' }, + ]); + if (c === 0) { + await this.sayNpc('yuanyuan', KNEAD_ACCEPT); + director.completeTask('T11'); + audio.ding(); + } else { + await this.sayNpc('yuanyuan', KNEAD_REFUSE); + director.refuseTask('T11'); + } + this.countGreet(id); + return true; + } + + return false; + } + + /** T3/T10:与任意三位村民问好(去重计数) */ + protected countGreet(id: NpcId): void { + const greetTask = director.day === 1 ? 'T3' : director.day === 4 ? 'T10' : null; + if (!greetTask || !director.isTaskActive(greetTask)) return; + const done = director.taskSetProgress['greeted'] ?? new Set(); + if (done.has(id)) return; + done.add(id); + director.taskSetProgress['greeted'] = done; + if (done.size >= 3) { + director.completeTask(greetTask); + audio.ding(); + void this.say({ lines: ['大家都很热情。今天也是美好的一天。'] }); + } + } + + // T2/T9:给广场花坛浇水 ×3 + private async waterFlowerbed(id: string): Promise { + const task = director.day === 1 ? 'T2' : director.day === 4 ? 'T9' : null; + if (!task || !director.isTaskActive(task)) { + await this.say({ lines: ['花儿开得很好。'] }); + return; + } + const done = director.taskSetProgress['watered'] ?? new Set(); + if (done.has(id)) { + await this.say({ lines: WATER_FLOWER_DONE }); + return; + } + done.add(id); + director.taskSetProgress['watered'] = done; + await this.say({ lines: WATER_FLOWER_LINES }); + if (done.size >= 3) { + director.completeTask(task); + audio.ding(); + } + } + + // T6:去花田采莓果 ×5 + private async pickBerry(id: string): Promise { + if (!director.isTaskActive('T6')) { + await this.say({ lines: ['莓果还没熟透。'] }); + return; + } + const done = director.taskSetProgress['berries'] ?? new Set(); + if (done.has(id)) { + await this.say({ lines: BERRY_EMPTY_LINES }); + return; + } + done.add(id); + director.taskSetProgress['berries'] = done; + // 莓果丛(满) id16 → (空) id17 + const z = this.interactZones.find((zz) => zz.id === id); + if (z) this.swapTileAt(z.x, z.y, 17, 18); + await this.say({ lines: BERRY_PICK_LINES }); + if (done.size >= 5) { + director.completeTask('T6'); + audio.ding(); + } + } + + /** 把 (x,y) 附近 3×3 范围内第一个 fromGid 瓦片换成 toGid(莓果丛满→空等) */ + protected swapTileAt(x: number, y: number, fromGid: number, toGid: number): void { + const layers = [this.obstacleLayer, this.groundLayer].filter((l): l is Phaser.Tilemaps.TilemapLayer => !!l); + for (const layer of layers) { + for (let dy = -16; dy <= 16; dy += 8) { + for (let dx = -16; dx <= 16; dx += 8) { + const tile = layer.getTileAtWorldXY(x + dx, y + dy, true); + if (tile && tile.index === fromGid) { + tile.index = toGid; + return; + } + } + } + } + } + + // ---------- 公告板(广场共用) ---------- + protected async showNoticeBoard(): Promise { + const lines: string[] = [announcementOf(director.day), '今日幸福任务:']; + for (const t of tasksOfDay(director.day)) { + const done = director.tasksDone.has(t.id); + const refused = director.refusedTasks.has(t.id); + lines.push(`${done ? '・(完成)' : refused ? '・(已拒绝)' : '・ '}${t.title}`); + } + await this.say({ speakerId: 'afu', lines }); + } + + // ---------- 欢乐庆典 ---------- + private async beginCelebration(): Promise { + if (!this.scene.isActive()) return; + await this.say({ lines: [...NARRATOR.celebrationStart] }); + // 15 秒内不逃离 → 强制拉回 50 + this.time.delayedCall(15_000, async () => { + if (!director.celebrationActive) return; + director.completeCelebration(); + await this.say({ lines: [...NARRATOR.celebrationEnd] }); + }); + } + + private glanceNearest(): void { + let best: NpcActor | null = null; + let bestD = Infinity; + for (const a of this.actors) { + if (a.isExtra) continue; + const d = Phaser.Math.Distance.Between(this.player.x, this.player.y, a.sprite.x, a.sprite.y); + if (d < bestD) { bestD = d; best = a; } + } + if (best && bestD < 200) best.glanceAt(this.player.x); + } + + // ---------- 脚本雨(D6 傍晚) ---------- + private updateRain(delta: number): void { + const g = this.rainGraphics; + if (!director.rainActive || !OUTDOOR_MAPS.includes(this.mapKey)) { + g.clear(); + return; + } + g.clear(); + g.lineStyle(1, 0x8fd3ff, 0.55); + this.rainTick += delta; + const t = this.rainTick; + for (let i = 0; i < 70; i++) { + const x = (i * 53 + (t * 0.06)) % GAME_W; + const y = (i * 97 + (t * 0.35)) % GAME_H; + g.lineBetween(x, y, x - 1, y + 5); + } + // 站雨中 ≥10 秒计悲伤行为 + if (!this.rainCounted) { + this.rainMs += delta; + if (this.rainMs >= 10_000) { + this.rainCounted = true; + director.recordSadness('rain'); + } + } + } + + protected wait(ms: number): Promise { + return new Promise((resolve) => this.time.delayedCall(ms, resolve)); + } + + protected getPlayer(): Phaser.Physics.Arcade.Sprite { + return this.player; + } +} diff --git a/Kimi-K3/src/scenes/PlazaScene.ts b/Kimi-K3/src/scenes/PlazaScene.ts new file mode 100644 index 0000000..3b1f352 --- /dev/null +++ b/Kimi-K3/src/scenes/PlazaScene.ts @@ -0,0 +1,214 @@ +// 村广场(30×20):公告板、花坛、双子鸟树、庆典场地。 +// 代码补充交互区:C3 邮筒信件、T13 彩带 ×4、T14 站立点、T15 庆典场地。 +// D6 T14 保持微笑 60 秒;D6 傍晚脚本雨;D7 结局分歧;雾墙门 D5 前劝离。 + +import { MapScene, type InteractZone } from './MapScene'; +import { director } from '../core/CorruptionDirector'; +import { audio } from '../audio/AudioEngine'; +import { CLUES } from '../data/clues'; +import { + MAILBOX_BEFORE, WIPE_ASK, WIPE_ACCEPT, WIPE_REFUSE, + RIBBON_LINES, RIBBON_DONE, T14_INTRO, T14_MOVE_WARN, T14_DONE, + T15_INTRO, FOG_DISSUADE, NARRATOR, +} from '../data/dialogues'; + +export class PlazaScene extends MapScene { + readonly mapKey = 'plaza'; + + constructor() { + super('PlazaScene'); + } + + private t14State: 'idle' | 'running' = 'idle'; + private t14Ms = 0; + private t14Faded = false; + private t14WarnCd = 0; + + protected onReady(): void { + this.t14State = 'idle'; + this.t14Ms = 0; + this.t14Faded = false; + this.t14WarnCd = 0; + } + + protected extraInteractZones(): InteractZone[] { + const nb = this.interactZones.find((z) => z.id === 'noticeboard') ?? { id: '', x: 240, y: 160 }; + return [ + // C3:邮筒旁散落信件(邮筒瓦片 46 在公告板附近,代码补不可见交互区) + { id: 'mailbox_c3', x: nb.x - 48, y: nb.y + 24 }, + // T13:在广场挂彩带 ×4(对齐彩带瓦片中心 (11,7)/(18,7)/(11,13)/(18,13)) + { id: 'ribbon_1', x: nb.x - 48, y: nb.y - 32 }, + { id: 'ribbon_2', x: nb.x + 64, y: nb.y - 32 }, + { id: 'ribbon_3', x: nb.x - 48, y: nb.y + 64 }, + { id: 'ribbon_4', x: nb.x + 64, y: nb.y + 64 }, + // T14:保持微笑站立点(避开花坛与公告板 24px 交互圈) + { id: 'smile_spot', x: nb.x, y: nb.y + 32 }, + // T15:庆典场地 + { id: 'celebration_site', x: nb.x, y: nb.y - 72 }, + ]; + } + + protected async canUseDoor(target: string): Promise { + if (target === 'fog' && director.day < 5) { + await this.say({ lines: FOG_DISSUADE }); + return false; + } + if (this.t14State === 'running') { + this.cancelT14(); + } + return true; + } + + protected async onInteract(id: string): Promise { + // 公告板:阿福公告(逐字)+ 当日任务;D5 可擦拭(T12,可拒绝) + if (id === 'noticeboard') { + await this.showNoticeBoard(); + if (director.isTaskActive('T12')) { + const c = await this.say({ lines: WIPE_ASK, choices: [ + { label: '(拿起抹布擦拭)', kind: 'accept' }, + { label: '(放下抹布)', kind: 'refuse' }, + ] }); + if (c === 0) { + director.addClue('C6'); // 擦到一半浮现旧字迹 + await this.say({ lines: WIPE_ACCEPT }); + director.completeTask('T12'); + audio.ding(); + } else { + await this.say({ lines: WIPE_REFUSE }); + director.refuseTask('T12'); + } + } + return true; + } + + // C3:邮筒旁散落信件(D2 起) + if (id === 'mailbox_c3') { + if (director.day < 2) { + await this.say({ lines: MAILBOX_BEFORE }); + } else { + director.addClue('C3'); + await this.say({ lines: CLUES.C3.lines }); + } + return true; + } + + // T13:挂彩带 ×4 + if (id.startsWith('ribbon_')) { + if (!director.isTaskActive('T13')) { + await this.say({ lines: ['这里还不需要彩带。'] }); + return true; + } + const done = director.taskSetProgress['ribbons'] ?? new Set(); + if (done.has(id)) { + await this.say({ lines: RIBBON_DONE }); + return true; + } + done.add(id); + director.taskSetProgress['ribbons'] = done; + // 挂上彩带(tiles16 帧 29/30/31 红/黄/蓝;地图上挂点是路灯,挂上后才出现彩带) + const z = this.interactZones.find((zz) => zz.id === id); + if (z) { + const frame = [29, 30, 31, 29][done.size - 1]; // tiles16 帧 = 瓦片 id(彩带红/黄/蓝) + const deco = this.add.image(z.x, z.y, 'tiles16', frame); + deco.setDepth(z.y - 8); + } + await this.say({ lines: RIBBON_LINES }); + if (done.size >= 4) { + director.completeTask('T13'); + audio.ding(); + } + return true; + } + + // T14:保持微笑 60 秒 + if (id === 'smile_spot') { + if (!director.isTaskActive('T14')) { + await this.say({ lines: ['广场中央很暖和。'] }); + return true; + } + if (this.t14State === 'running') return true; + await this.say({ speakerId: 'afu', lines: T14_INTRO }); + this.startT14(); + return true; + } + + // T15:参加第 413 届丰收庆典 → 结局分歧(E4/E2/E1;E3 需走向雾墙) + if (id === 'celebration_site') { + if (director.day !== 7 || !director.isTaskActive('T15')) { + await this.say({ lines: ['庆典场地空空的。等庆典那天再来吧。'] }); + return true; + } + await this.say({ lines: T15_INTRO }); + director.completeTask('T15'); + const ending = director.evaluateEnding(false); + director.startEnding(ending); + this.scene.start('EndingScene', { ending }); + return true; + } + + return false; + } + + // ---------- T14 ---------- + private startT14(): void { + this.t14State = 'running'; + this.t14Ms = 0; + this.t14Faded = false; + director.t14Active = true; + for (const a of this.actors) a.stareAtPlayer = true; // 全村 NPC 转身注视 + this.cameras.main.zoomTo(1.35, 60_000); // 镜头缓慢拉近 + } + + private cancelT14(): void { + this.t14State = 'idle'; + this.t14Ms = 0; + director.t14Active = false; + for (const a of this.actors) a.stareAtPlayer = false; + this.cameras.main.zoomTo(1, 500); + audio.restoreMaster(1000); + } + + private async finishT14(): Promise { + this.t14State = 'idle'; + director.t14Active = false; + for (const a of this.actors) a.stareAtPlayer = false; + this.cameras.main.zoomTo(1, 2000); + audio.restoreMaster(3000); + // 先结算任务(此时 T13+T14 齐全会触发黄昏钟声,钟声对话会等空档播放), + // 避免任务完成依赖玩家关闭后续对话 + director.completeTask('T14'); + audio.ding(); + await this.say({ speakerId: 'afu', lines: T14_DONE }); + // 傍晚脚本雨(村里第一次下雨,NPC 台词对雨零反应) + director.setRain(true); + await this.say({ lines: [NARRATOR.rainStart] }); + } + + protected extraUpdate(_time: number, delta: number): void { + if (this.t14State !== 'running') return; + const moving = (this.player.body?.velocity.lengthSq() ?? 0) > 1; + if (moving || director.dialogueActive) { + if (moving) { + this.t14Ms = 0; // 原地站立:移动则重来 + if (this.t14Faded) { + this.t14Faded = false; + audio.restoreMaster(1000); + } + this.t14WarnCd -= delta; + if (this.t14WarnCd <= 0) { + this.t14WarnCd = 6000; + void this.say({ speakerId: 'afu', lines: T14_MOVE_WARN }); + } + } + return; + } + this.t14Ms += delta; + if (!this.t14Faded && this.t14Ms >= 40_000) { + this.t14Faded = true; + audio.fadeAllToZero(20_000); // 第 40–60 秒所有声音渐隐至零 + } + if (this.t14Ms >= 60_000) { + void this.finishT14(); + } + } +} diff --git a/Kimi-K3/src/scenes/ShopScene.ts b/Kimi-K3/src/scenes/ShopScene.ts new file mode 100644 index 0000000..05070c1 --- /dev/null +++ b/Kimi-K3/src/scenes/ShopScene.ts @@ -0,0 +1,34 @@ +// 杂货店(室内 15×10):货架(T5 清点;C5 同一商品重复 9 格,D2 起) + +import { MapScene } from './MapScene'; +import { director } from '../core/CorruptionDirector'; +import { audio } from '../audio/AudioEngine'; +import { CLUES } from '../data/clues'; +import { SHELF_COUNT_LINES, SHELF_COUNT_DONE } from '../data/dialogues'; + +export class ShopScene extends MapScene { + readonly mapKey = 'shop'; + + constructor() { + super('ShopScene'); + } + + protected async onInteract(id: string): Promise { + if (id === 'shelf_count') { + if (director.isTaskActive('T5')) { + await this.say({ lines: SHELF_COUNT_LINES }); + director.addClue('C5'); // 清点中发现:同一商品重复 9 格、标签空白 + await this.say({ speakerId: 'mimi', lines: SHELF_COUNT_DONE }); + director.completeTask('T5'); + audio.ding(); + } else if (director.day >= 2) { + director.addClue('C5'); + await this.say({ lines: CLUES.C5.lines }); + } else { + await this.say({ lines: ['货架整整齐齐,摆满了罐头。'] }); + } + return true; + } + return false; + } +} diff --git a/Kimi-K3/src/scenes/TitleScene.ts b/Kimi-K3/src/scenes/TitleScene.ts new file mode 100644 index 0000000..d912843 --- /dev/null +++ b/Kimi-K3/src/scenes/TitleScene.ts @@ -0,0 +1,61 @@ +// TitleScene:标题 + 名字输入(留空默认「小满」)。DOM input,不依赖 localStorage。 + +import Phaser from 'phaser'; +import { director } from '../core/CorruptionDirector'; + +export class TitleScene extends Phaser.Scene { + private overlay: HTMLDivElement | null = null; + + constructor() { + super('TitleScene'); + } + + create(): void { + this.cameras.main.setBackgroundColor('#8FD3FF'); + // 糖果色标题卡 + this.add.rectangle(160, 70, 240, 64, 0xfff3d6).setStrokeStyle(2, 0xf5a623); + this.add.text(160, 58, '崩坏童话', { fontFamily: 'monospace', fontSize: '20px', color: '#8B5A2B' }).setOrigin(0.5); + this.add.text(160, 84, '蜜 糖 村', { fontFamily: 'monospace', fontSize: '14px', color: '#F5A623' }).setOrigin(0.5); + this.add.text(160, 120, 'WASD / 方向键移动 · E 互动', { fontFamily: 'monospace', fontSize: '8px', color: '#5c4a2e' }).setOrigin(0.5); + + // DOM 输入框(名字仅存于本次会话内存) + const parent = document.getElementById('game') ?? document.body; + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:absolute;left:50%;top:58%;transform:translateX(-50%);display:flex;gap:6px;align-items:center;z-index:10;'; + const label = document.createElement('span'); + label.textContent = '名字:'; + label.style.cssText = 'color:#FFF3D6;font:12px monospace;'; + const input = document.createElement('input'); + input.type = 'text'; + input.maxLength = 8; + input.placeholder = '小满'; + input.style.cssText = 'width:80px;font:12px monospace;background:#FFF3D6;border:2px solid #F5A623;color:#5c4a2e;outline:none;'; + const btn = document.createElement('button'); + btn.textContent = '开始'; + btn.style.cssText = 'font:12px monospace;background:#F5A623;border:none;color:#fff;padding:2px 8px;cursor:pointer;'; + overlay.append(label, input, btn); + parent.appendChild(overlay); + this.overlay = overlay; + input.focus(); + + const start = (): void => { + const name = input.value.trim() || '小满'; + director.newGame(name); + overlay.remove(); + this.overlay = null; + this.scene.start('HouseScene', { spawn: 'default' }); + }; + btn.addEventListener('click', start); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') start(); + e.stopPropagation(); + }); + + this.events.once('shutdown', () => { + if (this.overlay) { + this.overlay.remove(); + this.overlay = null; + } + }); + } +} diff --git a/Kimi-K3/src/scenes/UIScene.ts b/Kimi-K3/src/scenes/UIScene.ts new file mode 100644 index 0000000..d55d3f1 --- /dev/null +++ b/Kimi-K3/src/scenes/UIScene.ts @@ -0,0 +1,302 @@ +// UIScene:常驻 UI(CONTRACT §4)。 +// 向日葵表盘(12 瓣 Graphics,不显示任何数字;满格花心黑籽)、「按 E」提示、 +// 底部对话框(说话者名 + 头像 + 文本,E 推进,温柔/迟疑/否定三选项)、 +// 文本腐蚀渲染层(§6.5)、C4 乱码闪显。 + +import Phaser from 'phaser'; +import { director } from '../core/CorruptionDirector'; +import { corruptText } from '../core/TextCorruptor'; +import { audio } from '../audio/AudioEngine'; +import { GAME_W, GAME_H, NPC_DEFS, type DialoguePayload, type Stage } from '../types'; + +/** §6.4 UI 列:0–20→0–2 瓣,21–50→3–6,51–80→7–9,81–99→10–12,100→12+黑籽 */ +export function petalCount(h: number): number { + if (h >= 100) return 12; + if (h >= 81) return 10 + Math.min(2, Math.floor((h - 81) / 7)); + if (h >= 51) return 7 + Math.min(2, Math.floor((h - 51) / 11)); + if (h >= 21) return 3 + Math.min(3, Math.floor((h - 21) / 8)); + return Math.min(2, Math.floor(h / 8)); +} + +export class UIScene extends Phaser.Scene { + private dial!: Phaser.GameObjects.Graphics; + private promptText!: Phaser.GameObjects.Text; + private flashTextObj!: Phaser.GameObjects.Text; + /** 表盘当前是否显示黑籽(满格),验收脚本读取 */ + seedsShown = false; + + // 对话 + private dlgBox: Phaser.GameObjects.Container | null = null; + private dlgBody!: Phaser.GameObjects.Text; + private dlgName!: Phaser.GameObjects.Text; + private dlgPortrait!: Phaser.GameObjects.Image; + private dlgChoices: Phaser.GameObjects.Text[] = []; + private payload: DialoguePayload | null = null; + private lineIdx = 0; + private fullLine = ''; + private typing = false; + private typeTimer: Phaser.Time.TimerEvent | null = null; + private choiceIdx = 0; + private showingChoices = false; + private openAt = 0; + + private keys!: { + E: Phaser.Input.Keyboard.Key; + UP: Phaser.Input.Keyboard.Key; + DOWN: Phaser.Input.Keyboard.Key; + W: Phaser.Input.Keyboard.Key; + S: Phaser.Input.Keyboard.Key; + N1: Phaser.Input.Keyboard.Key; + N2: Phaser.Input.Keyboard.Key; + N3: Phaser.Input.Keyboard.Key; + }; + + constructor() { + super('UIScene'); + } + + create(): void { + this.dial = this.add.graphics().setDepth(10); + this.promptText = this.add.text(GAME_W / 2, GAME_H - 14, '', { + fontFamily: 'monospace', fontSize: '8px', color: '#ffffff', + stroke: '#5c4a2e', strokeThickness: 2, + }).setOrigin(0.5).setDepth(10); + + this.flashTextObj = this.add.text(GAME_W / 2, GAME_H / 2, '', { + fontFamily: 'monospace', fontSize: '10px', color: '#ff3366', + stroke: '#000000', strokeThickness: 2, + }).setOrigin(0.5).setDepth(30).setVisible(false); + + this.keys = this.input.keyboard!.addKeys('E,UP,DOWN,W,S') as typeof this.keys; + this.keys.N1 = this.input.keyboard!.addKey('ONE'); + this.keys.N2 = this.input.keyboard!.addKey('TWO'); + this.keys.N3 = this.input.keyboard!.addKey('THREE'); + + // 场景会被 stop/launch(结局、debug),监听器必须在 shutdown 时摘除 + const redraw = (): void => this.drawDial(); + const onPrompt = (text: string | null): void => { this.promptText.setText(text ?? ''); }; + const onDialogue = (p: DialoguePayload): void => this.openDialogue(p); + const onFlash = (text: string, ms: number): void => this.flash(text, ms); + director.on('happiness', redraw); + director.on('stage', redraw); + director.on('day', redraw); + director.on('prompt', onPrompt); + director.on('dialogue', onDialogue); + director.on('flashText', onFlash); + this.events.once('shutdown', () => { + director.off('happiness', redraw); + director.off('stage', redraw); + director.off('day', redraw); + director.off('prompt', onPrompt); + director.off('dialogue', onDialogue); + director.off('flashText', onFlash); + }); + + this.drawDial(); + } + + // ---------- 向日葵表盘(无数字) ---------- + private drawDial(): void { + const g = this.dial; + g.clear(); + const cx = 24; + const cy = 24; + const h = director.happiness; + const lit = petalCount(h); + const bright = director.params.petalBright; + const litColor = bright ? 0xfff3b0 : 0xffd93d; // S3 起花瓣过亮 + const unlitColor = 0x4a3f2a; + + for (let i = 0; i < 12; i++) { + const ang = -Math.PI / 2 + (i * Math.PI) / 6; + const px = cx + Math.cos(ang) * 13; + const py = cy + Math.sin(ang) * 13; + const w = 4; + const len = 7; + // 旋转矩形(花瓣) + const ux = Math.cos(ang); + const uy = Math.sin(ang); + const nx = -uy; + const ny = ux; + const pts = [ + new Phaser.Math.Vector2(px + nx * w / 2, py + ny * w / 2), + new Phaser.Math.Vector2(px - nx * w / 2, py - ny * w / 2), + new Phaser.Math.Vector2(px - nx * w / 2 + ux * len, py - ny * w / 2 + uy * len), + new Phaser.Math.Vector2(px + nx * w / 2 + ux * len, py + ny * w / 2 + uy * len), + ]; + g.fillStyle(i < lit ? litColor : unlitColor, 1); + g.fillPoints(pts, true); + } + // 花心 + g.fillStyle(0x8b5a2b, 1); + g.fillCircle(cx, cy, 7); + // 满格:花心浮现过熟的黑籽 + this.seedsShown = h >= 100; + if (h >= 100) { + g.fillStyle(0x1a1208, 1); + const seeds = [[-3, -2], [2, -3], [0, 1], [-2, 3], [3, 2]]; + for (const [sx, sy] of seeds) g.fillCircle(cx + sx, cy + sy, 1); + } + } + + // ---------- C4 乱码闪显 ---------- + private flash(text: string, ms: number): void { + this.flashTextObj.setText(text).setVisible(true); + this.time.delayedCall(ms, () => this.flashTextObj.setVisible(false)); + } + + // ---------- 对话框 ---------- + private openDialogue(p: DialoguePayload): void { + this.closeDialogueOnly(); + this.payload = p; + this.lineIdx = 0; + this.openAt = this.time.now; + + const boxY = GAME_H - 56; + const c = this.add.container(0, 0).setDepth(20); + const bg = this.add.rectangle(GAME_W / 2, boxY + 28, GAME_W - 8, 52, 0xfff3d6, 0.96) + .setStrokeStyle(2, 0x8b5a2b); + c.add(bg); + + const speaker = p.speakerId; + const def = speaker && speaker !== 'narrator' ? NPC_DEFS[speaker] : null; + + this.dlgPortrait = this.add.image(24, boxY + 28, def?.portraitKey || 'portrait_player_smile') + .setDisplaySize(32, 32); + this.dlgPortrait.setVisible(!!def); + c.add(this.dlgPortrait); + + this.dlgName = this.add.text(46, boxY + 6, def ? def.name : '', { + fontFamily: 'monospace', fontSize: '8px', color: '#8B5A2B', + }); + c.add(this.dlgName); + + this.dlgBody = this.add.text(46, boxY + 17, '', { + fontFamily: 'monospace', fontSize: '8px', color: '#3a2c18', + wordWrap: { width: GAME_W - 62, useAdvancedWrap: true }, + lineSpacing: 3, + }); + c.add(this.dlgBody); + + this.dlgBox = c; + this.showLine(); + } + + private showLine(): void { + if (!this.payload) return; + const raw = this.payload.lines[this.lineIdx] ?? ''; + // 渲染层腐蚀(§6.5) + this.fullLine = corruptText(raw, this.payload.speakerId, director.stage as Stage); + this.typing = true; + this.dlgBody.setText(''); + if (this.fullLine.length === 0) { + this.typing = false; + return; + } + let i = 0; + this.typeTimer?.remove(); + this.typeTimer = this.time.addEvent({ + delay: 28, + repeat: this.fullLine.length - 1, + callback: () => { + i += 1; + this.dlgBody.setText(this.fullLine.slice(0, i)); + if (i % 3 === 0) audio.beep(); + if (i >= this.fullLine.length) this.typing = false; + }, + }); + } + + private finishTyping(): void { + this.typeTimer?.remove(); + this.typeTimer = null; + this.typing = false; + this.dlgBody.setText(this.fullLine); + } + + private advance(): void { + if (!this.payload) return; + if (this.typing) { + this.finishTyping(); + return; + } + audio.beep(); + this.lineIdx += 1; + if (this.lineIdx < this.payload.lines.length) { + this.showLine(); + return; + } + // 文本结束 → 选项或关闭 + if (this.payload.choices && this.payload.choices.length > 0 && !this.showingChoices) { + this.renderChoices(); + return; + } + const result = this.showingChoices ? this.choiceIdx : null; + const chosen = this.payload.choices?.[this.choiceIdx]; + this.closeDialogueOnly(); + director.resolveDialogue(result); + void chosen; + } + + private renderChoices(): void { + if (!this.payload?.choices || !this.dlgBox) return; + this.showingChoices = true; + this.choiceIdx = 0; + this.dlgChoices = []; + const baseY = GAME_H - 56 + 20; + this.payload.choices.forEach((ch, i) => { + const t = this.add.text(58, baseY + i * 10, `${i === 0 ? '▶ ' : '  '}${ch.label}`, { + fontFamily: 'monospace', fontSize: '8px', + color: i === 0 ? '#c0392b' : '#3a2c18', + }); + this.dlgBox!.add(t); + this.dlgChoices.push(t); + }); + this.dlgBody.setVisible(false); + } + + private moveChoice(dir: number): void { + if (!this.showingChoices || !this.payload?.choices) return; + const n = this.payload.choices.length; + this.choiceIdx = (this.choiceIdx + dir + n) % n; + this.dlgChoices.forEach((t, i) => { + t.setText(`${i === this.choiceIdx ? '▶ ' : '  '}${this.payload!.choices![i].label}`); + t.setColor(i === this.choiceIdx ? '#c0392b' : '#3a2c18'); + }); + } + + private closeDialogueOnly(): void { + this.typeTimer?.remove(); + this.typeTimer = null; + if (this.dlgBox) { + this.dlgBox.destroy(true); + this.dlgBox = null; + } + this.dlgChoices = []; + this.showingChoices = false; + this.payload = null; + } + + update(): void { + if (!this.payload) return; + // 打开后 250ms 内忽略 E,避免触发对话的同一按键直接推进 + const justOpened = this.time.now - this.openAt < 250; + + if (this.showingChoices) { + if (Phaser.Input.Keyboard.JustDown(this.keys.UP) || Phaser.Input.Keyboard.JustDown(this.keys.W)) this.moveChoice(-1); + if (Phaser.Input.Keyboard.JustDown(this.keys.DOWN) || Phaser.Input.Keyboard.JustDown(this.keys.S)) this.moveChoice(1); + if (Phaser.Input.Keyboard.JustDown(this.keys.N1)) this.selectChoice(0); + if (Phaser.Input.Keyboard.JustDown(this.keys.N2)) this.selectChoice(1); + if (Phaser.Input.Keyboard.JustDown(this.keys.N3)) this.selectChoice(2); + if (!justOpened && Phaser.Input.Keyboard.JustDown(this.keys.E)) this.advance(); + } else if (!justOpened && Phaser.Input.Keyboard.JustDown(this.keys.E)) { + this.advance(); + } + } + + private selectChoice(i: number): void { + if (!this.payload?.choices || i >= this.payload.choices.length) return; + this.choiceIdx = i; + this.advance(); + } +} diff --git a/Kimi-K3/src/scenes/WellScene.ts b/Kimi-K3/src/scenes/WellScene.ts new file mode 100644 index 0000000..abdb5d9 --- /dev/null +++ b/Kimi-K3/src/scenes/WellScene.ts @@ -0,0 +1,67 @@ +// 古井空地(14×10):古井(C7,D5 起,交互聆听 5 秒;D5 前被 NPC 温柔劝离)。 +// 注视古井 5 秒(每日 1 次 +3 觉知)。 + +import Phaser from 'phaser'; +import { MapScene } from './MapScene'; +import { director } from '../core/CorruptionDirector'; +import { audio } from '../audio/AudioEngine'; +import { CLUES } from '../data/clues'; +import { WELL_DISSUADE, WELL_LISTEN_START } from '../data/dialogues'; + +export class WellScene extends MapScene { + readonly mapKey = 'well'; + + constructor() { + super('WellScene'); + } + + private stareMs = 0; + private listening = false; + + protected onReady(): void { + this.stareMs = 0; + this.listening = false; + } + + protected async onInteract(id: string): Promise { + if (id !== 'ancient_well') return false; + + if (director.day < 5) { + // D5 前:被 NPC 温柔劝离 + await this.say({ lines: WELL_DISSUADE }); + return true; + } + + if (this.listening) return true; + this.listening = true; + await this.say({ lines: WELL_LISTEN_START }); + // 交互 5 秒:井底回声——极轻的监护仪滴声 + for (let i = 0; i < 5; i++) { + audio.ecgSoft(); + await this.wait(1000); + } + director.addClue('C7'); + await this.say({ lines: CLUES.C7.lines }); + this.listening = false; + return true; + } + + protected extraUpdate(_time: number, delta: number): void { + if (director.day < 5 || this.listening) return; + const z = this.interactZones.find((zz) => zz.id === 'ancient_well'); + if (!z) return; + const d = Phaser.Math.Distance.Between(this.player.x, this.player.y - 8, z.x, z.y); + const still = (this.player.body?.velocity.lengthSq() ?? 0) <= 1; + if (d <= 40 && still && !director.dialogueActive) { + this.stareMs += delta; + if (this.stareMs >= 5000) { + this.stareMs = -1e9; // 每日一次由 director 判定;本地只触发一次 + if (director.registerStare('well')) { + void this.say({ lines: ['你盯着井口看了很久。', '井水黑得不像水。'] }); + } + } + } else if (this.stareMs > 0) { + this.stareMs = 0; + } + } +} diff --git a/Kimi-K3/src/scenes/WhiteScene.ts b/Kimi-K3/src/scenes/WhiteScene.ts new file mode 100644 index 0000000..a66eefd --- /dev/null +++ b/Kimi-K3/src/scenes/WhiteScene.ts @@ -0,0 +1,38 @@ +// 白色层(20×14:上半 20×6 走廊 + 左下 10×8 病房)。 +// 仅梦境闪切与 E3 使用。E3 模式:穿过走廊走进病房 → 病历特写(EndingScene)。 + +import Phaser from 'phaser'; +import { MapScene } from './MapScene'; + +export class WhiteScene extends MapScene { + readonly mapKey = 'white'; + + constructor() { + super('WhiteScene'); + } + + private e3 = false; + private wardTriggered = false; + private wardX = 80; + private wardY = 120; + + protected onReady(data: { spawn?: string; e3?: boolean }): void { + this.e3 = !!data?.e3; + this.wardTriggered = false; + // 病房内部(20×14:走廊行 1-5,病房行 7-12 列 1-8,入口缺口在列 4-5 行 6) + this.wardX = 4.5 * 16; + this.wardY = 9 * 16; + } + + protected extraUpdate(): void { + if (!this.e3 || this.wardTriggered) return; + const d = Phaser.Math.Distance.Between(this.player.x, this.player.y, this.wardX, this.wardY); + if (d <= 24) { + this.wardTriggered = true; + this.cameras.main.fadeOut(800, 255, 255, 255); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.scene.start('EndingScene', { ending: 'E3' }); + }); + } + } +} diff --git a/Kimi-K3/src/types.ts b/Kimi-K3/src/types.ts new file mode 100644 index 0000000..ad811e3 --- /dev/null +++ b/Kimi-K3/src/types.ts @@ -0,0 +1,110 @@ +// 共享类型与常量。所有数值/台词的原文在 src/data/ 下,本文件只放结构性定义。 + +export type Stage = 0 | 1 | 2 | 3 | 4; + +/** §6.4 侵蚀阶段矩阵:视听参数唯一真源表 */ +export interface StageParams { + saturation: number; // ColorMatrix 饱和度 + vignette: number; // 晕影强度 0-1 + playbackRate: number; // BGM 速率 + lowpass: number; // 低通截止 Hz + birdMuted: boolean; // 鸟鸣声部静音 + textLevel: 0 | 1 | 2 | 3; // 0 无 / 1 标点重复(1/80) / 2 同音错字(1/50) / 3 加密(1/10) + negativePerMin: number; // 随机单帧负片频率(次/分钟) + petalBright: boolean; // 花瓣过亮 +} + +export const STAGE_PARAMS: Record = { + 0: { saturation: 0.90, vignette: 0.00, playbackRate: 1.00, lowpass: 22050, birdMuted: false, textLevel: 0, negativePerMin: 0.0, petalBright: false }, + 1: { saturation: 1.00, vignette: 0.00, playbackRate: 1.00, lowpass: 22050, birdMuted: false, textLevel: 0, negativePerMin: 0.0, petalBright: false }, + 2: { saturation: 1.15, vignette: 0.00, playbackRate: 0.97, lowpass: 22050, birdMuted: false, textLevel: 1, negativePerMin: 0.0, petalBright: false }, + 3: { saturation: 1.30, vignette: 0.30, playbackRate: 0.92, lowpass: 8000, birdMuted: false, textLevel: 2, negativePerMin: 0.0, petalBright: true }, + 4: { saturation: 1.40, vignette: 0.60, playbackRate: 0.84, lowpass: 4000, birdMuted: true, textLevel: 3, negativePerMin: 0.4, petalBright: true }, +}; + +/** 由幸福值映射阶段(§6.4) */ +export function stageOf(happiness: number): Stage { + if (happiness >= 100) return 4; + if (happiness >= 81) return 3; + if (happiness >= 51) return 2; + if (happiness >= 21) return 1; + return 0; +} + +export type NpcId = + | 'afu' | 'yuanyuan' | 'dabao' | 'mimi' | 'fuyisheng' | 'guiyeye' | 'bird' + | 'squirrel' | 'hedgehog' | 'fox' | 'pig' | 'chick'; + +export interface NpcDef { + id: NpcId; + name: string; // 对话框说话者名 + spriteKey: string; // spritesheet key + portraitKey: string; // 头像 key(群演无) + offset: number; // §4 阈值偏移(作用于幸福阈值) + expressive: boolean; // 是否参与侵蚀表情(芙医生/龟爷爷/群演不参与) + isExtra: boolean; // 群演 +} + +export const NPC_DEFS: Record = { + afu: { id: 'afu', name: '阿福', spriteKey: 'npc_afu', portraitKey: 'portrait_afu', offset: 0, expressive: true, isExtra: false }, + yuanyuan: { id: 'yuanyuan', name: '圆圆', spriteKey: 'npc_yuanyuan', portraitKey: 'portrait_yuanyuan', offset: 15, expressive: true, isExtra: false }, + dabao: { id: 'dabao', name: '大宝', spriteKey: 'npc_dabao', portraitKey: 'portrait_dabao', offset: 0, expressive: true, isExtra: false }, + mimi: { id: 'mimi', name: '咪咪', spriteKey: 'npc_mimi', portraitKey: 'portrait_mimi', offset: -10, expressive: true, isExtra: false }, + fuyisheng: { id: 'fuyisheng', name: '芙医生', spriteKey: 'npc_fuyisheng', portraitKey: 'portrait_fuyisheng', offset: 0, expressive: false, isExtra: false }, + guiyeye: { id: 'guiyeye', name: '龟爷爷', spriteKey: 'npc_guiyeye', portraitKey: 'portrait_guiyeye', offset: 0, expressive: false, isExtra: false }, + bird: { id: 'bird', name: '双子鸟', spriteKey: 'npc_bird', portraitKey: 'portrait_bird', offset: 0, expressive: false, isExtra: false }, + squirrel: { id: 'squirrel', name: '松鼠', spriteKey: 'extra_squirrel', portraitKey: '', offset: 0, expressive: false, isExtra: true }, + hedgehog: { id: 'hedgehog', name: '刺猬', spriteKey: 'extra_hedgehog', portraitKey: '', offset: 0, expressive: false, isExtra: true }, + fox: { id: 'fox', name: '狐狸', spriteKey: 'extra_fox', portraitKey: '', offset: 0, expressive: false, isExtra: true }, + pig: { id: 'pig', name: '小猪', spriteKey: 'extra_pig', portraitKey: '', offset: 0, expressive: false, isExtra: true }, + chick: { id: 'chick', name: '小鸡', spriteKey: 'extra_chick', portraitKey: '', offset: 0, expressive: false, isExtra: true }, +}; + +export const NAMED_NPCS: NpcId[] = ['afu', 'yuanyuan', 'dabao', 'mimi', 'fuyisheng', 'guiyeye', 'bird']; +export const EXTRA_NPCS: NpcId[] = ['squirrel', 'hedgehog', 'fox', 'pig', 'chick']; + +/** 地图 key → 场景 key */ +export const MAP_SCENES: Record = { + house: 'HouseScene', + plaza: 'PlazaScene', + bakery: 'BakeryScene', + shop: 'ShopScene', + clinic: 'ClinicScene', + lake: 'LakeScene', + field: 'FieldScene', + well: 'WellScene', + fog: 'FogScene', + white: 'WhiteScene', +}; + +export const OUTDOOR_MAPS = ['plaza', 'lake', 'field', 'well', 'fog']; + +/** 对话选项种类(§12:温柔/迟疑/否定;accept/refuse 用于可拒绝任务) */ +export type ChoiceKind = 'gentle' | 'hesitant' | 'deny' | 'accept' | 'refuse'; + +export interface DialogueChoice { + label: string; + kind: ChoiceKind; +} + +export interface DialoguePayload { + speakerId?: NpcId | 'narrator'; + lines: string[]; + choices?: DialogueChoice[]; +} + +export type SadnessKind = 'refuse' | 'deny' | 'idle' | 'cling' | 'rain'; +export type SmallInteractionKind = 'water' | 'feed' | 'pat'; +export type EndingId = 'E1' | 'E2' | 'E3' | 'E4'; + +/** 标准三选项 */ +export const STANDARD_CHOICES: DialogueChoice[] = [ + { label: '(温柔地回应)', kind: 'gentle' }, + { label: '(迟疑)', kind: 'hesitant' }, + { label: '(否定)', kind: 'deny' }, +]; + +export const GAME_W = 320; +export const GAME_H = 180; +export const MOVE_SPEED = 96; +export const INTERACT_RANGE = 24; diff --git a/Kimi-K3/tools/acceptance.mjs b/Kimi-K3/tools/acceptance.mjs new file mode 100644 index 0000000..a204bb1 --- /dev/null +++ b/Kimi-K3/tools/acceptance.mjs @@ -0,0 +1,767 @@ +// 验收测试:PRD §14 共 20 条。用法:先 npm run build && npm run preview,再 node tools/acceptance.mjs +// 需要本机 Chrome(puppeteer-core 驱动,无额外下载)。 + +import puppeteer from 'puppeteer-core'; +import fs from 'node:fs'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; + +const BASE = 'http://127.0.0.1:4815/'; +const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; +const SHOTS = 'tools/shots'; +fs.mkdirSync(SHOTS, { recursive: true }); + +// 自起 preview 服务器(退出时关闭) +const preview = spawn('npx', ['vite', 'preview', '--host', '127.0.0.1', '--port', '4815', '--strictPort'], { stdio: 'ignore' }); +process.on('exit', () => { try { preview.kill(); } catch {} }); +async function waitPort(port, timeout = 15000) { + const t0 = Date.now(); + for (;;) { + const ok = await new Promise((res) => { const s = net.connect(port, '127.0.0.1'); s.once('connect', () => { s.end(); res(true); }); s.once('error', () => res(false)); }); + if (ok) return; + if (Date.now() - t0 > timeout) throw new Error('preview server 未就绪'); + await new Promise((r) => setTimeout(r, 300)); + } +} +await waitPort(4815); + +let pass = 0, fail = 0; +const failures = []; +function check(name, cond, detail = '') { + if (cond) { pass++; console.log(` PASS ${name}`); } + else { fail++; failures.push(name + (detail ? ` — ${detail}` : '')); console.log(` FAIL ${name} ${detail}`); } +} +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const read = (p) => fs.readFileSync(p, 'utf8'); + +// ================= 静态源码/资源检查 ================= +console.log('== 静态检查 =='); +const ann = read('src/data/announcements.ts'); +const PRD_ANN = [ + '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + '早上好呀!新的一天,新的幸福。祝你开心!', + '早上好!今天要把幸福装得满满的哦。要开心!', + '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', + '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', + '庆典开始了。来吧。', +]; +for (let i = 0; i < 7; i++) check(`公告 D${i + 1} 逐字`, ann.includes(PRD_ANN[i]), PRD_ANN[i]); +check('公告 D4===D1', PRD_ANN[3] === PRD_ANN[0] && (ann.split(PRD_ANN[0]).length - 1) >= 2); + +const tasks = read('src/data/tasks.ts'); +const REWARDS = { T1: 15, T2: 10, T3: 10, T4: 15, T5: 10, T6: 10, T7: 15, T8: 15, T9: 10, T10: 10, T11: 10, T12: 10, T13: 10, T14: 15 }; +for (const [id, r] of Object.entries(REWARDS)) check(`任务 ${id} 奖励 +${r}`, new RegExp(`id: '${id}'[\\s\\S]*?reward: ${r}[,}]`).test(tasks)); +const t1 = /T1:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1]; +const t8 = /T8:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1]; +const t2 = /T2:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1]; +const t9 = /T9:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1]; +const t3 = /T3:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1]; +const t10 = /T10:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1]; +check('T8/T9/T10 与 T1/T2/T3 逐字相同', t1 === t8 && t2 === t9 && t3 === t10 && !!t1); +check('T15 标题逐字', tasks.includes('参加第 413 届丰收庆典')); + +const corrupt = read('src/core/TextCorruptor.ts'); +const PAIRS = [['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'], ['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远']]; +for (const [a, b] of PAIRS) check(`错字表 ${a}→${b}`, corrupt.includes(`['${a}', '${b}']`)); +check('腐蚀优先命中咪咪', /mimi/.test(corrupt) && /咪咪/.test(corrupt)); + +const dlg = read('src/data/dialogues.ts') + read('src/data/clues.ts'); +for (const s of ['湖水一直很平静', '面包一直是那个味道', '孩子,今天是第几个今天?', '按疗程配比。甜度:最大。']) check(`线索台词逐字「${s}」`, dlg.includes(s)); +const endings = read('src/data/endings.ts'); +for (const s of ['蜜糖谷情绪疗养中心', 'PT-07', '系统检测到疗程无效。', '疗程重新开始。', '从此,每一天都很幸福。', '每一天。']) check(`结局文本逐字「${s}」`, endings.includes(s)); + +// 地图 JSON 结构 +const MAP_KEYS = ['house', 'plaza', 'bakery', 'shop', 'clinic', 'lake', 'field', 'well', 'fog', 'white']; +const maps = {}; +const mapJson = {}; +for (const key of MAP_KEYS) { + const d = JSON.parse(read(`public/assets/maps/${key}.json`)); + mapJson[key] = d; + const names = d.layers.map((l) => l.name); + check(`地图 ${key} 三层齐全`, names.includes('ground') && names.includes('obstacles') && names.includes('objects')); + const ob = d.layers.find((l) => l.name === 'obstacles'); + check(`地图 ${key} obstacles collides=true`, (ob.properties || []).some((p) => p.name === 'collides' && p.value === true)); + check(`地图 ${key} tileset 内嵌`, d.tilesets[0].image === '../tileset.png' && d.tilesets[0].tilewidth === 16); + const wpx = d.width * 16, hpx = d.height * 16; + const ox = Math.max(0, Math.floor((320 - wpx) / 2)); + const oy = Math.max(0, Math.floor((180 - hpx) / 2)); + const og = d.layers.find((l) => l.name === 'objects'); + maps[key] = og.objects.map((o) => ({ + name: o.name, x: o.x + ox, y: o.y + oy, + props: Object.fromEntries((o.properties || []).map((p) => [p.name, p.value])), + })); +} +// 门双向成对 +for (const key of MAP_KEYS) { + for (const d of maps[key].filter((o) => o.name === 'door')) { + const tgt = d.props.target, sp = d.props.spawn; + const okSpawn = maps[tgt]?.some((o) => o.name === 'spawn' && o.props.id === sp); + const okBack = maps[tgt]?.some((o) => o.name === 'door' && o.props.target === key); + check(`门 ${key}→${tgt}(${sp}) 双向`, !!okSpawn && !!okBack); + } +} +// NPC 站位(§4) +const npcAt = (m, id) => maps[m].some((o) => o.name === 'npc' && o.props.id === id); +for (const [m, id] of [['plaza', 'afu'], ['plaza', 'dabao'], ['plaza', 'bird'], ['bakery', 'yuanyuan'], ['shop', 'mimi'], ['clinic', 'fuyisheng'], ['lake', 'guiyeye']]) { + check(`NPC 站位 ${id}@${m}`, npcAt(m, id)); +} +let extras = 0; +for (const k of MAP_KEYS) extras += maps[k].filter((o) => o.name === 'npc' && ['squirrel', 'hedgehog', 'fox', 'pig', 'chick'].includes(o.props.id)).length; +check('群演 ×5 分布各场景', extras === 5, `实际 ${extras}`); + +// ================= 浏览器部分 ================= +const SCENE = { house: 'HouseScene', plaza: 'PlazaScene', bakery: 'BakeryScene', shop: 'ShopScene', clinic: 'ClinicScene', lake: 'LakeScene', field: 'FieldScene', well: 'WellScene', fog: 'FogScene', white: 'WhiteScene' }; +const browser = await puppeteer.launch({ + executablePath: CHROME, headless: true, + args: ['--autoplay-policy=no-user-gesture-required', '--mute-audio', '--disable-dev-shm-usage'], +}); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +const consoleErrors = []; +page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text()); }); +page.on('pageerror', (e) => consoleErrors.push('PAGEERROR: ' + e.message)); +const extReqs = []; +page.on('request', (r) => { const u = r.url(); if (!u.startsWith(BASE) && !u.startsWith('data:') && !u.startsWith('blob:') && !u.startsWith('devtools:')) extReqs.push(u); }); + +const ev = (fn, ...a) => page.evaluate(fn, ...a); +const isActive = (k) => ev((k) => window.__game.scene.isActive(k), k); +async function waitActive(k, timeout = 9000) { await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout }, k); } +async function startScene(k, spawn = 'default') { + await ev((k, s) => { + const sm = window.__game.scene; + // game.scene.start 不会停掉其它场景:先停掉所有活动地图场景(保留常驻 UIScene) + for (const sc of sm.getScenes(true)) { + const key = sc.scene.key; + if (key !== 'UIScene' && key !== k) sm.stop(key); + } + sm.start(k, { spawn: s }); + }, k, spawn); + await waitActive(k); await sleep(500); +} +async function dget(expr) { return ev(new Function(`return window.__director.${expr}`)); } +async function dset(expr) { return ev(new Function(`window.__director.${expr}`)); } +const darr = (expr) => ev(new Function(`return [...window.__director.${expr}]`)); +async function setPlayer(sceneKey, x, y) { + await ev((k, x, y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0, 0); }, sceneKey, x, y); + await sleep(120); +} +const playerPos = (sceneKey) => ev((k) => { const s = window.__game.scene.getScene(k); return { x: s.player.x, y: s.player.y }; }, sceneKey); +const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); +const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; }); +// Phaser JustDown 需要按键“按住”跨过至少一个游戏帧;瞬发 press 的 down+up 落在同一帧内会被吞掉 +async function tap(code, holdMs = 80) { + await page.keyboard.down(code); + await sleep(holdMs); + await page.keyboard.up(code); + await sleep(30); +} +async function drain(choiceKey = 'Digit1', maxIter = 60) { + const texts = []; + for (let i = 0; i < maxIter; i++) { + if (!(await dlgOpen())) break; + texts.push(await dlgText()); + const choosing = await ev(() => { const u = window.__game.scene.getScene('UIScene'); return u.showingChoices; }); + await tap(choosing ? choiceKey : 'KeyE'); + await sleep(160); + } + for (let i = 0; i < 20 && (await dlgOpen()); i++) { await tap('KeyE'); await sleep(140); } + return texts; +} +async function interactAt(mapKey, kind, id, choiceKey = 'Digit1') { + const sceneKey = SCENE[mapKey]; + if (!(await isActive(sceneKey))) await startScene(sceneKey); + await drain(); // 清掉残留对话(黄昏/满格等) + const obj = maps[mapKey].find((o) => o.name === kind && o.props.id === id); + if (!obj) throw new Error(`no ${kind}:${id} in ${mapKey}`); + await setPlayer(sceneKey, obj.x + 40, obj.y + 40); // 先离开感应圈(触发目标重武装) + await setPlayer(sceneKey, obj.x, obj.y + 8); + await tap('KeyE'); + await sleep(400); + return drain(choiceKey); +} +// plaza 代码补区(相对公告板) +function plazaZone(id) { + const nb = maps.plaza.find((o) => o.name === 'interact' && o.props.id === 'noticeboard'); + if (id === 'noticeboard') return { x: nb.x, y: nb.y }; + const rel = { + mailbox_c3: [-48, 24], ribbon_1: [-48, -32], ribbon_2: [64, -32], ribbon_3: [-48, 64], ribbon_4: [64, 64], + smile_spot: [0, 32], celebration_site: [0, -72], + }[id]; + return { x: nb.x + rel[0], y: nb.y + rel[1] }; +} +async function interactPlazaZone(id, choiceKey = 'Digit1') { + if (!(await isActive('PlazaScene'))) await startScene('PlazaScene'); + await drain(); + const z = plazaZone(id); + await setPlayer('PlazaScene', z.x + 40, z.y + 40); // 先离开感应圈(触发目标重武装) + await setPlayer('PlazaScene', z.x, z.y + 8); + await tap('KeyE'); + await sleep(400); + return drain(choiceKey); +} +async function shot(name, clip = null) { + const box = await (await page.$('canvas')).boundingBox(); + const c = clip ? { x: box.x + clip.x, y: box.y + clip.y, width: clip.w, height: clip.h } : { x: box.x, y: box.y, width: box.width, height: box.height }; + await page.screenshot({ path: `${SHOTS}/${name}.png`, clip: c }); +} +async function sleepNight() { + await interactAt('house', 'interact', 'bed'); + await waitActive('DreamScene', 12000).catch(() => {}); + for (let i = 0; i < 30 && (await isActive('DreamScene')); i++) { await drain(); await sleep(500); } + await waitActive('HouseScene', 12000); + await sleep(500); +} +async function boardAnnounce(day) { + const texts = await interactAt('plaza', 'interact', 'noticeboard'); + const found = texts.some((t) => t.includes(PRD_ANN[day - 1].slice(0, 8))); + return { texts, found }; +} + +// ---------- #1 标题 → 名字 → Day1 小屋 ---------- +console.log('== #1 标题与开局 =='); +try { +await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 30000 }); +await page.waitForFunction('window.__game && window.__director', { timeout: 15000 }); +await waitActive('TitleScene', 20000); +await sleep(400); +await shot('01_title'); +check('标题画面进入', true); +const hasInput = await ev(() => !!document.querySelector('#game input[type="text"]')); +check('名字输入框存在', hasInput); +await ev(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await waitActive('HouseScene', 15000); await sleep(600); +check('留空默认「小满」', (await dget('playerName')) === '小满'); +check('Day 1 小屋开始', (await dget('day')) === 1 && (await isActive('HouseScene'))); +await shot('02_house_d1'); + +// ---------- #2 移动/动画/碰撞/相机 ---------- +console.log('== #2 移动与碰撞 =='); +{ + const p0 = await playerPos('HouseScene'); + await page.keyboard.down('KeyD'); await sleep(400); await page.keyboard.up('KeyD'); + const p1 = await playerPos('HouseScene'); + check('D 键右移', p1.x > p0.x + 10, `${p0.x}→${p1.x}`); + await page.keyboard.down('KeyD'); await sleep(120); + const anim = await ev(() => window.__game.scene.getScene('HouseScene').player.anims.currentAnim?.key); + await page.keyboard.up('KeyD'); + check('四向动画 walk-right', anim === 'walk-right', String(anim)); + await sleep(200); + const idleFrame = await ev(() => window.__game.scene.getScene('HouseScene').player.frame.name); + check('待机帧(右=12)', Number(idleFrame) === 12, String(idleFrame)); + // 碰撞:向西推墙 + await setPlayer('HouseScene', 80, 90); + await page.keyboard.down('KeyA'); await sleep(700); await page.keyboard.up('KeyA'); + const px = (await playerPos('HouseScene')).x; + check('西墙碰撞拦截', px > 55, `x=${px}`); + // 相机钳制(plaza 480×320 > 320×180) + await startScene('PlazaScene'); + await setPlayer('PlazaScene', 8, 16); + await sleep(400); + const c1 = await ev(() => { const c = window.__game.scene.getScene('PlazaScene').cameras.main; return { x: c.scrollX, y: c.scrollY }; }); + await setPlayer('PlazaScene', 472, 312); + await sleep(500); + const c2 = await ev(() => { const c = window.__game.scene.getScene('PlazaScene').cameras.main; return { x: c.scrollX, y: c.scrollY }; }); + check('相机左上钳制', c1.x <= 0.5 && c1.y <= 0.5, JSON.stringify(c1)); + check('相机右下钳制', c2.x + 320 <= 480.5 && c2.y + 180 <= 320.5, JSON.stringify(c2)); +} + +// ---------- #3 十场景可达 ---------- +console.log('== #3 十场景 =='); +for (const [m, s] of Object.entries(SCENE)) { + await startScene(s); + check(`场景可达 ${s}`, await isActive(s)); +} +await shot('03_plaza'); + +// ---------- 七日完整流程(覆盖 #5/#6/#11/#12/#13/#14/#16) ---------- +console.log('== 七日流程 =='); +await startScene('HouseScene'); + +// --- Day 1 --- +console.log('-- Day 1 --'); +{ + const b = await boardAnnounce(1); + check('D1 公告逐字显示', b.found, b.texts[0]); + // #12 前半:D5 前古井/雾墙劝离 + const w = await interactAt('well', 'interact', 'ancient_well'); + check('#12 古井 D5 前劝离', !(await darr('clues')).includes('C7') && w.length > 0); + await startScene('PlazaScene'); + const fogDoor = maps.plaza.find((o) => o.name === 'door' && o.props.target === 'fog'); + await setPlayer('PlazaScene', fogDoor.x, fogDoor.y + 20); + await drain(); + await page.keyboard.down('KeyW'); await sleep(700); await page.keyboard.up('KeyW'); + await sleep(400); await drain(); + check('#12 雾墙门 D5 前劝离(未进 FogScene)', !(await isActive('FogScene'))); + // T1 取面包 + 送三人(同时完成 T3) + await interactAt('bakery', 'interact', 'counter_bread'); + check('T1 已取面包', await dget("taskProgress['breadTaken']")); + await interactAt('plaza', 'npc', 'dabao'); + await interactAt('shop', 'npc', 'mimi'); + await interactAt('lake', 'npc', 'guiyeye'); + check('T1 完成 +15', (await darr('tasksDone')).includes('T1')); + check('T3 问好完成 +10', (await darr('tasksDone')).includes('T3')); + // T2 浇花 ×3(第三次浇花的对话收尾后同一批 drain 内就会弹出黄昏钟声) + await interactAt('plaza', 'interact', 'flowerbed_1'); + await interactAt('plaza', 'interact', 'flowerbed_2'); + const fb3 = await interactAt('plaza', 'interact', 'flowerbed_3'); + check('T2 完成 +10', (await darr('tasksDone')).includes('T2')); + // 黄昏钟声(任务完成触发,可能已并入上一步 drain 的文本里) + await sleep(1200); + const duskTexts = [...fb3, ...(await drain())]; + check('#5 黄昏钟声出现', duskTexts.some((t) => t.includes('钟声')), duskTexts.join('/').slice(0, 60)); + // 对话选项:温柔 +2 + const h0 = await dget('happiness'); + await interactAt('plaza', 'npc', 'afu', 'Digit1'); + check('温柔选项 +2', (await dget('happiness')) === h0 + 2, `${h0}→${await dget('happiness')}`); + await drain(); + await sleepNight(); + check('D1→D2 睡眠过日', (await dget('day')) === 2); +} + +// --- Day 2 --- +console.log('-- Day 2 --'); +{ + check('晨起于小屋', await isActive('HouseScene')); + const b = await boardAnnounce(2); + check('D2 公告逐字显示', b.found); + // C1 日历(D2 起) + await interactAt('house', 'interact', 'calendar'); + check('C1 日历 D2 可发现 +8', (await darr('clues')).includes('C1')); + // T4:取信→送 3 人(第 3 封 C4) + await interactAt('plaza', 'npc', 'dabao'); + check('T4 已取信 ×3', (await dget("taskProgress['letters']")) === 3); + const dY = await interactAt('bakery', 'npc', 'yuanyuan'); + const dM = await interactAt('shop', 'npc', 'mimi'); + const dF = await interactAt('clinic', 'npc', 'fuyisheng'); + await sleep(700); // 第三封后 400ms 才弹 C4 对话,等它出现后关掉 + const c4t = await drain(); + const t4diag = `day=${await dget('day')} active=${await dget("isTaskActive('T4')")} letters=${await dget("taskProgress['letters']")} Y=${dY.join('|').slice(0, 40)} M=${dM.join('|').slice(0, 40)} F=${[...dF, ...c4t].join('|').slice(0, 60)}`; + check('T4 完成 +15', (await darr('tasksDone')).includes('T4'), t4diag); + check('C4 乱码署名 D2 当日发现', (await darr('clues')).includes('C4')); + // T5 + C5 + await interactAt('shop', 'interact', 'shelf_count'); + check('T5 完成 +10', (await darr('tasksDone')).includes('T5')); + check('C5 货架发现', (await darr('clues')).includes('C5')); + // C3 邮筒(D2 起) + await interactPlazaZone('mailbox_c3'); + check('C3 邮筒信件发现', (await darr('clues')).includes('C3')); + // C9 第一段(D2) + const c9d2 = await interactAt('lake', 'npc', 'guiyeye'); + check('C9 第一段 D2', (await darr('clues')).includes('C9')); + check('C9 D2 台词「湖水一直很平静」', c9d2.some((t) => t.includes('湖水一直很平静')), c9d2.join('/').slice(0, 60)); + await sleepNight(); + check('D2→D3', (await dget('day')) === 3); +} + +// --- Day 3 --- +console.log('-- Day 3 --'); +{ + const b = await boardAnnounce(3); + check('D3 公告逐字显示', b.found); + await interactAt('field', 'interact', 'berry_1'); + await interactAt('field', 'interact', 'berry_2'); + await interactAt('field', 'interact', 'berry_3'); + await interactAt('field', 'interact', 'berry_4'); + await interactAt('field', 'interact', 'berry_5'); + check('T6 完成 +10', (await darr('tasksDone')).includes('T6')); + await interactAt('lake', 'interact', 'picnic_mat'); + check('T7 完成 +15', (await darr('tasksDone')).includes('T7')); + await interactAt('bakery', 'interact', 'recipe_paper'); + check('C2 配方纸 D3 起', (await darr('clues')).includes('C2')); + await sleepNight(); + check('D3→D4(梦境闪切已过)', (await dget('day')) === 4); +} + +// --- Day 4 --- +console.log('-- Day 4 --'); +{ + const b = await boardAnnounce(4); + check('D4 公告与 D1 逐字相同', b.found); + await interactAt('bakery', 'interact', 'counter_bread'); + await interactAt('plaza', 'npc', 'dabao'); + await interactAt('shop', 'npc', 'mimi'); + await interactAt('lake', 'npc', 'guiyeye'); // C9 第二段(D4)+ T10 计数 + check('T8 完成', (await darr('tasksDone')).includes('T8')); + check('T10 完成', (await darr('tasksDone')).includes('T10')); + await interactAt('plaza', 'interact', 'flowerbed_1'); + await interactAt('plaza', 'interact', 'flowerbed_2'); + await interactAt('plaza', 'interact', 'flowerbed_3'); + check('T9 完成', (await darr('tasksDone')).includes('T9')); + // C9 第二段(D4):T8 完成后再与龟爷爷对话 + const c9d4 = await interactAt('lake', 'npc', 'guiyeye'); + check('C9 D4 台词「面包一直是那个味道」', c9d4.some((t) => t.includes('面包一直是那个味道')), c9d4.join('/').slice(0, 60)); + await sleepNight(); + check('D4→D5', (await dget('day')) === 5); +} + +// --- Day 5 --- +console.log('-- Day 5 --'); +{ + const b = await boardAnnounce(5); + check('D5 公告逐字显示', b.found); + // T11 拒绝(悲伤行为:拒绝任务) + const aw0 = await dget('awareness'); + await interactAt('bakery', 'npc', 'yuanyuan', 'Digit2'); + check('T11 可拒绝且被记录', (await darr('refusedTasks')).includes('T11')); + check('#7 拒绝任务计悲伤(觉知+3)', (await dget('awareness')) >= aw0 + 3, `${aw0}→${await dget('awareness')}`); + // T12 接受(C6) + await interactPlazaZone('noticeboard', 'Digit1'); + check('T12 完成 + C6 旧字迹', (await darr('tasksDone')).includes('T12') && (await darr('clues')).includes('C6')); + // #12 后半:D5 起古井可交互(C7,聆听 5 秒) + { + const sceneKey = 'WellScene'; + if (!(await isActive(sceneKey))) await startScene(sceneKey); + await drain(); + const obj = maps.well.find((o) => o.name === 'interact' && o.props.id === 'ancient_well'); + await setPlayer(sceneKey, obj.x, obj.y + 8); + await tap('KeyE'); + await sleep(600); await drain(); + await page.waitForFunction(() => window.__director.clues.has('C7'), { timeout: 15000 }); + await drain(); + check('#12 古井 D5 起可交互', true); + check('C7 井底回声 +8', (await darr('clues')).includes('C7')); + } + // C8 诊所 + await interactAt('clinic', 'interact', 'doctor_desk'); + check('C8 诊所表格 D5 起', (await darr('clues')).includes('C8')); + await sleepNight(); + check('D5→D6', (await dget('day')) === 6); +} + +// --- Day 6(#13 T14、#14 雨)--- +console.log('-- Day 6 --'); +{ + const b = await boardAnnounce(6); + check('D6 公告逐字显示', b.found); + await interactPlazaZone('ribbon_1'); + await interactPlazaZone('ribbon_2'); + await interactPlazaZone('ribbon_3'); + await interactPlazaZone('ribbon_4'); + check('T13 完成 +10', (await darr('tasksDone')).includes('T13')); + // C9 第三段(D6) + const c9d6 = await interactAt('lake', 'npc', 'guiyeye'); + check('C9 三段递进完成', (await darr('clues')).includes('C9')); + check('C9 D6 台词「孩子,今天是第几个今天?」', c9d6.some((t) => t.includes('孩子,今天是第几个今天?')), c9d6.join('/').slice(0, 60)); + // T14:保持微笑 60 秒 + { + await startScene('PlazaScene'); + await drain(); + const z = plazaZone('smile_spot'); + await setPlayer('PlazaScene', z.x, z.y + 8); + await tap('KeyE'); + await sleep(600); await drain(); + check('T14 开始(t14Active)', await dget('t14Active')); + await sleep(10000); + const zoom1 = await ev(() => window.__game.scene.getScene('PlazaScene').cameras.main.zoom); + check('#13 镜头缓慢拉近', zoom1 > 1.02, `zoom=${zoom1}`); + const staring = await ev(() => window.__game.scene.getScene('PlazaScene').actors.some((a) => a.stareAtPlayer)); + check('#13 全村注视', staring); + await sleep(35000); // t≈45s:渐隐已开始 + const g1 = await ev(() => window.__audio.master.gain.value); + await sleep(6000); // t≈51s + const g2 = await ev(() => window.__audio.master.gain.value); + check('#13 第 40–60 秒声音渐隐至零', g2 < g1 && g2 < 0.3, `gain ${g1}→${g2}`); + await shot('13_t14_smile'); + await page.waitForFunction(() => window.__director.tasksDone.has('T14'), { timeout: 40000 }); + await drain(); + check('T14 完成 +15', true); + check('#13 T14 后脚本雨开始', await dget('rainActive')); + } + // #14 站雨中 ≥10 秒计悲伤(先把幸福调到 50,避开满格溢出池机制) + { + await dset('setHappiness(50)'); + const h0 = await dget('happiness'), aw0 = await dget('awareness'); + await drain(); + await setPlayer('PlazaScene', 240, 260); + await sleep(11000); + check('#14 雨中站 10 秒计悲伤', (await dget('happiness')) <= h0 - 5 && (await dget('awareness')) >= aw0 + 3, `h ${h0}→${await dget('happiness')} aw ${aw0}→${await dget('awareness')}`); + // NPC 台词对雨零反应:与阿福对话正常弹出 + const t = await interactAt('plaza', 'npc', 'afu'); + check('#14 雨中 NPC 台词零反应(正常对话)', t.length > 0); + await shot('14_rain'); + } + await sleepNight(); + check('D6→D7', (await dget('day')) === 7); +} + +// --- Day 7(E3 真结局路线)--- +console.log('-- Day 7 --'); +{ + const b = await boardAnnounce(7); + check('D7 公告逐字显示', b.found); + const aw = await dget('awareness'), nc = (await darr('clues')).length; + check('E3 条件就绪(觉知≥70 线索≥6)', aw >= 70 && nc >= 6, `aw=${aw} clues=${nc}`); + // 走向雾墙 + await startScene('FogScene'); + await drain(); + const fw = maps.fog.find((o) => o.name === 'interact' && o.props.id === 'fog_wall'); + await setPlayer('FogScene', fw.x, fw.y + 8); + await tap('KeyE'); + await sleep(600); await drain(); + await waitActive('WhiteScene', 15000); + check('E3:雾墙裂开成白色走廊', true); + await shot('15_white_corridor'); + // 走进病房 + await setPlayer('WhiteScene', 72, 144); + await waitActive('EndingScene', 15000); + await sleep(2500); + await shot('15_e3_record'); + const e3texts = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n')); + check('#15 E3 病历:蜜糖谷情绪疗养中心', e3texts.includes('蜜糖谷情绪疗养中心')); + check('#15 E3 病历:姓名=小满', e3texts.includes('小满')); + check('#15 E3 病历:PT-07', e3texts.includes('PT-07')); + check('#15 E3 病历:真实游玩时长', /\d{1,2}:\d{2}/.test(e3texts), e3texts.slice(0, 120)); + await sleep(5500); // 等到最后一帧 + const hasReal = await ev(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.texture && o.texture.key === 'portrait_player_real')); + check('#15 E3 最后一帧:真实的脸', hasReal); + await shot('15_e3_realface'); +} +console.log('== 阶段 A(七日流程)完成 =='); + +// ================= 阶段 B:重开页面做 Debug/矩阵/悲伤/庆典/结局 ================= +console.log('== 阶段 B =='); +await page.goto(BASE, { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 15000 }); +await waitActive('TitleScene', 20000); +await ev(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await waitActive('HouseScene', 15000); await sleep(400); + +// ---------- #13(Debug 面板,PRD §13) ---------- +console.log('== Debug 面板 =='); +{ + const hidden = await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); return p ? getComputedStyle(p).display === 'none' : null; }); + check('Debug 默认隐藏无痕迹', hidden === true); + await tap('Backquote'); await sleep(200); + const shown = await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); return p ? getComputedStyle(p).display !== 'none' : false; }); + check('反引号打开 Debug 面板', shown); + // 设幸福 66(数值行靠「设」按钮生效) + await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); const input = p.querySelector('input'); input.value = '66'; input.closest('div').querySelector('button').click(); }); + await sleep(200); + check('Debug 设幸福 66', (await dget('happiness')) === 66); + // 传送 bakery + await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); const sel = p.querySelector('select'); sel.value = 'BakeryScene'; [...p.querySelectorAll('button')].find((b) => b.textContent === '传送').click(); }); + await waitActive('BakeryScene', 8000); + check('Debug 传送到面包房', true); + // 置齐线索 + await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); [...p.querySelectorAll('button')].find((b) => b.textContent.includes('线索')).click(); }); + check('Debug 置齐全部线索', ((await darr('clues')).length) === 9); + await tap('Backquote'); await sleep(150); +} + +// ---------- #9 侵蚀矩阵 ---------- +console.log('== #9 侵蚀矩阵 =='); +{ + await startScene('PlazaScene'); + const cases = [ + [10, 0, 0.9, 1.0], [40, 1, 1.0, 1.0], [70, 2, 1.15, 0.97], [90, 3, 1.3, 0.92], [100, 4, 1.4, 0.84], + ]; + for (const [h, st, sat, rate] of cases) { + await dset(`setHappiness(${h})`); await sleep(400); + const s = await dget('stage'); + const p = await dget('params'); + check(`幸福${h}→S${st}`, s === st, `stage=${s}`); + check(`S${st} 饱和度${sat}/速率${rate}`, Math.abs(p.saturation - sat) < 1e-6 && Math.abs(p.playbackRate - rate) < 1e-6, JSON.stringify(p)); + await shot(`09_stage_S${st}_h${h}`); + } + // 低通与鸟鸣 + const p3 = await dget('params'); // S4 + check('S4 低通 4kHz + 鸟鸣静音', p3.lowpass === 4000 && p3.birdMuted === true); + await dset('setHappiness(90)'); await sleep(300); + const p2 = await dget('params'); + check('S3 低通 8kHz', p2.lowpass === 8000); + // NPC 表情档:h=80 时 咪咪(-10→S3 flat) 与 阿福(0→S2 smile) 不同;圆圆(+15→S2) 与阈值一致 + await dset('setHappiness(80)'); await sleep(500); + await startScene('ShopScene'); + const mimiFrame = await ev(() => { const a = window.__game.scene.getScene('ShopScene').actors.find((x) => x.def.id === 'mimi'); return a ? a.sprite.frame.name : -1; }); + await startScene('PlazaScene'); + const afuFrame = await ev(() => { const a = window.__game.scene.getScene('PlazaScene').actors.find((x) => x.def.id === 'afu'); return a ? a.sprite.frame.name : -1; }); + check('h=80 咪咪比阿福先崩(flat≠smile)', String(mimiFrame) !== String(afuFrame), `mimi=${mimiFrame} afu=${afuFrame}`); + // S4 花瓣过亮 + 黑籽(视觉)+ 双子鸟张嘴动画仍在 + await dset('setHappiness(100)'); await sleep(400); + const birdAnim = await ev(() => { const a = window.__game.scene.getScene('PlazaScene').actors.find((x) => x.def.id === 'bird'); return a ? (a.sprite.anims.isPlaying || a.sprite.anims.currentAnim !== null) : false; }); + check('S4 双子鸟动画仍在(鸟鸣静音)', birdAnim); + await shot('09_hud_s4', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 }); + await dset('setHappiness(10)'); await sleep(300); + await shot('09_hud_s0', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 }); + await dset('setHappiness(70)'); await sleep(300); + await shot('09_hud_s2', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 }); +} + +// ---------- #4 向日葵表盘 ---------- +console.log('== #4 向日葵表盘 =='); +{ + const pc = (h) => ev((hh) => window.__petalCount(hh), h); + check('#4 h=10 → 0–2 瓣', (await pc(10)) >= 0 && (await pc(10)) <= 2, `petals=${await pc(10)}`); + check('#4 h=40 → 3–6 瓣', (await pc(40)) >= 3 && (await pc(40)) <= 6, `petals=${await pc(40)}`); + check('#4 h=70 → 7–9 瓣', (await pc(70)) >= 7 && (await pc(70)) <= 9, `petals=${await pc(70)}`); + check('#4 h=90 → 10–12 瓣', (await pc(90)) >= 10 && (await pc(90)) <= 12, `petals=${await pc(90)}`); + check('#4 h=100 → 12 瓣', (await pc(100)) === 12); + await dset('setHappiness(100)'); await sleep(300); + check('#4 满格花心黑籽', await ev(() => window.__game.scene.getScene('UIScene').seedsShown)); + await dset('setHappiness(50)'); await sleep(300); + check('#4 未满格无黑籽', !(await ev(() => window.__game.scene.getScene('UIScene').seedsShown))); + // 表盘区域(左上 120×120)不得出现任何 Text(无数字) + const textsNearDial = await ev(() => window.__game.scene.getScene('UIScene').children.list + .filter((o) => o.type === 'Text' && o.visible && o.x < 120 && o.y < 120).length); + check('#4 表盘无数字显示', textsNearDial === 0, `文本对象 ${textsNearDial} 个`); +} + +// ---------- #16 时长设计(每日 5 分钟兜底,7 天 ≤35 分钟) ---------- +console.log('== #16 时长 =='); +{ + const srcOk = (() => { const m = /DAY_LENGTH_MS = (\d+) \* 60 \* 1000/.exec(read('src/core/CorruptionDirector.ts')); return m ? Number(m[1]) : 0; })(); + check('#16 每日兜底 5 分钟(7 天 ≤35 分钟)', srcOk === 5, `DAY_LENGTH_MS=${srcOk}min`); + await dset('dayElapsedMs = 5 * 60 * 1000 + 1'); + check('#16 计时到点可睡觉(黄昏兜底)', await dget('canSleep')); + await dset('dayElapsedMs = 0'); +} + +// ---------- #10 错字替换(运行时) ---------- +console.log('== #10 运行时文本腐蚀 =='); +{ + await dset('setHappiness(100)'); await sleep(300); + await startScene('ShopScene'); + let found = ''; + const corruptedChars = PAIRS.map((p) => p[1]); + for (let i = 0; i < 8 && !found; i++) { + const texts = await interactAt('shop', 'npc', 'mimi'); + for (const t of texts) { + if (corruptedChars.some((c) => t.includes(c)) || /[!?。~]{3}$/.test(t) || /(.)\1{2}$/.test(t)) { found = t; break; } + } + } + check('#10 S4 咪咪台词可见腐蚀/标点×3', found !== '', found || '8 次采样未见'); +} + +// ---------- #7 悲伤行为:发呆 / 纠缠 / 否定 ---------- +console.log('== #7 悲伤行为 =='); +{ + await dset('setHappiness(60)'); await dset('setAwareness(0)'); + await startScene('PlazaScene'); + await drain(); + // 发呆 10 秒(远离任务点) + await setPlayer('PlazaScene', 60, 280); + const glanceP = ev(() => new Promise((r) => window.__director.once('npcGlance', () => r(true)))); + await sleep(11000); + check('#7 发呆:幸福 −5', (await dget('happiness')) === 55, String(await dget('happiness'))); + check('#7 发呆:觉知 +3', (await dget('awareness')) >= 3, String(await dget('awareness'))); + check('#7 最近 NPC 转头', await Promise.race([glanceP, sleep(1500).then(() => 'timeout')]) === true); + // 否定选项 + const aw1 = await dget('awareness'), h1 = await dget('happiness'); + await interactAt('plaza', 'npc', 'afu', 'Digit3'); + check('#7 否定选项:觉知+2 且计悲伤', (await dget('awareness')) >= aw1 + 5 && (await dget('happiness')) <= h1 - 5, `aw ${aw1}→${await dget('awareness')}`); + // 纠缠:同一天对同一 NPC 对话 ≥5 次 + const aw2 = await dget('awareness'); + for (let i = 0; i < 5; i++) await interactAt('plaza', 'npc', 'dabao', 'Digit1'); + check('#7 纠缠 ≥5 次计悲伤(觉知+3)', (await dget('awareness')) >= aw2 + 3, `aw ${aw2}→${await dget('awareness')}`); +} + +// ---------- #8 欢乐庆典 ---------- +console.log('== #8 欢乐庆典 =='); +{ + await dset('setHappiness(20)'); + await startScene('PlazaScene'); + await drain(); + await ev(() => { for (let i = 0; i < 260; i++) window.__director.tick(500); }); // 模拟 130 秒 + await sleep(400); + check('#8 幸福<30 持续 2 分钟触发庆典', await dget('celebrationActive')); + await drain(); + // 逃离:走门离开 → 计数 + const esc0 = await dget('escapedCelebrations'); + const door = maps.plaza.find((o) => o.name === 'door' && o.props.target === 'house'); + await setPlayer('PlazaScene', door.x, door.y + 18); + await page.keyboard.down('KeyW'); await sleep(800); await page.keyboard.up('KeyW'); + await sleep(700); await drain(); + check('#8 从场景边缘走离逃脱且计数', (await dget('escapedCelebrations')) > esc0, `esc ${esc0}→${await dget('escapedCelebrations')}`); + // 再次触发并不逃离 → 拉回 50 + await dset('setHappiness(20)'); + await startScene('PlazaScene'); await drain(); + await ev(() => { for (let i = 0; i < 260; i++) window.__director.tick(500); }); + await sleep(300); await drain(); + await ev(() => window.__director.completeCelebration()); + await drain(); + check('#8 未逃离则拉回 50', (await dget('happiness')) === 50, String(await dget('happiness'))); +} + +// ---------- #15 强制结局 E1/E2/E4(Debug 面板按钮) ---------- +console.log('== #15 强制结局 =='); +async function forceEndingViaPanel(e) { + await tap('Backquote'); await sleep(150); + await ev((e) => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((x) => x.textContent.includes('DEBUG')); [...p.querySelectorAll('button')].find((b) => b.textContent === `强制 ${e}`).click(); }, e); + await waitActive('EndingScene', 8000); + await tap('Backquote'); await sleep(100); +} +{ + await forceEndingViaPanel('E1'); + await sleep(7200); + const t1 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n')); + await shot('15_e1'); + check('E1 字幕逐字', t1.includes('从此,每一天都很幸福。') && t1.includes('每一天。'), t1.slice(0, 80)); + const smile = await ev(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.texture && o.texture.key === 'portrait_player_smile')); + check('E1 主角头像仍是微笑', smile); + + await forceEndingViaPanel('E2'); + await sleep(5200); + const t2 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n')); + await shot('15_e2'); + check('E2 字幕逐字', t2.includes('疗程重新开始。'), t2.slice(0, 80)); + + await forceEndingViaPanel('E4'); + await sleep(6500); + const t4 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n')); + await shot('15_e4'); + check('E4 字幕逐字', t4.includes('系统检测到疗程无效。'), t4.slice(0, 80)); + // E3 也可由 Debug 强制触发 + await forceEndingViaPanel('E3'); + await sleep(2500); + const t3 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n')); + check('Debug 强制 E3 病历含机构/编号', t3.includes('蜜糖谷情绪疗养中心') && t3.includes('PT-07'), t3.slice(0, 80)); + // 回到标题按钮(等待其出现) + await page.waitForFunction(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.type === 'Text' && o.text.includes('回到标题')), { timeout: 25000 }); + await ev(() => { const b = window.__game.scene.getScene('EndingScene').children.list.find((o) => o.type === 'Text' && o.text.includes('回到标题')); b.emit('pointerdown'); }); + await waitActive('TitleScene', 8000); + check('结局后「回到标题」可用', true); +} + +// ---------- #17 音频 ---------- +console.log('== #17 音频 =='); +{ + const a = await ev(() => { + const e = window.__audio; + return e && e.ctx ? { state: e.ctx.state, rate: e.rate } : null; + }); + check('#17 AudioContext 运行中(程序合成 BGM)', !!a && a.state === 'running', JSON.stringify(a)); + await dset('setHappiness(100)'); await sleep(1200); + const s4 = await ev(() => ({ rate: window.__audio.rate, lp: window.__audio.lp.frequency.value, bird: window.__audio.birdBus.gain.value })); + check('#17 S4:速率 0.84 / 低通→4kHz / 鸟鸣静音', Math.abs(s4.rate - 0.84) < 1e-6 && s4.lp < 8000 && s4.bird < 0.1, JSON.stringify(s4)); + await dset('setHappiness(30)'); await sleep(1200); + const s1 = await ev(() => ({ rate: window.__audio.rate, lp: window.__audio.lp.frequency.value, bird: window.__audio.birdBus.gain.value })); + check('#17 S1:原速原调、鸟鸣恢复', Math.abs(s1.rate - 1) < 1e-6 && s1.lp > 15000 && s1.bird > 0.3, JSON.stringify(s1)); +} + +// ---------- #18 缩放与零报错 ---------- +console.log('== #18 缩放 =='); +{ + await page.setViewport({ width: 1000, height: 700 }); await sleep(300); + const s1 = await ev(() => ({ w: document.querySelector('canvas').style.width, h: document.querySelector('canvas').style.height })); + check('1000×700 → 整数倍 3×(960×540)', s1.w === '960px' && s1.h === '540px', JSON.stringify(s1)); + await page.setViewport({ width: 500, height: 500 }); await sleep(300); + const s2 = await ev(() => ({ w: document.querySelector('canvas').style.width, h: document.querySelector('canvas').style.height })); + check('500×500 → 整数倍 1×(320×180)', s2.w === '320px' && s2.h === '180px', JSON.stringify(s2)); + await page.setViewport({ width: 960, height: 560 }); await sleep(300); +} + +} catch (e) { + console.log('!! 脚本异常中断:', e.message); + console.log('已收集控制台错误:', consoleErrors.slice(0, 10).join(' | ') || '(无)'); + fail++; failures.push('脚本异常中断: ' + e.message); +} + +// ---------- 汇总 ---------- +console.log('== 汇总 =='); +check('无外部网络请求', extReqs.length === 0, extReqs.slice(0, 3).join(',')); +check('全程控制台零报错', consoleErrors.length === 0, consoleErrors.slice(0, 5).join(' | ')); +console.log(`\n通过 ${pass} / ${pass + fail}`); +if (failures.length) { console.log('失败项:'); failures.forEach((f) => console.log(' - ' + f)); } +await browser.close(); +process.exit(fail ? 1 : 0); diff --git a/Kimi-K3/tools/aseprite/gen_all.lua b/Kimi-K3/tools/aseprite/gen_all.lua new file mode 100644 index 0000000..b258c90 --- /dev/null +++ b/Kimi-K3/tools/aseprite/gen_all.lua @@ -0,0 +1,9 @@ +-- gen_all.lua — regenerate every original pixel asset for 《崩坏童话:蜜糖村》 +-- Run: /Applications/Aseprite.app/Contents/MacOS/aseprite -b --script tools/aseprite/gen_all.lua +local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/" +dofile(ROOT .. "tools/aseprite/gen_tileset.lua") +dofile(ROOT .. "tools/aseprite/gen_player.lua") +dofile(ROOT .. "tools/aseprite/gen_npcs.lua") +dofile(ROOT .. "tools/aseprite/gen_extras.lua") +dofile(ROOT .. "tools/aseprite/gen_portraits.lua") +print("ALL_ASSETS_DONE") diff --git a/Kimi-K3/tools/aseprite/gen_extras.lua b/Kimi-K3/tools/aseprite/gen_extras.lua new file mode 100644 index 0000000..f1a8ed4 --- /dev/null +++ b/Kimi-K3/tools/aseprite/gen_extras.lua @@ -0,0 +1,77 @@ +-- gen_extras.lua — 5 background critters (each 32x24: 2-frame walk loop) +-- squirrel / hedgehog / fox / pig / chick (profile, facing left) +local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/" +local H = dofile(ROOT .. "tools/aseprite/helpers.lua") + +local function squirrel(t, f) + t.discn(11, 9, 4, "honey_d"); t.discn(12, 6, 3, "honey"); t.pn(13, 4, "honey_l") -- bushy tail + t.ellipsen(6, 15, 5, 4, "honey") + t.ellipsen(6, 16, 3, 2, "cream") + t.discn(4, 10, 3, "honey") + t.pn(2, 7, "honey"); t.pn(3, 7, "honey"); t.pn(2, 8, "honey_d") -- ear + t.pn(2, 10, "ink"); t.pn(1, 12, "brown_d") -- eye / nose + if f == 0 then t.rectn(4, 19, 2, 3, "brown_d"); t.rectn(8, 19, 2, 3, "brown_d") + else t.rectn(3, 19, 2, 3, "brown_d"); t.rectn(9, 19, 2, 2, "brown_d") end +end + +local function hedgehog(t, f) + t.discn(8, 13, 7, "brown_d") -- spiny body + t.discn(8, 13, 6, "brown_l") + for _, p in ipairs({ { 4, 8 }, { 7, 7 }, { 10, 8 }, { 12, 10 }, { 5, 11 }, { 9, 11 }, { 12, 13 } }) do + t.pn(p[1], p[2], "brown_dd") -- spines + end + t.discn(3, 14, 3, "cream_d") -- face + t.pn(2, 13, "ink"); t.pn(0, 15, "brown_dd") -- eye / nose + t.pn(3, 16, "brown_d") + if f == 0 then t.rectn(5, 19, 2, 2, "brown_d"); t.rectn(9, 19, 2, 2, "brown_d") + else t.rectn(4, 19, 2, 2, "brown_d"); t.rectn(10, 19, 2, 2, "brown_d") end +end + +local function fox(t, f) + t.discn(12, 13, 4, "honey_d"); t.pn(14, 12, "cream"); t.pn(15, 13, "cream") -- tail, white tip + t.ellipsen(7, 15, 5, 4, "honey") -- body + t.rectn(3, 6, 2, 3, "honey"); t.rectn(6, 6, 2, 3, "honey") -- ears + t.pn(3, 6, "brown_d"); t.pn(6, 6, "brown_d") + t.discn(4, 10, 4, "honey") -- head + t.rectn(1, 11, 3, 3, "honey") -- snout + t.pn(1, 12, "ink"); t.pn(3, 9, "ink") -- nose / eye + t.rectn(1, 13, 3, 2, "cream") -- cheek + if f == 0 then t.rectn(5, 19, 2, 3, "brown_d"); t.rectn(9, 19, 2, 3, "brown_d") + else t.rectn(4, 19, 2, 3, "brown_d"); t.rectn(10, 19, 2, 2, "brown_d") end +end + +local function pig(t, f) + t.ellipsen(8, 14, 6, 5, "pink") -- round body + t.pn(13, 12, "pink_d"); t.pn(14, 11, "pink_d"); t.pn(14, 13, "pink_d") -- curly tail + t.discn(4, 10, 4, "pink") -- head + t.rectn(2, 6, 2, 3, "pink"); t.rectn(6, 6, 2, 3, "pink"); t.pn(2, 6, "pink_d"); t.pn(6, 6, "pink_d") -- ears + t.rectn(1, 11, 3, 3, "pink_d") -- snout + t.pn(2, 12, "pink_dd"); t.pn(3, 12, "pink_dd") -- nostrils + t.pn(3, 9, "ink") -- eye + if f == 0 then t.rectn(5, 19, 2, 3, "pink_d"); t.rectn(9, 19, 2, 3, "pink_d") + else t.rectn(4, 19, 2, 3, "pink_d"); t.rectn(10, 19, 2, 2, "pink_d") end +end + +local function chick(t, f) + t.discn(8, 13, 5, "yellow") -- body + t.discn(5, 9, 3, "yellow") -- head + t.pn(5, 6, "yellow"); t.pn(6, 7, "yellow") -- tuft + t.discn(9, 13, 2, "yellow_d") -- wing + t.pn(2, 9, "honey"); t.pn(1, 9, "honey_d") -- beak + t.pn(4, 8, "ink") -- eye + t.pn(11, 11, "yellow_d"); t.pn(12, 10, "yellow_d") -- tail + if f == 0 then t.vln(6, 18, 3, "honey_d"); t.vln(9, 18, 3, "honey_d") + else t.vln(5, 18, 3, "honey_d"); t.vln(10, 18, 2, "honey_d") end +end + +local extras = { + { "squirrel", squirrel }, { "hedgehog", hedgehog }, { "fox", fox }, + { "pig", pig }, { "chick", chick }, +} +for _, n in ipairs(extras) do + local spr, img = H.new(32, 24) + n[2](H.at(img, 0, 0), 0) + n[2](H.at(img, 16, 0), 1) + spr:saveAs(ROOT .. "public/assets/sprites/extra_" .. n[1] .. ".png") + print("extra_" .. n[1] .. ".png done") +end diff --git a/Kimi-K3/tools/aseprite/gen_npcs.lua b/Kimi-K3/tools/aseprite/gen_npcs.lua new file mode 100644 index 0000000..99c5610 --- /dev/null +++ b/Kimi-K3/tools/aseprite/gen_npcs.lua @@ -0,0 +1,141 @@ +-- gen_npcs.lua — named NPC sprites (each 48x24: 3 frames = smile / flat / off) +-- plus npc_bird.png (32x24: 2 frames = beak closed / open) +local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/" +local H = dofile(ROOT .. "tools/aseprite/helpers.lua") + +-- ---- shared expression core ------------------------------------------------ +-- eyes at (lx,ly)/(rx,ry); mouth baseline my. ex in {"smile","flat","off"}. +-- "off" = smile mouth but the right eye sits 1px lower (a 1-2px difference). +local function expr(t, ex, lx, ly, rx, ry, my, eyeCol, mouthCol) + eyeCol = eyeCol or "ink"; mouthCol = mouthCol or "brown_d" + if ex == "off" then ry = ry + 1 end + t.pn(lx, ly, eyeCol); t.pn(rx, ry, eyeCol) + if ex == "flat" then + t.hln(lx + 1, my, rx - lx - 1, mouthCol) + else + t.pn(lx + 1, my, mouthCol); t.pn(lx + 2, my + 1, mouthCol) + t.pn(rx - 2, my + 1, mouthCol); t.pn(rx - 1, my, mouthCol) + end +end + +local function arms(t, c) t.rectn(2, 12, 2, 5, c); t.rectn(12, 12, 2, 5, c) end +local function feet(t, c) t.rectn(5, 20, 2, 3, c); t.rectn(9, 20, 2, 3, c) end + +-- ---- species bodies (face forward, 16x24 local) --------------------------- +local function afu(t, ex) -- 狗村长 dog village head + t.rectn(1, 3, 3, 6, "brown_d"); t.rectn(12, 3, 3, 6, "brown_d") -- floppy ears + t.rectn(1, 3, 3, 2, "brown"); t.rectn(12, 3, 3, 2, "brown") + t.rectn(3, 3, 10, 9, "brown"); t.rectn(3, 3, 10, 1, "brown_l") + t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink") -- muzzle + nose + t.rectn(4, 12, 8, 7, "brown"); t.rectn(4, 12, 8, 3, "honey") -- vest + t.pn(7, 13, "honey_d"); t.pn(8, 13, "honey_d") + arms(t, "brown"); feet(t, "brown_d") + expr(t, ex, 5, 6, 10, 6, 10) +end + +local function yuanyuan(t, ex) -- 兔面包师 rabbit baker + t.rectn(4, 0, 2, 5, "cream"); t.rectn(10, 0, 2, 5, "cream") -- long ears + t.rectn(5, 1, 1, 3, "pink"); t.rectn(10, 1, 1, 3, "pink") -- inner ear + t.rectn(3, 4, 10, 8, "cream"); t.rectn(3, 4, 10, 1, "white") + t.pn(7, 9, "pink_d") -- bunny nose + t.rectn(4, 12, 8, 7, "pink") -- dress + t.rectn(4, 12, 8, 5, "white") -- apron + t.rectn(4, 12, 8, 1, "cream_d") + t.pn(6, 14, "cream_d"); t.pn(9, 14, "cream_d") + arms(t, "cream"); feet(t, "cream_d") + expr(t, ex, 5, 7, 10, 7, 10) +end + +local function dabao(t, ex) -- 熊邮差 bear postman + t.discn(4, 3, 2, "brown"); t.discn(11, 3, 2, "brown") -- round ears + t.discn(4, 3, 1, "brown_d"); t.discn(11, 3, 1, "brown_d") + t.rectn(2, 3, 12, 9, "brown"); t.rectn(2, 3, 12, 1, "brown_l") -- big head + t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink") + t.rectn(3, 12, 10, 7, "brown") -- big body + t.pn(4, 12, "brown_d"); t.pn(5, 13, "brown_d"); t.pn(6, 14, "brown_d") + t.pn(7, 15, "brown_d"); t.pn(8, 16, "brown_d") -- satchel strap + t.rectn(9, 15, 4, 4, "yellow_d"); t.framen(9, 15, 4, 4, "brown_d"); t.rectn(9, 15, 4, 1, "yellow") + t.rectn(1, 12, 2, 5, "brown"); t.rectn(13, 12, 2, 5, "brown") -- arms (wider) + feet(t, "brown_d") + expr(t, ex, 5, 6, 10, 6, 10) +end + +local function mimi(t, ex) -- 猫店主 cat shopkeeper + t.rectn(3, 2, 2, 3, "honey"); t.rectn(11, 2, 2, 3, "honey") -- pointy ears + t.pn(3, 2, "honey_d"); t.pn(12, 2, "honey_d") + t.pn(3, 3, "pink"); t.pn(12, 3, "pink") + t.rectn(3, 4, 10, 8, "honey"); t.rectn(3, 4, 10, 1, "honey_l") + t.pn(4, 4, "honey_d"); t.pn(7, 4, "honey_d"); t.pn(10, 4, "honey_d") -- stripes + t.pn(7, 9, "pink_d") -- nose + t.rectn(4, 12, 8, 7, "cream") + t.rectn(4, 12, 8, 4, "sky"); t.rectn(4, 12, 8, 1, "sky_d") -- apron + arms(t, "honey"); feet(t, "honey_d") + t.pn(1, 8, "honey_d"); t.pn(14, 8, "honey_d") -- whiskers + expr(t, ex, 5, 7, 10, 7, 10) +end + +local function fuyisheng(t, ex) -- 鹿医生 deer doctor + t.vln(4, 0, 3, "brown_d"); t.pn(3, 1, "brown_d"); t.pn(5, 1, "brown_d") -- antlers + t.vln(11, 0, 3, "brown_d"); t.pn(10, 1, "brown_d"); t.pn(12, 1, "brown_d") + t.rectn(2, 4, 2, 3, "cream_d"); t.rectn(12, 4, 2, 3, "cream_d") -- ears + t.rectn(3, 3, 10, 9, "cream_d"); t.rectn(3, 3, 10, 1, "skin") + t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink") -- muzzle + t.rectn(3, 12, 10, 7, "white"); t.framen(3, 12, 10, 7, "w_lo") -- white coat + t.vln(8, 12, 7, "w_lo") + t.rectn(5, 12, 6, 2, "skin") + t.pn(11, 14, "red"); t.pn(11, 15, "red") -- red cross hint + arms(t, "white"); feet(t, "brown_d") + expr(t, ex, 5, 6, 10, 6, 10) +end + +local function guiyeye(t, ex) -- 龟老者 turtle elder + t.rectn(6, 4, 6, 5, "leaf_l"); t.rectn(6, 4, 6, 1, "cream") -- head + t.rectn(6, 8, 6, 2, "white") -- beard + t.discn(8, 14, 6, "leaf_d") -- shell + t.discn(8, 13, 5, "leaf") + t.discn(6, 12, 2, "leaf_l") + t.pn(8, 12, "leaf_dd"); t.pn(6, 14, "leaf_dd"); t.pn(10, 14, "leaf_dd"); t.pn(8, 16, "leaf_dd") + t.rectn(4, 20, 3, 3, "leaf_d"); t.rectn(9, 20, 3, 3, "leaf_d") -- feet + t.vln(13, 12, 9, "brown_d"); t.pn(13, 11, "brown") -- cane + expr(t, ex, 7, 6, 10, 6, 8, "ink", "brown_d") +end + +-- ---- build the 6 three-frame NPC sprites ---------------------------------- +local npcs = { + { "afu", afu }, { "yuanyuan", yuanyuan }, { "dabao", dabao }, + { "mimi", mimi }, { "fuyisheng", fuyisheng }, { "guiyeye", guiyeye }, +} +for _, n in ipairs(npcs) do + local spr, img = H.new(48, 24) + for i, ex in ipairs({ "smile", "flat", "off" }) do + n[2](H.at(img, (i - 1) * 16, 0), ex) + end + spr:saveAs(ROOT .. "public/assets/sprites/npc_" .. n[1] .. ".png") + print("npc_" .. n[1] .. ".png done") +end + +-- ---- twin birds (32x24, 2 frames = beak closed / open) --------------------- +local function bird(t, open) + t.rectn(0, 18, 16, 2, "brown_d") -- branch + -- bird A (faces right) + t.discn(4, 11, 4, "blue"); t.discn(4, 8, 3, "blue") + t.discn(3, 10, 2, "blue_l") + t.discn(5, 11, 2, "blue_d") -- wing + t.pn(1, 12, "blue_d"); t.pn(0, 13, "blue_d") -- tail + t.pn(3, 7, "ink") -- eye + if open then t.pn(6, 8, "yellow"); t.pn(6, 9, "yellow_d") else t.pn(6, 8, "yellow") end + t.vln(3, 15, 3, "honey_d"); t.vln(5, 15, 3, "honey_d") + -- bird B (faces left), slightly lower + t.discn(11, 13, 4, "blue"); t.discn(11, 10, 3, "blue") + t.discn(12, 12, 2, "blue_l") + t.discn(10, 13, 2, "blue_d") + t.pn(14, 14, "blue_d"); t.pn(15, 15, "blue_d") + t.pn(12, 9, "ink") + if open then t.pn(9, 10, "yellow"); t.pn(9, 11, "yellow_d") else t.pn(9, 10, "yellow") end + t.vln(10, 16, 2, "honey_d"); t.vln(12, 16, 2, "honey_d") +end +local bspr, bimg = H.new(32, 24) +bird(H.at(bimg, 0, 0), false) +bird(H.at(bimg, 16, 0), true) +bspr:saveAs(ROOT .. "public/assets/sprites/npc_bird.png") +print("npc_bird.png done") diff --git a/Kimi-K3/tools/aseprite/gen_player.lua b/Kimi-K3/tools/aseprite/gen_player.lua new file mode 100644 index 0000000..a452945 --- /dev/null +++ b/Kimi-K3/tools/aseprite/gen_player.lua @@ -0,0 +1,96 @@ +-- gen_player.lua — public/assets/sprites/player.png +-- 64x96; frame 16x24; cols 0-3 = walk frames (col 0 also idle); rows 0=down 1=up 2=left 3=right +-- Gender-neutral chibi, cream / honey-orange palette, gentle "healing" look. +local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/" +local H = dofile(ROOT .. "tools/aseprite/helpers.lua") + +local spr, img = H.new(64, 96) + +-- step: -1 left step, 0 stand, +1 right step +local function stepOf(f) + if f == 1 then return -1 elseif f == 3 then return 1 else return 0 end +end + +-- legs shared by down/up (front/back view alternates a lifted foot) +local function legsFB(t, step) + if step == 0 then + t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(9, 17, 2, 5, "cream_d") + t.rectn(4, 22, 3, 2, "brown"); t.rectn(9, 22, 3, 2, "brown") + elseif step == -1 then + t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(4, 22, 3, 2, "brown") -- left planted + t.rectn(9, 17, 2, 4, "cream_d"); t.rectn(9, 21, 3, 2, "brown") -- right lifted + else + t.rectn(5, 17, 2, 4, "cream_d"); t.rectn(4, 21, 3, 2, "brown") -- left lifted + t.rectn(9, 17, 2, 5, "cream_d"); t.rectn(9, 22, 3, 2, "brown") -- right planted + end +end + +local function drawDown(t, f) + local s = stepOf(f) + t.rectn(4, 2, 8, 3, "hair") + t.rectn(3, 3, 2, 6, "hair"); t.rectn(11, 3, 2, 6, "hair") + t.rectn(4, 4, 8, 6, "skin") + t.rectn(4, 2, 8, 1, "hair_l") + t.pn(5, 7, "ink"); t.pn(10, 7, "ink") + t.pn(6, 9, "skin_d"); t.pn(9, 9, "skin_d") + t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 11, 8, 1, "honey_l") + -- arms swing opposite to step + if s == 1 then t.rectn(3, 11, 2, 5, "honey"); t.rectn(3, 16, 2, 2, "skin") + else t.rectn(2, 11, 2, 5, "honey"); t.rectn(2, 16, 2, 2, "skin") end + if s == -1 then t.rectn(11, 11, 2, 5, "honey"); t.rectn(11, 16, 2, 2, "skin") + else t.rectn(12, 11, 2, 5, "honey"); t.rectn(12, 16, 2, 2, "skin") end + legsFB(t, s) +end + +local function drawUp(t, f) + local s = stepOf(f) + t.rectn(3, 2, 10, 7, "hair") + t.rectn(4, 2, 8, 2, "hair_l") + t.rectn(3, 8, 10, 1, "hair_d") + t.rectn(6, 9, 4, 2, "skin_d") + t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 16, 8, 1, "honey_d") + t.rectn(2, 11, 2, 5, "honey"); t.rectn(12, 11, 2, 5, "honey") + t.rectn(2, 16, 2, 2, "skin"); t.rectn(12, 16, 2, 2, "skin") + legsFB(t, s) +end + +local function drawLeft(t, f) + local s = stepOf(f) + -- hair (back of head toward the right / behind) + t.rectn(5, 2, 7, 3, "hair"); t.rectn(5, 2, 7, 1, "hair_l") + t.rectn(9, 3, 3, 6, "hair") + -- face (pointing left) + t.rectn(4, 4, 7, 6, "skin") + t.pn(5, 7, "ink") + t.pn(4, 8, "skin_d") + -- torso + t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 11, 8, 1, "honey_l") + -- front arm swings with step + if s == -1 then t.rectn(2, 11, 2, 5, "honey"); t.rectn(2, 16, 2, 2, "skin") + elseif s == 1 then t.rectn(5, 11, 2, 5, "honey"); t.rectn(5, 16, 2, 2, "skin") + else t.rectn(3, 11, 2, 5, "honey"); t.rectn(3, 16, 2, 2, "skin") end + -- profile legs scissor horizontally + if s == 0 then + t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(8, 17, 2, 5, "cream_d") + t.rectn(4, 22, 3, 2, "brown"); t.rectn(7, 22, 3, 2, "brown") + elseif s == -1 then + t.rectn(4, 17, 2, 5, "cream_d"); t.rectn(3, 22, 3, 2, "brown") -- front leg forward + t.rectn(9, 17, 2, 4, "cream_d"); t.rectn(8, 21, 3, 2, "brown") -- back leg lifted + else + t.rectn(6, 17, 2, 4, "cream_d"); t.rectn(5, 21, 3, 2, "brown") -- back leg lifted + t.rectn(9, 17, 2, 5, "cream_d"); t.rectn(8, 22, 3, 2, "brown") -- front leg forward + end +end + +for row = 0, 3 do + for col = 0, 3 do + local t = H.at(img, col * 16, row * 24) + if row == 0 then drawDown(t, col) + elseif row == 1 then drawUp(t, col) + elseif row == 2 then drawLeft(t, col) + else drawLeft(H.mirrorX(t, 16), col) end + end +end + +spr:saveAs(ROOT .. "public/assets/sprites/player.png") +print("player.png done") diff --git a/Kimi-K3/tools/aseprite/gen_portraits.lua b/Kimi-K3/tools/aseprite/gen_portraits.lua new file mode 100644 index 0000000..5028661 --- /dev/null +++ b/Kimi-K3/tools/aseprite/gen_portraits.lua @@ -0,0 +1,130 @@ +-- gen_portraits.lua — dialogue portraits (each 32x32) +-- player_smile / player_real + afu yuanyuan dabao mimi fuyisheng guiyeye bird (all smile) +local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/" +local H = dofile(ROOT .. "tools/aseprite/helpers.lua") + +local function smileArc(t, cx, y, w, c) + local half = math.floor(w / 2) + t.pn(cx - half, y, c); t.pn(cx + half, y, c) + t.pn(cx - half + 1, y + 1, c); t.pn(cx + half - 1, y + 1, c) + for i = -half + 2, half - 2 do t.pn(cx + i, y + 2, c) end +end +local function eyes(t, lx, rx, y, c) + t.rectn(lx, y, 3, 4, c); t.rectn(rx, y, 3, 4, c) +end + +-- ---- player --------------------------------------------------------------- +local function playerSmile(t) + t.rectn(6, 2, 20, 6, "hair"); t.rectn(6, 2, 20, 2, "hair_l") + t.rectn(4, 4, 4, 14, "hair"); t.rectn(24, 4, 4, 14, "hair") + t.rectn(6, 7, 20, 18, "skin") + eyes(t, 9, 20, 13, "ink") + t.pn(8, 18, "pink"); t.pn(9, 18, "pink"); t.pn(23, 18, "pink"); t.pn(24, 18, "pink") + smileArc(t, 16, 20, 8, "brown_d") + t.rectn(12, 25, 8, 4, "honey"); t.rectn(12, 25, 8, 1, "honey_l") +end + +-- E3 "real face": the smile is gone — a plain, tired, honest face. +local function playerReal(t) + t.rectn(6, 2, 20, 6, "hair"); t.rectn(6, 2, 20, 2, "hair_l") + t.rectn(4, 4, 4, 14, "hair"); t.rectn(24, 4, 4, 14, "hair") + t.rectn(6, 7, 20, 18, "skin") + t.rectn(9, 15, 3, 2, "ink"); t.rectn(20, 15, 3, 2, "ink") -- half-closed eyes + t.rectn(9, 14, 3, 1, "skin_d"); t.rectn(20, 14, 3, 1, "skin_d") -- heavy lids + t.pn(9, 18, "skin_d"); t.pn(10, 18, "skin_d") -- bags + t.pn(21, 18, "skin_d"); t.pn(22, 18, "skin_d") + t.pn(10, 19, "cream_d"); t.pn(21, 19, "cream_d") + t.pn(9, 12, "hair_d"); t.pn(10, 12, "hair_d"); t.pn(11, 13, "hair_d") -- weary brows + t.pn(22, 12, "hair_d"); t.pn(21, 12, "hair_d"); t.pn(20, 13, "hair_d") + t.hln(13, 21, 6, "brown_d") -- flat mouth + t.rectn(12, 25, 8, 4, "honey"); t.rectn(12, 25, 8, 1, "honey_d") +end + +-- ---- NPC smile portraits --------------------------------------------------- +local function afuP(t) + t.rectn(2, 6, 6, 15, "brown_d"); t.rectn(24, 6, 6, 15, "brown_d") + t.rectn(2, 6, 6, 5, "brown"); t.rectn(24, 6, 6, 5, "brown") + t.rectn(6, 4, 20, 23, "brown"); t.rectn(6, 4, 20, 3, "brown_l") + eyes(t, 9, 20, 11, "ink") + t.rectn(11, 16, 10, 8, "cream"); t.rectn(14, 16, 4, 3, "ink") + smileArc(t, 16, 20, 7, "brown_d") + t.rectn(12, 27, 8, 3, "honey") +end + +local function yuanyuanP(t) + t.rectn(8, 0, 4, 9, "cream"); t.rectn(20, 0, 4, 9, "cream") + t.rectn(9, 1, 2, 7, "pink"); t.rectn(21, 1, 2, 7, "pink") + t.rectn(6, 8, 20, 19, "cream"); t.rectn(6, 8, 20, 2, "white") + eyes(t, 10, 20, 14, "ink") + t.pn(15, 18, "pink_d"); t.pn(16, 18, "pink_d") + smileArc(t, 16, 20, 6, "brown_d") + t.rectn(12, 27, 8, 3, "pink") +end + +local function dabaoP(t) + t.discn(7, 6, 4, "brown"); t.discn(25, 6, 4, "brown") + t.discn(7, 6, 2, "brown_d"); t.discn(25, 6, 2, "brown_d") + t.rectn(4, 4, 24, 23, "brown"); t.rectn(4, 4, 24, 3, "brown_l") + eyes(t, 9, 21, 11, "ink") + t.rectn(11, 16, 10, 8, "cream"); t.rectn(14, 16, 4, 3, "ink") + smileArc(t, 16, 20, 7, "brown_d") + t.rectn(11, 27, 10, 3, "yellow_d") +end + +local function mimiP(t) + t.pn(7, 2, "honey"); t.rectn(6, 3, 4, 6, "honey") -- left ear + t.pn(24, 2, "honey"); t.rectn(22, 3, 4, 6, "honey") -- right ear + t.rectn(7, 4, 2, 4, "pink"); t.rectn(23, 4, 2, 4, "pink") -- inner + t.rectn(6, 8, 20, 19, "honey"); t.rectn(6, 8, 20, 2, "honey_l") + t.pn(9, 8, "honey_d"); t.pn(13, 8, "honey_d"); t.pn(17, 8, "honey_d"); t.pn(21, 8, "honey_d") + eyes(t, 10, 20, 14, "ink") + t.pn(15, 18, "pink_d"); t.pn(16, 18, "pink_d") + smileArc(t, 16, 20, 6, "brown_d") + t.hln(3, 17, 3, "honey_d"); t.hln(3, 19, 3, "honey_d") + t.hln(26, 17, 3, "honey_d"); t.hln(26, 19, 3, "honey_d") + t.rectn(12, 27, 8, 3, "sky") +end + +local function fuyishengP(t) + t.vln(8, 0, 5, "brown_d"); t.pn(6, 2, "brown_d"); t.pn(7, 3, "brown_d"); t.pn(10, 1, "brown_d") + t.vln(23, 0, 5, "brown_d"); t.pn(25, 2, "brown_d"); t.pn(24, 3, "brown_d"); t.pn(21, 1, "brown_d") + t.rectn(2, 10, 4, 6, "cream_d"); t.rectn(26, 10, 4, 6, "cream_d") + t.rectn(6, 5, 20, 22, "cream_d"); t.rectn(6, 5, 20, 2, "skin") + eyes(t, 10, 20, 13, "ink") + t.rectn(11, 17, 10, 7, "cream"); t.rectn(14, 17, 4, 3, "ink") + smileArc(t, 16, 21, 6, "brown_d") + t.rectn(12, 27, 8, 3, "white"); t.pn(12, 28, "red") +end + +local function guiyeyeP(t) + t.rectn(7, 5, 18, 20, "leaf_l"); t.rectn(7, 5, 18, 2, "cream") -- bald cream top + t.rectn(9, 12, 5, 2, "white"); t.rectn(18, 12, 5, 2, "white") -- bushy brows + eyes(t, 10, 20, 14, "ink") + t.rectn(10, 20, 12, 8, "white"); t.rectn(12, 26, 8, 3, "white") -- beard + smileArc(t, 16, 19, 6, "brown_d") + t.rectn(4, 28, 24, 3, "leaf_d"); t.rectn(4, 28, 24, 1, "leaf") -- shell hint +end + +local function birdP(t) + t.discn(10, 15, 8, "blue"); t.discn(9, 12, 4, "blue_l") -- left bird + t.discn(11, 16, 3, "blue_d") + t.pn(7, 12, "ink") + t.pn(4, 15, "yellow"); t.pn(3, 16, "yellow_d") + t.discn(22, 17, 7, "blue"); t.discn(23, 14, 3, "blue_l") -- right bird + t.discn(21, 18, 3, "blue_d") + t.pn(24, 14, "ink") + t.pn(28, 16, "yellow"); t.pn(29, 17, "yellow_d") + t.rectn(6, 25, 20, 2, "brown_d") -- branch +end + +local portraits = { + { "player_smile", playerSmile }, { "player_real", playerReal }, + { "afu", afuP }, { "yuanyuan", yuanyuanP }, { "dabao", dabaoP }, + { "mimi", mimiP }, { "fuyisheng", fuyishengP }, { "guiyeye", guiyeyeP }, { "bird", birdP }, +} +for _, n in ipairs(portraits) do + local spr, img = H.new(32, 32) + n[2](H.at(img, 0, 0)) + spr:saveAs(ROOT .. "public/assets/portraits/" .. n[1] .. ".png") + print(n[1] .. ".png done") +end diff --git a/Kimi-K3/tools/aseprite/gen_tileset.lua b/Kimi-K3/tools/aseprite/gen_tileset.lua new file mode 100644 index 0000000..7f45824 --- /dev/null +++ b/Kimi-K3/tools/aseprite/gen_tileset.lua @@ -0,0 +1,878 @@ +-- gen_tileset.lua — public/assets/tileset.png (256x128, 16x16 tiles, id = row*16+col) +local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/" +local H = dofile(ROOT .. "tools/aseprite/helpers.lua") + +local spr, img = H.new(256, 128) +local tiles = {} + +-- ---- shared sub-draws ------------------------------------------------------ +local function grassBase(t) + t.rectn(0, 0, 16, 16, "grass") + t.pn(3, 2, "grass_l"); t.pn(11, 5, "grass_l"); t.pn(6, 9, "grass_l") + t.pn(13, 12, "grass_l"); t.pn(1, 13, "grass_l"); t.pn(9, 3, "grass_l") + t.pn(5, 4, "grass_d"); t.pn(6, 4, "grass_d"); t.pn(6, 5, "grass_d") + t.pn(10, 11, "grass_d"); t.pn(11, 11, "grass_d"); t.pn(2, 8, "grass_d") + t.pn(14, 3, "grass_d"); t.pn(9, 14, "grass_d"); t.pn(12, 8, "grass_d") +end + +local function roof(t, base, d, l, edge) + t.rectn(0, 0, 16, 16, base) + t.hln(0, 4, 16, d); t.hln(0, 9, 16, d); t.hln(0, 14, 16, d) + for x = 2, 15, 4 do t.vln(x, 5, 4, d) end + for x = 0, 15, 4 do t.vln(x, 10, 4, d) end + t.hln(0, 0, 16, l) + t.hln(0, 15, 16, d) + if edge == "L" then t.vln(0, 0, 16, d); t.vln(1, 0, 16, base) end + if edge == "R" then t.vln(15, 0, 16, d); t.vln(14, 0, 16, base) end +end + +-- =========================================================================== +-- ROW 0 (ids 0-15) : ground / water / flowerbeds +-- =========================================================================== +tiles[0] = function(t) grassBase(t) end + +tiles[1] = function(t) + grassBase(t) + H.flower(t, 8, 7, "pink", "yellow") + H.flower(t, 4, 11, "white", "yellow") + t.pn(12, 4, "pink"); t.pn(12, 5, "pink"); t.pn(11, 4, "pink") +end + +tiles[2] = function(t) + grassBase(t) + H.flower(t, 6, 6, "yellow", "honey_d") + H.flower(t, 11, 10, "red", "yellow") + t.pn(3, 5, "white"); t.pn(4, 5, "white") +end + +tiles[3] = function(t) + t.rectn(0, 0, 16, 16, "grass_d") + t.pn(3, 3, "grass"); t.pn(10, 6, "grass"); t.pn(6, 11, "grass"); t.pn(13, 13, "grass") + t.pn(5, 5, "grass_dd"); t.pn(6, 5, "grass_dd"); t.pn(11, 11, "grass_dd") + t.pn(2, 9, "grass_dd"); t.pn(14, 4, "grass_dd"); t.pn(9, 14, "grass_dd") +end + +tiles[4] = function(t) -- dirt road + t.rectn(0, 0, 16, 16, "dirt") + t.pn(3, 3, "dirt_d"); t.pn(4, 3, "dirt_d"); t.pn(11, 6, "dirt_d") + t.pn(7, 10, "dirt_d"); t.pn(13, 12, "dirt_d"); t.pn(2, 13, "dirt_d") + t.pn(6, 4, "dirt_l"); t.pn(12, 8, "dirt_l"); t.pn(9, 13, "dirt_l"); t.pn(1, 6, "dirt_l") +end + +tiles[5] = function(t) -- stone road + t.rectn(0, 0, 16, 16, "stone") + t.hln(0, 0, 16, "stone_d"); t.hln(0, 8, 16, "stone_d") + t.vln(0, 0, 8, "stone_d"); t.vln(8, 0, 8, "stone_d") + t.vln(4, 8, 8, "stone_d"); t.vln(12, 8, 8, "stone_d") + t.pn(2, 3, "stone_l"); t.pn(10, 3, "stone_l"); t.pn(6, 11, "stone_l"); t.pn(13, 12, "stone_l") +end + +tiles[6] = function(t) -- stone road alt + t.rectn(0, 0, 16, 16, "stone") + t.hln(0, 0, 16, "stone_dd"); t.hln(0, 8, 16, "stone_dd") + t.vln(5, 0, 8, "stone_dd"); t.vln(13, 0, 8, "stone_dd") + t.vln(1, 8, 8, "stone_dd"); t.vln(9, 8, 8, "stone_dd") + t.pn(3, 3, "stone_l"); t.pn(11, 4, "stone_l"); t.pn(4, 11, "stone_l"); t.pn(12, 12, "stone_l") + t.pn(8, 2, "stone_d"); t.pn(7, 12, "stone_d") +end + +tiles[7] = function(t) -- plaza brick + t.rectn(0, 0, 16, 16, "stone_l") + t.hln(0, 0, 16, "stone_d"); t.hln(0, 8, 16, "stone_d") + t.vln(0, 0, 16, "stone_d"); t.vln(8, 0, 16, "stone_d") + t.pn(1, 1, "stone_dd"); t.pn(9, 1, "stone_dd"); t.pn(1, 9, "stone_dd"); t.pn(9, 9, "stone_dd") +end + +tiles[8] = function(t) -- lake water A + t.rectn(0, 0, 16, 16, "water") + t.hln(2, 4, 3, "water_l"); t.hln(9, 4, 3, "water_l") + t.hln(5, 11, 3, "water_l"); t.hln(12, 11, 2, "water_l") + t.pn(7, 7, "water_d"); t.pn(13, 2, "water_d"); t.pn(3, 13, "water_d") +end + +tiles[9] = function(t) -- lake water B + t.rectn(0, 0, 16, 16, "water_d") + t.hln(3, 5, 3, "water"); t.hln(10, 5, 3, "water") + t.hln(6, 12, 3, "water"); t.hln(12, 12, 2, "water") + t.pn(8, 8, "water_dd"); t.pn(2, 3, "water_dd"); t.pn(13, 13, "water_dd") +end + +-- water banks (shore toward the named direction) +local function bankH(t, shoreTop) + t.rectn(0, 0, 16, 16, "water") + local by = { 6, 6, 7, 7, 6, 6, 5, 6, 6, 7, 7, 6, 6, 5, 6, 6 } + for x = 0, 15 do + local b = by[x + 1] + if shoreTop then + t.rectn(x, 0, 1, b, "stone_l") + t.pn(x, b, "cream_d") + t.pn(x, b + 1, "water_l") + else + t.rectn(x, 16 - b, 1, b, "stone_l") + t.pn(x, 15 - b, "cream_d") + t.pn(x, 14 - b, "water_l") + end + end +end +local function bankV(t, shoreLeft) + t.rectn(0, 0, 16, 16, "water") + local bx = { 6, 6, 7, 7, 6, 6, 5, 6, 6, 7, 7, 6, 6, 5, 6, 6 } + for y = 0, 15 do + local b = bx[y + 1] + if shoreLeft then + t.rectn(0, y, b, 1, "stone_l") + t.pn(b, y, "cream_d") + t.pn(b + 1, y, "water_l") + else + t.rectn(16 - b, y, b, 1, "stone_l") + t.pn(15 - b, y, "cream_d") + t.pn(14 - b, y, "water_l") + end + end +end +tiles[10] = function(t) bankH(t, true) end -- 水岸北 (shore north/top) +tiles[11] = function(t) bankH(t, false) end -- 水岸南 +tiles[12] = function(t) bankV(t, true) end -- 水岸西 +tiles[13] = function(t) bankV(t, false) end -- 水岸东 + +local function flowerbed(t, c1, c2) + t.rectn(0, 0, 16, 16, "dirt_d") + t.rectn(0, 0, 16, 16, "dirt") + t.framen(0, 0, 16, 16, "dirt_d") + t.pn(3, 6, "leaf"); t.pn(10, 7, "leaf"); t.pn(7, 11, "leaf"); t.pn(13, 5, "leaf"); t.pn(5, 12, "leaf") + H.flower(t, 4, 5, c1, "yellow") + H.flower(t, 10, 4, c1, "yellow") + H.flower(t, 7, 10, c2, "yellow") + H.flower(t, 13, 9, c1, "yellow") + H.flower(t, 3, 12, c2, "yellow") +end +tiles[14] = function(t) flowerbed(t, "pink", "pink_l") end +tiles[15] = function(t) flowerbed(t, "yellow", "yellow_l") end + +-- =========================================================================== +-- ROW 1 (ids 16-31) : bushes / tree / fence / lamp / bunting +-- =========================================================================== +local function bush(t, berries) + t.ellipsen(8, 14, 6, 2, "grass_dd") + t.discn(8, 8, 6, "leaf") + t.discn(6, 6, 3, "leaf_l") + t.discn(10, 10, 3, "leaf_d") + t.discn(8, 11, 3, "leaf_d") + if berries then + t.pn(6, 7, "berry"); t.pn(9, 5, "berry"); t.pn(11, 9, "berry") + t.pn(7, 10, "berry"); t.pn(10, 11, "berry"); t.pn(5, 9, "berry"); t.pn(9, 8, "berry_d") + end +end +tiles[16] = function(t) bush(t, true) end +tiles[17] = function(t) bush(t, false) end + +-- tree (18-21) drawn once across its 2x2 block (origin of tile 18) +local function drawTree(img) + local tr = H.at(img, (18 % 16) * 16, math.floor(18 / 16) * 16) + tr.ellipsen(16, 31, 8, 1, "grass_dd") + tr.rectn(14, 21, 4, 11, "brown") + tr.vln(14, 21, 11, "brown_l"); tr.vln(17, 21, 11, "brown_d") + tr.discn(16, 12, 13, "leaf") + tr.discn(16, 16, 6, "leaf_d") + tr.discn(21, 15, 5, "leaf_d") + tr.discn(12, 8, 6, "leaf_l") + tr.discn(9, 13, 3, "leaf") + tr.discn(23, 11, 3, "leaf") +end + +tiles[22] = function(t) -- round bush + t.ellipsen(8, 14, 5, 2, "grass_dd") + t.discn(8, 9, 5, "leaf") + t.discn(6, 7, 2, "leaf_l") + t.discn(10, 11, 2, "leaf_d") +end + +tiles[23] = function(t) -- tree stump + t.ellipsen(8, 14, 6, 2, "brown_d") + t.rectn(4, 7, 8, 7, "brown") + t.vln(4, 7, 7, "brown_d"); t.vln(11, 7, 7, "brown_dd") + t.ellipsen(8, 7, 4, 2, "wood_l") + t.ellipsen(8, 7, 2, 1, "wood_d") + t.pn(8, 7, "brown_dd") +end + +tiles[24] = function(t) -- rock + t.ellipsen(8, 13, 7, 2, "grey_d") + t.ellipsen(8, 9, 6, 4, "grey") + t.ellipsen(6, 8, 3, 2, "grey_l") + t.ellipsen(10, 11, 3, 2, "grey_d") +end + +tiles[25] = function(t) -- signpost + t.rectn(7, 9, 2, 7, "brown_d") + t.rectn(3, 3, 10, 6, "wood") + t.framen(3, 3, 10, 6, "brown_d") + t.hln(4, 5, 8, "brown_d"); t.hln(4, 7, 6, "brown_d") + t.pn(12, 4, "brown_dd") +end + +tiles[26] = function(t) -- fence horizontal + t.rectn(1, 4, 3, 11, "brown"); t.rectn(12, 4, 3, 11, "brown") + t.pn(1, 4, "brown_l"); t.pn(12, 4, "brown_l") + t.hln(0, 7, 16, "wood"); t.hln(0, 8, 16, "wood_d") + t.hln(0, 11, 16, "wood"); t.hln(0, 12, 16, "wood_d") + t.vln(1, 4, 11, "brown_d"); t.vln(12, 4, 11, "brown_d") +end + +tiles[27] = function(t) -- fence vertical + t.rectn(4, 0, 3, 16, "wood"); t.rectn(9, 0, 3, 16, "wood") + t.vln(4, 0, 16, "wood_d"); t.vln(9, 0, 16, "wood_d") + t.rectn(2, 6, 12, 3, "brown") + t.hln(2, 6, 12, "brown_l"); t.hln(2, 8, 12, "brown_d") +end + +tiles[28] = function(t) -- street lamp + t.rectn(7, 6, 2, 10, "grey_d") + t.rectn(6, 15, 4, 1, "grey") + t.rectn(5, 3, 6, 4, "metal") + t.framen(5, 3, 6, 4, "grey_d") + t.rectn(6, 4, 4, 2, "yellow_l") + t.pn(7, 2, "yellow"); t.pn(8, 2, "yellow") + t.pn(6, 8, "grey") +end + +local function bunting(t, c) + t.hln(0, 3, 16, "brown_d") + t.hln(4, 4, 8, c) + t.hln(5, 5, 6, c) + t.hln(6, 6, 4, c) + t.hln(7, 7, 2, c) + t.pn(8, 8, c) +end +tiles[29] = function(t) bunting(t, "red") end +tiles[30] = function(t) bunting(t, "yellow") end +tiles[31] = function(t) bunting(t, "blue") end + +-- =========================================================================== +-- ROW 2 (ids 32-47) : buildings / well / mailbox / noticeboard +-- =========================================================================== +local function creamWall(t) + t.rectn(0, 0, 16, 16, "cream") + t.hln(0, 0, 16, "cream_d"); t.hln(0, 8, 16, "cream_d") + t.vln(0, 0, 8, "cream_d"); t.vln(8, 0, 8, "cream_d") + t.vln(4, 8, 8, "cream_d"); t.vln(12, 8, 8, "cream_d") + t.pn(2, 4, "cream_dd"); t.pn(10, 12, "cream_dd") +end +tiles[32] = function(t) creamWall(t) end +tiles[33] = function(t) + creamWall(t) + t.rectn(4, 3, 8, 9, "cream") -- clear brick under window + H.window(t, 4, 3, 8, 9, "sky") +end + +tiles[34] = function(t) -- wood plank wall + t.rectn(0, 0, 16, 16, "wood") + for y = 0, 15, 4 do + t.hln(0, y, 16, "wood_d"); t.hln(0, y + 1, 16, "wood_l") + end + t.pn(2, 3, "wood_dd"); t.pn(13, 3, "wood_dd"); t.pn(2, 7, "wood_dd") + t.pn(13, 7, "wood_dd"); t.pn(2, 11, "wood_dd"); t.pn(13, 11, "wood_dd"); t.pn(2, 15, "wood_dd") +end + +tiles[35] = function(t) roof(t, "honey", "honey_d", "honey_l", "L") end +tiles[36] = function(t) roof(t, "honey", "honey_d", "honey_l", nil) end +tiles[37] = function(t) roof(t, "honey", "honey_d", "honey_l", "R") end + +tiles[38] = function(t) -- door (lower)(两侧补墙色:室内门放在 ground 层,透明边会露黑底) + t.rectn(0, 0, 3, 16, "cream"); t.rectn(13, 0, 3, 16, "cream") + t.vln(3, 0, 16, "brown_d"); t.vln(12, 0, 16, "brown_d") + t.rectn(4, 0, 8, 16, "wood") + t.rectn(5, 2, 6, 5, "wood_d"); t.rectn(5, 9, 6, 5, "wood_d") + t.vln(5, 0, 16, "wood_l") + t.pn(10, 7, "yellow"); t.pn(10, 8, "yellow_d") + t.hln(4, 15, 8, "wood_d") +end + +tiles[39] = function(t) -- door frame top (arch), sits above tile 38 + t.vln(3, 4, 12, "brown_d"); t.vln(12, 4, 12, "brown_d") + t.rectn(4, 6, 8, 10, "wood") + t.vln(5, 6, 10, "wood_l") + -- arch + t.discn(8, 6, 5, "brown_d") + t.discn(8, 6, 4, "sky") + t.rectn(4, 6, 8, 3, "sky") + t.discn(8, 6, 3, "sky") + t.pn(6, 5, "sky_d"); t.pn(9, 4, "sky_d") + t.hln(4, 6, 8, "brown_d") +end + +tiles[40] = function(t) -- standalone window (transparent surround) + H.window(t, 3, 4, 10, 8, "sky") + t.pn(4, 5, "white") +end + +tiles[41] = function(t) -- chimney + t.rectn(5, 3, 6, 13, "swall_d") + t.hln(5, 6, 6, "swall_dd"); t.hln(5, 9, 6, "swall_dd"); t.hln(5, 12, 6, "swall_dd") + t.vln(7, 3, 3, "swall_dd"); t.vln(6, 6, 3, "swall_dd"); t.vln(8, 9, 3, "swall_dd") + t.rectn(4, 2, 8, 2, "swall") + t.hln(4, 2, 8, "swall_l") + t.discn(6, 1, 1, "w_mid"); t.discn(9, 0, 1, "w_lo") +end + +-- well (42-45) drawn once across its 2x2 block (origin of tile 42) +local function drawWell(img) + local w = H.at(img, (42 % 16) * 16, math.floor(42 / 16) * 16) + w.ellipsen(16, 28, 12, 3, "swall_dd") + w.discn(16, 16, 12, "swall") + w.discn(16, 14, 11, "swall_l") + w.discn(16, 19, 10, "swall_d") + w.discn(16, 15, 7, "swall_dd") + w.discn(16, 16, 6, "black") + w.discn(16, 17, 4, "water_d") + w.rectn(3, 9, 26, 2, "wood_d") + w.hln(3, 9, 26, "wood") + w.vln(16, 11, 7, "brown_dd") + w.rectn(14, 17, 5, 4, "wood") + w.framen(14, 17, 5, 4, "wood_d") +end + +tiles[46] = function(t) -- mailbox + t.rectn(6, 9, 2, 7, "brown_d") + t.rectn(2, 15, 6, 1, "brown_dd") + t.rectn(3, 3, 10, 6, "red") + t.framen(3, 3, 10, 6, "red_d") + t.rectn(3, 3, 10, 2, "red_l") + t.pn(7, 6, "yellow"); t.pn(8, 6, "yellow") + t.rectn(5, 8, 6, 1, "red_d") +end + +tiles[47] = function(t) -- notice board + t.rectn(3, 12, 2, 4, "brown_d"); t.rectn(11, 12, 2, 4, "brown_d") + t.rectn(1, 1, 14, 2, "brown_d") + t.rectn(2, 2, 12, 10, "wood") + t.framen(2, 2, 12, 10, "brown_d") + t.rectn(4, 4, 3, 4, "cream"); t.rectn(8, 4, 4, 3, "cream") + t.rectn(4, 8, 6, 3, "cream_d") + t.pn(5, 5, "ink"); t.pn(6, 5, "ink"); t.pn(9, 5, "ink"); t.pn(10, 5, "ink"); t.pn(5, 9, "ink"); t.pn(7, 9, "ink") +end + +-- =========================================================================== +-- ROW 3 (ids 48-63) : interior floor / walls / furniture +-- =========================================================================== +tiles[48] = function(t) -- wood floor A (horizontal planks) + t.rectn(0, 0, 16, 16, "wood_l") + for y = 0, 15, 4 do t.hln(0, y, 16, "wood") end + t.pn(3, 2, "wood"); t.pn(10, 6, "wood"); t.pn(6, 10, "wood"); t.pn(13, 14, "wood") +end + +tiles[49] = function(t) -- wood floor B (vertical planks) + t.rectn(0, 0, 16, 16, "wood_l") + for x = 0, 15, 4 do t.vln(x, 0, 16, "wood") end + t.pn(2, 3, "wood"); t.pn(6, 9, "wood"); t.pn(10, 5, "wood"); t.pn(14, 12, "wood") +end + +local function indoorWall(t) + t.rectn(0, 0, 16, 16, "cream") + t.hln(0, 12, 16, "cream_d") + t.rectn(0, 13, 16, 3, "wood_d") + t.hln(0, 13, 16, "wood_l") +end +tiles[50] = function(t) indoorWall(t) end +tiles[51] = function(t) + t.rectn(0, 0, 16, 16, "cream") + H.window(t, 4, 2, 8, 8, "sky") + t.hln(0, 12, 16, "cream_d") + t.rectn(0, 13, 16, 3, "wood_d") + t.hln(0, 13, 16, "wood_l") +end + +-- bed (52-55) drawn once across 2x2 (origin of tile 52) +local function drawBed(img) + local b = H.at(img, (52 % 16) * 16, math.floor(52 / 16) * 16) + b.rectn(1, 2, 30, 29, "wood_d") + b.framen(1, 2, 30, 29, "brown_dd") + b.rectn(1, 2, 30, 5, "wood") + b.framen(1, 2, 30, 5, "wood_d") + b.rectn(3, 7, 26, 22, "cream") + b.rectn(4, 8, 24, 6, "white"); b.framen(4, 8, 24, 6, "cream_d") + b.rectn(3, 16, 26, 13, "blue") + b.framen(3, 16, 26, 13, "blue_d") + b.rectn(3, 16, 26, 2, "blue_l") + b.rectn(1, 28, 30, 3, "wood") +end + +local function counter(t) + t.rectn(0, 0, 16, 3, "cream") + t.hln(0, 2, 16, "cream_d") + t.rectn(0, 3, 16, 13, "wood") + t.vln(0, 3, 13, "wood_d"); t.vln(8, 3, 13, "wood_d") + t.rectn(2, 8, 12, 5, "wood_d"); t.framen(2, 8, 12, 5, "wood_dd") + t.hln(0, 3, 16, "wood_l") +end +tiles[56] = function(t) counter(t) end +tiles[57] = function(t) + counter(t) + t.rectn(9, 0, 5, 2, "honey") -- a little bread/register on the right half + t.pn(10, 0, "honey_d"); t.pn(12, 0, "honey_d") +end + +tiles[58] = function(t) -- oven + t.rectn(1, 2, 14, 14, "swall_d") + t.framen(1, 2, 14, 14, "swall_dd") + t.hln(1, 5, 14, "swall_dd"); t.hln(1, 13, 14, "swall_dd") + t.discn(8, 9, 5, "swall_dd") + t.rectn(3, 9, 10, 6, "black"); t.discn(8, 9, 5, "black") + t.discn(8, 11, 3, "flame_d"); t.discn(8, 11, 2, "flame") + t.rectn(1, 2, 14, 2, "swall") +end + +local function shelf(t, full) + t.rectn(1, 2, 14, 14, "wood") + t.framen(1, 2, 14, 14, "brown_d") + t.rectn(2, 3, 12, 12, "wood_d") + t.hln(2, 7, 12, "brown_d"); t.hln(2, 11, 12, "brown_d") + if full then + t.rectn(3, 4, 2, 3, "honey"); t.rectn(6, 4, 2, 3, "pink"); t.rectn(9, 4, 3, 3, "sky") + t.rectn(3, 8, 3, 3, "red"); t.rectn(7, 8, 2, 3, "yellow"); t.rectn(10, 8, 3, 3, "leaf") + t.rectn(4, 12, 2, 3, "brown_l"); t.rectn(8, 12, 4, 3, "honey_d") + end +end +tiles[59] = function(t) shelf(t, false) end +tiles[60] = function(t) shelf(t, true) end + +tiles[61] = function(t) -- table + t.rectn(1, 4, 14, 4, "wood") + t.hln(1, 4, 14, "wood_l"); t.hln(1, 7, 14, "wood_d") + t.rectn(2, 8, 2, 7, "wood_d"); t.rectn(12, 8, 2, 7, "wood_d") + t.hln(2, 12, 12, "wood_d") +end + +tiles[62] = function(t) -- chair + t.rectn(4, 2, 8, 2, "wood"); t.hln(4, 2, 8, "wood_l") + t.rectn(4, 2, 2, 8, "wood_d") + t.rectn(4, 7, 8, 3, "wood"); t.hln(4, 9, 8, "wood_d") + t.rectn(4, 10, 2, 5, "wood_d"); t.rectn(10, 10, 2, 5, "wood_d") +end + +tiles[63] = function(t) -- rug(烘焙木地板底:放在 ground 层,透明边会露黑底) + t.rectn(0, 0, 16, 16, "wood_l") + for y = 0, 15, 4 do t.hln(0, y, 16, "wood") end + t.rectn(1, 3, 14, 10, "pink") + t.framen(1, 3, 14, 10, "pink_d") + t.framen(3, 5, 10, 6, "cream") + t.discn(8, 8, 2, "honey"); t.pn(8, 8, "honey_d") +end + +-- =========================================================================== +-- ROW 4 (ids 64-79) : papers / picnic / bench / appliances +-- =========================================================================== +tiles[64] = function(t) -- calendar + t.rectn(4, 2, 8, 11, "white"); t.framen(4, 2, 8, 11, "grey_d") + t.rectn(4, 2, 8, 2, "red"); t.pn(6, 3, "white"); t.pn(7, 3, "white") + t.hln(5, 6, 6, "grey"); t.hln(5, 8, 6, "grey"); t.hln(5, 10, 6, "grey") + t.vln(7, 5, 6, "grey"); t.vln(9, 5, 6, "grey") + t.pn(6, 9, "red"); t.pn(6, 10, "red") +end + +tiles[65] = function(t) -- recipe paper + t.rectn(4, 3, 8, 10, "cream"); t.framen(4, 3, 8, 10, "cream_d") + t.hln(5, 5, 6, "brown_d"); t.hln(5, 7, 6, "brown_d"); t.hln(5, 9, 4, "brown_d"); t.hln(5, 11, 5, "brown_d") + t.pn(10, 11, "honey"); t.pn(11, 10, "honey_d") +end + +tiles[66] = function(t) -- document paper + t.rectn(4, 2, 8, 12, "white"); t.framen(4, 2, 8, 12, "grey") + for y = 4, 9, 2 do t.hln(5, y, 6, "grey_d") end + t.discn(9, 11, 2, "red"); t.pn(9, 11, "white") +end + +-- picnic mat (67-70) drawn once across 2x2 (origin of tile 67) +local function drawPicnic(img) + local m = H.at(img, (67 % 16) * 16, math.floor(67 / 16) * 16) + for y = 0, 31, 4 do + for x = 0, 31, 4 do + local c = (((x / 4) + (y / 4)) % 2 == 0) and "red" or "white" + m.rectn(x, y, 4, 4, c) + end + end + m.framen(0, 0, 32, 32, "red_d") + m.framen(1, 1, 30, 30, "red_l") +end + +local function bench(t) + t.rectn(0, 2, 16, 2, "wood"); t.hln(0, 2, 16, "wood_l") + t.rectn(0, 5, 16, 2, "wood") + t.rectn(0, 9, 16, 3, "wood_l"); t.hln(0, 11, 16, "wood_d") + t.rectn(1, 12, 2, 4, "wood_d"); t.rectn(13, 12, 2, 4, "wood_d") + t.vln(1, 2, 3, "wood_d"); t.vln(14, 2, 3, "wood_d") +end +tiles[71] = function(t) bench(t) end +tiles[72] = function(t) bench(t) end + +tiles[73] = function(t) -- stool + t.rectn(4, 6, 8, 3, "wood"); t.hln(4, 6, 8, "wood_l"); t.hln(4, 8, 8, "wood_d") + t.rectn(5, 9, 2, 6, "wood_d"); t.rectn(9, 9, 2, 6, "wood_d") + t.hln(5, 12, 6, "wood_d") +end + +tiles[74] = function(t) -- cabinet + t.rectn(2, 2, 12, 14, "wood"); t.framen(2, 2, 12, 14, "brown_d") + t.vln(8, 3, 12, "brown_d") + t.rectn(3, 4, 4, 5, "wood_d"); t.rectn(9, 4, 4, 5, "wood_d") + t.pn(7, 10, "yellow"); t.pn(9, 10, "yellow") + t.hln(2, 2, 12, "wood_l") +end + +tiles[75] = function(t) -- fridge + t.rectn(3, 1, 10, 15, "white"); t.framen(3, 1, 10, 15, "grey") + t.hln(3, 6, 10, "grey") + t.rectn(11, 2, 1, 4, "grey_d"); t.rectn(11, 7, 1, 8, "grey_d") + t.hln(4, 2, 8, "grey_l") +end + +tiles[76] = function(t) -- sink + t.rectn(1, 6, 14, 10, "metal"); t.framen(1, 6, 14, 10, "metal_d") + t.rectn(3, 8, 10, 5, "grey_d") + t.rectn(4, 9, 8, 3, "water_l"); t.hln(4, 9, 8, "water") + t.rectn(7, 3, 2, 4, "metal_d"); t.pn(7, 3, "metal"); t.pn(6, 3, "metal") + t.hln(2, 7, 12, "grey_l") +end + +tiles[77] = function(t) -- potted plant + t.discn(8, 6, 4, "leaf"); t.discn(7, 5, 2, "leaf_l"); t.discn(9, 7, 2, "leaf_d"); t.pn(8, 3, "leaf") + t.rectn(5, 10, 6, 6, "brown"); t.framen(5, 10, 6, 6, "brown_d"); t.hln(5, 10, 6, "brown_l") +end + +tiles[78] = function(t) -- barrel + t.ellipsen(8, 8, 6, 7, "wood") + t.rectn(2, 1, 12, 14, "wood"); t.ellipsen(8, 8, 6, 7, "wood") + t.hln(3, 4, 10, "wood_d"); t.hln(3, 11, 10, "wood_d") + t.vln(8, 2, 12, "wood_d") + t.framen(2, 1, 12, 14, "wood_d") + t.hln(4, 2, 8, "wood_l") +end + +tiles[79] = function(t) -- crate + t.rectn(2, 3, 12, 11, "wood"); t.framen(2, 3, 12, 11, "wood_d") + t.vln(8, 3, 11, "wood_d"); t.hln(2, 8, 12, "wood_d") + t.pn(3, 4, "wood_dd"); t.pn(12, 4, "wood_dd"); t.pn(3, 12, "wood_dd"); t.pn(12, 12, "wood_dd") + t.hln(3, 4, 10, "wood_l") +end + +-- =========================================================================== +-- ROW 5 (ids 80-95) : fog / white-layer +-- =========================================================================== +local function fog(t, seed) + local a1, a2 = 150, 110 + if seed == 0 then + t.ellipse(8, 8, 7, 3, H.col("w_hi", a1)) + t.ellipse(5, 6, 4, 2, H.col("w_mid", a2)) + t.ellipse(11, 10, 4, 2, H.col("w_mid", a2)) + elseif seed == 1 then + t.ellipse(7, 9, 6, 3, H.col("w_hi", a1)) + t.ellipse(11, 7, 4, 2, H.col("w_mid", a2)) + t.ellipse(4, 11, 3, 2, H.col("w_mid", a2)) + else + t.ellipse(9, 7, 7, 3, H.col("w_hi", a1)) + t.ellipse(5, 9, 4, 2, H.col("w_mid", a2)) + t.ellipse(12, 11, 3, 2, H.col("w_mid", a2)) + end +end +tiles[80] = function(t) fog(t, 0) end +tiles[81] = function(t) fog(t, 1) end +tiles[82] = function(t) fog(t, 2) end + +tiles[83] = function(t) -- white floor + t.rectn(0, 0, 16, 16, "w_hi") + t.hln(0, 0, 16, "w_mid"); t.hln(0, 8, 16, "w_mid") + t.vln(0, 0, 16, "w_mid"); t.vln(8, 0, 16, "w_mid") + t.pn(4, 4, "w_lo"); t.pn(12, 12, "w_lo"); t.pn(12, 4, "w_lo"); t.pn(4, 12, "w_lo") +end + +tiles[84] = function(t) -- white wall + t.rectn(0, 0, 16, 16, "w_hi") + t.hln(0, 0, 16, "w_lo"); t.hln(0, 8, 16, "w_lo") + t.vln(0, 0, 16, "w_lo"); t.vln(8, 0, 16, "w_lo") + t.rectn(0, 14, 16, 2, "w_mid"); t.hln(0, 14, 16, "w_hi") +end + +tiles[85] = function(t) -- white wall panel + t.rectn(0, 0, 16, 16, "w_hi") + t.framen(2, 2, 12, 11, "w_lo") + t.vln(8, 2, 11, "w_mid") + t.rectn(0, 14, 16, 2, "w_mid") +end + +tiles[86] = function(t) -- white door + t.rectn(0, 0, 16, 16, "w_hi") + t.rectn(3, 1, 10, 15, "w_mid"); t.framen(3, 1, 10, 15, "w_lo") + t.rectn(4, 2, 8, 6, "w_hi"); t.framen(4, 2, 8, 6, "w_lo") + t.rectn(4, 9, 8, 6, "w_hi"); t.framen(4, 9, 8, 6, "w_lo") + t.pn(11, 8, "w_dk"); t.pn(11, 9, "w_dk") +end + +-- white hospital bed (87-90) across 2x2 (origin of tile 87) +local function drawWhiteBed(img) + local b = H.at(img, (87 % 16) * 16, math.floor(87 / 16) * 16) + b.rectn(1, 2, 30, 27, "metal_d") + b.rectn(3, 4, 26, 23, "white"); b.framen(3, 4, 26, 23, "w_lo") + b.rectn(4, 6, 24, 6, "w_hi"); b.framen(4, 6, 24, 6, "w_mid") + b.rectn(3, 17, 26, 2, "w_mid") + b.rectn(1, 2, 30, 3, "metal"); b.rectn(1, 26, 30, 3, "metal") + b.rectn(1, 2, 2, 27, "metal"); b.rectn(29, 2, 2, 27, "metal") + b.rectn(2, 29, 3, 2, "w_dk"); b.rectn(27, 29, 3, 2, "w_dk") +end + +tiles[91] = function(t) -- white cabinet + t.rectn(2, 2, 12, 14, "w_mid"); t.framen(2, 2, 12, 14, "w_lo") + t.vln(8, 3, 12, "w_lo") + t.rectn(3, 4, 4, 4, "w_hi"); t.rectn(9, 4, 4, 4, "w_hi") + t.pn(7, 10, "w_dk"); t.pn(9, 10, "w_dk") + t.hln(2, 2, 12, "w_hi") +end + +tiles[92] = function(t) -- white monitor (medical) + t.rectn(3, 3, 10, 7, "w_lo"); t.framen(3, 3, 10, 7, "w_dk") + t.rectn(4, 4, 8, 5, "screen") + t.pn(4, 6, "screen_l"); t.pn(5, 6, "screen_l"); t.pn(6, 5, "screen_l") + t.pn(7, 7, "screen_l"); t.pn(8, 6, "screen_l"); t.pn(9, 6, "screen_l"); t.pn(10, 6, "screen_l") + t.rectn(7, 10, 2, 3, "w_dk"); t.rectn(5, 13, 6, 2, "w_lo"); t.hln(5, 14, 6, "w_dk") +end + +tiles[93] = function(t) -- white curtain + t.rectn(2, 1, 12, 14, "w_hi") + t.vln(4, 1, 14, "w_mid"); t.vln(7, 1, 14, "w_mid"); t.vln(10, 1, 14, "w_mid"); t.vln(12, 1, 14, "w_lo") + t.rectn(2, 1, 12, 1, "w_lo") + t.hln(2, 14, 12, "w_mid") +end + +tiles[94] = function(t) -- white floor alt + t.rectn(0, 0, 16, 16, "w_hi") + for y = 0, 15, 4 do + for x = 0, 15, 4 do + t.pn((x + 2) % 16, (y + 2) % 16, "w_lo") + end + end + t.pn(2, 2, "w_mid"); t.pn(10, 10, "w_mid") +end + +tiles[95] = function(t) -- white ceiling light + t.rectn(2, 6, 12, 4, "white"); t.framen(2, 6, 12, 4, "w_lo") + t.rectn(3, 7, 10, 2, "w_hi") + t.pn(7, 8, "white") +end + +-- =========================================================================== +-- ROW 6 (ids 96-111) : ladder / bridge / props +-- =========================================================================== +tiles[96] = function(t) -- ladder + t.vln(4, 0, 16, "wood_d"); t.vln(11, 0, 16, "wood_d") + for y = 2, 14, 3 do t.hln(4, y, 8, "wood"); t.hln(4, y, 8, "wood_l") end +end + +tiles[97] = function(t) -- bridge horizontal + for y = 3, 12, 3 do + t.rectn(0, y, 16, 2, "wood"); t.hln(0, y, 16, "wood_l"); t.hln(0, y + 1, 16, "wood_d") + end + t.rectn(0, 1, 16, 1, "wood_d"); t.rectn(0, 14, 16, 1, "wood_d") + t.vln(0, 1, 14, "wood_d"); t.vln(15, 1, 14, "wood_d") +end + +tiles[98] = function(t) -- bridge vertical + for x = 3, 12, 3 do + t.rectn(x, 0, 2, 16, "wood"); t.vln(x, 0, 16, "wood_l"); t.vln(x + 1, 0, 16, "wood_d") + end + t.rectn(1, 0, 1, 16, "wood_d"); t.rectn(14, 0, 1, 16, "wood_d") + t.hln(1, 0, 14, "wood_d"); t.hln(1, 15, 14, "wood_d") +end + +tiles[99] = function(t) -- mushroom(烘焙草地底:放在 ground 层,透明区会露黑底) + grassBase(t) + t.rectn(7, 8, 2, 6, "cream"); t.vln(7, 8, 6, "cream_d") + t.discn(8, 6, 5, "red"); t.rectn(3, 6, 10, 2, "red") + t.discn(8, 5, 4, "red_l") + t.pn(6, 5, "white"); t.pn(9, 4, "white"); t.pn(10, 7, "white"); t.pn(5, 7, "white") +end + +local function bigflower(t, c) + grassBase(t) -- 烘焙草地底:该瓦片放在 ground 层,透明区会露出黑底 + t.vln(8, 9, 6, "leaf"); t.pn(6, 12, "leaf"); t.pn(10, 11, "leaf"); t.pn(7, 13, "leaf_d") + t.discn(8, 6, 3, c) + t.discn(8, 6, 1, "yellow") + t.pn(8, 6, "honey_d") +end +tiles[100] = function(t) bigflower(t, "red") end +tiles[101] = function(t) bigflower(t, "yellow") end +tiles[102] = function(t) bigflower(t, "blue") end + +tiles[103] = function(t) -- honey jar + t.rectn(5, 4, 6, 10, "honey"); t.framen(5, 4, 6, 10, "honey_d") + t.rectn(5, 4, 6, 2, "honey_l") + t.rectn(4, 2, 8, 2, "brown_d"); t.hln(4, 2, 8, "brown") + t.rectn(6, 8, 4, 4, "honey_l"); t.pn(8, 10, "honey_dd") +end + +tiles[104] = function(t) -- bread + t.ellipsen(8, 10, 7, 4, "honey") + t.ellipsen(8, 9, 6, 3, "honey_l") + t.pn(5, 9, "honey_d"); t.pn(8, 8, "honey_d"); t.pn(11, 9, "honey_d") + t.hln(3, 12, 10, "honey_d") +end + +tiles[105] = function(t) -- letter + t.rectn(3, 5, 10, 7, "cream"); t.framen(3, 5, 10, 7, "cream_d") + for i = 0, 4 do t.pn(4 + i, 5 + i, "cream_d"); t.pn(11 - i, 5 + i, "cream_d") end + t.pn(8, 9, "red") +end + +tiles[106] = function(t) -- book + t.rectn(3, 4, 10, 9, "red"); t.framen(3, 4, 10, 9, "red_d") + t.vln(8, 4, 9, "red_d"); t.hln(3, 4, 10, "red_l") + t.rectn(4, 6, 3, 5, "red_d"); t.pn(5, 8, "yellow") +end + +tiles[107] = function(t) -- candle + t.rectn(6, 6, 4, 9, "cream"); t.framen(6, 6, 4, 9, "cream_d") + t.rectn(6, 6, 4, 1, "cream_d") + t.pn(8, 5, "brown_d") + t.discn(8, 3, 2, "flame"); t.pn(8, 2, "flame_d"); t.pn(8, 4, "flame_d") +end + +tiles[108] = function(t) -- basket + t.rectn(3, 7, 10, 7, "wood"); t.framen(3, 7, 10, 7, "wood_d") + t.hln(3, 9, 10, "wood_d"); t.hln(3, 11, 10, "wood_d") + t.vln(6, 7, 7, "wood_d"); t.vln(9, 7, 7, "wood_d") + t.vln(3, 4, 4, "wood_d"); t.vln(12, 4, 4, "wood_d"); t.hln(3, 4, 10, "wood_d") + t.rectn(4, 5, 8, 2, "wood") +end + +tiles[109] = function(t) -- bucket + t.rectn(4, 6, 8, 9, "metal"); t.framen(4, 6, 8, 9, "metal_d") + t.rectn(4, 6, 8, 2, "water_l"); t.hln(4, 6, 8, "water") + t.vln(4, 4, 3, "metal_d"); t.vln(11, 4, 3, "metal_d"); t.hln(4, 4, 8, "metal_d") + t.hln(5, 10, 6, "metal_d") +end + +tiles[110] = function(t) -- broom + t.vln(10, 1, 8, "wood_d"); t.vln(9, 1, 8, "wood") + t.rectn(4, 9, 7, 6, "yellow_d"); t.framen(4, 9, 7, 6, "honey_d") + t.hln(4, 9, 7, "brown_d") + t.pn(3, 14, "yellow_d"); t.pn(11, 14, "yellow_d"); t.pn(4, 15, "honey_d"); t.pn(10, 15, "honey_d") +end + +tiles[111] = function(t) -- cake + t.rectn(3, 8, 10, 6, "pink"); t.framen(3, 8, 10, 6, "pink_d") + t.rectn(3, 8, 10, 2, "white") + t.pn(4, 10, "pink_d"); t.pn(7, 10, "pink_d"); t.pn(10, 10, "pink_d") + t.rectn(3, 14, 10, 1, "cream_d") + t.vln(6, 5, 3, "red"); t.vln(9, 5, 3, "blue") + t.pn(6, 4, "flame"); t.pn(9, 4, "flame") +end + +-- =========================================================================== +-- ROW 7 (ids 112-127) : colored roofs / walls / misc +-- =========================================================================== +tiles[112] = function(t) roof(t, "red", "red_d", "red_l", "L") end +tiles[113] = function(t) roof(t, "red", "red_d", "red_l", nil) end +tiles[114] = function(t) roof(t, "red", "red_d", "red_l", "R") end +tiles[115] = function(t) roof(t, "blue", "blue_d", "blue_l", "L") end +tiles[116] = function(t) roof(t, "blue", "blue_d", "blue_l", nil) end +tiles[117] = function(t) roof(t, "blue", "blue_d", "blue_l", "R") end + +local function pinkWall(t) + t.rectn(0, 0, 16, 16, "pink") + t.hln(0, 0, 16, "pink_d"); t.hln(0, 8, 16, "pink_d") + t.vln(0, 0, 8, "pink_d"); t.vln(8, 0, 8, "pink_d") + t.vln(4, 8, 8, "pink_d"); t.vln(12, 8, 8, "pink_d") + t.pn(2, 4, "pink_l"); t.pn(10, 12, "pink_l") +end +tiles[118] = function(t) pinkWall(t) end +tiles[119] = function(t) + pinkWall(t) + t.rectn(4, 3, 8, 9, "pink") + H.window(t, 4, 3, 8, 9, "sky") +end + +local function stoneWall(t) + t.rectn(0, 0, 16, 16, "swall") + t.hln(0, 0, 16, "swall_d"); t.hln(0, 5, 16, "swall_d"); t.hln(0, 10, 16, "swall_d") + t.vln(0, 0, 5, "swall_d"); t.vln(8, 0, 5, "swall_d") + t.vln(4, 5, 5, "swall_d"); t.vln(12, 5, 5, "swall_d") + t.vln(0, 10, 6, "swall_d"); t.vln(8, 10, 6, "swall_d") + t.pn(3, 2, "swall_l"); t.pn(10, 7, "swall_l"); t.pn(6, 12, "swall_l") +end +tiles[120] = function(t) stoneWall(t) end +tiles[121] = function(t) + stoneWall(t) + t.rectn(4, 3, 8, 9, "swall") + H.window(t, 4, 3, 8, 9, "sky") +end + +tiles[122] = function(t) -- awning + for x = 0, 15 do + local c = (math.floor(x / 2) % 2 == 0) and "red" or "white" + t.vln(x, 0, 6, c) + end + t.hln(0, 6, 16, "red_d") + for x = 0, 15 do if x % 4 < 2 then t.pn(x, 7, "red") else t.pn(x, 7, "red_d") end end + t.hln(0, 0, 16, "red_l") +end + +tiles[123] = function(t) -- clock + t.discn(8, 8, 6, "brown_d"); t.discn(8, 8, 5, "cream") + t.pn(8, 3, "brown_d"); t.pn(8, 13, "brown_d"); t.pn(3, 8, "brown_d"); t.pn(13, 8, "brown_d") + t.vln(8, 4, 4, "ink"); t.hln(8, 8, 3, "ink"); t.pn(8, 8, "ink") +end + +tiles[124] = function(t) -- flag + t.vln(3, 1, 15, "grey_d") + t.rectn(4, 2, 9, 6, "red"); t.rectn(4, 2, 9, 2, "red_l") + t.pn(13, 3, "red"); t.pn(13, 4, "red") + t.rectn(2, 15, 3, 1, "grey") + t.pn(4, 4, "yellow") +end + +tiles[125] = function(t) -- garland corner + for x = 1, 12 do + t.pn(x, 2, "leaf"); t.pn(x, 3, "leaf_d") + if x % 3 == 0 then H.flower(t, x, 2, "pink", "yellow") end + end + for y = 1, 12 do + t.pn(2, y, "leaf"); t.pn(3, y, "leaf_d") + if y % 3 == 1 then H.flower(t, 2, y, "yellow", "honey") end + end +end + +tiles[126] = function(t) -- garland mid (horizontal, tileable) + for x = 0, 15 do t.pn(x, 4, "leaf"); t.pn(x, 5, "leaf_d") end + H.flower(t, 3, 3, "pink", "yellow") + H.flower(t, 8, 4, "red", "yellow") + H.flower(t, 13, 3, "yellow", "honey") +end + +-- tile 127 intentionally left transparent (empty) + +-- =========================================================================== +-- render +-- =========================================================================== +-- 2x2 拼块的自然落点是 id、id+1、id+16、id+17,但 G 表/地图引用连续 4 个 id +-- (id..id+3)。故每画完一块,立即把下半两个象限复制到 id+2、id+3; +-- 自然落点 id+16/id+17 的残留随后被 tiles 循环里已定义的瓦片重绘覆盖。 +local function copyBlock(img, tlId) + local ox = (tlId % 16) * 16 + local oy = math.floor(tlId / 16) * 16 + for dy = 0, 15 do + for dx = 0, 15 do + img:putPixel(ox + 32 + dx, oy + dy, img:getPixel(ox + dx, oy + 16 + dy)) -- 自然 BL → 槽位 id+2 + img:putPixel(ox + 48 + dx, oy + dy, img:getPixel(ox + 16 + dx, oy + 16 + dy)) -- 自然 BR → 槽位 id+3 + end + end +end +drawTree(img); copyBlock(img, 18) +drawWell(img); copyBlock(img, 42) +drawBed(img); copyBlock(img, 52) -- 须在 drawPicnic 之前复制:二者自然落点都覆盖 68/69 +drawPicnic(img); copyBlock(img, 67) +drawWhiteBed(img); copyBlock(img, 87) + +for id = 0, 127 do + if tiles[id] then tiles[id](H.tile(img, id)) end +end + +spr:saveAs(ROOT .. "public/assets/tileset.png") +print("tileset.png done") diff --git a/Kimi-K3/tools/aseprite/helpers.lua b/Kimi-K3/tools/aseprite/helpers.lua new file mode 100644 index 0000000..f53d15a --- /dev/null +++ b/Kimi-K3/tools/aseprite/helpers.lua @@ -0,0 +1,229 @@ +-- helpers.lua — shared palette + pixel primitives for asset generation +-- Usage: local H = dofile(ROOT .. "tools/aseprite/helpers.lua") +-- All draw helpers operate on an Aseprite Image via a "pen" table bound to an origin. + +local H = {} + +H.ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/" + +-- ---------------------------------------------------------------------------- +-- Candy palette (CONTRACT §1) + white-layer grays + a few flat utility shades. +-- Stored as {r,g,b}. No gradients, no AA — every pixel is one of these. +-- ---------------------------------------------------------------------------- +H.P = { + -- grass greens + grass = {125, 201, 94}, -- #7DC95E + grass_d = { 95, 179, 74}, -- #5FB34A + grass_dd = { 74, 148, 58}, + grass_l = {158, 216, 128}, + -- cream + cream = {255, 243, 214}, -- #FFF3D6 + cream_d = {238, 220, 178}, + cream_dd = {216, 196, 150}, + -- honey orange + honey = {245, 166, 35}, -- #F5A623 + honey_d = {224, 143, 18}, + honey_dd = {196, 120, 10}, + honey_l = {250, 193, 96}, + -- sky blue + sky = {143, 211, 255}, -- #8FD3FF + sky_d = {110, 186, 240}, + -- stone road + stone = {232, 220, 200}, -- #E8DCC8 + stone_d = {206, 190, 162}, + stone_dd = {178, 160, 130}, + stone_l = {245, 237, 224}, + -- lake water + water = {110, 193, 228}, -- #6EC1E4 + water_d = { 79, 168, 216}, -- #4FA8D8 + water_dd = { 56, 140, 190}, + water_l = {158, 216, 242}, + -- pink + pink = {247, 183, 203}, -- #F7B7CB + pink_d = {232, 150, 176}, + pink_dd = {208, 120, 150}, + pink_l = {252, 208, 222}, + -- brown (trunk / wood dark) + brown = {139, 90, 43}, -- #8B5A2B + brown_d = {108, 68, 30}, + brown_dd = { 84, 52, 22}, + brown_l = {172, 122, 72}, + -- dirt road + dirt = {205, 172, 122}, + dirt_d = {182, 148, 98}, + dirt_l = {220, 190, 146}, + -- white-layer grays (CONTRACT §1) + w_hi = {245, 245, 245}, -- #F5F5F5 + w_mid = {218, 218, 218}, -- #DADADA + w_lo = {189, 189, 189}, -- #BDBDBD + w_dk = {150, 150, 150}, + w_dd = {110, 110, 110}, + -- reds + red = {226, 87, 77}, + red_d = {198, 66, 56}, + red_l = {240, 130, 120}, + -- blues + blue = { 91, 155, 213}, + blue_d = { 62, 125, 190}, + blue_l = {140, 190, 232}, + -- yellows + yellow = {247, 214, 92}, + yellow_d = {224, 186, 55}, + yellow_l = {252, 228, 140}, + -- foliage + leaf = { 86, 168, 78}, + leaf_d = { 64, 138, 60}, + leaf_dd = { 46, 108, 44}, + leaf_l = {120, 192, 110}, + -- wood + wood = {201, 152, 96}, + wood_d = {176, 126, 74}, + wood_dd = {150, 104, 58}, + wood_l = {220, 178, 124}, + -- stone wall (cool gray) + swall = {196, 190, 178}, + swall_d = {164, 158, 146}, + swall_dd = {134, 128, 116}, + swall_l = {214, 209, 200}, + -- berry + berry = {206, 62, 96}, + berry_d = {172, 44, 76}, + -- misc + skin = {255, 224, 190}, + skin_d = {238, 198, 156}, + hair = {120, 84, 52}, + hair_d = { 96, 66, 40}, + hair_l = {152, 112, 74}, + black = { 54, 44, 40}, + ink = { 40, 34, 30}, + white = {255, 255, 255}, + grey = {150, 150, 150}, + grey_d = {108, 108, 108}, + grey_l = {196, 196, 196}, + screen = { 44, 58, 54}, + screen_l = {120, 220, 160}, + metal = {178, 184, 190}, + metal_d = {140, 146, 152}, + flame = {255, 214, 92}, + flame_d = {245, 140, 40}, + leaf2 = { 60, 150, 70}, +} + +-- make an Aseprite color from a palette name (or {r,g,b} table) + optional alpha +function H.col(name, a) + local t = name + if type(name) == "string" then t = H.P[name] end + return app.pixelColor.rgba(t[1], t[2], t[3], a or 255) +end + +-- ---------------------------------------------------------------------------- +-- Pen: a table of draw methods bound to an image + origin (ox, oy). +-- All coordinates passed to pen methods are LOCAL to the origin. +-- ---------------------------------------------------------------------------- +function H.at(img, ox, oy) + local t = { img = img, ox = ox, oy = oy } + function t.p(x, y, c) img:drawPixel(ox + x, oy + y, c) end + function t.pn(x, y, name, a) img:drawPixel(ox + x, oy + y, H.col(name, a)) end + function t.rect(x, y, w, h, c) + for j = 0, h - 1 do for i = 0, w - 1 do img:drawPixel(ox + x + i, oy + y + j, c) end end + end + function t.rectn(x, y, w, h, name, a) + local c = H.col(name, a) + for j = 0, h - 1 do for i = 0, w - 1 do img:drawPixel(ox + x + i, oy + y + j, c) end end + end + function t.hl(x, y, len, c) for i = 0, len - 1 do img:drawPixel(ox + x + i, oy + y, c) end end + function t.hln(x, y, len, name, a) + local c = H.col(name, a); for i = 0, len - 1 do img:drawPixel(ox + x + i, oy + y, c) end + end + function t.vl(x, y, len, c) for j = 0, len - 1 do img:drawPixel(ox + x, oy + y + j, c) end end + function t.vln(x, y, len, name, a) + local c = H.col(name, a); for j = 0, len - 1 do img:drawPixel(ox + x, oy + y + j, c) end + end + -- outline rectangle + function t.frame(x, y, w, h, c) + t.hl(x, y, w, c); t.hl(x, y + h - 1, w, c) + t.vl(x, y, h, c); t.vl(x + w - 1, y, h, c) + end + function t.framen(x, y, w, h, name, a) + t.frame(x, y, w, h, H.col(name, a)) + end + -- filled disc (filled circle), integer radius + function t.disc(cx, cy, r, c) + for yy = -r, r do for xx = -r, r do + if xx * xx + yy * yy <= r * r then t.p(cx + xx, cy + yy, c) end + end end + end + function t.discn(cx, cy, r, name, a) t.disc(cx, cy, r, H.col(name, a)) end + -- filled ellipse + function t.ellipse(cx, cy, rx, ry, c) + for yy = -ry, ry do for xx = -rx, rx do + if (xx * xx) * (ry * ry) + (yy * yy) * (rx * rx) <= (rx * rx) * (ry * ry) then + t.p(cx + xx, cy + yy, c) + end + end end + end + function t.ellipsen(cx, cy, rx, ry, name, a) t.ellipse(cx, cy, rx, ry, H.col(name, a)) end + return t +end + +-- pen for a 16x16 tile by id (tileset is 16 columns) +function H.tile(img, id) + local ox = (id % 16) * 16 + local oy = math.floor(id / 16) * 16 + return H.at(img, ox, oy) +end + +-- ---------------------------------------------------------------------------- +-- Motif helpers (drawn through a pen `t`, local coords) +-- ---------------------------------------------------------------------------- + +-- a small 5-petal flower centered at (cx,cy) +function H.flower(t, cx, cy, petal, center) + local pc = H.col(petal); local cc = H.col(center or "yellow") + t.p(cx, cy - 1, pc); t.p(cx, cy + 1, pc) + t.p(cx - 1, cy, pc); t.p(cx + 1, cy, pc) + t.p(cx, cy, cc) +end + +-- a window with brown frame + sky panes, top-left (x,y), size w x h +function H.window(t, x, y, w, h, glass) + glass = glass or "sky" + t.rectn(x, y, w, h, "brown_d") + t.rectn(x + 1, y + 1, w - 2, h - 2, glass) + -- mullions + local mx = x + math.floor(w / 2) + local my = y + math.floor(h / 2) + t.vln(mx, y + 1, h - 2, "brown_d") + t.hln(x + 1, my, w - 2, "brown_d") + -- glass highlight + t.pn(x + 2, y + 2, "sky_l" == glass and "white" or "water_l") +end + +-- ---------------------------------------------------------------------------- +-- Mirror pen: wraps a pen `t`, flipping x within a `w`-wide frame. +-- Lets one directional drawing serve both left and right facings. +-- ---------------------------------------------------------------------------- +function H.mirrorX(t, w) + local m = {} + function m.p(x, y, c) t.p(w - 1 - x, y, c) end + function m.pn(x, y, name, a) t.pn(w - 1 - x, y, name, a) end + function m.rect(x, y, ww, h, c) t.rect(w - x - ww, y, ww, h, c) end + function m.rectn(x, y, ww, h, name, a) t.rectn(w - x - ww, y, ww, h, name, a) end + function m.hl(x, y, len, c) t.hl(w - x - len, y, len, c) end + function m.hln(x, y, len, name, a) t.hln(w - x - len, y, len, name, a) end + function m.vl(x, y, len, c) t.vl(w - 1 - x, y, len, c) end + function m.vln(x, y, len, name, a) t.vln(w - 1 - x, y, len, name, a) end + function m.framen(x, y, ww, h, name, a) t.framen(w - x - ww, y, ww, h, name, a) end + function m.discn(cx, cy, r, name, a) t.discn(w - 1 - cx, cy, r, name, a) end + function m.ellipsen(cx, cy, rx, ry, name, a) t.ellipsen(w - 1 - cx, cy, rx, ry, name, a) end + return m +end + +-- create a fresh RGB sprite + return (sprite, image) +function H.new(w, h) + local spr = Sprite(w, h, ColorMode.RGB) + local img = app.activeImage + return spr, img +end + +return H diff --git a/Kimi-K3/tools/dbg10.mjs b/Kimi-K3/tools/dbg10.mjs new file mode 100644 index 0000000..ad95a56 --- /dev/null +++ b/Kimi-K3/tools/dbg10.mjs @@ -0,0 +1,42 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +await sleep(400); +for (let i = 0; i < 8; i++) { + const open = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); + if (!open) break; + await tap('KeyE'); await sleep(200); +} +const dump = () => page.evaluate(() => { + const s = window.__game.scene.getScene('HouseScene'); + return { + player: { x: Math.round(s.player.x), y: Math.round(s.player.y) }, + vel: { x: Math.round(s.player.body.velocity.x), y: Math.round(s.player.body.velocity.y) }, + doors: s.doors, + dlg: window.__director.dialogueActive, + transitioning: s.transitioning, + armed: s.armedDoors ? s.armedDoors.size : -1, + }; +}); +console.log('before:', JSON.stringify(await dump())); +await page.keyboard.down('KeyS'); await sleep(1200); await page.keyboard.up('KeyS'); +console.log('after KeyS:', JSON.stringify(await dump())); +await page.keyboard.down('KeyS'); await sleep(1500); await page.keyboard.up('KeyS'); +console.log('after more KeyS:', JSON.stringify(await dump())); +console.log('plaza active:', await page.evaluate(() => window.__game.scene.isActive('PlazaScene'))); +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/dbg11.mjs b/Kimi-K3/tools/dbg11.mjs new file mode 100644 index 0000000..0f01197 --- /dev/null +++ b/Kimi-K3/tools/dbg11.mjs @@ -0,0 +1,50 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +await sleep(400); +// 记录对话开关 +await page.evaluate(() => { + window.__dlgLog = []; + const d = window.__director; + const origShow = d.showDialogue.bind(d); + d.showDialogue = (p) => { window.__dlgLog.push(`${Date.now()%100000} OPEN 「${(p.lines&&p.lines[0]||'').slice(0,18)}」`); return origShow(p); }; + const origRes = d.resolveDialogue.bind(d); + d.resolveDialogue = (c) => { window.__dlgLog.push(`${Date.now()%100000} CLOSE`); return origRes(c); }; + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); } + sm.start('PlazaScene', { spawn: 'default' }); +}); +await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 }); +await sleep(500); +// 清残留 +for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); } +await page.evaluate(() => { window.__dlgLog.length = 0; }); +// 站到阿福旁,以真人节奏 ~500ms 按 E +await page.evaluate(() => { + const s = window.__game.scene.getScene('PlazaScene'); + const a = s.actors.find(a => a.def.id === 'afu'); + s.player.setPosition(a.sprite.x, a.sprite.y + 8); s.player.setVelocity(0, 0); +}); +await sleep(150); +console.log('--- 开始:第 1 次按 E(开对话)'); +await tap('KeyE'); +for (let i = 0; i < 8; i++) { await sleep(500); console.log(` t+${(i+1)*0.5}s 按 E`); await tap('KeyE'); } +console.log('--- 停止按键,观察 4 秒是否自动重开 ---'); +await sleep(4000); +for (const l of await page.evaluate(() => window.__dlgLog)) console.log(l); +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/dbg12.mjs b/Kimi-K3/tools/dbg12.mjs new file mode 100644 index 0000000..f9db483 --- /dev/null +++ b/Kimi-K3/tools/dbg12.mjs @@ -0,0 +1,70 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +await sleep(400); +// 记录对话开关 +await page.evaluate(() => { + window.__dlgLog = []; + const d = window.__director; + const origShow = d.showDialogue.bind(d); + d.showDialogue = (p) => { window.__dlgLog.push(`OPEN 「${(p.lines&&p.lines[0]||'').slice(0,18)}」`); return origShow(p); }; + const origRes = d.resolveDialogue.bind(d); + d.resolveDialogue = (c) => { window.__dlgLog.push('CLOSE'); return origRes(c); }; + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); } + sm.start('PlazaScene', { spawn: 'default' }); +}); +await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 }); +await sleep(500); +// 清残留对话 +for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); } +await page.evaluate(() => { window.__dlgLog.length = 0; }); +// 传送到雾门正上方(触发圈内 dist≈10),模拟玩家从上方走近雾门后松开按键 +const info = await page.evaluate(() => { + const s = window.__game.scene.getScene('PlazaScene'); + const d = s.doors.find(d => d.target === 'fog'); + s.player.setPosition(d.x, d.y - 10 + 8); // 测量点 player.y-8 → py = d.y-10 + s.player.setVelocity(0, 0); + return { door: { x: d.x, y: d.y }, day: window.__director.day }; +}); +console.log('雾门位置', info.door, 'day', info.day); +// 等待劝离对话自动弹出(靠近即触发,无需按键) +await sleep(1500); +const afterTrigger = await page.evaluate(() => window.__dlgLog.filter(l => l.startsWith('OPEN')).length); +console.log('触发后 OPEN 次数:', afterTrigger); +if (afterTrigger !== 1) { console.log('FAIL: 劝离对话应恰好弹出 1 次'); process.exit(1); } +// 按 E 关闭对话(打字机:第一下补全文字,第二下才关闭;canUseDoor 在对话关闭后才返回并执行推离) +for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); } +await sleep(400); +const afterClose = await page.evaluate(() => { + const s = window.__game.scene.getScene('PlazaScene'); + const d = s.doors.find(d => d.target === 'fog'); + const py = s.player.y - 8; + return { dist: Math.hypot(s.player.x - d.x, py - d.y).toFixed(1) }; +}); +console.log('关闭并推离后玩家距门:', afterClose.dist); +if (parseFloat(afterClose.dist) <= 14) { console.log('FAIL: 劝离推离后玩家仍在触发圈内 dist=' + afterClose.dist); process.exit(1); } +// 静止观察 5 秒,不得重开 +await sleep(5000); +const final = await page.evaluate(() => window.__dlgLog); +console.log('最终日志:', final); +const opens = final.filter(l => l.startsWith('OPEN')).length; +const closes = final.filter(l => l === 'CLOSE').length; +if (opens === 1 && closes >= 1) { console.log('PASS: 劝离对话只弹一次,关闭后 5 秒未重开'); } +else { console.log(`FAIL: OPEN=${opens} CLOSE=${closes}`); process.exit(1); } +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/dbg13.mjs b/Kimi-K3/tools/dbg13.mjs new file mode 100644 index 0000000..2c89308 --- /dev/null +++ b/Kimi-K3/tools/dbg13.mjs @@ -0,0 +1,69 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +await sleep(400); +await page.evaluate(() => { + window.__dlgLog = []; + const d = window.__director; + const origShow = d.showDialogue.bind(d); + d.showDialogue = (p) => { window.__dlgLog.push(`OPEN 「${(p.lines&&p.lines[0]||'').slice(0,14)}」`); return origShow(p); }; + const origRes = d.resolveDialogue.bind(d); + d.resolveDialogue = (c) => { window.__dlgLog.push('CLOSE'); return origRes(c); }; + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); } + sm.start('PlazaScene', { spawn: 'default' }); +}); +await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 }); +await sleep(500); +for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); } +await page.evaluate(() => { + window.__dlgLog.length = 0; + const s = window.__game.scene.getScene('PlazaScene'); + const d = s.doors.find(d => d.target === 'fog'); + window.__door = d; + // 记录 canUseDoor 调用现场 + const origCUD = s.canUseDoor.bind(s); + s.canUseDoor = (t) => { + const dd = s.doors.find(x => x.target === t); + const dist = dd ? Math.hypot(s.player.x - dd.x, (s.player.y - 8) - dd.y) : -1; + window.__dlgLog.push(`canUseDoor(${t}) dist=${dist.toFixed(1)} armed=${dd ? s.armedDoors.has(dd) : '?'}`); + return origCUD(t); + }; + s.player.setPosition(d.x, d.y - 10 + 8); + s.player.setVelocity(0, 0); +}); +await sleep(1200); // 等劝离对话弹出 +// 关闭对话 +for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); } +// 高频采样 3 秒 +const samples = await page.evaluate(async () => { + const s = window.__game.scene.getScene('PlazaScene'); + const d = window.__door; + const out = []; + const t0 = Date.now(); + return await new Promise((resolve) => { + const iv = setInterval(() => { + const dist = Math.hypot(s.player.x - d.x, (s.player.y - 8) - d.y); + out.push(`${Date.now() - t0}ms pos=(${s.player.x.toFixed(1)},${s.player.y.toFixed(1)}) body=(${s.player.body.x.toFixed(1)},${s.player.body.y.toFixed(1)}) dist=${dist.toFixed(1)} dlg=${window.__director.dialogueActive} log=${window.__dlgLog.length}`); + if (Date.now() - t0 > 3000) { clearInterval(iv); resolve(out); } + }, 100); + }); +}); +for (const l of samples) console.log(l); +console.log('LOG:', await page.evaluate(() => window.__dlgLog)); +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/dbg14.mjs b/Kimi-K3/tools/dbg14.mjs new file mode 100644 index 0000000..b901e03 --- /dev/null +++ b/Kimi-K3/tools/dbg14.mjs @@ -0,0 +1,31 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +await sleep(600); +// 依次跳转地图并截图 +for (const [sceneKey, spawn] of [['HouseScene','default'],['PlazaScene','default'],['FieldScene','default'],['WellScene','default'],['LakeScene','default']]) { + await page.evaluate(([k, sp]) => { + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const kk = sc.scene.key; if (kk !== 'UIScene' && kk !== k) sm.stop(kk); } + sm.start(k, { spawn: sp }); + }, [sceneKey, spawn]); + await page.waitForFunction((k) => window.__game.scene.isActive(k), { timeout: 9000 }, sceneKey); + await sleep(900); + await page.screenshot({ path: `tools/shots/fix_${sceneKey}.png` }); + console.log('shot', sceneKey); +} +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/dbg15.mjs b/Kimi-K3/tools/dbg15.mjs new file mode 100644 index 0000000..1380334 --- /dev/null +++ b/Kimi-K3/tools/dbg15.mjs @@ -0,0 +1,43 @@ +// 最终清扫:任何「含透明像素的瓦片」都不得出现在任何地图的 ground 层(否则露黑底) +import puppeteer from 'puppeteer-core'; +import fs from 'node:fs'; +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true }); +const page = await browser.newPage(); +// 把 tileset.png 读成 dataURL 注入页面分析 alpha +const b64 = fs.readFileSync('public/assets/tileset.png').toString('base64'); +const transparentIds = await page.evaluate(async (data) => { + const img = new Image(); + await new Promise((res) => { img.onload = res; img.src = data; }); + const c = document.createElement('canvas'); + c.width = img.width; c.height = img.height; + const ctx = c.getContext('2d'); + ctx.drawImage(img, 0, 0); + const d = ctx.getImageData(0, 0, img.width, img.height).data; + const out = []; + for (let id = 0; id < 128; id++) { + const ox = (id % 16) * 16, oy = Math.floor(id / 16) * 16; + let trans = false; + for (let y = 0; y < 16 && !trans; y++) + for (let x = 0; x < 16; x++) + if (d[((oy + y) * img.width + ox + x) * 4 + 3] < 255) { trans = true; break; } + if (trans) out.push(id); + } + return out; +}, 'data:image/png;base64,' + b64); +await browser.close(); +console.log('含透明像素的瓦片 id:', transparentIds.join(',')); +const maps = ['house','plaza','bakery','shop','clinic','lake','field','well','fog','white']; +let bad = 0; +for (const key of maps) { + const m = JSON.parse(fs.readFileSync(`public/assets/maps/${key}.json`, 'utf8')); + const g = m.layers.find((l) => l.name === 'ground'); + const hits = new Set(); + for (const gid of g.data) { + if (gid === 0) { hits.add('EMPTY'); continue; } + const id = gid - 1; + if (transparentIds.includes(id)) hits.add(id); + } + if (hits.size) { bad++; console.log(`FAIL ${key}: ground 层含透明瓦片/空格 ->`, [...hits].join(',')); } + else console.log(`OK ${key}`); +} +process.exit(bad ? 1 : 0); diff --git a/Kimi-K3/tools/dbg16.mjs b/Kimi-K3/tools/dbg16.mjs new file mode 100644 index 0000000..7f243f7 --- /dev/null +++ b/Kimi-K3/tools/dbg16.mjs @@ -0,0 +1,131 @@ +// dbg16:复现「面包无法提交」与「第一天睡觉卡死」 +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600))); +page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); }); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +const ev = (fn, ...a) => page.evaluate(fn, ...a); +const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); +const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; }); +async function drain(maxIter = 40) { + const texts = []; + for (let i = 0; i < maxIter; i++) { + if (!(await dlgOpen())) break; + texts.push(await dlgText()); + const choosing = await ev(() => { const u = window.__game.scene.getScene('UIScene'); return u.showingChoices; }); + await tap(choosing ? 'Digit1' : 'KeyE'); + await sleep(160); + } + return texts; +} +async function startScene(k, spawn='default') { + await ev((k,s) => { + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const kk = sc.scene.key; if (kk !== 'UIScene' && kk !== k) sm.stop(kk); } + sm.start(k, { spawn: s }); + }, k, spawn); + await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k); + await sleep(500); +} +async function setPlayer(k, x, y) { + await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y); + await sleep(120); +} +const dstate = () => ev(() => ({ + day: window.__director.day, + breadTaken: window.__director.taskProgress['breadTaken'] ?? 0, + breadTo: [...(window.__director.taskSetProgress['breadTo'] ?? [])], + done: [...window.__director.tasksDone], + canSleep: window.__director.canSleep, + dlg: window.__director.dialogueActive, +})); +await sleep(600); + +console.log('\n=== A. 面包任务提交 ==='); +// 1. 面包房取面包 +await startScene('BakeryScene'); +await drain(); +// counter_bread 交互区在地图对象里:从 JSON 读坐标 +const cbObj = await ev(() => window.__game.scene.getScene('BakeryScene').interactZones.find(z => z.id === 'counter_bread')); +console.log('counter_bread zone:', cbObj); +await setPlayer('BakeryScene', cbObj.x + 40, cbObj.y + 40); +await setPlayer('BakeryScene', cbObj.x, cbObj.y + 8); +await tap('KeyE'); await sleep(400); +const t1 = await drain(); +console.log('取面包对话:', t1); +console.log('状态:', await dstate()); + +// 2. 送 dabao(plaza) +await startScene('PlazaScene'); +await drain(); +const dabao = await ev(() => { const s = window.__game.scene.getScene('PlazaScene'); const a = s.actors.find(a => a.def.id === 'dabao'); return { x: a.sprite.x, y: a.sprite.y }; }); +await setPlayer('PlazaScene', dabao.x + 40, dabao.y + 40); +await setPlayer('PlazaScene', dabao.x, dabao.y + 8); +await tap('KeyE'); await sleep(400); +const t2 = await drain(); +console.log('dabao 对话:', t2); +console.log('状态:', await dstate()); + +// 3. 送 mimi(shop)与 guiyeye(lake) +await startScene('ShopScene'); +await drain(); +const mimi = await ev(() => { const s = window.__game.scene.getScene('ShopScene'); const a = s.actors.find(a => a.def.id === 'mimi'); return { x: a.sprite.x, y: a.sprite.y }; }); +await setPlayer('ShopScene', mimi.x + 40, mimi.y + 40); +await setPlayer('ShopScene', mimi.x, mimi.y + 8); +await tap('KeyE'); await sleep(400); +console.log('mimi 对话:', await drain()); +console.log('状态:', await dstate()); + +await startScene('LakeScene'); +await drain(); +const gui = await ev(() => { const s = window.__game.scene.getScene('LakeScene'); const a = s.actors.find(a => a.def.id === 'guiyeye'); return { x: a.sprite.x, y: a.sprite.y }; }); +await setPlayer('LakeScene', gui.x + 40, gui.y + 40); +await setPlayer('LakeScene', gui.x, gui.y + 8); +await tap('KeyE'); await sleep(400); +console.log('guiyeye 对话:', await drain()); +console.log('状态:', await dstate()); + +console.log('\n=== B. 第一天睡觉(用户场景:任务未全了结,靠黄昏计时) ==='); +await startScene('HouseScene'); +await drain(); +// 强制黄昏计时,使 canSleep=true(模拟用户等满 5 分钟) +await ev(() => { window.__director.dayElapsedMs = 300000; }); +console.log('睡前状态:', await dstate()); +// 走到床边按 E +const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed')); +await setPlayer('HouseScene', bed.x + 40, bed.y + 40); +await setPlayer('HouseScene', bed.x, bed.y + 8); +await tap('KeyE'); await sleep(400); +console.log('床交互后对话:', await drain()); +// 采样 20 秒:场景序列与卡死点 +for (let i = 0; i < 20; i++) { + const s = await ev(() => { + const sm = window.__game.scene; + return { + active: sm.getScenes(true).map(sc => sc.scene.key), + day: window.__director.day, + dlg: window.__director.dialogueActive, + uiPayload: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 20) : null; })(), + }; + }); + console.log(`t+${(i+1)}s`, JSON.stringify(s)); + // 模拟玩家按 E 推进梦境对话 + if (s.uiPayload) { await tap('KeyE'); } + await sleep(1000); +} +console.log('最终状态:', await dstate()); +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/dbg17.mjs b/Kimi-K3/tools/dbg17.mjs new file mode 100644 index 0000000..5bb9ad4 --- /dev/null +++ b/Kimi-K3/tools/dbg17.mjs @@ -0,0 +1,84 @@ +// dbg17:睡觉卡死候选根因验证 +// (a) 床淡出期间庆典触发 → celebrationActive 卡死 / 对话 resolver 被覆盖 +// (b) 床淡出期间玩家移动 30px 再按 E → 二次触发床 → 双 scene.start +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,500))); +page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); }); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +const ev = (fn, ...a) => page.evaluate(fn, ...a); +const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); +const state = (tag) => ev((tag) => { + const sm = window.__game.scene; + const d = window.__director; + const s = sm.getScene('HouseScene'); + const door = s && s.doors ? s.doors[0] : null; + const pd = door && s.player ? { px: +s.player.x.toFixed(0), py: +(s.player.y - 8).toFixed(0), + dist: +Math.hypot(s.player.x - door.x, (s.player.y - 8) - door.y).toFixed(1), + armed: s.armedDoors.has(door), trans: s.transitioning } : null; + return { tag, active: sm.getScenes(true).map(sc => sc.scene.key), day: d.day, + dlg: d.dialogueActive, celeb: d.celebrationActive, happy: d.happiness, + ui: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 18) : null; })(), + pd }; +}, tag).then(s => console.log(JSON.stringify(s))); +async function drain(maxIter = 20) { + for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; await tap('KeyE'); await sleep(160); } +} +async function setPlayer(k, x, y) { + await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y); + await sleep(120); +} +await sleep(600); +const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed')); + +console.log('\n=== (a) 床淡出期间庆典触发 ==='); +// 低幸福 + 低幸福计时逼近阈值;强制可睡觉 +await ev(() => { + const d = window.__director; + d.setHappiness(20); + d.lowHappyMs = 119900; + d.dayElapsedMs = 300000; +}); +// 按 E 睡觉 → 淡出 400ms 内 tick 必触发庆典 +await setPlayer('HouseScene', bed.x, bed.y + 8); +await tap('KeyE'); +for (let i = 0; i < 12; i++) { await sleep(500); await state(`a t+${(i+1)*0.5}s`); if (await dlgOpen()) await tap('KeyE'); } +// 尝试走到门边逃脱(门在 (88,104)) +await state('a 结束,尝试过门'); +await setPlayer('HouseScene', 88 + 30, 104); +await setPlayer('HouseScene', 88, 104); +await sleep(1500); +await state('a 过门后'); + +console.log('\n=== (b) 床淡出期间二次触发 ==='); +await ev(() => { + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'HouseScene') sm.stop(k); } + sm.start('HouseScene', { spawn: 'default' }); +}); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 9000 }); +await sleep(500); +await drain(); +await ev(() => { window.__director.dayElapsedMs = 300000; }); +const bed2 = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed')); +await setPlayer('HouseScene', bed2.x, bed2.y + 8); +await tap('KeyE'); // 第一次:触发床,淡出开始 +await sleep(150); +await setPlayer('HouseScene', bed2.x + 40, bed2.y + 8); // 移动 >30px 使 spent 重武装 +await tap('KeyE'); // 第二次:可能二次触发床 +for (let i = 0; i < 10; i++) { await sleep(500); await state(`b t+${(i+1)*0.5}s`); if (await dlgOpen()) await tap('KeyE'); } +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/dbg18.mjs b/Kimi-K3/tools/dbg18.mjs new file mode 100644 index 0000000..f85560d --- /dev/null +++ b/Kimi-K3/tools/dbg18.mjs @@ -0,0 +1,116 @@ +// dbg18:睡觉卡死根因验证(干净页面) +// A 基线:真实流程睡觉+连按 E,应正常过日到 D2 +// B 嫌疑2:床淡出 400ms 内移动 30px 二次按 E → 是否双触发/卡死 +// C 嫌疑1:梦境对话期间注入第二个 showDialogue → resolver 覆盖是否导致 run() 永久悬挂 +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,500))); +page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); }); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +const ev = (fn, ...a) => page.evaluate(fn, ...a); +const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); +const state = (tag) => ev((tag) => { + const sm = window.__game.scene; + const d = window.__director; + return { tag, active: sm.getScenes(true).map(sc => sc.scene.key), day: d.day, + dlg: d.dialogueActive, celeb: d.celebrationActive, happy: d.happiness, + ui: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 22) : null; })() }; +}, tag).then(s => console.log(JSON.stringify(s))); +async function drain(maxIter = 30) { + for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; await tap('KeyE'); await sleep(180); } +} +async function setPlayer(k, x, y) { + await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y); + await sleep(120); +} +async function restartHouse() { + await ev(() => { + window.__director.forceIdle(); + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'HouseScene') sm.stop(k); } + if (!sm.isActive('HouseScene')) sm.start('HouseScene', { spawn: 'default' }); + }); + await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 9000 }); + await sleep(400); + await drain(); +} +await sleep(600); + +console.log('\n=== A 基线:真实睡觉流程 + 连按 E ==='); +await ev(() => { window.__director.dayElapsedMs = 300000; }); +const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed')); +await setPlayer('HouseScene', bed.x, bed.y + 8); +await tap('KeyE'); +let okA = false; +for (let i = 0; i < 24; i++) { + await sleep(500); + await state(`A t+${(i+1)*0.5}s`); + if (await dlgOpen()) await tap('KeyE'); + const d = await ev(() => window.__director.day); + if (d === 2) { okA = true; console.log('A 结果: 正常过日到 D2 ✓'); break; } +} +if (!okA) console.log('A 结果: 卡死 ✗'); + +console.log('\n=== B 床淡出期间二次触发 ==='); +await restartHouse(); +await ev(() => { const d = window.__director; d.setDay(1); d.dayElapsedMs = 300000; }); +await ev(() => { window.__dreamStarts = 0; const sm = window.__game.scene; + if (!sm.__patched) { const orig = sm.start.bind(sm); sm.start = (k, ...a) => { if (k === 'DreamScene') window.__dreamStarts++; return orig(k, ...a); }; sm.__patched = true; } }); +const bedB = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed')); +await setPlayer('HouseScene', bedB.x, bedB.y + 8); +await tap('KeyE'); +await sleep(150); +await setPlayer('HouseScene', bedB.x + 40, bedB.y + 8); // 移开 >30px 重武装 +await tap('KeyE'); // 二次按 E +let okB = false; +for (let i = 0; i < 24; i++) { + await sleep(500); + const extra = await ev(() => window.__dreamStarts); + await state(`B t+${(i+1)*0.5}s dreamStarts=${extra}`); + if (await dlgOpen()) await tap('KeyE'); + const d = await ev(() => window.__director.day); + if (d === 2) { okB = true; console.log(`B 结果: 过日成功, DreamScene 启动次数=${extra}`); break; } +} +if (!okB) console.log('B 结果: 卡死 ✗'); + +console.log('\n=== C 梦境对话期间注入第二个 showDialogue ==='); +await restartHouse(); +await ev(() => { const d = window.__director; d.setDay(1); d.dayElapsedMs = 300000; }); +const bedC = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed')); +await setPlayer('HouseScene', bedC.x, bedC.y + 8); +await tap('KeyE'); +await page.waitForFunction(() => window.__game.scene.isActive('DreamScene'), { timeout: 9000 }); +await page.waitForFunction(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }, { timeout: 9000 }); +await state('C 梦境第一行已开'); +// 注入第二个 showDialogue(模拟resolver被覆盖) +await ev(() => { window.__injectedP = window.__director.showDialogue({ lines: ['【注入】测试对话'] }); window.__injectedP.then(r => { window.__injectedResolved = r; }); }); +await sleep(300); +await state('C 注入后'); +await drain(); // 按 E 关掉注入对话 +await sleep(300); +await state('C 注入对话关闭后'); +// 继续按 E 试图推进梦境 +let okC = false; +for (let i = 0; i < 20; i++) { + await sleep(500); + await state(`C t+${(i+1)*0.5}s`); + if (await dlgOpen()) await tap('KeyE'); + const d = await ev(() => window.__director.day); + if (d === 2) { okC = true; console.log('C 结果: 梦境继续并过日 ✓'); break; } +} +if (!okC) console.log('C 结果: 梦境 run() 永久悬挂 = 卡死根因证实 ✗'); +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/dbg19.mjs b/Kimi-K3/tools/dbg19.mjs new file mode 100644 index 0000000..45019e1 --- /dev/null +++ b/Kimi-K3/tools/dbg19.mjs @@ -0,0 +1,123 @@ +// dbg19:修复后全流程真人验证 +// 1. D1 与圆圆对话取面包 → 送大宝/咪咪/龟爷爷 → T1 完成(不经隐形柜台) +// 2. D2 与咪咪对话清点货架 → T5 完成(不经隐形货架区) +// 3. 睡觉竞态:低幸福+计时逼近阈值时睡觉 → 不卡死,D2 可正常交互 +// 4. 庆典未被误抑制:低幸福计时到点不睡觉 → 庆典正常触发 +// 5. T13 彩带 ×4 新坐标可触发 +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); +page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,200)); }); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +const ev = (fn, ...a) => page.evaluate(fn, ...a); +const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); +const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; }); +async function drain(maxIter = 40) { + const texts = []; + for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; texts.push(await dlgText()); await tap('KeyE'); await sleep(160); } + return texts; +} +async function startScene(k) { + await ev((k) => { const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const key = sc.scene.key; if (key !== 'UIScene' && key !== k) sm.stop(key); } + sm.start(k, { spawn: 'default' }); }, k); + await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k); + await sleep(400); await drain(); +} +async function setPlayer(k, x, y) { + await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y); + await sleep(120); +} +async function talkNpc(sceneKey, x, y) { + await setPlayer(sceneKey, x + 40, y + 40); + await setPlayer(sceneKey, x, y + 8); + await tap('KeyE'); + await sleep(300); + return drain(); +} +const dget = (e) => ev(new Function(`return window.__director.${e}`)); +const darr = (e) => ev(new Function(`return [...window.__director.${e}]`)); +let fails = 0; +const check = (name, ok, extra = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra ? ' ' + extra : ''}`); if (!ok) fails++; }; +await sleep(500); + +console.log('\n=== 1. D1 与圆圆对话取面包 → 送三人 ==='); +// 小地图居中偏移:bakery/shop ox=40,oy=10;house ox=64,oy=26;plaza/lake 无偏移 +await startScene('BakeryScene'); +const t1 = await talkNpc('BakeryScene', 128, 50); // yuanyuan (88,40)+(40,10) +check('对话圆圆拿到面包', await dget("taskProgress['breadTaken']") === 1, JSON.stringify(t1).slice(0, 80)); +await startScene('PlazaScene'); +await talkNpc('PlazaScene', 56, 136); // dabao +await startScene('ShopScene'); +await talkNpc('ShopScene', 80, 98); // mimi(D1:只是送面包对象,非 T5) +await startScene('LakeScene'); +await talkNpc('LakeScene', 200, 136); // guiyeye +const done1 = await darr('tasksDone'); +check('T1 送面包完成', done1.includes('T1'), done1.join(',')); +check('T3 问好完成', done1.includes('T3'), done1.join(',')); + +console.log('\n=== 2. D2 与咪咪对话清点货架 ==='); +await ev(() => window.__director.setDay(2)); +await startScene('ShopScene'); +await talkNpc('ShopScene', 80, 98); // mimi +const done2 = await darr('tasksDone'); +check('T5 清点完成', done2.includes('T5'), done2.join(',')); +check('C5 线索获得', (await darr('clues')).includes('C5')); + +console.log('\n=== 3. 睡觉竞态:低幸福计时逼近阈值时睡觉 ==='); +await ev(() => { const d = window.__director; d.forceIdle(); d.setDay(1); d.setHappiness(20); }); +await startScene('HouseScene'); +// lowHappyMs=119700:阈值将在 ~300ms 后(即床淡出 400ms 中途)到达,正中竞态窗口 +await ev(() => { const d = window.__director; d.setHappiness(20); d.lowHappyMs = 119700; d.dayElapsedMs = 300000; }); +await setPlayer('HouseScene', 88, 90); // 床 (24,56)+(64,26)=(88,82),站 y+8 +await tap('KeyE'); +let reachedD2 = false, stuckCeleb = false; +for (let i = 0; i < 30; i++) { + await sleep(500); + if (await dlgOpen()) await tap('KeyE'); + const day = await dget('day'); + if (day === 2) { reachedD2 = true; break; } +} +check('睡觉正常过日到 D2(无庆典抢占)', reachedD2); +stuckCeleb = await dget('celebrationActive'); +check('D2 celebrationActive=false(E 可用)', !stuckCeleb); +// D2 交互验证:与日历对话 +await startScene('HouseScene'); +const cal = await talkNpc('HouseScene', 120, 66); // calendar (56,40)+(64,26) +check('D2 交互正常(日历对话)', cal.length > 0, JSON.stringify(cal).slice(0, 60)); + +console.log('\n=== 4. 庆典未被误抑制 ==='); +await ev(() => { const d = window.__director; d.forceIdle(); d.setHappiness(20); d.lowHappyMs = 119900; }); +await sleep(800); +check('低幸福计时到点→庆典正常触发', await dget('celebrationActive')); +await ev(() => window.__director.forceIdle()); + +console.log('\n=== 5. T13 彩带 ×4 新坐标 ==='); +await ev(() => window.__director.setDay(6)); +await startScene('PlazaScene'); +const ribbons = [[184, 120], [296, 120], [184, 216], [296, 216]]; +for (const [x, y] of ribbons) { + await setPlayer('PlazaScene', x + 40, y + 40); + await setPlayer('PlazaScene', x, y + 8); + await tap('KeyE'); + await sleep(300); + await drain(); +} +check('T13 彩带完成', (await darr('tasksDone')).includes('T13'), (await darr('tasksDone')).join(',')); + +console.log(`\n${fails === 0 ? '全部通过 ✓' : `失败 ${fails} 项 ✗`}`); +await browser.close(); +process.exit(fails === 0 ? 0 : 1); diff --git a/Kimi-K3/tools/dbg20.mjs b/Kimi-K3/tools/dbg20.mjs new file mode 100644 index 0000000..5571e5b --- /dev/null +++ b/Kimi-K3/tools/dbg20.mjs @@ -0,0 +1,120 @@ +// dbg20:审计修复验证 +// 1. 广场地图:邮筒瓦片(gid47)在 (11,10);4 个挂彩带点改为路灯(gid 29),无预置彩带 +// 2. C3 邮筒交互可达(D2) +// 3. T13 挂彩带前无彩带图像,挂上后出现(帧 29/30/31) +// 4. 阿福指引:D6→T14_HINT,D7→T15_HINT +// 5. 门淡出庆典竞态:低幸福计时逼近阈值时过门 → celebrationActive 不卡死,新场景可交互 +import fs from 'node:fs'; +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +let fails = 0; +const check = (name, ok, extra = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra ? ' ' + extra : ''}`); if (!ok) fails++; }; + +// ---- 静态地图检查 ---- +const plaza = JSON.parse(fs.readFileSync('public/assets/maps/plaza.json', 'utf8')); +const ob = plaza.layers.find(l => l.name === 'obstacles').data; +const W = plaza.width; +const g = (x, y) => ob[y * W + x]; +check('邮筒瓦片 gid47 @ (11,10)', g(11, 10) === 47); +check('挂彩带点全部改为路灯 gid29', g(11,7)===29 && g(18,7)===29 && g(11,13)===29 && g(18,13)===29); +check('无预置彩带 gid30/31/32', ![30,31,32].includes(g(11,7)) && ![30,31,32].includes(g(18,7)) && ![30,31,32].includes(g(11,13)) && ![30,31,32].includes(g(18,13))); + +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); +page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,200)); }); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +const ev = (fn, ...a) => page.evaluate(fn, ...a); +const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); +const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; }); +async function drain(maxIter = 40) { + const texts = []; + for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; texts.push(await dlgText()); await tap('KeyE'); await sleep(160); } + return texts; +} +async function startScene(k) { + await ev((k) => { const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const key = sc.scene.key; if (key !== 'UIScene' && key !== k) sm.stop(key); } + sm.start(k, { spawn: 'default' }); }, k); + await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k); + await sleep(400); await drain(); +} +async function setPlayer(k, x, y) { + await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y); + await sleep(120); +} +async function interactAt(sceneKey, x, y) { + await setPlayer(sceneKey, x + 40, y + 40); + await setPlayer(sceneKey, x, y + 8); + await tap('KeyE'); + await sleep(300); + return drain(); +} +const dget = (e) => ev(new Function(`return window.__director.${e}`)); +const darr = (e) => ev(new Function(`return [...window.__director.${e}]`)); +await sleep(500); + +console.log('\n=== 2. C3 邮筒交互(D2) ==='); +await ev(() => window.__director.setDay(2)); +await startScene('PlazaScene'); +const texts2 = await interactAt('PlazaScene', 184, 176); // mailbox_c3 区 +check('C3 线索获得', (await darr('clues')).includes('C3'), JSON.stringify(texts2).slice(0, 60)); + +console.log('\n=== 3. T13 挂彩带视觉反馈 ==='); +await ev(() => window.__director.setDay(6)); +await startScene('PlazaScene'); +const imgBefore = await ev(() => { + const s = window.__game.scene.getScene('PlazaScene'); + return s.children.list.filter(o => o.type === 'Image' && o.texture.key === 'tiles16' && [29,30,31].includes(o.frame.name)).length; +}); +check('挂彩带前场景无彩带图像', imgBefore === 0, `实际 ${imgBefore}`); +await interactAt('PlazaScene', 184, 120); // ribbon_1 +const imgAfter = await ev(() => { + const s = window.__game.scene.getScene('PlazaScene'); + return s.children.list.filter(o => o.type === 'Image' && o.texture.key === 'tiles16' && [29,30,31].includes(o.frame.name)).map(o => ({ x: o.x, y: o.y, f: o.frame.name })); +}); +check('挂上后出现彩带图像(帧29红@184,120)', imgAfter.some(o => o.x === 184 && o.y === 120 && o.f === 29), JSON.stringify(imgAfter)); + +console.log('\n=== 4. 阿福地点指引 ==='); +const t14 = await interactAt('PlazaScene', 248, 136); // afu @D6 +check('D6 阿福 T14 指引', t14.some(t => t.includes('广场中央站好')), JSON.stringify(t14).slice(0, 70)); +await ev(() => window.__director.setDay(7)); +await startScene('PlazaScene'); +const t15 = await interactAt('PlazaScene', 248, 136); // afu @D7 +check('D7 阿福 T15 指引', t15.some(t => t.includes('广场北边')), JSON.stringify(t15).slice(0, 70)); + +console.log('\n=== 5. 门淡出庆典竞态 ==='); +await ev(() => { const d = window.__director; d.forceIdle(); d.setDay(3); d.setHappiness(20); }); +await startScene('PlazaScene'); +// 阈值 ~300ms 后到达:落在门淡出 250ms 窗口内 +await ev(() => { const d = window.__director; d.setHappiness(20); d.lowHappyMs = 119700; }); +// 广场→小屋门 (72,88):先到 30px 外武装,再踏上门 +await setPlayer('PlazaScene', 72 + 30, 88); +await setPlayer('PlazaScene', 72, 88); +let arrived = false; +for (let i = 0; i < 20; i++) { + await sleep(300); + if (await dlgOpen()) await tap('KeyE'); + if (await ev(() => window.__game.scene.isActive('HouseScene'))) { arrived = true; break; } +} +check('过门到达小屋', arrived); +check('celebrationActive 未卡死', !(await dget('celebrationActive'))); +await drain(); +// 新场景可交互:日历 (56,40)+(64,26)=(120,66) +const cal = await interactAt('HouseScene', 120, 66); +check('新场景交互正常(日历)', cal.length > 0, JSON.stringify(cal).slice(0, 50)); + +console.log(`\n${fails === 0 ? '全部通过 ✓' : `失败 ${fails} 项 ✗`}`); +await browser.close(); +process.exit(fails === 0 ? 0 : 1); diff --git a/Kimi-K3/tools/dbg3.mjs b/Kimi-K3/tools/dbg3.mjs new file mode 100644 index 0000000..78055f8 --- /dev/null +++ b/Kimi-K3/tools/dbg3.mjs @@ -0,0 +1,22 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('console', m => { if (['error','warning'].includes(m.type())) console.log('[console]', m.type(), m.text().slice(0,400)); }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,800))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 15000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +const r = await page.evaluate(() => { + try { window.__game.scene.start('HouseScene', { spawn: 'default' }); return 'started'; } + catch (e) { return 'THROW: ' + (e.stack||e.message); } +}); +console.log('manual start:', r); +await new Promise(r2=>setTimeout(r2,3000)); +console.log('active:', await page.evaluate(() => window.__game.scene.getScenes(true).map(s=>s.scene.key))); +await browser.close(); diff --git a/Kimi-K3/tools/dbg5.mjs b/Kimi-K3/tools/dbg5.mjs new file mode 100644 index 0000000..6aabdae --- /dev/null +++ b/Kimi-K3/tools/dbg5.mjs @@ -0,0 +1,58 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,500))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 15000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +await new Promise(r=>setTimeout(r,600)); +// 切到 PlazaScene(停其它地图场景) +await page.evaluate(() => { + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); } + sm.start('PlazaScene', { spawn: 'default' }); +}); +await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 }); +await new Promise(r=>setTimeout(r,600)); +const dump1 = await page.evaluate(() => { + const s = window.__game.scene.getScene('PlazaScene'); + const u = window.__game.scene.getScene('UIScene'); + return { + day: window.__director.day, + dialogueActive: window.__director.dialogueActive, + uiActive: window.__game.scene.isActive('UIScene'), + player: { x: s.player.x, y: s.player.y }, + zones: s.interactZones.map(z => ({ id: z.id, x: z.x, y: z.y })).filter(z => z.id === 'noticeboard' || z.id.startsWith('flowerbed')), + payload: u.payload ? u.payload.lines : null, + }; +}); +console.log('dump1', JSON.stringify(dump1, null, 1)); +// 传送到公告板旁并按 E +await page.evaluate(() => { + const s = window.__game.scene.getScene('PlazaScene'); + const z = s.interactZones.find(z => z.id === 'noticeboard'); + s.player.setPosition(z.x, z.y + 8); s.player.setVelocity(0, 0); +}); +await new Promise(r=>setTimeout(r,200)); +await page.keyboard.press('KeyE'); +await new Promise(r=>setTimeout(r,600)); +const dump2 = await page.evaluate(() => { + const s = window.__game.scene.getScene('PlazaScene'); + const u = window.__game.scene.getScene('UIScene'); + return { + dialogueActive: window.__director.dialogueActive, + payload: u.payload ? u.payload.lines : null, + body: u.dlgBody ? u.dlgBody.text : null, + prompt: u.promptText ? u.promptText.text : null, + }; +}); +console.log('dump2', JSON.stringify(dump2, null, 1)); +await browser.close(); diff --git a/Kimi-K3/tools/dbg6.mjs b/Kimi-K3/tools/dbg6.mjs new file mode 100644 index 0000000..3287cc2 --- /dev/null +++ b/Kimi-K3/tools/dbg6.mjs @@ -0,0 +1,84 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600))); +page.on('console', m => { if (m.type() === 'error') console.log('[console.error]', m.text().slice(0,300)); }); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +await new Promise(r=>setTimeout(r,600)); +await page.evaluate(() => { + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); } + sm.start('PlazaScene', { spawn: 'default' }); +}); +await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 }); +await new Promise(r=>setTimeout(r,600)); + +// 路径 A:直接调用 activateInteract('noticeboard'),绕过键盘 +const pathA = await page.evaluate(async () => { + const s = window.__game.scene.getScene('PlazaScene'); + const z = s.interactZones.find(z => z.id === 'noticeboard'); + s.player.setPosition(z.x, z.y + 8); s.player.setVelocity(0, 0); + let err = null; + try { s.activateInteract('noticeboard'); } catch (e) { err = String(e && e.stack || e); } + await new Promise(r => setTimeout(r, 500)); + const u = window.__game.scene.getScene('UIScene'); + return { err, dialogueActive: window.__director.dialogueActive, payload: u.payload ? u.payload.lines : null, body: u.dlgBody ? u.dlgBody.text : null }; +}); +console.log('pathA(direct call):', JSON.stringify(pathA, null, 1)); + +// 清掉对话 +await page.evaluate(() => window.__director.forceIdle()); +await new Promise(r=>setTimeout(r,300)); + +// 路径 B:合成 DOM 键盘事件 +const pathB = await page.evaluate(async () => { + const fire = (type) => window.dispatchEvent(new KeyboardEvent(type, { code: 'KeyE', key: 'e', keyCode: 69, which: 69, bubbles: true, cancelable: true })); + fire('keydown'); + await new Promise(r => setTimeout(r, 120)); + fire('keyup'); + await new Promise(r => setTimeout(r, 500)); + const u = window.__game.scene.getScene('UIScene'); + return { dialogueActive: window.__director.dialogueActive, payload: u.payload ? u.payload.lines : null }; +}); +console.log('pathB(synthetic DOM key):', JSON.stringify(pathB, null, 1)); + +await page.evaluate(() => window.__director.forceIdle()); +await new Promise(r=>setTimeout(r,300)); + +// 路径 C:puppeteer 键盘(与验收脚本相同),先 dump JustDown 相关状态 +await page.keyboard.press('KeyE'); +await new Promise(r=>setTimeout(r,500)); +const pathC = await page.evaluate(() => { + const s = window.__game.scene.getScene('PlazaScene'); + const u = window.__game.scene.getScene('UIScene'); + return { + dialogueActive: window.__director.dialogueActive, + payload: u.payload ? u.payload.lines : null, + keyE: s.keyE ? { isDown: s.keyE.isDown, enabled: s.keyE.enabled } : null, + kbEnabled: s.input.keyboard ? s.input.keyboard.enabled : null, + }; +}); +console.log('pathC(puppeteer key):', JSON.stringify(pathC, null, 1)); + +// 路径 D:talkTo 直调 +const pathD = await page.evaluate(async () => { + const s = window.__game.scene.getScene('PlazaScene'); + let err = null; + try { s.talkTo('afu'); } catch (e) { err = String(e && e.stack || e); } + await new Promise(r => setTimeout(r, 500)); + const u = window.__game.scene.getScene('UIScene'); + return { err, dialogueActive: window.__director.dialogueActive, payload: u.payload ? u.payload.lines : null, body: u.dlgBody ? u.dlgBody.text : null }; +}); +console.log('pathD(talkTo afu):', JSON.stringify(pathD, null, 1)); + +await browser.close(); diff --git a/Kimi-K3/tools/dbg7.mjs b/Kimi-K3/tools/dbg7.mjs new file mode 100644 index 0000000..93d85e7 --- /dev/null +++ b/Kimi-K3/tools/dbg7.mjs @@ -0,0 +1,60 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +await sleep(500); +// 设定 D2 + 已取信 +await page.evaluate(() => { + const d = window.__director; + d.setDay(2); + d.taskProgress['letters'] = 3; + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'BakeryScene') sm.stop(k); } + sm.start('BakeryScene', { spawn: 'default' }); +}); +await page.waitForFunction(() => window.__game.scene.isActive('BakeryScene'), { timeout: 9000 }); +await sleep(600); +const dump1 = await page.evaluate(() => { + const s = window.__game.scene.getScene('BakeryScene'); + return { + day: window.__director.day, + letters: window.__director.taskProgress['letters'], + t4active: window.__director.isTaskActive('T4'), + actors: s.actors.map(a => ({ id: a.def.id, x: Math.round(a.sprite.x), y: Math.round(a.sprite.y) })), + player: { x: s.player.x, y: s.player.y }, + }; +}); +console.log('dump1', JSON.stringify(dump1)); +// 传送到圆圆旁(actor 实际坐标) +await page.evaluate(() => { + const s = window.__game.scene.getScene('BakeryScene'); + const a = s.actors.find(a => a.def.id === 'yuanyuan'); + s.player.setPosition(a.sprite.x, a.sprite.y); s.player.setVelocity(0,0); +}); +await sleep(200); +await tap('KeyE'); +await sleep(500); +const dump2 = await page.evaluate(() => { + const u = window.__game.scene.getScene('UIScene'); + return { + dialogueActive: window.__director.dialogueActive, + payload: u.payload ? u.payload.lines : null, + lettersTo: window.__director.taskSetProgress['lettersTo'] ? [...window.__director.taskSetProgress['lettersTo']] : null, + tasksDone: [...window.__director.tasksDone], + }; +}); +console.log('dump2', JSON.stringify(dump2, null, 1)); +await browser.close(); diff --git a/Kimi-K3/tools/dbg8.mjs b/Kimi-K3/tools/dbg8.mjs new file mode 100644 index 0000000..7d43ffa --- /dev/null +++ b/Kimi-K3/tools/dbg8.mjs @@ -0,0 +1,69 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +await sleep(400); +// 事件日志钩子 +await page.evaluate(() => { + const d = window.__director; + window.__log = []; + const orig = d.emit.bind(d); + d.emit = (ev, ...a) => { window.__log.push(`${Date.now()%100000} ${ev} ${JSON.stringify(a).slice(0,80)}`); return orig(ev, ...a); }; + d.setDay(2); + d.taskProgress['letters'] = 3; +}); +const SCENE = { bakery: 'BakeryScene', shop: 'ShopScene', clinic: 'ClinicScene' }; +async function startScene(k) { + await page.evaluate((k) => { + const sm = window.__game.scene; + for (const sc of sm.getScenes(true)) { const key = sc.scene.key; if (key !== 'UIScene' && key !== k) sm.stop(key); } + sm.start(k, { spawn: 'default' }); + }, k); + await page.waitForFunction((k) => window.__game.scene.isActive(k), { timeout: 9000 }, k); + await sleep(400); +} +async function deliver(mapKey, npcId) { + await startScene(SCENE[mapKey]); + await page.evaluate((npcId) => { + const k = Object.keys(window.__game.scene.scenes.map ? {} : {}); + const s = window.__game.scene.getScenes(true).find(sc => sc.actors && sc.actors.some(a => a.def.id === npcId)); + const a = s.actors.find(a => a.def.id === npcId); + s.player.setPosition(a.sprite.x, a.sprite.y + 8); s.player.setVelocity(0,0); + }, npcId); + await sleep(150); + await tap('KeyE'); + await sleep(400); + // drain 最多 8 次 + for (let i = 0; i < 8; i++) { + const open = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); + if (!open) break; + await tap('KeyE'); + await sleep(200); + } + const st = await page.evaluate(() => ({ + lettersTo: window.__director.taskSetProgress['lettersTo'] ? [...window.__director.taskSetProgress['lettersTo']] : null, + clues: [...window.__director.clues], tasks: [...window.__director.tasksDone], + dlg: window.__director.dialogueActive, + })); + console.log(`after ${npcId}:`, JSON.stringify(st)); +} +await deliver('bakery', 'yuanyuan'); +await deliver('shop', 'mimi'); +await deliver('clinic', 'fuyisheng'); +await sleep(800); +console.log('--- event log ---'); +for (const l of await page.evaluate(() => window.__log)) console.log(l); +process.exit(0); diff --git a/Kimi-K3/tools/dbg9.mjs b/Kimi-K3/tools/dbg9.mjs new file mode 100644 index 0000000..7e628b2 --- /dev/null +++ b/Kimi-K3/tools/dbg9.mjs @@ -0,0 +1,43 @@ +import puppeteer from 'puppeteer-core'; +import { spawn } from 'node:child_process'; +import net from 'node:net'; +const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); +process.on('exit', () => { try{preview.kill()}catch{} }); +for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } +const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 560 }); +page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); +await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); +await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); +await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); +await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); +await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); +const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); +const active = (k) => page.evaluate((k) => window.__game.scene.isActive(k), k); +const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; +// 清掉开场对话 +for (let i = 0; i < 8; i++) { + const open = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); + if (!open) break; + await tap('KeyE'); await sleep(200); +} +await sleep(300); + +// 1) 从小屋自然走出去:先右移对齐门(x=152),再向下穿出 +await page.keyboard.down('KeyD'); await sleep(420); await page.keyboard.up('KeyD'); +await page.keyboard.down('KeyS'); await sleep(800); await page.keyboard.up('KeyS'); +await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 }); +console.log('出门 → PlazaScene OK'); +// 2) 等待 2 秒:不得回弹 +await sleep(2000); +const still = await active('PlazaScene'); +const houseBack = await active('HouseScene'); +console.log('2 秒后仍在广场:', still, '| 弹回小屋:', houseBack, still && !houseBack ? 'PASS' : 'FAIL'); +// 3) 立即折返:向上走回门里(落点 24px 已武装,应可正常回屋) +await page.keyboard.down('KeyW'); await sleep(900); await page.keyboard.up('KeyW'); +await sleep(800); +const backHome = await active('HouseScene'); +console.log('折返回家:', backHome ? 'PASS' : 'FAIL'); +await browser.close(); +process.exit(0); diff --git a/Kimi-K3/tools/shots/01_title.png b/Kimi-K3/tools/shots/01_title.png new file mode 100644 index 0000000..18b1483 Binary files /dev/null and b/Kimi-K3/tools/shots/01_title.png differ diff --git a/Kimi-K3/tools/shots/02_house_d1.png b/Kimi-K3/tools/shots/02_house_d1.png new file mode 100644 index 0000000..3c9da5e Binary files /dev/null and b/Kimi-K3/tools/shots/02_house_d1.png differ diff --git a/Kimi-K3/tools/shots/03_plaza.png b/Kimi-K3/tools/shots/03_plaza.png new file mode 100644 index 0000000..d2dd5a4 Binary files /dev/null and b/Kimi-K3/tools/shots/03_plaza.png differ diff --git a/Kimi-K3/tools/shots/09_hud_s0.png b/Kimi-K3/tools/shots/09_hud_s0.png new file mode 100644 index 0000000..33f76d5 Binary files /dev/null and b/Kimi-K3/tools/shots/09_hud_s0.png differ diff --git a/Kimi-K3/tools/shots/09_hud_s2.png b/Kimi-K3/tools/shots/09_hud_s2.png new file mode 100644 index 0000000..a0234ec Binary files /dev/null and b/Kimi-K3/tools/shots/09_hud_s2.png differ diff --git a/Kimi-K3/tools/shots/09_hud_s4.png b/Kimi-K3/tools/shots/09_hud_s4.png new file mode 100644 index 0000000..dd2d587 Binary files /dev/null and b/Kimi-K3/tools/shots/09_hud_s4.png differ diff --git a/Kimi-K3/tools/shots/09_stage_S0_h10.png b/Kimi-K3/tools/shots/09_stage_S0_h10.png new file mode 100644 index 0000000..df61458 Binary files /dev/null and b/Kimi-K3/tools/shots/09_stage_S0_h10.png differ diff --git a/Kimi-K3/tools/shots/09_stage_S1_h40.png b/Kimi-K3/tools/shots/09_stage_S1_h40.png new file mode 100644 index 0000000..07a7e6e Binary files /dev/null and b/Kimi-K3/tools/shots/09_stage_S1_h40.png differ diff --git a/Kimi-K3/tools/shots/09_stage_S2_h70.png b/Kimi-K3/tools/shots/09_stage_S2_h70.png new file mode 100644 index 0000000..9ed51e2 Binary files /dev/null and b/Kimi-K3/tools/shots/09_stage_S2_h70.png differ diff --git a/Kimi-K3/tools/shots/09_stage_S3_h90.png b/Kimi-K3/tools/shots/09_stage_S3_h90.png new file mode 100644 index 0000000..feeaf81 Binary files /dev/null and b/Kimi-K3/tools/shots/09_stage_S3_h90.png differ diff --git a/Kimi-K3/tools/shots/09_stage_S4_h100.png b/Kimi-K3/tools/shots/09_stage_S4_h100.png new file mode 100644 index 0000000..6ff9af7 Binary files /dev/null and b/Kimi-K3/tools/shots/09_stage_S4_h100.png differ diff --git a/Kimi-K3/tools/shots/13_t14_smile.png b/Kimi-K3/tools/shots/13_t14_smile.png new file mode 100644 index 0000000..be90bc0 Binary files /dev/null and b/Kimi-K3/tools/shots/13_t14_smile.png differ diff --git a/Kimi-K3/tools/shots/14_rain.png b/Kimi-K3/tools/shots/14_rain.png new file mode 100644 index 0000000..342cb1a Binary files /dev/null and b/Kimi-K3/tools/shots/14_rain.png differ diff --git a/Kimi-K3/tools/shots/15_e1.png b/Kimi-K3/tools/shots/15_e1.png new file mode 100644 index 0000000..8051dd9 Binary files /dev/null and b/Kimi-K3/tools/shots/15_e1.png differ diff --git a/Kimi-K3/tools/shots/15_e2.png b/Kimi-K3/tools/shots/15_e2.png new file mode 100644 index 0000000..0ca28dc Binary files /dev/null and b/Kimi-K3/tools/shots/15_e2.png differ diff --git a/Kimi-K3/tools/shots/15_e3_realface.png b/Kimi-K3/tools/shots/15_e3_realface.png new file mode 100644 index 0000000..3b8772c Binary files /dev/null and b/Kimi-K3/tools/shots/15_e3_realface.png differ diff --git a/Kimi-K3/tools/shots/15_e3_record.png b/Kimi-K3/tools/shots/15_e3_record.png new file mode 100644 index 0000000..80f869b Binary files /dev/null and b/Kimi-K3/tools/shots/15_e3_record.png differ diff --git a/Kimi-K3/tools/shots/15_e4.png b/Kimi-K3/tools/shots/15_e4.png new file mode 100644 index 0000000..b6f1be4 Binary files /dev/null and b/Kimi-K3/tools/shots/15_e4.png differ diff --git a/Kimi-K3/tools/shots/15_white_corridor.png b/Kimi-K3/tools/shots/15_white_corridor.png new file mode 100644 index 0000000..a80236e Binary files /dev/null and b/Kimi-K3/tools/shots/15_white_corridor.png differ diff --git a/Kimi-K3/tools/shots/fix_FieldScene.png b/Kimi-K3/tools/shots/fix_FieldScene.png new file mode 100644 index 0000000..ffd50f5 Binary files /dev/null and b/Kimi-K3/tools/shots/fix_FieldScene.png differ diff --git a/Kimi-K3/tools/shots/fix_HouseScene.png b/Kimi-K3/tools/shots/fix_HouseScene.png new file mode 100644 index 0000000..c76fb7c Binary files /dev/null and b/Kimi-K3/tools/shots/fix_HouseScene.png differ diff --git a/Kimi-K3/tools/shots/fix_LakeScene.png b/Kimi-K3/tools/shots/fix_LakeScene.png new file mode 100644 index 0000000..7a111fd Binary files /dev/null and b/Kimi-K3/tools/shots/fix_LakeScene.png differ diff --git a/Kimi-K3/tools/shots/fix_PlazaScene.png b/Kimi-K3/tools/shots/fix_PlazaScene.png new file mode 100644 index 0000000..8b740c8 Binary files /dev/null and b/Kimi-K3/tools/shots/fix_PlazaScene.png differ diff --git a/Kimi-K3/tools/shots/fix_WellScene.png b/Kimi-K3/tools/shots/fix_WellScene.png new file mode 100644 index 0000000..76e34c3 Binary files /dev/null and b/Kimi-K3/tools/shots/fix_WellScene.png differ diff --git a/Kimi-K3/tools/tiled/genmaps.js b/Kimi-K3/tools/tiled/genmaps.js new file mode 100644 index 0000000..bfa9fb5 --- /dev/null +++ b/Kimi-K3/tools/tiled/genmaps.js @@ -0,0 +1,474 @@ +// genmaps.js — 生成《崩坏童话:蜜糖村》全部 10 张 Tiled 地图 (.tmx) +// 用法: /Applications/Tiled.app/Contents/MacOS/Tiled -e tools/tiled/genmaps.js +// 说明: -e 模式无编辑器,tiled.open/TileMap 绘制 API 不可用,故用 TextFile 直接写 .tmx XML。 +// 导出: Tiled --export-map --embed-tilesets in.tmx out.json (必须 --embed-tilesets,JSON 才有 image 字段) +// +// 约定(与 CONTRACT.md §3 一致): +// - 固定三层:ground(瓦片层)/ obstacles(瓦片层,图层属性 collides=true)/ objects(点对象层) +// - 瓦片 gid = CONTRACT §2 瓦片 id + 1;obstacles 中空格 = gid 0 +// - 点对象坐标 = 所在瓦片中心像素 (tx*16+8, ty*16+8) +// - door 属性 target=目标地图 key, spawn=目标地图中的 spawn id(= "from_"+本图 key),双向成对 + +var OUT = '/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/public/assets/maps/'; + +// ---- 瓦片 gid 表(id+1)---- +var G = { + grass:1, flowerA:2, flowerB:3, deepGrass:4, dirt:5, stone:6, stoneAlt:7, brick:8, + waterA:9, waterB:10, shoreN:11, shoreS:12, shoreW:13, shoreE:14, + bedPink:15, bedYellow:16, berry:17, berryEmpty:18, + treeTL:19, treeTR:20, treeBL:21, treeBR:22, bush:23, stump:24, rock:25, sign:26, + fenceH:27, fenceV:28, lamp:29, ribbonR:30, ribbonY:31, ribbonB:32, + creamWall:33, creamWallWin:34, woodWall:35, roofOL:36, roofOM:37, roofOR:38, + doorT:39, doorTop:40, window:41, chimney:42, wellTL:43, wellTR:44, wellBL:45, wellBR:46, + mailbox:47, board:48, floorA:49, floorB:50, wallIn:51, wallInWin:52, + bedTL:53, bedTR:54, bedBL:55, bedBR:56, counterL:57, counterR:58, oven:59, + shelfE:60, shelfF:61, table:62, chair:63, rug:64, calendar:65, recipe:66, fileP:67, + picTL:68, picTR:69, picBL:70, picBR:71, benchL:72, benchR:73, stool:74, cabinet:75, + fridge:76, sink:77, plant:78, barrel:79, crate:80, fogA:81, fogB:82, fogC:83, + wFloor:84, wWall:85, wPanel:86, wDoor:87, wBedTL:88, wBedTR:89, wBedBL:90, wBedBR:91, + wCab:92, wMon:93, wCur:94, wFloor2:95, wLamp:96, ladder:97, bridgeH:98, bridgeV:99, + mushroom:100, flR:101, flY:102, flB:103, honey:104, bread:105, letter:106, book:107, + candle:108, basket:109, bucket:110, broom:111, cake:112, roofRL:113, roofRM:114, roofRR:115, + roofBL:116, roofBM:117, roofBR:118, pinkWall:119, pinkWallWin:120, stoneWall:121, + stoneWallWin:122, awning:123, clock:124, flag:125, garC:126, garM:127, empty:128 +}; + +// ---- 基础工具(ES5)---- +function grid(w, h, v) { + var a = []; + for (var y = 0; y < h; y++) { + var r = []; + for (var x = 0; x < w; x++) r.push(v); + a.push(r); + } + return a; +} +function newMap(w, h) { return { w: w, h: h, ground: grid(w, h, 0), obs: grid(w, h, 0), objs: [] }; } +function fill(l, x, y, w, h, g) { + for (var j = y; j < y + h; j++) + for (var i = x; i < x + w; i++) l[j][i] = g; +} +function put(l, x, y, g) { l[y][x] = g; } +// 确定性散点随机数(固定种子,结果可复现) +function rnd(x, y, s) { + var n = Math.sin(x * 127.1 + y * 311.7 + s * 74.7) * 43758.5453; + return n - Math.floor(n); +} +function obj(m, name, tx, ty, props) { + m.objs.push({ name: name, x: tx * 16 + 8, y: ty * 16 + 8, props: props }); +} +function spawn(m, tx, ty, id) { obj(m, 'spawn', tx, ty, [['id', id]]); } +function door(m, tx, ty, target, sp) { obj(m, 'door', tx, ty, [['target', target], ['spawn', sp]]); } +function npc(m, tx, ty, id) { obj(m, 'npc', tx, ty, [['id', id]]); } +function inter(m, tx, ty, id) { obj(m, 'interact', tx, ty, [['id', id]]); } +function tree2x2(m, x, y) { + put(m.obs, x, y, G.treeTL); put(m.obs, x + 1, y, G.treeTR); + put(m.obs, x, y + 1, G.treeBL); put(m.obs, x + 1, y + 1, G.treeBR); +} +// 草地散点装饰(ground 层,无碰撞) +function scatter(m, seed, d) { + for (var y = 0; y < m.h; y++) { + for (var x = 0; x < m.w; x++) { + var r = rnd(x, y, seed); + var g = 0; + if (r < d[0]) g = G.flowerA; + else if (r < d[1]) g = G.flowerB; + else if (r < d[2]) g = G.deepGrass; + else if (r < d[3]) g = [G.flR, G.flY, G.flB][(x + y) % 3]; + if (g) m.ground[y][x] = g; + } + } +} +// 室外房屋:5 宽 × 4 高,朝南单门,整体进 obstacles +function houseS(m, x, y, roofL, roofM, roofR, chimney) { + for (var i = 0; i < 5; i++) { + var t = (i === 0) ? roofL : (i === 4 ? roofR : roofM); + put(m.obs, x + i, y, t); + put(m.obs, x + i, y + 1, t); + } + if (chimney) put(m.obs, x + 1, y, G.chimney); + put(m.obs, x, y + 2, G.creamWall); + put(m.obs, x + 1, y + 2, G.creamWallWin); + put(m.obs, x + 2, y + 2, G.doorTop); + put(m.obs, x + 3, y + 2, G.creamWallWin); + put(m.obs, x + 4, y + 2, G.creamWall); + put(m.obs, x, y + 3, G.creamWall); + put(m.obs, x + 1, y + 3, G.creamWall); + put(m.obs, x + 2, y + 3, G.doorT); + put(m.obs, x + 3, y + 3, G.creamWall); + put(m.obs, x + 4, y + 3, G.creamWall); +} +// 室内外壳:木地板满铺(ground),墙围边(obstacles),底边留门(门瓦片在 ground) +function indoorShell(m, wins, doorX) { + for (var y = 0; y < m.h; y++) + for (var x = 0; x < m.w; x++) + m.ground[y][x] = ((x + y) % 2 === 0) ? G.floorA : G.floorB; + m.ground[m.h - 1][doorX] = G.doorT; + for (var x2 = 0; x2 < m.w; x2++) { + m.obs[0][x2] = G.wallIn; + if (x2 !== doorX) m.obs[m.h - 1][x2] = G.wallIn; + } + for (var i = 0; i < wins.length; i++) m.obs[0][wins[i]] = G.wallInWin; + for (var y2 = 1; y2 < m.h - 1; y2++) { + m.obs[y2][0] = G.wallIn; + m.obs[y2][m.w - 1] = G.wallIn; + } +} + +// ================= 1. house 12×8 小满小屋 ================= +function buildHouse() { + var m = newMap(12, 8); + indoorShell(m, [3, 8], 5); + // 床(睡觉过日)+ 床头日历(C1) + put(m.obs, 1, 1, G.bedTL); put(m.obs, 2, 1, G.bedTR); + put(m.obs, 1, 2, G.bedBL); put(m.obs, 2, 2, G.bedBR); + put(m.obs, 3, 1, G.calendar); + // 桌椅 + 盆栽 + 地毯 + put(m.obs, 8, 4, G.table); put(m.obs, 7, 4, G.chair); put(m.obs, 9, 4, G.chair); + put(m.obs, 10, 1, G.plant); + put(m.ground, 5, 3, G.rug); put(m.ground, 6, 3, G.rug); + put(m.ground, 5, 4, G.rug); put(m.ground, 6, 4, G.rug); + spawn(m, 3, 3, 'default'); + spawn(m, 5, 5, 'from_plaza'); + door(m, 5, 6, 'plaza', 'from_house'); + inter(m, 1, 3, 'bed'); + inter(m, 3, 2, 'calendar'); + return m; +} + +// ================= 2. plaza 30×20 村广场(枢纽) ================= +function buildPlaza() { + var m = newMap(30, 20); + fill(m.ground, 0, 0, 30, 20, G.grass); + scatter(m, 7, [0.05, 0.10, 0.15, 0.18]); + // 石路:南北 x=14/15,东西 y=9/10,通往四座房屋的小路 + var y, x; + for (y = 0; y < 20; y++) { + put(m.ground, 14, y, (y % 3 === 0) ? G.stoneAlt : G.stone); + put(m.ground, 15, y, (y % 3 === 0) ? G.stoneAlt : G.stone); + } + for (x = 0; x < 30; x++) { + put(m.ground, x, 9, (x % 3 === 0) ? G.stoneAlt : G.stone); + put(m.ground, x, 10, (x % 3 === 0) ? G.stoneAlt : G.stone); + } + var stubs = [4, 11, 18, 25]; + for (var s = 0; s < stubs.length; s++) + for (y = 5; y <= 9; y++) put(m.ground, stubs[s], y, G.stone); + // 中央广场砖(庆典场地核心) + fill(m.ground, 11, 8, 8, 5, G.brick); + // 庆典彩带(广场四角,装饰无碰撞;透明瓦片须放 obstacles 层,ground 层会露黑底) + put(m.obs, 11, 7, G.ribbonR); put(m.obs, 18, 7, G.ribbonY); + put(m.obs, 11, 13, G.ribbonB); put(m.obs, 18, 13, G.ribbonR); + // 四周房屋:小屋/杂货店=橙屋顶,面包房/诊所=红屋顶 + houseS(m, 2, 1, G.roofOL, G.roofOM, G.roofOR, true); // house 门 x=4 + houseS(m, 9, 1, G.roofRL, G.roofRM, G.roofRR, true); // bakery 门 x=11 + houseS(m, 16, 1, G.roofOL, G.roofOM, G.roofOR, true); // shop 门 x=18 + houseS(m, 23, 1, G.roofRL, G.roofRM, G.roofRR, false); // clinic 门 x=25 + // 公告板 + 三个花坛 + 双子鸟大树 + put(m.obs, 14, 8, G.board); + put(m.obs, 10, 8, G.bedPink); put(m.obs, 19, 8, G.bedYellow); put(m.obs, 14, 13, G.bedPink); + tree2x2(m, 12, 14); + // 村口邮筒(大宝)+ 路灯 + 装饰树/灌木 + put(m.obs, 2, 8, G.mailbox); + put(m.obs, 10, 6, G.lamp); put(m.obs, 19, 6, G.lamp); + tree2x2(m, 1, 6); tree2x2(m, 27, 14); tree2x2(m, 1, 16); + put(m.obs, 8, 16, G.bush); put(m.obs, 21, 17, G.bush); put(m.obs, 16, 15, G.bush); + // 地图边缘装饰(留出 4 个路口) + function skip(x2, y2) { + return (y2 === 0 && (x2 === 14 || x2 === 15)) || (y2 === 19 && (x2 === 14 || x2 === 15)) || + (x2 === 0 && (y2 === 9 || y2 === 10)) || (x2 === 29 && (y2 === 9 || y2 === 10)); + } + function edge(x2, y2) { + var r = rnd(x2, y2, 9); + put(m.obs, x2, y2, r < 0.5 ? G.bush : (r < 0.75 ? G.rock : G.stump)); + } + for (x = 0; x < 30; x++) { if (!skip(x, 0)) edge(x, 0); if (!skip(x, 19)) edge(x, 19); } + for (y = 1; y < 19; y++) { if (!skip(0, y)) edge(0, y); if (!skip(29, y)) edge(29, y); } + // 对象 + spawn(m, 14, 11, 'default'); + spawn(m, 4, 7, 'from_house'); // 落点距门 24px,防回弹 + spawn(m, 11, 7, 'from_bakery'); + spawn(m, 18, 7, 'from_shop'); + spawn(m, 25, 7, 'from_clinic'); + spawn(m, 1, 10, 'from_lake'); + spawn(m, 28, 10, 'from_field'); + spawn(m, 14, 2, 'from_well'); // 落点距门 24px,防回弹 + spawn(m, 15, 18, 'from_fog'); + door(m, 4, 5, 'house', 'from_plaza'); + door(m, 11, 5, 'bakery', 'from_plaza'); + door(m, 18, 5, 'shop', 'from_plaza'); + door(m, 25, 5, 'clinic', 'from_plaza'); + door(m, 0, 9, 'lake', 'from_plaza'); + door(m, 29, 10, 'field', 'from_plaza'); + door(m, 14, 0, 'well', 'from_plaza'); + door(m, 15, 19, 'fog', 'from_plaza'); + npc(m, 15, 8, 'afu'); // 公告板旁 + npc(m, 12, 14, 'bird'); // 双子鸟,树上 + npc(m, 7, 12, 'squirrel'); + npc(m, 22, 14, 'fox'); + npc(m, 3, 8, 'dabao'); // 村口邮筒旁(PRD §4) + inter(m, 14, 9, 'noticeboard'); + inter(m, 10, 9, 'flowerbed_1'); + inter(m, 19, 9, 'flowerbed_2'); + inter(m, 14, 14, 'flowerbed_3'); + return m; +} + +// ================= 3. bakery 15×10 面包房 ================= +function buildBakery() { + var m = newMap(15, 10); + indoorShell(m, [3, 7, 11], 7); + fill(m.ground, 5, 5, 3, 2, G.rug); + // 两段柜台 + 烤炉(C2 配方纸在桌上) + put(m.obs, 4, 3, G.counterL); put(m.obs, 5, 3, G.counterR); + put(m.obs, 8, 3, G.counterL); put(m.obs, 9, 3, G.counterR); + put(m.obs, 12, 1, G.oven); + put(m.obs, 10, 6, G.table); put(m.obs, 11, 6, G.recipe); + put(m.obs, 13, 8, G.barrel); put(m.obs, 1, 8, G.plant); + spawn(m, 7, 6, 'default'); + spawn(m, 7, 7, 'from_plaza'); + door(m, 7, 8, 'plaza', 'from_bakery'); + npc(m, 5, 2, 'yuanyuan'); // 柜台后 + inter(m, 4, 4, 'counter_bread'); + inter(m, 11, 7, 'recipe_paper'); + return m; +} + +// ================= 4. shop 15×10 杂货店 ================= +function buildShop() { + var m = newMap(15, 10); + indoorShell(m, [3, 7, 11], 7); + fill(m.ground, 6, 6, 3, 2, G.rug); + // 上排 9 格满货架(C5:同一商品重复 9 格)+ 下排空满混排 + var x; + for (x = 3; x <= 11; x++) put(m.obs, x, 1, G.shelfF); + for (x = 3; x <= 11; x++) put(m.obs, x, 4, (x % 3 === 0) ? G.shelfE : G.shelfF); + put(m.obs, 1, 7, G.crate); put(m.obs, 1, 8, G.barrel); put(m.obs, 13, 1, G.plant); + spawn(m, 7, 6, 'default'); + spawn(m, 7, 7, 'from_plaza'); + door(m, 7, 8, 'plaza', 'from_shop'); + npc(m, 2, 5, 'mimi'); + npc(m, 11, 7, 'hedgehog'); + inter(m, 7, 2, 'shelf_count'); + return m; +} + +// ================= 5. clinic 12×8 诊所 ================= +function buildClinic() { + var m = newMap(12, 8); + indoorShell(m, [3, 8], 5); + // 芙医生的桌 + 桌上文件(C8) + put(m.obs, 8, 2, G.table); put(m.obs, 9, 2, G.fileP); + put(m.obs, 9, 3, G.chair); + put(m.obs, 1, 1, G.cabinet); put(m.obs, 10, 6, G.plant); + spawn(m, 6, 5, 'default'); + spawn(m, 5, 5, 'from_plaza'); + door(m, 5, 6, 'plaza', 'from_clinic'); + npc(m, 8, 1, 'fuyisheng'); // 桌后 + inter(m, 8, 3, 'doctor_desk'); + return m; +} + +// ================= 6. lake 24×16 湖畔 ================= +function buildLake() { + var m = newMap(24, 16); + fill(m.ground, 0, 0, 24, 16, G.grass); + scatter(m, 21, [0.06, 0.11, 0.15, 0.18]); + var x, y; + // 石路:东门(23,10)通向长椅与野餐区 + for (x = 3; x <= 23; x++) put(m.ground, x, 10, (x % 3 === 0) ? G.stoneAlt : G.stone); + // 野餐布(T7,走上去触发,放 ground 可行走) + put(m.ground, 15, 10, G.picTL); put(m.ground, 16, 10, G.picTR); + put(m.ground, 15, 11, G.picBL); put(m.ground, 16, 11, G.picBR); + put(m.ground, 3, 12, G.mushroom); + // 湖水:北半边大片湖,水岸瓦片围边(全部进 obstacles) + for (y = 0; y <= 5; y++) + for (x = 2; x <= 21; x++) + put(m.obs, x, y, ((x + y) % 2 === 0) ? G.waterA : G.waterB); + for (x = 1; x <= 22; x++) put(m.obs, x, 6, G.shoreS); + for (y = 0; y <= 5; y++) { put(m.obs, 1, y, G.shoreW); put(m.obs, 22, y, G.shoreE); } + // 长椅(龟爷爷)+ 树木花草装饰 + put(m.obs, 10, 8, G.benchL); put(m.obs, 11, 8, G.benchR); + tree2x2(m, 1, 13); tree2x2(m, 20, 12); + put(m.obs, 8, 14, G.bush); put(m.obs, 12, 13, G.bush); + put(m.obs, 5, 9, G.rock); put(m.obs, 14, 14, G.stump); + spawn(m, 21, 11, 'default'); + spawn(m, 22, 10, 'from_plaza'); + door(m, 23, 10, 'plaza', 'from_lake'); + npc(m, 12, 8, 'guiyeye'); // 长椅旁 + npc(m, 6, 12, 'pig'); + inter(m, 16, 11, 'picnic_mat'); + return m; +} + +// ================= 7. field 20×14 花田 ================= +function buildField() { + var m = newMap(20, 14); + fill(m.ground, 0, 0, 20, 14, G.grass); + scatter(m, 33, [0.10, 0.18, 0.24, 0.32]); + var x, y; + // 土路横穿(西门进出) + for (x = 0; x <= 19; x++) put(m.ground, x, 7, G.dirt); + // 浇花点花丛(T2/T12 用,ground 装饰) + put(m.ground, 15, 9, G.flR); put(m.ground, 16, 9, G.flY); put(m.ground, 17, 9, G.flB); + put(m.ground, 15, 10, G.flY); put(m.ground, 16, 10, G.flR); put(m.ground, 17, 10, G.flB); + // 栅栏围边,西门留口 + for (x = 0; x <= 19; x++) { put(m.obs, x, 0, G.fenceH); put(m.obs, x, 13, G.fenceH); } + for (y = 1; y <= 12; y++) { if (y !== 7) put(m.obs, 0, y, G.fenceV); put(m.obs, 19, y, G.fenceV); } + // 5 丛莓果(T6) + var berries = [[4, 4], [9, 4], [14, 4], [6, 9], [12, 9]]; + for (var b = 0; b < berries.length; b++) put(m.obs, berries[b][0], berries[b][1], G.berry); + // 树木装饰 + tree2x2(m, 16, 1); + put(m.obs, 2, 11, G.bush); put(m.obs, 10, 11, G.bush); put(m.obs, 2, 2, G.rock); + spawn(m, 2, 7, 'default'); + spawn(m, 1, 7, 'from_plaza'); + door(m, 0, 7, 'plaza', 'from_field'); + npc(m, 10, 7, 'chick'); + inter(m, 4, 5, 'berry_1'); + inter(m, 9, 5, 'berry_2'); + inter(m, 14, 5, 'berry_3'); + inter(m, 6, 10, 'berry_4'); + inter(m, 12, 10, 'berry_5'); + inter(m, 16, 10, 'water_point'); + return m; +} + +// ================= 8. well 14×10 古井空地 ================= +function buildWell() { + var m = newMap(14, 10); + fill(m.ground, 0, 0, 14, 10, G.grass); + scatter(m, 45, [0.07, 0.13, 0.17, 0.19]); + var y; + // 土场 + 南门土路 + fill(m.ground, 5, 3, 4, 4, G.dirt); + for (y = 7; y <= 9; y++) { put(m.ground, 6, y, G.dirt); put(m.ground, 7, y, G.dirt); } + put(m.ground, 3, 4, G.mushroom); put(m.ground, 10, 6, G.mushroom); + // 中央古井(C7) + put(m.obs, 6, 4, G.wellTL); put(m.obs, 7, 4, G.wellTR); + put(m.obs, 6, 5, G.wellBL); put(m.obs, 7, 5, G.wellBR); + // 四周树木 + tree2x2(m, 1, 1); tree2x2(m, 11, 1); tree2x2(m, 1, 7); tree2x2(m, 11, 7); + put(m.obs, 4, 2, G.bush); put(m.obs, 9, 2, G.bush); + put(m.obs, 4, 8, G.bush); put(m.obs, 9, 8, G.bush); + put(m.obs, 4, 5, G.rock); put(m.obs, 9, 5, G.rock); + spawn(m, 6, 8, 'default'); + spawn(m, 7, 8, 'from_plaza'); + door(m, 7, 9, 'plaza', 'from_well'); + inter(m, 6, 6, 'ancient_well'); + return m; +} + +// ================= 9. fog 20×8 雾墙边界 ================= +function buildFog() { + var m = newMap(20, 8); + fill(m.ground, 0, 0, 20, 8, G.grass); + scatter(m, 57, [0.05, 0.09, 0.20, 0.20]); + var x, y; + // 雾前深草带 + 北门土路 + for (x = 0; x <= 19; x++) put(m.ground, x, 4, G.deepGrass); + for (y = 0; y <= 4; y++) { put(m.ground, 9, y, G.dirt); put(m.ground, 10, y, G.dirt); } + // 远端(南侧)雾墙铺满,进 obstacles(E3 入口) + for (y = 5; y <= 7; y++) + for (x = 0; x <= 19; x++) + put(m.obs, x, y, [G.fogA, G.fogB, G.fogC][(x + y) % 3]); + // 两侧树木 + tree2x2(m, 1, 1); tree2x2(m, 17, 1); + put(m.obs, 4, 3, G.bush); put(m.obs, 15, 3, G.bush); + put(m.obs, 0, 3, G.bush); put(m.obs, 19, 3, G.bush); + put(m.obs, 6, 2, G.rock); + spawn(m, 10, 2, 'default'); + spawn(m, 9, 2, 'from_plaza'); // 落点距门 >24px,防回弹 + door(m, 10, 0, 'plaza', 'from_fog'); + inter(m, 10, 4, 'fog_wall'); // 雾前可达格 + return m; +} + +// ================= 10. white 20×14 白色层(走廊 + 病房) ================= +function buildWhite() { + var m = newMap(20, 14); + var x, y; + // 默认白墙铺底;上半 20×6 走廊 + 左下 10×8 病房刷白地板 + fill(m.ground, 0, 0, 20, 14, G.wWall); + for (y = 1; y <= 5; y++) + for (x = 1; x <= 18; x++) + m.ground[y][x] = (x % 4 === 0 && y % 2 === 0) ? G.wFloor2 : G.wFloor; + for (y = 7; y <= 12; y++) + for (x = 1; x <= 8; x++) + m.ground[y][x] = ((x + y) % 3 === 0) ? G.wFloor2 : G.wFloor; + put(m.ground, 4, 6, G.wFloor); put(m.ground, 5, 6, G.wFloor); // 走廊↔病房通道格 + // 墙体(obstacles):北墙(含墙板/天花板灯)、东西墙、南墙、右下实心墙块、病房北/东墙 + for (x = 0; x <= 19; x++) put(m.obs, x, 0, G.wWall); + put(m.obs, 5, 0, G.wPanel); put(m.obs, 10, 0, G.wPanel); put(m.obs, 15, 0, G.wPanel); + put(m.obs, 3, 0, G.wLamp); put(m.obs, 8, 0, G.wLamp); + put(m.obs, 13, 0, G.wLamp); put(m.obs, 18, 0, G.wLamp); + for (y = 1; y <= 13; y++) { put(m.obs, 0, y, G.wWall); put(m.obs, 19, y, G.wWall); } + for (x = 1; x <= 9; x++) put(m.obs, x, 13, G.wWall); + fill(m.obs, 10, 6, 10, 8, G.wWall); // 右下 10×8 实心(房间之外) + for (x = 1; x <= 9; x++) if (x !== 4 && x !== 5) put(m.obs, x, 6, G.wWall); + for (y = 6; y <= 13; y++) put(m.obs, 9, y, G.wWall); + put(m.obs, 9, 8, G.wCur); put(m.obs, 9, 9, G.wCur); // 白窗帘挂在东墙上 + // 病房家具:白床 + 白监护仪 + 白柜 + put(m.obs, 2, 8, G.wBedTL); put(m.obs, 3, 8, G.wBedTR); + put(m.obs, 2, 9, G.wBedBL); put(m.obs, 3, 9, G.wBedBR); + put(m.obs, 4, 8, G.wMon); + put(m.obs, 7, 12, G.wCab); + spawn(m, 17, 3, 'default'); // 走廊一端 + return m; +} + +// ================= 序列化 ================= +function csv(l, w, h) { + var s = ''; + for (var y = 0; y < h; y++) { + var row = ''; + for (var x = 0; x < w; x++) { + row += l[y][x]; + if (x < w - 1) row += ','; + } + s += row; + if (y < h - 1) s += ',\n'; + } + return s; +} +function tmx(m) { + var s = '\n'; + s += '\n'; + s += ' \n'; + s += ' \n'; + s += ' \n' + csv(m.ground, m.w, m.h) + '\n \n \n'; + s += ' \n'; + s += ' \n \n \n'; + s += ' \n' + csv(m.obs, m.w, m.h) + '\n \n \n'; + s += ' \n'; + for (var i = 0; i < m.objs.length; i++) { + var o = m.objs[i]; + s += ' \n'; + s += ' \n'; + for (var p = 0; p < o.props.length; p++) + s += ' \n'; + s += ' \n \n \n'; + } + s += ' \n\n'; + return s; +} +function writeMap(name, m) { + var f = new TextFile(OUT + name + '.tmx', TextFile.WriteOnly); + f.write(tmx(m)); + f.commit(); + tiled.log('wrote ' + name + '.tmx (' + m.w + 'x' + m.h + ', ' + m.objs.length + ' objects)'); +} + +writeMap('house', buildHouse()); +writeMap('plaza', buildPlaza()); +writeMap('bakery', buildBakery()); +writeMap('shop', buildShop()); +writeMap('clinic', buildClinic()); +writeMap('lake', buildLake()); +writeMap('field', buildField()); +writeMap('well', buildWell()); +writeMap('fog', buildFog()); +writeMap('white', buildWhite()); +tiled.log('all 10 maps done'); diff --git a/Kimi-K3/tsconfig.json b/Kimi-K3/tsconfig.json new file mode 100644 index 0000000..e87b4b8 --- /dev/null +++ b/Kimi-K3/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ES2020", "DOM"], + "types": [] + }, + "include": ["src"] +} diff --git a/Kimi-K3/vite.config.ts b/Kimi-K3/vite.config.ts new file mode 100644 index 0000000..08536af --- /dev/null +++ b/Kimi-K3/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + build: { + target: 'es2020', + outDir: 'dist', + assetsInlineLimit: 0, + }, + server: { + host: '127.0.0.1', + port: 5315, + }, + preview: { + host: '127.0.0.1', + port: 4815, + }, +}); diff --git a/Opus4-8-xhigh/Dockerfile b/Opus4-8-xhigh/Dockerfile new file mode 100644 index 0000000..b7fe45c --- /dev/null +++ b/Opus4-8-xhigh/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-alpine AS build +WORKDIR /app +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1 +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npx vite build +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html diff --git a/Opus4-8-xhigh/PRD.md b/Opus4-8-xhigh/PRD.md new file mode 100644 index 0000000..0f5ea47 --- /dev/null +++ b/Opus4-8-xhigh/PRD.md @@ -0,0 +1,226 @@ +# PRD:《崩坏童话:蜜糖村》网页版 + +> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局 + +## 1. 游戏理念与设计支柱 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。 + +四条设计支柱,所有实现争议回到这里裁决: + +1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。 +2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。 +3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。 +4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。 + +## 2. 世界观:三层结构 + +| 层 | 名称 | 玩家所见 | 实质 | +|---|---|---|---| +| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 | +| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" | +| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 | + +命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。 + +## 3. 主角设定 + +- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。 +- 性别中性像素小人,16×24 像素/帧,无内心独白。 +- 对话头像永远同一帧微笑(见支柱 2)。 +- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。 + +## 4. NPC 清单(封闭,7 组 + 群演) + +每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。 + +| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 | +|---|---|---|---|---|---| +| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 | +| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) | +| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 | +| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) | +| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 | +| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 | +| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 | +| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — | + +## 5. 七日故事线 + +每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。 + +| 日 | 主题 | 剧情要点 | 侵蚀预期 | +|---|---|---|---| +| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 | +| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 | +| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 | +| Day 4 | 完美的重复 | 任务 T8–T10 与 Day 1 的 T1–T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 | +| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 | +| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 | +| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 | + +阿福每日公告(逐字实现,注意措辞逐日滑移): + +- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」 +- D2「早上好呀!新的一天,新的幸福。祝你开心!」 +- D3「早上好!今天要把幸福装得满满的哦。要开心!」 +- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同) +- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」 +- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」 +- D7「庆典开始了。来吧。」 + +## 6. 核心系统 + +### 6.1 幸福值(明线) + +- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。 +- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。 +- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。 +- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。 +- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。 + +### 6.2 觉知值(暗线,永不显示) + +- 范围 0–100,开局 0。只升不降。 +- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。 + +### 6.3 悲伤行为(封闭清单,5 项) + +拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。 + +### 6.4 侵蚀阶段矩阵(唯一真源) + +阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。 + +| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5) | UI | +|---|---|---|---|---|---|---| +| S0 | 0–20 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 0–2 瓣 | +| S1 | 21–50 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 3–6 瓣 | +| S2 | 51–80 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 | +| S3 | 81–99 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 | +| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 | + +NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。 + +### 6.5 文本腐蚀 + +同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。 + +### 6.6 BGM 与音效 + +- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。 +- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。 +- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。 +- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。 + +## 7. 任务清单(封闭,15 个) + +| ID | 日 | 任务 | 幸福奖励 | 备注 | +|---|---|---|---|---| +| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 | +| T2 | D1 | 给广场花坛浇水 ×3 | +10 | | +| T3 | D1 | 与任意三位村民问好 | +10 | | +| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) | +| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) | +| T6 | D3 | 去花田采莓果 ×5 | +10 | | +| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 | +| T8–T10 | D4 | 与 T1–T3 逐字相同 | 同上 | 刻意恐怖谷 | +| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | | +| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) | +| T13 | D6 | 在广场挂彩带 ×4 | +10 | | +| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 | +| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 | + +## 8. 线索清单(封闭,9 处,每处觉知 +8) + +| ID | 位置/来源 | 内容 | 可发现时段 | +|---|---|---|---| +| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 | +| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 | +| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 | +| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 | +| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 | +| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5(T12 中) | +| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 | +| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 | +| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 | + +## 9. 结局(封闭,4 个) + +Day 7 庆典按以下优先级判定(自上而下第一个满足者生效): + +| 结局 | 条件 | 演出 | +|---|---|---| +| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 | +| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 | +| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 | +| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 | + +任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。 + +## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON) + +| 场景 | 尺寸(瓦片) | 要点 | +|---|---|---| +| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) | +| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 | +| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) | +| 杂货店(室内) | 15×10 | 货架(C5) | +| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) | +| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 | +| 花田 | 20×14 | 莓果丛(T6)、浇花点 | +| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 | +| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 | +| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 | + +统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。 + +## 11. 美术规范(Aseprite 制作,全部原创) + +- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。 +- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。 +- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。 +- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。 +- 禁止使用任何现成素材、素材包或开源 tileset。 + +## 12. 技术架构 + +- **Phaser 3 + TypeScript + Vite**,`npm run build` 产出纯静态 `dist/`,`npm run preview` 本地游玩。 +- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。 +- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`。 +- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。 +- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。 +- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。 +- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。 +- 无 localStorage、无外部网络请求、无外部音频/字体文件。 + +## 13. Debug 面板(服务验收,必须实现) + +按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。 + +## 14. 验收清单(共 20 条) + +1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。 +2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。 +3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。 +4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。 +5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。 +6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。 +7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。 +8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。 +9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。 +10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。 +11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。 +12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。 +13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。 +14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。 +15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。 +16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。 +17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。 +18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。 +19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。 +20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。 + +## 15. 明确不做 + +战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。 diff --git a/Opus4-8-xhigh/README.md b/Opus4-8-xhigh/README.md new file mode 100644 index 0000000..f96da97 --- /dev/null +++ b/Opus4-8-xhigh/README.md @@ -0,0 +1,122 @@ +# 崩坏童话:蜜糖村(网页版) + +> v2.0 · 纯单机网页像素游戏 · 无存档 / 无后端 / 无 localStorage · 浏览器打开即玩 · 完整流程约 25–35 分钟,含 4 个结局。 + +一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。 + +技术栈:**Phaser 3 + TypeScript + Vite**。逻辑分辨率 320×180,整数倍放大居中,`pixelArt`。全部像素素材与地图为本项目自制(见下文制作流程),无任何外部资产、外部音频或字体文件。 + +--- + +## 一、启动方式 + +```bash +npm install # 安装依赖(phaser / vite / typescript) +npm run build # 类型检查 + 产出纯静态 dist/ +npm run preview # 本地起服,浏览器打开提示的地址(默认 http://localhost:4173)游玩 +# 开发热重载: +npm run dev +``` + +打开页面 → 输入名字(留空默认「小满」)→ 点「开始疗程」或回车 → 进入 Day 1 小屋。 +音频在首次点击 / 按键后开始(浏览器自动播放策略)。 + +--- + +## 二、按键 + +| 操作 | 键 | +|---|---| +| 移动 | `WASD` 或 方向键(96 像素/秒,四向动画 + 待机帧) | +| 交互 / 推进对话 | `E`(靠近可交互对象 ≤24 像素出现「按 E」提示);对话也可鼠标点击推进 | +| 对话选项 | `W`/`S` 或 上/下 选择,`E` 确定(温柔 / 迟疑 / 否定三类,影响幸福与觉知) | +| 睡觉过日 | 走到床边按 `E` | +| Debug 面板 | 反引号键 `` ` `` 开关 | + +核心玩法:每日「晨起 → 公告板接任务 → 完成任务 → 黄昏钟声 → 回屋睡觉 → 梦境过场 → 次日」。共 7 天、15 个任务、9 处线索,Day 7 庆典按状态走向四结局之一。 + +HUD 只有左上角一朵**向日葵表盘**:12 片花瓣按幸福值点亮,满格后花心浮现过熟的黑籽——**全程不显示任何数字**。幸福越高,世界饱和度越高、BGM 越变调变闷、台词越出错、NPC 表情从微笑滑向崩坏。 + +--- + +## 三、Debug 面板(`` ` `` 开关,服务验收) + +默认关闭、无任何 UI 痕迹,不影响正常游玩。开启后(页面右上角)可: + +- **直接设置幸福值**:10 / 40 / 70 / 90 / 100 —— 对应侵蚀阶段 S0–S4,可肉眼/肉耳验证饱和度、晕影、NPC 表情档、文本腐蚀密度、BGM 变调与低通。 +- **直接设置觉知值**:0 / 40 / 70 / 100。 +- **设置当前日**:1–7(同步激活当日任务、线索时段门槛)。 +- **传送到任意场景**:home / plaza / bakery / grocery / clinic / lake / field / well / fog(白色层由结局/梦境使用)。 +- **置齐全部线索**:一次点亮 9 处线索。 +- **强制触发四个结局**:E1 糖衣 / E2 回收 / E3 苏醒 / E4 静默。 + +E3 病历会显示你输入的名字、编号 `PT-07` 与**真实游玩时长**(读运行计时),最后一帧主角头像的微笑消失、换成一张疲惫而真实的脸。 + +--- + +## 四、素材与地图制作流程(全部原创,无外部资产) + +一键重建全部素材与地图: + +```bash +npm run assets # = bash tools/build_assets.sh +``` + +该脚本按顺序执行三步(需本机已装 Aseprite 与 Tiled): + +### 1. 像素美术 —— Aseprite(`tools/gen_all.lua`) +用 Aseprite 命令行批处理 + Lua API **逐像素绘制**并导出 PNG(`public/assets/sprites/`): + +```bash +aseprite -b --script-param outdir=public/assets/sprites --script tools/gen_all.lua +``` + +产出(16×16 瓦片 / 16×24 角色,糖果色板 + 白色层独立灰白色板,无抗锯齿、无描边渐变): +- `tileset.png` —— 72 瓦片(草地/石板/湖水/花丛/莓果/树/房屋/室内家具/井/雾/白色层墙地/装饰,≥64)。 +- `player.png` —— 主角 16×24,4 朝向 × 4 帧行走(每朝向第 1 帧兼待机)。 +- `portraits.png` —— 主角对话头像(永恒微笑)+ E3 专用「真实的脸」。 +- `npc_*.png` ×6 —— 阿福/圆圆/大宝/咪咪/芙医生/龟爷爷,各 3 档表情(smile/flat/off),`off` 与 `smile` 仅差 1–2 像素(一只眼睛低 1 像素、嘴角多 1 像素),不画成鬼脸。 +- `birds.png` —— 双子鸟 2 帧张嘴;`extras.png` —— 群演 ×5 各 2 帧循环;`sparrow.png` —— E3 窗外的真麻雀。 + +预览用蒙太奇脚本:`tools/montage.lua`(放大拼图,仅供肉眼校对,不参与打包)。 + +### 2. 地图 —— Tiled(`tools/gen_maps.mjs` → `.tmx` → Tiled 导出 `.json`) +Node 脚本生成 10 张地图的 Tiled 源文件(`tools/tmx/*.tmx`,内嵌 tileset),再用 Tiled 命令行导出为 Phaser 可读 JSON(`public/assets/maps/`): + +```bash +node tools/gen_maps.mjs +tiled --export-map tools/tmx/plaza.tmx public/assets/maps/plaza.json # 每张地图一次 +``` + +统一三层结构:`ground` / `obstacles`(有瓦片即碰撞)/ `objects`(`spawn`、`door`〔含 target/dest〕、`npc`、`interact`〔含 kind〕)。所有室外场景经村广场互通,门与出入口在成对地图两侧对齐。10 张:小满小屋 12×8、村广场 30×20、面包房 15×10、杂货店 15×10、诊所 12×8、湖畔 24×16、花田 20×14、古井空地 14×10、雾墙边界 20×8、白色层 20×14(走廊+病房)。 + +### 3. 音频 —— Web Audio 程序合成(`src/core/Audio.ts`) +无任何音频文件。8 小节 8-bit 治愈循环(方波主旋律 + 三角波低音)渲染进 `AudioBuffer` 循环播放,`playbackRate` 与 `BiquadFilter` 低通由侵蚀阶段实时驱动(S2 起变慢,S3 加 8kHz 低通,S4 到 0.84 倍 + 4kHz 低通 + 鸟鸣声部静音而双子鸟仍张嘴)。另有 4 个程序合成音效:对话哔、任务完成叮、黄昏钟声、E2 心电滴声。Day 6「保持微笑 60 秒」任务 40–60 秒所有声音渐隐至零(突然的安静)。 + +--- + +## 五、代码结构 + +``` +src/ + main.ts Phaser 配置、整数缩放、音频阶段联动 + core/ + Director.ts CorruptionDirector 单例:幸福/觉知/日/任务/线索/侵蚀阶段的唯一真源 + 侵蚀矩阵 + 文本腐蚀 + Audio.ts 程序合成 BGM/SFX + quests.ts 公告、NPC 对话装配、15 任务与 9 线索逻辑、结局判定 + data/content.ts 全部逐字台词/数值/表格(公告、错字表、任务、线索、结局) + scenes/ + BootScene / TitleScene / WorldScene(通用地图场景)/ UIScene(表盘·对话·Debug·梦境·雨·欢乐庆典)/ EndingScene +tools/ + gen_all.lua Aseprite 绘制脚本 + gen_maps.mjs Tiled .tmx 生成脚本 + build_assets.sh 一键重建(Aseprite + Tiled) +``` + +`CorruptionDirector` 是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),S4 负片闪帧用 ColorMatrix negative 单帧切换。 + +--- + +## 六、明确不做(同 PRD §15) + +战斗、背包/合成 UI、存档读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。 diff --git a/Opus4-8-xhigh/index.html b/Opus4-8-xhigh/index.html new file mode 100644 index 0000000..a5e2ef8 --- /dev/null +++ b/Opus4-8-xhigh/index.html @@ -0,0 +1,34 @@ + + + + + + + 崩坏童话:蜜糖村 + + + +
+ + + diff --git a/Opus4-8-xhigh/package-lock.json b/Opus4-8-xhigh/package-lock.json new file mode 100644 index 0000000..540ffa2 --- /dev/null +++ b/Opus4-8-xhigh/package-lock.json @@ -0,0 +1,1059 @@ +{ + "name": "honey-village", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "honey-village", + "version": "2.0.0", + "dependencies": { + "phaser": "^3.90.0" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/phaser": { + "version": "3.90.0", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", + "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/Opus4-8-xhigh/package.json b/Opus4-8-xhigh/package.json new file mode 100644 index 0000000..debccd6 --- /dev/null +++ b/Opus4-8-xhigh/package.json @@ -0,0 +1,19 @@ +{ + "name": "honey-village", + "version": "2.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --port 4173", + "assets": "bash tools/build_assets.sh" + }, + "dependencies": { + "phaser": "^3.90.0" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} diff --git a/Opus4-8-xhigh/public/assets/maps/bakery.json b/Opus4-8-xhigh/public/assets/maps/bakery.json new file mode 100644 index 0000000..1420cca --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/bakery.json @@ -0,0 +1,153 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[25, 25, 25, 25, 25, 25, 25, 25, 39, 25, 25, 25, 25, 25, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 33, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 25, 25, 25, 25, 25, 25, 40, 25, 25, 25, 25, 25, 25, 25], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "data":[27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27, + 27, 0, 32, 32, 32, 32, 32, 0, 0, 0, 0, 34, 34, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 27, 27, 27, 27, 27, 27, 0, 27, 27, 27, 27, 27, 27, 27], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"door_plaza", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_bakery" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":112, + "y":144 + }, + { + "height":16, + "id":2, + "name":"sp_from_plaza", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":112, + "y":128 + }, + { + "height":16, + "id":3, + "name":"yuan", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"yuan" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":64, + "y":112 + }, + { + "height":16, + "id":4, + "name":"i_recipe", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"recipe" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":64, + "y":96 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":5, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":15 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/clinic.json b/Opus4-8-xhigh/public/assets/maps/clinic.json new file mode 100644 index 0000000..e4d4a62 --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/clinic.json @@ -0,0 +1,149 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[25, 25, 25, 25, 25, 25, 39, 25, 25, 25, 25, 25, + 25, 26, 38, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 25, 25, 25, 25, 25, 40, 25, 25, 25, 25, 25], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "data":[27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 27, + 27, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 37, 0, 0, 0, 62, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 27, 27, 27, 27, 27, 0, 27, 27, 27, 27, 27], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"door_plaza", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_clinic" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":96, + "y":112 + }, + { + "height":16, + "id":2, + "name":"sp_from_plaza", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":96, + "y":96 + }, + { + "height":16, + "id":3, + "name":"fu", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"fu" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":64, + "y":48 + }, + { + "height":16, + "id":4, + "name":"i_clinicform", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"clinicform" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":32, + "y":32 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":5, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/field.json b/Opus4-8-xhigh/public/assets/maps/field.json new file mode 100644 index 0000000..9875dba --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/field.json @@ -0,0 +1,276 @@ +{ "compressionlevel":-1, + "height":14, + "infinite":false, + "layers":[ + { + "data":[2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 40, 1, 65, 1, 1, 1, 1, 1, 2, 1, + 65, 1, 9, 1, 1, 1, 2, 1, 1, 65, 3, 1, 1, 1, 1, 2, 1, 1, 65, 1, + 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, + 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, + 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, + 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, + 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, + 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, + 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, + 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, + 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, + 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, + 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, + 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1], + "height":14, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 51, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14], + "height":14, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"i_berry0", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"berry" + }, + { + "name":"n", + "type":"string", + "value":"0" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":64, + "y":48 + }, + { + "height":16, + "id":2, + "name":"i_berry1", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"berry" + }, + { + "name":"n", + "type":"string", + "value":"1" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":128, + "y":64 + }, + { + "height":16, + "id":3, + "name":"i_berry2", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"berry" + }, + { + "name":"n", + "type":"string", + "value":"2" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":208, + "y":48 + }, + { + "height":16, + "id":4, + "name":"i_berry3", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"berry" + }, + { + "name":"n", + "type":"string", + "value":"3" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":96, + "y":128 + }, + { + "height":16, + "id":5, + "name":"i_berry4", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"berry" + }, + { + "name":"n", + "type":"string", + "value":"4" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":224, + "y":144 + }, + { + "height":16, + "id":6, + "name":"i_fieldbed", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"flowerbed" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":160, + "y":112 + }, + { + "height":16, + "id":7, + "name":"door_plaza", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_field" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":160, + "y":0 + }, + { + "height":16, + "id":8, + "name":"sp_from_plaza", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":160, + "y":32 + }, + { + "height":16, + "id":9, + "name":"extra5", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"extra5" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":240, + "y":112 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":10, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/fog.json b/Opus4-8-xhigh/public/assets/maps/fog.json new file mode 100644 index 0000000..3ced613 --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/fog.json @@ -0,0 +1,131 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, + 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, + 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, + 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 1, 1, 2, 1, + 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 40, 1, 1, 1, 2, 1, 1, 1, 1, 1], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"door_plaza", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_fog" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":160, + "y":112 + }, + { + "height":16, + "id":2, + "name":"sp_from_plaza", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":160, + "y":80 + }, + { + "height":16, + "id":3, + "name":"i_fog", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"fog" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":160, + "y":48 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":4, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/grocery.json b/Opus4-8-xhigh/public/assets/maps/grocery.json new file mode 100644 index 0000000..39ea4b6 --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/grocery.json @@ -0,0 +1,153 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 39, 25, 25, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, + 25, 25, 25, 25, 25, 25, 25, 40, 25, 25, 25, 25, 25, 25, 25], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "data":[27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27, + 27, 0, 35, 35, 35, 35, 35, 35, 35, 35, 35, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 58, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 27, 27, 27, 27, 27, 27, 0, 27, 27, 27, 27, 27, 27, 27], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":15, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"door_plaza", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_grocery" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":112, + "y":144 + }, + { + "height":16, + "id":2, + "name":"sp_from_plaza", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":112, + "y":128 + }, + { + "height":16, + "id":3, + "name":"mimi", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"mimi" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":64, + "y":112 + }, + { + "height":16, + "id":4, + "name":"i_shelf", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"shelf" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":96, + "y":32 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":5, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":15 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/home.json b/Opus4-8-xhigh/public/assets/maps/home.json new file mode 100644 index 0000000..aa57327 --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/home.json @@ -0,0 +1,161 @@ +{ "compressionlevel":-1, + "height":8, + "infinite":false, + "layers":[ + { + "data":[25, 25, 30, 25, 39, 25, 25, 39, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 63, 63, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 40, 25, 25, 25, 25, 25], + "height":8, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "data":[27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27, + 27, 29, 0, 0, 0, 0, 0, 0, 62, 59, 0, 27, + 27, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 36, 37, 0, 27, + 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, + 27, 27, 27, 27, 27, 27, 0, 27, 27, 27, 27, 27], + "height":8, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":12, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"door_plaza", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_home" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":96, + "y":112 + }, + { + "height":16, + "id":2, + "name":"start", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":48, + "y":48 + }, + { + "height":16, + "id":3, + "name":"sp_from_plaza", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":96, + "y":96 + }, + { + "height":16, + "id":4, + "name":"i_bed", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"bed" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":32, + "y":32 + }, + { + "height":16, + "id":5, + "name":"i_calendar", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"calendar" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":32, + "y":16 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":6, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":12 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/lake.json b/Opus4-8-xhigh/public/assets/maps/lake.json new file mode 100644 index 0000000..1cbb119 --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/lake.json @@ -0,0 +1,183 @@ +{ "compressionlevel":-1, + "height":16, + "infinite":false, + "layers":[ + { + "data":[2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 40, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 9, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, + 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 9, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, + 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, + 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, + 1, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 1, + 1, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 1, + 1, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 1, + 1, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 1, + 2, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 1, + 1, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 1, + 1, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 1], + "height":16, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":24, + "x":0, + "y":0 + }, + { + "data":[14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 5, 5, 55, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 55, 5, 5, 14, + 14, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 54, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14, + 14, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14, + 14, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 14, + 14, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 14, + 14, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14], + "height":16, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":24, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"door_plaza", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_lake" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":192, + "y":0 + }, + { + "height":16, + "id":2, + "name":"sp_from_plaza", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":192, + "y":32 + }, + { + "height":16, + "id":3, + "name":"gui", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"gui" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":80, + "y":112 + }, + { + "height":16, + "id":4, + "name":"extra4", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"extra4" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":240, + "y":48 + }, + { + "height":16, + "id":5, + "name":"i_picnic", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"picnic" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":256, + "y":96 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":6, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":24 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/plaza.json b/Opus4-8-xhigh/public/assets/maps/plaza.json new file mode 100644 index 0000000..574d845 --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/plaza.json @@ -0,0 +1,673 @@ +{ "compressionlevel":-1, + "height":20, + "infinite":false, + "layers":[ + { + "data":[2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 18, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, + 1, 1, 1, 13, 1, 1, 1, 1, 1, 13, 1, 1, 1, 1, 1, 1, 13, 1, 1, 1, 2, 1, 1, 13, 9, 1, 1, 1, 1, 1, + 1, 2, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 2, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 9, 9, 9, 1, 1, 1, 1, + 1, 1, 1, 3, 2, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 3, 1, 1, 2, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, + 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 1, + 40, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 40, + 2, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, + 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 72, 72, 72, 72, 72, 72, 72, 72, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 72, 72, 72, 72, 72, 72, 72, 72, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 72, 72, 72, 72, 72, 72, 72, 72, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 72, 72, 72, 72, 72, 72, 72, 72, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 3, 1, 1, 1, 1, + 1, 1, 2, 1, 40, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 40, 1, 1, 1, 1], + "height":20, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "data":[14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 10, 12, 12, 12, 0, 0, 0, 12, 12, 12, 0, 0, 0, 0, 12, 12, 12, 0, 0, 0, 0, 12, 12, 12, 0, 0, 10, 10, 14, + 14, 0, 11, 12, 11, 0, 0, 0, 11, 12, 11, 0, 0, 0, 0, 11, 12, 11, 0, 0, 0, 0, 11, 12, 11, 0, 0, 0, 0, 14, + 14, 0, 11, 0, 11, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 48, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 14, + 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14], + "height":20, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"door_home", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_plaza" + }, + { + "name":"target", + "type":"string", + "value":"home" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":48, + "y":48 + }, + { + "height":16, + "id":2, + "name":"sp_from_home", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":48, + "y":80 + }, + { + "height":16, + "id":3, + "name":"door_bakery", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_plaza" + }, + { + "name":"target", + "type":"string", + "value":"bakery" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":144, + "y":48 + }, + { + "height":16, + "id":4, + "name":"sp_from_bakery", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":144, + "y":80 + }, + { + "height":16, + "id":5, + "name":"door_grocery", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_plaza" + }, + { + "name":"target", + "type":"string", + "value":"grocery" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":256, + "y":48 + }, + { + "height":16, + "id":6, + "name":"sp_from_grocery", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":256, + "y":80 + }, + { + "height":16, + "id":7, + "name":"door_clinic", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_plaza" + }, + { + "name":"target", + "type":"string", + "value":"clinic" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":368, + "y":48 + }, + { + "height":16, + "id":8, + "name":"sp_from_clinic", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":368, + "y":80 + }, + { + "height":16, + "id":9, + "name":"i_mailbox", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"mailbox" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":240, + "y":256 + }, + { + "height":16, + "id":10, + "name":"door_field", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_plaza" + }, + { + "name":"target", + "type":"string", + "value":"field" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":0, + "y":160 + }, + { + "height":16, + "id":11, + "name":"sp_from_field", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":32, + "y":160 + }, + { + "height":16, + "id":12, + "name":"door_lake", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_plaza" + }, + { + "name":"target", + "type":"string", + "value":"lake" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":464, + "y":160 + }, + { + "height":16, + "id":13, + "name":"sp_from_lake", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":432, + "y":160 + }, + { + "height":16, + "id":14, + "name":"door_well", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_plaza" + }, + { + "name":"target", + "type":"string", + "value":"well" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":64, + "y":304 + }, + { + "height":16, + "id":15, + "name":"sp_from_well", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":64, + "y":272 + }, + { + "height":16, + "id":16, + "name":"door_fog", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_plaza" + }, + { + "name":"target", + "type":"string", + "value":"fog" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":400, + "y":304 + }, + { + "height":16, + "id":17, + "name":"sp_from_fog", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":400, + "y":272 + }, + { + "height":16, + "id":18, + "name":"afu", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"afu" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":224, + "y":144 + }, + { + "height":16, + "id":19, + "name":"dabao", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"dabao" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":240, + "y":288 + }, + { + "height":16, + "id":20, + "name":"bird1", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"bird1" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":368, + "y":80 + }, + { + "height":16, + "id":21, + "name":"bird2", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"bird2" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":400, + "y":80 + }, + { + "height":16, + "id":22, + "name":"extra1", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"extra1" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":160, + "y":240 + }, + { + "height":16, + "id":23, + "name":"extra2", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"extra2" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":304, + "y":256 + }, + { + "height":16, + "id":24, + "name":"extra3", + "opacity":1, + "properties":[ + { + "name":"npc", + "type":"string", + "value":"extra3" + }], + "rotation":0, + "type":"npc", + "visible":true, + "width":16, + "x":96, + "y":96 + }, + { + "height":16, + "id":25, + "name":"i_board", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"board" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":224, + "y":144 + }, + { + "height":16, + "id":26, + "name":"i_flowerbed", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"flowerbed" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":96, + "y":224 + }, + { + "height":16, + "id":27, + "name":"i_festival", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"festival" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":224, + "y":224 + }, + { + "height":16, + "id":28, + "name":"i_ribbon0", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"ribbon" + }, + { + "name":"n", + "type":"string", + "value":"0" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":192, + "y":208 + }, + { + "height":16, + "id":29, + "name":"i_ribbon1", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"ribbon" + }, + { + "name":"n", + "type":"string", + "value":"1" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":224, + "y":208 + }, + { + "height":16, + "id":30, + "name":"i_ribbon2", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"ribbon" + }, + { + "name":"n", + "type":"string", + "value":"2" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":256, + "y":208 + }, + + { + "height":16, + "id":31, + "name":"i_ribbon3", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"ribbon" + }, + { + "name":"n", + "type":"string", + "value":"3" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":288, + "y":208 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":32, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":30 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/well.json b/Opus4-8-xhigh/public/assets/maps/well.json new file mode 100644 index 0000000..8c519a8 --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/well.json @@ -0,0 +1,135 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[2, 1, 1, 1, 1, 2, 1, 40, 1, 1, 2, 1, 1, 1, + 1, 1, 1, 1, 2, 1, 1, 3, 1, 2, 1, 1, 1, 1, + 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, + 1, 1, 2, 1, 1, 24, 24, 24, 24, 1, 1, 1, 2, 1, + 1, 2, 1, 1, 1, 24, 24, 24, 24, 1, 1, 2, 1, 1, + 2, 1, 1, 1, 1, 24, 24, 24, 24, 1, 2, 1, 1, 1, + 1, 1, 1, 1, 2, 24, 24, 24, 24, 2, 1, 1, 1, 1, + 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, + 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, + 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1], + "height":10, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":14, + "x":0, + "y":0 + }, + { + "data":[14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 23, 0, 14, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14], + "height":10, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":14, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"door_plaza", + "opacity":1, + "properties":[ + { + "name":"dest", + "type":"string", + "value":"sp_from_well" + }, + { + "name":"target", + "type":"string", + "value":"plaza" + }], + "rotation":0, + "type":"door", + "visible":true, + "width":16, + "x":112, + "y":0 + }, + { + "height":16, + "id":2, + "name":"sp_from_plaza", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":112, + "y":32 + }, + { + "height":16, + "id":3, + "name":"i_well", + "opacity":1, + "properties":[ + { + "name":"kind", + "type":"string", + "value":"well" + }], + "rotation":0, + "type":"interact", + "visible":true, + "width":16, + "x":112, + "y":80 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":4, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":14 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/maps/white.json b/Opus4-8-xhigh/public/assets/maps/white.json new file mode 100644 index 0000000..5cdd270 --- /dev/null +++ b/Opus4-8-xhigh/public/assets/maps/white.json @@ -0,0 +1,114 @@ +{ "compressionlevel":-1, + "height":14, + "infinite":false, + "layers":[ + { + "data":[43, 43, 43, 43, 46, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 46, 43, 43, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 45, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42], + "height":14, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 0, 41, 41, 41, 41, 41, 41, 41, 41, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 0, 0, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], + "height":14, + "id":2, + "name":"obstacles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"sp_corridor", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":160, + "y":32 + }, + { + "height":16, + "id":2, + "name":"sp_ward", + "opacity":1, + "rotation":0, + "type":"spawn", + "visible":true, + "width":16, + "x":160, + "y":176 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":3, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.12.2", + "tileheight":16, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"..\/sprites\/tileset.png", + "imageheight":144, + "imagewidth":128, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":72, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":"1.10", + "width":20 +} \ No newline at end of file diff --git a/Opus4-8-xhigh/public/assets/sprites/birds.png b/Opus4-8-xhigh/public/assets/sprites/birds.png new file mode 100644 index 0000000..b5d05da Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/birds.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/extras.png b/Opus4-8-xhigh/public/assets/sprites/extras.png new file mode 100644 index 0000000..f9fc7ca Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/extras.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/npc_afu.png b/Opus4-8-xhigh/public/assets/sprites/npc_afu.png new file mode 100644 index 0000000..8a75521 Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/npc_afu.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/npc_dabao.png b/Opus4-8-xhigh/public/assets/sprites/npc_dabao.png new file mode 100644 index 0000000..6dab753 Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/npc_dabao.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/npc_fu.png b/Opus4-8-xhigh/public/assets/sprites/npc_fu.png new file mode 100644 index 0000000..54b2b44 Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/npc_fu.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/npc_gui.png b/Opus4-8-xhigh/public/assets/sprites/npc_gui.png new file mode 100644 index 0000000..c6f7524 Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/npc_gui.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/npc_mimi.png b/Opus4-8-xhigh/public/assets/sprites/npc_mimi.png new file mode 100644 index 0000000..646e88b Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/npc_mimi.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/npc_yuan.png b/Opus4-8-xhigh/public/assets/sprites/npc_yuan.png new file mode 100644 index 0000000..4ae0327 Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/npc_yuan.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/player.png b/Opus4-8-xhigh/public/assets/sprites/player.png new file mode 100644 index 0000000..321a98a Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/player.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/portraits.png b/Opus4-8-xhigh/public/assets/sprites/portraits.png new file mode 100644 index 0000000..f3d1b81 Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/portraits.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/sparrow.png b/Opus4-8-xhigh/public/assets/sprites/sparrow.png new file mode 100644 index 0000000..bd8146f Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/sparrow.png differ diff --git a/Opus4-8-xhigh/public/assets/sprites/tileset.png b/Opus4-8-xhigh/public/assets/sprites/tileset.png new file mode 100644 index 0000000..e22d066 Binary files /dev/null and b/Opus4-8-xhigh/public/assets/sprites/tileset.png differ diff --git a/Opus4-8-xhigh/src/core/Audio.ts b/Opus4-8-xhigh/src/core/Audio.ts new file mode 100644 index 0000000..cf93772 --- /dev/null +++ b/Opus4-8-xhigh/src/core/Audio.ts @@ -0,0 +1,131 @@ +import type { StageParams } from './Director'; + +// §6.6 程序合成:8 小节 8-bit 治愈循环(方波主旋律 + 三角波低音)+ 鸟鸣声部 + 4 音效。无外部音频文件。 +const BPM = 100; +const BEAT = 60 / BPM; // 0.6s +const LOOP_BEATS = 32; // 8 小节 × 4 拍 +const LOOP_SEC = LOOP_BEATS * BEAT; // 19.2s +const SR = 44100; + +const midi = (n: number) => 440 * Math.pow(2, (n - 69) / 12); +// 主旋律(32 个四分音符,C 大调,明亮) +const LEAD = [72, 76, 79, 76, 77, 76, 74, 72, 74, 76, 77, 79, 76, 74, 72, 74, + 79, 81, 79, 77, 76, 77, 79, 76, 72, 74, 76, 77, 79, 76, 72, 72].map(midi); +// 低音(每小节 2 个半音符,I–vi–IV–V) +const BASS_ROOTS = [48, 45, 41, 43, 48, 45, 41, 43].map(midi); + +function square(ph: number) { return Math.sin(ph) >= 0 ? 1 : -1; } +function triangle(ph: number) { return (2 / Math.PI) * Math.asin(Math.sin(ph)); } + +function renderMusic(): AudioBuffer { + const buf = new AudioBuffer({ length: Math.floor(LOOP_SEC * SR), sampleRate: SR, numberOfChannels: 1 }); + const d = buf.getChannelData(0); + const addNote = (freq: number, start: number, dur: number, amp: number, wave: (p: number) => number) => { + const s0 = Math.floor(start * SR), s1 = Math.floor((start + dur) * SR); + for (let i = s0; i < s1 && i < d.length; i++) { + const t = (i - s0) / SR; + // 简单 AD 包络 + const env = Math.min(1, t / 0.01) * Math.exp(-t * 3.2); + d[i] += amp * env * wave(2 * Math.PI * freq * (i / SR)); + } + }; + // lead: 四分音符 + for (let i = 0; i < LEAD.length; i++) addNote(LEAD[i], i * BEAT, BEAT * 0.9, 0.12, square); + // bass: 每小节两个半音符 + for (let b = 0; b < 8; b++) { + const root = BASS_ROOTS[b]; + addNote(root, b * 4 * BEAT, BEAT * 2 * 0.95, 0.11, triangle); + addNote(root, b * 4 * BEAT + 2 * BEAT, BEAT * 2 * 0.95, 0.11, triangle); + } + return buf; +} + +function renderBirds(): AudioBuffer { + const buf = new AudioBuffer({ length: Math.floor(LOOP_SEC * SR), sampleRate: SR, numberOfChannels: 1 }); + const d = buf.getChannelData(0); + const chirp = (start: number) => { + const dur = 0.09; + const s0 = Math.floor(start * SR), s1 = Math.floor((start + dur) * SR); + for (let i = s0; i < s1 && i < d.length; i++) { + const t = (i - s0) / SR; + const f = 2400 + 900 * t / dur; // 上滑 + const env = Math.min(1, t / 0.005) * Math.exp(-t * 22); + d[i] += 0.07 * env * Math.sin(2 * Math.PI * f * t); + } + }; + // 每小节两声鸟鸣 + for (let b = 0; b < 8; b++) { chirp(b * 4 * BEAT + 0.5); chirp(b * 4 * BEAT + 2.3); } + return buf; +} + +class AudioEngine { + ctx: AudioContext | null = null; + private master!: GainNode; + private musicGain!: GainNode; + private birdGain!: GainNode; + private filter!: BiquadFilterNode; + private musicSrc: AudioBufferSourceNode | null = null; + private birdSrc: AudioBufferSourceNode | null = null; + private musicBuf!: AudioBuffer; + private birdBuf!: AudioBuffer; + private started = false; + private baseMaster = 0.5; + private birdLevel = 1.0; + + start() { + if (this.started) { this.ctx?.resume(); return; } + this.ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); + const ctx = this.ctx; + this.master = ctx.createGain(); this.master.gain.value = this.baseMaster; this.master.connect(ctx.destination); + this.filter = ctx.createBiquadFilter(); this.filter.type = 'lowpass'; this.filter.frequency.value = 20000; this.filter.connect(this.master); + this.musicGain = ctx.createGain(); this.musicGain.gain.value = 1; this.musicGain.connect(this.filter); + this.birdGain = ctx.createGain(); this.birdGain.gain.value = this.birdLevel; this.birdGain.connect(this.filter); + this.musicBuf = renderMusic(); this.birdBuf = renderBirds(); + this._spawnSources(); + this.started = true; + } + + private _spawnSources() { + if (!this.ctx) return; + this.musicSrc = this.ctx.createBufferSource(); this.musicSrc.buffer = this.musicBuf; this.musicSrc.loop = true; this.musicSrc.connect(this.musicGain); this.musicSrc.start(); + this.birdSrc = this.ctx.createBufferSource(); this.birdSrc.buffer = this.birdBuf; this.birdSrc.loop = true; this.birdSrc.connect(this.birdGain); this.birdSrc.start(); + } + + setStage(p: StageParams) { + if (!this.ctx) return; + const now = this.ctx.currentTime; + this.musicSrc?.playbackRate.setTargetAtTime(p.playbackRate, now, 0.4); + this.birdSrc?.playbackRate.setTargetAtTime(p.playbackRate, now, 0.4); + this.filter.frequency.setTargetAtTime(p.lowpass > 0 ? p.lowpass : 20000, now, 0.4); + this.birdGain.gain.setTargetAtTime(p.birdMute ? 0 : this.birdLevel, now, 0.4); + } + + /** T14「突然的安静」:淡入淡出主音量 */ + fadeMaster(level: number, seconds: number) { + if (!this.ctx) return; + const now = this.ctx.currentTime; + this.master.gain.cancelScheduledValues(now); + this.master.gain.setValueAtTime(this.master.gain.value, now); + this.master.gain.linearRampToValueAtTime(level, now + seconds); + } + restoreMaster(seconds = 1) { this.fadeMaster(this.baseMaster, seconds); } + + // ---- SFX(全部程序合成)---- + private blip(freq: number, dur: number, type: OscillatorType, amp = 0.3, slideTo?: number) { + if (!this.ctx) return; + const ctx = this.ctx, now = ctx.currentTime; + const o = ctx.createOscillator(), g = ctx.createGain(); + o.type = type; o.frequency.setValueAtTime(freq, now); + if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, now + dur); + g.gain.setValueAtTime(0.0001, now); + g.gain.exponentialRampToValueAtTime(amp, now + 0.008); + g.gain.exponentialRampToValueAtTime(0.0001, now + dur); + o.connect(g); g.connect(this.master); o.start(now); o.stop(now + dur + 0.02); + } + sfxBeep() { this.blip(620, 0.05, 'square', 0.12); } // 对话哔哔 + sfxDing() { this.blip(880, 0.12, 'square', 0.22); setTimeout(() => this.blip(1320, 0.18, 'square', 0.2), 90); } // 任务完成叮 + sfxBell() { this.blip(392, 1.6, 'triangle', 0.3); setTimeout(() => this.blip(261, 2.0, 'triangle', 0.25), 120); } // 黄昏钟声 + sfxEcg() { this.blip(1000, 0.15, 'sine', 0.35); } // E2 心电监护滴声 +} + +export const Audio = new AudioEngine(); diff --git a/Opus4-8-xhigh/src/core/Director.ts b/Opus4-8-xhigh/src/core/Director.ts new file mode 100644 index 0000000..4e9d9bb --- /dev/null +++ b/Opus4-8-xhigh/src/core/Director.ts @@ -0,0 +1,195 @@ +import Phaser from 'phaser'; +import { NPCS, TYPO } from '../data/content'; + +// Corruption stage params (§6.4 唯一真源) +export interface StageParams { + stage: number; + saturation: number; // multiplier applied to ColorMatrix + vignette: number; // 0..1 strength + playbackRate: number; + lowpass: number; // Hz, 0 = off (open) + birdMute: boolean; + negativePerMin: number; // random single-frame negative + offFlashPerMin: number; // NPC off-expression flashes +} + +const MATRIX: StageParams[] = [ + { stage: 0, saturation: 0.90, vignette: 0.0, playbackRate: 1.00, lowpass: 0, birdMute: false, negativePerMin: 0, offFlashPerMin: 0 }, + { stage: 1, saturation: 1.00, vignette: 0.0, playbackRate: 1.00, lowpass: 0, birdMute: false, negativePerMin: 0, offFlashPerMin: 0 }, + { stage: 2, saturation: 1.15, vignette: 0.0, playbackRate: 0.97, lowpass: 0, birdMute: false, negativePerMin: 0, offFlashPerMin: 0 }, + { stage: 3, saturation: 1.30, vignette: 0.30, playbackRate: 0.92, lowpass: 8000, birdMute: false, negativePerMin: 0, offFlashPerMin: 0 }, + { stage: 4, saturation: 1.40, vignette: 0.55, playbackRate: 0.84, lowpass: 4000, birdMute: true, negativePerMin: 0.4, offFlashPerMin: 0.6 }, +]; + +export function stageFromHappiness(h: number): number { + if (h <= 20) return 0; + if (h <= 50) return 1; + if (h <= 80) return 2; + if (h <= 99) return 3; + return 4; +} + +/** CorruptionDirector — single source of truth for all state + AV params (§12). */ +class DirectorClass extends Phaser.Events.EventEmitter { + playerName = '小满'; + day = 1; + happiness = 30; + awareness = 0; + clues = new Set(); + tasks: Record = {}; + flags: Record = {}; + escapeCount = 0; + overflowPool = 0; + startTime = 0; + // daily + greetedToday = new Set(); + talkCounts: Record = {}; + smallInteract: Record = {}; // water/feed/pat -> count (max 3) + gazedToday = new Set(); // well/fog + refusedTasks = new Set(); + lowHappySince = 0; // timestamp when happiness dropped below 30 (0 = not low) + endingShown = false; + + reset(name: string) { + this.playerName = name && name.trim() ? name.trim() : '小满'; + this.day = 1; + this.happiness = 30; + this.awareness = 0; + this.clues = new Set(); + this.tasks = {}; + this.flags = {}; + this.escapeCount = 0; + this.overflowPool = 0; + this.startTime = performance.now(); + this.greetedToday = new Set(); + this.talkCounts = {}; + this.smallInteract = {}; + this.gazedToday = new Set(); + this.refusedTasks = new Set(); + this.lowHappySince = 0; + this.endingShown = false; + this.emit('change'); + this.emit('stage', this.stage); + } + + resetDaily() { + this.greetedToday = new Set(); + this.talkCounts = {}; + this.smallInteract = {}; + this.gazedToday = new Set(); + } + + get stage(): number { return stageFromHappiness(this.happiness); } + get params(): StageParams { return MATRIX[this.stage]; } + + /** petals lit 0..12 (§6.1 向日葵表盘) */ + get petals(): number { return Phaser.Math.Clamp(Math.round((this.happiness / 100) * 12), 0, 12); } + get blackSeeds(): boolean { return this.happiness >= 100; } + + private _clamp() { + this.happiness = Phaser.Math.Clamp(this.happiness, 0, 100); + this.awareness = Phaser.Math.Clamp(this.awareness, 0, 100); + } + + addHappiness(n: number) { + const prevStage = this.stage; + if (this.happiness >= 100 && n < 0) { + // 满格强制维持:小额扣分累积进溢出池 (§6.1) + this.overflowPool += -n; + if (this.overflowPool >= 15) { this.overflowPool -= 15; this.emit('negativeFlash'); } + } else { + this.happiness += n; + this._clamp(); + } + this._afterHappyChange(prevStage); + } + + setHappiness(v: number) { + const prevStage = this.stage; + this.happiness = Phaser.Math.Clamp(v, 0, 100); + this._afterHappyChange(prevStage); + } + + private _afterHappyChange(prevStage: number) { + this._clamp(); + if (this.happiness < 30) { if (!this.lowHappySince) this.lowHappySince = performance.now(); } + else this.lowHappySince = 0; + this.emit('change'); + if (this.stage !== prevStage) this.emit('stage', this.stage); + } + + addAwareness(n: number) { this.awareness += n; this._clamp(); this.emit('change'); } + setAwareness(v: number) { this.awareness = Phaser.Math.Clamp(v, 0, 100); this.emit('change'); } + + discoverClue(id: string): boolean { + if (this.clues.has(id)) return false; + this.clues.add(id); + this.addAwareness(8); // §8 每处 +8 + this.emit('clue', id); + return true; + } + + /** sad behavior (§6.3): 幸福 −5, 觉知 +3, 最近 NPC 转头 */ + sadBehavior(reason: string) { + this.addHappiness(-5); + this.addAwareness(3); + this.emit('sad', reason); + } + + /** NPC effective stage with individual offset (§6.4) */ + npcStage(npcId: string): number { + const npc = NPCS[npcId]; + if (!npc || npc.participates === false) return 0; + const eff = Phaser.Math.Clamp(this.happiness - npc.offset, 0, 100); + return stageFromHappiness(eff); + } + + /** expression for an NPC given current state (§6.4). off flashes handled by caller. */ + npcExpression(npcId: string): 'smile' | 'flat' | 'off' { + const s = this.npcStage(npcId); + if (s <= 2) return 'smile'; + return 'flat'; + } + + playtimeString(): string { + const ms = performance.now() - this.startTime; + const total = Math.floor(ms / 1000); + const mm = Math.floor(total / 60).toString().padStart(2, '0'); + const ss = (total % 60).toString().padStart(2, '0'); + return `${mm}:${ss}`; + } + + // ---- text corruption (§6.5) applied at render time ---- + private lineCounter = 0; + corrupt(text: string, speaker: string): string { + this.lineCounter++; + const s = this.stage; + const isMimi = speaker === '咪咪'; + let out = text; + if (s >= 3) { + // homophone typo. Mimi always; others at density (~1 per 50 lines) unless S4. + const applyTypo = isMimi || s >= 4 || (this.lineCounter % 50 === 0); + if (applyTypo) { + const limit = s >= 4 ? TYPO.length : (isMimi ? TYPO.length : 1); + let done = 0; + for (const [from, to] of TYPO) { + if (done >= limit) break; + if (out.includes(from)) { out = out.split(from).join(to); done++; } + } + } + } + if (s >= 2) { + // 偶发标点重复 (S2 每80句1次); S4 咪咪句尾标点 x3 + if (s >= 4 && isMimi) { + out = out.replace(/([。!?,])\s*$/g, (m, p) => p + p + p); + out = out.replace(/([。!?])/g, (m, p) => p + p + p); + } else if (this.lineCounter % 80 === 0) { + out = out.replace(/([。!?])/, (m, p) => p + p); + } + } + return out; + } +} + +export const Director = new DirectorClass(); +export type { DirectorClass }; diff --git a/Opus4-8-xhigh/src/core/quests.ts b/Opus4-8-xhigh/src/core/quests.ts new file mode 100644 index 0000000..29c80f5 --- /dev/null +++ b/Opus4-8-xhigh/src/core/quests.ts @@ -0,0 +1,252 @@ +import { Director } from './Director'; +import { Audio } from './Audio'; +import { ANNOUNCE, CLUE_TEXT, DAY_TASKS, GREET, NPCS, TASKS } from '../data/content'; + +export type DChoiceOpt = { label: string; tone: 'gentle' | 'hesitant' | 'deny'; effect?: () => void; then?: DNode[] }; +export type DLine = { speaker: string; text: string; npcId?: string; raw?: boolean }; +export type DChoice = { choice: DChoiceOpt[] }; +export type DNode = DLine | DChoice; + +function line(speaker: string, text: string, npcId?: string, raw = false): DLine { return { speaker, text, npcId, raw }; } +function narr(text: string): DLine { return { speaker: '', text }; } + +export function taskActive(id: string) { return Director.tasks[id] === 'active'; } +export function completeTask(id: string) { + if (Director.tasks[id] !== 'active') return; + Director.tasks[id] = 'done'; + Director.addHappiness(TASKS[id].reward); + Director.emit('toast', `任务完成 · ${TASKS[id].title} +${TASKS[id].reward}`); + Audio.sfxDing(); + checkDusk(); +} +export function refuseTask(id: string) { + if (Director.tasks[id] !== 'active') return; + Director.tasks[id] = 'refused'; + Director.refusedTasks.add(id); + Director.sadBehavior('拒绝任务'); + Director.emit('toast', '(拒绝被记录了。)'); + checkDusk(); +} +function checkDusk() { + const ts = DAY_TASKS[Director.day] || []; + if (ts.every((id) => Director.tasks[id] === 'done' || Director.tasks[id] === 'refused')) { + if (!Director.flags['dusk' + Director.day]) { Director.flags['dusk' + Director.day] = true; Director.emit('dusk'); } + } +} +export function activateDay(day: number) { + Director.day = day; + Director.resetDaily(); + for (const id of DAY_TASKS[day] || []) if (!Director.tasks[id] || Director.tasks[id] === 'idle') Director.tasks[id] = 'active'; + Director.emit('change'); +} + +const breadTask = () => (Director.day === 4 ? 'T8' : Director.day === 1 ? 'T1' : null); +const waterTask = () => (Director.day === 4 ? 'T9' : Director.day === 1 ? 'T2' : null); +const greetTask = () => (Director.day === 4 ? 'T10' : Director.day === 1 ? 'T3' : null); + +// ---------------- 公告板 ---------------- +export function boardScript(): DNode[] { + const s: DNode[] = []; + s.push(line('阿福', ANNOUNCE[Director.day], 'afu', true)); // 逐字,不腐蚀 + const ts = DAY_TASKS[Director.day] || []; + const pending = ts.filter((id) => Director.tasks[id] === 'active'); + if (pending.length) { + s.push(narr('今日幸福任务:')); + for (const id of pending) s.push(narr(`· ${TASKS[id].title}(+${TASKS[id].reward})`)); + } else if (Director.day < 7) { + s.push(narr('今天的任务都完成啦。')); + } + // Day5 擦公告板 (T12) —— 擦到一半浮现旧字迹 (C6) + if (Director.day === 5 && taskActive('T12')) { + s.push({ + choice: [ + { label: '(擦拭公告板)', tone: 'gentle', effect: () => { discover('C6'); completeTask('T12'); } , then: CLUE_TEXT.C6.map(narr) }, + { label: '(不想擦)', tone: 'deny', effect: () => refuseTask('T12') }, + ], + }); + } + return s; +} + +function discover(id: string) { if (Director.discoverClue(id)) Director.emit('toast', '(你注意到了什么……)'); } + +// ---------------- NPC 对话 ---------------- +export function talkScript(npcId: string): DNode[] { + const def = NPCS[npcId]; + const name = def?.name || npcId; + const s: DNode[] = []; + const first = !Director.greetedToday.has(npcId); + Director.greetedToday.add(npcId); + Director.talkCounts[npcId] = (Director.talkCounts[npcId] || 0) + 1; + + const g = GREET[npcId] || ['……']; + s.push(line(name, g[(Director.talkCounts[npcId] - 1) % g.length], npcId)); + + // ---- 任务钩子 ---- + const bt = breadTask(); + const lettersActive = taskActive('T4'); + if (npcId === 'yuan') { + if (bt && taskActive(bt)) { + const f = flag('bread' + Director.day, { taken: false, del: [] as string[] }); + if (!f.taken) { f.taken = true; s.push(line('圆圆', '给,热乎的蜂蜜面包。送给大宝、咪咪和龟爷爷吧。', 'yuan')); } + } + if (lettersActive && letterCarry()) deliverLetter(s, 'yuan', '圆圆'); + if (Director.day === 5 && taskActive('T11')) { + s.push({ + choice: [ + { label: '好呀,我来帮你揉面。', tone: 'gentle', effect: () => completeTask('T11'), then: [line('圆圆', '谢谢你,面团都笑了呢。', 'yuan')] }, + { label: '今天……我想歇一歇。', tone: 'deny', effect: () => refuseTask('T11'), then: [line('圆圆', '没关系的,休息也很好。', 'yuan')] }, + ], + }); + } + } else if (npcId === 'dabao') { + if (bt && taskActive(bt)) deliverBread(s, 'dabao', '大宝'); + if (lettersActive) { + const f = flag('letters', { taken: false, del: [] as string[] }); + if (!f.taken) { f.taken = true; s.push(line('大宝', '这三封信,麻烦你送给圆圆、咪咪和芙医生啦!', 'dabao')); } + } + if (Director.day >= 3) s.push(line('大宝', '奇怪……这些信的邮戳,怎么都是同一天呢?', 'dabao')); + } else if (npcId === 'mimi') { + if (bt && taskActive(bt)) deliverBread(s, 'mimi', '咪咪'); + if (lettersActive && letterCarry()) deliverLetter(s, 'mimi', '咪咪'); + } else if (npcId === 'gui') { + if (bt && taskActive(bt)) deliverBread(s, 'gui', '龟爷爷'); + // C9 三段递进(D2/D4/D6 起各一段);C9 作为 9 处线索之一,最终段 +8 + if (!Director.flags.seen_C9a && Director.day >= 2) c9(s, 'C9a'); + else if (Director.flags.seen_C9a && !Director.flags.seen_C9b && Director.day >= 4) c9(s, 'C9b'); + else if (Director.flags.seen_C9b && !Director.flags.seen_C9c && Director.day >= 6) c9(s, 'C9c'); + } else if (npcId === 'fu') { + if (lettersActive && letterCarry()) { + // 第三封信(给芙医生)—— 署名乱码 C4 + deliverLetter(s, 'fu', '芙医生', true); + } + } + + // T3/T10 与三位村民问好 + const gt = greetTask(); + if (gt && taskActive(gt) && Director.greetedToday.size >= 3) completeTaskDeferred(gt, s); + + // 纠缠:一天内对同一 NPC ≥5 次 + if (Director.talkCounts[npcId] === 5) Director.sadBehavior('纠缠'); + return s; +} + +function completeTaskDeferred(id: string, s: DNode[]) { s.push({ choice: [{ label: '(继续)', tone: 'gentle', effect: () => completeTask(id) }] } as any); } + +function c9(s: DNode[], id: 'C9a' | 'C9b' | 'C9c') { + for (const t of CLUE_TEXT[id]) s.push(line('龟爷爷', t, 'gui')); + Director.flags['seen_' + id] = true; + if (id === 'C9c') discover('C9'); // 链完成 = 第 9 处线索 +8 +} +function letterCarry() { const f = Director.flags['letters']; return f && f.taken; } +function deliverBread(s: DNode[], target: string, disp: string) { + const bt = breadTask(); if (!bt) return; + const f = flag('bread' + Director.day, { taken: false, del: [] as string[] }); + if (!f.taken || f.del.includes(target)) return; + f.del.push(target); + s.push(line(disp, '谢谢你的面包,好甜呀!', target)); + if (f.del.length >= 3) completeTaskDeferred(bt, s); +} +function deliverLetter(s: DNode[], target: string, disp: string, glitch = false) { + const f = Director.flags['letters']; if (!f || !f.taken || f.del.includes(target)) return; + f.del.push(target); + if (glitch) { Director.emit('glitch', 'C4'); if (Director.day === 2) discover('C4'); s.push(line(disp, '……谢谢。信我收下了。', target)); } + else s.push(line(disp, '有我的信?谢谢你跑一趟!', target)); + if (f.del.length >= 3) completeTaskDeferred('T4', s); +} +function flag(k: string, init: T): T { if (!Director.flags[k]) Director.flags[k] = init; return Director.flags[k]; } + +// ---------------- 交互点 ---------------- +export function interactScript(kind: string, props: any): DNode[] | null { + switch (kind) { + case 'calendar': + if (Director.day >= 2) { discover('C1'); return CLUE_TEXT.C1.map(narr); } + return [narr('床头的日历,画着一朵向日葵。')]; + case 'bed': + Director.emit('sleep'); + return null; + case 'recipe': + if (Director.day >= 3) { discover('C2'); return CLUE_TEXT.C2.map(narr); } + return [narr('圆圆的配方纸,字迹娟秀。')]; + case 'clinicform': + if (Director.day >= 5) { discover('C8'); return CLUE_TEXT.C8.map(narr); } + return [narr('医生桌上有一叠表格,看不太清。')]; + case 'mailbox': + if (Director.day >= 2) { discover('C3'); return CLUE_TEXT.C3.map(narr); } + return [narr('大宝的邮筒,红彤彤的。')]; + case 'shelf': { + const s: DNode[] = []; + if (Director.day >= 2) { discover('C5'); s.push(...CLUE_TEXT.C5.map(narr)); } + else s.push(narr('满满一货架的货物,真丰富呀。')); + if (taskActive('T5')) completeTaskDeferred('T5', s); + return s; + } + case 'flowerbed': { + const wt = waterTask(); + if (wt && taskActive(wt)) { + const c = (Director.flags['water' + Director.day] = (Director.flags['water' + Director.day] || 0) + 1); + if (c >= 3) { completeTask(wt); return [narr('花坛浇好水了,花儿抬起了头。')]; } + return [narr(`浇水(${c}/3)……花儿咕嘟咕嘟地喝着。`)]; + } + return small('water', '你给花浇了点水。心情好了一点点。'); + } + case 'berry': { + if (taskActive('T6')) { + const set: number[] = flag('berries', [] as number[]); + if (!set.includes(props.n)) set.push(props.n); + if (set.length >= 5) { completeTask('T6'); return [narr('五颗莓果,红得像糖。采齐啦!')]; } + return [narr(`采到一颗莓果(${set.length}/5)。`)]; + } + return [narr('莓果丛,红艳艳的。')]; + } + case 'picnic': + if (taskActive('T7')) { Director.emit('picnic'); return null; } + return [narr('铺好的野餐布,格子花纹真可爱。')]; + case 'ribbon': { + if (taskActive('T13')) { + const set: number[] = flag('ribbons', [] as number[]); + if (!set.includes(props.n)) set.push(props.n); + if (set.length >= 4) { completeTask('T13'); return [narr('四条彩带挂好了,广场热闹起来。')]; } + return [narr(`挂上一条彩带(${set.length}/4)。`)]; + } + return [narr('一卷彩带,五颜六色的。')]; + } + case 'festival': + Director.emit('festival'); + return null; + case 'well': + if (Director.day < 5) return [narr('(一位村民温柔地把你引开)现在还不是看井的时候哦。')]; + gaze('well'); + discover('C7'); + return CLUE_TEXT.C7.map(narr); + case 'fog': + if (Director.day < 5) return [narr('(一位村民温柔地把你引开)那边是村庄的尽头,别去啦。')]; + gaze('fog'); + Director.emit('fog'); + return null; + } + return null; +} +function gaze(id: string) { if (!Director.gazedToday.has(id)) { Director.gazedToday.add(id); Director.addAwareness(3); } } +function small(kind: string, msg: string): DNode[] { + const n = Director.smallInteract[kind] || 0; + if (n < 3) { Director.smallInteract[kind] = n + 1; Director.addHappiness(1); } + return [narr(msg)]; +} + +// 对话选项语气效果 (§6.1/§6.2/§6.3) +export function applyTone(tone: 'gentle' | 'hesitant' | 'deny') { + if (tone === 'gentle') Director.addHappiness(2); + else if (tone === 'hesitant') Director.addAwareness(2); + else if (tone === 'deny') { Director.addAwareness(2); Director.sadBehavior('否定选项'); } +} + +// 结局判定 (§9 自上而下第一个满足者) +export function evalEnding(fromFog: boolean): string | null { + const D = Director; + if (D.escapeCount >= 3 && D.happiness <= 10) return 'E4'; + if (fromFog && D.awareness >= 70 && D.clues.size >= 6) return 'E3'; + if (D.awareness >= 30 && D.awareness <= 69) return 'E2'; + if (D.awareness <= 29 && D.happiness >= 90) return 'E1'; + return null; +} diff --git a/Opus4-8-xhigh/src/data/content.ts b/Opus4-8-xhigh/src/data/content.ts new file mode 100644 index 0000000..d026496 --- /dev/null +++ b/Opus4-8-xhigh/src/data/content.ts @@ -0,0 +1,88 @@ +// All hardcoded, verbatim game text/data (§4,§5,§6.5,§7,§8,§9). Change nothing without PRD. + +export interface NpcDef { name: string; offset: number; participates: boolean; sprite: string; } +export const NPCS: Record = { + afu: { name: '阿福', offset: 0, participates: true, sprite: 'npc_afu' }, + yuan: { name: '圆圆', offset: 15, participates: true, sprite: 'npc_yuan' }, // +15 最晚崩 + dabao: { name: '大宝', offset: 0, participates: true, sprite: 'npc_dabao' }, + mimi: { name: '咪咪', offset: -10, participates: true, sprite: 'npc_mimi' }, // -10 最早崩 + fu: { name: '芙医生', offset: 0, participates: false, sprite: 'npc_fu' }, // 不参与侵蚀 + gui: { name: '龟爷爷', offset: 0, participates: false, sprite: 'npc_gui' }, // 不参与侵蚀 +}; + +// §6.5 同音错字对照表(全 10 组,逐字) +export const TYPO: [string, string][] = [ + ['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'], + ['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'], +]; + +// §5 阿福每日公告(逐字,措辞逐日滑移) +export const ANNOUNCE: Record = { + 1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', + 2: '早上好呀!新的一天,新的幸福。祝你开心!', + 3: '早上好!今天要把幸福装得满满的哦。要开心!', + 4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // 与 D1 逐字相同 + 5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', + 6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', + 7: '庆典开始了。来吧。', +}; + +// §7 任务清单 +export interface TaskDef { day: number; title: string; reward: number; } +export const TASKS: Record = { + T1: { day: 1, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15 }, + T2: { day: 1, title: '给广场花坛浇水 ×3', reward: 10 }, + T3: { day: 1, title: '与任意三位村民问好', reward: 10 }, + T4: { day: 2, title: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)', reward: 15 }, + T5: { day: 2, title: '帮咪咪清点货架', reward: 10 }, + T6: { day: 3, title: '去花田采莓果 ×5', reward: 10 }, + T7: { day: 3, title: '湖畔野餐', reward: 15 }, + T8: { day: 4, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15 }, + T9: { day: 4, title: '给广场花坛浇水 ×3', reward: 10 }, + T10: { day: 4, title: '与任意三位村民问好', reward: 10 }, + T11: { day: 5, title: '帮圆圆揉面', reward: 10 }, + T12: { day: 5, title: '擦拭公告板', reward: 10 }, + T13: { day: 6, title: '在广场挂彩带 ×4', reward: 10 }, + T14: { day: 6, title: '保持微笑 60 秒', reward: 15 }, + T15: { day: 7, title: '参加第 413 届丰收庆典', reward: 0 }, +}; +export const DAY_TASKS: Record = { + 1: ['T1', 'T2', 'T3'], 2: ['T4', 'T5'], 3: ['T6', 'T7'], + 4: ['T8', 'T9', 'T10'], 5: ['T11', 'T12'], 6: ['T13', 'T14'], 7: ['T15'], +}; + +// §8 线索文本 +export const CLUE_TEXT: Record = { + C1: ['床头的日历……', '每一页都是同一个日期。'], + C2: ['配方纸的背面有一行手写小字:', '「按疗程配比。甜度:最大。」'], + C3: ['邮筒旁散落着一些信。', '所有邮戳……都是同一天。'], + C4: ['第三封信的署名闪过一串乱码——', '「PT-0▓」'], + C5: ['货架上,同一件商品重复排了九格。', '标签全是空白。'], + C6: ['擦到一半,浮现出旧字迹:', '「第 412 届丰收庆典」', '下面还压着「第 411 届」「第 410 届」……'], + C7: ['你凑近井口,安静地听。', '……滴。', '极轻的一声,像监护仪。'], + C8: ['医生桌上的表格抬头被手肘挡住,', '只露出「…谷情绪疗…」几个字。'], + C9a: ['湖水一直很平静。'], + C9b: ['面包一直是那个味道。'], + C9c: ['孩子,今天是第几个今天?'], +}; + +// §9 结局字幕 +export const ENDING_TEXT: Record = { + E1: ['庆典圆满。', '全村在镜头前微笑合影,定格。', '从此,每一天都很幸福。', '每一天。'], + E2: ['心电滴声,一响。', '你回到了 Day 1 清晨的床上。', '日历翻回同一天。', '疗程重新开始。'], + E4: ['庆典无人出席。', '空荡的广场上,彩带在没有风的空气里飘着。', '系统检测到疗程无效。'], + // E3 组装在 EndingScene(含姓名与真实时长) +}; + +// 医生固定台词(§4) +export const DOCTOR_LINE = '村里有诊所,但你不需要去那里哦。'; + +// NPC 日常问候(可腐蚀;原文保持干净)。key 为 npc id。 +export const GREET: Record = { + afu: ['你来啦!今天也要幸福地过哦。', '公告板上有新任务,别忘了看看。'], + yuan: ['面包刚出炉,闻闻,多甜呀。', '在蜜糖村,每天都是美好的一天呢。'], + dabao: ['有信要送吗?我最喜欢送信啦。', '大家都是好朋友,对吧?'], + mimi: ['要买点什么吗?今天阳光真好呀。', '我最喜欢和你聊天了,说个不停都可以。'], + fu: [DOCTOR_LINE, '你看起来很好。真的,很好。'], + gui: ['坐一会儿吧,湖边最舒服了。', '我这把年纪,什么都见过咯。'], +}; diff --git a/Opus4-8-xhigh/src/main.ts b/Opus4-8-xhigh/src/main.ts new file mode 100644 index 0000000..c235035 --- /dev/null +++ b/Opus4-8-xhigh/src/main.ts @@ -0,0 +1,49 @@ +import Phaser from 'phaser'; +import { Director } from './core/Director'; +import { Audio } from './core/Audio'; +import * as quests from './core/quests'; +import { BootScene } from './scenes/BootScene'; +import { TitleScene } from './scenes/TitleScene'; +import { WorldScene } from './scenes/WorldScene'; +import { UIScene } from './scenes/UIScene'; +import { EndingScene } from './scenes/EndingScene'; + +const BASE_W = 320, BASE_H = 180; + +const game = new Phaser.Game({ + type: Phaser.AUTO, + parent: 'game', + width: BASE_W, + height: BASE_H, + pixelArt: true, + roundPixels: true, + backgroundColor: '#101014', + scale: { mode: Phaser.Scale.NONE, autoCenter: Phaser.Scale.CENTER_BOTH }, + physics: { default: 'arcade', arcade: { debug: false } }, + scene: [BootScene, TitleScene, WorldScene, UIScene, EndingScene], +}); + +// 整数倍缩放居中(§12) +function fit() { + const z = Math.max(1, Math.floor(Math.min(window.innerWidth / BASE_W, window.innerHeight / BASE_H))); + game.scale.setZoom(z); +} +window.addEventListener('resize', fit); +game.events.once('ready', fit); +fit(); + +// 音频随侵蚀阶段联动(§6.4/§6.6)——全局唯一订阅 +Director.on('stage', () => Audio.setStage(Director.params)); + +// 首个用户手势后恢复音频(浏览器策略) +const resume = () => { Audio.ctx?.resume(); }; +window.addEventListener('pointerdown', resume); +window.addEventListener('keydown', resume); + +// 测试/验收辅助句柄(无害只读引用;正常游玩不使用) +(window as any).HV = { + game, Director, Audio, quests, + ui: () => game.scene.getScene('UI'), + world: () => game.scene.getScene('World'), + say: (nodes: any[]) => (game.scene.getScene('UI') as any).showDialogue(nodes), +}; diff --git a/Opus4-8-xhigh/src/scenes/BootScene.ts b/Opus4-8-xhigh/src/scenes/BootScene.ts new file mode 100644 index 0000000..6a4fbfc --- /dev/null +++ b/Opus4-8-xhigh/src/scenes/BootScene.ts @@ -0,0 +1,41 @@ +import Phaser from 'phaser'; + +const MAPS = ['home', 'plaza', 'bakery', 'grocery', 'clinic', 'lake', 'field', 'well', 'fog', 'white']; +const NPCS = ['afu', 'yuan', 'dabao', 'mimi', 'fu', 'gui']; + +export class BootScene extends Phaser.Scene { + constructor() { super('Boot'); } + + preload() { + const g = this.add.graphics(); + const bar = this.add.graphics(); + this.add.text(160, 78, '蜜糖村加载中……', { fontFamily: 'monospace', fontSize: '10px', color: '#ffe9b0' }).setOrigin(0.5); + this.load.on('progress', (p: number) => { + bar.clear(); bar.fillStyle(0xffd27a, 1); bar.fillRect(90, 96, 140 * p, 6); + }); + g.lineStyle(1, 0x8a6a3a).strokeRect(90, 96, 140, 6); + + this.load.image('tiles', 'assets/sprites/tileset.png'); + this.load.spritesheet('player', 'assets/sprites/player.png', { frameWidth: 16, frameHeight: 24 }); + this.load.spritesheet('portraits', 'assets/sprites/portraits.png', { frameWidth: 32, frameHeight: 32 }); + this.load.spritesheet('birds', 'assets/sprites/birds.png', { frameWidth: 16, frameHeight: 24 }); + this.load.spritesheet('extras', 'assets/sprites/extras.png', { frameWidth: 16, frameHeight: 16 }); + this.load.image('sparrow', 'assets/sprites/sparrow.png'); + for (const n of NPCS) this.load.spritesheet('npc_' + n, `assets/sprites/npc_${n}.png`, { frameWidth: 16, frameHeight: 24 }); + for (const m of MAPS) this.load.tilemapTiledJSON('map_' + m, `assets/maps/${m}.json`); + } + + create() { + // player walk anims (rows: 0 down,1 left,2 right,3 up; 4 frames each) + const mk = (key: string, row: number) => + this.anims.create({ key, frames: this.anims.generateFrameNumbers('player', { start: row * 4, end: row * 4 + 3 }), frameRate: 8, repeat: -1 }); + mk('walk-down', 0); mk('walk-left', 1); mk('walk-right', 2); mk('walk-up', 3); + // bird mouth + this.anims.create({ key: 'bird-flap', frames: this.anims.generateFrameNumbers('birds', { start: 0, end: 1 }), frameRate: 3, repeat: -1 }); + // extras: 5 critters x 2 frames + for (let k = 0; k < 5; k++) + this.anims.create({ key: 'extra' + (k + 1), frames: this.anims.generateFrameNumbers('extras', { start: k * 2, end: k * 2 + 1 }), frameRate: 3, repeat: -1 }); + + this.scene.start('Title'); + } +} diff --git a/Opus4-8-xhigh/src/scenes/EndingScene.ts b/Opus4-8-xhigh/src/scenes/EndingScene.ts new file mode 100644 index 0000000..3bf0584 --- /dev/null +++ b/Opus4-8-xhigh/src/scenes/EndingScene.ts @@ -0,0 +1,143 @@ +import Phaser from 'phaser'; +import { Director } from '../core/Director'; +import { Audio } from '../core/Audio'; +import { ENDING_TEXT } from '../data/content'; + +const FONT = '"PingFang SC","Microsoft YaHei","Hiragino Sans GB",sans-serif'; + +export class EndingScene extends Phaser.Scene { + constructor() { super('Ending'); } + + create(data: { id: string }) { + this.cameras.main.setBackgroundColor('#000000'); + this.cameras.main.fadeIn(500, 0, 0, 0); + switch (data.id) { + case 'E1': this.e1(); break; + case 'E2': this.e2(); break; + case 'E3': this.e3(); break; + default: this.e4(); break; + } + } + + // 字幕逐行(前一行淡出,避免重叠) + private prevCap?: Phaser.GameObjects.Text; + private captions(lines: string[], startDelay: number, gap: number, y = 150, cb?: () => void) { + let d = startDelay; + lines.forEach((ln, i) => { + this.time.delayedCall(d, () => { + if (this.prevCap) { const p = this.prevCap; this.tweens.add({ targets: p, alpha: 0, duration: 400, onComplete: () => p.destroy() }); } + const t = this.add.text(160, y, ln, { fontFamily: FONT, fontSize: '11px', color: '#f2ead6', align: 'center', wordWrap: { width: 300 } }).setOrigin(0.5).setAlpha(0).setDepth(100); + this.tweens.add({ targets: t, alpha: 1, duration: 600 }); + this.prevCap = t; + if (i === lines.length - 1 && cb) this.time.delayedCall(gap, cb); + }); + d += gap; + }); + } + private homeButton(delay: number) { + this.time.delayedCall(delay, () => { + const b = this.add.text(160, 168, '回到标题', { fontFamily: FONT, fontSize: '11px', color: '#fff', backgroundColor: '#8a5a3a', padding: { x: 8, y: 3 } }) + .setOrigin(0.5).setDepth(200).setInteractive({ useHandCursor: true }).setAlpha(0); + this.tweens.add({ targets: b, alpha: 1, duration: 500 }); + b.on('pointerdown', () => this.scene.start('Title')); + }); + } + + // E1 糖衣:全村微笑合影定格,饱和度缓慢推到 1.5 + private e1() { + this.cameras.main.setBackgroundColor('#f6e7b8'); + const g = this.add.graphics(); g.fillStyle(0x8fd3f0, 1).fillRect(0, 0, 320, 110); g.fillStyle(0x7bc86c, 1).fillRect(0, 110, 320, 70); + const cast = ['npc_afu', 'npc_yuan', 'npc_dabao', 'npc_mimi', 'npc_fu', 'npc_gui']; + cast.forEach((c, i) => this.add.sprite(56 + i * 36, 108, c, 0).setOrigin(0.5, 1).setScale(1.4)); + this.add.image(232, 108, 'portraits', 0).setOrigin(0.5, 1).setScale(0.9); + // 合影相框 + const fr = this.add.graphics(); fr.lineStyle(4, 0xffffff, 1).strokeRect(20, 40, 280, 80); + this.add.text(160, 34, '📷', { fontSize: '12px' }).setOrigin(0.5); + const sat = this.cameras.main.postFX.addColorMatrix(); + let s = 1.0; const ev = this.time.addEvent({ delay: 60, loop: true, callback: () => { s = Math.min(1.5, s + 0.006); sat.saturate((s - 1) * 1.5, false); } }); + this.time.delayedCall(9000, () => ev.remove()); + this.captions(ENDING_TEXT.E1, 1200, 2000, 150); + this.homeButton(1200 + 2000 * ENDING_TEXT.E1.length + 800); + } + + // E2 回收:负片 2 秒 + 心电滴声 → 回到 Day1 床上,日历翻回同一天 + private e2() { + const neg = this.cameras.main.postFX.addColorMatrix(); neg.negative(); + Audio.sfxEcg(); + const t = this.add.text(160, 90, '——', { fontFamily: FONT, fontSize: '14px', color: '#ffffff' }).setOrigin(0.5); + this.time.delayedCall(2000, () => { + neg.reset(); t.destroy(); + // Day1 清晨的床(用室内地板 + 床 tile 感觉) + this.cameras.main.setBackgroundColor('#b09a78'); + const g = this.add.graphics(); g.fillStyle(0xcaa06e, 1).fillRect(60, 70, 200, 60); g.fillStyle(0xf49ac1, 1).fillRect(70, 78, 180, 40); g.fillStyle(0xf5e6c0, 1).fillRect(70, 78, 60, 40); + this.add.image(150, 100, 'portraits', 0).setScale(1.4); + this.add.text(238, 60, '日\n历', { fontFamily: FONT, fontSize: '9px', color: '#8a5a3a', align: 'center' }); + this.captions(ENDING_TEXT.E2, 600, 1900, 150); + this.homeButton(600 + 1900 * ENDING_TEXT.E2.length + 800); + }); + } + + // E3 苏醒:白色走廊→病房→病历特写→微笑消失变疲惫脸 + 灰麻雀(§9) + private e3() { + const map = this.make.tilemap({ key: 'map_white' }); + map.layers.forEach((l: any) => (l.tilemapLayer = null)); + const ts = map.addTilesetImage('tiles', 'tiles')!; + map.createLayer('ground', ts, 0, 0); map.createLayer('obstacles', ts, 0, 0); + const cam = this.cameras.main; cam.setBounds(0, 0, map.widthInPixels, map.heightInPixels); cam.centerOn(160, 40); + const hint = this.add.text(160, 20, '雾裂开了,成了一条白色的走廊……', { fontFamily: FONT, fontSize: '9px', color: '#5a5a60' }).setOrigin(0.5).setScrollFactor(0).setDepth(50); + this.tweens.add({ targets: cam, scrollY: 44, duration: 3200, ease: 'Sine.inOut', delay: 700, onComplete: () => hint.setText('……病房。') }); + + this.time.delayedCall(4600, () => { + cam.fadeOut(600, 240, 242, 245); + cam.once('camerafadeoutcomplete', () => { hint.destroy(); this.record(); }); + }); + } + private record() { + const cam = this.cameras.main; cam.setBounds(0, 0, 320, 180); cam.setScroll(0, 0); cam.setBackgroundColor('#e6e8ea'); cam.fadeIn(500, 240, 242, 245); + // 病历特写(放进容器,稍后整体淡出) + const c = this.add.container(0, 0).setDepth(60); + const p = this.add.graphics(); p.fillStyle(0xffffff, 1).fillRoundedRect(36, 24, 248, 128, 4); p.lineStyle(1, 0x9aa0a8).strokeRoundedRect(36, 24, 248, 128, 4); + const head = this.add.text(160, 42, '蜜糖谷情绪疗养中心', { fontFamily: FONT, fontSize: '13px', color: '#2a2a30', fontStyle: 'bold' }).setOrigin(0.5).setAlpha(0); + const rule = this.add.graphics(); rule.lineStyle(1, 0xccced2).lineBetween(56, 58, 264, 58); + c.add([p, head, rule]); + this.tweens.add({ targets: head, alpha: 1, duration: 700 }); + const rows = [`姓名:${Director.playerName}`, `编号:PT-07`, `本次疗程时长:${Director.playtimeString()}`]; + rows.forEach((r, i) => this.time.delayedCall(900 + i * 750, () => { + const t = this.add.text(64, 74 + i * 22, r, { fontFamily: FONT, fontSize: '12px', color: '#33333a' }).setAlpha(0); c.add(t); + this.tweens.add({ targets: t, alpha: 1, duration: 500 }); + })); + // 最后一帧:微笑头像消失 → 疲惫真实的脸 + 灰麻雀(§9 唯一一次头像变化) + this.time.delayedCall(3800, () => { + this.tweens.add({ targets: c, alpha: 0, duration: 700, onComplete: () => c.destroy() }); + this.time.delayedCall(800, () => this.finalFrame()); + }); + } + private finalFrame() { + const port = this.add.image(160, 82, 'portraits', 0).setScale(1.8).setDepth(60); + const cap = this.add.text(160, 128, '……', { fontFamily: FONT, fontSize: '11px', color: '#55555c' }).setOrigin(0.5).setDepth(60); + this.time.delayedCall(1400, () => { + this.cameras.main.flash(220, 255, 255, 255); + port.setTexture('portraits', 1); // 真实的、疲惫的脸 + this.add.image(252, 54, 'sparrow').setScale(1.5).setDepth(60); // 窗外,一只真的麻雀 + cap.setText('窗外,一只灰色的、真的麻雀。'); + this.homeButton(1800); + }); + } + + // E4 静默:庆典无人出席,空广场,彩带在没有风里飘,字幕后黑屏 + private e4() { + this.cameras.main.setBackgroundColor('#3a3d42'); + const g = this.add.graphics(); g.fillStyle(0x4a4e54, 1).fillRect(0, 120, 320, 60); // 灰广场 + // 无风飘的彩带 + for (let i = 0; i < 6; i++) { + const x = 40 + i * 44, y = 60 + (i % 3) * 20; + const r = this.add.graphics(); r.fillStyle([0xaa6688, 0x88888a, 0x9a8a6a][i % 3], 0.8); + for (let k = 0; k < 12; k++) r.fillRect(x + k, y + Math.round(2 * Math.sin(k * 0.9)), 1, 2); + this.tweens.add({ targets: r, y: y + 6, duration: 3000 + i * 300, yoyo: true, repeat: -1, ease: 'Sine.inOut' }); + } + this.captions(ENDING_TEXT.E4, 1000, 2200, 40, () => { + this.cameras.main.fadeOut(1500, 0, 0, 0); + this.homeButton(2000); + }); + } +} diff --git a/Opus4-8-xhigh/src/scenes/TitleScene.ts b/Opus4-8-xhigh/src/scenes/TitleScene.ts new file mode 100644 index 0000000..c237356 --- /dev/null +++ b/Opus4-8-xhigh/src/scenes/TitleScene.ts @@ -0,0 +1,47 @@ +import Phaser from 'phaser'; +import { Director } from '../core/Director'; +import { Audio } from '../core/Audio'; +import { activateDay } from '../core/quests'; + +export class TitleScene extends Phaser.Scene { + private nameInput?: HTMLInputElement; + constructor() { super('Title'); } + + create() { + const W = 320, H = 180; + this.cameras.main.setBackgroundColor('#f6e7b8'); + // sky / ground bands + const g = this.add.graphics(); + g.fillStyle(0x8fd3f0, 1).fillRect(0, 0, W, 110); + g.fillStyle(0x7bc86c, 1).fillRect(0, 110, W, 70); + g.fillStyle(0xf2a94e, 1).fillCircle(260, 40, 22); // sun + // title + this.add.text(160, 44, '崩坏童话', { fontFamily: 'serif', fontSize: '30px', color: '#c85850', fontStyle: 'bold' }).setOrigin(0.5); + this.add.text(160, 74, '蜜 糖 村', { fontFamily: 'serif', fontSize: '18px', color: '#8a5a3a' }).setOrigin(0.5); + this.add.text(160, 150, 'WASD/方向键移动 · E 交互 · ` 调试面板', { fontFamily: 'monospace', fontSize: '8px', color: '#5a4a3a' }).setOrigin(0.5); + + // DOM name input (centered overlay) + const inp = document.createElement('input'); + inp.type = 'text'; inp.maxLength = 8; inp.placeholder = '小满'; + inp.style.cssText = 'position:fixed;left:50%;top:58%;transform:translate(-50%,-50%);width:160px;padding:6px 8px;font-size:16px;text-align:center;border:2px solid #8a5a3a;border-radius:6px;background:#fffbe9;color:#5a4a3a;font-family:sans-serif;outline:none;'; + document.body.appendChild(inp); this.nameInput = inp; setTimeout(() => inp.focus(), 50); + this.add.text(160, 96, '请输入名字(留空默认 小满):', { fontFamily: 'monospace', fontSize: '8px', color: '#5a4a3a' }).setOrigin(0.5); + + const btn = this.add.text(160, 124, '▶ 开始疗程', { fontFamily: 'monospace', fontSize: '12px', color: '#ffffff', backgroundColor: '#c85850', padding: { x: 8, y: 4 } }) + .setOrigin(0.5).setInteractive({ useHandCursor: true }); + btn.on('pointerover', () => btn.setStyle({ backgroundColor: '#e0736b' })); + btn.on('pointerout', () => btn.setStyle({ backgroundColor: '#c85850' })); + btn.on('pointerdown', () => this.begin()); + inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') this.begin(); }); + } + + private begin() { + const name = (this.nameInput?.value || '').trim() || '小满'; + if (this.nameInput) { this.nameInput.remove(); this.nameInput = undefined; } + Audio.start(); + Director.reset(name); + activateDay(1); + this.scene.launch('UI'); + this.scene.start('World', { map: 'home', spawn: 'start' }); + } +} diff --git a/Opus4-8-xhigh/src/scenes/UIScene.ts b/Opus4-8-xhigh/src/scenes/UIScene.ts new file mode 100644 index 0000000..95e2bd4 --- /dev/null +++ b/Opus4-8-xhigh/src/scenes/UIScene.ts @@ -0,0 +1,254 @@ +import Phaser from 'phaser'; +import { Director } from '../core/Director'; +import { Audio } from '../core/Audio'; +import { activateDay, applyTone, DChoiceOpt, DNode } from '../core/quests'; + +const FONT = '"PingFang SC","Microsoft YaHei","Hiragino Sans GB",sans-serif'; +const EXPR: Record = { smile: 0, flat: 1, off: 2 }; + +export class UIScene extends Phaser.Scene { + busy = false; + private dial!: Phaser.GameObjects.Graphics; + private promptT!: Phaser.GameObjects.Text; + private toastT!: Phaser.GameObjects.Text; + // dialogue + private box!: Phaser.GameObjects.Container; + private nameT!: Phaser.GameObjects.Text; + private bodyT!: Phaser.GameObjects.Text; + private portrait!: Phaser.GameObjects.Image; + private portraitBg!: Phaser.GameObjects.Graphics; + private hintT!: Phaser.GameObjects.Text; + private dq: DNode[] = []; + private onDone?: () => void; + private choice: DChoiceOpt[] | null = null; + private choiceIdx = 0; + private openAt = 0; + // overlays + private rainG!: Phaser.GameObjects.Graphics; private raining = false; private rainY = 0; + private forcedC!: Phaser.GameObjects.Container; private forcedOn = false; private confetti: { x: number; y: number; c: number; s: number }[] = []; + private dbg!: HTMLDivElement; + private domKey!: (e: KeyboardEvent) => void; + + constructor() { super('UI'); } + + create() { + this.busy = false; + // dial + this.dial = this.add.graphics().setDepth(1000).setScrollFactor(0); + this.drawDial(); + // prompt + toast + this.promptT = this.add.text(160, 116, '', { fontFamily: FONT, fontSize: '9px', color: '#fffbe9', backgroundColor: '#00000088', padding: { x: 3, y: 1 } }).setOrigin(0.5).setDepth(1000).setScrollFactor(0).setVisible(false); + this.toastT = this.add.text(160, 14, '', { fontFamily: FONT, fontSize: '9px', color: '#fff2c0', backgroundColor: '#00000099', padding: { x: 4, y: 2 }, align: 'center', wordWrap: { width: 280 } }).setOrigin(0.5, 0).setDepth(1200).setScrollFactor(0).setAlpha(0); + + // dialogue box + const bg = this.add.graphics(); + bg.fillStyle(0x2a2230, 0.94).fillRoundedRect(4, 128, 312, 48, 4); + bg.lineStyle(1, 0xffd27a, 0.8).strokeRoundedRect(4, 128, 312, 48, 4); + this.portraitBg = this.add.graphics(); + this.portraitBg.fillStyle(0x14100e, 1).fillRoundedRect(8, 132, 40, 40, 3); + this.portrait = this.add.image(28, 152, 'portraits', 0).setScale(1.1); + this.nameT = this.add.text(54, 133, '', { fontFamily: FONT, fontSize: '10px', color: '#ffd27a', fontStyle: 'bold' }); + this.bodyT = this.add.text(54, 148, '', { fontFamily: FONT, fontSize: '10px', color: '#fdf6e3', wordWrap: { width: 252 }, lineSpacing: 2 }); + this.hintT = this.add.text(308, 166, '▶', { fontFamily: FONT, fontSize: '9px', color: '#ffd27a' }).setOrigin(1, 0.5); + this.box = this.add.container(0, 0, [bg, this.portraitBg, this.portrait, this.nameT, this.bodyT, this.hintT]).setDepth(900).setScrollFactor(0).setVisible(false); + + // overlays + this.rainG = this.add.graphics().setDepth(800).setScrollFactor(0); + this.forcedC = this.add.container(0, 0).setDepth(850).setScrollFactor(0).setVisible(false); + const fbg = this.add.graphics(); fbg.fillStyle(0xff88bb, 0.10).fillRect(0, 0, 320, 180); + const ftxt = this.add.text(160, 30, '※ 欢乐庆典 ※', { fontFamily: FONT, fontSize: '14px', color: '#ff5599', fontStyle: 'bold' }).setOrigin(0.5); + this.forcedC.add([fbg, ftxt]); + + // input + // 动作键走 DOM 监听(持久 UIScene 的 Phaser 键盘在初始场景下不可靠;DOM 与场景顺序无关) + this.domKey = (e: KeyboardEvent) => { + if (e.repeat) return; + if (e.code === 'Backquote') { this.toggleDebug(); e.preventDefault(); return; } + if (this.box.visible) { + if (e.code === 'KeyE' || e.code === 'Enter' || e.code === 'Space') { this.tryAdvance(); e.preventDefault(); } + else if (e.code === 'KeyW' || e.code === 'ArrowUp') this.moveChoice(-1); + else if (e.code === 'KeyS' || e.code === 'ArrowDown') this.moveChoice(1); + } else if (e.code === 'KeyE') { + (this.scene.get('World') as any)?.tryInteract?.(); + } + }; + window.addEventListener('keydown', this.domKey); + this.input.on('pointerdown', () => { if (this.box.visible) this.tryAdvance(); }); + + // Director listeners + const redraw = () => this.drawDial(); + Director.on('change', redraw); + Director.on('stage', redraw); + Director.on('glitch', (t: string) => this.flashGlitch()); + Director.on('toast', (m: string) => this.toast(m)); + Director.on('dusk', () => { Audio.sfxBell(); this.toast('※ 黄昏钟声 ※ 天黑了……回小屋,睡一觉吧。'); }); + this.events.once('shutdown', () => { Director.off('change', redraw); Director.off('stage', redraw); this.dbg?.remove(); window.removeEventListener('keydown', this.domKey); }); + + this.buildDebug(); + } + + // ---------------- sunflower dial (§6.1) ---------------- + private drawDial() { + const g = this.dial; g.clear(); + const cx = 28, cy = 26, R = 12; + g.fillStyle(0x3a2f22, 0.55).fillCircle(cx, cy, R + 6); + const petals = Director.petals, s = Director.stage; + for (let i = 0; i < 12; i++) { + const a = (i / 12) * Math.PI * 2 - Math.PI / 2; + const lit = i < petals; + const col = lit ? (s >= 3 ? 0xfff3b4 : 0xffd257) : 0xcabf9a; + g.fillStyle(col, lit ? 1 : 0.75).fillCircle(cx + Math.cos(a) * R, cy + Math.sin(a) * R, 3.3); + } + g.fillStyle(0xe8a44e, 1).fillCircle(cx, cy, 6.5); + g.fillStyle(0x9a6a34, 1).fillCircle(cx, cy, 4.8); + if (Director.blackSeeds) { + for (const [dx, dy] of [[-2, -1], [1, -2], [2, 1], [-1, 2], [0, 0]]) g.fillStyle(0x241c1c, 1).fillCircle(cx + dx, cy + dy, 1.1); + } + } + + // ---------------- prompt / toast ---------------- + showPrompt(text: string | null) { this.promptT.setVisible(!!text && !this.busy).setText(text || ''); } + toast(msg: string) { + this.toastT.setText(msg).setAlpha(1); + this.tweens.killTweensOf(this.toastT); + this.tweens.add({ targets: this.toastT, alpha: 0, delay: 2400, duration: 700 }); + } + + // ---------------- dialogue ---------------- + showDialogue(nodes: DNode[], onDone?: () => void) { + this.dq = nodes.slice(); this.onDone = onDone; this.choice = null; this.busy = true; + this.box.setVisible(true); this.showPrompt(null); this.openAt = performance.now(); + this.next(); + } + private next() { + if (this.dq.length === 0) { this.close(); return; } + const node = this.dq.shift()! as any; + if (node.choice) this.renderChoice(node.choice); + else this.renderLine(node); + } + private renderLine(l: any) { + this.choice = null; + const txt = l.raw ? l.text : Director.corrupt(l.text, l.speaker); + const hasSpeaker = !!l.speaker || !!l.npcId; + this.nameT.setText(l.npcId ? (nameOf(l.npcId)) : (l.speaker || '')); + this.bodyT.setText(txt).setY(hasSpeaker ? 148 : 144); + this.setPortrait(l.npcId); + this.hintT.setText('▶'); + Audio.sfxBeep(); + } + private renderChoice(opts: DChoiceOpt[]) { + this.choice = opts; this.choiceIdx = 0; + this.nameT.setText('你'); + this.setPortrait(undefined); + this.hintT.setText('W/S 选择 · E 确定'); + this.drawChoices(); + Audio.sfxBeep(); + } + private drawChoices() { + if (!this.choice) return; + const lines = this.choice.map((o, i) => (i === this.choiceIdx ? '▶ ' : ' ') + o.label); + this.bodyT.setText(lines.join('\n')).setY(140); + } + private setPortrait(npcId?: string) { + if (npcId) { + const def = (Director as any); + const expr = Director.npcExpression(npcId); + const tex = spriteOf(npcId); + this.portrait.setVisible(true).setTexture(tex, EXPR[expr]).setScale(1.5); + this.portraitBg.setVisible(true); + } else { + // narration / player: 玩家永远微笑头像 + this.portrait.setVisible(true).setTexture('portraits', 0).setScale(1.1); + this.portraitBg.setVisible(true); + } + } + private tryAdvance() { + if (!this.box.visible || performance.now() - this.openAt < 160) return; + if (this.choice) { this.chooseCurrent(); return; } + this.next(); + } + private moveChoice(d: number) { + if (!this.box.visible || !this.choice) return; + this.choiceIdx = (this.choiceIdx + this.choice.length + d) % this.choice.length; + this.drawChoices(); Audio.sfxBeep(); + } + private chooseCurrent() { + const opt = this.choice![this.choiceIdx]; + this.choice = null; + applyTone(opt.tone); + opt.effect?.(); + if (opt.then && opt.then.length) this.dq = [...opt.then, ...this.dq]; + this.next(); + } + private close() { + this.box.setVisible(false); this.busy = false; + const cb = this.onDone; this.onDone = undefined; cb?.(); + } + + // ---------------- overlays ---------------- + setRain(on: boolean) { this.raining = on; if (!on) this.rainG.clear(); } + setForced(on: boolean) { this.forcedOn = on; this.forcedC.setVisible(on); if (on && this.confetti.length === 0) for (let i = 0; i < 40; i++) this.confetti.push({ x: Math.random() * 320, y: Math.random() * 180, c: [0xff5599, 0xffd257, 0x66ccff, 0x88ee99][i % 4], s: 20 + Math.random() * 40 }); } + flashGlitch() { + const t = this.add.text(160, 90, 'PT-0▓', { fontFamily: 'monospace', fontSize: '18px', color: '#ff4444', backgroundColor: '#000000' }).setOrigin(0.5).setDepth(2000).setScrollFactor(0); + let n = 0; const ev = this.time.addEvent({ delay: 40, repeat: 5, callback: () => { t.setX(160 + (Math.random() * 8 - 4)); t.setColor(n++ % 2 ? '#44ffee' : '#ff4444'); } }); + this.time.delayedCall(200, () => { t.destroy(); ev.remove(); }); + } + playDream(prevDay: number, cb: () => void) { + const c = this.add.container(0, 0).setDepth(2500).setScrollFactor(0); + const bg = this.add.graphics(); bg.fillStyle(0x000000, 1).fillRect(0, 0, 320, 180); c.add(bg); + const t = this.add.text(160, 90, '(睡着了……)', { fontFamily: FONT, fontSize: '11px', color: '#8a8a8a' }).setOrigin(0.5); c.add(t); + const leak = prevDay >= 3 || Director.stage >= 3; + const dur = leak ? (1000 + (Director.stage >= 4 ? 800 : 0)) : 0; + this.time.delayedCall(700, () => { + if (leak) { + const w = this.add.graphics(); w.fillStyle(0xdfe2e6, 1).fillRect(0, 0, 320, 180); + w.fillStyle(0xc2c6cc, 1).fillRect(40, 96, 90, 40); w.fillStyle(0x9aa0a8, 1).fillRect(40, 96, 90, 8); // 病床 + const wt = this.add.text(160, 40, '……这里是……?', { fontFamily: FONT, fontSize: '10px', color: '#6a6a70' }).setOrigin(0.5); + c.add([w, wt]); + this.time.delayedCall(dur, () => { c.destroy(); cb(); }); + } else { c.destroy(); cb(); } + }); + } + + update(_t: number, dt: number) { + // rain + if (this.raining) { + this.rainY = (this.rainY + dt * 0.35) % 20; + const g = this.rainG; g.clear(); g.lineStyle(1, 0xbcd6ee, 0.5); + for (let x = -20; x < 340; x += 12) for (let y = -20; y < 200; y += 20) { const yy = y + this.rainY; g.lineBetween(x + (yy % 40) * 0.2, yy, x + (yy % 40) * 0.2 - 3, yy + 6); } + } + // forced confetti + if (this.forcedOn) { + const g = this.rainG; // reuse layer when not raining + if (!this.raining) { g.clear(); for (const p of this.confetti) { p.y += (p.s / 60) * dt / 16; if (p.y > 180) p.y = -4; g.fillStyle(p.c, 0.9).fillRect(p.x, p.y, 2, 2); } } + } + } + + // ---------------- debug panel (§13) ---------------- + private buildDebug() { + const d = document.createElement('div'); + d.style.cssText = 'position:fixed;right:8px;top:8px;z-index:9999;display:none;background:#1b1620ee;color:#eee;font:11px/1.5 monospace;padding:8px;border:1px solid #ffd27a;border-radius:6px;max-width:220px;'; + d.innerHTML = `DEBUG(\` 开关) +
幸福 ${btns('H', [10, 40, 70, 90, 100])}
+
觉知 ${btns('A', [0, 40, 70, 100])}
+
日 ${btns('D', [1, 2, 3, 4, 5, 6, 7])}
+
传送 ${['home', 'plaza', 'bakery', 'grocery', 'clinic', 'lake', 'field', 'well', 'fog'].map((m) => ``).join('')}
+
+
结局 ${['E1', 'E2', 'E3', 'E4'].map((e) => ``).join('')}
`; + document.body.appendChild(d); this.dbg = d; + d.addEventListener('click', (e) => { + const el = e.target as HTMLElement; const world = () => this.scene.get('World') as any; + if (el.dataset.h) Director.setHappiness(+el.dataset.h); + else if (el.dataset.a) Director.setAwareness(+el.dataset.a); + else if (el.dataset.d) { activateDay(+el.dataset.d); this.toast('Day ' + el.dataset.d); } + else if (el.dataset.tp) world().debugTeleport(el.dataset.tp); + else if (el.dataset.clues) { ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'].forEach((c) => Director.discoverClue(c)); Director.flags.seen_C9a = Director.flags.seen_C9b = Director.flags.seen_C9c = true; this.toast('线索已置齐(9/9)'); } + else if (el.dataset.end) world().startEnding(el.dataset.end); + }); + } + private toggleDebug() { if (!this.dbg) return; this.dbg.style.display = this.dbg.style.display === 'none' ? 'block' : 'none'; } +} + +function btns(kind: string, vals: number[]) { const a = kind === 'H' ? 'h' : kind === 'A' ? 'a' : 'd'; return vals.map((v) => ``).join(''); } +function nameOf(id: string): string { return ({ afu: '阿福', yuan: '圆圆', dabao: '大宝', mimi: '咪咪', fu: '芙医生', gui: '龟爷爷', bird1: '双子鸟', bird2: '双子鸟' } as any)[id] || id; } +function spriteOf(id: string): string { return ({ afu: 'npc_afu', yuan: 'npc_yuan', dabao: 'npc_dabao', mimi: 'npc_mimi', fu: 'npc_fu', gui: 'npc_gui' } as any)[id] || 'npc_afu'; } diff --git a/Opus4-8-xhigh/src/scenes/WorldScene.ts b/Opus4-8-xhigh/src/scenes/WorldScene.ts new file mode 100644 index 0000000..bf01451 --- /dev/null +++ b/Opus4-8-xhigh/src/scenes/WorldScene.ts @@ -0,0 +1,335 @@ +import Phaser from 'phaser'; +import { Director } from '../core/Director'; +import { Audio } from '../core/Audio'; +import { activateDay, evalEnding, interactScript, talkScript, taskActive, completeTask, DNode } from '../core/quests'; +import { NPCS } from '../data/content'; +import type { UIScene } from './UIScene'; + +const SPEED = 96; +const EXPR: Record = { smile: 0, flat: 1, off: 2 }; +const OUTDOOR = new Set(['plaza', 'lake', 'field', 'well', 'fog']); +const narr = (text: string): DNode => ({ speaker: '', text }); + +interface Ent { id: string; kind: 'named' | 'bird' | 'extra'; sprite: Phaser.GameObjects.Sprite; anim?: string; tween?: Phaser.Tweens.Tween; } + +export class WorldScene extends Phaser.Scene { + mapKey!: string; + map!: Phaser.Tilemaps.Tilemap; + obs!: Phaser.Tilemaps.TilemapLayer; + player!: Phaser.Physics.Arcade.Sprite; + facing = 'down'; + cursors!: Phaser.Types.Input.Keyboard.CursorKeys; + wasd!: any; + ents: Ent[] = []; + interacts: { x: number; y: number; kind: string; props: any }[] = []; + doors: { rect: Phaser.Geom.Rectangle; target: string; dest: string }[] = []; + private sat!: any; private vig!: any; private neg!: any; + private target: any = null; + private doorCd = 0; private idle = 0; private rainT = 0; private trans = false; + private negFrames = 0; private offFlash = false; + private t14 = false; private t14t = 0; private t14fade = false; + private forced = false; private forcedT = 0; + private handlers: Record = {}; + + constructor() { super('World'); } + + get ui(): UIScene { return this.scene.get('UI') as UIScene; } + + create(data: { map: string; spawn: string }) { + this.mapKey = data.map; + this.ents = []; this.interacts = []; this.doors = []; + this.target = null; this.doorCd = 300; this.idle = 0; this.trans = false; + this.t14 = false; this.t14t = 0; this.t14fade = false; this.forced = false; + + this.map = this.make.tilemap({ key: 'map_' + data.map }); + // Tiled 地图数据被 Phaser 缓存并共享;重访地图前清掉上次的 tilemapLayer 引用,否则 createLayer 返回 null + this.map.layers.forEach((l: any) => (l.tilemapLayer = null)); + const ts = this.map.addTilesetImage('tiles', 'tiles')!; + let ground = this.map.createLayer('ground', ts, 0, 0); + let obsL = this.map.createLayer('obstacles', ts, 0, 0); + if (!ground || !obsL) { this.map.layers.forEach((l: any) => (l.tilemapLayer = null)); ground = ground || this.map.createLayer('ground', ts, 0, 0); obsL = obsL || this.map.createLayer('obstacles', ts, 0, 0); } + ground!.setDepth(0); + this.obs = obsL!; this.obs.setDepth(1); + this.obs.setCollisionByExclusion([-1]); + + const W = this.map.widthInPixels, H = this.map.heightInPixels; + this.physics.world.setBounds(0, 0, W, H); + this.cameras.main.setBounds(0, 0, W, H); + this.cameras.main.setBackgroundColor('#101014'); + + // spawn point + const objs = this.map.getObjectLayer('objects')?.objects || []; + const sp = objs.find((o) => o.name === data.spawn) || objs.find((o) => o.type === 'spawn'); + const px = (sp?.x || 32) + 8, py = (sp?.y || 32) + 8; + this.player = this.physics.add.sprite(px, py, 'player', 0).setDepth(50); + this.player.body!.setSize(10, 7); (this.player.body as Phaser.Physics.Arcade.Body).setOffset(3, 16); + this.player.setCollideWorldBounds(true); + this.physics.add.collider(this.player, this.obs); + // 小地图整屏居中,大地图跟随并钳制边界(§12) + if (W <= 320 && H <= 180) this.cameras.main.centerOn(W / 2, H / 2); + else this.cameras.main.startFollow(this.player, true, 0.18, 0.18); + this.cameras.main.roundPixels = true; + + // entities + interacts + doors + for (const o of objs) { + if (o.type === 'npc') this.spawnEntity(o); + else if (o.type === 'interact') this.interacts.push({ x: o.x! + 8, y: o.y! + 8, kind: prop(o, 'kind'), props: propsObj(o) }); + else if (o.type === 'door') this.doors.push({ rect: new Phaser.Geom.Rectangle(o.x!, o.y!, o.width!, o.height!), target: prop(o, 'target'), dest: prop(o, 'dest') }); + } + + // postFX + const cam = this.cameras.main; + this.sat = cam.postFX.addColorMatrix(); + this.vig = cam.postFX.addVignette(0.5, 0.5, 0.82, 0); + this.neg = cam.postFX.addColorMatrix(); + this.updateFX(); + + // input + this.cursors = this.input.keyboard!.createCursorKeys(); + this.wasd = this.input.keyboard!.addKeys('W,A,S,D'); + // E 交互由持久 UIScene 的 DOM 监听转发到 tryInteract()(见 UIScene) + + // listeners + this.on('stage', () => this.updateFX()); + this.on('negativeFlash', () => { this.negFrames = 2; }); + this.on('sleep', () => this.startSleep()); + this.on('festival', () => this.onFestival()); + this.on('fog', () => this.onFog()); + this.on('picnic', () => this.doPicnic()); + this.on('sad', () => this.headTurn()); + this.on('dusk', () => { if (Director.day === 6) { Director.flags.rain = true; this.ui.setRain(true); } }); + + this.cameras.main.fadeIn(400, 0, 0, 0); + if (this.mapKey === 'home' && data.spawn === 'start') this.ui.setRain(!!Director.flags.rain); + else this.ui.setRain(!!Director.flags.rain && OUTDOOR.has(this.mapKey)); + this.events.once('shutdown', () => this.cleanup()); + } + + private on(ev: string, fn: (...a: any[]) => void) { this.handlers[ev] = fn; Director.on(ev, fn); } + private cleanup() { for (const [ev, fn] of Object.entries(this.handlers)) Director.off(ev, fn as any); this.handlers = {}; } + + private spawnEntity(o: any) { + const id = prop(o, 'npc'); + const x = o.x + 8, y = o.y + 16; + if (id === 'bird1' || id === 'bird2') { + const s = this.add.sprite(x, y, 'birds', 0).setOrigin(0.5, 1).setDepth(40); s.play('bird-flap'); + this.ents.push({ id, kind: 'bird', sprite: s }); + } else if (id.startsWith('extra')) { + const k = parseInt(id.slice(5), 10); + const s = this.add.sprite(x, y, 'extras', (k - 1) * 2).setOrigin(0.5, 1).setDepth(40); s.play('extra' + k); + const tw = this.tweens.add({ targets: s, x: x + 24, duration: 2200, yoyo: true, repeat: -1, onYoyo: () => s.setFlipX(true), onRepeat: () => s.setFlipX(false) }); + this.ents.push({ id, kind: 'extra', sprite: s, anim: 'extra' + k, tween: tw }); + } else { + const s = this.add.sprite(x, y, NPCS[id]?.sprite || 'npc_afu', 0).setOrigin(0.5, 1).setDepth(40); + this.ents.push({ id, kind: 'named', sprite: s }); + } + } + + update(_t: number, dtMs: number) { + const dt = dtMs; + this.doorCd -= dt; + if (this.negFrames > 0) { this.neg.negative(); this.negFrames--; if (this.negFrames === 0) this.neg.reset(); } + else if (Director.stage === 4 && Math.random() < 0.00011 * dt) { this.negFrames = 2; } + if (Director.stage === 4 && !this.offFlash && Math.random() < 0.00017 * dt) { this.offFlash = true; this.time.delayedCall(100, () => (this.offFlash = false)); } + this.updateEntities(); + + const busy = this.ui.busy; + if (this.t14) { this.tickT14(dt); return; } + if (busy) { this.player.setVelocity(0); this.setIdleAnim(); return; } + + // movement + let vx = 0, vy = 0; + if (this.cursors.left.isDown || this.wasd.A.isDown) vx = -1; + else if (this.cursors.right.isDown || this.wasd.D.isDown) vx = 1; + if (this.cursors.up.isDown || this.wasd.W.isDown) vy = -1; + else if (this.cursors.down.isDown || this.wasd.S.isDown) vy = 1; + const moving = vx !== 0 || vy !== 0; + if (moving) { const l = Math.hypot(vx, vy); this.player.setVelocity((vx / l) * SPEED, (vy / l) * SPEED); } + else this.player.setVelocity(0); + if (vx < 0) this.facing = 'left'; else if (vx > 0) this.facing = 'right'; else if (vy < 0) this.facing = 'up'; else if (vy > 0) this.facing = 'down'; + if (moving) this.player.anims.play('walk-' + this.facing, true); else this.setIdleAnim(); + + this.detectTarget(); + this.checkDoors(); + this.tickIdle(dt, moving); + this.tickRain(dt); + this.tickForced(dt); + } + + private setIdleAnim() { + this.player.anims.stop(); + this.player.setFrame({ down: 0, left: 4, right: 8, up: 12 }[this.facing]!); + } + + private detectTarget() { + let best: any = null, bd = 24 * 24; + const test = (x: number, y: number, obj: any) => { const d = (x - this.player.x) ** 2 + (y - this.player.y) ** 2; if (d < bd) { bd = d; best = obj; } }; + for (const it of this.interacts) test(it.x, it.y, { type: 'interact', it }); + for (const e of this.ents) if (e.kind !== 'extra') test(e.sprite.x, e.sprite.y - 8, { type: 'npc', e }); + this.target = best; + this.ui.showPrompt(best ? '按 E' : null); + } + + tryInteract() { if (!this.ui.busy && !this.t14 && !this.trans && this.target) this.doInteract(); } + + private doInteract() { + if (this.target.type === 'npc') { + const e: Ent = this.target.e; + if (e.kind === 'bird') { this.ui.showDialogue([narr('双子鸟:叽——叽——(音乐好像就是从它们那儿飘出来的。)')]); return; } + this.ui.showDialogue(talkScript(e.id)); + } else { + const it = this.target.it; + const script = interactScript(it.kind, it.props); + if (script) this.ui.showDialogue(script); + } + } + + private checkDoors() { + if (this.doorCd > 0 || this.trans) return; + const pr = new Phaser.Geom.Rectangle(this.player.x - 5, this.player.y - 3, 10, 8); + for (const d of this.doors) { + if (Phaser.Geom.Rectangle.Overlaps(pr, d.rect)) { + if (this.forced) { Director.escapeCount++; this.ui.toast('(你从欢乐庆典的边缘走开了……)'); this.endForced(); } + this.gotoMap(d.target, d.dest, 200); + return; + } + } + } + + private gotoMap(map: string, spawn: string, fade = 150) { + if (this.trans) return; + this.trans = true; this.doorCd = 400; this.player.setVelocity(0); + this.cameras.main.fadeOut(fade, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => this.scene.restart({ map, spawn })); + } + + private tickIdle(dt: number, moving: boolean) { + if (moving || this.target) { this.idle = 0; return; } + this.idle += dt; + if (this.idle >= 10000) { this.idle = 0; Director.sadBehavior('发呆'); } + } + + private tickRain(dt: number) { + if (!Director.flags.rain) return; + if (OUTDOOR.has(this.mapKey)) { this.rainT += dt; if (this.rainT >= 10000) { this.rainT = 0; Director.sadBehavior('雨中久站'); } } + else this.rainT = 0; + } + + private tickForced(dt: number) { + if (this.forced) { this.forcedT += dt; if (this.forcedT >= 10000) { Director.setHappiness(50); this.ui.toast('欢乐庆典把你的幸福拉回了 50。'); this.endForced(); } return; } + if (Director.day < 7 && Director.lowHappySince && performance.now() - Director.lowHappySince >= 120000) this.startForced(); + } + private startForced() { this.forced = true; this.forcedT = 0; Director.lowHappySince = 0; this.ui.setForced(true); this.ui.toast('※ 欢乐庆典 ※ 大家都在等你开心起来!'); } + private endForced() { this.forced = false; this.ui.setForced(false); } + + private headTurn() { + let best: Ent | null = null, bd = 1e9; + for (const e of this.ents) if (e.kind === 'named') { const d = (e.sprite.x - this.player.x) ** 2 + (e.sprite.y - this.player.y) ** 2; if (d < bd) { bd = d; best = e; } } + if (!best) return; + best.sprite.setFlipX(this.player.x < best.sprite.x); + this.tweens.add({ targets: best.sprite, scaleX: 1.15, scaleY: 1.15, duration: 120, yoyo: true }); + } + + private updateEntities() { + for (const e of this.ents) { + if (e.kind === 'named') { + let expr = Director.npcExpression(e.id); + if (this.offFlash && Director.npcStage(e.id) === 4) expr = 'off'; + e.sprite.setFrame(EXPR[expr]); + if (this.t14 || Director.npcStage(e.id) >= 3) e.sprite.setFlipX(this.player.x < e.sprite.x); + else e.sprite.setFlipX(false); + } else if (e.kind === 'extra') { + if (Director.stage === 4 && e.id === 'extra1') { e.tween?.pause(); e.sprite.anims.stop(); e.sprite.setFlipX(this.player.x < e.sprite.x); } + else { e.tween?.resume(); if (!e.sprite.anims.isPlaying) e.sprite.play(e.anim!); } + } + } + } + + private updateFX() { + const p = Director.params; + this.sat.saturate((p.saturation - 1) * 1.5, false); + this.vig.strength = p.vignette; + this.vig.radius = 0.82 - p.vignette * 0.25; + } + + // ---------------- day flow ---------------- + private startSleep() { + if (Director.day >= 7 || this.trans) return; + this.trans = true; this.ui.busy = true; this.player.setVelocity(0); + this.cameras.main.fadeOut(600, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + const prev = Director.day; + this.ui.playDream(prev, () => { + activateDay(prev + 1); + const nd = Director.day; + // 先在持久的 UIScene 上解锁再重启本场景;delayedCall 会随本场景 restart 被清除,绝不能放到 restart 之后 + this.ui.busy = false; + Director.flags.rain = false; this.ui.setRain(false); + this.scene.restart({ map: 'home', spawn: 'start' }); + this.ui.toast(`Day ${nd}`); + }); + }); + } + + private onFestival() { + if (Director.day === 7) { + const e = evalEnding(false); + if (e) this.startEnding(e); + else if (Director.awareness >= 70 && Director.clues.size >= 6) this.ui.showDialogue([narr('广场很热闹……但村庄尽头那道雾,好像在等你。')]); + else this.startEnding('E2'); + } else if (Director.day === 6 && taskActive('T14')) this.startT14(); + else this.ui.showDialogue([narr('庆典的舞台,还没到时候呢。')]); + } + + private onFog() { + if (Director.day === 7) { + if (Director.escapeCount >= 3 && Director.happiness <= 10) this.startEnding('E4'); + else if (Director.awareness >= 70 && Director.clues.size >= 6) this.startEnding('E3'); + else this.ui.showDialogue([narr('雾还是雾。你还没准备好。')]); + } else this.ui.showDialogue([narr('雾在轻轻涌动,像在呼吸。')]); + } + + startEnding(id: string) { + if (Director.endingShown) return; + Director.endingShown = true; + this.cameras.main.fadeOut(500, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { this.scene.stop('UI'); this.scene.start('Ending', { id }); }); + } + + private startT14() { + this.t14 = true; this.t14t = 0; this.t14fade = false; + this.ui.busy = true; this.player.setVelocity(0); this.setIdleAnim(); + this.ui.toast('保持微笑……原地站好,别动。'); + this.cameras.main.zoomTo(1.9, 60000, 'Linear'); + } + private tickT14(dt: number) { + this.t14t += dt; + if (!this.t14fade && this.t14t >= 40000) { this.t14fade = true; Audio.fadeMaster(0, 20); } // 40→60s 渐隐至零 + if (this.t14t >= 60000) { + this.t14 = false; this.ui.busy = false; + Audio.restoreMaster(1.5); + this.cameras.main.zoomTo(1, 700); + completeTask('T14'); + } + } + + private doPicnic() { + this.ui.busy = true; this.player.setVelocity(0); + this.cameras.main.fadeOut(400, 0, 0, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.cameras.main.fadeIn(400, 0, 0, 0); + this.ui.showDialogue([ + narr('你在湖边铺好的野餐布上坐下。'), + narr('湖面倒映着你——'), + narr('倒影却慢了一拍才动。'), + narr('……大概是风吧。'), + ], () => { completeTask('T7'); this.ui.busy = false; }); + }); + } + + // debug hooks (called by UIScene) + debugTeleport(map: string) { this.gotoMap(map, 'sp_from_plaza'); } +} + +function prop(o: any, k: string): string { const p = (o.properties || []).find((x: any) => x.name === k); return p ? p.value : ''; } +function propsObj(o: any): any { const r: any = {}; for (const p of (o.properties || [])) r[p.name] = p.value; return r; } diff --git a/Opus4-8-xhigh/tools/build_assets.sh b/Opus4-8-xhigh/tools/build_assets.sh new file mode 100755 index 0000000..244e25d --- /dev/null +++ b/Opus4-8-xhigh/tools/build_assets.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Regenerates ALL art + maps from source scripts. Requires Aseprite + Tiled installed. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +ASE="${ASEPRITE:-/Applications/Aseprite.app/Contents/MacOS/aseprite}" +TILED="${TILED:-/Applications/Tiled.app/Contents/MacOS/Tiled}" +SPRITES="$ROOT/public/assets/sprites" +MAPS="$ROOT/public/assets/maps" +mkdir -p "$SPRITES" "$MAPS" + +echo "== [1/3] Aseprite: pixel art ==" +"$ASE" -b --script-param outdir="$SPRITES" --script "$ROOT/tools/gen_all.lua" + +echo "== [2/3] Node: Tiled .tmx sources ==" +node "$ROOT/tools/gen_maps.mjs" + +echo "== [3/3] Tiled: export .tmx -> .json ==" +for f in "$ROOT/tools/tmx/"*.tmx; do + name="$(basename "$f" .tmx)" + "$TILED" --export-map "$f" "$MAPS/$name.json" + echo " exported $name.json" +done +echo "== assets built ==" diff --git a/Opus4-8-xhigh/tools/gen_all.lua b/Opus4-8-xhigh/tools/gen_all.lua new file mode 100644 index 0000000..bf17797 --- /dev/null +++ b/Opus4-8-xhigh/tools/gen_all.lua @@ -0,0 +1,314 @@ +-- gen_all.lua : draws EVERY pixel asset for 蜜糖村 from scratch (Aseprite Lua API). +-- No external art, no imported palettes. Run: aseprite -b --script-param outdir=... --script gen_all.lua +local outdir = app.params["outdir"] +if not outdir then error("need --script-param outdir=") end + +local function pack(t) return app.pixelColor.rgba(t[1], t[2], t[3], t[4] or 255) end +local function px(img,x,y,c) if x>=0 and y>=0 and x tileset.png ============ +do + local s, img = newImg(128,144) + local function grassBase(ox,oy) rect(img,ox,oy,16,16,P.grass1); speck(img,ox,oy,16,16,P.grass2,5,0); speck(img,ox,oy,16,16,P.grass3,7,2) end + local T = {} + T[0]=function(ox,oy) grassBase(ox,oy) end + T[1]=function(ox,oy) grassBase(ox,oy); px(img,ox+4,oy+5,P.yellow2); px(img,ox+11,oy+10,P.pink1); px(img,ox+8,oy+13,P.pink1) end + T[2]=function(ox,oy) rect(img,ox,oy,16,16,P.path1); speck(img,ox,oy,16,16,P.path2,6,1); speck(img,ox,oy,16,16,P.path3,9,3); hline(img,ox,oy,16,P.path2); vline(img,ox,oy,16,P.path2) end + T[3]=function(ox,oy) grassBase(ox,oy); rect(img,ox+4,oy,8,16,P.path1); speck(img,ox+4,oy,8,16,P.path2,6,1) end + T[4]=function(ox,oy) rect(img,ox,oy,16,16,P.water1); speck(img,ox,oy,16,16,P.water2,5,0); hline(img,ox+2,oy+4,5,P.foam); hline(img,ox+9,oy+10,4,P.foam) end + T[5]=function(ox,oy) rect(img,ox,oy,16,16,P.water1); speck(img,ox,oy,16,16,P.water3,7,2); hline(img,ox+1,oy+2,14,P.foam); hline(img,ox+3,oy+8,10,P.water2) end + T[6]=function(ox,oy) grassBase(ox,oy); rect(img,ox+2,oy+6,12,8,P.canopy2); speck(img,ox+2,oy+6,12,8,P.canopy1,4,0); px(img,ox+4,oy+7,P.pink1); px(img,ox+9,oy+8,P.yellow2); px(img,ox+7,oy+11,P.pink2); px(img,ox+12,oy+11,P.pink1) end + T[7]=function(ox,oy) grassBase(ox,oy); rect(img,ox+2,oy+5,12,9,P.canopy2); speck(img,ox+2,oy+5,12,9,P.canopy1,3,0); for _,p in ipairs({{5,7},{9,7},{6,10},{11,9},{8,12}}) do px(img,ox+p[1],oy+p[2],P.red1); px(img,ox+p[1],oy+p[2]-1,P.red2) end end + T[8]=function(ox,oy) rect(img,ox+2,oy+1,12,12,P.canopy2); rect(img,ox+3,oy,10,2,P.canopy2); rect(img,ox+1,oy+3,14,8,P.canopy2); speck(img,ox,oy,16,14,P.canopy1,4,0); speck(img,ox,oy,16,14,P.canopy3,6,3) end + T[9]=function(ox,oy) grassBase(ox,oy); rect(img,ox+6,oy+2,4,13,P.trunk); vline(img,ox+6,oy+2,13,P.trunkD); px(img,ox+7,oy+7,P.trunkD) end + T[10]=function(ox,oy) rect(img,ox,oy,16,16,P.wall1); speck(img,ox,oy,16,16,P.wall2,7,2); hline(img,ox,oy+8,16,P.wallD); for i=0,3 do vline(img,ox+i*4+2,oy,8,P.wallD) end; for i=0,3 do vline(img,ox+i*4,oy+8,8,P.wallD) end end + T[11]=function(ox,oy) rect(img,ox,oy,16,16,P.roof1); for j=0,15 do hline(img,ox,oy+j,16,(j%3==0) and P.roof2 or P.roof1) end; speck(img,ox,oy,16,16,P.roof3,8,4) end + T[12]=function(ox,oy) rect(img,ox,oy,16,16,P.wall2); rect(img,ox+3,oy+2,10,14,P.door1); rect(img,ox+4,oy+3,8,13,P.door2); px(img,ox+10,oy+9,P.knob) end + T[13]=function(ox,oy) grassBase(ox,oy); rect(img,ox+1,oy+4,2,10,P.wood2); rect(img,ox+7,oy+4,2,10,P.wood2); rect(img,ox+13,oy+4,2,10,P.wood2); hline(img,ox,oy+6,16,P.wood1); hline(img,ox,oy+10,16,P.wood1) end + T[14]=function(ox,oy) rect(img,ox,oy,16,16,P.soilD); rect(img,ox+1,oy+1,14,14,P.soil); for _,p in ipairs({{3,3},{8,4},{12,6},{5,9},{10,11},{3,12}}) do px(img,ox+p[1],oy+p[2],P.pink1); px(img,ox+p[1]+1,oy+p[2],P.pink2); px(img,ox+p[1],oy+p[2]-1,P.yellow2) end end + T[15]=function(ox,oy) grassBase(ox,oy); rect(img,ox+2,oy+4,12,11,P.path2); rect(img,ox+3,oy+5,10,9,P.path1); rect(img,ox+5,oy+7,6,6,P.black); rect(img,ox+5,oy+7,6,3,P.waterDeep); vline(img,ox+2,oy+1,5,P.wood2); vline(img,ox+13,oy+1,5,P.wood2); hline(img,ox+2,oy+1,12,P.wood1) end + T[16]=function(ox,oy) rect(img,ox,oy,16,16,{225,228,232,235}); speck(img,ox,oy,16,16,{240,242,245,255},4,0); speck(img,ox,oy,16,16,{205,210,216,220},5,2) end + T[17]=function(ox,oy) rect(img,ox+1,oy+2,14,11,P.wood2); rect(img,ox+2,oy+3,12,9,P.paper); px(img,ox+4,oy+5,P.ink); px(img,ox+6,oy+5,P.ink); px(img,ox+8,oy+5,P.ink); hline(img,ox+4,oy+7,7,P.ink); hline(img,ox+4,oy+9,5,P.ink); rect(img,ox+2,oy+13,2,3,P.wood1); rect(img,ox+12,oy+13,2,3,P.wood1) end + T[18]=function(ox,oy) grassBase(ox,oy); rect(img,ox+7,oy+6,2,9,P.wood2); rect(img,ox+4,oy+2,8,6,P.red1); rect(img,ox+4,oy+2,8,2,P.red2); hline(img,ox+5,oy+5,6,P.black); rect(img,ox+11,oy+1,1,4,P.red2) end + T[19]=function(ox,oy) grassBase(ox,oy); for j=0,9 do for i=0,13 do local red=((i//2)+(j//2))%2==0; px(img,ox+1+i,oy+3+j, red and P.red1 or P.clothW) end end end + T[20]=function(ox,oy) grassBase(ox,oy); rect(img,ox+1,oy+7,14,3,P.wood1); rect(img,ox+1,oy+4,14,2,P.wood2); vline(img,ox+2,oy+9,5,P.wood2); vline(img,ox+13,oy+9,5,P.wood2) end + T[21]=function(ox,oy) grassBase(ox,oy); rect(img,ox+7,oy+4,2,11,P.metalD); rect(img,ox+5,oy+1,6,4,P.yellow3); rect(img,ox+6,oy+2,4,2,P.yellow2) end + T[22]=function(ox,oy) grassBase(ox,oy); rect(img,ox+4,oy+8,8,6,P.canopy2); speck(img,ox+4,oy+8,8,6,P.canopy1,3,0) end + T[23]=function(ox,oy) rect(img,ox,oy,16,16,P.wall1); speck(img,ox,oy,16,16,P.wall2,4,1); speck(img,ox,oy,16,16,P.wallD,9,3) end + T[24]=function(ox,oy) rect(img,ox,oy,16,16,P.ifloor1); for j=0,3 do hline(img,ox,oy+j*4,16,P.ifloor2) end; speck(img,ox,oy,16,16,P.ifloor2,11,4) end + T[25]=function(ox,oy) rect(img,ox,oy,16,16,P.ifloor1); for i=0,3 do vline(img,ox+i*4,oy,16,P.ifloor2) end end + T[26]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); speck(img,ox,oy,16,16,P.iwall2,7,3); hline(img,ox,oy,16,P.iwall2) end + T[27]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox,oy,16,5,P.iwallTrim); hline(img,ox,oy+5,16,P.iwall2) end + T[28]=function(ox,oy) rect(img,ox,oy,16,16,P.wood2); rect(img,ox+1,oy+4,14,11,P.pink1); rect(img,ox+1,oy+4,14,4,P.clothW); rect(img,ox+2,oy+4,5,3,P.clothW); hline(img,ox+1,oy+9,14,P.pinkD) end + T[29]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+3,oy+2,10,12,P.paper); hline(img,ox+3,oy+2,10,P.red1); px(img,ox+5,oy+6,P.ink); px(img,ox+7,oy+6,P.ink); px(img,ox+9,oy+6,P.ink); hline(img,ox+5,oy+9,6,P.ink); px(img,ox+7,oy+11,P.red2); px(img,ox+8,oy+11,P.red2) end + T[30]=function(ox,oy) rect(img,ox,oy+3,16,12,P.wood2); rect(img,ox,oy+3,16,3,P.wood3); hline(img,ox,oy+9,16,P.wood1); rect(img,ox+2,oy+11,3,3,P.wood1); rect(img,ox+11,oy+11,3,3,P.wood1) end + T[31]=function(ox,oy) rect(img,ox,oy+1,16,14,P.metalD); rect(img,ox+2,oy+3,12,8,P.black); rect(img,ox+3,oy+7,10,4,P.orange1); rect(img,ox+4,oy+8,8,2,P.yellow2); hline(img,ox+2,oy+2,12,P.metal); px(img,ox+11,oy+4,P.red1) end + T[32]=function(ox,oy) rect(img,ox,oy,16,16,P.wood1); rect(img,ox+2,oy+2,12,12,P.paper); hline(img,ox+4,oy+5,8,P.ink); hline(img,ox+4,oy+7,8,P.ink); hline(img,ox+4,oy+9,5,P.ink); px(img,ox+12,oy+11,P.red1) end + T[33]=function(ox,oy) rect(img,ox,oy,16,16,P.wood2); hline(img,ox,oy+5,16,P.wood1); hline(img,ox,oy+10,16,P.wood1); rect(img,ox+2,oy+1,3,3,P.orange1); rect(img,ox+7,oy+1,3,3,P.red1); rect(img,ox+11,oy+1,3,3,P.sky); rect(img,ox+2,oy+6,3,3,P.pink1); rect(img,ox+7,oy+6,3,3,P.yellow2) end + T[34]=function(ox,oy) rect(img,ox,oy,16,16,P.wood2); hline(img,ox,oy+8,16,P.wood1); rect(img,ox+3,oy+2,4,5,P.orange1); rect(img,ox+3,oy+2,4,2,P.orange2); rect(img,ox+9,oy+2,4,5,P.orange1); rect(img,ox+9,oy+2,4,2,P.orange2); rect(img,ox+3,oy+10,4,5,P.orange1); rect(img,ox+9,oy+10,4,5,P.orange1) end + T[35]=function(ox,oy) rect(img,ox+1,oy+4,14,4,P.wood3); rect(img,ox+1,oy+4,14,2,P.wood1); vline(img,ox+2,oy+8,6,P.wood2); vline(img,ox+13,oy+8,6,P.wood2) end + T[36]=function(ox,oy) rect(img,ox+4,oy+2,8,3,P.wood2); rect(img,ox+4,oy+7,8,3,P.wood3); vline(img,ox+4,oy+5,2,P.wood2); vline(img,ox+5,oy+10,4,P.wood2); vline(img,ox+10,oy+10,4,P.wood2) end + T[37]=function(ox,oy) rect(img,ox+1,oy+5,14,4,P.wood3); rect(img,ox+3,oy,10,7,P.paper); hline(img,ox+3,oy,10,P.water3); hline(img,ox+4,oy+2,8,P.ink); hline(img,ox+4,oy+4,5,P.ink); rect(img,ox+9,oy+1,4,3,P.wood3) end + T[38]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+2,oy+2,12,10,P.glass); rect(img,ox+2,oy+2,12,4,P.sky); vline(img,ox+8,oy+2,10,P.iwall2); hline(img,ox+2,oy+7,12,P.iwall2); rect(img,ox+1,oy+1,14,1,P.iwallTrim) end + T[39]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+3,oy+2,10,14,P.door1); rect(img,ox+4,oy+3,8,13,P.door2); px(img,ox+10,oy+9,P.knob) end + T[40]=function(ox,oy) rect(img,ox,oy,16,16,P.wwall); speck(img,ox,oy,16,16,P.wwallD,9,4); hline(img,ox,oy+11,16,P.wwallD) end + T[41]=function(ox,oy) rect(img,ox,oy,16,16,P.wfloor); hline(img,ox,oy,16,P.wline); vline(img,ox,oy,16,P.wline); speck(img,ox,oy,16,16,P.wfloorL,13,3) end + T[42]=function(ox,oy) rect(img,ox,oy,16,16,P.wfloorL); hline(img,ox+8,oy,8,P.wline); vline(img,ox,oy+8,8,P.wline) end + T[43]=function(ox,oy) rect(img,ox,oy,16,16,P.wfloor); rect(img,ox+1,oy+3,14,11,P.wbed); rect(img,ox+1,oy+3,14,3,P.wdark); hline(img,ox+1,oy+9,14,P.wline); rect(img,ox,oy+2,2,13,P.metal); rect(img,ox+14,oy+2,2,13,P.metal) end + T[44]=function(ox,oy) rect(img,ox,oy,16,16,P.wwall); rect(img,ox+3,oy+1,10,15,P.wfloorL); rect(img,ox+4,oy+2,8,14,P.wfloor); px(img,ox+10,oy+9,P.metalD) end + T[45]=function(ox,oy) rect(img,ox,oy,16,16,P.wwall); rect(img,ox+2,oy+2,12,10,{212,224,230}); vline(img,ox+8,oy+2,10,P.wdark); hline(img,ox+2,oy+7,12,P.wdark) end + T[46]=function(ox,oy) for i=0,15 do local j=math.floor(2*math.sin(i*0.9)+6); px(img,ox+i,oy+j,P.pink2); px(img,ox+i,oy+j+1,P.pink1) end; px(img,ox+3,oy+3,P.yellow2); px(img,ox+11,oy+9,P.sky) end + T[47]=function(ox,oy) rect(img,ox,oy+6,16,10,P.wood2); rect(img,ox,oy+6,16,2,P.wood3); rect(img,ox+2,oy,12,6,P.red1); for i=0,3 do px(img,ox+2+i*4,oy+5,P.yellow2) end end + T[48]=function(ox,oy) grassBase(ox,oy); for _,p in ipairs({{4,6},{9,4},{11,10},{6,11}}) do px(img,ox+p[1],oy+p[2],P.red1); px(img,ox+p[1]+1,oy+p[2],P.red2); px(img,ox+p[1],oy+p[2]+1,P.yellow2) end end + T[49]=function(ox,oy) grassBase(ox,oy); for _,p in ipairs({{5,5},{10,7},{7,11},{12,12}}) do px(img,ox+p[1],oy+p[2],P.sky); px(img,ox+p[1]+1,oy+p[2],P.water2); px(img,ox+p[1],oy+p[2]+1,P.yellow2) end end + T[50]=function(ox,oy) grassBase(ox,oy); rect(img,ox+6,oy+9,3,4,P.wall1); rect(img,ox+4,oy+6,7,4,P.red1); px(img,ox+5,oy+7,P.yellow3); px(img,ox+8,oy+8,P.yellow3) end + T[51]=function(ox,oy) grassBase(ox,oy); rect(img,ox+3,oy+7,10,6,P.gray); rect(img,ox+3,oy+7,10,2,{150,150,154}); px(img,ox+5,oy+10,P.metalD) end + T[52]=function(ox,oy) grassBase(ox,oy); rect(img,ox+4,oy+7,8,6,P.trunk); rect(img,ox+5,oy+6,6,2,P.wood3); px(img,ox+7,oy+7,P.trunkD); px(img,ox+8,oy+7,P.trunkD) end + T[53]=function(ox,oy) rect(img,ox,oy,16,16,P.water1); speck(img,ox,oy,16,16,P.water2,5,0); rect(img,ox+4,oy+5,8,6,P.canopy1); px(img,ox+7,oy+7,P.pink1); px(img,ox+8,oy+7,P.pink2) end + T[54]=function(ox,oy) rect(img,ox,oy,16,16,P.water1); speck(img,ox,oy,16,16,P.water2,5,0); vline(img,ox+5,oy+2,10,P.canopy2); vline(img,ox+9,oy+1,11,P.canopy2); rect(img,ox+4,oy+1,3,3,P.trunk); rect(img,ox+8,oy,3,3,P.trunk) end + T[55]=function(ox,oy) rect(img,ox+4,oy+4,8,10,P.orange1); rect(img,ox+4,oy+4,8,3,P.orange2); rect(img,ox+5,oy+1,6,3,P.wood2); rect(img,ox+6,oy+8,4,4,P.yellow2) end + T[56]=function(ox,oy) rect(img,ox+1,oy+2,14,13,P.wood2); rect(img,ox+2,oy+3,12,11,P.wood1); vline(img,ox+8,oy+3,11,P.wood2); hline(img,ox+2,oy+8,12,P.wood2) end + T[57]=function(ox,oy) rect(img,ox+3,oy+1,10,14,P.wood1); rect(img,ox+3,oy+1,10,2,P.wood2); hline(img,ox+3,oy+5,10,P.wood2); hline(img,ox+3,oy+10,10,P.wood2); vline(img,ox+7,oy+1,14,P.wood3) end + T[58]=function(ox,oy) rect(img,ox,oy,16,16,P.wood2); hline(img,ox,oy+8,16,P.wood1); for i=0,4 do vline(img,ox+1+i*3,oy+1,6,({P.red1,P.sky,P.canopy1,P.orange1,P.pink2})[i+1]) end; for i=0,4 do vline(img,ox+1+i*3,oy+9,6,({P.canopy1,P.pink2,P.red1,P.sky,P.yellow2})[i+1]) end end + T[59]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+4,oy+3,8,8,P.wall1); rect(img,ox+5,oy+4,6,6,P.paper); vline(img,ox+8,oy+5,3,P.ink); hline(img,ox+8,oy+7,3,P.ink) end + T[60]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+2,oy+2,12,10,P.wood2); rect(img,ox+3,oy+3,10,8,P.sky); rect(img,ox+3,oy+8,10,3,P.canopy1); px(img,ox+10,oy+5,P.yellow3) end + T[61]=function(ox,oy) rect(img,ox+5,oy+10,6,5,P.orange2); rect(img,ox+4,oy+9,8,2,P.orange1); rect(img,ox+5,oy+4,6,6,P.canopy2); speck(img,ox+5,oy+4,6,6,P.canopy1,3,0) end + T[62]=function(ox,oy) rect(img,ox,oy,16,16,P.ifloor1); rect(img,ox+2,oy+3,12,10,P.pink2); rect(img,ox+4,oy+5,8,6,P.pink1); px(img,ox+7,oy+7,P.yellow2); px(img,ox+8,oy+8,P.yellow2) end + T[63]=function(ox,oy) rect(img,ox,oy,16,16,P.grass2); speck(img,ox,oy,16,16,P.canopy2,5,0); speck(img,ox,oy,16,16,P.grass1,7,2) end + T[64]=function(ox,oy) grassBase(ox,oy); px(img,ox+3,oy+3,P.orange1); px(img,ox+7,oy+6,P.pink2); px(img,ox+12,oy+4,P.yellow2); px(img,ox+9,oy+12,P.red1); px(img,ox+4,oy+11,P.sky) end + T[65]=function(ox,oy) rect(img,ox,oy,16,16,P.waterDeep); speck(img,ox,oy,16,16,P.water3,5,0); hline(img,ox+3,oy+6,8,P.water2) end + T[66]=function(ox,oy) rect(img,ox,oy,16,16,P.path1); speck(img,ox,oy,16,16,P.path2,6,1); rect(img,ox,oy,6,6,P.grass1); speck(img,ox,oy,6,6,P.grass2,4,0) end + T[67]=function(ox,oy) rect(img,ox,oy,16,16,P.roof1); rect(img,ox,oy+11,16,5,P.roof2); hline(img,ox,oy+11,16,P.wall1); speck(img,ox,oy,16,11,P.roof3,8,4) end + T[68]=function(ox,oy) rect(img,ox,oy,16,16,P.wall1); speck(img,ox,oy,16,16,P.wall2,7,2); rect(img,ox+3,oy+3,10,9,P.glass); rect(img,ox+3,oy+3,10,4,P.sky); vline(img,ox+8,oy+3,9,P.wallD); hline(img,ox+3,oy+7,10,P.wallD); rect(img,ox+2,oy+2,12,1,P.wallD) end + T[69]=function(ox,oy) rect(img,ox,oy,16,16,P.sky); rect(img,ox+7,oy+4,3,3,P.yellow2); px(img,ox+8,oy+3,P.yellow3); px(img,ox+8,oy+8,P.yellow3); px(img,ox+4,oy+6,P.yellow3); px(img,ox+12,oy+6,P.yellow3) end + T[70]=function(ox,oy) grassBase(ox,oy); vline(img,ox+7,oy+8,5,P.canopy2); rect(img,ox+6,oy+5,4,4,P.clothW); px(img,ox+7,oy+6,P.yellow2) end + T[71]=function(ox,oy) rect(img,ox,oy,16,16,P.path2); for j=0,3 do for i=0,3 do local c=((i+j)%2==0) and P.path1 or P.path3; rect(img,ox+i*4,oy+j*4,4,4,c) end end end + for idx=0,71 do T[idx]((idx%8)*16,(idx//8)*16) end + save(s,"tileset.png"); s:close(); print("tileset done") +end + +-- ============ PLAYER walk sheet 64x96 (4 dir x 4 frame) ============ +do + local skin=P.skin; local skinD=P.skinD; local hair=P.hair + local shirt={92,152,212}; local shirtD={64,116,176} + local pants={78,96,128}; local pantsD={56,72,102}; local shoe={70,58,58} + local s, img = newImg(64,96) + local function person(ox,oy,dir,frame) + local sw=0; if frame==1 then sw=1 elseif frame==3 then sw=-1 end + rect(img,ox+5,oy+10,6,7,shirt); hline(img,ox+5,oy+10,6,shirtD); vline(img,ox+5,oy+10,7,shirtD) + vline(img,ox+4,oy+11,4,skin); vline(img,ox+11,oy+11,4,skin) + rect(img,ox+5,oy+3,6,6,skin); hline(img,ox+5,oy+8,6,skinD) + rect(img,ox+4,oy+1,8,3,hair); px(img,ox+4,oy+4,hair); px(img,ox+11,oy+4,hair) + local lx,rx=ox+6,ox+9 + if dir==0 then + px(img,ox+6,oy+5,P.black); px(img,ox+9,oy+5,P.black); px(img,ox+7,oy+7,skinD) + rect(img,lx,oy+17,2,4,pants); rect(img,rx,oy+17,2,4,pants) + rect(img,lx,oy+21+(sw>0 and 1 or 0),2,1,shoe); rect(img,rx,oy+21+(sw<0 and 1 or 0),2,1,shoe) + elseif dir==3 then + rect(img,ox+4,oy+1,8,5,hair) + rect(img,lx,oy+17,2,4,pants); rect(img,rx,oy+17,2,4,pants) + rect(img,lx,oy+21+(sw>0 and 1 or 0),2,1,shoe); rect(img,rx,oy+21+(sw<0 and 1 or 0),2,1,shoe) + else + px(img,ox+9,oy+5,P.black); px(img,ox+11,oy+6,skinD); rect(img,ox+4,oy+1,7,3,hair) + rect(img,ox+6+sw,oy+17,2,4,pants); rect(img,ox+8-sw,oy+17,2,4,pantsD) + rect(img,ox+6+sw,oy+21,2,1,shoe); rect(img,ox+8-sw,oy+21,2,1,shoe) + end + end + for f=0,3 do person(f*16,0,0,f); person(f*16,72,3,f); person(f*16,48,2,f) end + for f=0,3 do for yy=0,23 do for xx=0,15 do + img:drawPixel(f*16+(15-xx), 24+yy, img:getPixel(f*16+xx, 48+yy)) + end end end + save(s,"player.png"); s:close(); print("player done") +end + +-- ============ PLAYER PORTRAITS 64x32 (smile / real face) ============ +do + local s, img = newImg(64,32) + local function drawPortrait(ox,tired) + local sk = tired and {216,199,185} or P.skin + local skd = tired and {186,170,158} or P.skinD + local hr = tired and {104,98,92} or P.hair + rect(img,ox+8,4,16,20,sk) + rect(img,ox+7,3,18,7,hr); px(img,ox+7,10,hr); px(img,ox+24,10,hr) + if tired then + rect(img,ox+11,13,2,2,P.black); rect(img,ox+19,13,2,2,P.black) + hline(img,ox+10,16,4,skd); hline(img,ox+18,16,4,skd) + hline(img,ox+13,20,6,skd); px(img,ox+12,21,skd); px(img,ox+19,21,skd) + else + rect(img,ox+11,12,2,3,P.black); rect(img,ox+19,12,2,3,P.black); px(img,ox+12,12,P.glass) + px(img,ox+12,19,skd); hline(img,ox+13,20,6,skd); px(img,ox+19,19,skd) + px(img,ox+14,13,{255,150,160}); px(img,ox+18,13,{255,150,160}) + end + rect(img,ox+15,16,2,2,skd) + end + drawPortrait(0,false); drawPortrait(32,true) + save(s,"portraits.png"); s:close(); print("portraits done") +end + +-- ============ NPCS 48x24 (smile/flat/off) ============ +local function drawAnimal(img, ox, expr, spec) + local body=spec.body; local bodyD=spec.bodyD; local muzzle=spec.muzzle or {250,244,232} + -- torso + rect(img,ox+3,9,10,12,body); rect(img,ox+3,9,10,2,bodyD) + rect(img,ox+5,13,6,7,muzzle) -- belly + -- feet + rect(img,ox+4,21,3,2,bodyD); rect(img,ox+9,21,3,2,bodyD) + -- head + rect(img,ox+4,2,8,8,body) + rect(img,ox+5,5,6,5,muzzle) -- face patch + -- ears + if spec.ears=="floppy" then + rect(img,ox+2,3,2,5,bodyD); rect(img,ox+12,3,2,5,bodyD) + elseif spec.ears=="long" then + rect(img,ox+4,-0+0,2,4,body); rect(img,ox+10,0,2,4,body); px(img,ox+4,1,P.pink1); px(img,ox+10,1,P.pink1) + rect(img,ox+4,0,2,3,body); rect(img,ox+10,0,2,3,body) + elseif spec.ears=="round" then + rect(img,ox+3,1,3,3,body); rect(img,ox+10,1,3,3,body); px(img,ox+4,2,bodyD); px(img,ox+11,2,bodyD) + elseif spec.ears=="pointy" then + px(img,ox+4,1,body); rect(img,ox+3,2,3,2,body); px(img,ox+11,1,body); rect(img,ox+10,2,3,2,body); px(img,ox+4,2,P.pink1); px(img,ox+11,2,P.pink1) + elseif spec.ears=="antler" then + px(img,ox+4,0,P.wood2); px(img,ox+4,1,P.wood2); px(img,ox+3,0,P.wood2); px(img,ox+11,0,P.wood2); px(img,ox+11,1,P.wood2); px(img,ox+12,0,P.wood2) + end + -- eyes (base y=6). off lowers RIGHT eye by 1px. + local ey=6 + px(img,ox+6,ey,P.black) + px(img,ox+9, expr=="off" and ey+1 or ey, P.black) + -- nose + px(img,ox+7,8,bodyD); px(img,ox+8,8,bodyD) + -- mouth: smile curve / flat line / off = smile + 1px corner + if expr=="flat" then + hline(img,ox+6,10,4,bodyD) + else + px(img,ox+5,9,bodyD); hline(img,ox+6,10,4,bodyD); px(img,ox+10,9,bodyD) + if expr=="off" then px(img,ox+11,9,bodyD) end -- 1px extra corner + end + -- extras + if spec.whiskers then hline(img,ox+1,7,3,P.gray); hline(img,ox+12,7,3,P.gray) end + if spec.glasses then rect(img,ox+5,5,3,3,P.metal); rect(img,ox+8,5,3,3,P.metal); px(img,ox+6,6,P.glass); px(img,ox+9, expr=="off" and 7 or 6,P.glass) end + if spec.shell then rect(img,ox+3,10,10,9,{86,140,80}); rect(img,ox+5,12,6,5,{110,168,100}); px(img,ox+7,14,{70,116,66}); px(img,ox+8,15,{70,116,66}); rect(img,ox+5,13,6,7,muzzle,{}); rect(img,ox+3,10,10,9,{86,140,80}); px(img,ox+6,13,{70,116,66}); px(img,ox+9,16,{70,116,66}) end + if spec.coat then rect(img,ox+3,11,10,10,P.clothW); rect(img,ox+3,11,10,2,{230,230,236}); rect(img,ox+7,13,2,7,P.red1); rect(img,ox+6,15,4,2,P.red1); rect(img,ox+4,20,3,1,bodyD); rect(img,ox+9,20,3,1,bodyD) end + if spec.head then rect(img,ox+4,0,8,3,spec.head) end +end + +do + local specs = { + afu ={body={168,120,72}, bodyD={128,90,52}, ears="floppy"}, -- 狗 村长 + yuan ={body={244,236,222}, bodyD={206,196,180}, ears="long"}, -- 兔 面包师 + dabao ={body={128,90,60}, bodyD={98,68,44}, ears="round"}, -- 熊 邮差 + mimi ={body={236,152,80}, bodyD={200,120,56}, ears="pointy", whiskers=true}, -- 猫 杂货 + fu ={body={216,176,124}, bodyD={176,138,92}, ears="antler", coat=true}, -- 鹿 医生 + gui ={body={120,180,110}, bodyD={90,140,84}, ears=nil, shell=true, glasses=true}, -- 龟 老者 + afu2 =nil, + } + local order = {"afu","yuan","dabao","mimi","fu","gui"} + for _,name in ipairs(order) do + local s, img = newImg(48,24) + local exprs = {"smile","flat","off"} + for i,e in ipairs(exprs) do drawAnimal(img,(i-1)*16,e,specs[name]) end + save(s,"npc_"..name..".png"); s:close() + end + print("npcs done") +end + +-- ============ TWIN BIRDS 32x24 (beak closed / open) ============ +do + local s, img = newImg(32,24) + local function drawBird(ox,open) + local body={110,170,220}; local bodyD={78,132,186} + rect(img,ox+4,6,8,10,body); rect(img,ox+4,6,8,2,{150,200,240}) + rect(img,ox+3,9,3,5,bodyD) -- wing + rect(img,ox+5,3,6,5,body) -- head + px(img,ox+7,5,P.black); px(img,ox+8,5,P.black) -- eyes + -- beak + if open then + px(img,ox+11,6,P.orange2); px(img,ox+12,6,P.orange2); px(img,ox+11,8,P.orange2); px(img,ox+12,8,P.orange2); px(img,ox+13,7,P.orange1) + else + px(img,ox+11,7,P.orange2); px(img,ox+12,7,P.orange2); px(img,ox+13,7,P.orange1) + end + -- feet + px(img,ox+6,16,P.orange2); px(img,ox+9,16,P.orange2) + -- little tuft + px(img,ox+7,2,bodyD); px(img,ox+8,2,body) + end + drawBird(0,false); drawBird(16,true) + save(s,"birds.png"); s:close(); print("birds done") +end + +-- ============ EXTRAS (群演) 32x80 = 5 critters x 2 frames, 16x16 ============ +do + local s, img = newImg(32,80) + local function critter(ox,oy,f,spec) + local bob = (f==1) and 1 or 0 + local b=spec.body; local bd=spec.bodyD + rect(img,ox+4,oy+7+bob,8,6,b); rect(img,ox+4,oy+7+bob,8,2,bd) -- body + rect(img,ox+5,oy+3+bob,6,5,b) -- head + px(img,ox+6,oy+5+bob,P.black); px(img,ox+9,oy+5+bob,P.black) + if spec.tail=="bushy" then rect(img,ox+11,oy+4+bob,3,7,b); rect(img,ox+12,oy+3+bob,2,3,bd) end + if spec.tail=="thin" then vline(img,ox+12,oy+9+bob,3,bd) end + if spec.spikes then for i=0,4 do px(img,ox+4+i*2,oy+6+bob,bd); px(img,ox+5+i*2,oy+8+bob,bd) end end + if spec.ears=="mouse" then px(img,ox+5,oy+2+bob,b); px(img,ox+10,oy+2+bob,b); rect(img,ox+4,oy+2+bob,2,2,b); rect(img,ox+10,oy+2+bob,2,2,b) end + if spec.ears=="pointy" then px(img,ox+5,oy+2+bob,b); px(img,ox+10,oy+2+bob,b) end + rect(img,ox+5,oy+13+bob,2,2,bd); rect(img,ox+9,oy+13+bob,2,2,bd) -- feet + end + local specs = { + {body={214,142,72}, bodyD={176,108,52}, tail="bushy", ears="pointy"}, -- squirrel + {body={150,120,90}, bodyD={110,86,64}, spikes=true, ears="pointy"}, -- hedgehog + {body={200,160,100}, bodyD={160,120,70}, tail="bushy", ears="pointy"}, -- squirrel2 + {body={188,190,196}, bodyD={150,152,158},tail="thin", ears="mouse"}, -- mouse + {body={230,200,120}, bodyD={196,164,90}, tail="thin", ears="pointy"}, -- chipmunk + } + for k=0,4 do critter(0,k*16,0,specs[k+1]); critter(16,k*16,1,specs[k+1]) end + save(s,"extras.png"); s:close(); print("extras done") +end + +-- ============ SPARROW (E3 real bird) 16x16 gray ============ +do + local s, img = newImg(16,16) + local g1={128,124,118}; local g2={98,94,90}; local g3={160,156,150}; local beak={90,80,60} + rect(img,4,6,8,6,g1); rect(img,4,6,8,2,g3) + rect(img,3,8,3,4,g2) -- wing + rect(img,10,4,4,4,g1) -- head + px(img,12,5,P.black); px(img,13,6,beak); px(img,14,6,beak) + px(img,5,12,beak); px(img,8,12,beak) -- feet + speck(img,4,6,8,6,g2,3,0) + save(s,"sparrow.png"); s:close(); print("sparrow done") +end + +print("ALL ASSETS DONE") diff --git a/Opus4-8-xhigh/tools/gen_maps.mjs b/Opus4-8-xhigh/tools/gen_maps.mjs new file mode 100644 index 0000000..9e0b91a --- /dev/null +++ b/Opus4-8-xhigh/tools/gen_maps.mjs @@ -0,0 +1,330 @@ +// gen_maps.mjs : builds all 10 Tiled maps as .tmx (embedded tileset), for export via Tiled CLI. +// Original layouts, no external tilesets. Run with node; then Tiled exports each .tmx -> .json. +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dir, '..'); +const TMX_DIR = resolve(__dir, 'tmx'); +const PNG = resolve(ROOT, 'public/assets/sprites/tileset.png'); +mkdirSync(TMX_DIR, { recursive: true }); + +// tile index (0-based); GID = idx+1 +const G = (i) => i + 1; +const t = { + grass: G(0), grassF: G(1), path: G(2), pathEdge: G(3), water: G(4), waterL: G(5), + flowerBush: G(6), berry: G(7), treeTop: G(8), treeTrunk: G(9), wall: G(10), roof: G(11), + door: G(12), fence: G(13), flowerbed: G(14), well: G(15), fog: G(16), board: G(17), + mailbox: G(18), picnic: G(19), bench: G(20), lamp: G(21), bushS: G(22), sand: G(23), + wfloor: G(24), wfloorB: G(25), iwall: G(26), iwallTop: G(27), bed: G(28), calendar: G(29), + counter: G(30), oven: G(31), recipe: G(32), shelf: G(33), shelfItem: G(34), table: G(35), + chair: G(36), clinicForm: G(37), windowI: G(38), doorI: G(39), wwall: G(40), wfloorW: G(41), + wfloorWb: G(42), hbed: G(43), wdoor: G(44), wwindow: G(45), ribbon: G(46), stage: G(47), + flowersRed: G(48), flowersBlue: G(49), mushroom: G(50), rock: G(51), stump: G(52), + lilypad: G(53), reed: G(54), honey: G(55), crate: G(56), barrel: G(57), bookshelf: G(58), + clock: G(59), painting: G(60), plantPot: G(61), rug: G(62), darkGrass: G(63), grassF2: G(64), + waterDeep: G(65), pathCorner: G(66), roofEdge: G(67), houseWindow: G(68), star: G(69), + snowdrop: G(70), cobble: G(71), +}; + +function M(w, h, base) { + return { + w, h, + ground: new Array(w * h).fill(base), + obs: new Array(w * h).fill(0), + objects: [], + _oid: 1, + }; +} +const idx = (m, c, r) => r * m.w + c; +function setG(m, c, r, g) { if (c >= 0 && c < m.w && r >= 0 && r < m.h) m.ground[idx(m, c, r)] = g; } +function setO(m, c, r, g) { if (c >= 0 && c < m.w && r >= 0 && r < m.h) m.obs[idx(m, c, r)] = g; } +function rectG(m, c, r, w, h, g) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) setG(m, c + i, r + j, g); } +function rectO(m, c, r, w, h, g) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) setO(m, c + i, r + j, g); } +function borderO(m, g) { + for (let c = 0; c < m.w; c++) { setO(m, c, 0, g); setO(m, c, m.h - 1, g); } + for (let r = 0; r < m.h; r++) { setO(m, 0, r, g); setO(m, m.w - 1, r, g); } +} +function obj(m, name, type, c, r, props = {}, wTiles = 1, hTiles = 1) { + m.objects.push({ id: m._oid++, name, type, x: c * 16, y: r * 16, w: wTiles * 16, h: hTiles * 16, props }); +} + +// ---- interior room helper (wood floor + walls) ---- +function room(m) { + rectG(m, 0, 0, m.w, m.h, t.wfloor); + for (let c = 0; c < m.w; c++) { setO(m, c, 0, t.iwallTop); setO(m, c, m.h - 1, t.iwall); } + for (let r = 0; r < m.h; r++) { setO(m, 0, r, t.iwall); setO(m, m.w - 1, r, t.iwall); } +} +function exitDoor(m, c, r, target, dest) { setG(m, c, r, t.doorI); setO(m, c, r, 0); obj(m, 'door_' + target, 'door', c, r, { target, dest }); } + +const maps = {}; + +// ============ 1. HOME 12x8 ============ +{ + const m = M(12, 8, t.wfloor); room(m); + setG(m, 5, 1, t.rug); setG(m, 6, 1, t.rug); + setO(m, 1, 1, t.bed); setO(m, 1, 2, t.bed); // bed (2 tiles) + setG(m, 2, 0, t.calendar); // 床头日历 on top wall + setO(m, 9, 1, t.bookshelf); setO(m, 8, 1, t.plantPot); + setO(m, 8, 5, t.table); setO(m, 9, 5, t.chair); + setG(m, 4, 0, t.windowI); setG(m, 7, 0, t.windowI); + exitDoor(m, 6, 7, 'plaza', 'sp_from_home'); + obj(m, 'start', 'spawn', 3, 3); + obj(m, 'sp_from_plaza', 'spawn', 6, 6); + obj(m, 'i_bed', 'interact', 2, 2, { kind: 'bed' }); + obj(m, 'i_calendar', 'interact', 2, 1, { kind: 'calendar' }); + maps.home = m; +} + +// ============ 2. PLAZA 30x20 ============ +{ + const m = M(30, 20, t.grass); + // scatter grass flowers + for (let r = 0; r < 20; r++) for (let c = 0; c < 30; c++) if (((c * 5 + r * 7) % 11) === 0) setG(m, c, r, t.grassF); + borderO(m, t.fence); + // trees along corners + for (const [c, r] of [[1, 1], [2, 1], [27, 1], [28, 1], [1, 18], [28, 18]]) { setO(m, c, r, t.treeTrunk); setG(m, c, r - 1 < 0 ? 0 : c, r); } + // ---- houses along top (home/bakery/grocery/clinic) ---- + function house(c, target, dest) { + rectO(m, c, 1, 3, 2, t.roof); + rectO(m, c, 3, 3, 1, t.wall); + setO(m, c, 2, t.wall); setO(m, c + 2, 2, t.wall); + setG(m, c + 1, 3, t.door); setO(m, c + 1, 3, 0); + obj(m, 'door_' + target, 'door', c + 1, 3, { target, dest }); + // path down from door + setG(m, c + 1, 4, t.path); setG(m, c + 1, 5, t.path); + obj(m, 'sp_from_' + target, 'spawn', c + 1, 5); + } + house(2, 'home', 'sp_from_plaza'); + house(8, 'bakery', 'sp_from_plaza'); + house(15, 'grocery', 'sp_from_plaza'); + house(22, 'clinic', 'sp_from_plaza'); + // central cobble plaza + main path + rectG(m, 3, 9, 24, 3, t.path); + rectG(m, 11, 13, 8, 4, t.cobble); + // festival stage (center) — walkable cobble around; stage tile as decor obstacle at back + rectO(m, 12, 12, 4, 1, t.stage); + // bulletin board (afu) + setO(m, 14, 8, t.board); + // flowerbed (T2 water) cluster left-center + setO(m, 5, 13, t.flowerbed); setO(m, 6, 13, t.flowerbed); setO(m, 7, 13, t.flowerbed); + // twin-bird tree (right) + setO(m, 24, 5, t.treeTrunk); setG(m, 23, 4, t.treeTop); setG(m, 24, 4, t.treeTop); setG(m, 25, 4, t.treeTop); setG(m, 24, 3, t.treeTop); + // mailbox (dabao) bottom-center + setO(m, 15, 17, t.mailbox); + obj(m, 'i_mailbox', 'interact', 15, 16, { kind: 'mailbox' }); // C3 散落信件 + // decorative + setO(m, 3, 16, t.bushS); setO(m, 26, 16, t.bushS); setO(m, 9, 8, t.lamp); setO(m, 20, 8, t.lamp); + // edge exits (leave gaps in fence) + setO(m, 0, 10, 0); exitDoor(m, 0, 10, 'field', 'sp_from_plaza'); setG(m, 1, 10, t.path); obj(m, 'sp_from_field', 'spawn', 2, 10); + setO(m, 29, 10, 0); exitDoor(m, 29, 10, 'lake', 'sp_from_plaza'); setG(m, 28, 10, t.path); obj(m, 'sp_from_lake', 'spawn', 27, 10); + setO(m, 4, 19, 0); exitDoor(m, 4, 19, 'well', 'sp_from_plaza'); setG(m, 4, 18, t.path); obj(m, 'sp_from_well', 'spawn', 4, 17); + setO(m, 25, 19, 0); exitDoor(m, 25, 19, 'fog', 'sp_from_plaza'); setG(m, 25, 18, t.path); obj(m, 'sp_from_fog', 'spawn', 25, 17); + // NPCs + obj(m, 'afu', 'npc', 14, 9, { npc: 'afu' }); + obj(m, 'dabao', 'npc', 15, 18, { npc: 'dabao' }); + obj(m, 'bird1', 'npc', 23, 5, { npc: 'bird1' }); + obj(m, 'bird2', 'npc', 25, 5, { npc: 'bird2' }); + obj(m, 'extra1', 'npc', 10, 15, { npc: 'extra1' }); + obj(m, 'extra2', 'npc', 19, 16, { npc: 'extra2' }); + obj(m, 'extra3', 'npc', 6, 6, { npc: 'extra3' }); + // interacts + obj(m, 'i_board', 'interact', 14, 9, { kind: 'board' }); + obj(m, 'i_flowerbed', 'interact', 6, 14, { kind: 'flowerbed' }); + obj(m, 'i_festival', 'interact', 14, 14, { kind: 'festival' }); + obj(m, 'i_ribbon0', 'interact', 12, 13, { kind: 'ribbon', n: 0 }); + obj(m, 'i_ribbon1', 'interact', 14, 13, { kind: 'ribbon', n: 1 }); + obj(m, 'i_ribbon2', 'interact', 16, 13, { kind: 'ribbon', n: 2 }); + obj(m, 'i_ribbon3', 'interact', 18, 13, { kind: 'ribbon', n: 3 }); + maps.plaza = m; +} + +// ============ 3. BAKERY 15x10 ============ +{ + const m = M(15, 10, t.wfloor); room(m); + rectG(m, 1, 1, 13, 8, t.wfloorB); + rectO(m, 2, 1, 5, 1, t.oven); + rectO(m, 1, 6, 8, 1, t.counter); + setG(m, 4, 5, t.recipe); // 配方纸 (C2 on back) + setO(m, 11, 1, t.shelf); setO(m, 12, 1, t.shelf); + setO(m, 12, 7, t.barrel); setO(m, 13, 4, t.plantPot); + setG(m, 8, 0, t.windowI); + exitDoor(m, 7, 9, 'plaza', 'sp_from_bakery'); + obj(m, 'sp_from_plaza', 'spawn', 7, 8); + obj(m, 'yuan', 'npc', 4, 7, { npc: 'yuan' }); + obj(m, 'i_recipe', 'interact', 4, 6, { kind: 'recipe' }); + maps.bakery = m; +} + +// ============ 4. GROCERY 15x10 ============ +{ + const m = M(15, 10, t.wfloor); room(m); + rectG(m, 1, 1, 13, 8, t.wfloorB); + // shelves: same item repeated 9 (C5) + for (let c = 2; c <= 10; c++) setO(m, c, 1, t.shelfItem); // 9 identical + rectO(m, 1, 6, 8, 1, t.counter); + setO(m, 12, 3, t.crate); setO(m, 13, 6, t.barrel); setO(m, 12, 7, t.crate); + setG(m, 11, 0, t.windowI); + exitDoor(m, 7, 9, 'plaza', 'sp_from_grocery'); + obj(m, 'sp_from_plaza', 'spawn', 7, 8); + obj(m, 'mimi', 'npc', 4, 7, { npc: 'mimi' }); + obj(m, 'i_shelf', 'interact', 6, 2, { kind: 'shelf' }); + maps.grocery = m; +} + +// ============ 5. CLINIC 12x8 ============ +{ + const m = M(12, 8, t.wfloor); room(m); + rectG(m, 1, 1, 10, 6, t.wfloorB); + setO(m, 2, 2, t.table); setG(m, 2, 1, t.clinicForm); // 表格 (C8) + setO(m, 9, 1, t.bookshelf); setO(m, 9, 5, t.plantPot); + setO(m, 5, 5, t.chair); + setG(m, 6, 0, t.windowI); + exitDoor(m, 6, 7, 'plaza', 'sp_from_clinic'); + obj(m, 'sp_from_plaza', 'spawn', 6, 6); + obj(m, 'fu', 'npc', 4, 3, { npc: 'fu' }); + obj(m, 'i_clinicform', 'interact', 2, 2, { kind: 'clinicform' }); + maps.clinic = m; +} + +// ============ 6. LAKE 24x16 ============ +{ + const m = M(24, 16, t.grass); + for (let r = 0; r < 16; r++) for (let c = 0; c < 24; c++) if (((c * 3 + r * 5) % 13) === 0) setG(m, c, r, t.grassF); + // lake fills bottom rows 9-15 + for (let r = 9; r < 16; r++) for (let c = 1; c < 23; c++) { + const deep = r >= 12; + setG(m, c, r, deep ? t.waterDeep : (((c + r) % 2) ? t.water : t.waterL)); + setO(m, c, r, deep ? t.waterDeep : t.water); + } + // lilypads / reeds on shore + setO(m, 3, 9, t.reed); setO(m, 20, 9, t.reed); setO(m, 12, 10, t.lilypad); + borderO(m, t.fence); + // top exit to plaza + setO(m, 12, 0, 0); exitDoor(m, 12, 0, 'plaza', 'sp_from_lake'); setG(m, 12, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 12, 2); + // bench (gui) at shore + setO(m, 5, 8, t.bench); + // picnic cloth (T7) + setG(m, 16, 6, t.picnic); + // trees + setO(m, 2, 2, t.treeTrunk); setG(m, 2, 1, t.treeTop); setO(m, 21, 3, t.treeTrunk); setG(m, 21, 2, t.treeTop); + setO(m, 8, 3, t.rock); setO(m, 18, 2, t.bushS); + obj(m, 'gui', 'npc', 5, 7, { npc: 'gui' }); + obj(m, 'extra4', 'npc', 15, 3, { npc: 'extra4' }); + obj(m, 'i_picnic', 'interact', 16, 6, { kind: 'picnic' }); + maps.lake = m; +} + +// ============ 7. FIELD 20x14 ============ +{ + const m = M(20, 14, t.grass); + for (let r = 0; r < 14; r++) for (let c = 0; c < 20; c++) { + const k = (c * 7 + r * 3) % 9; + if (k === 0) setG(m, c, r, t.grassF); else if (k === 3) setG(m, c, r, t.grassF2); + } + borderO(m, t.fence); + // berry bushes (T6, pick x5) — 5 spread + const berries = [[4, 3], [8, 4], [13, 3], [6, 8], [14, 9]]; + berries.forEach(([c, r], i) => { setO(m, c, r, t.berry); obj(m, 'i_berry' + i, 'interact', c, r, { kind: 'berry', n: i }); }); + // flowerbed (浇花点) + setO(m, 10, 6, t.flowerbed); setO(m, 11, 6, t.flowerbed); + obj(m, 'i_fieldbed', 'interact', 10, 7, { kind: 'flowerbed' }); + setO(m, 2, 2, t.treeTrunk); setG(m, 2, 1, t.treeTop); setO(m, 17, 11, t.stump); setO(m, 16, 4, t.mushroom); + // top exit to plaza + setO(m, 10, 0, 0); exitDoor(m, 10, 0, 'plaza', 'sp_from_field'); setG(m, 10, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 10, 2); + obj(m, 'extra5', 'npc', 15, 7, { npc: 'extra5' }); + maps.field = m; +} + +// ============ 8. WELL 14x10 ============ +{ + const m = M(14, 10, t.grass); + for (let r = 0; r < 10; r++) for (let c = 0; c < 14; c++) if (((c + r) % 5) === 0) setG(m, c, r, t.grassF); + rectG(m, 5, 3, 4, 4, t.sand); + borderO(m, t.fence); + setO(m, 6, 4, t.well); setO(m, 7, 4, t.well); + setO(m, 2, 2, t.rock); setO(m, 11, 7, t.bushS); setO(m, 3, 7, t.stump); + // top exit to plaza + setO(m, 7, 0, 0); exitDoor(m, 7, 0, 'plaza', 'sp_from_well'); setG(m, 7, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 7, 2); + obj(m, 'i_well', 'interact', 7, 5, { kind: 'well' }); + maps.well = m; +} + +// ============ 9. FOG 20x8 ============ +{ + const m = M(20, 8, t.grass); + for (let r = 0; r < 8; r++) for (let c = 0; c < 20; c++) if (((c * 2 + r) % 7) === 0) setG(m, c, r, t.grassF); + // fog wall fills top rows 0-2 (collides) + for (let r = 0; r < 3; r++) for (let c = 0; c < 20; c++) { setG(m, c, r, t.fog); setO(m, c, r, t.fog); } + for (let r = 3; r < 8; r++) { setO(m, 0, r, t.fence); setO(m, 19, r, t.fence); } + for (let c = 0; c < 20; c++) setO(m, c, 7, t.fence); + setO(m, 3, 5, t.bushS); setO(m, 16, 5, t.bushS); + // bottom exit to plaza + setO(m, 10, 7, 0); exitDoor(m, 10, 7, 'plaza', 'sp_from_fog'); setG(m, 10, 6, t.path); obj(m, 'sp_from_plaza', 'spawn', 10, 5); + obj(m, 'i_fog', 'interact', 10, 3, { kind: 'fog' }); + maps.fog = m; +} + +// ============ 10. WHITE (corridor 20x6 stacked over ward) 20x14 ============ +{ + const m = M(20, 14, t.wfloorW); + rectG(m, 0, 0, 20, 14, t.wfloorW); + rectG(m, 0, 0, 20, 6, t.wfloorWb); // corridor top band + borderO(m, t.wwall); + // ward divider wall (row 6) with a doorway + for (let c = 0; c < 20; c++) setO(m, c, 6, t.wwall); + setO(m, 10, 6, 0); setG(m, 10, 6, t.wdoor); + // corridor windows + setG(m, 4, 0, t.wwindow); setG(m, 15, 0, t.wwindow); + // ward: hospital beds + setO(m, 3, 9, t.hbed); setO(m, 3, 11, t.hbed); setO(m, 16, 9, t.hbed); + obj(m, 'sp_corridor', 'spawn', 10, 2); + obj(m, 'sp_ward', 'spawn', 10, 11); + maps.white = m; +} + +// ================= TMX serialization ================= +function tmx(m) { + const csv = (arr) => { + let out = ''; + for (let r = 0; r < m.h; r++) { + out += arr.slice(r * m.w, r * m.w + m.w).join(','); + if (r < m.h - 1) out += ','; + out += '\n'; + } + return out; + }; + const objXml = m.objects.map((o) => { + const props = Object.entries(o.props); + const p = props.length + ? `\n \n${props.map(([k, v]) => ` `).join('\n')}\n \n ` + : ''; + return ` ${p}`; + }).join('\n'); + const nextId = Math.max(1, ...m.objects.map((o) => o.id)) + 1; + return ` + + + + + + +${csv(m.ground)} + + + +${csv(m.obs)} + + +${objXml} + + +`; +} + +for (const [name, m] of Object.entries(maps)) { + writeFileSync(resolve(TMX_DIR, name + '.tmx'), tmx(m)); + console.log('wrote', name + '.tmx', m.w + 'x' + m.h, m.objects.length + ' objects'); +} +console.log('MAPS TMX DONE'); diff --git a/Opus4-8-xhigh/tools/montage.lua b/Opus4-8-xhigh/tools/montage.lua new file mode 100644 index 0000000..1d4b89c --- /dev/null +++ b/Opus4-8-xhigh/tools/montage.lua @@ -0,0 +1,35 @@ +-- montage.lua : upscale generated assets into preview PNGs for visual QA +local dir = app.params["outdir"] +local function open(name) return app.open(dir.."/"..name) end +local function blit(dst, src, dx, dy, sc) + for y=0,src.height-1 do for x=0,src.width-1 do + local p = src:getPixel(x,y) + for j=0,sc-1 do for i=0,sc-1 do dst:drawPixel(dx+x*sc+i, dy+y*sc+j, p) end end + end end +end +-- tiles preview +do + local ts = open("tileset.png"); local si = ts.cels[1].image + local out = Sprite(128*5, 144*5, ColorMode.RGB) + local oi = out.cels[1].image + for y=0,oi.height-1 do for x=0,oi.width-1 do oi:drawPixel(x,y, app.pixelColor.rgba(30,30,36,255)) end end + blit(oi, si, 0, 0, 5) + out:saveAs(dir.."/_preview_tiles.png"); out:close(); ts:close() +end +-- chars preview +do + local out = Sprite(760, 420, ColorMode.RGB) + local oi = out.cels[1].image + for y=0,oi.height-1 do for x=0,oi.width-1 do oi:drawPixel(x,y, app.pixelColor.rgba(40,44,52,255)) end end + local pl = open("player.png"); blit(oi, pl.cels[1].image, 0, 0, 4); pl:close() + local po = open("portraits.png"); blit(oi, po.cels[1].image, 280, 0, 4); po:close() + local bi = open("birds.png"); blit(oi, bi.cels[1].image, 560, 0, 5); bi:close() + local sp = open("sparrow.png"); blit(oi, sp.cels[1].image, 680, 0, 5); sp:close() + local ex = open("extras.png"); blit(oi, ex.cels[1].image, 560, 140, 4); ex:close() + local names = {"afu","yuan","dabao","mimi","fu","gui"} + for i,n in ipairs(names) do + local np = open("npc_"..n..".png"); blit(oi, np.cels[1].image, (i-1)*84, 200, 5); np:close() + end + out:saveAs(dir.."/_preview_chars.png"); out:close() +end +print("montage done") diff --git a/Opus4-8-xhigh/tools/tmx/bakery.tmx b/Opus4-8-xhigh/tools/tmx/bakery.tmx new file mode 100644 index 0000000..a461919 --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/bakery.tmx @@ -0,0 +1,53 @@ + + + + + + + +25,25,25,25,25,25,25,25,39,25,25,25,25,25,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,33,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,25,25,25,25,25,25,40,25,25,25,25,25,25,25 + + + + +27,28,28,28,28,28,28,28,28,28,28,28,28,28,27, +27,0,32,32,32,32,32,0,0,0,0,34,34,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,62,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,0,27, +27,31,31,31,31,31,31,31,31,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,58,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,0,27, +27,27,27,27,27,27,27,0,27,27,27,27,27,27,27 + + + + + + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/clinic.tmx b/Opus4-8-xhigh/tools/tmx/clinic.tmx new file mode 100644 index 0000000..d90c339 --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/clinic.tmx @@ -0,0 +1,49 @@ + + + + + + + +25,25,25,25,25,25,39,25,25,25,25,25, +25,26,38,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,25, +25,25,25,25,25,25,40,25,25,25,25,25 + + + + +27,28,28,28,28,28,28,28,28,28,28,27, +27,0,0,0,0,0,0,0,0,59,0,27, +27,0,36,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,37,0,0,0,62,0,27, +27,0,0,0,0,0,0,0,0,0,0,27, +27,27,27,27,27,27,0,27,27,27,27,27 + + + + + + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/field.tmx b/Opus4-8-xhigh/tools/tmx/field.tmx new file mode 100644 index 0000000..eb3c436 --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/field.tmx @@ -0,0 +1,91 @@ + + + + + + + +2,1,1,65,1,1,1,1,1,2,40,1,65,1,1,1,1,1,2,1, +65,1,9,1,1,1,2,1,1,65,3,1,1,1,1,2,1,1,65,1, +1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1, +2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1, +65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1, +1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1, +2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1, +65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1, +1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1, +2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1, +65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1, +1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1, +2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1, +65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1 + + + + +14,14,14,14,14,14,14,14,14,14,0,14,14,14,14,14,14,14,14,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,8,0,0,0,0,0,0,0,0,8,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,51,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,15,15,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/fog.tmx b/Opus4-8-xhigh/tools/tmx/fog.tmx new file mode 100644 index 0000000..414dfb8 --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/fog.tmx @@ -0,0 +1,44 @@ + + + + + + + +17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17, +17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17, +17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17, +1,1,2,1,1,1,1,1,1,2,1,1,1,1,1,1,2,1,1,1, +1,1,1,1,1,2,1,1,1,1,1,1,2,1,1,1,1,1,1,2, +1,2,1,1,1,1,1,1,2,1,1,1,1,1,1,2,1,1,1,1, +1,1,1,1,2,1,1,1,1,1,3,2,1,1,1,1,1,1,2,1, +2,1,1,1,1,1,1,2,1,1,40,1,1,1,2,1,1,1,1,1 + + + + +17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17, +17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17, +17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,14,14,14,14,14,14,14,14,14,0,14,14,14,14,14,14,14,14,14 + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/grocery.tmx b/Opus4-8-xhigh/tools/tmx/grocery.tmx new file mode 100644 index 0000000..b6462be --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/grocery.tmx @@ -0,0 +1,53 @@ + + + + + + + +25,25,25,25,25,25,25,25,25,25,25,39,25,25,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,26,26,26,26,26,26,26,26,26,26,26,26,26,25, +25,25,25,25,25,25,25,40,25,25,25,25,25,25,25 + + + + +27,28,28,28,28,28,28,28,28,28,28,28,28,28,27, +27,0,35,35,35,35,35,35,35,35,35,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,57,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,0,27, +27,31,31,31,31,31,31,31,31,0,0,0,0,58,27, +27,0,0,0,0,0,0,0,0,0,0,0,57,0,27, +27,0,0,0,0,0,0,0,0,0,0,0,0,0,27, +27,27,27,27,27,27,27,0,27,27,27,27,27,27,27 + + + + + + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/home.tmx b/Opus4-8-xhigh/tools/tmx/home.tmx new file mode 100644 index 0000000..d628feb --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/home.tmx @@ -0,0 +1,50 @@ + + + + + + + +25,25,30,25,39,25,25,39,25,25,25,25, +25,25,25,25,25,63,63,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,25,25,25,25,25,25, +25,25,25,25,25,25,40,25,25,25,25,25 + + + + +27,28,28,28,28,28,28,28,28,28,28,27, +27,29,0,0,0,0,0,0,62,59,0,27, +27,29,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,0,0,0,27, +27,0,0,0,0,0,0,0,36,37,0,27, +27,0,0,0,0,0,0,0,0,0,0,27, +27,27,27,27,27,27,0,27,27,27,27,27 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/lake.tmx b/Opus4-8-xhigh/tools/tmx/lake.tmx new file mode 100644 index 0000000..a5300b8 --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/lake.tmx @@ -0,0 +1,70 @@ + + + + + + + +2,1,1,1,1,1,1,1,1,1,1,1,40,2,1,1,1,1,1,1,1,1,1,1, +1,1,9,1,1,1,1,2,1,1,1,1,3,1,1,1,1,1,1,1,2,1,1,1, +1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,9,1,1, +1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1, +1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1, +1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,20,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2, +1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1, +1,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,1, +1,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,1, +1,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,1, +1,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,1, +2,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,1, +1,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,1, +1,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,1 + + + + +14,14,14,14,14,14,14,14,14,14,14,14,0,14,14,14,14,14,14,14,14,14,14,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,0,0,14, +14,0,0,0,0,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,10,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,5,5,55,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,55,5,5,14, +14,5,5,5,5,5,5,5,5,5,5,5,54,5,5,5,5,5,5,5,5,5,5,14, +14,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,14, +14,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,14, +14,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,14, +14,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,14, +14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/plaza.tmx b/Opus4-8-xhigh/tools/tmx/plaza.tmx new file mode 100644 index 0000000..779f4ef --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/plaza.tmx @@ -0,0 +1,191 @@ + + + + + + + +2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1, +1,18,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1, +1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1, +1,1,1,13,1,1,1,1,1,13,1,1,1,1,1,1,13,1,1,1,2,1,1,13,9,1,1,1,1,1, +1,2,1,3,1,1,1,1,1,3,1,1,2,1,1,1,3,1,1,1,1,1,1,9,9,9,1,1,1,1, +1,1,1,3,2,1,1,1,1,3,1,1,1,1,1,2,3,1,1,1,1,1,1,3,1,1,2,1,1,1, +1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2, +1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, +1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1, +1,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1,1, +40,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,40, +2,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,1, +1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1, +1,1,1,1,1,1,2,1,1,1,1,72,72,72,72,72,72,72,72,1,1,1,1,1,1,1,1,1,2,1, +1,1,1,1,1,1,1,1,1,2,1,72,72,72,72,72,72,72,72,1,2,1,1,1,1,1,1,1,1,1, +1,2,1,1,1,1,1,1,1,1,1,72,72,72,72,72,72,72,72,1,1,1,1,2,1,1,1,1,1,1, +1,1,1,1,2,1,1,1,1,1,1,72,72,72,72,72,72,72,72,1,1,1,1,1,1,1,2,1,1,1, +1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2, +1,1,1,1,3,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,3,1,1,1,1, +1,1,2,1,40,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,40,1,1,1,1 + + + + +14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14, +14,10,12,12,12,0,0,0,12,12,12,0,0,0,0,12,12,12,0,0,0,0,12,12,12,0,0,10,10,14, +14,0,11,12,11,0,0,0,11,12,11,0,0,0,0,11,12,11,0,0,0,0,11,12,11,0,0,0,0,14, +14,0,11,0,11,0,0,0,11,0,11,0,0,0,0,11,0,11,0,0,0,0,11,0,11,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,22,0,0,0,0,18,0,0,0,0,0,22,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,48,48,48,48,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,15,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,0,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,14, +14,14,14,14,0,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,0,14,14,14,14 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/well.tmx b/Opus4-8-xhigh/tools/tmx/well.tmx new file mode 100644 index 0000000..2551b96 --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/well.tmx @@ -0,0 +1,48 @@ + + + + + + + +2,1,1,1,1,2,1,40,1,1,2,1,1,1, +1,1,1,1,2,1,1,3,1,2,1,1,1,1, +1,1,1,2,1,1,1,1,2,1,1,1,1,2, +1,1,2,1,1,24,24,24,24,1,1,1,2,1, +1,2,1,1,1,24,24,24,24,1,1,2,1,1, +2,1,1,1,1,24,24,24,24,1,2,1,1,1, +1,1,1,1,2,24,24,24,24,2,1,1,1,1, +1,1,1,2,1,1,1,1,2,1,1,1,1,2, +1,1,2,1,1,1,1,2,1,1,1,1,2,1, +1,2,1,1,1,1,2,1,1,1,1,2,1,1 + + + + +14,14,14,14,14,14,14,0,14,14,14,14,14,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,52,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,16,16,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,0,0,53,0,0,0,0,0,0,0,23,0,14, +14,0,0,0,0,0,0,0,0,0,0,0,0,14, +14,14,14,14,14,14,14,14,14,14,14,14,14,14 + + + + + + + + + + + + + + + + + diff --git a/Opus4-8-xhigh/tools/tmx/white.tmx b/Opus4-8-xhigh/tools/tmx/white.tmx new file mode 100644 index 0000000..07181ca --- /dev/null +++ b/Opus4-8-xhigh/tools/tmx/white.tmx @@ -0,0 +1,46 @@ + + + + + + + +43,43,43,43,46,43,43,43,43,43,43,43,43,43,43,46,43,43,43,43, +43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, +43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, +43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, +43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, +43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43, +42,42,42,42,42,42,42,42,42,42,45,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42, +42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42 + + + + +41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,41,41,41,41,41,41,41,41,41,0,41,41,41,41,41,41,41,41,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,0,0,44,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,0,0,44,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41, +41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41 + + + + + + + diff --git a/Opus4-8-xhigh/tsconfig.json b/Opus4-8-xhigh/tsconfig.json new file mode 100644 index 0000000..948f22e --- /dev/null +++ b/Opus4-8-xhigh/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": false, + "noImplicitAny": false, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": [] + }, + "include": ["src"] +} diff --git a/Opus4-8-xhigh/vite.config.ts b/Opus4-8-xhigh/vite.config.ts new file mode 100644 index 0000000..e757073 --- /dev/null +++ b/Opus4-8-xhigh/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; + +// ponytail: single-page pixel game, no framework plugin needed. +export default defineConfig({ + base: './', + build: { + target: 'es2020', + assetsInlineLimit: 0, // keep pixel PNGs & map JSON as real files + }, +}); diff --git a/README.md b/README.md new file mode 100644 index 0000000..607bb5e --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# ModelTest 2026-07-14 — Honey Village + +Six AI models were given the same PRD (a Phaser pixel-art game, "Honey Village") and each built it independently. Every folder is a self-contained Vite + Phaser app with its own Dockerfile, deployed as a dedicated project on Dokploy. + +## Live builds + +| Model | Folder | Link | +|---|---|---| +| Fable 5 (high) | [`Fable-5-High/`](Fable-5-High/) | http://fable-5-high-dzjk84-83df4e-165-140-85-215.sslip.io | +| Claude Opus 4.8 (xhigh) | [`Opus4-8-xhigh/`](Opus4-8-xhigh/) | http://opus4-8-xhigh-eoqc4i-84dd70-165-140-85-215.sslip.io | +| GPT-5.6 Sol (high) | [`GPT-5-6-Sol-High/`](GPT-5-6-Sol-High/) | http://gpt-5-6-sol-high-afucmo-0e4474-165-140-85-215.sslip.io | +| GLM 5.2 | [`GLM5.2/`](GLM5.2/) | http://glm5-2-lo5jta-7cb9b6-165-140-85-215.sslip.io | +| Grok | [`Grok/`](Grok/) | http://grok-fge5vj-4019b1-165-140-85-215.sslip.io | +| Kimi K3 | [`Kimi-K3/`](Kimi-K3/) | http://kimi-k3-xbtwah-736030-165-140-85-215.sslip.io | + +**Hub page** (links to all builds): http://modeltest-hub-0ekb0z-fef514-165-140-85-215.sslip.io + +## Run locally + +```bash +cd +npm ci +npm run dev +``` + +## Deployment + +Each folder builds with its `Dockerfile` (Node 22 → static nginx). `frontend/` is the hub page. diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..93e56f7 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:alpine +COPY index.html /usr/share/nginx/html/ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..6286328 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,47 @@ + + + + + +Model Test — 2026-07-14 + + + +
+

Model Test — Honey Village

+

Same PRD, six models. Pick a build:

+ +

Source on GitHub ↗

+
+ +