Honey Village: six model builds + hub frontend, Dockerized for Dokploy

This commit is contained in:
2026-07-15 10:50:12 -04:00
commit 67253bded5
523 changed files with 58879 additions and 0 deletions
+9
View File
@@ -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
+226
View File
@@ -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 | 完美的重复 | 任务 T8T10 与 Day 1 的 T1T3 **逐字相同**,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 | 020 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 02 瓣 |
| S1 | 2150 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 36 瓣 |
| S2 | 5180 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 |
| S3 | 8199 | 饱和度 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 | 湖面倒影演出 |
| T8T10 | D4 | 与 T1T3 逐字相同 | 同上 | 刻意恐怖谷 |
| 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 届」 | D5T12 中) |
| 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 相机 postFXColorMatrix / 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 一次脚本演出)。
+89
View File
@@ -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 个场景,白色层含走廊+病房两张图)
- **置齐全部线索**C1C9
- **强制触发四个结局**(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
```
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>崩坏童话:蜜糖村</title>
<style>
html, body { margin: 0; padding: 0; background: #1a1a22; height: 100%; overflow: hidden; }
#game { display: flex; align-items: center; justify-content: center; height: 100%; }
#game canvas { image-rendering: pixelated; image-rendering: crisp-edges; }
#debug-panel {
display: none; position: fixed; top: 8px; right: 8px; z-index: 100;
background: rgba(20,20,30,.92); color: #cfe; font: 12px monospace;
padding: 10px; border: 1px solid #5a7; border-radius: 4px; width: 230px;
}
#debug-panel h3 { margin: 0 0 6px; font-size: 12px; color: #8f8; }
#debug-panel .row { margin: 4px 0; display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
#debug-panel button { font: 11px monospace; background: #2a3a4a; color: #cfe; border: 1px solid #567; border-radius: 3px; padding: 2px 6px; cursor: pointer; }
#debug-panel input { width: 48px; font: 11px monospace; background: #111; color: #cfe; border: 1px solid #567; }
#name-entry {
display: none; position: fixed; z-index: 90; left: 50%; top: 62%; transform: translate(-50%, -50%);
text-align: center;
}
#name-entry input {
font: 16px monospace; padding: 6px 10px; background: #fff8e8; color: #5a4632;
border: 3px solid #e8a04c; border-radius: 2px; outline: none; text-align: center; width: 180px;
}
</style>
</head>
<body>
<div id="game"></div>
<div id="name-entry"><input id="name-input" maxlength="8" placeholder="小满" autocomplete="off" /></div>
<div id="debug-panel"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="15" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="6">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="15" height="10">
<data encoding="csv">
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,25,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
</data>
</layer>
<layer id="2" name="obstacles" width="15" height="10">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="yuanyuan" type="npc" x="80" y="49.6"><point/></object>
<object id="2" name="recipe" type="poi" x="128" y="75.2"><properties><property name="clue" value="C2"/></properties><point/></object>
<object id="3" name="knead" type="poi" x="176" y="107.2"><point/></object>
<object id="4" name="entry" type="spawn" x="128" y="136"><point/></object>
<object id="5" name="to_plaza" type="door" x="112" y="152" width="32" height="8"><properties><property name="target" value="plaza"/><property name="spawn" value="fromBakery"/></properties></object>
</objectgroup>
</map>
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="12" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="5">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="12" height="8">
<data encoding="csv">
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
</data>
</layer>
<layer id="2" name="obstacles" width="12" height="8">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="fu" type="npc" x="112" y="49.6"><point/></object>
<object id="2" name="chart" type="poi" x="80" y="75.2"><properties><property name="clue" value="C8"/></properties><point/></object>
<object id="3" name="entry" type="spawn" x="96" y="104"><point/></object>
<object id="4" name="to_plaza" type="door" x="80" y="120" width="32" height="8"><properties><property name="target" value="plaza"/><property name="spawn" value="fromClinic"/></properties></object>
</objectgroup>
</map>
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="14" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="9">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="20" height="14">
<data encoding="csv">
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
</data>
</layer>
<layer id="2" name="obstacles" width="20" height="14">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="berry1" type="poi" x="80" y="96"><point/></object>
<object id="2" name="berry2" type="poi" x="144" y="112"><point/></object>
<object id="3" name="berry3" type="poi" x="208" y="96"><point/></object>
<object id="4" name="berry4" type="poi" x="256" y="144"><point/></object>
<object id="5" name="berry5" type="poi" x="112" y="176"><point/></object>
<object id="6" name="fieldwater" type="poi" x="64" y="203.2"><point/></object>
<object id="7" name="entry" type="spawn" x="33.6" y="104"><point/></object>
<object id="8" name="to_plaza" type="door" x="0" y="72" width="9.6" height="80"><properties><property name="target" value="plaza"/><property name="spawn" value="fromField"/></properties></object>
</objectgroup>
</map>
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="4">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="20" height="8">
<data encoding="csv">
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
</data>
</layer>
<layer id="2" name="obstacles" width="20" height="8">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="fog" type="poi" x="168" y="96"><point/></object>
<object id="2" name="entry" type="spawn" x="168" y="46.4"><point/></object>
<object id="3" name="to_plaza" type="door" x="120" y="0" width="80" height="11.2"><properties><property name="target" value="plaza"/><property name="spawn" value="fromFog"/></properties></object>
</objectgroup>
</map>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="12" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="6">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="12" height="8">
<data encoding="csv">
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,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
</data>
</layer>
<layer id="2" name="obstacles" width="12" height="8">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="bed" type="spawn" x="40" y="56"><point/></object>
<object id="2" name="entry" type="spawn" x="88" y="104"><point/></object>
<object id="3" name="to_plaza" type="door" x="80" y="120" width="32" height="8"><properties><property name="target" value="plaza"/><property name="spawn" value="fromHouse"/></properties></object>
<object id="4" name="calendar" type="poi" x="40" y="40"><properties><property name="clue" value="C1"/></properties><point/></object>
<object id="5" name="bed" type="poi" x="24" y="72"><point/></object>
</objectgroup>
</map>
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="24" height="16" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="7">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="24" height="16">
<data encoding="csv">
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
</data>
</layer>
<layer id="2" name="obstacles" width="24" height="16">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="gui" type="npc" x="310.4" y="96"><point/></object>
<object id="2" name="bench" type="poi" x="272" y="112"><point/></object>
<object id="3" name="picnic" type="poi" x="136" y="120"><point/></object>
<object id="4" name="entry" type="spawn" x="360" y="120"><point/></object>
<object id="5" name="picnicSpot" type="spawn" x="136" y="129.6"><point/></object>
<object id="6" name="to_plaza" type="door" x="374.4" y="80" width="9.6" height="80"><properties><property name="target" value="plaza"/><property name="spawn" value="fromLake"/></properties></object>
</objectgroup>
</map>
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="39">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="30" height="20">
<data encoding="csv">
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
</data>
</layer>
<layer id="2" name="obstacles" width="30" height="20">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="sign_house" type="poi" x="24" y="88"><point/></object>
<object id="2" name="board" type="poi" x="192" y="128"><point/></object>
<object id="3" name="letters" type="poi" x="288" y="48"><properties><property name="clue" value="C3"/></properties><point/></object>
<object id="4" name="birdtree" type="poi" x="344" y="144"><point/></object>
<object id="5" name="flowerbed1" type="poi" x="128" y="200"><point/></object>
<object id="6" name="flowerbed2" type="poi" x="128" y="232"><point/></object>
<object id="7" name="flowerbed3" type="poi" x="128" y="264"><point/></object>
<object id="8" name="ribbon1" type="poi" x="192" y="187.2"><point/></object>
<object id="9" name="ribbon2" type="poi" x="304" y="187.2"><point/></object>
<object id="10" name="ribbon3" type="poi" x="192" y="267.2"><point/></object>
<object id="11" name="ribbon4" type="poi" x="304" y="267.2"><point/></object>
<object id="12" name="festival" type="poi" x="248" y="216"><point/></object>
<object id="13" name="afu" type="npc" x="208" y="144"><point/></object>
<object id="14" name="dabao" type="npc" x="272" y="64"><point/></object>
<object id="15" name="bird1" type="npc" x="334.4" y="108.8"><point/></object>
<object id="16" name="bird2" type="npc" x="353.6" y="108.8"><point/></object>
<object id="17" name="extra1" type="npc" x="88" y="168"><properties><property name="patrol" value="40"/></properties><point/></object>
<object id="18" name="extra2" type="npc" x="360" y="152"><properties><property name="patrol" value="56"/></properties><point/></object>
<object id="19" name="extra3" type="npc" x="392" y="280"><properties><property name="patrol" value="40"/></properties><point/></object>
<object id="20" name="extra4" type="npc" x="88" y="280"><properties><property name="patrol" value="24"/></properties><point/></object>
<object id="21" name="extra5" type="npc" x="360" y="280"><properties><property name="patrol" value="16"/></properties><point/></object>
<object id="22" name="fromHouse" type="spawn" x="72" y="107.2"><point/></object>
<object id="23" name="fromBakery" type="spawn" x="152" y="107.2"><point/></object>
<object id="24" name="fromStore" type="spawn" x="344" y="107.2"><point/></object>
<object id="25" name="fromClinic" type="spawn" x="424" y="107.2"><point/></object>
<object id="26" name="fromLake" type="spawn" x="33.6" y="152"><point/></object>
<object id="27" name="fromField" type="spawn" x="462.4" y="152"><point/></object>
<object id="28" name="fromWell" type="spawn" x="240" y="33.6"><point/></object>
<object id="29" name="fromFog" type="spawn" x="240" y="283.2"><point/></object>
<object id="30" name="center" type="spawn" x="248" y="200"><point/></object>
<object id="31" name="to_house" type="door" x="64" y="67.2" width="16" height="12.8"><properties><property name="target" value="house"/><property name="spawn" value="entry"/></properties></object>
<object id="32" name="to_bakery" type="door" x="144" y="67.2" width="16" height="12.8"><properties><property name="target" value="bakery"/><property name="spawn" value="entry"/></properties></object>
<object id="33" name="to_store" type="door" x="336" y="67.2" width="16" height="12.8"><properties><property name="target" value="store"/><property name="spawn" value="entry"/></properties></object>
<object id="34" name="to_clinic" type="door" x="416" y="67.2" width="16" height="12.8"><properties><property name="target" value="clinic"/><property name="spawn" value="entry"/></properties></object>
<object id="35" name="to_lake" type="door" x="0" y="112" width="9.6" height="64"><properties><property name="target" value="lake"/><property name="spawn" value="entry"/></properties></object>
<object id="36" name="to_field" type="door" x="470.4" y="112" width="9.6" height="64"><properties><property name="target" value="field"/><property name="spawn" value="entry"/></properties></object>
<object id="37" name="to_well" type="door" x="200" y="0" width="80" height="11.2"><properties><property name="target" value="well"/><property name="spawn" value="entry"/></properties></object>
<object id="38" name="to_fogwall" type="door" x="200" y="308.8" width="80" height="11.2"><properties><property name="target" value="fogwall"/><property name="spawn" value="entry"/></properties></object>
</objectgroup>
</map>
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="15" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="8">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="15" height="10">
<data encoding="csv">
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
</data>
</layer>
<layer id="2" name="obstacles" width="15" height="10">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="mimi" type="npc" x="64" y="78.4"><point/></object>
<object id="2" name="shelfA" type="poi" x="64" y="59.2"><point/></object>
<object id="3" name="shelfB" type="poi" x="112" y="59.2"><point/></object>
<object id="4" name="shelfC" type="poi" x="160" y="59.2"><point/></object>
<object id="5" name="shelf_c5" type="poi" x="206.4" y="96"><properties><property name="clue" value="C5"/></properties><point/></object>
<object id="6" name="entry" type="spawn" x="128" y="136"><point/></object>
<object id="7" name="to_plaza" type="door" x="112" y="152" width="32" height="8"><properties><property name="target" value="plaza"/><property name="spawn" value="fromStore"/></properties></object>
</objectgroup>
</map>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.12.2" name="tiles" tilewidth="16" tileheight="16" tilecount="80" columns="10">
<image source="../public/assets/tiles.png" width="160" height="128"/>
</tileset>
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="14" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="4">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="14" height="10">
<data encoding="csv">
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
</data>
</layer>
<layer id="2" name="obstacles" width="14" height="10">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="well" type="poi" x="112" y="96"><properties><property name="clue" value="C7"/></properties><point/></object>
<object id="2" name="entry" type="spawn" x="112" y="129.6"><point/></object>
<object id="3" name="to_plaza" type="door" x="72" y="148.8" width="80" height="11.2"><properties><property name="target" value="plaza"/><property name="spawn" value="fromWell"/></properties></object>
</objectgroup>
</map>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="6" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="3">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="20" height="6">
<data encoding="csv">
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44
</data>
</layer>
<layer id="2" name="obstacles" width="20" height="6">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="entry" type="spawn" x="24" y="56"><point/></object>
<object id="2" name="to_white_ward" type="door" x="312" y="0" width="8" height="96"><properties><property name="target" value="white_ward"/><property name="spawn" value="entry"/></properties></object>
</objectgroup>
</map>
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="10" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="3">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="10" height="8">
<data encoding="csv">
44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44,
44,44,44,44,44,44,44,44,44,44
</data>
</layer>
<layer id="2" name="obstacles" width="10" height="8">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
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
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="chart_ward" type="poi" x="88" y="80"><point/></object>
<object id="2" name="entry" type="spawn" x="24" y="88"><point/></object>
</objectgroup>
</map>
+984
View File
@@ -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
}
}
}
}
}
+23
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

+169
View File
@@ -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
}
+152
View File
@@ -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
}
+210
View File
@@ -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
}
@@ -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
}
+165
View File
@@ -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
}
+188
View File
@@ -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
}
+719
View File
@@ -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
}
+195
View File
@@ -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
}
+143
View File
@@ -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
}
@@ -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
}
@@ -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
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+181
View File
@@ -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<AudioBuffer> {
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<AudioBuffer> {
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();
+80
View File
@@ -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 = `
<h3>DEBUG</h3>
<div class="row">状态:<span id="dbg-state"></span></div>
<div class="row">幸福 <input id="dbg-h" type="number" min="0" max="100"/><button id="dbg-h-set">设置</button></div>
<div class="row">觉知 <input id="dbg-a" type="number" min="0" max="100"/><button id="dbg-a-set">设置</button></div>
<div class="row">日:${[1, 2, 3, 4, 5, 6, 7].map(d => `<button data-day="${d}">${d}</button>`).join("")}</div>
<div class="row">传送:${SCENES.map(s => `<button data-tp="${s}">${s}</button>`).join("")}</div>
<div class="row"><button id="dbg-clues">置齐全部线索</button></div>
<div class="row">结局:${["E1", "E2", "E3", "E4"].map(e => `<button data-end="${e}">${e}</button>`).join("")}</div>
`;
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";
}
});
}
+302
View File
@@ -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<string, number | null> = {
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<string>();
questStatus: Record<string, QuestStatus> = {};
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<string>();
lettersHeld = 0; lettersDelivered = new Set<string>();
shelves = new Set<string>();
berries = new Set<string>();
watered = new Set<string>();
ribbons = new Set<string>();
wipes = 0;
greeted = new Set<string>();
talkCounts: Record<string, number> = {};
nagged = new Set<string>();
smallActs: Record<string, number> = {};
stared = new Set<string>();
guiChain = new Set<number>();
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();
+51
View File
@@ -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;
}
+206
View File
@@ -0,0 +1,206 @@
// 全部文案(原文保持干净,腐蚀在渲染层实施)。
// PRD 写死的台词在此逐字实现。
// 阿福每日公告(PRD §5,逐字。D4 与 D1 逐字相同)
export const ANNOUNCEMENTS: Record<number, string> = {
1: "早上好呀!今天的幸福任务贴在板上啦。祝你开心!",
2: "早上好呀!新的一天,新的幸福。祝你开心!",
3: "早上好!今天要把幸福装得满满的哦。要开心!",
4: "早上好呀!今天的幸福任务贴在板上啦。祝你开心!",
5: "早上好。今天是自由日,你可以自由地选择幸福。要开心哦。",
6: "明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。",
7: "庆典开始了。来吧。",
};
export const NPC_NAMES: Record<string, string> = {
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<string, Record<number, DialogNode>> = {
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<string, string>,
t4Give: ["这三封信,帮我送给圆圆、咪咪和芙医生。", "拜托啦。"],
t4Deliver: {
yuanyuan: "给我的信?谢谢你呀。",
mimi: "信?好久没收到过信了……哦不,昨天也收到了吧。",
fu: "……谢谢。我收下了。",
} as Record<string, string>,
t5Ask: ["帮我清点一下货架好吗?就数一数,很快的。"],
t5Done: ["都数完啦?谢谢谢谢!"],
t11Ask: "今天能帮我揉面吗?当然,你也可以拒绝的……大概。",
t11Work: ["(你把面团揉了又揉。面团很软,很甜。)", "谢谢你!揉过的面会更幸福哦。"],
t11Refused: "……好的。没关系。真的没关系。",
t12Ask: "(公告板上积了一层薄灰。要擦拭吗?)",
t12Refused: "(你转身离开了公告板。背后好像有目光。)",
wipe1: "(你擦去了一角灰尘。)",
wipe2: "(灰下浮现出旧字迹——)",
wipe3: "(公告板干净了。崭新得像什么都没发生过。)",
};
// 线索文本(PRD §8
export const CLUE_TEXT: Record<string, string[]> = {
C1: ["(床头挂着一本日历。)", "(每一页……都是同一个日期。)"],
C2: ["(配方纸的背面有一行手写小字:)", "「按疗程配比。甜度:最大。」"],
C3: ["(邮筒旁散落着几封信。)", "(所有邮戳,是同一天。)"],
C4: ["(信的署名处闪过一行字——)", "「PT-0▓」", "(0.2 秒后,它变回了「蜜糖村邮政」。)"],
C5: ["(同一种商品,重复排了九格。)", "(标签上,全部空白。)"],
C6: ["「第 412 届丰收庆典」", "(下面覆盖着更早的字——「第 411 届」「第 410 届」……)"],
C7: ["(你俯身向井口。井底传来极轻的——)", "(滴。滴。滴。)", "(像某种仪器的声音。)"],
C8: ["(桌上的表格抬头被芙医生的手肘挡住了。)", "(只露出——「…谷情绪疗…」)"],
C9: ["(龟爷爷望着湖面,没有再说话。)"],
};
// 梦境过场(侵蚀越高,梦境越"漏")
export const DREAMS: Record<number, string[]> = {
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: "(全村的目光,都转向了你。)",
};
+43
View File
@@ -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;
+44
View File
@@ -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");
}
}
+52
View File
@@ -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");
}
}
+147
View File
@@ -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);
}
}
+56
View File
@@ -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);
}
}
+313
View File
@@ -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();
}
}
}
}
+941
View File
@@ -0,0 +1,941 @@
// 地图场景基类:每张 Tiled 地图一个 ScenePRD §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<string, Phaser.Input.Keyboard.Key>;
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<string, any> = {};
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); } // 4060 秒渐隐至零
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;
}
// S4off 档闪现(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;
}
// 日历(C1D2 起)
case name === "calendar": {
if (d.day >= 2) narr(CLUE_TEXT.C1, () => d.foundClue("C1"));
else narr(["(床头的日历。明天也会是晴天。)"]);
return;
}
// 配方纸(C2D3 起)
case name === "recipe": {
if (d.day >= 3) narr(CLUE_TEXT.C2, () => d.foundClue("C2"));
else narr(["(柜台上的配方纸。字迹工整。)"]);
return;
}
// 散落信件(C3D2 起)
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;
}
// 诊所表格(C8D5 起)
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;
}
// 古井(C7D5 起可交互)
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"); } }
+21
View File
@@ -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);
+74
View File
@@ -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);
+62
View File
@@ -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);
+291
View File
@@ -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);
+138
View File
@@ -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);
+757
View File
@@ -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")
-- ============ 具名 NPC16×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")
-- ============ 群演 ×516×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")
+352
View File
@@ -0,0 +1,352 @@
// 《崩坏童话:蜜糖村》10 张地图生成脚本
// 生成 maps/*.tmxTiled 源文件),随后由 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 `<properties>${e.map(([k, v]) => `<property name="${k}" value="${v}"/>`).join("")}</properties>`;
};
const objs = this.objects.map(o => {
const size = o.w || o.h ? ` width="${o.w}" height="${o.h}"` : "";
const body = props(o.props);
return ` <object id="${o.id}" name="${o.name}" type="${o.type}" x="${o.x}" y="${o.y}"${size}>${body}${o.w || o.h ? "" : "<point/>"}</object>`;
}).join("\n");
return `<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${this.w}" height="${this.h}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="${this.oid}">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="${this.w}" height="${this.h}">
<data encoding="csv">
${csv(this.ground)}
</data>
</layer>
<layer id="2" name="obstacles" width="${this.w}" height="${this.h}">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
${csv(this.obst)}
</data>
</layer>
<objectgroup id="3" name="objects">
${objs}
</objectgroup>
</map>
`;
}
}
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);
// 花坛 ×3T2 浇水)
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); }
// 莓果丛 ×5T6
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", `<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.12.2" name="tiles" tilewidth="16" tileheight="16" tilecount="80" columns="10">
<image source="../public/assets/tiles.png" width="160" height="128"/>
</tileset>
`);
for (const m of maps) writeFileSync(`maps/${m.name}.tmx`, m.tmx());
console.log("TMX written:", maps.map(m => m.name).join(", "));
+14
View File
@@ -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"]
}