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
+6
View File
@@ -0,0 +1,6 @@
node_modules/
dist/
test-results/
playwright-report/
.DS_Store
.qa/
+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 一次脚本演出)。
+57
View File
@@ -0,0 +1,57 @@
# Broken Fairytale: Honey Village
Single-player browser narrative game built from the specification in `PRD.md`. The runtime uses Phaser 3, TypeScript, and Vite. It has no backend, persistence, external network requests, external fonts, or recorded audio.
## Run
```bash
npm install
npm run generate
npm run dev
```
Production output:
```bash
npm run build
npm run preview
```
The preview server defaults to `http://127.0.0.1:4173`.
## Controls
| Input | Action |
|---|---|
| `WASD` / arrow keys | Move |
| `E` / Enter | Interact, advance dialogue, confirm a choice |
| Up / Down | Select a dialogue choice |
| `1` / `2` / `3` | Select a dialogue choice directly |
| `` ` `` | Toggle the Debug panel |
## Debug panel
The Debug panel is hidden by default and leaves no visible UI while closed. It can set happiness, awareness, and the current day; teleport to any of the ten scenes; discover all nine clues; and force endings E1E4. Debug state exists only in memory and resets when the title flow starts again.
## Original asset pipeline
Every shipped pixel asset was produced for this project by `tools/generate-assets.lua` through Aseprite 1.3.17.2. Editable sources are stored in `public/assets/source/`; exported RGBA sprite sheets are stored in `public/assets/`. The 256×64 tileset contains 64 original 16×16 tiles. `characters.png` contains the player's four directions and four walk frames, seven named NPC expression tiers, five extras, and two-frame bird animation. `portraits.png` contains the fixed smiling player portrait, E3 real-face portrait, and seven named NPC portraits.
Every map was produced for this project by `tools/generate-maps.mjs` and exported through Tiled 1.12.2. Editable TMX sources live in `public/maps/source/`; Tiled JSON exports live in `public/maps/`. Each map contains `ground`, `obstacles`, and `objects` layers. The object layer contains spawn points, paired doors, NPC positions, and interaction points.
The generation commands use the installed macOS application binaries. Custom locations can be supplied without changing the repository:
```bash
ASEPRITE=/path/to/aseprite TILED=/path/to/tiled npm run generate
```
The music loop, bird voice, dialogue beep, task chime, dusk bell, and E2 monitor beep are synthesized at runtime with Web Audio. No audio files are shipped.
## Verification
```bash
npm run build
npm test
```
The Playwright suite checks title/name entry, movement, all ten scene loads, Debug controls, corruption stages, all clues, an ending path, console errors, and integer scaling at 960×540 and 640×360.
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#7acb59" />
<title>崩坏童话:蜜糖村</title>
</head>
<body>
<main id="game" aria-label="崩坏童话:蜜糖村游戏画面"></main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+1258
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "broken-fairytale-honey-village",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"assets": "\"${ASEPRITE:-/Applications/Aseprite.app/Contents/MacOS/aseprite}\" -b --script-param root=\"$PWD\" --script tools/generate-assets.lua",
"maps:source": "node tools/generate-maps.mjs",
"maps": "npm run maps:source && node tools/export-maps.mjs",
"generate": "npm run assets && npm run maps",
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"test": "playwright test"
},
"dependencies": {
"phaser": "^3.90.0"
},
"devDependencies": {
"@playwright/test": "^1.55.0",
"@types/node": "^26.1.1",
"typescript": "^5.8.3",
"vite": "^7.0.0"
}
}
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
expect: { timeout: 5_000 },
use: {
baseURL: 'http://127.0.0.1:4173',
viewport: { width: 960, height: 540 },
deviceScaleFactor: 1,
colorScheme: 'dark',
},
reporter: 'line',
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,319 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4,
4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 13, 5, 5, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 10, 4, 4, 4, 4, 4, 4, 4],
"height":10,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":15,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":240,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":240,
"x":0,
"y":156
},
{
"height":160,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":160,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":236,
"y":0
},
{
"height":32,
"id":910,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":208,
"x":16,
"y":16
},
{
"height":30,
"id":911,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":176,
"y":32
},
{
"height":12,
"id":912,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":176,
"x":32,
"y":80
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":120,
"y":128
},
{
"height":16,
"id":2,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"bakery"
},
{
"name":"target",
"type":"string",
"value":"PlazaScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":120,
"y":144
},
{
"height":16,
"id":3,
"name":"\u5706\u5706",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"yuanyuan"
},
{
"name":"label",
"type":"string",
"value":"\u5706\u5706"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":120,
"y":72
},
{
"height":16,
"id":4,
"name":"\u914d\u65b9\u7eb8",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"recipe"
},
{
"name":"label",
"type":"string",
"value":"\u914d\u65b9\u7eb8"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":48,
"y":46
},
{
"height":16,
"id":5,
"name":"\u70e4\u7089",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"oven"
},
{
"name":"label",
"type":"string",
"value":"\u70e4\u7089"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":184,
"y":50
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u9762\u5305\u623f"
},
{
"name":"sceneKey",
"type":"string",
"value":"BakeryScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":15
}
@@ -0,0 +1,258 @@
{ "compressionlevel":-1,
"height":8,
"infinite":false,
"layers":[
{
"data":[16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 5, 5, 5, 5, 5, 6, 6, 15, 6, 6,
6, 6, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 10, 6, 6, 6, 6, 6],
"height":8,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":12,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":192,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":192,
"x":0,
"y":124
},
{
"height":128,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":128,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":188,
"y":0
},
{
"height":32,
"id":910,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":80,
"x":32,
"y":32
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":96,
"y":96
},
{
"height":16,
"id":2,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"clinic"
},
{
"name":"target",
"type":"string",
"value":"PlazaScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":96,
"y":112
},
{
"height":16,
"id":3,
"name":"\u8299\u533b\u751f",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"doctor"
},
{
"name":"label",
"type":"string",
"value":"\u8299\u533b\u751f"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":96,
"y":50
},
{
"height":16,
"id":4,
"name":"\u684c\u4e0a\u8868\u683c",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"form"
},
{
"name":"label",
"type":"string",
"value":"\u684c\u4e0a\u8868\u683c"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":48,
"y":42
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u8bca\u6240"
},
{
"name":"sceneKey",
"type":"string",
"value":"ClinicScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":12
}
@@ -0,0 +1,338 @@
{ "compressionlevel":-1,
"height":14,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 7, 2, 2, 8, 2, 2, 7, 2, 2, 8, 2, 2, 7, 2, 2, 8, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":14,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":320,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":320,
"x":0,
"y":220
},
{
"height":224,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":224,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":316,
"y":0
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":16,
"y":112
},
{
"height":16,
"id":2,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"field"
},
{
"name":"target",
"type":"string",
"value":"PlazaScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":0,
"y":112
},
{
"height":16,
"id":3,
"name":"\u6751\u6c11",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"extra3"
},
{
"name":"label",
"type":"string",
"value":"\u6751\u6c11"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":244,
"y":148
},
{
"height":16,
"id":4,
"name":"\u8393\u679c\u4e1b",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"berry1"
},
{
"name":"label",
"type":"string",
"value":"\u8393\u679c\u4e1b"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":80,
"y":72
},
{
"height":16,
"id":5,
"name":"\u8393\u679c\u4e1b",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"berry2"
},
{
"name":"label",
"type":"string",
"value":"\u8393\u679c\u4e1b"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":128,
"y":56
},
{
"height":16,
"id":6,
"name":"\u8393\u679c\u4e1b",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"berry3"
},
{
"name":"label",
"type":"string",
"value":"\u8393\u679c\u4e1b"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":176,
"y":88
},
{
"height":16,
"id":7,
"name":"\u8393\u679c\u4e1b",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"berry4"
},
{
"name":"label",
"type":"string",
"value":"\u8393\u679c\u4e1b"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":224,
"y":64
},
{
"height":16,
"id":8,
"name":"\u8393\u679c\u4e1b",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"berry5"
},
{
"name":"label",
"type":"string",
"value":"\u8393\u679c\u4e1b"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":272,
"y":96
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u82b1\u7530"
},
{
"name":"sceneKey",
"type":"string",
"value":"FieldScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":20
}
@@ -0,0 +1,240 @@
{ "compressionlevel":-1,
"height":8,
"infinite":false,
"layers":[
{
"data":[5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 5, 5, 5, 4, 4, 4, 11, 4, 4, 4,
4, 4, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 8, 8, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 10, 5, 5, 5, 5, 5],
"height":8,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":12,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":192,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":192,
"x":0,
"y":124
},
{
"height":128,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":128,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":188,
"y":0
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":96,
"y":88
},
{
"height":16,
"id":2,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"house"
},
{
"name":"target",
"type":"string",
"value":"PlazaScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":96,
"y":112
},
{
"height":16,
"id":3,
"name":"\u5e8a\u5934\u65e5\u5386",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"calendar"
},
{
"name":"label",
"type":"string",
"value":"\u5e8a\u5934\u65e5\u5386"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":42,
"y":34
},
{
"height":16,
"id":4,
"name":"\u5e8a",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"bed"
},
{
"name":"label",
"type":"string",
"value":"\u5e8a"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":64,
"y":42
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u5c0f\u6ee1\u5c0f\u5c4b"
},
{
"name":"sceneKey",
"type":"string",
"value":"HouseScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":12
}
+312
View File
@@ -0,0 +1,312 @@
{ "compressionlevel":-1,
"height":16,
"infinite":false,
"layers":[
{
"data":[3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 5, 5, 5, 8, 8, 8, 1, 1, 1, 12, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":16,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":24,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":384,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":384,
"x":0,
"y":252
},
{
"height":256,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":256,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":380,
"y":0
},
{
"height":256,
"id":910,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":150,
"x":0,
"y":0
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":368,
"y":128
},
{
"height":16,
"id":2,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"lake"
},
{
"name":"target",
"type":"string",
"value":"PlazaScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":368,
"y":128
},
{
"height":16,
"id":3,
"name":"\u9f9f\u7237\u7237",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"turtle"
},
{
"name":"label",
"type":"string",
"value":"\u9f9f\u7237\u7237"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":142,
"y":132
},
{
"height":16,
"id":4,
"name":"\u6751\u6c11",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"extra4"
},
{
"name":"label",
"type":"string",
"value":"\u6751\u6c11"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":284,
"y":68
},
{
"height":16,
"id":5,
"name":"\u91ce\u9910\u5e03",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"picnic"
},
{
"name":"label",
"type":"string",
"value":"\u91ce\u9910\u5e03"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":190,
"y":148
},
{
"height":16,
"id":6,
"name":"\u6e56\u9762",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"lake"
},
{
"name":"label",
"type":"string",
"value":"\u6e56\u9762"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":110,
"y":68
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u6e56\u7554"
},
{
"name":"sceneKey",
"type":"string",
"value":"LakeScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":24
}
+217
View File
@@ -0,0 +1,217 @@
{ "compressionlevel":-1,
"height":8,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15],
"height":8,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":320,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":320,
"x":0,
"y":124
},
{
"height":128,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":128,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":316,
"y":0
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":16,
"y":64
},
{
"height":16,
"id":2,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"mist"
},
{
"name":"target",
"type":"string",
"value":"PlazaScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":0,
"y":64
},
{
"height":16,
"id":3,
"name":"\u96fe\u5899",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"mist"
},
{
"name":"label",
"type":"string",
"value":"\u96fe\u5899"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":288,
"y":64
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u96fe\u5899\u8fb9\u754c"
},
{
"name":"sceneKey",
"type":"string",
"value":"MistScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":20
}
@@ -0,0 +1,965 @@
{ "compressionlevel":-1,
"height":20,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1,
1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1,
1, 13, 4, 4, 4, 4, 4, 4, 4, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4, 13, 1,
1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1,
1, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 13, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1,
1, 1, 4, 4, 4, 10, 4, 4, 4, 13, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 11, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 17, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 8, 2, 8, 2, 8, 2, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
1, 1, 1, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2, 2, 8, 1, 1, 1, 1, 1, 1, 1, 13, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1,
1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1,
1, 1, 4, 4, 4, 4, 4, 4, 4, 1, 13, 1, 1, 2, 2, 2, 2, 1, 1, 13, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1,
1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1,
1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":20,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":480,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":480,
"x":0,
"y":316
},
{
"height":320,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":320,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":476,
"y":0
},
{
"height":96,
"id":910,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":112,
"x":32,
"y":16
},
{
"height":96,
"id":911,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":112,
"x":336,
"y":16
},
{
"height":76,
"id":912,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":112,
"x":32,
"y":224
},
{
"height":76,
"id":913,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":112,
"x":336,
"y":224
},
{
"height":48,
"id":914,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":48,
"x":160,
"y":32
},
{
"height":24,
"id":915,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":232,
"y":128
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":240,
"y":260
},
{
"height":16,
"id":2,
"name":"house",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"house"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":240,
"y":280
},
{
"height":16,
"id":3,
"name":"bakery",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"bakery"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":32,
"y":160
},
{
"height":16,
"id":4,
"name":"shop",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"shop"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":448,
"y":160
},
{
"height":16,
"id":5,
"name":"clinic",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"clinic"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":240,
"y":32
},
{
"height":16,
"id":6,
"name":"lake",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"lake"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":32,
"y":48
},
{
"height":16,
"id":7,
"name":"field",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"field"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":448,
"y":48
},
{
"height":16,
"id":8,
"name":"well",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"well"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":32,
"y":280
},
{
"height":16,
"id":9,
"name":"mist",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"mist"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":448,
"y":280
},
{
"height":16,
"id":10,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"default"
},
{
"name":"target",
"type":"string",
"value":"HouseScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":240,
"y":304
},
{
"height":16,
"id":11,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"default"
},
{
"name":"target",
"type":"string",
"value":"BakeryScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":0,
"y":160
},
{
"height":16,
"id":12,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"default"
},
{
"name":"target",
"type":"string",
"value":"ShopScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":464,
"y":160
},
{
"height":16,
"id":13,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"default"
},
{
"name":"target",
"type":"string",
"value":"ClinicScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":240,
"y":0
},
{
"height":16,
"id":14,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"default"
},
{
"name":"target",
"type":"string",
"value":"LakeScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":0,
"y":48
},
{
"height":16,
"id":15,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"default"
},
{
"name":"target",
"type":"string",
"value":"FieldScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":464,
"y":48
},
{
"height":16,
"id":16,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"default"
},
{
"name":"target",
"type":"string",
"value":"WellScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":0,
"y":280
},
{
"height":16,
"id":17,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"default"
},
{
"name":"target",
"type":"string",
"value":"MistScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":464,
"y":280
},
{
"height":16,
"id":18,
"name":"\u963f\u798f",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"afu"
},
{
"name":"label",
"type":"string",
"value":"\u963f\u798f"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":240,
"y":132
},
{
"height":16,
"id":19,
"name":"\u5927\u5b9d",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"dabao"
},
{
"name":"label",
"type":"string",
"value":"\u5927\u5b9d"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":180,
"y":162
},
{
"height":16,
"id":20,
"name":"\u53cc\u5b50\u9e1f",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"birds"
},
{
"name":"label",
"type":"string",
"value":"\u53cc\u5b50\u9e1f"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":232,
"y":76
},
{
"height":16,
"id":21,
"name":"\u6751\u6c11",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"extra1"
},
{
"name":"label",
"type":"string",
"value":"\u6751\u6c11"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":124,
"y":210
},
{
"height":16,
"id":22,
"name":"\u6751\u6c11",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"extra2"
},
{
"name":"label",
"type":"string",
"value":"\u6751\u6c11"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":352,
"y":224
},
{
"height":16,
"id":23,
"name":"\u516c\u544a\u677f",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"board"
},
{
"name":"label",
"type":"string",
"value":"\u516c\u544a\u677f"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":240,
"y":146
},
{
"height":16,
"id":24,
"name":"\u6563\u843d\u4fe1\u4ef6",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"mail"
},
{
"name":"label",
"type":"string",
"value":"\u6563\u843d\u4fe1\u4ef6"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":92,
"y":148
},
{
"height":16,
"id":25,
"name":"\u82b1\u575b",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"flower1"
},
{
"name":"label",
"type":"string",
"value":"\u82b1\u575b"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":200,
"y":184
},
{
"height":16,
"id":26,
"name":"\u82b1\u575b",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"flower2"
},
{
"name":"label",
"type":"string",
"value":"\u82b1\u575b"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":240,
"y":200
},
{
"height":16,
"id":27,
"name":"\u82b1\u575b",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"flower3"
},
{
"name":"label",
"type":"string",
"value":"\u82b1\u575b"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":280,
"y":184
},
{
"height":16,
"id":28,
"name":"\u5f69\u5e26\u67b6",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"ribbon1"
},
{
"name":"label",
"type":"string",
"value":"\u5f69\u5e26\u67b6"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":160,
"y":100
},
{
"height":16,
"id":29,
"name":"\u5f69\u5e26\u67b6",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"ribbon2"
},
{
"name":"label",
"type":"string",
"value":"\u5f69\u5e26\u67b6"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":224,
"y":92
},
{
"height":16,
"id":30,
"name":"\u5f69\u5e26\u67b6",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"ribbon3"
},
{
"name":"label",
"type":"string",
"value":"\u5f69\u5e26\u67b6"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":288,
"y":92
},
{
"height":16,
"id":31,
"name":"\u5f69\u5e26\u67b6",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"ribbon4"
},
{
"name":"label",
"type":"string",
"value":"\u5f69\u5e26\u67b6"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":352,
"y":100
},
{
"height":16,
"id":32,
"name":"\u5e86\u5178\u573a\u5730",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"festival"
},
{
"name":"label",
"type":"string",
"value":"\u5e86\u5178\u573a\u5730"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":240,
"y":224
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u6751\u5e7f\u573a"
},
{
"name":"sceneKey",
"type":"string",
"value":"PlazaScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":30
}
+332
View File
@@ -0,0 +1,332 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 5, 5, 4, 5, 5, 4, 5, 5, 4, 5, 5, 4, 5, 5,
4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5,
4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5,
4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 10, 4, 4, 4, 4, 4, 4, 4],
"height":10,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":15,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":240,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":240,
"x":0,
"y":156
},
{
"height":160,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":160,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":236,
"y":0
},
{
"height":64,
"id":910,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":16,
"y":16
},
{
"height":64,
"id":911,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":64,
"y":16
},
{
"height":64,
"id":912,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":112,
"y":16
},
{
"height":64,
"id":913,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":160,
"y":16
},
{
"height":64,
"id":914,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":208,
"y":16
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":120,
"y":128
},
{
"height":16,
"id":2,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"shop"
},
{
"name":"target",
"type":"string",
"value":"PlazaScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":120,
"y":144
},
{
"height":16,
"id":3,
"name":"\u54aa\u54aa",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"mimi"
},
{
"name":"label",
"type":"string",
"value":"\u54aa\u54aa"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":120,
"y":72
},
{
"height":16,
"id":4,
"name":"\u8d27\u67b6",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"shelf"
},
{
"name":"label",
"type":"string",
"value":"\u8d27\u67b6"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":48,
"y":52
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u6742\u8d27\u5e97"
},
{
"name":"sceneKey",
"type":"string",
"value":"ShopScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":15
}
+260
View File
@@ -0,0 +1,260 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1,
1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1,
1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1,
1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 10,
1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1,
1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":14,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":224,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":224,
"x":0,
"y":156
},
{
"height":160,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":160,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":220,
"y":0
},
{
"height":64,
"id":910,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":64,
"x":80,
"y":48
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":208,
"y":80
},
{
"height":16,
"id":2,
"name":"undefined",
"opacity":1,
"properties":[
{
"name":"spawn",
"type":"string",
"value":"well"
},
{
"name":"target",
"type":"string",
"value":"PlazaScene"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":208,
"y":80
},
{
"height":16,
"id":3,
"name":"\u6751\u6c11",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"extra5"
},
{
"name":"label",
"type":"string",
"value":"\u6751\u6c11"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":46,
"y":116
},
{
"height":16,
"id":4,
"name":"\u53e4\u4e95",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"well"
},
{
"name":"label",
"type":"string",
"value":"\u53e4\u4e95"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":104,
"y":72
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u53e4\u4e95\u7a7a\u5730"
},
{
"name":"sceneKey",
"type":"string",
"value":"WellScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":14
}
@@ -0,0 +1,212 @@
{ "compressionlevel":-1,
"height":8,
"infinite":false,
"layers":[
{
"data":[16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 5, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16],
"height":8,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"obstacles",
"objects":[
{
"height":4,
"id":900,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":480,
"x":0,
"y":0
},
{
"height":4,
"id":901,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":480,
"x":0,
"y":124
},
{
"height":128,
"id":902,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":0,
"y":0
},
{
"height":128,
"id":903,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":4,
"x":476,
"y":0
},
{
"height":96,
"id":910,
"name":"",
"opacity":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":304,
"y":16
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"default",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"default"
}],
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":24,
"y":64
},
{
"height":16,
"id":2,
"name":"\u75c5\u5386",
"opacity":1,
"properties":[
{
"name":"id",
"type":"string",
"value":"record"
},
{
"name":"label",
"type":"string",
"value":"\u75c5\u5386"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":400,
"y":64
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1000,
"orientation":"orthogonal",
"properties":[
{
"name":"displayName",
"type":"string",
"value":"\u767d\u8272\u5c42"
},
{
"name":"sceneKey",
"type":"string",
"value":"WhiteScene"
}],
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":16,
"firstgid":1,
"image":"..\/assets\/tileset.png",
"imageheight":64,
"imagewidth":256,
"margin":0,
"name":"honey-village",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":30
}
@@ -0,0 +1,44 @@
<?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="1000">
<properties><property name="sceneKey" value="BakeryScene"/><property name="displayName" value="面包房"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="15" height="10"><data encoding="csv">4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,4,4,5,5,5,5,5,5,5,5,5,5,13,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,10,4,4,4,4,4,4,4</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="240" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="156" width="240" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="236" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="910" x="16" y="16" width="208" height="32"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="911" x="176" y="32" width="32" height="30"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="912" x="32" y="80" width="176" height="12"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="120" y="128" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="undefined" type="door" x="120" y="144" width="16" height="16">
<properties>
<property name="target" value="PlazaScene"/>
<property name="spawn" value="bakery"/>
</properties>
</object>
<object id="3" name="圆圆" type="npc" x="120" y="72" width="16" height="16">
<properties>
<property name="id" value="yuanyuan"/>
<property name="label" value="圆圆"/>
</properties>
</object>
<object id="4" name="配方纸" type="interact" x="48" y="46" width="16" height="16">
<properties>
<property name="id" value="recipe"/>
<property name="label" value="配方纸"/>
</properties>
</object>
<object id="5" name="烤炉" type="interact" x="184" y="50" width="16" height="16">
<properties>
<property name="id" value="oven"/>
<property name="label" value="烤炉"/>
</properties>
</object>
</objectgroup>
</map>
@@ -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="1000">
<properties><property name="sceneKey" value="ClinicScene"/><property name="displayName" value="诊所"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="12" height="8"><data encoding="csv">16,16,16,16,16,16,16,16,16,16,16,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,5,5,5,5,5,6,6,15,6,6,6,6,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,10,6,6,6,6,6</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="192" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="124" width="192" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="188" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="910" x="32" y="32" width="80" height="32"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="96" y="96" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="undefined" type="door" x="96" y="112" width="16" height="16">
<properties>
<property name="target" value="PlazaScene"/>
<property name="spawn" value="clinic"/>
</properties>
</object>
<object id="3" name="芙医生" type="npc" x="96" y="50" width="16" height="16">
<properties>
<property name="id" value="doctor"/>
<property name="label" value="芙医生"/>
</properties>
</object>
<object id="4" name="桌上表格" type="interact" x="48" y="42" width="16" height="16">
<properties>
<property name="id" value="form"/>
<property name="label" value="桌上表格"/>
</properties>
</object>
</objectgroup>
</map>
@@ -0,0 +1,59 @@
<?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="1000">
<properties><property name="sceneKey" value="FieldScene"/><property name="displayName" value="花田"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="20" height="14"><data encoding="csv">1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,7,2,2,8,2,2,7,2,2,8,2,2,7,2,2,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="320" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="220" width="320" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="224"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="316" y="0" width="4" height="224"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="16" y="112" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="undefined" type="door" x="0" y="112" width="16" height="16">
<properties>
<property name="target" value="PlazaScene"/>
<property name="spawn" value="field"/>
</properties>
</object>
<object id="3" name="村民" type="npc" x="244" y="148" width="16" height="16">
<properties>
<property name="id" value="extra3"/>
<property name="label" value="村民"/>
</properties>
</object>
<object id="4" name="莓果丛" type="interact" x="80" y="72" width="16" height="16">
<properties>
<property name="id" value="berry1"/>
<property name="label" value="莓果丛"/>
</properties>
</object>
<object id="5" name="莓果丛" type="interact" x="128" y="56" width="16" height="16">
<properties>
<property name="id" value="berry2"/>
<property name="label" value="莓果丛"/>
</properties>
</object>
<object id="6" name="莓果丛" type="interact" x="176" y="88" width="16" height="16">
<properties>
<property name="id" value="berry3"/>
<property name="label" value="莓果丛"/>
</properties>
</object>
<object id="7" name="莓果丛" type="interact" x="224" y="64" width="16" height="16">
<properties>
<property name="id" value="berry4"/>
<property name="label" value="莓果丛"/>
</properties>
</object>
<object id="8" name="莓果丛" type="interact" x="272" y="96" width="16" height="16">
<properties>
<property name="id" value="berry5"/>
<property name="label" value="莓果丛"/>
</properties>
</object>
</objectgroup>
</map>
@@ -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="1000">
<properties><property name="sceneKey" value="HouseScene"/><property name="displayName" value="小满小屋"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="12" height="8"><data encoding="csv">5,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,4,4,4,11,4,4,4,4,4,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,8,8,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,10,5,5,5,5,5</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="192" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="124" width="192" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="188" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="96" y="88" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="undefined" type="door" x="96" y="112" width="16" height="16">
<properties>
<property name="target" value="PlazaScene"/>
<property name="spawn" value="house"/>
</properties>
</object>
<object id="3" name="床头日历" type="interact" x="42" y="34" width="16" height="16">
<properties>
<property name="id" value="calendar"/>
<property name="label" value="床头日历"/>
</properties>
</object>
<object id="4" name="床" type="interact" x="64" y="42" width="16" height="16">
<properties>
<property name="id" value="bed"/>
<property name="label" value="床"/>
</properties>
</object>
</objectgroup>
</map>
@@ -0,0 +1,48 @@
<?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="1000">
<properties><property name="sceneKey" value="LakeScene"/><property name="displayName" value="湖畔"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="24" height="16"><data encoding="csv">3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,2,5,5,5,8,8,8,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,1,5,5,5,8,8,8,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="384" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="252" width="384" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="256"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="380" y="0" width="4" height="256"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="910" x="0" y="0" width="150" height="256"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="368" y="128" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="undefined" type="door" x="368" y="128" width="16" height="16">
<properties>
<property name="target" value="PlazaScene"/>
<property name="spawn" value="lake"/>
</properties>
</object>
<object id="3" name="龟爷爷" type="npc" x="142" y="132" width="16" height="16">
<properties>
<property name="id" value="turtle"/>
<property name="label" value="龟爷爷"/>
</properties>
</object>
<object id="4" name="村民" type="npc" x="284" y="68" width="16" height="16">
<properties>
<property name="id" value="extra4"/>
<property name="label" value="村民"/>
</properties>
</object>
<object id="5" name="野餐布" type="interact" x="190" y="148" width="16" height="16">
<properties>
<property name="id" value="picnic"/>
<property name="label" value="野餐布"/>
</properties>
</object>
<object id="6" name="湖面" type="interact" x="110" y="68" width="16" height="16">
<properties>
<property name="id" value="lake"/>
<property name="label" value="湖面"/>
</properties>
</object>
</objectgroup>
</map>
@@ -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="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1000">
<properties><property name="sceneKey" value="MistScene"/><property name="displayName" value="雾墙边界"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="20" height="8"><data encoding="csv">1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="320" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="124" width="320" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="316" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="16" y="64" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="undefined" type="door" x="0" y="64" width="16" height="16">
<properties>
<property name="target" value="PlazaScene"/>
<property name="spawn" value="mist"/>
</properties>
</object>
<object id="3" name="雾墙" type="interact" x="288" y="64" width="16" height="16">
<properties>
<property name="id" value="mist"/>
<property name="label" value="雾墙"/>
</properties>
</object>
</objectgroup>
</map>
@@ -0,0 +1,201 @@
<?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="1000">
<properties><property name="sceneKey" value="PlazaScene"/><property name="displayName" value="村广场"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="30" height="20"><data encoding="csv">1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,9,9,9,9,9,9,9,1,12,12,12,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,13,4,4,4,4,4,4,4,1,12,12,12,2,2,2,2,1,1,1,1,4,4,4,4,4,4,4,13,1,1,1,4,15,4,4,15,4,4,1,12,12,12,2,2,2,2,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,4,4,4,4,4,4,4,1,1,1,1,2,2,2,2,1,1,13,1,4,4,4,4,4,4,4,1,1,1,1,4,4,4,10,4,4,4,13,1,1,1,2,2,2,2,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,11,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,17,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,8,2,8,2,8,2,8,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,13,1,1,1,1,1,1,1,1,8,2,2,2,8,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,4,4,4,4,4,4,4,1,13,1,1,2,2,2,2,1,1,13,1,4,4,4,4,4,4,4,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,2,2,2,2,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,2,2,2,2,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="480" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="316" width="480" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="320"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="476" y="0" width="4" height="320"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="910" x="32" y="16" width="112" height="96"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="911" x="336" y="16" width="112" height="96"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="912" x="32" y="224" width="112" height="76"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="913" x="336" y="224" width="112" height="76"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="914" x="160" y="32" width="48" height="48"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="915" x="232" y="128" width="32" height="24"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="240" y="260" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="house" type="spawn" x="240" y="280" width="16" height="16">
<properties>
<property name="id" value="house"/>
</properties>
</object>
<object id="3" name="bakery" type="spawn" x="32" y="160" width="16" height="16">
<properties>
<property name="id" value="bakery"/>
</properties>
</object>
<object id="4" name="shop" type="spawn" x="448" y="160" width="16" height="16">
<properties>
<property name="id" value="shop"/>
</properties>
</object>
<object id="5" name="clinic" type="spawn" x="240" y="32" width="16" height="16">
<properties>
<property name="id" value="clinic"/>
</properties>
</object>
<object id="6" name="lake" type="spawn" x="32" y="48" width="16" height="16">
<properties>
<property name="id" value="lake"/>
</properties>
</object>
<object id="7" name="field" type="spawn" x="448" y="48" width="16" height="16">
<properties>
<property name="id" value="field"/>
</properties>
</object>
<object id="8" name="well" type="spawn" x="32" y="280" width="16" height="16">
<properties>
<property name="id" value="well"/>
</properties>
</object>
<object id="9" name="mist" type="spawn" x="448" y="280" width="16" height="16">
<properties>
<property name="id" value="mist"/>
</properties>
</object>
<object id="10" name="undefined" type="door" x="240" y="304" width="16" height="16">
<properties>
<property name="target" value="HouseScene"/>
<property name="spawn" value="default"/>
</properties>
</object>
<object id="11" name="undefined" type="door" x="0" y="160" width="16" height="16">
<properties>
<property name="target" value="BakeryScene"/>
<property name="spawn" value="default"/>
</properties>
</object>
<object id="12" name="undefined" type="door" x="464" y="160" width="16" height="16">
<properties>
<property name="target" value="ShopScene"/>
<property name="spawn" value="default"/>
</properties>
</object>
<object id="13" name="undefined" type="door" x="240" y="0" width="16" height="16">
<properties>
<property name="target" value="ClinicScene"/>
<property name="spawn" value="default"/>
</properties>
</object>
<object id="14" name="undefined" type="door" x="0" y="48" width="16" height="16">
<properties>
<property name="target" value="LakeScene"/>
<property name="spawn" value="default"/>
</properties>
</object>
<object id="15" name="undefined" type="door" x="464" y="48" width="16" height="16">
<properties>
<property name="target" value="FieldScene"/>
<property name="spawn" value="default"/>
</properties>
</object>
<object id="16" name="undefined" type="door" x="0" y="280" width="16" height="16">
<properties>
<property name="target" value="WellScene"/>
<property name="spawn" value="default"/>
</properties>
</object>
<object id="17" name="undefined" type="door" x="464" y="280" width="16" height="16">
<properties>
<property name="target" value="MistScene"/>
<property name="spawn" value="default"/>
</properties>
</object>
<object id="18" name="阿福" type="npc" x="240" y="132" width="16" height="16">
<properties>
<property name="id" value="afu"/>
<property name="label" value="阿福"/>
</properties>
</object>
<object id="19" name="大宝" type="npc" x="180" y="162" width="16" height="16">
<properties>
<property name="id" value="dabao"/>
<property name="label" value="大宝"/>
</properties>
</object>
<object id="20" name="双子鸟" type="npc" x="232" y="76" width="16" height="16">
<properties>
<property name="id" value="birds"/>
<property name="label" value="双子鸟"/>
</properties>
</object>
<object id="21" name="村民" type="npc" x="124" y="210" width="16" height="16">
<properties>
<property name="id" value="extra1"/>
<property name="label" value="村民"/>
</properties>
</object>
<object id="22" name="村民" type="npc" x="352" y="224" width="16" height="16">
<properties>
<property name="id" value="extra2"/>
<property name="label" value="村民"/>
</properties>
</object>
<object id="23" name="公告板" type="interact" x="240" y="146" width="16" height="16">
<properties>
<property name="id" value="board"/>
<property name="label" value="公告板"/>
</properties>
</object>
<object id="24" name="散落信件" type="interact" x="92" y="148" width="16" height="16">
<properties>
<property name="id" value="mail"/>
<property name="label" value="散落信件"/>
</properties>
</object>
<object id="25" name="花坛" type="interact" x="200" y="184" width="16" height="16">
<properties>
<property name="id" value="flower1"/>
<property name="label" value="花坛"/>
</properties>
</object>
<object id="26" name="花坛" type="interact" x="240" y="200" width="16" height="16">
<properties>
<property name="id" value="flower2"/>
<property name="label" value="花坛"/>
</properties>
</object>
<object id="27" name="花坛" type="interact" x="280" y="184" width="16" height="16">
<properties>
<property name="id" value="flower3"/>
<property name="label" value="花坛"/>
</properties>
</object>
<object id="28" name="彩带架" type="interact" x="160" y="100" width="16" height="16">
<properties>
<property name="id" value="ribbon1"/>
<property name="label" value="彩带架"/>
</properties>
</object>
<object id="29" name="彩带架" type="interact" x="224" y="92" width="16" height="16">
<properties>
<property name="id" value="ribbon2"/>
<property name="label" value="彩带架"/>
</properties>
</object>
<object id="30" name="彩带架" type="interact" x="288" y="92" width="16" height="16">
<properties>
<property name="id" value="ribbon3"/>
<property name="label" value="彩带架"/>
</properties>
</object>
<object id="31" name="彩带架" type="interact" x="352" y="100" width="16" height="16">
<properties>
<property name="id" value="ribbon4"/>
<property name="label" value="彩带架"/>
</properties>
</object>
<object id="32" name="庆典场地" type="interact" x="240" y="224" width="16" height="16">
<properties>
<property name="id" value="festival"/>
<property name="label" value="庆典场地"/>
</properties>
</object>
</objectgroup>
</map>
@@ -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="1000">
<properties><property name="sceneKey" value="ShopScene"/><property name="displayName" value="杂货店"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="15" height="10"><data encoding="csv">4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,4,5,5,4,5,5,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,10,4,4,4,4,4,4,4</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="240" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="156" width="240" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="236" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="910" x="16" y="16" width="32" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="911" x="64" y="16" width="32" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="912" x="112" y="16" width="32" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="913" x="160" y="16" width="32" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="914" x="208" y="16" width="16" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="120" y="128" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="undefined" type="door" x="120" y="144" width="16" height="16">
<properties>
<property name="target" value="PlazaScene"/>
<property name="spawn" value="shop"/>
</properties>
</object>
<object id="3" name="咪咪" type="npc" x="120" y="72" width="16" height="16">
<properties>
<property name="id" value="mimi"/>
<property name="label" value="咪咪"/>
</properties>
</object>
<object id="4" name="货架" type="interact" x="48" y="52" width="16" height="16">
<properties>
<property name="id" value="shelf"/>
<property name="label" value="货架"/>
</properties>
</object>
</objectgroup>
</map>
@@ -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="14" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1000">
<properties><property name="sceneKey" value="WellScene"/><property name="displayName" value="古井空地"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="14" height="10"><data encoding="csv">1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,10,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="224" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="156" width="224" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="220" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="910" x="80" y="48" width="64" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="208" y="80" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="undefined" type="door" x="208" y="80" width="16" height="16">
<properties>
<property name="target" value="PlazaScene"/>
<property name="spawn" value="well"/>
</properties>
</object>
<object id="3" name="村民" type="npc" x="46" y="116" width="16" height="16">
<properties>
<property name="id" value="extra5"/>
<property name="label" value="村民"/>
</properties>
</object>
<object id="4" name="古井" type="interact" x="104" y="72" width="16" height="16">
<properties>
<property name="id" value="well"/>
<property name="label" value="古井"/>
</properties>
</object>
</objectgroup>
</map>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1000">
<properties><property name="sceneKey" value="WhiteScene"/><property name="displayName" value="白色层"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="30" height="8"><data encoding="csv">16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16</data></layer>
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="480" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="901" x="0" y="124" width="480" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="902" x="0" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="903" x="476" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
<object id="910" x="304" y="16" width="16" height="96"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
<objectgroup id="3" name="objects">
<object id="1" name="default" type="spawn" x="24" y="64" width="16" height="16">
<properties>
<property name="id" value="default"/>
</properties>
</object>
<object id="2" name="病历" type="interact" x="400" y="64" width="16" height="16">
<properties>
<property name="id" value="record"/>
<property name="label" value="病历"/>
</properties>
</object>
</objectgroup>
</map>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.12.2" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16">
<image source="../../assets/tileset.png" width="256" height="64"/>
</tileset>
+116
View File
@@ -0,0 +1,116 @@
import { director } from './CorruptionDirector';
const RATE = { S0: 1, S1: 1, S2: 0.97, S3: 0.92, S4: 0.84 } as const;
const CUTOFF = { S0: 20_000, S1: 20_000, S2: 20_000, S3: 8_000, S4: 4_000 } as const;
export class AudioDirector {
private context?: AudioContext;
private music?: AudioBufferSourceNode;
private birds?: AudioBufferSourceNode;
private musicGain?: GainNode;
private birdGain?: GainNode;
private filter?: BiquadFilterNode;
private master?: GainNode;
unlock(): void {
if (!this.context) this.create();
void this.context?.resume();
}
private create(): void {
const context = new AudioContext();
this.context = context;
this.master = context.createGain();
this.master.gain.value = 0.28;
this.master.connect(context.destination);
this.filter = context.createBiquadFilter();
this.filter.type = 'lowpass';
this.filter.connect(this.master);
this.musicGain = context.createGain();
this.musicGain.gain.value = 0.34;
this.musicGain.connect(this.filter);
this.birdGain = context.createGain();
this.birdGain.gain.value = 0.12;
this.birdGain.connect(this.master);
this.music = context.createBufferSource();
this.music.buffer = this.makeMusicBuffer(context);
this.music.loop = true;
this.music.connect(this.musicGain);
this.music.start();
this.birds = context.createBufferSource();
this.birds.buffer = this.makeBirdBuffer(context);
this.birds.loop = true;
this.birds.connect(this.birdGain);
this.birds.start();
this.applyCorruption();
director.on('change', this.applyCorruption, this);
}
private makeMusicBuffer(context: AudioContext): AudioBuffer {
const sampleRate = context.sampleRate;
const duration = 16;
const buffer = context.createBuffer(1, sampleRate * duration, sampleRate);
const data = buffer.getChannelData(0);
const melody = [523.25, 659.25, 783.99, 659.25, 587.33, 698.46, 880, 698.46, 523.25, 659.25, 783.99, 1046.5, 880, 783.99, 659.25, 587.33];
const bass = [130.81, 146.83, 164.81, 146.83, 130.81, 174.61, 146.83, 130.81];
for (let i = 0; i < data.length; i += 1) {
const t = i / sampleRate;
const note = melody[Math.floor(t * 2) % melody.length];
const low = bass[Math.floor(t / 2) % bass.length];
const envelope = Math.min(1, (t % 0.5) * 20) * Math.max(0, 1 - (t % 0.5) * 0.9);
const square = Math.sin(2 * Math.PI * note * t) >= 0 ? 1 : -1;
const triangle = 2 * Math.abs(2 * ((t * low) % 1) - 1) - 1;
data[i] = (square * 0.17 * envelope) + (triangle * 0.12);
}
return buffer;
}
private makeBirdBuffer(context: AudioContext): AudioBuffer {
const duration = 4;
const buffer = context.createBuffer(1, context.sampleRate * duration, context.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i += 1) {
const t = i / context.sampleRate;
const active = (t > 0.45 && t < 0.6) || (t > 0.75 && t < 0.9) || (t > 2.3 && t < 2.52);
if (active) data[i] = Math.sin(2 * Math.PI * (1250 + 500 * Math.sin(t * 35)) * t) * 0.25;
}
return buffer;
}
private applyCorruption(): void {
if (!this.context || !this.music || !this.filter || !this.birdGain) return;
const now = this.context.currentTime;
this.music.playbackRate.setTargetAtTime(RATE[director.stage], now, 0.1);
this.filter.frequency.setTargetAtTime(CUTOFF[director.stage], now, 0.1);
this.birdGain.gain.setTargetAtTime(director.stage === 'S4' ? 0 : 0.12, now, 0.1);
}
fadeForSmile(seconds: number): void {
if (!this.context || !this.master) return;
const now = this.context.currentTime;
this.master.gain.cancelScheduledValues(now);
if (seconds < 40) this.master.gain.setTargetAtTime(0.28, now, 0.2);
else this.master.gain.linearRampToValueAtTime(Math.max(0, 0.28 * (60 - seconds) / 20), now + 0.12);
}
restore(): void {
if (!this.context || !this.master) return;
this.master.gain.setTargetAtTime(0.28, this.context.currentTime, 0.3);
}
beep(kind: 'dialogue' | 'task' | 'bell' | 'monitor'): void {
if (!this.context || !this.master) return;
const frequency = { dialogue: 740, task: 1046, bell: 392, monitor: 880 }[kind];
const duration = { dialogue: 0.035, task: 0.28, bell: 1.1, monitor: 0.22 }[kind];
const oscillator = this.context.createOscillator();
const gain = this.context.createGain();
oscillator.type = kind === 'bell' ? 'sine' : 'square';
oscillator.frequency.value = frequency;
gain.gain.setValueAtTime(kind === 'dialogue' ? 0.035 : 0.12, this.context.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, this.context.currentTime + duration);
oscillator.connect(gain); gain.connect(this.master);
oscillator.start(); oscillator.stop(this.context.currentTime + duration);
}
}
export const audio = new AudioDirector();
@@ -0,0 +1,279 @@
import Phaser from 'phaser';
import { CLUES, CORRUPTIONS, NPCS, TASKS, type EndingId, type NpcId, type Stage } from './data';
interface RuntimeState {
playerName: string;
startTime: number;
happiness: number;
awareness: number;
day: number;
dayStartedAt: number;
boardRead: boolean;
sunset: boolean;
clues: Set<string>;
completed: Set<string>;
refused: Set<string>;
flags: Set<string>;
counters: Map<string, number>;
dialogueCounts: Map<string, number>;
overflow: number;
cheerEscapes: number;
cheerActive: boolean;
lowSince: number | null;
lowRouteValid: boolean;
dialogueOpen: boolean;
t14Active: boolean;
festivalStarted: boolean;
}
const createState = (): RuntimeState => ({
playerName: '小满', startTime: performance.now(), happiness: 30, awareness: 0, day: 1, dayStartedAt: performance.now(),
boardRead: false, sunset: false, clues: new Set(), completed: new Set(), refused: new Set(),
flags: new Set(), counters: new Map(), dialogueCounts: new Map(), overflow: 0,
cheerEscapes: 0, cheerActive: false, lowSince: null, lowRouteValid: true,
dialogueOpen: false, t14Active: false, festivalStarted: false,
});
export class CorruptionDirector extends Phaser.Events.EventEmitter {
private state = createState();
private renderCount = 0;
get snapshot(): Readonly<RuntimeState> { return this.state; }
get stage(): Stage { return this.stageFor(this.state.happiness); }
reset(playerName = '小满'): void {
this.state = createState();
this.renderCount = 0;
this.state.playerName = playerName.trim() || '小满';
this.emit('reset');
this.emitChange();
}
stageFor(value: number): Stage {
if (value <= 20) return 'S0';
if (value <= 50) return 'S1';
if (value <= 80) return 'S2';
if (value <= 99) return 'S3';
return 'S4';
}
npcExpression(id: NpcId): 'smile' | 'flat' | 'off' {
if (id === 'doctor' || id === 'turtle') return 'smile';
const effective = this.state.happiness - NPCS[id].offset;
const npcStage = this.stageFor(Math.max(0, effective));
if (npcStage === 'S4') return 'flat';
if (npcStage === 'S3') return 'flat';
return 'smile';
}
setHappiness(value: number): void {
const wasFull = this.state.happiness === 100;
this.state.happiness = Phaser.Math.Clamp(Math.round(value), 0, 100);
if (!wasFull && this.state.happiness === 100 && !this.state.flags.has('first-full')) {
this.state.flags.add('first-full');
this.emit('first-full');
}
this.checkLowTimer();
this.emitChange();
}
adjustHappiness(delta: number): void {
if (this.state.happiness === 100 && delta < 0 && Math.abs(delta) <= 5) {
this.state.overflow += Math.abs(delta);
if (this.state.overflow >= 15) {
this.state.overflow -= 15;
this.emit('negative-flash');
}
this.emitChange();
return;
}
this.setHappiness(this.state.happiness + delta);
}
setAwareness(value: number): void {
this.state.awareness = Phaser.Math.Clamp(Math.round(value), 0, 100);
this.emitChange();
}
adjustAwareness(delta: number): void { this.setAwareness(this.state.awareness + Math.max(0, delta)); }
setDay(day: number): void {
this.state.day = Phaser.Math.Clamp(Math.round(day), 1, 7);
this.state.boardRead = false;
this.state.sunset = false;
this.state.dayStartedAt = performance.now();
this.state.dialogueCounts.clear();
if (this.state.day >= 5 && this.state.happiness > 10) this.state.lowRouteValid = false;
this.emitChange();
}
acceptDay(): void {
this.state.boardRead = true;
this.emitChange();
}
tasksForDay(day = this.state.day) { return TASKS.filter((task) => task.day === day); }
isTaskActive(id: string): boolean {
const task = TASKS.find((item) => item.id === id);
return Boolean(task && task.day === this.state.day && this.state.boardRead && !this.state.completed.has(id) && !this.state.refused.has(id));
}
completeTask(id: string): boolean {
const task = TASKS.find((item) => item.id === id);
if (!task || this.state.completed.has(id) || this.state.refused.has(id)) return false;
this.state.completed.add(id);
this.adjustHappiness(task.reward);
this.emit('task-complete', task);
this.checkDayComplete();
return true;
}
refuseTask(id: string): void {
const task = TASKS.find((item) => item.id === id && item.optional);
if (!task || this.state.completed.has(id) || this.state.refused.has(id)) return;
this.state.refused.add(id);
this.sadBehavior(`refuse-${id}`);
this.emit('task-refused', task);
this.checkDayComplete();
}
private checkDayComplete(): void {
if (this.state.day === 7) return;
const done = this.tasksForDay().every((task) => this.state.completed.has(task.id) || this.state.refused.has(task.id));
if (done && !this.state.sunset && performance.now() - this.state.dayStartedAt >= 240_000) {
this.state.sunset = true;
this.emit('sunset');
this.emitChange();
}
}
counter(key: string): number { return this.state.counters.get(`${this.state.day}:${key}`) ?? 0; }
increment(key: string, cap = Number.POSITIVE_INFINITY): number {
const id = `${this.state.day}:${key}`;
const value = Math.min(cap, (this.state.counters.get(id) ?? 0) + 1);
this.state.counters.set(id, value);
this.emitChange();
return value;
}
smallInteraction(kind: 'water' | 'feed' | 'pet'): boolean {
const key = `small-${kind}`;
if (this.counter(key) >= 3) return false;
this.increment(key, 3);
this.adjustHappiness(1);
this.emit('small-interaction', kind);
return true;
}
flag(key: string): boolean { return this.state.flags.has(`${this.state.day}:${key}`) || this.state.flags.has(key); }
setFlag(key: string, global = false): void {
this.state.flags.add(global ? key : `${this.state.day}:${key}`);
this.emitChange();
}
discoverClue(id: string): boolean {
if (!CLUES[id] || this.state.clues.has(id)) return false;
this.state.clues.add(id);
this.adjustAwareness(8);
this.emit('clue', id, CLUES[id]);
return true;
}
discoverAllClues(): void {
Object.keys(CLUES).forEach((id) => this.state.clues.add(id));
this.setAwareness(Math.max(this.state.awareness, 72));
}
sadBehavior(key: string): boolean {
const scoped = `${this.state.day}:sad:${key}`;
if (this.state.flags.has(scoped)) return false;
this.state.flags.add(scoped);
this.adjustHappiness(-5);
this.adjustAwareness(3);
this.emit('sad-behavior', key);
return true;
}
recordDialogue(npc: string): number {
const count = (this.state.dialogueCounts.get(npc) ?? 0) + 1;
this.state.dialogueCounts.set(npc, count);
if (count === 5) this.sadBehavior(`cling-${npc}`);
return count;
}
corruptText(text: string, speaker: string): string {
this.renderCount += 1;
const stage = this.stage;
if (stage === 'S0' || stage === 'S1') return text;
if (stage === 'S2') return this.renderCount % 80 === 0 ? text.replace(/[。!?]/, '$&$&') : text;
const interval = stage === 'S4' ? 18 : 50;
const prioritized = speaker === '咪咪';
let result = text;
if (prioritized || this.renderCount % interval === 0) {
const pair = CORRUPTIONS.find(([clean]) => result.includes(clean));
if (pair) result = result.replace(pair[0], pair[1]);
}
if (stage === 'S4' && speaker === '咪咪') result = result.replace(/([。!?])$/, '$1$1$1');
return result;
}
setDialogueOpen(open: boolean): void { this.state.dialogueOpen = open; }
setT14(active: boolean): void { this.state.t14Active = active; this.emitChange(); }
setFestivalStarted(): void { this.state.festivalStarted = true; this.emitChange(); }
advanceDay(): void {
if (!this.state.sunset || this.state.day >= 7) return;
this.setDay(this.state.day + 1);
}
elapsedLabel(): string {
const total = Math.max(0, Math.floor((performance.now() - this.state.startTime) / 1000));
const minutes = Math.floor(total / 60).toString().padStart(2, '0');
const seconds = (total % 60).toString().padStart(2, '0');
return `${minutes}:${seconds}`;
}
ending(): EndingId | undefined {
if (this.state.day >= 5 && this.state.happiness <= 10 && this.state.cheerEscapes >= 3 && this.state.lowRouteValid) return 'E4';
if (this.state.awareness >= 70 && this.state.clues.size >= 6) return 'E3';
if (this.state.awareness >= 30 && this.state.awareness <= 69) return 'E2';
if (this.state.awareness <= 29 && this.state.happiness >= 90) return 'E1';
return undefined;
}
tick(): void {
this.checkLowTimer();
this.checkDayComplete();
if (this.stage === 'S4' && Math.random() < 0.4 / 3600) this.emit('negative-flash');
}
private checkLowTimer(): void {
const now = performance.now();
if (this.state.day >= 5 && this.state.happiness > 10) this.state.lowRouteValid = false;
if (this.state.happiness < 30 && !this.state.cheerActive) {
this.state.lowSince ??= now;
if (now - this.state.lowSince >= 120_000) {
this.state.cheerActive = true;
this.emit('forced-cheer');
}
} else if (this.state.happiness >= 30) this.state.lowSince = null;
}
escapeCheer(): void {
if (!this.state.cheerActive) return;
this.state.cheerActive = false;
if (this.state.day >= 5) this.state.cheerEscapes += 1;
this.state.lowSince = performance.now();
this.emitChange();
}
finishCheer(): void {
if (!this.state.cheerActive) return;
this.state.cheerActive = false;
this.setHappiness(50);
}
private emitChange(): void { this.emit('change', this.state); }
}
export const director = new CorruptionDirector();
+94
View File
@@ -0,0 +1,94 @@
export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4';
export type EndingId = 'E1' | 'E2' | 'E3' | 'E4';
export interface TaskDefinition {
id: string;
day: number;
title: string;
reward: number;
optional?: boolean;
}
export const ANNOUNCEMENTS: Record<number, string> = {
1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
2: '早上好呀!新的一天,新的幸福。祝你开心!',
3: '早上好!今天要把幸福装得满满的哦。要开心!',
4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。',
6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。',
7: '庆典开始了。来吧。',
};
export const TASKS: TaskDefinition[] = [
{ id: 'T1', day: 1, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15 },
{ id: 'T2', day: 1, title: '给广场花坛浇水 ×3', reward: 10 },
{ id: 'T3', day: 1, title: '与任意三位村民问好', reward: 10 },
{ id: 'T4', day: 2, title: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)', reward: 15 },
{ id: 'T5', day: 2, title: '帮咪咪清点货架', reward: 10 },
{ id: 'T6', day: 3, title: '去花田采莓果 ×5', reward: 10 },
{ id: 'T7', day: 3, title: '湖畔野餐(走到野餐布触发过场)', reward: 15 },
{ id: 'T8', day: 4, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15 },
{ id: 'T9', day: 4, title: '给广场花坛浇水 ×3', reward: 10 },
{ id: 'T10', day: 4, title: '与任意三位村民问好', reward: 10 },
{ id: 'T11', day: 5, title: '帮圆圆揉面', reward: 10, optional: true },
{ id: 'T12', day: 5, title: '擦拭公告板', reward: 10, optional: true },
{ id: 'T13', day: 6, title: '在广场挂彩带 ×4', reward: 10 },
{ id: 'T14', day: 6, title: '保持微笑 60 秒', reward: 15 },
{ id: 'T15', day: 7, title: '参加第 413 届丰收庆典', reward: 0 },
];
export const CLUES: Record<string, string> = {
C1: '每一页都是同一个日期',
C2: '手写小字:「按疗程配比。甜度:最大。」',
C3: '所有邮戳是同一天',
C4: '署名闪过乱码「PT-0▓」',
C5: '同一商品重复排列,标签全部空白',
C6: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」',
C7: '井底回声:极轻的监护仪滴声(靠近并交互 5 秒)',
C8: '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」',
C9: '三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」',
};
export const C9_LINES: Record<number, string> = {
2: '湖水一直很平静',
4: '面包一直是那个味道',
6: '孩子,今天是第几个今天?',
};
export const CORRUPTIONS: Array<[string, string]> = [
['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'],
['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'],
];
export const NPCS = {
afu: { name: '阿福', portrait: 2, frame: 16, offset: 0 },
yuanyuan: { name: '圆圆', portrait: 3, frame: 19, offset: 15 },
dabao: { name: '大宝', portrait: 4, frame: 22, offset: 0 },
mimi: { name: '咪咪', portrait: 5, frame: 25, offset: -10 },
doctor: { name: '芙医生', portrait: 6, frame: 28, offset: 999 },
birds: { name: '双子鸟', portrait: 7, frame: 31, offset: 0 },
turtle: { name: '龟爷爷', portrait: 8, frame: 34, offset: 999 },
} as const;
export type NpcId = keyof typeof NPCS;
export const MAP_KEYS = [
'HouseScene', 'PlazaScene', 'BakeryScene', 'ShopScene', 'ClinicScene',
'LakeScene', 'FieldScene', 'WellScene', 'MistScene', 'WhiteScene',
] as const;
export const MAP_LABELS: Record<(typeof MAP_KEYS)[number], string> = {
HouseScene: '小满小屋', PlazaScene: '村广场', BakeryScene: '面包房', ShopScene: '杂货店',
ClinicScene: '诊所', LakeScene: '湖畔', FieldScene: '花田', WellScene: '古井空地',
MistScene: '雾墙边界', WhiteScene: '白色层',
};
export const NPC_CHAT: Record<NpcId, string[]> = {
afu: ['今天也要把幸福装满哦。', '微笑会让大家都安心。'],
yuanyuan: ['刚出炉的蜂蜜面包最甜啦。', '只要按配方做,每次都会一样美好。'],
dabao: ['每一封信都会准时送到。', '名册上写着的住户,一个也不会少。'],
mimi: ['货架永远整整齐齐,真让人开心。', '朋友需要什么,我这里都有哦。'],
doctor: ['村里有诊所,但你不需要去那里哦。'],
birds: ['啾、啾。旋律每天都一样好听。'],
turtle: ['湖水一直很平静。'],
};
@@ -0,0 +1,24 @@
import Phaser from 'phaser';
import { MAP_KEYS } from '../data';
export class BootScene extends Phaser.Scene {
constructor() { super('BootScene'); }
preload(): void {
this.load.image('tiles', 'assets/tileset.png');
this.load.spritesheet('characters', 'assets/characters.png', { frameWidth: 16, frameHeight: 24 });
this.load.spritesheet('portraits', 'assets/portraits.png', { frameWidth: 48, frameHeight: 48 });
MAP_KEYS.forEach((key) => this.load.tilemapTiledJSON(key, `maps/${key}.json`));
}
create(): void {
const directionFrames = [
['walk-down', 0], ['walk-up', 4], ['walk-left', 8], ['walk-right', 12],
] as const;
directionFrames.forEach(([key, start]) => {
if (!this.anims.exists(key)) this.anims.create({ key, frames: this.anims.generateFrameNumbers('characters', { start, end: start + 3 }), frameRate: 8, repeat: -1 });
});
if (!this.anims.exists('birds-sing')) this.anims.create({ key: 'birds-sing', frames: [{ key: 'characters', frame: 42 }, { key: 'characters', frame: 43 }], frameRate: 3, repeat: -1 });
this.scene.start('TitleScene');
}
}
@@ -0,0 +1,115 @@
import Phaser from 'phaser';
import { audio } from '../AudioDirector';
import { director } from '../CorruptionDirector';
import { MAP_KEYS, type EndingId } from '../data';
export class EndingScene extends Phaser.Scene {
private ending: EndingId = 'E1';
constructor() { super('EndingScene'); }
init(data: { ending?: EndingId }): void { this.ending = data.ending ?? director.ending() ?? 'E1'; }
create(): void {
MAP_KEYS.forEach((key) => { if (this.scene.isActive(key)) this.scene.stop(key); });
if (this.scene.isActive('UIScene')) this.scene.stop('UIScene');
this.cameras.main.setBackgroundColor('#241f22');
({ E1: () => this.playE1(), E2: () => this.playE2(), E3: () => this.playE3(), E4: () => this.playE4() })[this.ending]();
}
private title(text: string): Phaser.GameObjects.Text {
return this.add.text(160, 18, text, this.style(13, '#fff0bd')).setOrigin(0.5).setDepth(20);
}
private caption(text: string, y = 132): Phaser.GameObjects.Text {
return this.add.text(160, y, text, { ...this.style(9, '#fff0bd'), align: 'center', wordWrap: { width: 280 } }).setOrigin(0.5).setDepth(20).setLineSpacing(4);
}
private style(size: number, color: string): Phaser.Types.GameObjects.Text.TextStyle {
return { fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: `${size}px`, fontStyle: 'bold', color };
}
private playE1(): void {
this.cameras.main.setBackgroundColor('#86d55b');
this.title('E1 糖衣');
for (let i = 0; i < 12; i += 1) this.add.sprite(50 + (i % 6) * 44, 67 + Math.floor(i / 6) * 34, 'characters', i < 7 ? 16 + i * 3 : 37 + (i - 7)).setScale(1.35);
this.caption('从此,每一天都很幸福。\n\n每一天。', 137);
const saturation = this.cameras.main.postFX.addColorMatrix();
this.tweens.addCounter({ from: 0, to: 0.5, duration: 5000, onUpdate: (tween) => saturation.reset().saturate(tween.getValue() ?? 0), onComplete: () => this.showReturn() });
}
private playE2(): void {
this.cameras.main.setBackgroundColor('#f5e8b1');
this.title('E2 回收');
const frozen = this.add.sprite(160, 90, 'characters', 0).setScale(3);
const negative = this.cameras.main.postFX.addColorMatrix().negative();
this.time.delayedCall(2000, () => {
negative.reset();
audio.beep('monitor');
frozen.destroy();
this.add.rectangle(160, 90, 190, 112, 0xffe8a4).setStrokeStyle(3, 0x5b362b);
this.add.sprite(160, 85, 'characters', 0).setScale(2);
this.add.text(92, 53, '日历:同一天', this.style(7, '#58372e'));
this.caption('疗程重新开始。', 145);
this.showReturn(2100);
});
}
private playE3(): void {
this.cameras.main.setBackgroundColor('#e7e8e5');
const map = this.make.tilemap({ key: 'WhiteScene' });
const tileset = map.addTilesetImage('honey-village', 'tiles');
if (tileset) map.createLayer('ground', tileset, 0, 26)?.setAlpha(0.45);
this.title('E3 苏醒');
const corridor = this.add.graphics();
corridor.fillStyle(0xf8f8f4).fillRect(18, 38, 284, 96);
corridor.lineStyle(2, 0xb8bec0).strokeRect(18, 38, 284, 96);
corridor.fillStyle(0xc9cecf).fillRect(156, 38, 8, 96);
const player = this.add.sprite(42, 112, 'characters', 0);
this.tweens.add({ targets: player, x: 260, duration: 2800, onComplete: () => {
corridor.clear().fillStyle(0xfafafa).fillRect(18, 38, 284, 96);
player.destroy();
const record = this.add.rectangle(160, 87, 240, 108, 0xffffff).setStrokeStyle(2, 0x899092);
const recordText = this.add.text(54, 43,
`蜜糖谷情绪疗养中心\n\n姓名:${director.snapshot.playerName}\n编号:PT-07\n疗程时长:${director.elapsedLabel()}`,
{ ...this.style(9, '#394044'), lineSpacing: 5 });
this.time.delayedCall(2800, () => {
record.destroy(); recordText.destroy();
const face = this.add.image(119, 86, 'portraits', 0).setScale(1.5);
this.time.delayedCall(400, () => {
face.setFrame(1);
this.add.text(171, 65, '微笑消失了。', this.style(8, '#555b5d'));
this.add.text(171, 83, '窗外,一只灰色的、\n真的麻雀。', { ...this.style(8, '#555b5d'), lineSpacing: 4 });
const bird = this.add.sprite(240, 113, 'characters', 42).setTint(0x92999a);
this.tweens.add({ targets: bird, y: 106, yoyo: true, repeat: -1, duration: 450 });
this.showReturn(1800);
});
});
} });
}
private playE4(): void {
this.cameras.main.setBackgroundColor('#8fd35f');
this.title('E4 静默');
this.add.rectangle(160, 101, 270, 70, 0xcdbb8e).setStrokeStyle(2, 0x78644c);
for (let i = 0; i < 7; i += 1) {
const ribbon = this.add.rectangle(65 + i * 31, 58, 20, 3, i % 2 ? 0xf2a64c : 0xef7195);
this.tweens.add({ targets: ribbon, angle: 4, yoyo: true, repeat: -1, duration: 900 + i * 80 });
}
this.caption('系统检测到疗程无效。', 145);
this.cameras.main.fadeOut(3500, 0, 0, 0, (_camera: Phaser.Cameras.Scene2D.Camera, progress: number) => {
if (progress === 1) {
this.children.removeAll(true);
this.cameras.main.resetFX().setBackgroundColor('#000000');
this.showReturn();
}
});
}
private showReturn(delay = 0): void {
this.time.delayedCall(delay, () => {
const button = this.add.text(160, 166, '回到标题', this.style(8, '#fff4ca')).setOrigin(0.5).setPadding(8, 4, 8, 4).setBackgroundColor('#5b362b').setInteractive({ useHandCursor: true }).setDepth(50);
button.on('pointerdown', () => this.scene.start('TitleScene'));
});
}
}
@@ -0,0 +1,45 @@
import Phaser from 'phaser';
import { audio } from '../AudioDirector';
import { director } from '../CorruptionDirector';
export class TitleScene extends Phaser.Scene {
private form?: Phaser.GameObjects.DOMElement;
constructor() { super('TitleScene'); }
create(): void {
this.cameras.main.setBackgroundColor('#79cce4');
const background = this.add.tileSprite(160, 90, 320, 180, 'tiles', 0).setTint(0x72c85a);
background.tileScaleX = 1; background.tileScaleY = 1;
this.add.rectangle(160, 38, 286, 58, 0xffe8a4, 0.92).setStrokeStyle(3, 0x5b362b);
this.add.text(160, 24, '崩坏童话:蜜糖村', {
fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '19px', fontStyle: 'bold', color: '#57342b',
}).setOrigin(0.5).setShadow(2, 2, '#e99a3b', 0, true, true);
this.add.text(160, 48, '每一天都很幸福。', {
fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '8px', fontStyle: 'bold', color: '#8d4f33',
}).setOrigin(0.5);
[0, 1, 2, 3, 4].forEach((index) => this.add.sprite(112 + index * 24, 76, 'characters', 37 + index).setScale(1));
const html = `<form class="name-form"><label for="player-name">你的名字</label><input id="player-name" maxlength="12" placeholder="小满" autocomplete="off"><button type="submit">进入蜜糖村</button></form>`;
this.form = this.add.dom(160, 119).createFromHTML(html);
const node = this.form.node as HTMLFormElement;
node.addEventListener('submit', (event) => {
event.preventDefault();
const input = node.querySelector<HTMLInputElement>('#player-name');
this.startGame(input?.value ?? '');
});
this.input.keyboard?.once('keydown-ENTER', () => {
const input = node.querySelector<HTMLInputElement>('#player-name');
this.startGame(input?.value ?? '');
});
}
private startGame(name: string): void {
if (!this.form?.active) return;
audio.unlock();
director.reset(name);
this.form.destroy();
this.scene.start('HouseScene', { spawn: 'default' });
this.scene.launch('UIScene');
this.scene.bringToTop('UIScene');
}
}
+229
View File
@@ -0,0 +1,229 @@
import Phaser from 'phaser';
import { audio } from '../AudioDirector';
import { director } from '../CorruptionDirector';
import { MAP_KEYS, MAP_LABELS, type EndingId } from '../data';
export type DialogueChoice = 'gentle' | 'hesitate' | 'negative';
interface DialogueOptions {
speaker: string;
text: string | string[];
portrait?: number;
choices?: Array<{ id: DialogueChoice; label: string }>;
clean?: boolean;
}
interface ActiveDialogue {
pages: string[];
page: number;
choices?: Array<{ id: DialogueChoice; label: string }>;
selection: number;
resolve: (choice?: DialogueChoice) => void;
}
export class UIScene extends Phaser.Scene {
private hud!: Phaser.GameObjects.Graphics;
private dayText!: Phaser.GameObjects.Text;
private questText!: Phaser.GameObjects.Text;
private promptText!: Phaser.GameObjects.Text;
private toastText!: Phaser.GameObjects.Text;
private dialoguePanel!: Phaser.GameObjects.Graphics;
private portrait!: Phaser.GameObjects.Image;
private speakerText!: Phaser.GameObjects.Text;
private bodyText!: Phaser.GameObjects.Text;
private choiceTexts: Phaser.GameObjects.Text[] = [];
private active?: ActiveDialogue;
private debugDom?: Phaser.GameObjects.DOMElement;
constructor() { super({ key: 'UIScene', active: false }); }
create(): void {
this.cameras.main.setRoundPixels(true);
this.hud = this.add.graphics().setDepth(100);
this.dayText = this.add.text(4, 28, '', this.pixelStyle(8, '#fff7d7')).setDepth(101).setShadow(1, 1, '#4b3128', 0, true, true);
this.questText = this.add.text(316, 4, '', this.pixelStyle(7, '#fff7d7')).setOrigin(1, 0).setAlign('right').setDepth(101).setShadow(1, 1, '#4b3128', 0, true, true);
this.promptText = this.add.text(160, 144, '', this.pixelStyle(8, '#fff6c7')).setOrigin(0.5).setPadding(4, 2, 4, 2).setBackgroundColor('#402a25').setDepth(110).setVisible(false);
this.toastText = this.add.text(160, 42, '', this.pixelStyle(8, '#3d2925')).setOrigin(0.5).setPadding(6, 3, 6, 3).setBackgroundColor('#fff0ae').setDepth(120).setVisible(false);
this.dialoguePanel = this.add.graphics().setDepth(130).setVisible(false);
this.portrait = this.add.image(29, 153, 'portraits', 0).setDisplaySize(40, 40).setDepth(131).setVisible(false);
this.speakerText = this.add.text(54, 132, '', this.pixelStyle(8, '#f0ad3f')).setDepth(131).setVisible(false);
this.bodyText = this.add.text(54, 144, '', this.pixelStyle(8, '#4b3128')).setWordWrapWidth(254).setLineSpacing(2).setDepth(131).setVisible(false);
this.createDebugPanel();
this.bindKeys();
this.redraw();
director.on('change', this.redraw, this);
director.on('task-complete', this.taskComplete, this);
director.on('task-refused', () => this.toast('任务已拒绝。'), this);
director.on('clue', (_id: string, content: string) => this.toast(`发现线索:${content}`, 2600), this);
director.on('sunset', () => { audio.beep('bell'); this.toast('黄昏钟声响起了。回小屋睡觉。', 3000); }, this);
director.on('forced-cheer', () => this.toast('欢乐庆典开始了。边缘仍有一条路。', 3000), this);
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
director.off('change', this.redraw, this);
director.off('task-complete', this.taskComplete, this);
director.off('task-refused'); director.off('clue'); director.off('sunset'); director.off('forced-cheer');
});
}
private pixelStyle(size: number, color: string): Phaser.Types.GameObjects.Text.TextStyle {
return { fontFamily: '"Courier New", "PingFang SC", "Microsoft YaHei", monospace', fontSize: `${size}px`, fontStyle: 'bold', color };
}
private bindKeys(): void {
const keyboard = this.input.keyboard;
if (!keyboard) return;
keyboard.on('keydown-E', () => this.advance());
keyboard.on('keydown-ENTER', () => this.advance());
keyboard.on('keydown-UP', () => this.moveChoice(-1));
keyboard.on('keydown-DOWN', () => this.moveChoice(1));
keyboard.on('keydown-ONE', () => this.choose(0));
keyboard.on('keydown-TWO', () => this.choose(1));
keyboard.on('keydown-THREE', () => this.choose(2));
keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.BACKTICK).on('down', () => this.toggleDebug());
}
private redraw = (): void => {
this.drawSunflower();
this.dayText.setText(`Day ${director.snapshot.day}`);
const pending = director.tasksForDay().find((task) => !director.snapshot.completed.has(task.id) && !director.snapshot.refused.has(task.id));
let quest = director.snapshot.boardRead ? (pending ? `${pending.id} ${pending.title}` : (director.snapshot.sunset ? '回小屋睡觉' : '自由活动')) : '前往公告板';
if (director.snapshot.day === 7 && director.snapshot.festivalStarted && director.ending() === 'E3') quest = '走向雾墙';
this.questText.setText(quest).setWordWrapWidth(170);
};
private drawSunflower(): void {
const value = director.snapshot.happiness;
const lit = value <= 20 ? Math.round(value / 10)
: value <= 50 ? 3 + Math.floor((value - 21) * 4 / 30)
: value <= 80 ? 7 + Math.floor((value - 51) * 3 / 30)
: value < 100 ? 10 + Math.floor((value - 81) * 3 / 19) : 12;
this.hud.clear();
this.hud.fillStyle(0x3d2925, 0.65).fillRoundedRect(3, 3, 29, 24, 3);
for (let i = 0; i < 12; i += 1) {
const angle = (Math.PI * 2 * i) / 12 - Math.PI / 2;
const x = 17.5 + Math.cos(angle) * 8;
const y = 14.5 + Math.sin(angle) * 8;
this.hud.fillStyle(0x4b3128, 1).fillCircle(x, y, 3.2);
this.hud.fillStyle(i < lit ? (value >= 81 ? 0xffff6b : 0xffd53e) : 0x8e7653, 1).fillCircle(x, y, 2.3);
}
this.hud.fillStyle(value === 100 ? 0x2a211d : 0x9a592d, 1).fillCircle(17.5, 14.5, 5);
if (value === 100) {
this.hud.fillStyle(0x070707, 1);
[[16, 13], [19, 14], [16, 17], [20, 17]].forEach(([x, y]) => this.hud.fillRect(x, y, 1, 1));
}
}
setPrompt(label?: string): void {
this.promptText.setVisible(Boolean(label)).setText(label ? `按 E ${label}` : '');
}
toast(message: string, duration = 1800): void {
this.toastText.setText(message).setVisible(true).setAlpha(1);
this.tweens.killTweensOf(this.toastText);
this.tweens.add({ targets: this.toastText, alpha: 0, delay: duration, duration: 300, onComplete: () => this.toastText.setVisible(false) });
}
showDialogue(options: DialogueOptions): Promise<DialogueChoice | undefined> {
if (this.active) return Promise.resolve(undefined);
director.setDialogueOpen(true);
const pages = (Array.isArray(options.text) ? options.text : [options.text]).map((text) => options.clean ? text : director.corruptText(text, options.speaker));
this.dialoguePanel.setVisible(true).clear().fillStyle(0x3e2924, 0.98).fillRect(2, 126, 316, 52).fillStyle(0xffefbd, 1).fillRect(5, 129, 310, 46);
this.portrait.setFrame(options.portrait ?? 0).setVisible(true);
this.speakerText.setText(options.speaker).setVisible(true);
this.bodyText.setText(pages[0]).setVisible(true);
audio.beep('dialogue');
return new Promise((resolve) => {
this.active = { pages, page: 0, choices: options.choices, selection: 0, resolve };
});
}
private advance(): void {
if (!this.active) return;
if (this.active.page < this.active.pages.length - 1) {
this.active.page += 1;
this.bodyText.setText(this.active.pages[this.active.page]);
audio.beep('dialogue');
return;
}
if (this.active.choices?.length) {
if (this.choiceTexts.length === 0) this.renderChoices();
else this.choose(this.active.selection);
return;
}
this.closeDialogue();
}
private renderChoices(): void {
this.bodyText.setVisible(false);
this.active?.choices?.forEach((choice, index) => {
const text = this.add.text(58, 139 + index * 11, `${index + 1}. ${choice.label}`, this.pixelStyle(7, '#4b3128')).setDepth(132);
this.choiceTexts.push(text);
});
this.updateChoiceStyles();
}
private moveChoice(delta: number): void {
if (!this.active?.choices || this.choiceTexts.length === 0) return;
this.active.selection = Phaser.Math.Wrap(this.active.selection + delta, 0, this.active.choices.length);
this.updateChoiceStyles();
}
private updateChoiceStyles(): void {
this.choiceTexts.forEach((text, index) => text.setColor(index === this.active?.selection ? '#c75f38' : '#4b3128'));
}
private choose(index: number): void {
const choice = this.active?.choices?.[index];
if (!choice) return;
this.closeDialogue(choice.id);
}
private closeDialogue(choice?: DialogueChoice): void {
const resolve = this.active?.resolve;
this.active = undefined;
this.dialoguePanel.setVisible(false);
this.portrait.setVisible(false);
this.speakerText.setVisible(false);
this.bodyText.setVisible(false);
this.choiceTexts.forEach((text) => text.destroy());
this.choiceTexts = [];
director.setDialogueOpen(false);
resolve?.(choice);
}
private taskComplete = (task: { id: string; title: string }): void => {
audio.beep('task');
this.toast(`${task.id} 完成:${task.title}`, 2400);
};
private createDebugPanel(): void {
const sceneOptions = MAP_KEYS.map((key) => `<option value="${key}">${MAP_LABELS[key]}</option>`).join('');
const html = `<section class="debug-panel" aria-label="Debug Panel">
<h2>Debug Panel</h2>
<div class="debug-row"><label>Happiness</label><input data-field="happiness" type="number" min="0" max="100" value="30"><button data-action="happiness">Set</button></div>
<div class="debug-row"><label>Awareness</label><input data-field="awareness" type="number" min="0" max="100" value="0"><button data-action="awareness">Set</button></div>
<div class="debug-row"><label>Day</label><input data-field="day" type="number" min="1" max="7" value="1"><button data-action="day">Set</button></div>
<div class="debug-row"><label>Teleport</label><select data-field="scene">${sceneOptions}</select><button data-action="teleport">Go</button></div>
<div class="debug-row"><button data-action="clues">Set all clues</button></div>
<div class="ending-grid">${(['E1', 'E2', 'E3', 'E4'] as EndingId[]).map((id) => `<button data-ending="${id}">${id}</button>`).join('')}</div>
</section>`;
this.debugDom = this.add.dom(190, 90).createFromHTML(html).setScale(0.7).setDepth(500).setVisible(false);
const node = this.debugDom.node as HTMLElement;
node.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const action = target.dataset.action;
const ending = target.dataset.ending as EndingId | undefined;
const input = (name: string) => node.querySelector<HTMLInputElement | HTMLSelectElement>(`[data-field="${name}"]`);
if (action === 'happiness') director.setHappiness(Number(input('happiness')?.value));
if (action === 'awareness') director.setAwareness(Number(input('awareness')?.value));
if (action === 'day') director.setDay(Number(input('day')?.value));
if (action === 'teleport') director.emit('debug-teleport', input('scene')?.value);
if (action === 'clues') director.discoverAllClues();
if (ending) director.emit('force-ending', ending);
});
}
private toggleDebug(): void {
if (!this.debugDom) return;
this.debugDom.setVisible(!this.debugDom.visible);
}
}
@@ -0,0 +1,543 @@
import Phaser from 'phaser';
import { audio } from '../AudioDirector';
import { director } from '../CorruptionDirector';
import { ANNOUNCEMENTS, C9_LINES, NPCS, type EndingId, type NpcId } from '../data';
import type { DialogueChoice, UIScene } from './UIScene';
interface SceneData { spawn?: string; dream?: boolean }
interface MapProperty { name: string; value: string | number | boolean }
interface MapObject extends Phaser.Types.Tilemaps.TiledObject { type: string; properties?: MapProperty[] }
const NPC_IDS = new Set(Object.keys(NPCS));
const STAGE_SATURATION = { S0: 0.9, S1: 1, S2: 1.15, S3: 1.3, S4: 1.4 } as const;
export class WorldScene extends Phaser.Scene {
private spawn = 'default';
private dream = false;
private player!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
private wasd!: Record<'up' | 'down' | 'left' | 'right', Phaser.Input.Keyboard.Key>;
private objects: MapObject[] = [];
private npcs = new Map<string, Phaser.GameObjects.Sprite>();
private extras: Phaser.GameObjects.Sprite[] = [];
private nearest?: MapObject;
private interacting = false;
private idleSince = 0;
private movedAfterIdle = true;
private rainGraphics?: Phaser.GameObjects.Graphics;
private rainSince?: number;
private t14Start?: number;
private cheerTimer?: Phaser.Time.TimerEvent;
private cheerSprites: Phaser.GameObjects.Sprite[] = [];
private reflection?: Phaser.GameObjects.Sprite;
private reflectionHistory: Array<{ x: number; y: number; frame: number }> = [];
private colorFx?: Phaser.FX.ColorMatrix;
private vignetteFx?: Phaser.FX.Vignette;
constructor(key: string) { super(key); }
init(data: SceneData): void {
this.spawn = data.spawn ?? 'default';
this.dream = data.dream ?? false;
}
create(): void {
const map = this.make.tilemap({ key: this.scene.key });
const tileset = map.addTilesetImage('honey-village', 'tiles');
if (!tileset) throw new Error(`Tileset missing in ${this.scene.key}`);
map.createLayer('ground', tileset, 0, 0)?.setDepth(0);
this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels).setRoundPixels(true);
this.objects = (map.getObjectLayer('objects')?.objects ?? []) as MapObject[];
this.createCollision(map);
this.createNpcs();
const spawn = this.objects.find((object) => object.type === 'spawn' && this.prop(object, 'id') === this.spawn)
?? this.objects.find((object) => object.type === 'spawn');
this.player = this.physics.add.sprite(spawn?.x ?? 32, spawn?.y ?? 32, 'characters', 0).setDepth(20);
this.player.body.setSize(10, 8).setOffset(3, 15);
this.player.setCollideWorldBounds(true);
this.cameras.main.startFollow(this.player, true, 0.12, 0.12);
this.createInputs();
this.createReflection();
this.setupPostFx();
this.bindDirectorEvents();
this.applyCorruption();
this.idleSince = performance.now();
if (this.scene.key === 'WhiteScene' && this.dream) this.playDreamFlash();
}
private createCollision(map: Phaser.Tilemaps.Tilemap): void {
const group = this.physics.add.staticGroup();
const obstacles = (map.getObjectLayer('obstacles')?.objects ?? []) as MapObject[];
obstacles.forEach((object) => {
if (!this.prop(object, 'collides')) return;
const block = this.add.rectangle((object.x ?? 0) + (object.width ?? 0) / 2, (object.y ?? 0) + (object.height ?? 0) / 2, object.width ?? 0, object.height ?? 0, 0, 0);
this.physics.add.existing(block, true); group.add(block);
});
this.events.once(Phaser.Scenes.Events.CREATE, () => this.physics.add.collider(this.player, group));
this.time.delayedCall(0, () => this.physics.add.collider(this.player, group));
}
private createInputs(): void {
const keyboard = this.input.keyboard;
if (!keyboard) throw new Error('Keyboard input unavailable');
this.cursors = keyboard.createCursorKeys();
const keys = keyboard.addKeys('W,S,A,D') as Record<'W' | 'S' | 'A' | 'D', Phaser.Input.Keyboard.Key>;
this.wasd = { up: keys.W, down: keys.S, left: keys.A, right: keys.D };
keyboard.on('keydown-E', () => void this.interact());
}
private createNpcs(): void {
this.objects.filter((object) => object.type === 'npc').forEach((object, index) => {
const id = String(this.prop(object, 'id'));
if (id === 'birds') {
const left = this.add.sprite(object.x ?? 0, object.y ?? 0, 'characters', 42).setDepth(18).play('birds-sing');
const right = this.add.sprite((object.x ?? 0) + 13, object.y ?? 0, 'characters', 44).setDepth(18).play('birds-sing');
this.npcs.set(id, left); this.npcs.set('birds-right', right);
return;
}
const info = NPCS[id as NpcId];
const frame = info?.frame ?? 37 + (index % 5);
const sprite = this.add.sprite(object.x ?? 0, object.y ?? 0, 'characters', frame).setDepth(18);
if (id.startsWith('extra')) {
this.extras.push(sprite);
this.tweens.add({ targets: sprite, x: sprite.x + (index % 2 ? 22 : -22), duration: 1800 + index * 140, yoyo: true, repeat: -1, ease: 'Linear' });
}
this.npcs.set(id, sprite);
});
}
private createReflection(): void {
if (this.scene.key !== 'LakeScene' || director.snapshot.day < 2) return;
this.reflection = this.add.sprite(this.player?.x ?? 0, 88, 'characters', 0).setAlpha(0.35).setTint(0x89dbe8).setFlipY(true).setDepth(5);
}
private setupPostFx(): void {
if (this.game.renderer.type !== Phaser.WEBGL) return;
this.colorFx = this.cameras.main.postFX.addColorMatrix();
this.vignetteFx = this.cameras.main.postFX.addVignette(0.5, 0.5, 0, 0.3);
}
private bindDirectorEvents(): void {
director.on('change', this.applyCorruption, this);
director.on('negative-flash', this.negativeFlash, this);
director.on('first-full', this.firstFull, this);
director.on('sad-behavior', this.turnNearestNpc, this);
director.on('forced-cheer', this.startForcedCheer, this);
director.on('debug-teleport', this.debugTeleport, this);
director.on('force-ending', this.startEnding, this);
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
director.off('change', this.applyCorruption, this);
director.off('negative-flash', this.negativeFlash, this);
director.off('first-full', this.firstFull, this);
director.off('sad-behavior', this.turnNearestNpc, this);
director.off('forced-cheer', this.startForcedCheer, this);
director.off('debug-teleport', this.debugTeleport, this);
director.off('force-ending', this.startEnding, this);
this.ui().setPrompt();
});
}
update(_time: number, delta: number): void {
if (!this.player?.body) return;
director.tick();
this.updateMovement();
this.updateNearest();
this.updateIdle();
this.updateT14();
this.updateRain(delta);
this.updateReflection();
this.updateNpcFacing();
}
private updateMovement(): void {
const locked = this.interacting || director.snapshot.dialogueOpen || director.snapshot.t14Active || (this.scene.key === 'WhiteScene' && this.dream);
const left = this.cursors.left.isDown || this.wasd.left.isDown;
const right = this.cursors.right.isDown || this.wasd.right.isDown;
const up = this.cursors.up.isDown || this.wasd.up.isDown;
const down = this.cursors.down.isDown || this.wasd.down.isDown;
let vx = locked ? 0 : Number(right) - Number(left);
let vy = locked ? 0 : Number(down) - Number(up);
if (vx && vy) { vx *= Math.SQRT1_2; vy *= Math.SQRT1_2; }
this.player.setVelocity(vx * 96, vy * 96);
if (vx < 0) this.player.play('walk-left', true);
else if (vx > 0) this.player.play('walk-right', true);
else if (vy < 0) this.player.play('walk-up', true);
else if (vy > 0) this.player.play('walk-down', true);
else { this.player.stop(); this.player.setFrame(this.idleFrame()); }
if (vx || vy) { this.idleSince = performance.now(); this.movedAfterIdle = true; }
}
private idleFrame(): number {
const key = this.player.anims.currentAnim?.key;
return key === 'walk-up' ? 4 : key === 'walk-left' ? 8 : key === 'walk-right' ? 12 : 0;
}
private updateNearest(): void {
if (director.snapshot.dialogueOpen) { this.ui().setPrompt(); return; }
let distance = Number.POSITIVE_INFINITY;
let nearest: MapObject | undefined;
this.objects.filter((object) => object.type === 'door' || object.type === 'npc' || object.type === 'interact').forEach((object) => {
const current = Phaser.Math.Distance.Between(this.player.x, this.player.y, object.x ?? 0, object.y ?? 0);
if (current <= 24 && current < distance) { distance = current; nearest = object; }
});
this.nearest = nearest;
this.ui().setPrompt(nearest ? String(this.prop(nearest, 'label') || (nearest.type === 'door' ? '离开' : '交互')) : undefined);
}
private updateIdle(): void {
if (this.player.body.speed > 0 || director.snapshot.dialogueOpen || this.nearest || director.snapshot.t14Active) return;
if (this.movedAfterIdle && performance.now() - this.idleSince >= 10_000) {
if (director.sadBehavior('idle')) this.ui().toast('你发了一会儿呆。');
this.movedAfterIdle = false;
}
}
private updateT14(): void {
if (!this.t14Start) return;
const seconds = Math.min(60, (performance.now() - this.t14Start) / 1000);
this.cameras.main.setZoom(1 + 0.28 * seconds / 60);
audio.fadeForSmile(seconds);
this.npcs.forEach((sprite, id) => { if (!id.startsWith('birds')) sprite.setFlipX(sprite.x > this.player.x); });
if (seconds >= 60) {
this.t14Start = undefined;
director.setT14(false);
audio.restore();
this.cameras.main.setZoom(1);
director.completeTask('T14');
this.ui().toast('你保持了整整 60 秒的微笑。', 2500);
}
}
private updateRain(_delta: number): void {
const raining = director.snapshot.day === 6 && director.snapshot.sunset;
if (!raining) { this.rainGraphics?.clear(); this.rainSince = undefined; return; }
this.rainGraphics ??= this.add.graphics().setScrollFactor(0).setDepth(50);
this.rainGraphics.clear().lineStyle(1, 0xb8e7f2, 0.75);
for (let i = 0; i < 45; i += 1) {
const x = (i * 43 + Math.floor(performance.now() / 12)) % 340 - 10;
const y = (i * 29 + Math.floor(performance.now() / 7)) % 190 - 10;
this.rainGraphics.lineBetween(x, y, x - 3, y + 8);
}
if (this.player.body.speed > 0) { this.rainSince = undefined; return; }
this.rainSince ??= performance.now();
if (performance.now() - this.rainSince >= 10_000 && director.sadBehavior('rain')) this.ui().toast('你在雨里站了十秒。');
}
private updateReflection(): void {
if (!this.reflection) return;
this.reflectionHistory.push({ x: this.player.x, y: this.player.y, frame: Number(this.player.frame.name) });
const delayed = this.reflectionHistory.length > 1 ? this.reflectionHistory.shift() : undefined;
if (!delayed) return;
this.reflection.setPosition(delayed.x, 106 + (106 - delayed.y) * 0.15).setFrame(delayed.frame);
}
private updateNpcFacing(): void {
if (director.stage === 'S3' || director.stage === 'S4') this.npcs.forEach((sprite, id) => { if (!id.startsWith('birds')) sprite.setFlipX(sprite.x > this.player.x); });
if (director.stage === 'S4' && this.extras.length) this.tweens.killTweensOf(this.extras[0]);
if (director.stage === 'S4' && Math.random() < 0.6 / 3600) {
const ids = [...this.npcs.keys()].filter((id): id is NpcId => id in NPCS && id !== 'doctor' && id !== 'turtle' && id !== 'birds');
const id = Phaser.Utils.Array.GetRandom(ids);
const sprite = id ? this.npcs.get(id) : undefined;
if (id && sprite) {
sprite.setFrame(NPCS[id].frame + 2);
this.time.delayedCall(100, () => sprite.active && sprite.setFrame(NPCS[id].frame + 1));
}
}
}
private async interact(): Promise<void> {
if (!this.nearest || this.interacting || director.snapshot.dialogueOpen) return;
this.interacting = true;
try {
if (this.nearest.type === 'door') this.useDoor(this.nearest);
else if (this.nearest.type === 'npc') await this.interactNpc(String(this.prop(this.nearest, 'id')));
else await this.interactPoint(String(this.prop(this.nearest, 'id')));
} finally { this.interacting = false; }
}
private useDoor(object: MapObject): void {
if (director.snapshot.cheerActive) director.escapeCheer();
const target = String(this.prop(object, 'target'));
const spawn = String(this.prop(object, 'spawn') || 'default');
this.scene.start(target, { spawn });
}
private async interactNpc(id: string): Promise<void> {
if (id.startsWith('extra')) { await this.say('村民', '今天也是完美的一天。', 0); return; }
if (!NPC_IDS.has(id)) return;
const npcId = id as NpcId;
director.recordDialogue(id);
if (await this.handleNpcTask(npcId)) return;
if (npcId === 'turtle' && C9_LINES[director.snapshot.day]) {
await this.say('龟爷爷', C9_LINES[director.snapshot.day], NPCS.turtle.portrait);
director.setFlag(`c9-${director.snapshot.day}`, true);
if (director.snapshot.day === 6) director.discoverClue('C9');
this.checkGreeting(id);
return;
}
const lines: Record<NpcId, string> = {
afu: '今天也要把幸福装满哦。', yuanyuan: '刚出炉的蜂蜜面包最甜啦。', dabao: '每一封信都会准时送到。',
mimi: '货架永远整整齐齐,真让人开心。', doctor: '村里有诊所,但你不需要去那里哦。',
birds: '啾、啾。旋律每天都一样好听。', turtle: '湖水一直很平静。',
};
const replayGreeting = director.snapshot.day === 4 && director.isTaskActive('T10');
const choice = await this.say(NPCS[npcId].name, lines[npcId], NPCS[npcId].portrait, true, undefined, replayGreeting);
this.applyChoice(choice, id);
if (npcId === 'birds') director.smallInteraction('feed');
if (npcId === 'afu') director.smallInteraction('pet');
this.checkGreeting(id);
}
private async handleNpcTask(id: NpcId): Promise<boolean> {
const breadTask = director.snapshot.day === 4 ? 'T8' : 'T1';
const greetTask = director.snapshot.day === 4 ? 'T10' : 'T3';
if (id === 'yuanyuan' && director.isTaskActive(breadTask) && !director.flag(`${breadTask}-picked`)) {
director.setFlag(`${breadTask}-picked`);
await this.say('圆圆', '蜂蜜面包刚刚出炉。请趁热送给大家吧。', NPCS.yuanyuan.portrait, false, undefined, director.snapshot.day === 4);
this.checkGreeting(id, greetTask); return true;
}
if (director.isTaskActive(breadTask) && director.flag(`${breadTask}-picked`) && ['dabao', 'mimi', 'turtle'].includes(id) && !director.flag(`${breadTask}-${id}`)) {
director.setFlag(`${breadTask}-${id}`);
await this.say(NPCS[id].name, '谢谢你。今天的面包也和昨天一样甜。', NPCS[id].portrait, false, undefined, director.snapshot.day === 4);
if (director.increment(`${breadTask}-delivered`, 3) === 3) director.completeTask(breadTask);
this.checkGreeting(id, greetTask); return true;
}
if (id === 'dabao' && director.isTaskActive('T4') && !director.flag('T4-picked')) {
director.setFlag('T4-picked');
await this.say('大宝', '三封信。圆圆、咪咪、芙医生。邮戳都已经盖好了。', NPCS.dabao.portrait);
return true;
}
if (director.isTaskActive('T4') && director.flag('T4-picked') && ['yuanyuan', 'mimi', 'doctor'].includes(id) && !director.flag(`T4-${id}`)) {
director.setFlag(`T4-${id}`);
await this.say(NPCS[id].name, '信收到了。谢谢你。', NPCS[id].portrait);
const count = director.increment('T4-delivered', 3);
if (count === 3) {
this.ui().toast('署名:PT-0▓', 200);
await this.wait(200);
director.discoverClue('C4');
director.completeTask('T4');
}
return true;
}
if (id === 'yuanyuan' && director.isTaskActive('T11')) {
const choice = await this.say('圆圆', '来帮我揉面吧。只要一直微笑,面团就会很柔软。', NPCS.yuanyuan.portrait, false, [
{ id: 'gentle', label: '好,我来帮忙。' }, { id: 'hesitate', label: '让我想一想。' }, { id: 'negative', label: '我不想做。' },
]);
if (choice === 'negative') director.refuseTask('T11');
else { if (choice === 'hesitate') director.adjustAwareness(2); director.completeTask('T11'); }
return true;
}
return false;
}
private checkGreeting(id: string, forcedTask?: string): void {
const task = forcedTask ?? (director.snapshot.day === 4 ? 'T10' : 'T3');
if (!director.isTaskActive(task) || director.flag(`${task}-greet-${id}`)) return;
director.setFlag(`${task}-greet-${id}`);
if (director.increment(`${task}-greetings`, 3) === 3) director.completeTask(task);
}
private async interactPoint(id: string): Promise<void> {
if (id === 'board') { await this.interactBoard(); return; }
if (id === 'bed') { await this.interactBed(); return; }
if (id === 'calendar') {
if (director.snapshot.day < 2) await this.say('日历', '今天的日期旁画着一朵向日葵。');
else { await this.say('日历', '每一页都是同一个日期'); director.discoverClue('C1'); }
return;
}
if (id === 'recipe') {
if (director.snapshot.day < 3) await this.say('配方纸', '纸张背面粘着糖霜。');
else { await this.say('配方纸', '手写小字:「按疗程配比。甜度:最大。」'); director.discoverClue('C2'); }
return;
}
if (id === 'mail') {
if (director.snapshot.day < 2) await this.say('散落信件', '几封还没有投递的信。');
else { await this.say('散落信件', '所有邮戳是同一天'); director.discoverClue('C3'); }
return;
}
if (id === 'shelf') {
await this.say('货架', '同一商品重复排列,标签全部空白');
if (director.snapshot.day >= 2) director.discoverClue('C5');
if (director.isTaskActive('T5')) director.completeTask('T5');
return;
}
if (id.startsWith('flower')) {
const task = director.snapshot.day === 4 ? 'T9' : 'T2';
if (director.isTaskActive(task)) this.handleRepeatedPoint(id, task, 'flowers', 3, '你给花坛浇了水。');
else if (director.smallInteraction('water')) this.ui().toast('你给花浇了一点水。');
else this.ui().toast('今天的花已经喝饱了。');
return;
}
if (id.startsWith('berry')) { this.handleRepeatedPoint(id, 'T6', 'berries', 5, '你采下一把莓果。'); return; }
if (id.startsWith('ribbon')) { this.handleRepeatedPoint(id, 'T13', 'ribbons', 4, '彩带被挂得笔直。'); return; }
if (id === 'picnic' && director.isTaskActive('T7')) {
await this.say('湖畔', ['野餐布上的餐具摆得一丝不差。', '湖中的倒影晚了一帧才坐下。']);
director.completeTask('T7'); return;
}
if (id === 'well') { await this.interactWell(); return; }
if (id === 'form') {
if (director.snapshot.day < 5) await this.say('芙医生', '桌上的表格还没有填写,请别碰哦。', NPCS.doctor.portrait);
else { await this.say('表格', '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」'); director.discoverClue('C8'); }
return;
}
if (id === 'mist') { await this.interactMist(); return; }
if (id === 'festival') { await this.interactFestival(); return; }
if (id === 'oven') await this.say('烤炉', '炉火永远保持同样的温度。');
if (id === 'lake') await this.say('湖面', '倒影跟着你。慢了一帧。');
}
private async interactBoard(): Promise<void> {
const day = director.snapshot.day;
if (!director.snapshot.boardRead) {
await this.say('阿福', ANNOUNCEMENTS[day], NPCS.afu.portrait, false, undefined, day === 4);
director.acceptDay();
return;
}
if (day === 5 && director.isTaskActive('T12')) {
const choice = await this.say('公告板', '木板需要擦得干干净净。', undefined, false, [
{ id: 'gentle', label: '认真擦拭。' }, { id: 'hesitate', label: '停下来看看旧字。' }, { id: 'negative', label: '拒绝擦拭。' },
]);
if (choice === 'negative') director.refuseTask('T12');
else {
if (choice === 'hesitate') director.adjustAwareness(2);
await this.say('旧字迹', '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」');
director.discoverClue('C6'); director.completeTask('T12');
}
return;
}
const tasks = director.tasksForDay().map((task) => `${task.id} ${task.title}`).join('\n');
await this.say('今日幸福任务', tasks || '今天没有新的任务。');
}
private async interactBed(): Promise<void> {
if (!director.snapshot.sunset) { await this.say('床', '现在还不到睡觉时间。'); return; }
const oldDay = director.snapshot.day;
await this.say('梦境', oldDay >= 3 ? ['你闭上眼睛。', '白色墙壁闪过一秒。'] : '你闭上眼睛。甜味沉进梦里。');
this.cameras.main.fadeOut(450, 255, 245, 220);
this.time.delayedCall(500, () => {
director.advanceDay();
if (oldDay >= 3) this.scene.start('WhiteScene', { dream: true });
else this.scene.restart({ spawn: 'default' });
});
}
private async interactWell(): Promise<void> {
if (director.snapshot.day < 5) { await this.say('阿福', '井边有点滑。等自由日再来吧。', NPCS.afu.portrait); return; }
await this.say('古井', '你向井底看去。五秒里,只有自己的呼吸。');
this.ui().toast('注视古井……', 5000);
await this.wait(5000);
if (!director.flag('well-gaze')) { director.setFlag('well-gaze'); director.adjustAwareness(3); }
audio.beep('monitor'); director.discoverClue('C7');
}
private async interactMist(): Promise<void> {
if (director.snapshot.day < 5) { await this.say('雾墙', '一位村民温柔地摇头。现在还不能靠近。'); return; }
if (director.snapshot.day === 7 && director.snapshot.festivalStarted && director.ending() === 'E3') { this.startEnding('E3'); return; }
await this.say('雾墙', '雾在五秒里维持着完全相同的形状。');
this.ui().toast('注视雾墙……', 5000);
await this.wait(5000);
if (!director.flag('mist-gaze')) { director.setFlag('mist-gaze'); director.adjustAwareness(3); }
}
private async interactFestival(): Promise<void> {
if (director.isTaskActive('T14')) {
if (!director.snapshot.completed.has('T13')) { await this.say('庆典场地', '彩带还没有挂完。'); return; }
await this.say('阿福', '请站在这里,保持微笑 60 秒。', NPCS.afu.portrait);
director.setT14(true); this.t14Start = performance.now(); this.ui().toast('保持微笑。', 2000); return;
}
if (!director.isTaskActive('T15')) { await this.say('庆典场地', '这里正等待第 413 届丰收庆典。'); return; }
director.completeTask('T15'); director.setFestivalStarted();
const ending = director.ending();
if (ending === 'E3') { await this.say('雾墙', '庆典的歌声里,雾墙裂开了一条白色的缝。'); this.ui().toast('走向雾墙。', 3000); }
else if (ending) this.startEnding(ending);
else await this.say('庆典场地', '庆典仍在等待幸福抵达满格。');
}
private handleRepeatedPoint(id: string, task: string, counter: string, total: number, message: string): void {
if (!director.isTaskActive(task)) { this.ui().toast('这里暂时没有任务。'); return; }
if (director.flag(`${task}-${id}`)) { this.ui().toast('这里已经完成了。'); return; }
director.setFlag(`${task}-${id}`);
this.ui().toast(message);
if (director.increment(`${task}-${counter}`, total) === total) director.completeTask(task);
}
private async say(speaker: string, text: string | string[], portrait?: number, defaultChoices = false, choices?: Array<{ id: DialogueChoice; label: string }>, clean = false): Promise<DialogueChoice | undefined> {
const options = choices ?? (defaultChoices ? [
{ id: 'gentle' as const, label: '温柔地回应。' }, { id: 'hesitate' as const, label: '迟疑片刻。' }, { id: 'negative' as const, label: '否定这句话。' },
] : undefined);
return this.ui().showDialogue({ speaker, text, portrait, choices: options, clean });
}
private applyChoice(choice: DialogueChoice | undefined, npc: string): void {
if (choice === 'gentle') director.adjustHappiness(2);
if (choice === 'hesitate') director.adjustAwareness(2);
if (choice === 'negative') director.sadBehavior(`negative-${npc}-${director.increment(`negative-${npc}`)}`);
}
private applyCorruption = (): void => {
const saturation = STAGE_SATURATION[director.stage];
this.colorFx?.reset().saturate(saturation - 1);
if (this.vignetteFx) {
this.vignetteFx.strength = director.stage === 'S4' ? 0.55 : director.stage === 'S3' ? 0.28 : 0;
this.vignetteFx.radius = 0.72;
}
this.npcs.forEach((sprite, id) => {
if (id.startsWith('birds')) {
sprite.anims.timeScale = director.stage === 'S2' ? 0.8 : director.stage === 'S3' ? 0.55 : 1;
}
if (!(id in NPCS)) return;
const npcId = id as NpcId;
const expression = director.npcExpression(npcId);
const offset = expression === 'flat' ? 1 : expression === 'off' ? 2 : 0;
if (npcId !== 'birds') sprite.setFrame(NPCS[npcId].frame + offset);
});
};
private negativeFlash = (): void => {
this.colorFx?.negative();
this.time.delayedCall(17, () => this.applyCorruption());
};
private firstFull = (): void => {
this.cameras.main.flash(1000, 255, 250, 210);
this.ui().toast('幸福溢出了。', 1800);
};
private turnNearestNpc = (): void => {
const nearest = [...this.npcs.values()].sort((a, b) => Phaser.Math.Distance.Between(a.x, a.y, this.player.x, this.player.y) - Phaser.Math.Distance.Between(b.x, b.y, this.player.x, this.player.y))[0];
if (!nearest) return;
nearest.setFlipX(nearest.x > this.player.x);
nearest.setTint(0xfff1c2);
this.time.delayedCall(1000, () => nearest.active && nearest.clearTint());
};
private startForcedCheer = (): void => {
this.cameras.main.flash(500, 255, 224, 120);
this.cheerSprites.forEach((sprite) => sprite.destroy());
this.cheerSprites = [];
for (let i = 0; i < 6; i += 1) this.cheerSprites.push(this.add.sprite(this.player.x + Math.cos(i) * 38, this.player.y + Math.sin(i) * 28, 'characters', 37 + (i % 5)).setDepth(19));
this.cheerTimer?.remove();
this.cheerTimer = this.time.delayedCall(15_000, () => {
this.cheerSprites.forEach((sprite) => sprite.destroy()); this.cheerSprites = [];
director.finishCheer();
});
};
private debugTeleport = (key: string): void => { if (key && key !== this.scene.key) this.scene.start(key, { spawn: 'default' }); };
private startEnding = (ending: EndingId): void => { this.scene.start('EndingScene', { ending }); };
private playDreamFlash(): void {
this.player.setVisible(false);
this.cameras.main.flash(250, 255, 255, 255);
this.add.text(160, 82, '白色病房。', { fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '9px', color: '#626a6c' }).setOrigin(0.5).setScrollFactor(0).setDepth(60);
this.time.delayedCall(1000, () => this.scene.start('HouseScene', { spawn: 'default' }));
}
private prop(object: MapObject, name: string): string | number | boolean | undefined {
return object.properties?.find((property) => property.name === name)?.value;
}
private wait(ms: number): Promise<void> { return new Promise((resolve) => this.time.delayedCall(ms, resolve)); }
private ui(): UIScene { return this.scene.get('UIScene') as UIScene; }
}
+47
View File
@@ -0,0 +1,47 @@
import Phaser from 'phaser';
import './style.css';
import { MAP_KEYS } from './game/data';
import { BootScene } from './game/scenes/BootScene';
import { EndingScene } from './game/scenes/EndingScene';
import { TitleScene } from './game/scenes/TitleScene';
import { UIScene } from './game/scenes/UIScene';
import { WorldScene } from './game/scenes/WorldScene';
import { director } from './game/CorruptionDirector';
declare global {
interface Window { __HONEY_GAME__: Phaser.Game }
}
const game = new Phaser.Game({
type: Phaser.WEBGL,
parent: 'game',
backgroundColor: '#79cce4',
pixelArt: true,
roundPixels: true,
width: 320,
height: 180,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
autoRound: true,
zoom: Phaser.Scale.MAX_ZOOM,
width: 320,
height: 180,
},
physics: {
default: 'arcade',
arcade: { debug: false },
},
dom: { createContainer: true },
render: { antialias: false, pixelArt: true, roundPixels: true },
scene: [
BootScene,
TitleScene,
...MAP_KEYS.map((key) => new WorldScene(key)),
UIScene,
EndingScene,
],
});
game.registry.set('director', director);
window.__HONEY_GAME__ = game;
+87
View File
@@ -0,0 +1,87 @@
:root {
color: #fff4d6;
background: #2f2627;
font-family: "Courier New", "PingFang SC", "Microsoft YaHei", monospace;
font-synthesis: none;
}
* { box-sizing: border-box; }
html, body, #game {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #2f2627;
}
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.name-form {
width: 210px;
padding: 12px;
color: #3d2925;
background: #fff0bc;
border: 3px solid #5b362b;
box-shadow: 0 4px 0 #3b2725;
text-align: center;
}
.name-form label {
display: block;
margin-bottom: 8px;
font-size: 12px;
font-weight: 700;
}
.name-form input {
width: 100%;
height: 30px;
padding: 5px 8px;
color: #3d2925;
background: #fffaf0;
border: 2px solid #9b5b30;
border-radius: 0;
font: 700 14px "Courier New", "PingFang SC", monospace;
outline: none;
}
.name-form input:focus { border-color: #e06f46; }
.name-form button {
width: 100%;
margin-top: 8px;
padding: 7px 10px;
color: #fff7d5;
background: #c75f38;
border: 2px solid #5b362b;
border-radius: 0;
font: 700 12px "Courier New", "PingFang SC", monospace;
cursor: pointer;
}
.debug-panel {
width: 250px;
padding: 10px;
color: #f4f4f4;
background: rgba(24, 22, 26, 0.97);
border: 1px solid #f0b64a;
font: 11px/1.4 "Courier New", "PingFang SC", monospace;
}
.debug-panel h2 { margin: 0 0 8px; color: #f0b64a; font-size: 13px; }
.debug-row { display: flex; gap: 5px; margin: 5px 0; }
.debug-panel label { width: 70px; padding-top: 3px; }
.debug-panel input, .debug-panel select, .debug-panel button {
min-width: 0;
flex: 1;
color: #fff;
background: #38333c;
border: 1px solid #716a78;
font: inherit;
}
.debug-panel button { padding: 4px; cursor: pointer; }
.debug-panel .ending-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
+38
View File
@@ -0,0 +1,38 @@
import { expect, test } from '@playwright/test';
import { readFileSync } from 'node:fs';
import { MAP_KEYS } from '../src/game/data';
const doors = MAP_KEYS.flatMap((source) => {
const map = JSON.parse(readFileSync(`public/maps/${source}.json`, 'utf8')) as {
layers: Array<{ name: string; objects?: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }>;
};
return (map.layers.find((layer) => layer.name === 'objects')?.objects ?? [])
.filter((object) => object.type === 'door')
.map((object) => ({ source, target: object.properties?.find((property) => property.name === 'target')?.value ?? '' }));
});
test('every authored door reaches its paired target scene', async ({ page }) => {
await page.goto('/');
await page.locator('#player-name').press('Enter');
await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true);
for (const { source, target } of doors) {
await page.evaluate((key) => {
const director = window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, value: string) => void };
director.emit('debug-teleport', key);
}, source);
await expect.poll(() => page.evaluate((key) => window.__HONEY_GAME__.scene.isActive(key), source)).toBe(true);
await page.evaluate(({ sceneKey, targetKey }) => {
const scene = window.__HONEY_GAME__.scene.getScene(sceneKey) as unknown as {
objects: Array<{ type: string; x: number; y: number; properties?: Array<{ name: string; value: string }> }>;
player: { setPosition: (x: number, y: number) => void };
};
const door = scene.objects.find((object) => object.type === 'door' && object.properties?.some((property) => property.name === 'target' && property.value === targetKey));
if (!door) throw new Error(`Door ${sceneKey} -> ${targetKey} not found`);
scene.player.setPosition(door.x, door.y);
}, { sceneKey: source, targetKey: target });
await page.waitForTimeout(80);
await page.keyboard.press('e');
await expect.poll(() => page.evaluate((key) => window.__HONEY_GAME__.scene.isActive(key), target)).toBe(true);
}
});
+129
View File
@@ -0,0 +1,129 @@
import { expect, test, type Page } from '@playwright/test';
async function activeScenes(page: Page): Promise<string[]> {
return page.evaluate(() => window.__HONEY_GAME__.scene.getScenes(true).map((scene) => scene.scene.key));
}
test('title, movement, maps, corruption controls, and ending flow', async ({ page }) => {
const errors: string[] = [];
page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()); });
page.on('pageerror', (error) => errors.push(error.message));
await page.goto('/');
await expect(page.locator('.name-form')).toBeVisible();
await expect(page.locator('#player-name')).toHaveAttribute('placeholder', '小满');
await page.screenshot({ path: '.qa/title.png' });
await page.locator('#player-name').fill('验收员');
await page.getByRole('button', { name: '进入蜜糖村' }).click();
await expect.poll(() => activeScenes(page)).toContain('HouseScene');
await expect.poll(() => activeScenes(page)).toContain('UIScene');
const startX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x);
await page.keyboard.down('ArrowRight'); await page.waitForTimeout(250); await page.keyboard.up('ArrowRight');
const movedX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x);
expect(movedX).toBeGreaterThan(startX);
for (const [key, animation] of [['w', 'walk-up'], ['s', 'walk-down'], ['a', 'walk-left'], ['d', 'walk-right']] as const) {
await page.keyboard.down(key); await page.waitForTimeout(80);
const activeAnimation = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { anims: { currentAnim?: { key: string } } } }).player.anims.currentAnim?.key);
expect(activeAnimation).toBe(animation);
await page.keyboard.up(key);
}
await page.keyboard.down('ArrowLeft'); await page.waitForTimeout(1600); await page.keyboard.up('ArrowLeft');
const boundedX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x);
expect(boundedX).toBeGreaterThanOrEqual(5);
await page.keyboard.press('Backquote');
await expect(page.locator('.debug-panel')).toBeVisible();
await page.locator('[data-field="happiness"]').fill('100');
await page.locator('[data-action="happiness"]').click();
await page.locator('[data-field="awareness"]').fill('72');
await page.locator('[data-action="awareness"]').click();
await page.locator('[data-field="day"]').fill('7');
await page.locator('[data-action="day"]').click();
const state = await page.evaluate(() => {
const d = window.__HONEY_GAME__.registry.get('director') as { snapshot: { happiness: number; awareness: number; day: number }; stage: string };
return { ...d.snapshot, stage: d.stage };
});
expect(state).toMatchObject({ happiness: 100, awareness: 72, day: 7, stage: 'S4' });
await page.screenshot({ path: '.qa/debug.png' });
const maps = ['HouseScene', 'PlazaScene', 'BakeryScene', 'ShopScene', 'ClinicScene', 'LakeScene', 'FieldScene', 'WellScene', 'MistScene', 'WhiteScene'];
for (const key of maps) {
await page.locator('[data-field="scene"]').selectOption(key);
await page.locator('[data-action="teleport"]').click();
await expect.poll(() => activeScenes(page)).toContain(key);
}
await page.locator('[data-field="scene"]').selectOption('PlazaScene');
await page.locator('[data-action="teleport"]').click();
await expect.poll(() => activeScenes(page)).toContain('PlazaScene');
await page.keyboard.press('Backquote');
await page.screenshot({ path: '.qa/plaza-s4.png' });
await page.keyboard.press('Backquote');
await page.locator('[data-field="happiness"]').fill('40');
await page.locator('[data-action="happiness"]').click();
await page.locator('[data-field="day"]').fill('1');
await page.locator('[data-action="day"]').click();
await page.keyboard.press('Backquote');
await page.waitForTimeout(2000);
await page.screenshot({ path: '.qa/plaza-s1.png' });
await page.evaluate(() => {
const ui = window.__HONEY_GAME__.scene.getScene('UIScene') as unknown as { showDialogue: (options: object) => Promise<unknown> };
void ui.showDialogue({ speaker: '阿福', text: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', portrait: 2 });
});
await page.waitForTimeout(100);
await page.screenshot({ path: '.qa/dialogue.png' });
await page.keyboard.press('e');
await page.keyboard.press('Backquote');
await page.locator('[data-action="clues"]').click();
const clueCount = await page.evaluate(() => (window.__HONEY_GAME__.registry.get('director') as { snapshot: { clues: Set<string> } }).snapshot.clues.size);
expect(clueCount).toBe(9);
await page.locator('[data-ending="E3"]').click();
await expect.poll(() => activeScenes(page)).toContain('EndingScene');
await page.waitForTimeout(3200);
await page.screenshot({ path: '.qa/e3.png' });
expect(errors).toEqual([]);
});
test('integer scaling stays sharp at desktop and compact viewport', async ({ page }) => {
await page.goto('/');
await page.locator('#player-name').press('Enter');
await expect.poll(() => activeScenes(page)).toContain('HouseScene');
const desktop = await page.locator('canvas').boundingBox();
expect(desktop?.width).toBe(960);
expect(desktop?.height).toBe(540);
await page.setViewportSize({ width: 640, height: 360 });
await page.waitForTimeout(250);
const compact = await page.locator('canvas').boundingBox();
expect(compact?.width).toBe(640);
expect(compact?.height).toBe(360);
});
test('all four forced endings render their specified evidence', async ({ page }) => {
const startEnding = async (ending: string, name = '小满') => {
await page.goto('/');
await page.locator('#player-name').fill(name);
await page.locator('#player-name').press('Enter');
await expect.poll(() => activeScenes(page)).toContain('HouseScene');
await page.evaluate((id) => (window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, ending: string) => void }).emit('force-ending', id), ending);
await expect.poll(() => activeScenes(page)).toContain('EndingScene');
};
const texts = () => page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('EndingScene') as unknown as { children: { list: Array<{ type?: string; text?: string }> } }).children.list
.filter((child) => child.type === 'Text')
.map((text) => text.text ?? ''));
await startEnding('E1');
expect((await texts()).join('\n')).toContain('从此,每一天都很幸福。\n\n每一天。');
await startEnding('E2'); await page.waitForTimeout(2200);
expect((await texts()).join('\n')).toContain('疗程重新开始。');
await startEnding('E3', '真实姓名'); await page.waitForTimeout(3200);
const record = (await texts()).join('\n');
expect(record).toContain('蜜糖谷情绪疗养中心');
expect(record).toContain('姓名:真实姓名');
expect(record).toContain('编号:PT-07');
expect(record).toMatch(/疗程时长:\d{2}:\d{2}/);
await page.waitForTimeout(2900);
expect((await texts()).join('\n')).toContain('微笑消失了。');
await startEnding('E4');
expect((await texts()).join('\n')).toContain('系统检测到疗程无效。');
});
@@ -0,0 +1,141 @@
import { expect, test } from '@playwright/test';
import { readFileSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
import { ANNOUNCEMENTS, C9_LINES, CLUES, CORRUPTIONS, MAP_KEYS, TASKS } from '../src/game/data';
test('fixed PRD tables remain exact', () => {
expect(TASKS).toHaveLength(15);
expect(Object.keys(CLUES)).toHaveLength(9);
expect(CORRUPTIONS).toEqual([
['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'],
['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'],
]);
expect(ANNOUNCEMENTS).toEqual({
1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
2: '早上好呀!新的一天,新的幸福。祝你开心!',
3: '早上好!今天要把幸福装得满满的哦。要开心!',
4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。',
6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。',
7: '庆典开始了。来吧。',
});
expect(CLUES).toEqual({
C1: '每一页都是同一个日期', C2: '手写小字:「按疗程配比。甜度:最大。」', C3: '所有邮戳是同一天', C4: '署名闪过乱码「PT-0▓」',
C5: '同一商品重复排列,标签全部空白', C6: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」',
C7: '井底回声:极轻的监护仪滴声(靠近并交互 5 秒)', C8: '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」',
C9: '三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」',
});
expect(C9_LINES).toEqual({ 2: '湖水一直很平静', 4: '面包一直是那个味道', 6: '孩子,今天是第几个今天?' });
expect(TASKS.slice(7, 10).map(({ title, reward }) => ({ title, reward }))).toEqual(TASKS.slice(0, 3).map(({ title, reward }) => ({ title, reward })));
});
test('Aseprite sheets and Tiled exports have the required dimensions and layers', () => {
const pngSize = (path: string) => {
const png = readFileSync(path);
return { width: png.readUInt32BE(16), height: png.readUInt32BE(20) };
};
expect(pngSize(resolve('public/assets/tileset.png'))).toEqual({ width: 256, height: 64 });
expect(pngSize(resolve('public/assets/characters.png'))).toEqual({ width: 256, height: 96 });
expect(pngSize(resolve('public/assets/portraits.png'))).toEqual({ width: 432, height: 48 });
expect(readdirSync('public/assets/source').filter((file) => file.endsWith('.aseprite'))).toHaveLength(3);
const expectedSizes: Record<string, [number, number]> = {
HouseScene: [12, 8], PlazaScene: [30, 20], BakeryScene: [15, 10], ShopScene: [15, 10], ClinicScene: [12, 8],
LakeScene: [24, 16], FieldScene: [20, 14], WellScene: [14, 10], MistScene: [20, 8], WhiteScene: [30, 8],
};
const maps = new Map<string, Record<string, unknown>>();
MAP_KEYS.forEach((key) => {
const map = JSON.parse(readFileSync(resolve(`public/maps/${key}.json`), 'utf8')) as {
width: number; height: number; layers: Array<{ name: string; type: string; objects?: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }>;
tilesets: Array<{ tilecount: number }>;
};
expect([map.width, map.height]).toEqual(expectedSizes[key]);
expect(map.layers.map((layer) => layer.name)).toEqual(['ground', 'obstacles', 'objects']);
expect(map.tilesets[0].tilecount).toBe(64);
maps.set(key, map as unknown as Record<string, unknown>);
});
for (const key of MAP_KEYS) {
const map = maps.get(key) as unknown as { layers: Array<{ name: string; objects: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }> };
const doors = map.layers.find((layer) => layer.name === 'objects')?.objects.filter((object) => object.type === 'door') ?? [];
doors.forEach((door) => {
const target = door.properties?.find((property) => property.name === 'target')?.value;
const targetMap = maps.get(String(target)) as unknown as { layers: Array<{ name: string; objects: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }> };
const reverse = targetMap.layers.find((layer) => layer.name === 'objects')?.objects.some((object) => object.type === 'door' && object.properties?.some((property) => property.name === 'target' && property.value === key));
expect(reverse, `${key} -> ${target} needs a reverse door`).toBe(true);
});
}
});
test('director enforces rewards, clues, sad behavior, stages, overflow, and endings', async ({ page }) => {
await page.goto('/');
await page.locator('#player-name').press('Enter');
await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true);
const result = await page.evaluate(({ tasks, corruptionPairs, clueIds }) => {
const d = window.__HONEY_GAME__.registry.get('director') as any;
const rewards = tasks.map((task: { id: string; day: number; reward: number }) => {
d.reset('Test'); d.setDay(task.day); d.acceptDay(); d.setHappiness(0); d.completeTask(task.id);
return [task.id, d.snapshot.happiness];
});
d.reset('Test');
const clueDeltas: number[] = [];
clueIds.forEach((id) => { const before = d.snapshot.awareness; d.discoverClue(id); clueDeltas.push(d.snapshot.awareness - before); });
d.reset('Test'); d.setHappiness(50);
['refuse', 'negative', 'idle', 'cling', 'rain'].forEach((key) => d.sadBehavior(key));
const sad = { happiness: d.snapshot.happiness, awareness: d.snapshot.awareness };
const stages = [10, 40, 70, 90, 100].map((value) => { d.setHappiness(value); return d.stage; });
const corruptions = corruptionPairs.map(([clean, broken]: [string, string]) => { d.setHappiness(100); return [broken, d.corruptText(`${clean}`, '咪咪')]; });
d.reset('Test'); d.setHappiness(100); d.adjustHappiness(-5); d.adjustHappiness(-5); d.adjustHappiness(-5);
const overflow = { happiness: d.snapshot.happiness, overflow: d.snapshot.overflow };
d.reset('Test'); d.setHappiness(30);
const small = [d.smallInteraction('water'), d.smallInteraction('water'), d.smallInteraction('water'), d.smallInteraction('water'), d.snapshot.happiness];
const endings: string[] = [];
d.reset('Test'); d.setHappiness(90); endings.push(d.ending());
d.setAwareness(30); endings.push(d.ending());
d.reset('Test'); clueIds.slice(0, 6).forEach((id) => d.discoverClue(id)); d.setAwareness(70); endings.push(d.ending());
d.reset('Test'); d.setHappiness(10); d.setDay(7); d.snapshot.cheerEscapes = 3; endings.push(d.ending());
d.reset('Test'); d.setHappiness(90); d.setAwareness(70); endings.push(d.ending() ?? 'none');
d.reset('Test'); d.setHappiness(10); d.snapshot.lowSince = performance.now() - 121_000; d.tick();
const forcedCheer = d.snapshot.cheerActive;
d.reset('Test'); d.snapshot.dayStartedAt = performance.now() - 241_000; d.acceptDay(); ['T1', 'T2', 'T3'].forEach((id) => d.completeTask(id));
const duskReached = d.snapshot.sunset; d.advanceDay();
const nextDay = { day: d.snapshot.day, boardRead: d.snapshot.boardRead, sunset: d.snapshot.sunset };
return { rewards, clueDeltas, clueCount: d.snapshot.clues.size, sad, stages, corruptions, overflow, small, endings, forcedCheer, duskReached, nextDay };
}, { tasks: TASKS.map(({ id, day, reward }) => ({ id, day, reward })), corruptionPairs: CORRUPTIONS, clueIds: Object.keys(CLUES) });
expect(result.rewards).toEqual(TASKS.map(({ id, reward }) => [id, reward]));
expect(result.clueDeltas).toEqual(Array(9).fill(8));
expect(result.sad).toEqual({ happiness: 25, awareness: 15 });
expect(result.stages).toEqual(['S0', 'S1', 'S2', 'S3', 'S4']);
result.corruptions.forEach(([broken, rendered]) => expect(rendered).toContain(broken));
expect(result.overflow).toEqual({ happiness: 100, overflow: 0 });
expect(result.small).toEqual([true, true, true, false, 33]);
expect(result.endings).toEqual(['E1', 'E2', 'E3', 'E4', 'none']);
expect(result.forcedCheer).toBe(true);
expect(result.duskReached).toBe(true);
expect(result.nextDay).toEqual({ day: 2, boardRead: false, sunset: false });
});
test('Day 6 smile timer completes at 60 seconds and scripted rain records sadness', async ({ page }) => {
await page.goto('/');
await page.locator('#player-name').press('Enter');
await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true);
await page.evaluate(() => (window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, key: string) => void }).emit('debug-teleport', 'PlazaScene'));
await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('PlazaScene'))).toBe(true);
const result = await page.evaluate(() => {
const director = window.__HONEY_GAME__.registry.get('director') as any;
const scene = window.__HONEY_GAME__.scene.getScene('PlazaScene') as any;
director.reset('Test'); director.setDay(6); director.acceptDay(); director.setHappiness(30);
director.snapshot.dayStartedAt = performance.now() - 241_000;
director.completeTask('T13'); director.setT14(true);
scene.t14Start = performance.now() - 60_100;
scene.updateT14();
const smile = { completed: director.snapshot.completed.has('T14'), active: director.snapshot.t14Active, sunset: director.snapshot.sunset, happiness: director.snapshot.happiness };
scene.rainSince = performance.now() - 10_100;
scene.updateRain(16);
return { smile, rain: { awareness: director.snapshot.awareness, happiness: director.snapshot.happiness } };
});
expect(result.smile).toEqual({ completed: true, active: false, sunset: true, happiness: 55 });
expect(result.rain).toEqual({ awareness: 3, happiness: 50 });
});
+13
View File
@@ -0,0 +1,13 @@
import { execFileSync } from 'node:child_process';
import { readdirSync, mkdirSync } from 'node:fs';
import { resolve } from 'node:path';
const root = resolve(import.meta.dirname, '..');
const sourceDir = resolve(root, 'public/maps/source');
const outDir = resolve(root, 'public/maps');
const tiled = process.env.TILED ?? '/Applications/Tiled.app/Contents/MacOS/Tiled';
mkdirSync(outDir, { recursive: true });
for (const file of readdirSync(sourceDir).filter((name) => name.endsWith('.tmx'))) {
execFileSync(tiled, ['--export-map', 'json', resolve(sourceDir, file), resolve(outDir, file.replace('.tmx', '.json'))], { stdio: 'inherit' });
}
console.log('Exported 10 Tiled JSON maps.');
+179
View File
@@ -0,0 +1,179 @@
local root = app.params["root"]
local assetDir = root .. "/public/assets"
local sourceDir = assetDir .. "/source"
app.fs.makeDirectory(root .. "/public")
app.fs.makeDirectory(assetDir)
app.fs.makeDirectory(sourceDir)
local C = {
transparent = Color{ r=0, g=0, b=0, a=0 },
grass = Color{ r=104, g=190, b=82, a=255 },
grass2 = Color{ r=76, g=158, b=70, a=255 },
cream = Color{ r=255, g=233, b=164, a=255 },
honey = Color{ r=239, g=151, b=49, a=255 },
orange = Color{ r=199, g=97, b=43, a=255 },
sky = Color{ r=93, g=190, b=224, a=255 },
water = Color{ r=68, g=171, b=216, a=255 },
water2 = Color{ r=139, g=224, b=231, a=255 },
stone = Color{ r=206, g=199, b=171, a=255 },
stone2 = Color{ r=162, g=155, b=133, a=255 },
brown = Color{ r=91, g=54, b=43, a=255 },
wood = Color{ r=151, g=91, b=48, a=255 },
leaf = Color{ r=51, g=130, b=60, a=255 },
pink = Color{ r=237, g=111, b=150, a=255 },
red = Color{ r=194, g=66, b=64, a=255 },
white = Color{ r=247, g=244, b=226, a=255 },
gray = Color{ r=194, g=199, b=199, a=255 },
gray2 = Color{ r=132, g=142, b=145, a=255 },
black = Color{ r=48, g=38, b=37, a=255 },
yellow = Color{ r=255, g=213, b=62, a=255 },
purple = Color{ r=133, g=89, b=159, a=255 },
}
local function fill(img, x, y, w, h, color)
for yy = y, y + h - 1 do
for xx = x, x + w - 1 do img:drawPixel(xx, yy, color) end
end
end
local function pixel(img, x, y, color) img:drawPixel(x, y, color) end
local function outlineRect(img, x, y, w, h, color)
fill(img, x, y, w, 1, color); fill(img, x, y+h-1, w, 1, color)
fill(img, x, y, 1, h, color); fill(img, x+w-1, y, 1, h, color)
end
local function save(sprite, sourceName, pngName)
sprite:saveAs(sourceDir .. "/" .. sourceName)
sprite:saveCopyAs(assetDir .. "/" .. pngName)
sprite:close()
end
local function makeTileset()
local sprite = Sprite(256, 64, ColorMode.RGB)
sprite.filename = "tileset"
sprite.cels[1].image:clear(C.transparent)
local img = sprite.cels[1].image
local function tile(index, base)
local x = (index % 16) * 16; local y = math.floor(index / 16) * 16
fill(img, x, y, 16, 16, base)
return x, y
end
for i=0,63 do
local palette = {C.grass, C.stone, C.water, C.cream, C.wood, C.gray}
local x,y = tile(i, palette[(i % #palette)+1])
if i % 6 == 0 then
pixel(img,x+3,y+5,C.grass2); pixel(img,x+11,y+3,C.cream); pixel(img,x+12,y+3,C.pink)
elseif i % 6 == 1 then
outlineRect(img,x+1,y+1,7,6,C.stone2); outlineRect(img,x+8,y+7,7,8,C.stone2)
elseif i % 6 == 2 then
fill(img,x,y+3,16,2,C.water2); fill(img,x+5,y+10,9,1,C.water2)
elseif i % 6 == 3 then
fill(img,x,y,16,4,C.honey); fill(img,x+2,y+7,4,3,C.orange); fill(img,x+10,y+6,4,4,C.orange)
elseif i % 6 == 4 then
fill(img,x,y+4,16,2,C.brown); fill(img,x+4,y,2,16,C.brown); fill(img,x+12,y,1,16,C.brown)
else
outlineRect(img,x+1,y+1,14,14,C.gray2); fill(img,x+3,y+3,10,10,C.white)
end
end
local x,y = tile(0,C.grass); pixel(img,x+3,y+5,C.grass2); pixel(img,x+11,y+9,C.grass2)
x,y = tile(1,C.stone); outlineRect(img,x,y,8,8,C.stone2); outlineRect(img,x+8,y+8,8,8,C.stone2)
x,y = tile(2,C.water); fill(img,x,y+4,12,2,C.water2); fill(img,x+5,y+11,11,1,C.water2)
x,y = tile(3,C.cream); fill(img,x,y,16,3,C.honey); fill(img,x,y+13,16,3,C.honey)
x,y = tile(4,C.wood); fill(img,x,y+4,16,2,C.brown); fill(img,x,y+11,16,1,C.brown)
x,y = tile(5,C.white); outlineRect(img,x,y,16,16,C.gray)
x,y = tile(6,C.grass); fill(img,x+2,y+3,12,10,C.leaf); pixel(img,x+6,y+6,C.red); pixel(img,x+11,y+9,C.red)
x,y = tile(7,C.grass); fill(img,x+1,y+2,14,12,C.leaf); fill(img,x+4,y+4,3,3,C.pink); fill(img,x+10,y+7,3,3,C.yellow)
x,y = tile(8,C.honey); fill(img,x,y+6,16,10,C.cream); outlineRect(img,x,y+6,16,10,C.orange)
x,y = tile(9,C.orange); fill(img,x,y+6,16,10,C.cream); fill(img,x+6,y+9,4,7,C.brown)
x,y = tile(10,C.cream); fill(img,x+2,y+5,12,8,C.wood); outlineRect(img,x+2,y+5,12,8,C.brown)
x,y = tile(11,C.grass); fill(img,x+6,y,4,16,C.wood); fill(img,x+1,y,14,8,C.leaf)
x,y = tile(12,C.grass); fill(img,x+2,y+2,12,12,C.pink); fill(img,x+5,y+5,6,6,C.yellow)
x,y = tile(13,C.wood); fill(img,x+4,y+2,8,14,C.black); fill(img,x+6,y+3,4,11,C.water)
x,y = tile(14,C.sky); fill(img,x+4,y,8,16,C.white); fill(img,x+1,y+4,14,8,C.white)
x,y = tile(15,C.white); fill(img,x,y,16,3,C.gray); fill(img,x,y+13,16,3,C.gray)
x,y = tile(16,C.grass); fill(img,x+3,y+5,10,7,C.white); outlineRect(img,x+3,y+5,10,7,C.brown); pixel(img,x+8,y+8,C.red)
save(sprite, "tileset.aseprite", "tileset.png")
end
local speciesColors = {C.cream,C.pink,C.brown,C.orange,C.gray,C.honey,C.grass2,C.purple,C.red,C.sky,C.yellow,C.wood,C.white}
local function drawCharacter(img, ox, oy, body, facing, step, expression, species)
fill(img,ox,oy,16,24,C.transparent)
local skin = body
fill(img,ox+4,oy+3,8,8,skin)
fill(img,ox+3,oy+5,10,5,skin)
if species == "rabbit" then fill(img,ox+4,oy,2,5,skin); fill(img,ox+10,oy,2,5,skin) end
if species == "deer" then pixel(img,ox+3,oy+2,C.brown); pixel(img,ox+12,oy+2,C.brown) end
if species == "bird" then fill(img,ox+3,oy+6,10,8,skin); pixel(img,ox+13,oy+8,C.honey) end
local eyeY = oy+6
pixel(img,ox+6,eyeY,C.black); pixel(img,ox+10,eyeY + (expression == "off" and 1 or 0),C.black)
if expression == "smile" then pixel(img,ox+7,oy+9,C.red); pixel(img,ox+8,oy+10,C.red); pixel(img,ox+9,oy+9,C.red)
elseif expression == "flat" then fill(img,ox+7,oy+9,3,1,C.black)
else pixel(img,ox+7,oy+9,C.black); fill(img,ox+8,oy+10,3,1,C.black) end
fill(img,ox+4,oy+12,8,8,body)
fill(img,ox+3,oy+13,2,6,C.cream); fill(img,ox+11,oy+13,2,6,C.cream)
local foot = step % 2
if facing == "left" or facing == "right" then
fill(img,ox+4-foot,oy+20,3,3,C.black); fill(img,ox+9+foot,oy+20,3,3,C.black)
else
fill(img,ox+4+foot,oy+20,3,3,C.black); fill(img,ox+9-foot,oy+20,3,3,C.black)
end
if facing == "up" then fill(img,ox+5,oy+5,6,4,skin); fill(img,ox+5,oy+6,6,2,C.brown) end
if facing == "left" then pixel(img,ox+5,eyeY,C.black); fill(img,ox+10,eyeY,2,2,skin) end
if facing == "right" then pixel(img,ox+11,eyeY,C.black); fill(img,ox+4,eyeY,2,2,skin) end
end
local function makeCharacters()
local cols, rows = 16, 4
local sprite = Sprite(cols*16, rows*24, ColorMode.RGB)
sprite.cels[1].image:clear(C.transparent)
local img = sprite.cels[1].image
local dirs = {"down","up","left","right"}
for d=0,3 do for f=0,3 do drawCharacter(img,f*16,d*24,C.sky,dirs[d+1],f,"smile","human") end end
local npc = {
{C.honey,"dog"},{C.white,"rabbit"},{C.brown,"bear"},{C.orange,"cat"},
{C.cream,"deer"},{C.sky,"bird"},{C.grass2,"turtle"}
}
for n=0,6 do for e=0,2 do
local index = 16 + n*3 + e; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24
local exp=({"smile","flat","off"})[e+1]
drawCharacter(img,ox,oy,npc[n+1][1],"down",0,exp,npc[n+1][2])
end end
for n=0,4 do
local index=37+n; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24
drawCharacter(img,ox,oy,speciesColors[n+8],"down",n%2,"smile","human")
end
for n=0,3 do
local index=42+n; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24
drawCharacter(img,ox,oy,(n%2==0 and C.pink or C.sky),"down",n%2,"smile","bird")
if n%2==1 then fill(img,ox+7,oy+9,4,2,C.black) end
end
save(sprite,"characters.aseprite","characters.png")
end
local function drawPortrait(img, ox, body, expression, real)
fill(img,ox,0,48,48,C.cream)
fill(img,ox+7,6,34,34,C.brown)
fill(img,ox+9,8,30,30,body)
fill(img,ox+14,17,4,5,C.black); fill(img,ox+30,17 + (expression=="off" and 2 or 0),4,5,C.black)
if real then
fill(img,ox+15,31,18,2,C.black); fill(img,ox+13,13,8,2,C.gray2); fill(img,ox+27,13,8,2,C.gray2)
elseif expression == "smile" then
fill(img,ox+17,29,4,3,C.red); fill(img,ox+21,32,8,3,C.red); fill(img,ox+29,29,3,3,C.red)
else fill(img,ox+18,30,14,2,C.black) end
end
local function makePortraits()
local sprite=Sprite(9*48,48,ColorMode.RGB); sprite.cels[1].image:clear(C.transparent)
local img=sprite.cels[1].image
drawPortrait(img,0,C.sky,"smile",false); drawPortrait(img,48,C.gray,"flat",true)
local colors={C.honey,C.white,C.brown,C.orange,C.cream,C.sky,C.grass2}
for i=0,6 do drawPortrait(img,(i+2)*48,colors[i+1],"smile",false) end
save(sprite,"portraits.aseprite","portraits.png")
end
makeTileset(); makeCharacters(); makePortraits()
print("Generated original Aseprite assets in " .. assetDir)
+129
View File
@@ -0,0 +1,129 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
const root = resolve(import.meta.dirname, '..');
const sourceDir = resolve(root, 'public/maps/source');
await mkdir(sourceDir, { recursive: true });
const maps = [
['HouseScene', '小满小屋', 12, 8, 4, [
['spawn', 96, 88, { id: 'default' }], ['door', 96, 112, { target: 'PlazaScene', spawn: 'house' }],
['interact', 42, 34, { id: 'calendar', label: '床头日历' }], ['interact', 64, 42, { id: 'bed', label: '床' }],
]],
['PlazaScene', '村广场', 30, 20, 0, [
['spawn', 240, 260, { id: 'default' }], ['spawn', 240, 280, { id: 'house' }], ['spawn', 32, 160, { id: 'bakery' }], ['spawn', 448, 160, { id: 'shop' }], ['spawn', 240, 32, { id: 'clinic' }], ['spawn', 32, 48, { id: 'lake' }], ['spawn', 448, 48, { id: 'field' }], ['spawn', 32, 280, { id: 'well' }], ['spawn', 448, 280, { id: 'mist' }],
['door', 240, 304, { target: 'HouseScene', spawn: 'default' }], ['door', 0, 160, { target: 'BakeryScene', spawn: 'default' }], ['door', 464, 160, { target: 'ShopScene', spawn: 'default' }], ['door', 240, 0, { target: 'ClinicScene', spawn: 'default' }], ['door', 0, 48, { target: 'LakeScene', spawn: 'default' }], ['door', 464, 48, { target: 'FieldScene', spawn: 'default' }], ['door', 0, 280, { target: 'WellScene', spawn: 'default' }], ['door', 464, 280, { target: 'MistScene', spawn: 'default' }],
['npc', 240, 132, { id: 'afu', label: '阿福' }], ['npc', 180, 162, { id: 'dabao', label: '大宝' }],
['npc', 232, 76, { id: 'birds', label: '双子鸟' }], ['npc', 124, 210, { id: 'extra1', label: '村民' }], ['npc', 352, 224, { id: 'extra2', label: '村民' }],
['interact', 240, 146, { id: 'board', label: '公告板' }], ['interact', 92, 148, { id: 'mail', label: '散落信件' }], ['interact', 200, 184, { id: 'flower1', label: '花坛' }], ['interact', 240, 200, { id: 'flower2', label: '花坛' }], ['interact', 280, 184, { id: 'flower3', label: '花坛' }],
['interact', 160, 100, { id: 'ribbon1', label: '彩带架' }], ['interact', 224, 92, { id: 'ribbon2', label: '彩带架' }], ['interact', 288, 92, { id: 'ribbon3', label: '彩带架' }], ['interact', 352, 100, { id: 'ribbon4', label: '彩带架' }], ['interact', 240, 224, { id: 'festival', label: '庆典场地' }],
]],
['BakeryScene', '面包房', 15, 10, 3, [
['spawn', 120, 128, { id: 'default' }], ['door', 120, 144, { target: 'PlazaScene', spawn: 'bakery' }], ['npc', 120, 72, { id: 'yuanyuan', label: '圆圆' }], ['interact', 48, 46, { id: 'recipe', label: '配方纸' }], ['interact', 184, 50, { id: 'oven', label: '烤炉' }],
]],
['ShopScene', '杂货店', 15, 10, 3, [
['spawn', 120, 128, { id: 'default' }], ['door', 120, 144, { target: 'PlazaScene', spawn: 'shop' }], ['npc', 120, 72, { id: 'mimi', label: '咪咪' }], ['interact', 48, 52, { id: 'shelf', label: '货架' }],
]],
['ClinicScene', '诊所', 12, 8, 5, [
['spawn', 96, 96, { id: 'default' }], ['door', 96, 112, { target: 'PlazaScene', spawn: 'clinic' }], ['npc', 96, 50, { id: 'doctor', label: '芙医生' }], ['interact', 48, 42, { id: 'form', label: '桌上表格' }],
]],
['LakeScene', '湖畔', 24, 16, 2, [
['spawn', 368, 128, { id: 'default' }], ['door', 368, 128, { target: 'PlazaScene', spawn: 'lake' }], ['npc', 142, 132, { id: 'turtle', label: '龟爷爷' }], ['npc', 284, 68, { id: 'extra4', label: '村民' }], ['interact', 190, 148, { id: 'picnic', label: '野餐布' }], ['interact', 110, 68, { id: 'lake', label: '湖面' }],
]],
['FieldScene', '花田', 20, 14, 0, [
['spawn', 16, 112, { id: 'default' }], ['door', 0, 112, { target: 'PlazaScene', spawn: 'field' }], ['npc', 244, 148, { id: 'extra3', label: '村民' }],
['interact', 80, 72, { id: 'berry1', label: '莓果丛' }], ['interact', 128, 56, { id: 'berry2', label: '莓果丛' }], ['interact', 176, 88, { id: 'berry3', label: '莓果丛' }], ['interact', 224, 64, { id: 'berry4', label: '莓果丛' }], ['interact', 272, 96, { id: 'berry5', label: '莓果丛' }],
]],
['WellScene', '古井空地', 14, 10, 0, [
['spawn', 208, 80, { id: 'default' }], ['door', 208, 80, { target: 'PlazaScene', spawn: 'well' }], ['npc', 46, 116, { id: 'extra5', label: '村民' }], ['interact', 104, 72, { id: 'well', label: '古井' }],
]],
['MistScene', '雾墙边界', 20, 8, 0, [
['spawn', 16, 64, { id: 'default' }], ['door', 0, 64, { target: 'PlazaScene', spawn: 'mist' }], ['interact', 288, 64, { id: 'mist', label: '雾墙' }],
]],
['WhiteScene', '白色层', 30, 8, 5, [
['spawn', 24, 64, { id: 'default' }], ['interact', 400, 64, { id: 'record', label: '病历' }],
]],
];
function createTileData(key, width, height, ground) {
const data = Array(width * height).fill(ground + 1);
const set = (x, y, gid) => { if (x >= 0 && y >= 0 && x < width && y < height) data[y * width + x] = gid; };
const rect = (x, y, w, h, gid) => { for (let yy = y; yy < y + h; yy += 1) for (let xx = x; xx < x + w; xx += 1) set(xx, yy, gid); };
const building = (x, y, w, h) => {
rect(x, y, w, 2, 9); rect(x, y + 2, w, h - 2, 4);
set(x + Math.floor(w / 2), y + h - 1, 10);
for (let xx = x + 1; xx < x + w - 1; xx += 3) set(xx, y + 3, 15);
};
if (key === 'PlazaScene') {
rect(13, 0, 4, height, 2); rect(0, 8, width, 4, 2);
building(2, 1, 7, 6); building(21, 1, 7, 6); building(2, 14, 7, 5); building(21, 14, 7, 5);
rect(10, 2, 3, 3, 12); set(15, 8, 11);
set(5, 9, 17);
[[12, 11], [13, 12], [14, 11], [16, 11], [17, 12], [18, 11]].forEach(([x, y]) => set(x, y, 8));
[[1, 3], [9, 6], [19, 5], [28, 3], [4, 12], [25, 12], [10, 16], [19, 16]].forEach(([x, y]) => set(x, y, 13));
} else if (key === 'HouseScene') {
rect(0, 0, width, height, 4); rect(0, 0, width, 1, 5); rect(0, height - 1, width, 1, 5);
rect(2, 2, 3, 2, 5); set(8, 2, 11); rect(6, 5, 2, 1, 8); set(6, 7, 10);
} else if (key === 'BakeryScene') {
rect(0, 0, width, height, 4); rect(1, 1, width - 2, 2, 5); set(11, 2, 13); rect(2, 5, 11, 1, 5); set(7, 9, 10);
} else if (key === 'ShopScene') {
rect(0, 0, width, height, 4); for (let x = 1; x < width - 1; x += 3) rect(x, 1, 2, 4, 5); set(7, 9, 10);
for (let y = 2; y <= 4; y += 1) for (const x of [2, 5, 8]) set(x, y, 7);
} else if (key === 'ClinicScene') {
rect(0, 0, width, height, 6); rect(0, 0, width, 1, 16); rect(2, 2, 5, 2, 5); set(6, 7, 10); set(9, 2, 15);
} else if (key === 'LakeScene') {
rect(0, 0, 10, height, 3); rect(10, 0, width - 10, height, 1); rect(10, 7, width - 10, 3, 2);
rect(11, 9, 3, 2, 5); rect(14, 9, 3, 2, 8); for (let y = 2; y < height - 2; y += 4) set(20, y, 12);
} else if (key === 'FieldScene') {
rect(0, 6, width, 3, 2);
for (let y = 1; y < height - 1; y += 3) for (let x = 2; x < width - 1; x += 3) set(x, y, (x + y) % 2 ? 7 : 8);
} else if (key === 'WellScene') {
rect(3, 2, 8, 6, 2); rect(5, 3, 4, 4, 14); set(13, 5, 10);
} else if (key === 'MistScene') {
rect(0, 3, width - 3, 3, 2); rect(width - 4, 0, 4, height, 15);
} else if (key === 'WhiteScene') {
rect(0, 0, width, height, 6); rect(0, 0, width, 1, 16); rect(0, height - 1, width, 1, 16); rect(19, 1, 1, height - 2, 16); set(25, 4, 5);
}
return data;
}
const collisionSpecs = {
PlazaScene: [[32, 16, 112, 96], [336, 16, 112, 96], [32, 224, 112, 76], [336, 224, 112, 76], [160, 32, 48, 48], [232, 128, 32, 24]],
BakeryScene: [[16, 16, 208, 32], [176, 32, 32, 30], [32, 80, 176, 12]],
ShopScene: [[16, 16, 32, 64], [64, 16, 32, 64], [112, 16, 32, 64], [160, 16, 32, 64], [208, 16, 16, 64]],
ClinicScene: [[32, 32, 80, 32]],
LakeScene: [[0, 0, 150, 256]],
WellScene: [[80, 48, 64, 64]],
WhiteScene: [[304, 16, 16, 96]],
};
const propertyXml = (properties) => Object.entries(properties).map(([name, value]) => ` <property name="${name}" value="${String(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;')}"/>`).join('\n');
for (const [key, displayName, width, height, ground, objects] of maps) {
const data = createTileData(key, width, height, ground);
const borders = [
`<object id="900" x="0" y="0" width="${width * 16}" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>`,
`<object id="901" x="0" y="${height * 16 - 4}" width="${width * 16}" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>`,
`<object id="902" x="0" y="0" width="4" height="${height * 16}"><properties><property name="collides" type="bool" value="true"/></properties></object>`,
`<object id="903" x="${width * 16 - 4}" y="0" width="4" height="${height * 16}"><properties><property name="collides" type="bool" value="true"/></properties></object>`,
];
(collisionSpecs[key] ?? []).forEach(([x, y, w, h], index) => borders.push(`<object id="${910 + index}" x="${x}" y="${y}" width="${w}" height="${h}"><properties><property name="collides" type="bool" value="true"/></properties></object>`));
const objectXml = objects.map(([type, x, y, properties], index) => ` <object id="${index + 1}" name="${properties.label ?? properties.id}" type="${type}" x="${x}" y="${y}" width="16" height="16">\n <properties>\n${propertyXml(properties)}\n </properties>\n </object>`).join('\n');
const tmx = `<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${width}" height="${height}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1000">
<properties><property name="sceneKey" value="${key}"/><property name="displayName" value="${displayName}"/></properties>
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
<layer id="1" name="ground" width="${width}" height="${height}"><data encoding="csv">${data.join(',')}</data></layer>
<objectgroup id="2" name="obstacles">${borders.join('\n')}</objectgroup>
<objectgroup id="3" name="objects">\n${objectXml}\n </objectgroup>
</map>`;
await writeFile(resolve(sourceDir, `${key}.tmx`), tmx);
}
await writeFile(resolve(sourceDir, 'tileset.tsx'), `<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.12.2" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16">
<image source="../../assets/tileset.png" width="256" height="64"/>
</tileset>`);
console.log(`Generated ${maps.length} original Tiled source maps in ${sourceDir}`);
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": false,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src", "tests", "vite.config.ts", "playwright.config.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
export default defineConfig({
base: './',
server: { host: '127.0.0.1', port: 4173 },
preview: { host: '127.0.0.1', port: 4173 },
});