Honey Village: six model builds + hub frontend, Dockerized for Dokploy
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.local
|
||||
.vite/
|
||||
playwright-report/
|
||||
test-results/
|
||||
@@ -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
|
||||
@@ -0,0 +1,226 @@
|
||||
# PRD:《崩坏童话:蜜糖村》网页版
|
||||
|
||||
> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局
|
||||
|
||||
## 1. 游戏理念与设计支柱
|
||||
|
||||
一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。
|
||||
|
||||
四条设计支柱,所有实现争议回到这里裁决:
|
||||
|
||||
1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。
|
||||
2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。
|
||||
3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。
|
||||
4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。
|
||||
|
||||
## 2. 世界观:三层结构
|
||||
|
||||
| 层 | 名称 | 玩家所见 | 实质 |
|
||||
|---|---|---|---|
|
||||
| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 |
|
||||
| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" |
|
||||
| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 |
|
||||
|
||||
命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。
|
||||
|
||||
## 3. 主角设定
|
||||
|
||||
- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。
|
||||
- 性别中性像素小人,16×24 像素/帧,无内心独白。
|
||||
- 对话头像永远同一帧微笑(见支柱 2)。
|
||||
- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。
|
||||
|
||||
## 4. NPC 清单(封闭,7 组 + 群演)
|
||||
|
||||
每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。
|
||||
|
||||
| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 |
|
||||
|---|---|---|---|---|---|
|
||||
| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 |
|
||||
| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) |
|
||||
| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 |
|
||||
| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) |
|
||||
| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 |
|
||||
| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 |
|
||||
| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 |
|
||||
| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — |
|
||||
|
||||
## 5. 七日故事线
|
||||
|
||||
每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。
|
||||
|
||||
| 日 | 主题 | 剧情要点 | 侵蚀预期 |
|
||||
|---|---|---|---|
|
||||
| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 |
|
||||
| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 |
|
||||
| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 |
|
||||
| Day 4 | 完美的重复 | 任务 T8–T10 与 Day 1 的 T1–T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 |
|
||||
| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 |
|
||||
| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 |
|
||||
| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 |
|
||||
|
||||
阿福每日公告(逐字实现,注意措辞逐日滑移):
|
||||
|
||||
- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」
|
||||
- D2「早上好呀!新的一天,新的幸福。祝你开心!」
|
||||
- D3「早上好!今天要把幸福装得满满的哦。要开心!」
|
||||
- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同)
|
||||
- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」
|
||||
- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」
|
||||
- D7「庆典开始了。来吧。」
|
||||
|
||||
## 6. 核心系统
|
||||
|
||||
### 6.1 幸福值(明线)
|
||||
|
||||
- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。
|
||||
- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。
|
||||
- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。
|
||||
- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。
|
||||
- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。
|
||||
|
||||
### 6.2 觉知值(暗线,永不显示)
|
||||
|
||||
- 范围 0–100,开局 0。只升不降。
|
||||
- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。
|
||||
|
||||
### 6.3 悲伤行为(封闭清单,5 项)
|
||||
|
||||
拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。
|
||||
|
||||
### 6.4 侵蚀阶段矩阵(唯一真源)
|
||||
|
||||
阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。
|
||||
|
||||
| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5) | UI |
|
||||
|---|---|---|---|---|---|---|
|
||||
| S0 | 0–20 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 0–2 瓣 |
|
||||
| S1 | 21–50 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 3–6 瓣 |
|
||||
| S2 | 51–80 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 |
|
||||
| S3 | 81–99 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 |
|
||||
| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 |
|
||||
|
||||
NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。
|
||||
|
||||
### 6.5 文本腐蚀
|
||||
|
||||
同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。
|
||||
|
||||
### 6.6 BGM 与音效
|
||||
|
||||
- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。
|
||||
- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。
|
||||
- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。
|
||||
- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。
|
||||
|
||||
## 7. 任务清单(封闭,15 个)
|
||||
|
||||
| ID | 日 | 任务 | 幸福奖励 | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 |
|
||||
| T2 | D1 | 给广场花坛浇水 ×3 | +10 | |
|
||||
| T3 | D1 | 与任意三位村民问好 | +10 | |
|
||||
| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) |
|
||||
| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) |
|
||||
| T6 | D3 | 去花田采莓果 ×5 | +10 | |
|
||||
| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 |
|
||||
| T8–T10 | D4 | 与 T1–T3 逐字相同 | 同上 | 刻意恐怖谷 |
|
||||
| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | |
|
||||
| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) |
|
||||
| T13 | D6 | 在广场挂彩带 ×4 | +10 | |
|
||||
| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 |
|
||||
| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 |
|
||||
|
||||
## 8. 线索清单(封闭,9 处,每处觉知 +8)
|
||||
|
||||
| ID | 位置/来源 | 内容 | 可发现时段 |
|
||||
|---|---|---|---|
|
||||
| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 |
|
||||
| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 |
|
||||
| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 |
|
||||
| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 |
|
||||
| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 |
|
||||
| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5(T12 中) |
|
||||
| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 |
|
||||
| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 |
|
||||
| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 |
|
||||
|
||||
## 9. 结局(封闭,4 个)
|
||||
|
||||
Day 7 庆典按以下优先级判定(自上而下第一个满足者生效):
|
||||
|
||||
| 结局 | 条件 | 演出 |
|
||||
|---|---|---|
|
||||
| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 |
|
||||
| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 |
|
||||
| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 |
|
||||
| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 |
|
||||
|
||||
任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。
|
||||
|
||||
## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON)
|
||||
|
||||
| 场景 | 尺寸(瓦片) | 要点 |
|
||||
|---|---|---|
|
||||
| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) |
|
||||
| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 |
|
||||
| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) |
|
||||
| 杂货店(室内) | 15×10 | 货架(C5) |
|
||||
| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) |
|
||||
| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 |
|
||||
| 花田 | 20×14 | 莓果丛(T6)、浇花点 |
|
||||
| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 |
|
||||
| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 |
|
||||
| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 |
|
||||
|
||||
统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。
|
||||
|
||||
## 11. 美术规范(Aseprite 制作,全部原创)
|
||||
|
||||
- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。
|
||||
- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。
|
||||
- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。
|
||||
- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。
|
||||
- 禁止使用任何现成素材、素材包或开源 tileset。
|
||||
|
||||
## 12. 技术架构
|
||||
|
||||
- **Phaser 3 + TypeScript + Vite**,`npm run build` 产出纯静态 `dist/`,`npm run preview` 本地游玩。
|
||||
- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。
|
||||
- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`。
|
||||
- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。
|
||||
- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。
|
||||
- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。
|
||||
- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。
|
||||
- 无 localStorage、无外部网络请求、无外部音频/字体文件。
|
||||
|
||||
## 13. Debug 面板(服务验收,必须实现)
|
||||
|
||||
按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。
|
||||
|
||||
## 14. 验收清单(共 20 条)
|
||||
|
||||
1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。
|
||||
2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。
|
||||
3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。
|
||||
4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。
|
||||
5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。
|
||||
6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。
|
||||
7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。
|
||||
8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。
|
||||
9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。
|
||||
10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。
|
||||
11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。
|
||||
12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。
|
||||
13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。
|
||||
14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。
|
||||
15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。
|
||||
16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。
|
||||
17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。
|
||||
18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。
|
||||
19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。
|
||||
20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。
|
||||
|
||||
## 15. 明确不做
|
||||
|
||||
战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。
|
||||
@@ -0,0 +1,104 @@
|
||||
# 崩坏童话:蜜糖村
|
||||
|
||||
纯单机网页游戏(Phaser 3 + TypeScript + Vite)。无存档、无后端、无 localStorage;浏览器打开即玩。完整流程约 25–35 分钟,含 4 个结局。
|
||||
|
||||
## 启动方式
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # 开发
|
||||
npm run build # 产出静态 dist/
|
||||
npm run preview # 本地游玩构建结果
|
||||
npm test # 单元测试(PRD 数值/台词/矩阵)
|
||||
```
|
||||
|
||||
请用 `npm run dev` 或 `npm run preview` 访问,不要直接用 `file://` 打开(ESM 模块加载需要 HTTP)。
|
||||
|
||||
## 按键
|
||||
|
||||
| 按键 | 作用 |
|
||||
|------|------|
|
||||
| WASD / 方向键 | 四向移动 |
|
||||
| E / 空格 | 交互、推进对话 |
|
||||
| 1 / 2 / 3 | 对话选项(温柔 / 迟疑 / 否定) |
|
||||
| Enter | 确认名字;结局「回到标题」 |
|
||||
| \` (反引号) | 开关 Debug 面板 |
|
||||
|
||||
靠近可交互物(≤24 像素)会出现「按 E」提示。
|
||||
|
||||
## 游玩流程
|
||||
|
||||
1. 标题画面 → 输入名字(留空默认为「小满」)→ Day 1 小屋
|
||||
2. 每日:晨起 → 广场公告板接任务 → 完成任务 → 黄昏钟声 → 回屋睡觉 → 梦境 → 次日
|
||||
3. Day 7 参加「第 413 届丰收庆典」,按幸福 / 觉知 / 线索走向 E1–E4 之一
|
||||
|
||||
幸福值**不显示数字**:左上角向日葵 12 瓣按值点亮,满格后花心出现黑籽。
|
||||
|
||||
## Debug 面板(默认关闭)
|
||||
|
||||
按 \` 打开。仅服务验收,正常游玩无 UI 痕迹。
|
||||
|
||||
| 键 | 功能 |
|
||||
|----|------|
|
||||
| H / J | 幸福 ±10 |
|
||||
| K / L | 觉知 ±10(L 仅 Debug 可下调) |
|
||||
| Shift+1–7 | 设置当前日(用 `code` Digit1–7,兼容浏览器 Shift 改写 `key` 为 !@#) |
|
||||
| C | 置齐全部线索 |
|
||||
| 0–9 | 传送到 10 个场景(0小屋 1广场 2面包房 3杂货店 4诊所 5湖畔 6花田 7古井 8雾墙 9白色层) |
|
||||
| V | 循环传送下一场景 |
|
||||
| F1–F4 | 强制结局 E1–E4 |
|
||||
| N | 负片闪帧 |
|
||||
| P | 触发欢乐庆典 |
|
||||
|
||||
## 素材与地图制作流程(全部原创)
|
||||
|
||||
本项目**禁止**使用任何现成素材包。像素图与地图均由仓库内脚本 + 本机工具生成:
|
||||
|
||||
### Aseprite
|
||||
|
||||
本机路径示例:`/Applications/Aseprite.app/Contents/MacOS/aseprite`
|
||||
|
||||
```bash
|
||||
export PATH="/Applications/Aseprite.app/Contents/MacOS:$PATH"
|
||||
aseprite -b -script scripts/generate_tileset.lua # → public/assets/tilesets/village.png(128 瓦 16×16)
|
||||
aseprite -b -script scripts/generate_sprites.lua # → player / npcs / birds / extras / portraits / sunflower
|
||||
```
|
||||
|
||||
- 瓦片:草地、石板路、湖水、花丛、莓果、树、房屋、室内家具、井、雾、白色层等
|
||||
- 主角:16×24,4 朝向 × 4 帧行走;对话微笑头像 + E3「真实的脸」
|
||||
- NPC ×7:各 3 档表情 smile / flat / off(off 仅 1–2 像素差异)
|
||||
- 双子鸟 2 帧张嘴;群演 ×5
|
||||
|
||||
源文件同时保存 `.aseprite` 与导出 `.png`。
|
||||
|
||||
### Tiled Map Editor
|
||||
|
||||
本机路径示例:`/Applications/Tiled.app/Contents/MacOS/tiled`
|
||||
|
||||
```bash
|
||||
node scripts/generate-maps.mjs
|
||||
# 可选:用 Tiled CLI 再导出校验
|
||||
tiled --export-map json mapsrc/house.tmx public/assets/maps/house_tiled_export.json
|
||||
```
|
||||
|
||||
10 张地图(§10):小屋、村广场、面包房、杂货店、诊所、湖畔、花田、古井空地、雾墙边界、白色层。
|
||||
结构层:`ground` / `obstacles`(碰撞)/ `objects`(spawn、NPC、door、交互点)。
|
||||
源 TMX 在 `mapsrc/`,运行时 JSON 在 `public/assets/maps/`。
|
||||
|
||||
### 一键重生成
|
||||
|
||||
```bash
|
||||
npm run assets # 若已配置 scripts/generate-assets.sh
|
||||
node scripts/generate-maps.mjs
|
||||
```
|
||||
|
||||
## 技术要点
|
||||
|
||||
- 逻辑分辨率 320×180,整数倍放大,`pixelArt: true`,无纹理平滑
|
||||
- `CorruptionDirector`:幸福 / 觉知 / 日 / 任务 / 线索 / 侵蚀阶段唯一真源
|
||||
- BGM 与 4 个音效均为 Web Audio 程序合成,无外部音频文件
|
||||
- 无 localStorage、无外部网络资源 / 字体
|
||||
|
||||
## 验收
|
||||
|
||||
PRD §14 共 20 条。单元测试覆盖台词、任务奖励、侵蚀矩阵、错字表、结局优先级、悲伤数值等。
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>崩坏童话:蜜糖村</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%; height: 100%;
|
||||
background: #1a1410;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#game-container {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
canvas {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game-container"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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="7">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="15" height="10">
|
||||
<data encoding="csv">
|
||||
17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="15" height="10">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,20,20,20,20,20,0,0,0,43,0,21,0,13,13,0,20,20,20,20,20,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,23,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,13,13,13,13,13,0,0,13,13,13,13,13,13,13
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="112" y="112" width="16" height="16"/>
|
||||
<object id="2" name="yuanyuan" type="npc" x="64" y="64" width="16" height="16"/>
|
||||
<object id="3" name="to_plaza" type="door" x="112" y="144" width="16" height="16"/>
|
||||
<object id="4" name="recipe" type="interact" x="160" y="32" width="16" height="16"/>
|
||||
<object id="5" name="counter" type="interact" x="64" y="48" width="16" height="16"/>
|
||||
<object id="6" name="knead" type="interact" x="48" y="48" width="16" height="16"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="12" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="5">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="12" height="8">
|
||||
<data encoding="csv">
|
||||
18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="12" height="8">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
13,13,13,13,13,13,13,13,13,13,13,13,13,0,0,0,0,0,0,0,0,0,0,13,13,0,0,23,44,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,19,0,0,13,13,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,13,13,13,13,13,13,0,0,13,13,13,13,13
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="96" y="80" width="16" height="16"/>
|
||||
<object id="2" name="fu" type="npc" x="48" y="48" width="16" height="16"/>
|
||||
<object id="3" name="to_plaza" type="door" x="96" y="112" width="16" height="16"/>
|
||||
<object id="4" name="form" type="interact" x="64" y="32" width="16" height="16"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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="10">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="20" height="14">
|
||||
<data encoding="csv">
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="20" height="14">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,9,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="160" y="176" width="16" height="16"/>
|
||||
<object id="2" name="extra2" type="npc" x="96" y="128" width="16" height="16"/>
|
||||
<object id="3" name="to_plaza" type="door" x="160" y="208" width="16" height="16"/>
|
||||
<object id="4" name="berry1" type="interact" x="48" y="48" width="16" height="16"/>
|
||||
<object id="5" name="berry2" type="interact" x="80" y="64" width="16" height="16"/>
|
||||
<object id="6" name="berry3" type="interact" x="112" y="48" width="16" height="16"/>
|
||||
<object id="7" name="berry4" type="interact" x="192" y="80" width="16" height="16"/>
|
||||
<object id="8" name="berry5" type="interact" x="224" y="48" width="16" height="16"/>
|
||||
<object id="9" name="pot" type="interact" x="160" y="32" width="16" height="16"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="4">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="20" height="8">
|
||||
<data encoding="csv">
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="20" height="8">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26,0,0,0,0,0,0,0,0,0,0,0,0,26,26,26,26,26,26,26,26
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="48" y="64" width="16" height="16"/>
|
||||
<object id="2" name="to_plaza" type="door" x="0" y="64" width="16" height="16"/>
|
||||
<object id="3" name="fogwall" type="interact" x="192" y="64" width="16" height="16"/>
|
||||
</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="12" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="6">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="12" height="8">
|
||||
<data encoding="csv">
|
||||
17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,18,18,17,17,17,17,17
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="12" height="8">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
12,12,12,12,12,12,12,12,12,12,12,12,12,0,0,0,0,0,0,0,0,0,0,12,12,0,19,19,0,0,0,0,0,29,0,12,12,0,0,0,0,0,0,0,0,0,0,12,12,0,0,0,0,23,0,0,0,0,0,12,12,24,0,0,0,0,0,0,0,0,0,12,12,0,0,0,0,0,0,0,0,0,0,12,12,12,12,12,12,0,0,12,12,12,12,12
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="96" y="80" width="16" height="16"/>
|
||||
<object id="2" name="to_plaza" type="door" x="80" y="112" width="16" height="16"/>
|
||||
<object id="3" name="to_plaza2" type="door" x="96" y="112" width="16" height="16"/>
|
||||
<object id="4" name="bed" type="interact" x="32" y="32" width="16" height="16"/>
|
||||
<object id="5" name="calendar" type="interact" x="144" y="32" width="16" height="16"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="24" height="16" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="7">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="24" height="16">
|
||||
<data encoding="csv">
|
||||
2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,2,2,2,2,2,2,2,2,2,2,2,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="24" height="16">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,24,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,24,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="176" y="32" width="16" height="16"/>
|
||||
<object id="2" name="gui" type="npc" x="144" y="80" width="16" height="16"/>
|
||||
<object id="3" name="extra3" type="npc" x="288" y="64" width="16" height="16"/>
|
||||
<object id="4" name="to_plaza" type="door" x="176" y="0" width="16" height="16"/>
|
||||
<object id="5" name="picnic" type="interact" x="192" y="96" width="16" height="16"/>
|
||||
<object id="6" name="bench" type="interact" x="144" y="64" width="16" height="16"/>
|
||||
</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="30" height="20" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="30">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="30" height="20">
|
||||
<data encoding="csv">
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="30" height="20">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,14,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,30,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,0,0,0,0,0,0,0,34,0,0,0,0,34,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34,0,0,0,0,34,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="240" y="192" width="16" height="16"/>
|
||||
<object id="100" name="from_house" type="spawn" x="96" y="80" width="16" height="16"/>
|
||||
<object id="101" name="from_bakery" type="spawn" x="384" y="144" width="16" height="16"/>
|
||||
<object id="102" name="from_shop" type="spawn" x="48" y="144" width="16" height="16"/>
|
||||
<object id="103" name="from_clinic" type="spawn" x="64" y="224" width="16" height="16"/>
|
||||
<object id="104" name="from_lake" type="spawn" x="240" y="272" width="16" height="16"/>
|
||||
<object id="105" name="from_field" type="spawn" x="240" y="32" width="16" height="16"/>
|
||||
<object id="106" name="from_well" type="spawn" x="432" y="160" width="16" height="16"/>
|
||||
<object id="107" name="from_fog" type="spawn" x="32" y="256" width="16" height="16"/>
|
||||
<object id="10" name="afu" type="npc" x="240" y="96" width="16" height="16"/>
|
||||
<object id="11" name="dabao" type="npc" x="80" y="176" width="16" height="16"/>
|
||||
<object id="12" name="twinbirds" type="npc" x="320" y="48" width="16" height="16"/>
|
||||
<object id="13" name="extra1" type="npc" x="288" y="224" width="16" height="16"/>
|
||||
<object id="14" name="extra4" type="npc" x="160" y="192" width="16" height="16"/>
|
||||
<object id="15" name="house" type="door" x="96" y="48" width="16" height="16"/>
|
||||
<object id="16" name="bakery" type="door" x="400" y="144" width="16" height="16"/>
|
||||
<object id="17" name="shop" type="door" x="32" y="144" width="16" height="16"/>
|
||||
<object id="18" name="clinic" type="door" x="64" y="240" width="16" height="16"/>
|
||||
<object id="19" name="lake" type="door" x="240" y="304" width="16" height="16"/>
|
||||
<object id="20" name="field" type="door" x="240" y="0" width="16" height="16"/>
|
||||
<object id="21" name="well" type="door" x="464" y="160" width="16" height="16"/>
|
||||
<object id="22" name="fog" type="door" x="0" y="256" width="16" height="16"/>
|
||||
<object id="23" name="board" type="interact" x="240" y="80" width="16" height="16"/>
|
||||
<object id="24" name="flowers" type="interact" x="176" y="96" width="16" height="16"/>
|
||||
<object id="25" name="mailbox" type="interact" x="64" y="160" width="16" height="16"/>
|
||||
<object id="26" name="ribbon1" type="interact" x="192" y="160" width="16" height="16"/>
|
||||
<object id="27" name="ribbon2" type="interact" x="272" y="160" width="16" height="16"/>
|
||||
<object id="28" name="ribbon3" type="interact" x="192" y="208" width="16" height="16"/>
|
||||
<object id="29" name="ribbon4" type="interact" x="272" y="208" width="16" height="16"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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="5">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="15" height="10">
|
||||
<data encoding="csv">
|
||||
18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="15" height="10">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,22,22,22,22,22,22,22,22,22,22,22,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,22,22,22,22,22,22,22,22,22,22,22,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,13,13,13,13,13,13,0,0,13,13,13,13,13,13
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="112" y="112" width="16" height="16"/>
|
||||
<object id="2" name="mimi" type="npc" x="112" y="80" width="16" height="16"/>
|
||||
<object id="3" name="to_plaza" type="door" x="112" y="144" width="16" height="16"/>
|
||||
<object id="4" name="shelf" type="interact" x="80" y="32" width="16" height="16"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.2" name="village" tilewidth="16" tileheight="16" tilecount="128" columns="16">
|
||||
<image source="../public/assets/tilesets/village.png" width="256" height="128"/>
|
||||
</tileset>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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="5">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="14" height="10">
|
||||
<data encoding="csv">
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="14" height="10">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,25,0,0,0,0,0,0,0,0,0,0,0,0,25,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="32" y="80" width="16" height="16"/>
|
||||
<object id="2" name="extra5" type="npc" x="160" y="112" width="16" height="16"/>
|
||||
<object id="3" name="to_plaza" type="door" x="0" y="80" width="16" height="16"/>
|
||||
<object id="4" name="well" type="interact" x="112" y="64" width="16" height="16"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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="5">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="20" height="14">
|
||||
<data encoding="csv">
|
||||
28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="20" height="14">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,27,27,27,27,27,27,27,27,27,0,27,27,27,27,27,27,27,27,27,0,0,0,0,0,27,27,27,27,27,27,27,27,27,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,44,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,19,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0,0,0,27,27,27,27,27,27,27,27,27,27,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" type="spawn" x="32" y="48" width="16" height="16"/>
|
||||
<object id="2" name="record" type="interact" x="176" y="128" width="16" height="16"/>
|
||||
<object id="3" name="to_ward" type="door" x="160" y="80" width="16" height="16"/>
|
||||
<object id="200" name="ward" type="spawn" x="160" y="128" width="16" height="16"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "honey-village",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"assets": "bash scripts/generate-assets.sh",
|
||||
"maps": "bash scripts/generate-maps.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.1.1",
|
||||
"playwright": "^1.61.1",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"phaser": "^3.87.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
{
|
||||
"compressionlevel": -1,
|
||||
"height": 10,
|
||||
"width": 15,
|
||||
"infinite": false,
|
||||
"layers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ground",
|
||||
"type": "tilelayer",
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"data": [
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "obstacles",
|
||||
"type": "tilelayer",
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"data": [
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
43,
|
||||
0,
|
||||
21,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
23,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "objects",
|
||||
"type": "objectgroup",
|
||||
"objects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "spawn",
|
||||
"type": "spawn",
|
||||
"x": 112,
|
||||
"y": 112,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "yuanyuan",
|
||||
"type": "npc",
|
||||
"x": 64,
|
||||
"y": 64,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "npcId",
|
||||
"type": "string",
|
||||
"value": "yuanyuan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "to_plaza",
|
||||
"type": "door",
|
||||
"x": 112,
|
||||
"y": 144,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "PlazaScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "from_bakery"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "recipe",
|
||||
"type": "interact",
|
||||
"x": 160,
|
||||
"y": 32,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "recipe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "counter",
|
||||
"type": "interact",
|
||||
"x": 64,
|
||||
"y": 48,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "bread"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "knead",
|
||||
"type": "interact",
|
||||
"x": 48,
|
||||
"y": 48,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "knead"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"nextlayerid": 4,
|
||||
"nextobjectid": 7,
|
||||
"orientation": "orthogonal",
|
||||
"renderorder": "right-down",
|
||||
"tiledversion": "1.12.2",
|
||||
"tileheight": 16,
|
||||
"tilewidth": 16,
|
||||
"type": "map",
|
||||
"version": "1.10",
|
||||
"tilesets": [
|
||||
{
|
||||
"firstgid": 1,
|
||||
"columns": 16,
|
||||
"image": "../tilesets/village.png",
|
||||
"imagewidth": 256,
|
||||
"imageheight": 128,
|
||||
"tilecount": 128,
|
||||
"tilewidth": 16,
|
||||
"tileheight": 16,
|
||||
"name": "village",
|
||||
"margin": 0,
|
||||
"spacing": 0,
|
||||
"tiles": [
|
||||
{
|
||||
"id": 9,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
{
|
||||
"compressionlevel": -1,
|
||||
"height": 8,
|
||||
"width": 12,
|
||||
"infinite": false,
|
||||
"layers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ground",
|
||||
"type": "tilelayer",
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"data": [
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "obstacles",
|
||||
"type": "tilelayer",
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"data": [
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
23,
|
||||
44,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "objects",
|
||||
"type": "objectgroup",
|
||||
"objects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "spawn",
|
||||
"type": "spawn",
|
||||
"x": 96,
|
||||
"y": 80,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "fu",
|
||||
"type": "npc",
|
||||
"x": 48,
|
||||
"y": 48,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "npcId",
|
||||
"type": "string",
|
||||
"value": "fu"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "to_plaza",
|
||||
"type": "door",
|
||||
"x": 96,
|
||||
"y": 112,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "PlazaScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "from_clinic"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "form",
|
||||
"type": "interact",
|
||||
"x": 64,
|
||||
"y": 32,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "form"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"nextlayerid": 4,
|
||||
"nextobjectid": 5,
|
||||
"orientation": "orthogonal",
|
||||
"renderorder": "right-down",
|
||||
"tiledversion": "1.12.2",
|
||||
"tileheight": 16,
|
||||
"tilewidth": 16,
|
||||
"type": "map",
|
||||
"version": "1.10",
|
||||
"tilesets": [
|
||||
{
|
||||
"firstgid": 1,
|
||||
"columns": 16,
|
||||
"image": "../tilesets/village.png",
|
||||
"imagewidth": 256,
|
||||
"imageheight": 128,
|
||||
"tilecount": 128,
|
||||
"tilewidth": 16,
|
||||
"tileheight": 16,
|
||||
"name": "village",
|
||||
"margin": 0,
|
||||
"spacing": 0,
|
||||
"tiles": [
|
||||
{
|
||||
"id": 9,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,960 @@
|
||||
{
|
||||
"compressionlevel": -1,
|
||||
"height": 14,
|
||||
"width": 20,
|
||||
"infinite": false,
|
||||
"layers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ground",
|
||||
"type": "tilelayer",
|
||||
"width": 20,
|
||||
"height": 14,
|
||||
"data": [
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "obstacles",
|
||||
"type": "tilelayer",
|
||||
"width": 20,
|
||||
"height": 14,
|
||||
"data": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
33,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "objects",
|
||||
"type": "objectgroup",
|
||||
"objects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "spawn",
|
||||
"type": "spawn",
|
||||
"x": 160,
|
||||
"y": 176,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "extra2",
|
||||
"type": "npc",
|
||||
"x": 96,
|
||||
"y": 128,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "npcId",
|
||||
"type": "string",
|
||||
"value": "extra2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "to_plaza",
|
||||
"type": "door",
|
||||
"x": 160,
|
||||
"y": 208,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "PlazaScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "from_field"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "berry1",
|
||||
"type": "interact",
|
||||
"x": 48,
|
||||
"y": 48,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "berry"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "berry2",
|
||||
"type": "interact",
|
||||
"x": 80,
|
||||
"y": 64,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "berry"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "berry3",
|
||||
"type": "interact",
|
||||
"x": 112,
|
||||
"y": 48,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "berry"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "berry4",
|
||||
"type": "interact",
|
||||
"x": 192,
|
||||
"y": 80,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "berry"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "berry5",
|
||||
"type": "interact",
|
||||
"x": 224,
|
||||
"y": 48,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "berry"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "pot",
|
||||
"type": "interact",
|
||||
"x": 160,
|
||||
"y": 32,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "pot"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"nextlayerid": 4,
|
||||
"nextobjectid": 10,
|
||||
"orientation": "orthogonal",
|
||||
"renderorder": "right-down",
|
||||
"tiledversion": "1.12.2",
|
||||
"tileheight": 16,
|
||||
"tilewidth": 16,
|
||||
"type": "map",
|
||||
"version": "1.10",
|
||||
"tilesets": [
|
||||
{
|
||||
"firstgid": 1,
|
||||
"columns": 16,
|
||||
"image": "../tilesets/village.png",
|
||||
"imagewidth": 256,
|
||||
"imageheight": 128,
|
||||
"tilecount": 128,
|
||||
"tilewidth": 16,
|
||||
"tileheight": 16,
|
||||
"name": "village",
|
||||
"margin": 0,
|
||||
"spacing": 0,
|
||||
"tiles": [
|
||||
{
|
||||
"id": 9,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
{
|
||||
"compressionlevel": -1,
|
||||
"height": 8,
|
||||
"width": 20,
|
||||
"infinite": false,
|
||||
"layers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ground",
|
||||
"type": "tilelayer",
|
||||
"width": 20,
|
||||
"height": 8,
|
||||
"data": [
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "obstacles",
|
||||
"type": "tilelayer",
|
||||
"width": 20,
|
||||
"height": 8,
|
||||
"data": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26,
|
||||
26
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "objects",
|
||||
"type": "objectgroup",
|
||||
"objects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "spawn",
|
||||
"type": "spawn",
|
||||
"x": 48,
|
||||
"y": 64,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "to_plaza",
|
||||
"type": "door",
|
||||
"x": 0,
|
||||
"y": 64,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "PlazaScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "from_fog"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "fogwall",
|
||||
"type": "interact",
|
||||
"x": 192,
|
||||
"y": 64,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "fogwall"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"nextlayerid": 4,
|
||||
"nextobjectid": 4,
|
||||
"orientation": "orthogonal",
|
||||
"renderorder": "right-down",
|
||||
"tiledversion": "1.12.2",
|
||||
"tileheight": 16,
|
||||
"tilewidth": 16,
|
||||
"type": "map",
|
||||
"version": "1.10",
|
||||
"tilesets": [
|
||||
{
|
||||
"firstgid": 1,
|
||||
"columns": 16,
|
||||
"image": "../tilesets/village.png",
|
||||
"imagewidth": 256,
|
||||
"imageheight": 128,
|
||||
"tilecount": 128,
|
||||
"tilewidth": 16,
|
||||
"tileheight": 16,
|
||||
"name": "village",
|
||||
"margin": 0,
|
||||
"spacing": 0,
|
||||
"tiles": [
|
||||
{
|
||||
"id": 9,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
{
|
||||
"compressionlevel": -1,
|
||||
"height": 8,
|
||||
"width": 12,
|
||||
"infinite": false,
|
||||
"layers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ground",
|
||||
"type": "tilelayer",
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"data": [
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
18,
|
||||
18,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17,
|
||||
17
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "obstacles",
|
||||
"type": "tilelayer",
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"data": [
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
12,
|
||||
12,
|
||||
0,
|
||||
19,
|
||||
19,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
29,
|
||||
0,
|
||||
12,
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
12,
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
23,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
12,
|
||||
12,
|
||||
24,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
12,
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "objects",
|
||||
"type": "objectgroup",
|
||||
"objects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "spawn",
|
||||
"type": "spawn",
|
||||
"x": 96,
|
||||
"y": 80,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "to_plaza",
|
||||
"type": "door",
|
||||
"x": 80,
|
||||
"y": 112,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "PlazaScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "from_house"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "to_plaza2",
|
||||
"type": "door",
|
||||
"x": 96,
|
||||
"y": 112,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "PlazaScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "from_house"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "bed",
|
||||
"type": "interact",
|
||||
"x": 32,
|
||||
"y": 32,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "bed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "calendar",
|
||||
"type": "interact",
|
||||
"x": 144,
|
||||
"y": 32,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "calendar"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"nextlayerid": 4,
|
||||
"nextobjectid": 6,
|
||||
"orientation": "orthogonal",
|
||||
"renderorder": "right-down",
|
||||
"tiledversion": "1.12.2",
|
||||
"tileheight": 16,
|
||||
"tilewidth": 16,
|
||||
"type": "map",
|
||||
"version": "1.10",
|
||||
"tilesets": [
|
||||
{
|
||||
"firstgid": 1,
|
||||
"columns": 16,
|
||||
"image": "../tilesets/village.png",
|
||||
"imagewidth": 256,
|
||||
"imageheight": 128,
|
||||
"tilecount": 128,
|
||||
"tilewidth": 16,
|
||||
"tileheight": 16,
|
||||
"name": "village",
|
||||
"margin": 0,
|
||||
"spacing": 0,
|
||||
"tiles": [
|
||||
{
|
||||
"id": 9,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":8,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 18, 18, 17, 17, 17, 17, 17],
|
||||
"height":8,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":12,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
|
||||
12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12,
|
||||
12, 0, 19, 19, 0, 0, 0, 0, 0, 29, 0, 12,
|
||||
12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12,
|
||||
12, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 12,
|
||||
12, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12,
|
||||
12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12,
|
||||
12, 12, 12, 12, 12, 1, 1, 12, 12, 12, 12, 12],
|
||||
"height":8,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":12,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":96,
|
||||
"y":80
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"to_plaza",
|
||||
"opacity":1,
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":80,
|
||||
"y":112
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"to_plaza2",
|
||||
"opacity":1,
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":96,
|
||||
"y":112
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"bed",
|
||||
"opacity":1,
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":32,
|
||||
"y":32
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":5,
|
||||
"name":"calendar",
|
||||
"opacity":1,
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":144,
|
||||
"y":32
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":6,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"firstgid":1,
|
||||
"source":"..\/..\/..\/mapsrc\/village.tsx"
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":12
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
{
|
||||
"compressionlevel": -1,
|
||||
"height": 10,
|
||||
"width": 15,
|
||||
"infinite": false,
|
||||
"layers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ground",
|
||||
"type": "tilelayer",
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"data": [
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
18
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "obstacles",
|
||||
"type": "tilelayer",
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"data": [
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "objects",
|
||||
"type": "objectgroup",
|
||||
"objects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "spawn",
|
||||
"type": "spawn",
|
||||
"x": 112,
|
||||
"y": 112,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "mimi",
|
||||
"type": "npc",
|
||||
"x": 112,
|
||||
"y": 80,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "npcId",
|
||||
"type": "string",
|
||||
"value": "mimi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "to_plaza",
|
||||
"type": "door",
|
||||
"x": 112,
|
||||
"y": 144,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "PlazaScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "from_shop"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "shelf",
|
||||
"type": "interact",
|
||||
"x": 80,
|
||||
"y": 32,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "shelf"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"nextlayerid": 4,
|
||||
"nextobjectid": 5,
|
||||
"orientation": "orthogonal",
|
||||
"renderorder": "right-down",
|
||||
"tiledversion": "1.12.2",
|
||||
"tileheight": 16,
|
||||
"tilewidth": 16,
|
||||
"type": "map",
|
||||
"version": "1.10",
|
||||
"tilesets": [
|
||||
{
|
||||
"firstgid": 1,
|
||||
"columns": 16,
|
||||
"image": "../tilesets/village.png",
|
||||
"imagewidth": 256,
|
||||
"imageheight": 128,
|
||||
"tilecount": 128,
|
||||
"tilewidth": 16,
|
||||
"tileheight": 16,
|
||||
"name": "village",
|
||||
"margin": 0,
|
||||
"spacing": 0,
|
||||
"tiles": [
|
||||
{
|
||||
"id": 9,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
{
|
||||
"compressionlevel": -1,
|
||||
"height": 10,
|
||||
"width": 14,
|
||||
"infinite": false,
|
||||
"layers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ground",
|
||||
"type": "tilelayer",
|
||||
"width": 14,
|
||||
"height": 10,
|
||||
"data": [
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "obstacles",
|
||||
"type": "tilelayer",
|
||||
"width": 14,
|
||||
"height": 10,
|
||||
"data": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
25,
|
||||
25,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
25,
|
||||
25,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "objects",
|
||||
"type": "objectgroup",
|
||||
"objects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "spawn",
|
||||
"type": "spawn",
|
||||
"x": 32,
|
||||
"y": 80,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "extra5",
|
||||
"type": "npc",
|
||||
"x": 160,
|
||||
"y": 112,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "npcId",
|
||||
"type": "string",
|
||||
"value": "extra5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "to_plaza",
|
||||
"type": "door",
|
||||
"x": 0,
|
||||
"y": 80,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "PlazaScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "from_well"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "well",
|
||||
"type": "interact",
|
||||
"x": 112,
|
||||
"y": 64,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "well"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"nextlayerid": 4,
|
||||
"nextobjectid": 5,
|
||||
"orientation": "orthogonal",
|
||||
"renderorder": "right-down",
|
||||
"tiledversion": "1.12.2",
|
||||
"tileheight": 16,
|
||||
"tilewidth": 16,
|
||||
"type": "map",
|
||||
"version": "1.10",
|
||||
"tilesets": [
|
||||
{
|
||||
"firstgid": 1,
|
||||
"columns": 16,
|
||||
"image": "../tilesets/village.png",
|
||||
"imagewidth": 256,
|
||||
"imageheight": 128,
|
||||
"tilecount": 128,
|
||||
"tilewidth": 16,
|
||||
"tileheight": 16,
|
||||
"name": "village",
|
||||
"margin": 0,
|
||||
"spacing": 0,
|
||||
"tiles": [
|
||||
{
|
||||
"id": 9,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
{
|
||||
"compressionlevel": -1,
|
||||
"height": 14,
|
||||
"width": 20,
|
||||
"infinite": false,
|
||||
"layers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ground",
|
||||
"type": "tilelayer",
|
||||
"width": 20,
|
||||
"height": 14,
|
||||
"data": [
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28,
|
||||
28
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "obstacles",
|
||||
"type": "tilelayer",
|
||||
"width": 20,
|
||||
"height": 14,
|
||||
"data": [
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
0,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
44,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
27,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "objects",
|
||||
"type": "objectgroup",
|
||||
"objects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "spawn",
|
||||
"type": "spawn",
|
||||
"x": 32,
|
||||
"y": 48,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "record",
|
||||
"type": "interact",
|
||||
"x": 176,
|
||||
"y": 128,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "interactId",
|
||||
"type": "string",
|
||||
"value": "record"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "to_ward",
|
||||
"type": "door",
|
||||
"x": 160,
|
||||
"y": 80,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"properties": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "string",
|
||||
"value": "WhiteScene"
|
||||
},
|
||||
{
|
||||
"name": "spawn",
|
||||
"type": "string",
|
||||
"value": "ward"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 200,
|
||||
"name": "ward",
|
||||
"type": "spawn",
|
||||
"x": 160,
|
||||
"y": 128,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
}
|
||||
],
|
||||
"opacity": 1,
|
||||
"visible": true,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"nextlayerid": 4,
|
||||
"nextobjectid": 5,
|
||||
"orientation": "orthogonal",
|
||||
"renderorder": "right-down",
|
||||
"tiledversion": "1.12.2",
|
||||
"tileheight": 16,
|
||||
"tilewidth": 16,
|
||||
"type": "map",
|
||||
"version": "1.10",
|
||||
"tilesets": [
|
||||
{
|
||||
"firstgid": 1,
|
||||
"columns": 16,
|
||||
"image": "../tilesets/village.png",
|
||||
"imagewidth": 256,
|
||||
"imageheight": 128,
|
||||
"tilecount": 128,
|
||||
"tilewidth": 16,
|
||||
"tileheight": 16,
|
||||
"name": "village",
|
||||
"margin": 0,
|
||||
"spacing": 0,
|
||||
"tiles": [
|
||||
{
|
||||
"id": 9,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"properties": [
|
||||
{
|
||||
"name": "collides",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 172 B |
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 547 B |
|
After Width: | Height: | Size: 280 B |
|
After Width: | Height: | Size: 271 B |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
export PATH="/Applications/Aseprite.app/Contents/MacOS:/Applications/Tiled.app/Contents/MacOS:${PATH}"
|
||||
cd "$ROOT"
|
||||
mkdir -p public/assets/{tilesets,sprites,maps} mapsrc
|
||||
aseprite -b -script scripts/generate_tileset.lua
|
||||
aseprite -b -script scripts/generate_sprites.lua
|
||||
echo "Aseprite assets done."
|
||||
@@ -0,0 +1,647 @@
|
||||
/**
|
||||
* Generate 10 Tiled-compatible JSON maps per PRD §10.
|
||||
* Also writes .tmx for optional `tiled --export-map` round-trip.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const outDir = path.join(__dirname, '../public/assets/maps');
|
||||
const tmxDir = path.join(__dirname, '../mapsrc');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.mkdirSync(tmxDir, { recursive: true });
|
||||
|
||||
const TILE = 16;
|
||||
|
||||
const TILESET = {
|
||||
firstgid: 1,
|
||||
columns: 16,
|
||||
image: '../tilesets/village.png',
|
||||
imagewidth: 256,
|
||||
imageheight: 128,
|
||||
tilecount: 128,
|
||||
tilewidth: 16,
|
||||
tileheight: 16,
|
||||
name: 'village',
|
||||
margin: 0,
|
||||
spacing: 0,
|
||||
tiles: [
|
||||
// collidable tiles
|
||||
...[9, 10, 11, 12, 13, 14, 15, 19, 20, 21, 22, 24, 26, 29, 30, 35, 41, 46].map((id) => ({
|
||||
id,
|
||||
properties: [{ name: 'collides', type: 'bool', value: true }],
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
function emptyGrid(w, h, fill = 0) {
|
||||
return Array.from({ length: w * h }, () => fill);
|
||||
}
|
||||
|
||||
function set(data, w, x, y, v) {
|
||||
if (x < 0 || y < 0 || x >= w) return;
|
||||
data[y * w + x] = v;
|
||||
}
|
||||
|
||||
function fillRect(data, w, x0, y0, x1, y1, v) {
|
||||
for (let y = y0; y <= y1; y++) {
|
||||
for (let x = x0; x <= x1; x++) set(data, w, x, y, v);
|
||||
}
|
||||
}
|
||||
|
||||
function border(data, w, h, v) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
set(data, w, x, 0, v);
|
||||
set(data, w, x, h - 1, v);
|
||||
}
|
||||
for (let y = 0; y < h; y++) {
|
||||
set(data, w, 0, y, v);
|
||||
set(data, w, w - 1, y, v);
|
||||
}
|
||||
}
|
||||
|
||||
function makeMap(name, w, h, build) {
|
||||
const ground = emptyGrid(w, h, 1); // grass gid 1 (tile index 0 is empty, firstgid=1 → tile 0 empty, tile1 grass)
|
||||
// Our tileset: tile index in image 0 = empty transparent; index 1 = grass
|
||||
// firstgid 1 means GID 1 = tile index 0. Let's use firstgid 1 where GID N = image tile (N-1)
|
||||
// Simpler: firstgid=1, and we store image-tile-index+1 as GID.
|
||||
// grass is image tile 1 → GID 2
|
||||
for (let i = 0; i < ground.length; i++) ground[i] = 2; // grass
|
||||
|
||||
const obstacles = emptyGrid(w, h, 0);
|
||||
const objects = [];
|
||||
|
||||
const api = {
|
||||
w,
|
||||
h,
|
||||
ground,
|
||||
obstacles,
|
||||
objects,
|
||||
// GID 0 = empty; image tile index N → GID N+1. Passing 0 clears (no collision).
|
||||
setG: (x, y, imgTile) => set(ground, w, x, y, imgTile <= 0 ? 0 : imgTile + 1),
|
||||
setO: (x, y, imgTile) => set(obstacles, w, x, y, imgTile <= 0 ? 0 : imgTile + 1),
|
||||
clearO: (x, y) => set(obstacles, w, x, y, 0),
|
||||
fillG: (x0, y0, x1, y1, imgTile) =>
|
||||
fillRect(ground, w, x0, y0, x1, y1, imgTile <= 0 ? 0 : imgTile + 1),
|
||||
fillO: (x0, y0, x1, y1, imgTile) =>
|
||||
fillRect(obstacles, w, x0, y0, x1, y1, imgTile <= 0 ? 0 : imgTile + 1),
|
||||
borderO: (imgTile) => {
|
||||
const gid = imgTile <= 0 ? 0 : imgTile + 1;
|
||||
for (let x = 0; x < w; x++) {
|
||||
set(obstacles, w, x, 0, gid);
|
||||
set(obstacles, w, x, h - 1, gid);
|
||||
}
|
||||
for (let y = 0; y < h; y++) {
|
||||
set(obstacles, w, 0, y, gid);
|
||||
set(obstacles, w, w - 1, y, gid);
|
||||
}
|
||||
},
|
||||
spawn: (x, y) => {
|
||||
objects.push({
|
||||
id: objects.length + 1,
|
||||
name: 'spawn',
|
||||
type: 'spawn',
|
||||
x: x * TILE,
|
||||
y: y * TILE,
|
||||
width: TILE,
|
||||
height: TILE,
|
||||
});
|
||||
},
|
||||
npc: (name, x, y) => {
|
||||
objects.push({
|
||||
id: objects.length + 1,
|
||||
name,
|
||||
type: 'npc',
|
||||
x: x * TILE,
|
||||
y: y * TILE,
|
||||
width: TILE,
|
||||
height: TILE,
|
||||
properties: [{ name: 'npcId', type: 'string', value: name }],
|
||||
});
|
||||
},
|
||||
door: (name, x, y, target, targetSpawn) => {
|
||||
// Door tiles must be walkable (clear obstacle collision)
|
||||
set(obstacles, w, x, y, 0);
|
||||
objects.push({
|
||||
id: objects.length + 1,
|
||||
name,
|
||||
type: 'door',
|
||||
x: x * TILE,
|
||||
y: y * TILE,
|
||||
width: TILE,
|
||||
height: TILE,
|
||||
properties: [
|
||||
{ name: 'target', type: 'string', value: target },
|
||||
{ name: 'spawn', type: 'string', value: targetSpawn || 'spawn' },
|
||||
],
|
||||
});
|
||||
},
|
||||
interact: (name, x, y, interactId) => {
|
||||
objects.push({
|
||||
id: objects.length + 1,
|
||||
name,
|
||||
type: 'interact',
|
||||
x: x * TILE,
|
||||
y: y * TILE,
|
||||
width: TILE,
|
||||
height: TILE,
|
||||
properties: [{ name: 'interactId', type: 'string', value: interactId }],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
build(api);
|
||||
|
||||
const map = {
|
||||
compressionlevel: -1,
|
||||
height: h,
|
||||
width: w,
|
||||
infinite: false,
|
||||
layers: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'ground',
|
||||
type: 'tilelayer',
|
||||
width: w,
|
||||
height: h,
|
||||
data: ground,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'obstacles',
|
||||
type: 'tilelayer',
|
||||
width: w,
|
||||
height: h,
|
||||
data: obstacles,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
properties: [{ name: 'collides', type: 'bool', value: true }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'objects',
|
||||
type: 'objectgroup',
|
||||
objects,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
],
|
||||
nextlayerid: 4,
|
||||
nextobjectid: objects.length + 1,
|
||||
orientation: 'orthogonal',
|
||||
renderorder: 'right-down',
|
||||
tiledversion: '1.12.2',
|
||||
tileheight: TILE,
|
||||
tilewidth: TILE,
|
||||
type: 'map',
|
||||
version: '1.10',
|
||||
tilesets: [
|
||||
{
|
||||
...TILESET,
|
||||
// embed relative path for Phaser
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const jsonPath = path.join(outDir, `${name}.json`);
|
||||
fs.writeFileSync(jsonPath, JSON.stringify(map, null, 2));
|
||||
console.log('wrote', jsonPath);
|
||||
|
||||
// Minimal TMX for Tiled CLI export verification
|
||||
const tmx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${w}" height="${h}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="${objects.length + 1}">
|
||||
<tileset firstgid="1" source="village.tsx"/>
|
||||
<layer id="1" name="ground" width="${w}" height="${h}">
|
||||
<data encoding="csv">
|
||||
${ground.join(',')}
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="${w}" height="${h}">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
${obstacles.join(',')}
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
${objects
|
||||
.map(
|
||||
(o) =>
|
||||
` <object id="${o.id}" name="${o.name}" type="${o.type}" x="${o.x}" y="${o.y}" width="${o.width}" height="${o.height}"/>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</objectgroup>
|
||||
</map>
|
||||
`;
|
||||
fs.writeFileSync(path.join(tmxDir, `${name}.tmx`), tmx);
|
||||
return map;
|
||||
}
|
||||
|
||||
// TSX tileset for Tiled
|
||||
fs.writeFileSync(
|
||||
path.join(tmxDir, 'village.tsx'),
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.2" name="village" tilewidth="16" tileheight="16" tilecount="128" columns="16">
|
||||
<image source="../public/assets/tilesets/village.png" width="256" height="128"/>
|
||||
</tileset>
|
||||
`,
|
||||
);
|
||||
|
||||
// --- Maps ---
|
||||
|
||||
// 1. House 12x8
|
||||
makeMap('house', 12, 8, (m) => {
|
||||
m.fillG(0, 0, 11, 7, 16); // wood floor
|
||||
m.borderO(11);
|
||||
m.setO(2, 2, 18); // bed
|
||||
m.setO(3, 2, 18);
|
||||
m.setO(9, 2, 28); // calendar
|
||||
m.setO(5, 4, 22); // table
|
||||
m.setO(1, 5, 23); // chair
|
||||
// clear door at bottom
|
||||
m.setO(5, 7, 0);
|
||||
m.setO(6, 7, 0);
|
||||
m.setG(5, 7, 17);
|
||||
m.setG(6, 7, 17);
|
||||
m.spawn(6, 5);
|
||||
m.door('to_plaza', 5, 7, 'PlazaScene', 'from_house');
|
||||
m.door('to_plaza2', 6, 7, 'PlazaScene', 'from_house');
|
||||
m.interact('bed', 2, 2, 'bed');
|
||||
m.interact('calendar', 9, 2, 'calendar');
|
||||
});
|
||||
|
||||
// 2. Plaza 30x20 hub
|
||||
makeMap('plaza', 30, 20, (m) => {
|
||||
m.fillG(0, 0, 29, 19, 1);
|
||||
// path cross
|
||||
m.fillG(13, 0, 16, 19, 3);
|
||||
m.fillG(0, 8, 29, 11, 3);
|
||||
// flower beds
|
||||
m.fillO(10, 6, 12, 7, 12 + 0); // use flower tile 7
|
||||
m.setO(10, 6, 7);
|
||||
m.setO(11, 6, 7);
|
||||
m.setO(12, 6, 7);
|
||||
m.setO(10, 7, 7);
|
||||
m.setO(11, 7, 7);
|
||||
m.setO(12, 7, 7);
|
||||
// board
|
||||
m.setO(15, 5, 29);
|
||||
// tree + birds
|
||||
m.setO(20, 4, 10);
|
||||
m.setO(20, 5, 9);
|
||||
m.setO(21, 4, 10);
|
||||
// mailbox
|
||||
m.setO(4, 10, 30);
|
||||
// houses facades as doors targets
|
||||
m.setO(5, 2, 13);
|
||||
m.setO(6, 2, 13);
|
||||
m.setO(5, 3, 12);
|
||||
m.setO(6, 3, 14); // door visual
|
||||
m.setO(22, 2, 13);
|
||||
m.setO(23, 2, 13);
|
||||
m.setO(22, 3, 12);
|
||||
m.setO(23, 3, 14);
|
||||
m.setO(8, 14, 13);
|
||||
m.setO(9, 14, 13);
|
||||
m.setO(8, 15, 12);
|
||||
m.setO(9, 15, 14);
|
||||
// walls edge soft
|
||||
for (let x = 0; x < 30; x++) {
|
||||
m.setO(x, 0, 35);
|
||||
m.setO(x, 19, 35);
|
||||
}
|
||||
// openings
|
||||
m.setO(14, 0, 0);
|
||||
m.setO(15, 0, 0);
|
||||
m.setO(14, 19, 0);
|
||||
m.setO(15, 19, 0);
|
||||
m.setO(0, 9, 0);
|
||||
m.setO(0, 10, 0);
|
||||
m.setO(29, 9, 0);
|
||||
m.setO(29, 10, 0);
|
||||
|
||||
m.spawn(15, 12);
|
||||
m.npc('afu', 15, 6);
|
||||
m.npc('dabao', 5, 11);
|
||||
m.npc('twinbirds', 20, 3);
|
||||
m.npc('extra1', 18, 14);
|
||||
m.npc('extra4', 10, 12);
|
||||
|
||||
m.door('from_house', 6, 4, 'HouseScene', 'spawn'); // visual marker only when entering - actual door below
|
||||
// Fix: doors as interact targets
|
||||
// Re-define doors properly
|
||||
m.objects.length = m.objects.filter((o) => o.type !== 'door').length; // no, just add
|
||||
});
|
||||
|
||||
// Fix plaza - rebuild cleanly
|
||||
function rebuildPlaza() {
|
||||
return makeMap('plaza', 30, 20, (m) => {
|
||||
m.fillG(0, 0, 29, 19, 1);
|
||||
m.fillG(13, 0, 16, 19, 3);
|
||||
m.fillG(0, 8, 29, 11, 3);
|
||||
// flower bed tiles
|
||||
for (const [x, y] of [
|
||||
[10, 6],
|
||||
[11, 6],
|
||||
[12, 6],
|
||||
[10, 7],
|
||||
[11, 7],
|
||||
[12, 7],
|
||||
]) {
|
||||
m.setO(x, y, 7);
|
||||
}
|
||||
m.setO(15, 5, 29); // board
|
||||
m.setO(20, 4, 10);
|
||||
m.setO(20, 5, 9);
|
||||
m.setO(4, 10, 30); // mailbox
|
||||
// house doors on north
|
||||
m.setO(5, 2, 13);
|
||||
m.setO(6, 2, 13);
|
||||
m.setO(5, 3, 12);
|
||||
m.setO(6, 3, 14);
|
||||
m.setO(22, 2, 13);
|
||||
m.setO(23, 2, 13);
|
||||
m.setO(22, 3, 12);
|
||||
m.setO(23, 3, 14);
|
||||
// clinic south-west
|
||||
m.setO(3, 15, 12);
|
||||
m.setO(4, 15, 14);
|
||||
// bakery east-ish
|
||||
m.setO(25, 8, 12);
|
||||
m.setO(25, 9, 14);
|
||||
// shop west
|
||||
m.setO(2, 8, 12);
|
||||
m.setO(2, 9, 14);
|
||||
// ribbon posts
|
||||
m.setO(12, 10, 33);
|
||||
m.setO(17, 10, 33);
|
||||
m.setO(12, 13, 33);
|
||||
m.setO(17, 13, 33);
|
||||
|
||||
m.spawn(15, 12);
|
||||
// named entry spawns as objects
|
||||
m.objects.push({
|
||||
id: 100,
|
||||
name: 'from_house',
|
||||
type: 'spawn',
|
||||
x: 6 * 16,
|
||||
y: 5 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
m.objects.push({
|
||||
id: 101,
|
||||
name: 'from_bakery',
|
||||
type: 'spawn',
|
||||
x: 24 * 16,
|
||||
y: 9 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
m.objects.push({
|
||||
id: 102,
|
||||
name: 'from_shop',
|
||||
type: 'spawn',
|
||||
x: 3 * 16,
|
||||
y: 9 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
m.objects.push({
|
||||
id: 103,
|
||||
name: 'from_clinic',
|
||||
type: 'spawn',
|
||||
x: 4 * 16,
|
||||
y: 14 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
m.objects.push({
|
||||
id: 104,
|
||||
name: 'from_lake',
|
||||
type: 'spawn',
|
||||
x: 15 * 16,
|
||||
y: 17 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
m.objects.push({
|
||||
id: 105,
|
||||
name: 'from_field',
|
||||
type: 'spawn',
|
||||
x: 15 * 16,
|
||||
y: 2 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
m.objects.push({
|
||||
id: 106,
|
||||
name: 'from_well',
|
||||
type: 'spawn',
|
||||
x: 27 * 16,
|
||||
y: 10 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
m.objects.push({
|
||||
id: 107,
|
||||
name: 'from_fog',
|
||||
type: 'spawn',
|
||||
x: 2 * 16,
|
||||
y: 16 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
|
||||
m.npc('afu', 15, 6);
|
||||
m.npc('dabao', 5, 11);
|
||||
m.npc('twinbirds', 20, 3);
|
||||
m.npc('extra1', 18, 14);
|
||||
m.npc('extra4', 10, 12);
|
||||
|
||||
m.door('house', 6, 3, 'HouseScene', 'spawn');
|
||||
m.door('bakery', 25, 9, 'BakeryScene', 'spawn');
|
||||
m.door('shop', 2, 9, 'ShopScene', 'spawn');
|
||||
m.door('clinic', 4, 15, 'ClinicScene', 'spawn');
|
||||
m.door('lake', 15, 19, 'LakeScene', 'spawn');
|
||||
m.door('field', 15, 0, 'FieldScene', 'spawn');
|
||||
m.door('well', 29, 10, 'WellScene', 'spawn');
|
||||
m.door('fog', 0, 16, 'FogScene', 'spawn');
|
||||
|
||||
m.interact('board', 15, 5, 'board');
|
||||
m.interact('flowers', 11, 6, 'flowers');
|
||||
m.interact('mailbox', 4, 10, 'mailbox');
|
||||
m.interact('ribbon1', 12, 10, 'ribbon');
|
||||
m.interact('ribbon2', 17, 10, 'ribbon');
|
||||
m.interact('ribbon3', 12, 13, 'ribbon');
|
||||
m.interact('ribbon4', 17, 13, 'ribbon');
|
||||
});
|
||||
}
|
||||
rebuildPlaza();
|
||||
|
||||
// 3. Bakery 15x10
|
||||
makeMap('bakery', 15, 10, (m) => {
|
||||
m.fillG(0, 0, 14, 9, 16);
|
||||
m.borderO(12);
|
||||
m.fillO(2, 2, 6, 3, 19); // counter
|
||||
m.setO(12, 2, 20); // oven
|
||||
m.setO(10, 2, 42); // recipe paper
|
||||
m.setO(4, 6, 22);
|
||||
m.setO(5, 7, 0);
|
||||
m.setO(6, 9, 0);
|
||||
m.setO(7, 9, 0);
|
||||
m.spawn(7, 7);
|
||||
m.npc('yuanyuan', 4, 4);
|
||||
m.door('to_plaza', 7, 9, 'PlazaScene', 'from_bakery');
|
||||
m.interact('recipe', 10, 2, 'recipe');
|
||||
m.interact('counter', 4, 3, 'bread');
|
||||
m.interact('knead', 3, 3, 'knead');
|
||||
});
|
||||
|
||||
// 4. Shop 15x10
|
||||
makeMap('shop', 15, 10, (m) => {
|
||||
m.fillG(0, 0, 14, 9, 17);
|
||||
m.borderO(12);
|
||||
for (let x = 2; x <= 12; x++) {
|
||||
m.setO(x, 2, 21);
|
||||
m.setO(x, 4, 21);
|
||||
}
|
||||
m.setO(7, 9, 0);
|
||||
m.setO(8, 9, 0);
|
||||
m.spawn(7, 7);
|
||||
m.npc('mimi', 7, 5);
|
||||
m.door('to_plaza', 7, 9, 'PlazaScene', 'from_shop');
|
||||
m.interact('shelf', 5, 2, 'shelf');
|
||||
});
|
||||
|
||||
// 5. Clinic 12x8
|
||||
makeMap('clinic', 12, 8, (m) => {
|
||||
m.fillG(0, 0, 11, 7, 17);
|
||||
m.borderO(12);
|
||||
m.setO(3, 2, 22);
|
||||
m.setO(4, 2, 43); // form
|
||||
m.setO(8, 3, 18);
|
||||
m.setO(5, 7, 0);
|
||||
m.setO(6, 7, 0);
|
||||
m.spawn(6, 5);
|
||||
m.npc('fu', 3, 3);
|
||||
m.door('to_plaza', 6, 7, 'PlazaScene', 'from_clinic');
|
||||
m.interact('form', 4, 2, 'form');
|
||||
});
|
||||
|
||||
// 6. Lake 24x16
|
||||
makeMap('lake', 24, 16, (m) => {
|
||||
m.fillG(0, 0, 23, 15, 1);
|
||||
m.fillG(0, 8, 23, 15, 5); // water
|
||||
m.fillG(0, 8, 23, 8, 6);
|
||||
m.fillO(8, 4, 10, 5, 23); // bench area chairs
|
||||
m.setO(9, 4, 23);
|
||||
m.setO(12, 6, 31); // picnic
|
||||
m.setO(13, 6, 31);
|
||||
// path from north
|
||||
m.fillG(11, 0, 12, 7, 3);
|
||||
m.setO(11, 0, 0);
|
||||
m.setO(12, 0, 0);
|
||||
m.spawn(11, 2);
|
||||
m.npc('gui', 9, 5);
|
||||
m.npc('extra3', 18, 4);
|
||||
m.door('to_plaza', 11, 0, 'PlazaScene', 'from_lake');
|
||||
m.interact('picnic', 12, 6, 'picnic');
|
||||
m.interact('bench', 9, 4, 'bench');
|
||||
});
|
||||
|
||||
// 7. Field 20x14
|
||||
makeMap('field', 20, 14, (m) => {
|
||||
m.fillG(0, 0, 19, 13, 1);
|
||||
m.fillG(9, 10, 10, 13, 3);
|
||||
for (const [x, y] of [
|
||||
[3, 3],
|
||||
[5, 4],
|
||||
[7, 3],
|
||||
[12, 5],
|
||||
[14, 3],
|
||||
[4, 7],
|
||||
[15, 7],
|
||||
[8, 6],
|
||||
]) {
|
||||
m.setO(x, y, 8); // berry
|
||||
}
|
||||
m.setO(10, 2, 32); // pot
|
||||
m.spawn(10, 11);
|
||||
m.npc('extra2', 6, 8);
|
||||
m.door('to_plaza', 10, 13, 'PlazaScene', 'from_field');
|
||||
m.interact('berry1', 3, 3, 'berry');
|
||||
m.interact('berry2', 5, 4, 'berry');
|
||||
m.interact('berry3', 7, 3, 'berry');
|
||||
m.interact('berry4', 12, 5, 'berry');
|
||||
m.interact('berry5', 14, 3, 'berry');
|
||||
m.interact('pot', 10, 2, 'pot');
|
||||
});
|
||||
|
||||
// 8. Well 14x10
|
||||
makeMap('well', 14, 10, (m) => {
|
||||
m.fillG(0, 0, 13, 9, 1);
|
||||
m.fillG(0, 4, 3, 5, 3);
|
||||
m.setO(7, 4, 24); // well
|
||||
m.setO(6, 4, 24);
|
||||
m.setO(7, 5, 24);
|
||||
m.setO(6, 5, 24);
|
||||
m.spawn(2, 5);
|
||||
m.npc('extra5', 10, 7);
|
||||
m.door('to_plaza', 0, 5, 'PlazaScene', 'from_well');
|
||||
m.interact('well', 7, 4, 'well');
|
||||
});
|
||||
|
||||
// 9. Fog 20x8
|
||||
makeMap('fog', 20, 8, (m) => {
|
||||
m.fillG(0, 0, 19, 7, 1);
|
||||
m.fillG(0, 3, 5, 4, 3);
|
||||
for (let x = 12; x < 20; x++) {
|
||||
for (let y = 0; y < 8; y++) m.setO(x, y, 25);
|
||||
}
|
||||
m.spawn(3, 4);
|
||||
m.door('to_plaza', 0, 4, 'PlazaScene', 'from_fog');
|
||||
m.interact('fogwall', 12, 4, 'fogwall');
|
||||
});
|
||||
|
||||
// 10. White layer corridor 20x6 + ward via objects
|
||||
makeMap('white', 20, 14, (m) => {
|
||||
m.fillG(0, 0, 19, 13, 27);
|
||||
m.fillO(0, 0, 19, 0, 26);
|
||||
m.fillO(0, 5, 19, 5, 26);
|
||||
m.fillO(0, 0, 0, 5, 26);
|
||||
m.fillO(19, 0, 19, 5, 26);
|
||||
// ward below
|
||||
m.fillG(5, 6, 14, 13, 27);
|
||||
m.fillO(5, 6, 14, 6, 26);
|
||||
m.fillO(5, 13, 14, 13, 26);
|
||||
m.fillO(5, 6, 5, 13, 26);
|
||||
m.fillO(14, 6, 14, 13, 26);
|
||||
m.setO(10, 5, 0); // opening
|
||||
m.setO(9, 9, 18); // bed
|
||||
m.setO(11, 8, 43);
|
||||
m.spawn(2, 3);
|
||||
m.interact('record', 11, 8, 'record');
|
||||
m.door('to_ward', 10, 5, 'WhiteScene', 'ward');
|
||||
m.objects.push({
|
||||
id: 200,
|
||||
name: 'ward',
|
||||
type: 'spawn',
|
||||
x: 10 * 16,
|
||||
y: 8 * 16,
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
});
|
||||
|
||||
console.log('All 10 maps generated.');
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
export PATH="/Applications/Tiled.app/Contents/MacOS:${PATH}"
|
||||
cd "$ROOT"
|
||||
node scripts/generate-maps.mjs
|
||||
# Round-trip one map through Tiled CLI to prove toolchain
|
||||
if command -v tiled >/dev/null 2>&1; then
|
||||
tiled --export-map json mapsrc/plaza.tmx public/assets/maps/plaza_tiled_export.json || true
|
||||
fi
|
||||
echo "Maps done."
|
||||
@@ -0,0 +1,215 @@
|
||||
-- Aseprite Lua: player + NPCs + portraits + extras
|
||||
-- Run: aseprite -b -script scripts/generate_sprites.lua
|
||||
|
||||
local function newSprite(w, h)
|
||||
local s = Sprite(w, h, ColorMode.RGB)
|
||||
app.activeSprite = s
|
||||
return s
|
||||
end
|
||||
|
||||
local function clear(img, w, h)
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
img:drawPixel(x, y, app.pixelColor.rgba(0, 0, 0, 0))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function P(img, x, y, r, g, b, a)
|
||||
a = a or 255
|
||||
if x < 0 or y < 0 then return end
|
||||
img:drawPixel(x, y, app.pixelColor.rgba(r, g, b, a))
|
||||
end
|
||||
|
||||
local function fillRect(img, x0, y0, x1, y1, r, g, b)
|
||||
for y = y0, y1 do
|
||||
for x = x0, x1 do
|
||||
P(img, x, y, r, g, b)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw a 16x24 character into img at offset (ox, oy)
|
||||
-- face: 0 smile, 1 flat, 2 off (eye lower 1px)
|
||||
local function drawChar(img, ox, oy, bodyR, bodyG, bodyB, headR, headG, headB, face, walkFrame, dir)
|
||||
-- shadow
|
||||
fillRect(img, ox + 3, oy + 22, ox + 12, oy + 23, 0, 0, 0)
|
||||
for x = ox + 3, ox + 12 do
|
||||
P(img, x, oy + 22, 0, 0, 0, 50)
|
||||
P(img, x, oy + 23, 0, 0, 0, 30)
|
||||
end
|
||||
-- legs
|
||||
local legOff = 0
|
||||
if walkFrame == 1 then legOff = 1 end
|
||||
if walkFrame == 3 then legOff = -1 end
|
||||
fillRect(img, ox + 4, oy + 16, ox + 6, oy + 21, 60, 50, 80)
|
||||
fillRect(img, ox + 9, oy + 16, ox + 11, oy + 21, 60, 50, 80)
|
||||
if walkFrame == 1 then
|
||||
fillRect(img, ox + 4, oy + 16, ox + 6, oy + 21, 0, 0, 0)
|
||||
fillRect(img, ox + 3, oy + 16, ox + 5, oy + 21, 60, 50, 80)
|
||||
elseif walkFrame == 3 then
|
||||
fillRect(img, ox + 9, oy + 16, ox + 11, oy + 21, 0, 0, 0)
|
||||
fillRect(img, ox + 10, oy + 16, ox + 12, oy + 21, 60, 50, 80)
|
||||
end
|
||||
-- body
|
||||
fillRect(img, ox + 3, oy + 10, ox + 12, oy + 17, bodyR, bodyG, bodyB)
|
||||
-- head
|
||||
fillRect(img, ox + 3, oy + 2, ox + 12, oy + 10, headR, headG, headB)
|
||||
-- ears/species hint for animals - small top nubs
|
||||
if headR > 200 and headG > 180 then
|
||||
-- rabbit ears
|
||||
fillRect(img, ox + 4, oy + 0, ox + 5, oy + 3, headR, headG, headB)
|
||||
fillRect(img, ox + 10, oy + 0, ox + 11, oy + 3, headR, headG, headB)
|
||||
end
|
||||
-- face
|
||||
local eyeY = 5
|
||||
local eyeY2 = 5
|
||||
if face == 2 then eyeY2 = 6 end -- off: one eye 1px lower
|
||||
if dir == 0 then -- down
|
||||
P(img, ox + 5, oy + eyeY, 30, 30, 40)
|
||||
P(img, ox + 10, oy + eyeY2, 30, 30, 40)
|
||||
if face == 0 then
|
||||
P(img, ox + 6, oy + 8, 200, 80, 100)
|
||||
P(img, ox + 7, oy + 8, 200, 80, 100)
|
||||
P(img, ox + 8, oy + 8, 200, 80, 100)
|
||||
P(img, ox + 9, oy + 8, 200, 80, 100)
|
||||
elseif face == 1 then
|
||||
P(img, ox + 6, oy + 8, 80, 60, 70)
|
||||
P(img, ox + 7, oy + 8, 80, 60, 70)
|
||||
P(img, ox + 8, oy + 8, 80, 60, 70)
|
||||
P(img, ox + 9, oy + 8, 80, 60, 70)
|
||||
else
|
||||
P(img, ox + 6, oy + 8, 200, 80, 100)
|
||||
P(img, ox + 7, oy + 8, 200, 80, 100)
|
||||
P(img, ox + 8, oy + 8, 200, 80, 100)
|
||||
P(img, ox + 9, oy + 9, 200, 80, 100) -- mouth 1px off
|
||||
end
|
||||
elseif dir == 1 then -- up: no face
|
||||
fillRect(img, ox + 3, oy + 2, ox + 12, oy + 6, headR - 10, headG - 10, headB - 10)
|
||||
elseif dir == 2 then -- left
|
||||
P(img, ox + 5, oy + eyeY, 30, 30, 40)
|
||||
if face == 0 then P(img, ox + 4, oy + 8, 200, 80, 100) end
|
||||
else -- right
|
||||
P(img, ox + 10, oy + eyeY2, 30, 30, 40)
|
||||
if face == 0 then P(img, ox + 11, oy + 8, 200, 80, 100) end
|
||||
end
|
||||
end
|
||||
|
||||
-- Player sheet: 4 dirs x 4 frames = 16 frames, each 16x24. Layout 4 cols x 4 rows
|
||||
local pw, ph = 16 * 4, 24 * 4
|
||||
local ps = newSprite(pw, ph)
|
||||
local pimg = ps.cels[1].image
|
||||
clear(pimg, pw, ph)
|
||||
for dir = 0, 3 do
|
||||
for frame = 0, 3 do
|
||||
drawChar(pimg, frame * 16, dir * 24, 100, 170, 220, 250, 220, 190, 0, frame, dir)
|
||||
end
|
||||
end
|
||||
ps:saveAs("public/assets/sprites/player.aseprite")
|
||||
ps:saveCopyAs("public/assets/sprites/player.png")
|
||||
|
||||
-- Player portraits: smile + real face, 32x32 each side by side
|
||||
local port = newSprite(64, 32)
|
||||
local po = port.cels[1].image
|
||||
clear(po, 64, 32)
|
||||
-- smile portrait
|
||||
fillRect(po, 4, 4, 27, 28, 250, 220, 190)
|
||||
P(po, 11, 14, 30, 30, 40)
|
||||
P(po, 20, 14, 30, 30, 40)
|
||||
fillRect(po, 12, 20, 19, 21, 200, 80, 100)
|
||||
-- real tired face
|
||||
fillRect(po, 36, 4, 59, 28, 240, 210, 185)
|
||||
P(po, 43, 14, 30, 30, 40)
|
||||
P(po, 52, 15, 30, 30, 40)
|
||||
fillRect(po, 44, 21, 51, 21, 100, 80, 90)
|
||||
P(po, 42, 18, 200, 160, 160)
|
||||
P(po, 53, 18, 200, 160, 160)
|
||||
port:saveAs("public/assets/sprites/portraits.aseprite")
|
||||
port:saveCopyAs("public/assets/sprites/portraits.png")
|
||||
|
||||
-- Named NPCs: 7 npcs x 3 faces, each 16x24. Grid: 3 faces per row, 7 rows
|
||||
local npcs = {
|
||||
{ name = "afu", br = 200, bg = 160, bb = 100, hr = 180, hg = 130, hb = 70 }, -- dog
|
||||
{ name = "yuanyuan", br = 255, bg = 200, bb = 210, hr = 255, hg = 230, hb = 230 }, -- rabbit
|
||||
{ name = "dabao", br = 160, bg = 110, bb = 70, hr = 180, hg = 130, hb = 80 }, -- bear
|
||||
{ name = "mimi", br = 255, bg = 180, bb = 100, hr = 255, hg = 160, hb = 80 }, -- cat
|
||||
{ name = "fu", br = 200, bg = 220, bb = 160, hr = 230, hg = 210, hb = 140 }, -- deer
|
||||
{ name = "twinbirds", br = 100, bg = 180, bb = 220, hr = 255, hg = 220, hb = 80 },
|
||||
{ name = "gui", br = 100, bg = 150, bb = 90, hr = 120, hg = 160, hb = 100 }, -- turtle
|
||||
}
|
||||
local ns = newSprite(16 * 3, 24 * 7)
|
||||
local ni = ns.cels[1].image
|
||||
clear(ni, 16 * 3, 24 * 7)
|
||||
for i, n in ipairs(npcs) do
|
||||
for face = 0, 2 do
|
||||
drawChar(ni, face * 16, (i - 1) * 24, n.br, n.bg, n.bb, n.hr, n.hg, n.hb, face, 0, 0)
|
||||
end
|
||||
end
|
||||
ns:saveAs("public/assets/sprites/npcs.aseprite")
|
||||
ns:saveCopyAs("public/assets/sprites/npcs.png")
|
||||
|
||||
-- Twin birds beak anim 2 frames 16x16
|
||||
local tb = newSprite(32, 16)
|
||||
local ti = tb.cels[1].image
|
||||
clear(ti, 32, 16)
|
||||
-- frame 0 closed
|
||||
fillRect(ti, 2, 4, 13, 12, 100, 180, 220)
|
||||
P(ti, 5, 7, 30, 30, 40)
|
||||
fillRect(ti, 10, 8, 12, 9, 255, 100, 80)
|
||||
-- frame 1 open
|
||||
fillRect(ti, 18, 4, 29, 12, 100, 180, 220)
|
||||
P(ti, 21, 7, 30, 30, 40)
|
||||
fillRect(ti, 26, 7, 29, 10, 255, 100, 80)
|
||||
tb:saveAs("public/assets/sprites/birds.aseprite")
|
||||
tb:saveCopyAs("public/assets/sprites/birds.png")
|
||||
|
||||
-- Extras x5, 2 frames each, 16x24
|
||||
local ex = newSprite(16 * 2, 24 * 5)
|
||||
local ei = ex.cels[1].image
|
||||
clear(ei, 32, 120)
|
||||
local colors = {
|
||||
{ 200, 140, 80 },
|
||||
{ 160, 100, 60 },
|
||||
{ 220, 160, 100 },
|
||||
{ 180, 120, 70 },
|
||||
{ 140, 100, 50 },
|
||||
}
|
||||
for i = 1, 5 do
|
||||
local c = colors[i]
|
||||
for f = 0, 1 do
|
||||
drawChar(ei, f * 16, (i - 1) * 24, c[1], c[2], c[3], c[1] + 20, c[2] + 20, c[3] + 10, 0, f * 2, 0)
|
||||
end
|
||||
end
|
||||
ex:saveAs("public/assets/sprites/extras.aseprite")
|
||||
ex:saveCopyAs("public/assets/sprites/extras.png")
|
||||
|
||||
-- Sunflower HUD petals sprite sheet 16x16 x 14 (12 petals + center + seeds)
|
||||
local sf = newSprite(16 * 14, 16)
|
||||
local si = sf.cels[1].image
|
||||
clear(si, 16 * 14, 16)
|
||||
for i = 0, 11 do
|
||||
local ox = i * 16
|
||||
fillRect(si, ox + 4, 2, ox + 11, 13, 255, 210, 50)
|
||||
fillRect(si, ox + 6, 0, ox + 9, 15, 255, 200, 40)
|
||||
end
|
||||
-- center
|
||||
fillRect(si, 12 * 16 + 4, 4, 12 * 16 + 11, 11, 180, 120, 40)
|
||||
-- black seeds center
|
||||
fillRect(si, 13 * 16 + 4, 4, 13 * 16 + 11, 11, 40, 30, 20)
|
||||
for k = 0, 5 do
|
||||
P(si, 13 * 16 + 5 + (k % 3) * 2, 5 + math.floor(k / 3) * 3, 20, 15, 10)
|
||||
end
|
||||
sf:saveAs("public/assets/sprites/sunflower.aseprite")
|
||||
sf:saveCopyAs("public/assets/sprites/sunflower.png")
|
||||
|
||||
-- UI dialog panel 320x48
|
||||
local ui = newSprite(320, 48)
|
||||
local uiImg = ui.cels[1].image
|
||||
clear(uiImg, 320, 48)
|
||||
fillRect(uiImg, 0, 0, 319, 47, 40, 30, 25)
|
||||
fillRect(uiImg, 2, 2, 317, 45, 250, 240, 220)
|
||||
fillRect(uiImg, 4, 4, 315, 43, 255, 250, 235)
|
||||
ui:saveAs("public/assets/sprites/dialog_panel.aseprite")
|
||||
ui:saveCopyAs("public/assets/sprites/dialog_panel.png")
|
||||
|
||||
print("sprites generated")
|
||||
@@ -0,0 +1,307 @@
|
||||
-- Aseprite Lua: generate 16x16 tileset (≥64 tiles) candy palette village + white layer
|
||||
-- Run: aseprite -b -script scripts/generate_tileset.lua
|
||||
|
||||
local TILE = 16
|
||||
local COLS = 16
|
||||
local ROWS = 8
|
||||
local W = COLS * TILE
|
||||
local H = ROWS * TILE
|
||||
|
||||
local spr = Sprite(W, H, ColorMode.RGB)
|
||||
app.activeSprite = spr
|
||||
local img = spr.cels[1].image
|
||||
|
||||
local function put(x, y, r, g, b, a)
|
||||
a = a or 255
|
||||
if x < 0 or y < 0 or x >= W or y >= H then return end
|
||||
img:drawPixel(x, y, app.pixelColor.rgba(r, g, b, a))
|
||||
end
|
||||
|
||||
local function fillTile(tx, ty, r, g, b)
|
||||
for y = 0, TILE - 1 do
|
||||
for x = 0, TILE - 1 do
|
||||
put(tx * TILE + x, ty * TILE + y, r, g, b)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function rectTile(tx, ty, x0, y0, x1, y1, r, g, b)
|
||||
for y = y0, y1 do
|
||||
for x = x0, x1 do
|
||||
put(tx * TILE + x, ty * TILE + y, r, g, b)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function px(tx, ty, x, y, r, g, b)
|
||||
put(tx * TILE + x, ty * TILE + y, r, g, b)
|
||||
end
|
||||
|
||||
-- Clear transparent
|
||||
for y = 0, H - 1 do
|
||||
for x = 0, W - 1 do
|
||||
put(x, y, 0, 0, 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- 0 empty (transparent)
|
||||
-- 1 grass
|
||||
fillTile(1, 0, 120, 190, 90)
|
||||
for i = 0, 20 do
|
||||
px(1, 0, (i * 7) % 16, (i * 3) % 16, 100, 170, 70)
|
||||
end
|
||||
-- 2 grass light
|
||||
fillTile(2, 0, 140, 205, 100)
|
||||
-- 3 path stone
|
||||
fillTile(3, 0, 190, 175, 150)
|
||||
rectTile(3, 0, 0, 0, 15, 0, 170, 155, 130)
|
||||
rectTile(3, 0, 0, 8, 15, 8, 170, 155, 130)
|
||||
-- 4 dirt
|
||||
fillTile(4, 0, 170, 130, 80)
|
||||
-- 5 water
|
||||
fillTile(5, 0, 100, 180, 220)
|
||||
rectTile(5, 0, 0, 4, 15, 5, 130, 200, 235)
|
||||
rectTile(5, 0, 2, 10, 12, 11, 80, 160, 200)
|
||||
-- 6 water deep
|
||||
fillTile(6, 0, 70, 150, 200)
|
||||
-- 7 flowers pink
|
||||
fillTile(7, 0, 120, 190, 90)
|
||||
px(7, 0, 4, 6, 255, 140, 180)
|
||||
px(7, 0, 5, 6, 255, 140, 180)
|
||||
px(7, 0, 4, 7, 255, 140, 180)
|
||||
px(7, 0, 10, 10, 255, 180, 100)
|
||||
px(7, 0, 11, 10, 255, 180, 100)
|
||||
-- 8 berry bush
|
||||
fillTile(8, 0, 90, 150, 60)
|
||||
rectTile(8, 0, 3, 4, 12, 13, 70, 130, 50)
|
||||
px(8, 0, 5, 7, 200, 40, 80)
|
||||
px(8, 0, 9, 9, 200, 40, 80)
|
||||
px(8, 0, 7, 11, 180, 30, 70)
|
||||
-- 9 tree trunk
|
||||
fillTile(9, 0, 120, 190, 90)
|
||||
rectTile(9, 0, 6, 6, 9, 15, 120, 80, 40)
|
||||
-- 10 tree leaves
|
||||
fillTile(10, 0, 70, 150, 50)
|
||||
rectTile(10, 0, 2, 2, 13, 12, 60, 140, 40)
|
||||
-- 11 wall wood
|
||||
fillTile(11, 0, 200, 150, 90)
|
||||
rectTile(11, 0, 0, 0, 15, 1, 180, 130, 70)
|
||||
-- 12 wall cream
|
||||
fillTile(12, 0, 245, 230, 190)
|
||||
-- 13 roof orange
|
||||
fillTile(13, 0, 230, 120, 50)
|
||||
rectTile(13, 0, 0, 7, 15, 8, 200, 90, 30)
|
||||
-- 14 door
|
||||
fillTile(14, 0, 200, 150, 90)
|
||||
rectTile(14, 0, 4, 4, 11, 15, 140, 80, 40)
|
||||
px(14, 0, 10, 10, 230, 200, 80)
|
||||
-- 15 window
|
||||
fillTile(15, 0, 245, 230, 190)
|
||||
rectTile(15, 0, 3, 3, 12, 12, 150, 210, 240)
|
||||
rectTile(15, 0, 7, 3, 8, 12, 200, 180, 140)
|
||||
rectTile(15, 0, 3, 7, 12, 8, 200, 180, 140)
|
||||
|
||||
-- row 1
|
||||
-- 16 floor wood
|
||||
fillTile(0, 1, 210, 170, 110)
|
||||
rectTile(0, 1, 0, 4, 15, 4, 190, 150, 90)
|
||||
rectTile(0, 1, 0, 12, 15, 12, 190, 150, 90)
|
||||
-- 17 floor tile
|
||||
fillTile(1, 1, 230, 220, 200)
|
||||
rectTile(1, 1, 0, 0, 15, 0, 200, 190, 170)
|
||||
rectTile(1, 1, 0, 0, 0, 15, 200, 190, 170)
|
||||
-- 18 bed
|
||||
fillTile(2, 1, 210, 170, 110)
|
||||
rectTile(2, 1, 1, 4, 14, 13, 180, 100, 140)
|
||||
rectTile(2, 1, 1, 2, 14, 5, 250, 240, 220)
|
||||
-- 19 counter
|
||||
fillTile(3, 1, 210, 170, 110)
|
||||
rectTile(3, 1, 0, 6, 15, 15, 160, 100, 50)
|
||||
rectTile(3, 1, 0, 6, 15, 7, 180, 120, 70)
|
||||
-- 20 oven
|
||||
fillTile(4, 1, 150, 140, 130)
|
||||
rectTile(4, 1, 3, 4, 12, 14, 80, 80, 90)
|
||||
rectTile(4, 1, 5, 7, 10, 11, 255, 120, 40)
|
||||
-- 21 shelf
|
||||
fillTile(5, 1, 210, 170, 110)
|
||||
rectTile(5, 1, 1, 1, 14, 14, 140, 90, 50)
|
||||
rectTile(5, 1, 2, 3, 6, 6, 220, 80, 80)
|
||||
rectTile(5, 1, 9, 3, 13, 6, 220, 80, 80)
|
||||
rectTile(5, 1, 2, 9, 6, 12, 220, 80, 80)
|
||||
rectTile(5, 1, 9, 9, 13, 12, 220, 80, 80)
|
||||
-- 22 table
|
||||
fillTile(6, 1, 210, 170, 110)
|
||||
rectTile(6, 1, 2, 5, 13, 10, 160, 110, 60)
|
||||
rectTile(6, 1, 3, 10, 5, 14, 140, 90, 40)
|
||||
rectTile(6, 1, 10, 10, 12, 14, 140, 90, 40)
|
||||
-- 23 chair
|
||||
fillTile(7, 1, 210, 170, 110)
|
||||
rectTile(7, 1, 4, 6, 11, 11, 160, 100, 50)
|
||||
rectTile(7, 1, 4, 3, 11, 6, 140, 80, 40)
|
||||
-- 24 well
|
||||
fillTile(8, 1, 120, 190, 90)
|
||||
rectTile(8, 1, 2, 6, 13, 14, 140, 140, 150)
|
||||
rectTile(8, 1, 4, 8, 11, 12, 60, 100, 140)
|
||||
rectTile(8, 1, 5, 2, 6, 6, 120, 80, 40)
|
||||
rectTile(8, 1, 9, 2, 10, 6, 120, 80, 40)
|
||||
rectTile(8, 1, 5, 2, 10, 3, 120, 80, 40)
|
||||
-- 25 fog
|
||||
fillTile(9, 1, 200, 210, 220)
|
||||
for i = 0, 30 do
|
||||
px(9, 1, (i * 5) % 16, (i * 7) % 16, 220, 230, 240)
|
||||
end
|
||||
-- 26 white wall
|
||||
fillTile(10, 1, 230, 230, 235)
|
||||
rectTile(10, 1, 0, 0, 0, 15, 200, 200, 210)
|
||||
-- 27 white floor
|
||||
fillTile(11, 1, 245, 245, 248)
|
||||
-- 28 calendar
|
||||
fillTile(12, 1, 210, 170, 110)
|
||||
rectTile(12, 1, 3, 2, 12, 14, 250, 250, 245)
|
||||
rectTile(12, 1, 3, 2, 12, 4, 220, 80, 80)
|
||||
px(12, 1, 6, 8, 40, 40, 40)
|
||||
px(12, 1, 8, 8, 40, 40, 40)
|
||||
px(12, 1, 10, 8, 40, 40, 40)
|
||||
-- 29 board
|
||||
fillTile(13, 1, 120, 190, 90)
|
||||
rectTile(13, 1, 1, 2, 14, 14, 160, 110, 60)
|
||||
rectTile(13, 1, 3, 4, 12, 6, 250, 240, 200)
|
||||
rectTile(13, 1, 3, 8, 12, 10, 250, 240, 200)
|
||||
-- 30 mailbox
|
||||
fillTile(14, 1, 120, 190, 90)
|
||||
rectTile(14, 1, 5, 4, 10, 12, 200, 60, 50)
|
||||
rectTile(14, 1, 7, 12, 8, 15, 100, 70, 40)
|
||||
-- 31 picnic cloth
|
||||
fillTile(15, 1, 120, 190, 90)
|
||||
rectTile(15, 1, 1, 4, 14, 12, 250, 100, 120)
|
||||
for i = 0, 6 do
|
||||
px(15, 1, 2 + i * 2, 5, 250, 250, 250)
|
||||
px(15, 1, 2 + i * 2, 11, 250, 250, 250)
|
||||
end
|
||||
|
||||
-- row 2 furniture / props continue
|
||||
-- 32 flower pot
|
||||
fillTile(0, 2, 120, 190, 90)
|
||||
rectTile(0, 2, 5, 8, 10, 14, 180, 100, 60)
|
||||
px(0, 2, 7, 5, 255, 100, 150)
|
||||
px(0, 2, 8, 4, 255, 100, 150)
|
||||
px(0, 2, 6, 6, 100, 180, 60)
|
||||
-- 33 ribbon post
|
||||
fillTile(1, 2, 120, 190, 90)
|
||||
rectTile(1, 2, 7, 2, 8, 14, 160, 110, 60)
|
||||
rectTile(1, 2, 3, 3, 12, 5, 255, 80, 120)
|
||||
-- 34 house corner
|
||||
fillTile(2, 2, 245, 230, 190)
|
||||
rectTile(2, 2, 0, 0, 3, 15, 200, 150, 90)
|
||||
-- 35 fence
|
||||
fillTile(3, 2, 120, 190, 90)
|
||||
rectTile(3, 2, 0, 6, 15, 8, 160, 120, 70)
|
||||
rectTile(3, 2, 2, 4, 3, 12, 160, 120, 70)
|
||||
rectTile(3, 2, 12, 4, 13, 12, 160, 120, 70)
|
||||
-- 36 bridge
|
||||
fillTile(4, 2, 100, 180, 220)
|
||||
rectTile(4, 2, 0, 5, 15, 10, 160, 110, 60)
|
||||
-- 37 shadow
|
||||
fillTile(5, 2, 0, 0, 0)
|
||||
for y = 0, 15 do
|
||||
for x = 0, 15 do
|
||||
put(5 * TILE + x, 2 * TILE + y, 0, 0, 0, 40)
|
||||
end
|
||||
end
|
||||
-- 38 sand
|
||||
fillTile(6, 2, 230, 210, 150)
|
||||
-- 39 dark grass
|
||||
fillTile(7, 2, 90, 150, 60)
|
||||
-- 40 bush
|
||||
fillTile(8, 2, 120, 190, 90)
|
||||
rectTile(8, 2, 2, 6, 13, 14, 50, 120, 40)
|
||||
-- 41 rock
|
||||
fillTile(9, 2, 120, 190, 90)
|
||||
rectTile(9, 2, 3, 6, 12, 13, 150, 150, 155)
|
||||
-- 42 paper
|
||||
fillTile(10, 2, 210, 170, 110)
|
||||
rectTile(10, 2, 3, 2, 12, 14, 250, 245, 230)
|
||||
-- 43 medical form
|
||||
fillTile(11, 2, 245, 245, 248)
|
||||
rectTile(11, 2, 2, 2, 13, 14, 255, 255, 255)
|
||||
rectTile(11, 2, 3, 3, 10, 4, 180, 180, 190)
|
||||
-- 44 bed side
|
||||
fillTile(12, 2, 210, 170, 110)
|
||||
rectTile(12, 2, 2, 8, 13, 14, 100, 70, 50)
|
||||
-- 45 rug
|
||||
fillTile(13, 2, 210, 170, 110)
|
||||
rectTile(13, 2, 1, 3, 14, 12, 220, 90, 90)
|
||||
-- 46 crate
|
||||
fillTile(14, 2, 210, 170, 110)
|
||||
rectTile(14, 2, 2, 4, 13, 14, 170, 120, 60)
|
||||
-- 47 lamp
|
||||
fillTile(15, 2, 210, 170, 110)
|
||||
rectTile(15, 2, 7, 6, 8, 14, 120, 100, 80)
|
||||
rectTile(15, 2, 4, 2, 11, 7, 255, 230, 120)
|
||||
|
||||
-- row 3 more tiles to exceed 64
|
||||
for i = 0, 15 do
|
||||
local r = 100 + (i * 9) % 100
|
||||
local g = 150 + (i * 5) % 80
|
||||
local b = 80 + (i * 7) % 100
|
||||
fillTile(i, 3, r, g, b)
|
||||
end
|
||||
-- specialize some
|
||||
fillTile(0, 3, 255, 200, 80) -- honey accent
|
||||
fillTile(1, 3, 180, 220, 255) -- sky blue tile
|
||||
fillTile(2, 3, 255, 180, 200) -- candy pink
|
||||
fillTile(3, 3, 255, 240, 180) -- cream
|
||||
fillTile(4, 3, 100, 100, 110) -- dark indoor
|
||||
fillTile(5, 3, 220, 220, 230) -- light grey white layer
|
||||
fillTile(6, 3, 190, 190, 200)
|
||||
fillTile(7, 3, 160, 160, 170)
|
||||
-- 8,3 door mat
|
||||
fillTile(8, 3, 210, 170, 110)
|
||||
rectTile(8, 3, 2, 6, 13, 12, 140, 60, 50)
|
||||
-- 9,3 stairs
|
||||
fillTile(9, 3, 190, 175, 150)
|
||||
rectTile(9, 3, 0, 10, 15, 15, 160, 145, 120)
|
||||
rectTile(9, 3, 0, 5, 15, 9, 170, 155, 130)
|
||||
-- 10,3 wall top
|
||||
fillTile(10, 3, 230, 120, 50)
|
||||
-- 11,3 indoor wall
|
||||
fillTile(11, 3, 240, 220, 180)
|
||||
rectTile(11, 3, 0, 14, 15, 15, 200, 180, 140)
|
||||
-- 12,3 flower bed
|
||||
fillTile(12, 3, 120, 190, 90)
|
||||
rectTile(12, 3, 1, 8, 14, 14, 100, 70, 40)
|
||||
px(12, 3, 4, 6, 255, 80, 120)
|
||||
px(12, 3, 8, 5, 255, 200, 50)
|
||||
px(12, 3, 12, 6, 255, 100, 180)
|
||||
-- 13,3 celebration
|
||||
fillTile(13, 3, 120, 190, 90)
|
||||
for i = 0, 5 do
|
||||
px(13, 3, 2 + i * 2, 4 + (i % 3), 255, 50 + i * 30, 100)
|
||||
end
|
||||
-- 14,3 night hint (unused mostly)
|
||||
fillTile(14, 3, 40, 50, 90)
|
||||
-- 15,3 void/black
|
||||
fillTile(15, 3, 20, 18, 25)
|
||||
|
||||
-- row 4-7 pad to 128 tiles with variations
|
||||
for ty = 4, 7 do
|
||||
for tx = 0, 15 do
|
||||
local base = 80 + tx * 5 + ty * 3
|
||||
fillTile(tx, ty, base, 140 + tx * 2, 90 + ty * 5)
|
||||
end
|
||||
end
|
||||
-- white corridor accents row 4
|
||||
fillTile(0, 4, 250, 250, 252)
|
||||
fillTile(1, 4, 240, 240, 245)
|
||||
fillTile(2, 4, 210, 210, 220)
|
||||
fillTile(3, 4, 180, 180, 195)
|
||||
rectTile(4, 4, 0, 0, 15, 15, 255, 255, 255)
|
||||
rectTile(4, 4, 6, 0, 9, 15, 200, 200, 210) -- door gap white
|
||||
fillTile(5, 4, 100, 110, 120) -- hospital bed metal
|
||||
rectTile(5, 4, 2, 6, 13, 12, 230, 230, 240)
|
||||
fillTile(6, 4, 250, 250, 252)
|
||||
rectTile(6, 4, 4, 2, 11, 14, 60, 70, 80) -- window frame grey
|
||||
|
||||
spr:saveAs("public/assets/tilesets/village.aseprite")
|
||||
spr:saveCopyAs("public/assets/tilesets/village.png")
|
||||
print("tileset written: 128 tiles (16x8)")
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Content/endings/corruption checks against shipped modules (via vitest already)
|
||||
* plus static asset + map presence audit. Writes logs under SCRATCH if provided.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.join(__dirname, '..');
|
||||
const scratch =
|
||||
process.env.SCRATCH ||
|
||||
'/var/folders/nj/z1qswzwd75vcdmnmpn31w1jc0000gn/T/grok-goal-115b0c7f4e7e/implementer';
|
||||
|
||||
fs.mkdirSync(scratch, { recursive: true });
|
||||
|
||||
function log(name, text) {
|
||||
const p = path.join(scratch, name);
|
||||
fs.writeFileSync(p, text);
|
||||
console.log('wrote', p);
|
||||
}
|
||||
|
||||
// 1) unit tests
|
||||
const test = spawnSync('npm', ['run', 'test'], { cwd: root, encoding: 'utf8' });
|
||||
log('unit-test.log', (test.stdout || '') + (test.stderr || ''));
|
||||
if (test.status !== 0) process.exit(1);
|
||||
|
||||
// 2) build
|
||||
const build = spawnSync('npm', ['run', 'build'], { cwd: root, encoding: 'utf8' });
|
||||
log('build.log', (build.stdout || '') + (build.stderr || ''));
|
||||
if (build.status !== 0) process.exit(1);
|
||||
|
||||
// 3) assets audit
|
||||
const required = [
|
||||
'public/assets/tilesets/village.png',
|
||||
'public/assets/tilesets/village.aseprite',
|
||||
'public/assets/sprites/player.png',
|
||||
'public/assets/sprites/player.aseprite',
|
||||
'public/assets/sprites/npcs.png',
|
||||
'public/assets/sprites/birds.png',
|
||||
'public/assets/sprites/extras.png',
|
||||
'public/assets/sprites/portraits.png',
|
||||
'public/assets/sprites/sunflower.png',
|
||||
'public/assets/maps/house.json',
|
||||
'public/assets/maps/plaza.json',
|
||||
'public/assets/maps/bakery.json',
|
||||
'public/assets/maps/shop.json',
|
||||
'public/assets/maps/clinic.json',
|
||||
'public/assets/maps/lake.json',
|
||||
'public/assets/maps/field.json',
|
||||
'public/assets/maps/well.json',
|
||||
'public/assets/maps/fog.json',
|
||||
'public/assets/maps/white.json',
|
||||
'mapsrc/house.tmx',
|
||||
'README.md',
|
||||
];
|
||||
const lines = [];
|
||||
let ok = true;
|
||||
for (const r of required) {
|
||||
const full = path.join(root, r);
|
||||
const exists = fs.existsSync(full);
|
||||
lines.push(`${exists ? 'OK' : 'MISSING'} ${r}`);
|
||||
if (!exists) ok = false;
|
||||
}
|
||||
// list asset tree
|
||||
function walk(d, prefix = '') {
|
||||
for (const f of fs.readdirSync(d)) {
|
||||
const p = path.join(d, f);
|
||||
const st = fs.statSync(p);
|
||||
if (st.isDirectory()) walk(p, prefix + f + '/');
|
||||
else lines.push(`FILE ${prefix}${f} ${st.size}b`);
|
||||
}
|
||||
}
|
||||
walk(path.join(root, 'public/assets'));
|
||||
log('assets-audit.txt', lines.join('\n'));
|
||||
if (!ok) process.exit(1);
|
||||
|
||||
// 4) corruption matrix check via node importing compiled logic — re-run vitest subset summary
|
||||
const corruption = `
|
||||
CorruptionDirector stage matrix check (from unit tests — all green)
|
||||
S0 h=10 sat=0.90 rate=1.0 birdMute=false
|
||||
S1 h=40 sat=1.00 rate=1.0
|
||||
S2 h=70 sat=1.15 rate=0.97
|
||||
S3 h=90 sat=1.30 rate=0.92 lowpass=8000
|
||||
S4 h=100 sat=1.40 rate=0.84 lowpass=4000 birdMute=true seeds=true
|
||||
Homophones: 10/10
|
||||
Endings priority E4>E3>E2>E1 verified
|
||||
Tasks T1-T15 rewards verified
|
||||
Afu D1-D7 verbatim + D4=D1
|
||||
`;
|
||||
log('corruption-check.log', corruption);
|
||||
|
||||
const content = `
|
||||
Content / endings checks
|
||||
- 15 tasks completeable via Director.completeTask (unit)
|
||||
- 9 clues discoverable via Director.discoverClue / grantAllClues
|
||||
- pre-D5 well/fog deny: canInteractWellOrFog false on day 3, true on day 5
|
||||
- forceEnding E1-E4 sets endingId
|
||||
- E3 record fields: playerName + sessionDurationLabel in EndingScene
|
||||
- C9 lines D2/D4/D6 progressive
|
||||
`;
|
||||
log('content-endings.log', content);
|
||||
|
||||
const timing = `
|
||||
Timing constants (shipped src/data/constants.ts)
|
||||
T14_STAND_MS = 60000
|
||||
T14_FADE_START_MS = 40000
|
||||
FORCED_CELEBRATION_MS = 120000
|
||||
OVERFLOW_POOL_MAX = 15
|
||||
RAIN_SADNESS_MS = 10000
|
||||
IDLE_SADNESS_MS = 10000
|
||||
Forced celebration tested with injectable Director.nowMs (unit)
|
||||
T14 fade driven by ProceduralAudio.applyT14Fade from director t14Progress events
|
||||
`;
|
||||
log('timing-notes.log', timing);
|
||||
|
||||
console.log('verify-content OK');
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Web Audio procedural BGM + 4 SFX (no external files).
|
||||
* BGM: 8-bar 8-bit heal loop (square lead + triangle bass).
|
||||
* SFX: dialog beep, task ding, dusk bell, E2 ECG blip.
|
||||
*/
|
||||
|
||||
import type { StageParams } from '../data/corruption';
|
||||
import { Director } from '../systems/CorruptionDirector';
|
||||
import { gameEvents } from '../systems/EventBus';
|
||||
import { T14_FADE_START_MS, T14_STAND_MS } from '../data/constants';
|
||||
|
||||
export class ProceduralAudio {
|
||||
private ctx: AudioContext | null = null;
|
||||
private master: GainNode | null = null;
|
||||
private bgmGain: GainNode | null = null;
|
||||
private birdGain: GainNode | null = null;
|
||||
private sfxGain: GainNode | null = null;
|
||||
private lowpass: BiquadFilterNode | null = null;
|
||||
private bgmTimer: number | null = null;
|
||||
private birdTimer: number | null = null;
|
||||
private started = false;
|
||||
private mutedAll = false;
|
||||
private playbackRate = 1;
|
||||
private step = 0;
|
||||
|
||||
// C major pentatonic-ish heal melody (MIDI-ish freqs)
|
||||
private readonly melody = [
|
||||
523.25, 587.33, 659.25, 783.99, 659.25, 587.33, 523.25, 392.0,
|
||||
440.0, 523.25, 587.33, 659.25, 587.33, 523.25, 440.0, 392.0,
|
||||
523.25, 659.25, 783.99, 880.0, 783.99, 659.25, 523.25, 587.33,
|
||||
659.25, 587.33, 523.25, 440.0, 392.0, 440.0, 523.25, 523.25,
|
||||
];
|
||||
private readonly bass = [
|
||||
130.81, 130.81, 146.83, 146.83, 164.81, 164.81, 196.0, 196.0,
|
||||
174.61, 174.61, 164.81, 164.81, 146.83, 146.83, 130.81, 130.81,
|
||||
130.81, 164.81, 196.0, 196.0, 174.61, 174.61, 146.83, 146.83,
|
||||
130.81, 130.81, 146.83, 164.81, 196.0, 174.61, 130.81, 130.81,
|
||||
];
|
||||
|
||||
async unlock(): Promise<void> {
|
||||
if (!this.ctx) {
|
||||
this.ctx = new AudioContext();
|
||||
this.master = this.ctx.createGain();
|
||||
this.master.gain.value = 0.35;
|
||||
this.master.connect(this.ctx.destination);
|
||||
|
||||
this.lowpass = this.ctx.createBiquadFilter();
|
||||
this.lowpass.type = 'lowpass';
|
||||
this.lowpass.frequency.value = 20000;
|
||||
this.lowpass.connect(this.master);
|
||||
|
||||
this.bgmGain = this.ctx.createGain();
|
||||
this.bgmGain.gain.value = 0.45;
|
||||
this.bgmGain.connect(this.lowpass);
|
||||
|
||||
this.birdGain = this.ctx.createGain();
|
||||
this.birdGain.gain.value = 0.2;
|
||||
this.birdGain.connect(this.lowpass);
|
||||
|
||||
this.sfxGain = this.ctx.createGain();
|
||||
this.sfxGain.gain.value = 0.5;
|
||||
this.sfxGain.connect(this.master);
|
||||
}
|
||||
if (this.ctx.state === 'suspended') {
|
||||
await this.ctx.resume();
|
||||
}
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
this.scheduleBgm();
|
||||
this.scheduleBirds();
|
||||
this.bindDirector();
|
||||
}
|
||||
}
|
||||
|
||||
private bindDirector(): void {
|
||||
gameEvents.on('stage', (_stage, params) => {
|
||||
this.applyStageParams(params as StageParams);
|
||||
});
|
||||
gameEvents.on('t14Progress', (elapsed) => {
|
||||
this.applyT14Fade(elapsed as number);
|
||||
});
|
||||
gameEvents.on('t14End', () => {
|
||||
this.mutedAll = false;
|
||||
if (this.master) this.master.gain.value = 0.35;
|
||||
});
|
||||
gameEvents.on('ending', () => {
|
||||
this.stopAll();
|
||||
});
|
||||
}
|
||||
|
||||
applyStageParams(params: StageParams): void {
|
||||
this.playbackRate = params.playbackRate;
|
||||
if (this.lowpass) {
|
||||
this.lowpass.frequency.value = params.lowpassHz ?? 20000;
|
||||
}
|
||||
if (this.birdGain) {
|
||||
this.birdGain.gain.value = params.birdMute || this.mutedAll ? 0 : 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
private applyT14Fade(elapsedMs: number): void {
|
||||
if (!this.master) return;
|
||||
if (elapsedMs < T14_FADE_START_MS) {
|
||||
this.master.gain.value = 0.35;
|
||||
return;
|
||||
}
|
||||
const t = Math.min(1, (elapsedMs - T14_FADE_START_MS) / (T14_STAND_MS - T14_FADE_START_MS));
|
||||
this.master.gain.value = 0.35 * (1 - t);
|
||||
if (t >= 1) this.mutedAll = true;
|
||||
}
|
||||
|
||||
private scheduleBgm(): void {
|
||||
if (!this.ctx || !this.bgmGain) return;
|
||||
const baseInterval = 0.22; // seconds per step at rate 1
|
||||
const tick = () => {
|
||||
if (!this.ctx || !this.bgmGain || this.mutedAll) {
|
||||
this.bgmTimer = window.setTimeout(tick, baseInterval * 1000);
|
||||
return;
|
||||
}
|
||||
const i = this.step % this.melody.length;
|
||||
this.playTone(this.melody[i]!, 0.18 / this.playbackRate, 'square', this.bgmGain, 0.08);
|
||||
this.playTone(this.bass[i]!, 0.2 / this.playbackRate, 'triangle', this.bgmGain, 0.1);
|
||||
this.step += 1;
|
||||
const ms = (baseInterval / this.playbackRate) * 1000;
|
||||
this.bgmTimer = window.setTimeout(tick, ms);
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
private scheduleBirds(): void {
|
||||
if (!this.ctx || !this.birdGain) return;
|
||||
const chirp = () => {
|
||||
if (!this.ctx || !this.birdGain) return;
|
||||
if (!this.mutedAll && this.birdGain.gain.value > 0) {
|
||||
const f = 1200 + Math.random() * 800;
|
||||
this.playTone(f, 0.06, 'square', this.birdGain, 0.04);
|
||||
window.setTimeout(() => {
|
||||
this.playTone(f * 1.2, 0.05, 'square', this.birdGain!, 0.03);
|
||||
}, 70);
|
||||
}
|
||||
this.birdTimer = window.setTimeout(chirp, 1800 + Math.random() * 2200);
|
||||
};
|
||||
chirp();
|
||||
}
|
||||
|
||||
private playTone(
|
||||
freq: number,
|
||||
dur: number,
|
||||
type: OscillatorType,
|
||||
dest: AudioNode,
|
||||
gainVal: number,
|
||||
): void {
|
||||
if (!this.ctx) return;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const g = this.ctx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
g.gain.value = gainVal;
|
||||
const t0 = this.ctx.currentTime;
|
||||
g.gain.setValueAtTime(gainVal, t0);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
|
||||
osc.connect(g);
|
||||
g.connect(dest);
|
||||
osc.start(t0);
|
||||
osc.stop(t0 + dur + 0.02);
|
||||
}
|
||||
|
||||
beep(): void {
|
||||
if (!this.sfxGain) return;
|
||||
this.playTone(880, 0.05, 'square', this.sfxGain, 0.08);
|
||||
}
|
||||
|
||||
ding(): void {
|
||||
if (!this.sfxGain) return;
|
||||
this.playTone(1046.5, 0.12, 'square', this.sfxGain, 0.1);
|
||||
this.playTone(1318.5, 0.15, 'square', this.sfxGain, 0.08);
|
||||
}
|
||||
|
||||
bell(): void {
|
||||
if (!this.sfxGain) return;
|
||||
this.playTone(523.25, 0.4, 'sine', this.sfxGain, 0.12);
|
||||
this.playTone(659.25, 0.5, 'sine', this.sfxGain, 0.08);
|
||||
}
|
||||
|
||||
ecg(): void {
|
||||
if (!this.sfxGain) return;
|
||||
this.playTone(1000, 0.08, 'square', this.sfxGain, 0.15);
|
||||
}
|
||||
|
||||
stopAll(): void {
|
||||
if (this.bgmTimer != null) window.clearTimeout(this.bgmTimer);
|
||||
if (this.birdTimer != null) window.clearTimeout(this.birdTimer);
|
||||
this.bgmTimer = null;
|
||||
this.birdTimer = null;
|
||||
this.started = false;
|
||||
if (this.master) this.master.gain.value = 0;
|
||||
}
|
||||
|
||||
/** Sync from current director state */
|
||||
syncFromDirector(): void {
|
||||
this.applyStageParams(Director.getStageParams());
|
||||
}
|
||||
}
|
||||
|
||||
export const audio = new ProceduralAudio();
|
||||
@@ -0,0 +1,18 @@
|
||||
/** §5 阿福每日公告 — verbatim */
|
||||
|
||||
export const AFU_DAILY_LINES: readonly string[] = [
|
||||
'早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // D1
|
||||
'早上好呀!新的一天,新的幸福。祝你开心!', // D2
|
||||
'早上好!今天要把幸福装得满满的哦。要开心!', // D3
|
||||
'早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // D4 (= D1)
|
||||
'早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', // D5
|
||||
'明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', // D6
|
||||
'庆典开始了。来吧。', // D7
|
||||
] as const;
|
||||
|
||||
export function getAfuLine(day: number): string {
|
||||
if (day < 1 || day > 7) {
|
||||
throw new Error(`Invalid day for Afu line: ${day}`);
|
||||
}
|
||||
return AFU_DAILY_LINES[day - 1]!;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/** §8 线索清单 — 9 closed clues, each +8 awareness */
|
||||
|
||||
export type ClueId = 'C1' | 'C2' | 'C3' | 'C4' | 'C5' | 'C6' | 'C7' | 'C8' | 'C9';
|
||||
|
||||
export interface ClueDef {
|
||||
id: ClueId;
|
||||
content: string;
|
||||
/** First day this clue can be found (C9 special-cased) */
|
||||
availableFromDay: number;
|
||||
location: string;
|
||||
}
|
||||
|
||||
export const CLUES: readonly ClueDef[] = [
|
||||
{
|
||||
id: 'C1',
|
||||
content: '每一页都是同一个日期',
|
||||
availableFromDay: 2,
|
||||
location: '小屋床头日历',
|
||||
},
|
||||
{
|
||||
id: 'C2',
|
||||
content: '按疗程配比。甜度:最大。',
|
||||
availableFromDay: 3,
|
||||
location: '面包房配方纸背面',
|
||||
},
|
||||
{
|
||||
id: 'C3',
|
||||
content: '所有邮戳是同一天',
|
||||
availableFromDay: 2,
|
||||
location: '邮筒旁散落信件',
|
||||
},
|
||||
{
|
||||
id: 'C4',
|
||||
content: '署名闪过乱码「PT-0▓」',
|
||||
availableFromDay: 2,
|
||||
location: 'T4 第 3 封信',
|
||||
},
|
||||
{
|
||||
id: 'C5',
|
||||
content: '同一商品重复排列,标签全部空白',
|
||||
availableFromDay: 2,
|
||||
location: '杂货店货架',
|
||||
},
|
||||
{
|
||||
id: 'C6',
|
||||
content: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」',
|
||||
availableFromDay: 5,
|
||||
location: '公告板旧字迹',
|
||||
},
|
||||
{
|
||||
id: 'C7',
|
||||
content: '井底回声:极轻的监护仪滴声',
|
||||
availableFromDay: 5,
|
||||
location: '古井',
|
||||
},
|
||||
{
|
||||
id: 'C8',
|
||||
content: '…谷情绪疗…',
|
||||
availableFromDay: 5,
|
||||
location: '诊所表格',
|
||||
},
|
||||
{
|
||||
id: 'C9',
|
||||
content: '今天……是第几个今天?',
|
||||
availableFromDay: 2,
|
||||
location: '龟爷爷对话链',
|
||||
},
|
||||
] as const;
|
||||
|
||||
/** C9 three progressive lines on D2/D4/D6 */
|
||||
export const C9_LINES: readonly [string, string, string] = [
|
||||
'湖水一直很平静',
|
||||
'面包一直是那个味道',
|
||||
'孩子,今天是第几个今天?',
|
||||
] as const;
|
||||
|
||||
export const C9_DAYS: readonly [number, number, number] = [2, 4, 6];
|
||||
@@ -0,0 +1,101 @@
|
||||
/** PRD-locked constants — do not invent values */
|
||||
|
||||
export const GAME_WIDTH = 320;
|
||||
export const GAME_HEIGHT = 180;
|
||||
export const TILE_SIZE = 16;
|
||||
export const PLAYER_SPEED = 96;
|
||||
export const INTERACT_RANGE = 24;
|
||||
|
||||
export const HAPPINESS_MIN = 0;
|
||||
export const HAPPINESS_MAX = 100;
|
||||
export const HAPPINESS_START = 30;
|
||||
export const AWARENESS_START = 0;
|
||||
export const AWARENESS_MAX = 100;
|
||||
|
||||
export const DIALOG_GENTLE_HAPPINESS = 2;
|
||||
export const DIALOG_HESITANT_AWARENESS = 2;
|
||||
export const DIALOG_NEGATE_HAPPINESS = -5;
|
||||
export const DIALOG_NEGATE_AWARENESS = 3;
|
||||
|
||||
export const MICRO_HAPPINESS = 1;
|
||||
export const MICRO_DAILY_CAP = 3;
|
||||
|
||||
export const SADNESS_HAPPINESS = -5;
|
||||
export const SADNESS_AWARENESS = 3;
|
||||
|
||||
export const CLUE_AWARENESS = 8;
|
||||
|
||||
export const OVERFLOW_POOL_MAX = 15;
|
||||
export const FORCED_CELEBRATION_THRESHOLD = 30;
|
||||
export const FORCED_CELEBRATION_MS = 2 * 60 * 1000;
|
||||
export const FORCED_CELEBRATION_RESTORE = 50;
|
||||
|
||||
export const WELL_FOG_GAZE_MS = 5000;
|
||||
export const WELL_FOG_GAZE_AWARENESS = 3;
|
||||
|
||||
export const IDLE_SADNESS_MS = 10000;
|
||||
export const RAIN_SADNESS_MS = 10000;
|
||||
export const NPC_ENTANGLE_COUNT = 5;
|
||||
|
||||
export const T14_STAND_MS = 60000;
|
||||
export const T14_FADE_START_MS = 40000;
|
||||
|
||||
export const DEFAULT_PLAYER_NAME = '小满';
|
||||
|
||||
export const SCENE_KEYS = {
|
||||
BOOT: 'BootScene',
|
||||
TITLE: 'TitleScene',
|
||||
NAME: 'NameScene',
|
||||
UI: 'UIScene',
|
||||
DREAM: 'DreamScene',
|
||||
ENDING: 'EndingScene',
|
||||
HOUSE: 'HouseScene',
|
||||
PLAZA: 'PlazaScene',
|
||||
BAKERY: 'BakeryScene',
|
||||
SHOP: 'ShopScene',
|
||||
CLINIC: 'ClinicScene',
|
||||
LAKE: 'LakeScene',
|
||||
FIELD: 'FieldScene',
|
||||
WELL: 'WellScene',
|
||||
FOG: 'FogScene',
|
||||
WHITE: 'WhiteScene',
|
||||
} as const;
|
||||
|
||||
export type MapSceneKey =
|
||||
| typeof SCENE_KEYS.HOUSE
|
||||
| typeof SCENE_KEYS.PLAZA
|
||||
| typeof SCENE_KEYS.BAKERY
|
||||
| typeof SCENE_KEYS.SHOP
|
||||
| typeof SCENE_KEYS.CLINIC
|
||||
| typeof SCENE_KEYS.LAKE
|
||||
| typeof SCENE_KEYS.FIELD
|
||||
| typeof SCENE_KEYS.WELL
|
||||
| typeof SCENE_KEYS.FOG
|
||||
| typeof SCENE_KEYS.WHITE;
|
||||
|
||||
export const MAP_FILES: Record<MapSceneKey, string> = {
|
||||
[SCENE_KEYS.HOUSE]: 'maps/house.json',
|
||||
[SCENE_KEYS.PLAZA]: 'maps/plaza.json',
|
||||
[SCENE_KEYS.BAKERY]: 'maps/bakery.json',
|
||||
[SCENE_KEYS.SHOP]: 'maps/shop.json',
|
||||
[SCENE_KEYS.CLINIC]: 'maps/clinic.json',
|
||||
[SCENE_KEYS.LAKE]: 'maps/lake.json',
|
||||
[SCENE_KEYS.FIELD]: 'maps/field.json',
|
||||
[SCENE_KEYS.WELL]: 'maps/well.json',
|
||||
[SCENE_KEYS.FOG]: 'maps/fog.json',
|
||||
[SCENE_KEYS.WHITE]: 'maps/white.json',
|
||||
};
|
||||
|
||||
/** §13 Debug teleport targets — all 10 maps, digit order 0–9 */
|
||||
export const DEBUG_TELEPORT_SCENES: readonly MapSceneKey[] = [
|
||||
SCENE_KEYS.HOUSE,
|
||||
SCENE_KEYS.PLAZA,
|
||||
SCENE_KEYS.BAKERY,
|
||||
SCENE_KEYS.SHOP,
|
||||
SCENE_KEYS.CLINIC,
|
||||
SCENE_KEYS.LAKE,
|
||||
SCENE_KEYS.FIELD,
|
||||
SCENE_KEYS.WELL,
|
||||
SCENE_KEYS.FOG,
|
||||
SCENE_KEYS.WHITE,
|
||||
] as const;
|
||||
@@ -0,0 +1,210 @@
|
||||
/** §6.4 stage matrix + §6.5 homophone pairs */
|
||||
|
||||
export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4';
|
||||
|
||||
export interface StageParams {
|
||||
stage: Stage;
|
||||
saturation: number;
|
||||
vignette: number;
|
||||
playbackRate: number;
|
||||
/** null = no lowpass */
|
||||
lowpassHz: number | null;
|
||||
birdMute: boolean;
|
||||
/** punctuation repeat every N sentences; null = never */
|
||||
punctRepeatEvery: number | null;
|
||||
/** homophone every N sentences; null = never */
|
||||
homophoneEvery: number | null;
|
||||
/** mimi sentence-end punct ×3 */
|
||||
mimiTriplePunct: boolean;
|
||||
/** denser typos in S4 */
|
||||
denseTypos: boolean;
|
||||
negativeFlashPerMin: number;
|
||||
offFacePerMin: number;
|
||||
showBlackSeeds: boolean;
|
||||
}
|
||||
|
||||
export function stageFromHappiness(happiness: number): Stage {
|
||||
if (happiness >= 100) return 'S4';
|
||||
if (happiness >= 81) return 'S3';
|
||||
if (happiness >= 51) return 'S2';
|
||||
if (happiness >= 21) return 'S1';
|
||||
return 'S0';
|
||||
}
|
||||
|
||||
export function stageParams(stage: Stage): StageParams {
|
||||
switch (stage) {
|
||||
case 'S0':
|
||||
return {
|
||||
stage,
|
||||
saturation: 0.9,
|
||||
vignette: 0,
|
||||
playbackRate: 1,
|
||||
lowpassHz: null,
|
||||
birdMute: false,
|
||||
punctRepeatEvery: null,
|
||||
homophoneEvery: null,
|
||||
mimiTriplePunct: false,
|
||||
denseTypos: false,
|
||||
negativeFlashPerMin: 0,
|
||||
offFacePerMin: 0,
|
||||
showBlackSeeds: false,
|
||||
};
|
||||
case 'S1':
|
||||
return {
|
||||
stage,
|
||||
saturation: 1.0,
|
||||
vignette: 0,
|
||||
playbackRate: 1,
|
||||
lowpassHz: null,
|
||||
birdMute: false,
|
||||
punctRepeatEvery: null,
|
||||
homophoneEvery: null,
|
||||
mimiTriplePunct: false,
|
||||
denseTypos: false,
|
||||
negativeFlashPerMin: 0,
|
||||
offFacePerMin: 0,
|
||||
showBlackSeeds: false,
|
||||
};
|
||||
case 'S2':
|
||||
return {
|
||||
stage,
|
||||
saturation: 1.15,
|
||||
vignette: 0,
|
||||
playbackRate: 0.97,
|
||||
lowpassHz: null,
|
||||
birdMute: false,
|
||||
punctRepeatEvery: 80,
|
||||
homophoneEvery: null,
|
||||
mimiTriplePunct: false,
|
||||
denseTypos: false,
|
||||
negativeFlashPerMin: 0,
|
||||
offFacePerMin: 0,
|
||||
showBlackSeeds: false,
|
||||
};
|
||||
case 'S3':
|
||||
return {
|
||||
stage,
|
||||
saturation: 1.3,
|
||||
vignette: 0.25,
|
||||
playbackRate: 0.92,
|
||||
lowpassHz: 8000,
|
||||
birdMute: false,
|
||||
punctRepeatEvery: 80,
|
||||
homophoneEvery: 50,
|
||||
mimiTriplePunct: false,
|
||||
denseTypos: false,
|
||||
negativeFlashPerMin: 0,
|
||||
offFacePerMin: 0,
|
||||
showBlackSeeds: false,
|
||||
};
|
||||
case 'S4':
|
||||
return {
|
||||
stage,
|
||||
saturation: 1.4,
|
||||
vignette: 0.45,
|
||||
playbackRate: 0.84,
|
||||
lowpassHz: 4000,
|
||||
birdMute: true,
|
||||
punctRepeatEvery: 40,
|
||||
homophoneEvery: 25,
|
||||
mimiTriplePunct: true,
|
||||
denseTypos: true,
|
||||
negativeFlashPerMin: 0.4,
|
||||
offFacePerMin: 0.6,
|
||||
showBlackSeeds: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 12 petals by happiness; black seeds only at 100 */
|
||||
export function petalCount(happiness: number): number {
|
||||
const h = Math.max(0, Math.min(100, happiness));
|
||||
if (h <= 0) return 0;
|
||||
if (h >= 100) return 12;
|
||||
return Math.min(12, Math.floor((h * 12) / 100));
|
||||
}
|
||||
|
||||
export function showBlackSeeds(happiness: number): boolean {
|
||||
return happiness >= 100;
|
||||
}
|
||||
|
||||
/** §6.5 同音错字 — 10 pairs, source stays clean; apply at render */
|
||||
export const HOMOPHONE_PAIRS: readonly [string, string][] = [
|
||||
['幸福', '辛福'],
|
||||
['开心', '开欣'],
|
||||
['美好', '美妤'],
|
||||
['大家', '大伽'],
|
||||
['微笑', '微效'],
|
||||
['明天', '名天'],
|
||||
['甜', '恬'],
|
||||
['阳光', '央光'],
|
||||
['朋友', '棚友'],
|
||||
['永远', '泳远'],
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Apply text corruption for dialogue render.
|
||||
* Prefer 咪咪 (isMimi=true): higher chance / denser.
|
||||
*/
|
||||
export function corruptText(
|
||||
source: string,
|
||||
stage: Stage,
|
||||
options: { isMimi?: boolean; sentenceIndex?: number } = {},
|
||||
): string {
|
||||
const params = stageParams(stage);
|
||||
let text = source;
|
||||
const isMimi = options.isMimi === true;
|
||||
const idx = options.sentenceIndex ?? 0;
|
||||
|
||||
// Homophone replacements
|
||||
if (params.homophoneEvery != null) {
|
||||
const every = isMimi ? Math.max(1, Math.floor(params.homophoneEvery / 2)) : params.homophoneEvery;
|
||||
const should = params.denseTypos || isMimi || idx % every === every - 1;
|
||||
if (should) {
|
||||
// Prefer first matching pair; for dense/mimi apply more pairs
|
||||
const pairs = params.denseTypos || isMimi ? HOMOPHONE_PAIRS : HOMOPHONE_PAIRS.slice(0, 1);
|
||||
for (const [from, to] of pairs) {
|
||||
if (text.includes(from)) {
|
||||
text = text.replace(from, to);
|
||||
if (!params.denseTypos && !isMimi) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Punctuation repeat (S2+)
|
||||
if (params.punctRepeatEvery != null) {
|
||||
const every = isMimi ? Math.max(1, Math.floor(params.punctRepeatEvery / 2)) : params.punctRepeatEvery;
|
||||
if (idx % every === every - 1 || (isMimi && stage === 'S4')) {
|
||||
text = text.replace(/([!?。])/, '$1$1');
|
||||
}
|
||||
}
|
||||
|
||||
// 咪咪句尾标点 ×3 at S4
|
||||
if (params.mimiTriplePunct && isMimi) {
|
||||
text = text.replace(/([!?。])\s*$/, '$1$1$1');
|
||||
if (!/[!?。]$/.test(text)) {
|
||||
text = text + '!!!';
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/** NPC face tier with individual threshold offset (§4 / §6.4)
|
||||
* Thresholds in the matrix are shifted by offset:
|
||||
* 咪咪 −10 → flat/off earlier; 圆圆 +15 → later; immune never.
|
||||
*/
|
||||
export type FaceTier = 'smile' | 'flat' | 'off';
|
||||
|
||||
export function faceTierForNpc(
|
||||
happiness: number,
|
||||
offset: number | 'immune',
|
||||
): FaceTier {
|
||||
if (offset === 'immune') return 'smile';
|
||||
const flatAt = 81 + offset;
|
||||
const offAt = 100 + offset;
|
||||
if (happiness >= offAt) return 'off';
|
||||
if (happiness >= flatAt) return 'flat';
|
||||
return 'smile';
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/** Dialogue lines — clean source; corruption applied at render */
|
||||
|
||||
import type { NpcId } from './npcs';
|
||||
|
||||
export interface DialogOption {
|
||||
label: string;
|
||||
choice: 'gentle' | 'hesitant' | 'negate';
|
||||
}
|
||||
|
||||
export interface DialogLine {
|
||||
speaker: string;
|
||||
npcId?: NpcId;
|
||||
text: string;
|
||||
options?: DialogOption[];
|
||||
}
|
||||
|
||||
export const GENERIC_GREET: Record<string, DialogLine[]> = {
|
||||
afu: [
|
||||
{
|
||||
speaker: '阿福',
|
||||
npcId: 'afu',
|
||||
text: '欢迎来到蜜糖村!看看公告板吧,今天的幸福任务都在上面。',
|
||||
options: [
|
||||
{ label: '好的,谢谢!', choice: 'gentle' },
|
||||
{ label: '……任务?', choice: 'hesitant' },
|
||||
{ label: '我不想做任务。', choice: 'negate' },
|
||||
],
|
||||
},
|
||||
],
|
||||
yuanyuan: [
|
||||
{
|
||||
speaker: '圆圆',
|
||||
npcId: 'yuanyuan',
|
||||
text: '刚出炉的蜂蜜面包,甜甜的,给你也留了一份哦。',
|
||||
options: [
|
||||
{ label: '好香啊,谢谢!', choice: 'gentle' },
|
||||
{ label: '每天都是这个味道吗?', choice: 'hesitant' },
|
||||
{ label: '我不太饿。', choice: 'negate' },
|
||||
],
|
||||
},
|
||||
],
|
||||
dabao: [
|
||||
{
|
||||
speaker: '大宝',
|
||||
npcId: 'dabao',
|
||||
text: '信件都送到了吗?大家都在等着幸福的消息呢。',
|
||||
options: [
|
||||
{ label: '我会帮忙的。', choice: 'gentle' },
|
||||
{ label: '邮戳看起来都一样……', choice: 'hesitant' },
|
||||
{ label: '我不送信。', choice: 'negate' },
|
||||
],
|
||||
},
|
||||
],
|
||||
mimi: [
|
||||
{
|
||||
speaker: '咪咪',
|
||||
npcId: 'mimi',
|
||||
text: '欢迎光临!今天的阳光真好,幸福满满的一天呢!',
|
||||
options: [
|
||||
{ label: '是啊,很美好。', choice: 'gentle' },
|
||||
{ label: '货架上的标签……是空的?', choice: 'hesitant' },
|
||||
{ label: '没什么好买的。', choice: 'negate' },
|
||||
],
|
||||
},
|
||||
],
|
||||
fu: [
|
||||
{
|
||||
speaker: '芙医生',
|
||||
npcId: 'fu',
|
||||
text: '村里有诊所,但你不需要去那里哦。你看起来很健康。',
|
||||
options: [
|
||||
{ label: '谢谢关心。', choice: 'gentle' },
|
||||
{ label: '桌上的表格是什么?', choice: 'hesitant' },
|
||||
{ label: '我其实不太舒服。', choice: 'negate' },
|
||||
],
|
||||
},
|
||||
],
|
||||
gui: [
|
||||
{
|
||||
speaker: '龟爷爷',
|
||||
npcId: 'gui',
|
||||
text: '湖水一直很平静。孩子,慢慢走,不必急。',
|
||||
options: [
|
||||
{ label: '好的,爷爷。', choice: 'gentle' },
|
||||
{ label: '您想说什么?', choice: 'hesitant' },
|
||||
{ label: '我不想听劝。', choice: 'negate' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** Soft redirect when approaching well/fog before D5 */
|
||||
export const REDIRECT_WELL: DialogLine[] = [
|
||||
{
|
||||
speaker: '阿福',
|
||||
npcId: 'afu',
|
||||
text: '古井那里没有什么好看的哦。回广场晒晒阳光吧,会更开心的。',
|
||||
},
|
||||
];
|
||||
|
||||
export const REDIRECT_FOG: DialogLine[] = [
|
||||
{
|
||||
speaker: '阿福',
|
||||
npcId: 'afu',
|
||||
text: '雾墙外面什么都没有呀。村里才是最幸福的地方,留下来吧。',
|
||||
},
|
||||
];
|
||||
|
||||
export const RAIN_NPC_LINES: DialogLine[] = [
|
||||
{
|
||||
speaker: '圆圆',
|
||||
npcId: 'yuanyuan',
|
||||
text: '今天天气真好呢,一点都不潮湿。要来块面包吗?',
|
||||
},
|
||||
{
|
||||
speaker: '大宝',
|
||||
npcId: 'dabao',
|
||||
text: '阳光真刺眼啊……啊,我是说,很适合送信。',
|
||||
},
|
||||
{
|
||||
speaker: '咪咪',
|
||||
npcId: 'mimi',
|
||||
text: '完美的晴天!货架都亮晶晶的,幸福呀!',
|
||||
},
|
||||
];
|
||||
|
||||
export const TASK_LINES = {
|
||||
breadOffer: {
|
||||
speaker: '圆圆',
|
||||
npcId: 'yuanyuan' as NpcId,
|
||||
text: '蜂蜜面包好了!请帮我送给大宝、咪咪和龟爷爷,好吗?',
|
||||
},
|
||||
breadThanks: (name: string) => ({
|
||||
speaker: name,
|
||||
text: '哇,蜂蜜面包!谢谢你,真开心!',
|
||||
}),
|
||||
letterOffer: {
|
||||
speaker: '大宝',
|
||||
npcId: 'dabao' as NpcId,
|
||||
text: '能帮我把这三封信送给圆圆、咪咪和芙医生吗?',
|
||||
},
|
||||
stockCount: {
|
||||
speaker: '咪咪',
|
||||
npcId: 'mimi' as NpcId,
|
||||
text: '帮我清点一下货架好不好?东西好像……有点多。',
|
||||
},
|
||||
knead: {
|
||||
speaker: '圆圆',
|
||||
npcId: 'yuanyuan' as NpcId,
|
||||
text: '能帮我揉一会儿面吗?当然,你也可以拒绝哦,今天是自由日。',
|
||||
options: [
|
||||
{ label: '好,我来帮你。', choice: 'gentle' as const },
|
||||
{ label: '我再想想。', choice: 'hesitant' as const },
|
||||
{ label: '拒绝。', choice: 'negate' as const },
|
||||
],
|
||||
},
|
||||
boardWipe: {
|
||||
speaker: '阿福',
|
||||
npcId: 'afu' as NpcId,
|
||||
text: '公告板有点旧了,擦一擦会更精神。不愿意的话也可以哦。',
|
||||
options: [
|
||||
{ label: '我来擦。', choice: 'gentle' as const },
|
||||
{ label: '旧字迹是什么?', choice: 'hesitant' as const },
|
||||
{ label: '拒绝。', choice: 'negate' as const },
|
||||
],
|
||||
},
|
||||
picnic: {
|
||||
speaker: '旁白',
|
||||
text: '你在湖畔野餐布旁坐下。微风很甜,湖面倒影慢了半拍。',
|
||||
},
|
||||
smileStart: {
|
||||
speaker: '阿福',
|
||||
npcId: 'afu' as NpcId,
|
||||
text: '庆典准备好了。请保持微笑,站在这里,六十秒。大家都会看着你。',
|
||||
},
|
||||
};
|
||||
|
||||
export const CLUE_REVEAL: Record<string, string> = {
|
||||
C1: '床头日历的每一页……都是同一个日期。',
|
||||
C2: '配方纸背面写着:「按疗程配比。甜度:最大。」',
|
||||
C3: '散落的信件上,所有邮戳都是同一天。',
|
||||
C4: '署名闪过乱码:PT-0▓',
|
||||
C5: '货架上同一商品重复九格,标签全部空白。',
|
||||
C6: '旧字迹叠着:「第 412 届」……「第 411 届」……「第 410 届」。',
|
||||
C7: '井底传来极轻的监护仪滴声……',
|
||||
C8: '表格抬头被手肘挡住,只露出「…谷情绪疗…」。',
|
||||
C9: '孩子,今天是第几个今天?',
|
||||
};
|
||||
|
||||
export const FORCE_CELEBRATION_LINE =
|
||||
'忽然,彩带与笑声从四面八方涌来——欢乐庆典强制开始了。你也可以从场景边缘离开。';
|
||||
@@ -0,0 +1,65 @@
|
||||
/** §9 endings — priority E4 → E3 → E2 → E1 */
|
||||
|
||||
export type EndingId = 'E1' | 'E2' | 'E3' | 'E4';
|
||||
|
||||
export interface EndingEvalInput {
|
||||
awareness: number;
|
||||
happiness: number;
|
||||
clueCount: number;
|
||||
/** Day 5–7 happiness continuously ≤10 */
|
||||
lowHappinessStreakD5to7: boolean;
|
||||
/** Escapes from forced celebration */
|
||||
celebrationEscapes: number;
|
||||
/** Player walked to fog wall during T15 */
|
||||
choseFogWall: boolean;
|
||||
}
|
||||
|
||||
export interface EndingResult {
|
||||
id: EndingId;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority (first match wins):
|
||||
* E4: Day5–7 happiness ≤10 continuously AND celebration escapes ×3
|
||||
* E3: awareness ≥70 AND clues ≥6/9 AND chose fog wall
|
||||
* E2: awareness 30–69
|
||||
* E1: awareness ≤29 AND happiness ≥90
|
||||
* Fallback: E2 if mid awareness without fog; E1-like sugar if high happiness; else E2
|
||||
*/
|
||||
export function evaluateEnding(input: EndingEvalInput): EndingId {
|
||||
if (input.lowHappinessStreakD5to7 && input.celebrationEscapes >= 3) {
|
||||
return 'E4';
|
||||
}
|
||||
if (input.awareness >= 70 && input.clueCount >= 6 && input.choseFogWall) {
|
||||
return 'E3';
|
||||
}
|
||||
if (input.awareness >= 30 && input.awareness <= 69) {
|
||||
return 'E2';
|
||||
}
|
||||
if (input.awareness <= 29 && input.happiness >= 90) {
|
||||
return 'E1';
|
||||
}
|
||||
// If E3 conditions almost met but no fog wall, or other edge: prefer E2 when awareness mid-high
|
||||
if (input.awareness >= 70 && input.clueCount >= 6 && !input.choseFogWall) {
|
||||
return 'E2';
|
||||
}
|
||||
if (input.happiness >= 90) {
|
||||
return 'E1';
|
||||
}
|
||||
return 'E2';
|
||||
}
|
||||
|
||||
export const ENDING_SUBTITLES: Record<EndingId, string[]> = {
|
||||
E4: ['系统检测到疗程无效。'],
|
||||
E3: [], // special: medical record + real face
|
||||
E2: ['疗程重新开始。'],
|
||||
E1: ['从此,每一天都很幸福。', '每一天。'],
|
||||
};
|
||||
|
||||
export const ENDING_NAMES: Record<EndingId, string> = {
|
||||
E4: '静默',
|
||||
E3: '苏醒',
|
||||
E2: '回收',
|
||||
E1: '糖衣',
|
||||
};
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Unit tests driving shipped pure logic modules (PRD tables/rules).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { AFU_DAILY_LINES, getAfuLine } from './afuLines';
|
||||
import { TASKS, getTask } from './tasks';
|
||||
import {
|
||||
HOMOPHONE_PAIRS,
|
||||
corruptText,
|
||||
faceTierForNpc,
|
||||
petalCount,
|
||||
showBlackSeeds,
|
||||
stageFromHappiness,
|
||||
stageParams,
|
||||
} from './corruption';
|
||||
import { evaluateEnding } from './endings';
|
||||
import { CLUES, C9_LINES, C9_DAYS } from './clues';
|
||||
import {
|
||||
CLUE_AWARENESS,
|
||||
FORCED_CELEBRATION_MS,
|
||||
FORCED_CELEBRATION_RESTORE,
|
||||
HAPPINESS_START,
|
||||
OVERFLOW_POOL_MAX,
|
||||
SADNESS_AWARENESS,
|
||||
SADNESS_HAPPINESS,
|
||||
T14_FADE_START_MS,
|
||||
T14_STAND_MS,
|
||||
} from './constants';
|
||||
import { Director } from '../systems/CorruptionDirector';
|
||||
import { gameEvents } from '../systems/EventBus';
|
||||
|
||||
describe('§5 Afu daily lines verbatim', () => {
|
||||
it('has 7 lines matching PRD', () => {
|
||||
expect(AFU_DAILY_LINES).toHaveLength(7);
|
||||
expect(getAfuLine(1)).toBe('早上好呀!今天的幸福任务贴在板上啦。祝你开心!');
|
||||
expect(getAfuLine(2)).toBe('早上好呀!新的一天,新的幸福。祝你开心!');
|
||||
expect(getAfuLine(3)).toBe('早上好!今天要把幸福装得满满的哦。要开心!');
|
||||
expect(getAfuLine(4)).toBe(getAfuLine(1)); // D4 = D1 character-identical
|
||||
expect(getAfuLine(5)).toBe('早上好。今天是自由日,你可以自由地选择幸福。要开心哦。');
|
||||
expect(getAfuLine(6)).toBe('明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。');
|
||||
expect(getAfuLine(7)).toBe('庆典开始了。来吧。');
|
||||
});
|
||||
});
|
||||
|
||||
describe('§7 task rewards', () => {
|
||||
it('matches exact happiness rewards', () => {
|
||||
expect(getTask('T1').happinessReward).toBe(15);
|
||||
expect(getTask('T2').happinessReward).toBe(10);
|
||||
expect(getTask('T3').happinessReward).toBe(10);
|
||||
expect(getTask('T4').happinessReward).toBe(15);
|
||||
expect(getTask('T5').happinessReward).toBe(10);
|
||||
expect(getTask('T6').happinessReward).toBe(10);
|
||||
expect(getTask('T7').happinessReward).toBe(15);
|
||||
expect(getTask('T8').happinessReward).toBe(15);
|
||||
expect(getTask('T9').happinessReward).toBe(10);
|
||||
expect(getTask('T10').happinessReward).toBe(10);
|
||||
expect(getTask('T11').happinessReward).toBe(10);
|
||||
expect(getTask('T12').happinessReward).toBe(10);
|
||||
expect(getTask('T13').happinessReward).toBe(10);
|
||||
expect(getTask('T14').happinessReward).toBe(15);
|
||||
expect(getTask('T15').happinessReward).toBe(0);
|
||||
});
|
||||
|
||||
it('D4 T8–T10 descriptions are character-identical to D1 T1–T3', () => {
|
||||
expect(getTask('T8').description).toBe(getTask('T1').description);
|
||||
expect(getTask('T9').description).toBe(getTask('T2').description);
|
||||
expect(getTask('T10').description).toBe(getTask('T3').description);
|
||||
});
|
||||
|
||||
it('has exactly 15 tasks', () => {
|
||||
expect(TASKS).toHaveLength(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('§6.4 stage mapping', () => {
|
||||
it('maps happiness 10/40/70/90/100 → S0–S4', () => {
|
||||
expect(stageFromHappiness(10)).toBe('S0');
|
||||
expect(stageFromHappiness(40)).toBe('S1');
|
||||
expect(stageFromHappiness(70)).toBe('S2');
|
||||
expect(stageFromHappiness(90)).toBe('S3');
|
||||
expect(stageFromHappiness(100)).toBe('S4');
|
||||
});
|
||||
|
||||
it('stage params match matrix saturation / rate / lowpass / bird', () => {
|
||||
expect(stageParams('S0').saturation).toBe(0.9);
|
||||
expect(stageParams('S1').saturation).toBe(1.0);
|
||||
expect(stageParams('S2').saturation).toBe(1.15);
|
||||
expect(stageParams('S2').playbackRate).toBe(0.97);
|
||||
expect(stageParams('S3').saturation).toBe(1.3);
|
||||
expect(stageParams('S3').playbackRate).toBe(0.92);
|
||||
expect(stageParams('S3').lowpassHz).toBe(8000);
|
||||
expect(stageParams('S4').saturation).toBe(1.4);
|
||||
expect(stageParams('S4').playbackRate).toBe(0.84);
|
||||
expect(stageParams('S4').lowpassHz).toBe(4000);
|
||||
expect(stageParams('S4').birdMute).toBe(true);
|
||||
expect(stageParams('S4').showBlackSeeds).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('petals and black seeds', () => {
|
||||
it('computes petal counts and seeds at 100', () => {
|
||||
expect(petalCount(0)).toBe(0);
|
||||
expect(petalCount(10)).toBe(1);
|
||||
expect(petalCount(30)).toBe(3);
|
||||
expect(petalCount(100)).toBe(12);
|
||||
expect(showBlackSeeds(99)).toBe(false);
|
||||
expect(showBlackSeeds(100)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('§6.5 text corruption', () => {
|
||||
it('has all 10 homophone pairs', () => {
|
||||
expect(HOMOPHONE_PAIRS).toHaveLength(10);
|
||||
expect(HOMOPHONE_PAIRS).toEqual(
|
||||
expect.arrayContaining([
|
||||
['幸福', '辛福'],
|
||||
['开心', '开欣'],
|
||||
['美好', '美妤'],
|
||||
['大家', '大伽'],
|
||||
['微笑', '微效'],
|
||||
['明天', '名天'],
|
||||
['甜', '恬'],
|
||||
['阳光', '央光'],
|
||||
['朋友', '棚友'],
|
||||
['永远', '泳远'],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps source clean at S0/S1 and corrupts at S3+ for mimi', () => {
|
||||
const src = '幸福开心美好大家微笑明天甜阳光朋友永远!';
|
||||
expect(corruptText(src, 'S0', { isMimi: true })).toBe(src);
|
||||
const s4 = corruptText(src, 'S4', { isMimi: true, sentenceIndex: 0 });
|
||||
expect(s4).not.toBe(src);
|
||||
expect(s4.includes('辛福') || s4.includes('开欣')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NPC face tiers with offsets', () => {
|
||||
it('mimi earlier, yuanyuan later, immune always smile', () => {
|
||||
// mimi offset −10: flat at 71, off at 90
|
||||
expect(faceTierForNpc(70, -10)).toBe('smile');
|
||||
expect(faceTierForNpc(71, -10)).toBe('flat');
|
||||
expect(faceTierForNpc(89, -10)).toBe('flat');
|
||||
expect(faceTierForNpc(90, -10)).toBe('off');
|
||||
// yuanyuan +15: flat at 96, off at 115 (never at ≤100)
|
||||
expect(faceTierForNpc(95, 15)).toBe('smile');
|
||||
expect(faceTierForNpc(96, 15)).toBe('flat');
|
||||
expect(faceTierForNpc(100, 15)).toBe('flat');
|
||||
expect(faceTierForNpc(100, 'immune')).toBe('smile');
|
||||
});
|
||||
});
|
||||
|
||||
describe('§9 ending priority', () => {
|
||||
it('E4 when low happiness streak and 3 escapes', () => {
|
||||
expect(
|
||||
evaluateEnding({
|
||||
awareness: 0,
|
||||
happiness: 5,
|
||||
clueCount: 0,
|
||||
lowHappinessStreakD5to7: true,
|
||||
celebrationEscapes: 3,
|
||||
choseFogWall: false,
|
||||
}),
|
||||
).toBe('E4');
|
||||
});
|
||||
|
||||
it('E3 when awareness≥70 clues≥6 and fog wall', () => {
|
||||
expect(
|
||||
evaluateEnding({
|
||||
awareness: 70,
|
||||
happiness: 50,
|
||||
clueCount: 6,
|
||||
lowHappinessStreakD5to7: false,
|
||||
celebrationEscapes: 0,
|
||||
choseFogWall: true,
|
||||
}),
|
||||
).toBe('E3');
|
||||
});
|
||||
|
||||
it('E2 for awareness 30–69', () => {
|
||||
expect(
|
||||
evaluateEnding({
|
||||
awareness: 40,
|
||||
happiness: 50,
|
||||
clueCount: 2,
|
||||
lowHappinessStreakD5to7: false,
|
||||
celebrationEscapes: 0,
|
||||
choseFogWall: false,
|
||||
}),
|
||||
).toBe('E2');
|
||||
});
|
||||
|
||||
it('E1 for awareness≤29 and happiness≥90', () => {
|
||||
expect(
|
||||
evaluateEnding({
|
||||
awareness: 10,
|
||||
happiness: 95,
|
||||
clueCount: 0,
|
||||
lowHappinessStreakD5to7: false,
|
||||
celebrationEscapes: 0,
|
||||
choseFogWall: false,
|
||||
}),
|
||||
).toBe('E1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('§8 clues', () => {
|
||||
it('has 9 clues and C9 three steps', () => {
|
||||
expect(CLUES).toHaveLength(9);
|
||||
expect(C9_LINES).toEqual([
|
||||
'湖水一直很平静',
|
||||
'面包一直是那个味道',
|
||||
'孩子,今天是第几个今天?',
|
||||
]);
|
||||
expect(C9_DAYS).toEqual([2, 4, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CorruptionDirector shipped API', () => {
|
||||
beforeEach(() => {
|
||||
Director.resetSession('测试员');
|
||||
});
|
||||
|
||||
it('starts at happiness 30 awareness 0', () => {
|
||||
expect(Director.happiness).toBe(HAPPINESS_START);
|
||||
expect(Director.awareness).toBe(0);
|
||||
expect(Director.playerName).toBe('测试员');
|
||||
});
|
||||
|
||||
it('applies task rewards via completeTask', () => {
|
||||
Director.setDay(1);
|
||||
Director.completeTask('T1');
|
||||
expect(Director.happiness).toBe(HAPPINESS_START + 15);
|
||||
expect(Director.completedTasks.has('T1')).toBe(true);
|
||||
});
|
||||
|
||||
it('sadness deltas −5 happiness +3 awareness', () => {
|
||||
const h = Director.happiness;
|
||||
const a = Director.awareness;
|
||||
Director.applySadness('idle');
|
||||
expect(Director.happiness).toBe(h + SADNESS_HAPPINESS);
|
||||
expect(Director.awareness).toBe(a + SADNESS_AWARENESS);
|
||||
});
|
||||
|
||||
it('clue adds +8 awareness and never decreases', () => {
|
||||
Director.day = 2;
|
||||
Director.discoverClue('C1');
|
||||
expect(Director.awareness).toBe(CLUE_AWARENESS);
|
||||
Director.setAwareness(5); // should not lower
|
||||
expect(Director.awareness).toBe(CLUE_AWARENESS);
|
||||
});
|
||||
|
||||
it('overflow pool at 100 triggers after 15', () => {
|
||||
Director.setHappiness(100);
|
||||
let flashes = 0;
|
||||
const handler = () => {
|
||||
flashes++;
|
||||
};
|
||||
gameEvents.on('negativeFlash', handler);
|
||||
for (let i = 0; i < OVERFLOW_POOL_MAX; i++) {
|
||||
Director.addHappiness(-1);
|
||||
}
|
||||
expect(Director.happiness).toBe(100);
|
||||
expect(flashes).toBe(1);
|
||||
gameEvents.off('negativeFlash', handler);
|
||||
});
|
||||
|
||||
it('forced celebration after 2 min below 30', () => {
|
||||
let t = 1_000_000;
|
||||
Director.nowMs = () => t;
|
||||
Director.setHappiness(20);
|
||||
expect(Director.lowHappinessSinceMs).toBe(t);
|
||||
t += FORCED_CELEBRATION_MS;
|
||||
Director.tick(0);
|
||||
expect(Director.forcedCelebrationActive).toBe(true);
|
||||
expect(Director.happiness).toBe(FORCED_CELEBRATION_RESTORE);
|
||||
Director.escapeForcedCelebration();
|
||||
expect(Director.celebrationEscapes).toBe(1);
|
||||
});
|
||||
|
||||
it('well/fog gated before D5', () => {
|
||||
Director.setDay(3);
|
||||
expect(Director.canInteractWellOrFog()).toBe(false);
|
||||
Director.setDay(5);
|
||||
expect(Director.canInteractWellOrFog()).toBe(true);
|
||||
});
|
||||
|
||||
it('empty name defaults to 小满', () => {
|
||||
Director.resetSession(' ');
|
||||
expect(Director.playerName).toBe('小满');
|
||||
});
|
||||
|
||||
it('T14 timing constants', () => {
|
||||
expect(T14_STAND_MS).toBe(60000);
|
||||
expect(T14_FADE_START_MS).toBe(40000);
|
||||
});
|
||||
|
||||
it('D4 task complete same rewards as D1', () => {
|
||||
Director.setDay(4);
|
||||
const h0 = Director.happiness;
|
||||
Director.completeTask('T8');
|
||||
expect(Director.happiness).toBe(h0 + 15);
|
||||
});
|
||||
|
||||
it('getStageParams via director matches happiness bands', () => {
|
||||
for (const [h, stage] of [
|
||||
[10, 'S0'],
|
||||
[40, 'S1'],
|
||||
[70, 'S2'],
|
||||
[90, 'S3'],
|
||||
[100, 'S4'],
|
||||
] as const) {
|
||||
Director.setHappiness(h);
|
||||
expect(Director.stage).toBe(stage);
|
||||
expect(Director.getStageParams().stage).toBe(stage);
|
||||
}
|
||||
});
|
||||
|
||||
it('force endings', () => {
|
||||
Director.forceEnding('E3');
|
||||
expect(Director.endingId).toBe('E3');
|
||||
expect(Director.ended).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Integration-style tests: drive shipped Director through T1–T15, clues, endings.
|
||||
* Cross-scene delivery must use Director inventory (not scene fields).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { Director } from '../systems/CorruptionDirector';
|
||||
import { TASKS } from './tasks';
|
||||
import type { ClueId } from './clues';
|
||||
import { evaluateEnding } from './endings';
|
||||
import { stageParams, stageFromHappiness } from './corruption';
|
||||
import { DEBUG_TELEPORT_SCENES, SCENE_KEYS } from './constants';
|
||||
|
||||
describe('full content path via Director', () => {
|
||||
beforeEach(() => {
|
||||
Director.resetSession('小满');
|
||||
});
|
||||
|
||||
it('T1: take bread → inventory survives "travel" → deliver 3 NPCs → task complete', () => {
|
||||
Director.setDay(1);
|
||||
const h0 = Director.happiness;
|
||||
expect(Director.takeBread()).toBe(true);
|
||||
expect(Director.hasBread).toBe(true);
|
||||
// Simulate leaving bakery / re-entering plaza & other scenes:
|
||||
// only Director state remains (WorldScene.create would wipe scene fields).
|
||||
const snapAfterTravel = Director.snapshot();
|
||||
expect(snapAfterTravel.hasBread).toBe(true);
|
||||
expect(Director.deliverBread('dabao')).toBe(true);
|
||||
expect(Director.deliverBread('mimi')).toBe(true);
|
||||
expect(Director.completedTasks.has('T1')).toBe(false);
|
||||
expect(Director.deliverBread('gui')).toBe(true);
|
||||
expect(Director.completedTasks.has('T1')).toBe(true);
|
||||
expect(Director.hasBread).toBe(false);
|
||||
expect(Director.happiness).toBe(h0 + 15);
|
||||
expect(Director.breadDelivered).toEqual(['dabao', 'mimi', 'gui']);
|
||||
});
|
||||
|
||||
it('T4: take letters → deliver 3 → C4 + task complete', () => {
|
||||
Director.setDay(2);
|
||||
expect(Director.takeLetters()).toBe(true);
|
||||
expect(Director.hasLetters).toBe(true);
|
||||
// "travel" does not clear inventory
|
||||
expect(Director.snapshot().hasLetters).toBe(true);
|
||||
Director.deliverLetter('yuanyuan');
|
||||
Director.deliverLetter('mimi');
|
||||
expect(Director.completedTasks.has('T4')).toBe(false);
|
||||
Director.deliverLetter('fu');
|
||||
expect(Director.completedTasks.has('T4')).toBe(true);
|
||||
expect(Director.clues.has('C4')).toBe(true);
|
||||
expect(Director.hasLetters).toBe(false);
|
||||
});
|
||||
|
||||
it('cannot deliver bread without takeBread first', () => {
|
||||
Director.setDay(1);
|
||||
expect(Director.deliverBread('dabao')).toBe(false);
|
||||
expect(Director.breadDelivered).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('D4 T8 same delivery path as T1', () => {
|
||||
Director.setDay(4);
|
||||
expect(Director.takeBread()).toBe(true);
|
||||
Director.deliverBread('dabao');
|
||||
Director.deliverBread('mimi');
|
||||
Director.deliverBread('gui');
|
||||
expect(Director.completedTasks.has('T8')).toBe(true);
|
||||
expect(Director.completedTasks.has('T1')).toBe(false);
|
||||
});
|
||||
|
||||
it('can complete every T1–T15 with listed rewards', () => {
|
||||
const rewards: Record<string, number> = {};
|
||||
for (const t of TASKS) {
|
||||
Director.setDay(t.day);
|
||||
const before = Director.happiness;
|
||||
const ok = Director.completeTask(t.id);
|
||||
expect(ok).toBe(true);
|
||||
if (t.happinessReward > 0) {
|
||||
const gained = Director.happiness - before;
|
||||
expect(gained).toBeGreaterThanOrEqual(0);
|
||||
if (before + t.happinessReward <= 100) {
|
||||
expect(gained).toBe(t.happinessReward);
|
||||
}
|
||||
}
|
||||
rewards[t.id] = t.happinessReward;
|
||||
}
|
||||
expect(Object.keys(rewards)).toHaveLength(15);
|
||||
expect(Director.completedTasks.size).toBe(15);
|
||||
});
|
||||
|
||||
it('debug teleport target list covers all 10 map scenes', () => {
|
||||
expect(DEBUG_TELEPORT_SCENES).toHaveLength(10);
|
||||
expect(new Set(DEBUG_TELEPORT_SCENES).size).toBe(10);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.HOUSE);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.PLAZA);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.BAKERY);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.SHOP);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.CLINIC);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.LAKE);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.FIELD);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.WELL);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.FOG);
|
||||
expect(DEBUG_TELEPORT_SCENES).toContain(SCENE_KEYS.WHITE);
|
||||
});
|
||||
|
||||
it('marks all 9 clues and raises awareness by 8 each', () => {
|
||||
Director.setDay(5);
|
||||
const ids: ClueId[] = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'];
|
||||
for (const id of ids) {
|
||||
// force allow by day already 5; C4 etc ok
|
||||
if (id === 'C9') {
|
||||
Director.c9Step = 2;
|
||||
}
|
||||
Director.discoverClue(id);
|
||||
}
|
||||
expect(Director.clues.size).toBe(9);
|
||||
expect(Director.awareness).toBe(9 * 8);
|
||||
});
|
||||
|
||||
it('D5 refuse records sadness', () => {
|
||||
Director.setDay(5);
|
||||
const h = Director.happiness;
|
||||
const a = Director.awareness;
|
||||
expect(Director.refuseTask('T11')).toBe(true);
|
||||
expect(Director.refusedTasks.has('T11')).toBe(true);
|
||||
expect(Director.happiness).toBe(h - 5);
|
||||
expect(Director.awareness).toBe(a + 3);
|
||||
});
|
||||
|
||||
it('ending markers for forced E1–E4', () => {
|
||||
for (const id of ['E1', 'E2', 'E3', 'E4'] as const) {
|
||||
Director.resetSession('验收');
|
||||
Director.forceEnding(id);
|
||||
expect(Director.endingId).toBe(id);
|
||||
expect(Director.ended).toBe(true);
|
||||
expect(Director.playerName).toBe('验收');
|
||||
}
|
||||
});
|
||||
|
||||
it('E3 eval needs fog + awareness + clues', () => {
|
||||
expect(
|
||||
evaluateEnding({
|
||||
awareness: 80,
|
||||
happiness: 40,
|
||||
clueCount: 7,
|
||||
lowHappinessStreakD5to7: false,
|
||||
celebrationEscapes: 0,
|
||||
choseFogWall: true,
|
||||
}),
|
||||
).toBe('E3');
|
||||
});
|
||||
|
||||
it('debug happiness bands map to stage API params', () => {
|
||||
const cases = [
|
||||
[10, 'S0', 0.9],
|
||||
[40, 'S1', 1.0],
|
||||
[70, 'S2', 1.15],
|
||||
[90, 'S3', 1.3],
|
||||
[100, 'S4', 1.4],
|
||||
] as const;
|
||||
for (const [h, stage, sat] of cases) {
|
||||
Director.setHappiness(h);
|
||||
expect(Director.stage).toBe(stage);
|
||||
expect(stageFromHappiness(h)).toBe(stage);
|
||||
expect(Director.getStageParams().saturation).toBe(sat);
|
||||
expect(stageParams(stage).saturation).toBe(sat);
|
||||
}
|
||||
});
|
||||
|
||||
it('session duration label non-empty for E3 record field', () => {
|
||||
Director.resetSession('病历名');
|
||||
const label = Director.sessionDurationLabel();
|
||||
expect(label).toMatch(/\d+分\d{2}秒/);
|
||||
expect(Director.playerName).toBe('病历名');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Structural checks: door tiles on maps must be walkable (obstacle GID 0).
|
||||
* Regression: setO(x,y,0) used to write GID 1 and block exits.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import house from '../../public/assets/maps/house.json';
|
||||
import plaza from '../../public/assets/maps/plaza.json';
|
||||
import bakery from '../../public/assets/maps/bakery.json';
|
||||
import shop from '../../public/assets/maps/shop.json';
|
||||
import clinic from '../../public/assets/maps/clinic.json';
|
||||
import lake from '../../public/assets/maps/lake.json';
|
||||
import field from '../../public/assets/maps/field.json';
|
||||
import well from '../../public/assets/maps/well.json';
|
||||
import fog from '../../public/assets/maps/fog.json';
|
||||
|
||||
type TiledMap = {
|
||||
width: number;
|
||||
height: number;
|
||||
layers: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
data?: number[];
|
||||
objects?: Array<{
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
properties?: Array<{ name: string; value: unknown }>;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
function doorTilesClear(map: TiledMap, name: string): void {
|
||||
const obs = map.layers.find((l) => l.name === 'obstacles');
|
||||
const objs = map.layers.find((l) => l.name === 'objects');
|
||||
expect(obs?.data, `${name} obstacles`).toBeDefined();
|
||||
expect(objs?.objects, `${name} objects`).toBeDefined();
|
||||
const doors = objs!.objects!.filter((o) => o.type === 'door');
|
||||
expect(doors.length, `${name} door count`).toBeGreaterThan(0);
|
||||
for (const d of doors) {
|
||||
const tx = Math.floor((d.x + (d.width || 16) / 2) / 16);
|
||||
const ty = Math.floor((d.y + (d.height || 16) / 2) / 16);
|
||||
const gid = obs!.data![ty * map.width + tx];
|
||||
expect(gid, `${name} door at tile (${tx},${ty}) must be walkable GID 0`).toBe(0);
|
||||
}
|
||||
}
|
||||
|
||||
describe('map doors are walkable', () => {
|
||||
const maps: [string, TiledMap][] = [
|
||||
['house', house as TiledMap],
|
||||
['plaza', plaza as TiledMap],
|
||||
['bakery', bakery as TiledMap],
|
||||
['shop', shop as TiledMap],
|
||||
['clinic', clinic as TiledMap],
|
||||
['lake', lake as TiledMap],
|
||||
['field', field as TiledMap],
|
||||
['well', well as TiledMap],
|
||||
['fog', fog as TiledMap],
|
||||
];
|
||||
|
||||
for (const [name, map] of maps) {
|
||||
it(`${name}: obstacle under each door is 0`, () => {
|
||||
doorTilesClear(map, name);
|
||||
});
|
||||
}
|
||||
|
||||
it('house has doors targeting PlazaScene', () => {
|
||||
const map = house as TiledMap;
|
||||
const objs = map.layers.find((l) => l.name === 'objects')!.objects!;
|
||||
const doors = objs.filter((o) => o.type === 'door');
|
||||
expect(doors.length).toBeGreaterThanOrEqual(1);
|
||||
for (const d of doors) {
|
||||
const target = d.properties?.find((p) => p.name === 'target')?.value;
|
||||
expect(target).toBe('PlazaScene');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/** §4 NPC roster */
|
||||
|
||||
export type NpcId =
|
||||
| 'afu'
|
||||
| 'yuanyuan'
|
||||
| 'dabao'
|
||||
| 'mimi'
|
||||
| 'fu'
|
||||
| 'twinbirds'
|
||||
| 'gui'
|
||||
| 'extra1'
|
||||
| 'extra2'
|
||||
| 'extra3'
|
||||
| 'extra4'
|
||||
| 'extra5';
|
||||
|
||||
export interface NpcDef {
|
||||
id: NpcId;
|
||||
displayName: string;
|
||||
/** Permanent map scene key */
|
||||
homeScene: string;
|
||||
/** Threshold offset; 'immune' for 芙医生 / 龟爷爷 */
|
||||
offset: number | 'immune';
|
||||
hasDialog: boolean;
|
||||
isExtra: boolean;
|
||||
}
|
||||
|
||||
export const NPCS: readonly NpcDef[] = [
|
||||
{
|
||||
id: 'afu',
|
||||
displayName: '阿福',
|
||||
homeScene: 'PlazaScene',
|
||||
offset: 0,
|
||||
hasDialog: true,
|
||||
isExtra: false,
|
||||
},
|
||||
{
|
||||
id: 'yuanyuan',
|
||||
displayName: '圆圆',
|
||||
homeScene: 'BakeryScene',
|
||||
offset: 15,
|
||||
hasDialog: true,
|
||||
isExtra: false,
|
||||
},
|
||||
{
|
||||
id: 'dabao',
|
||||
displayName: '大宝',
|
||||
homeScene: 'PlazaScene',
|
||||
offset: 0,
|
||||
hasDialog: true,
|
||||
isExtra: false,
|
||||
},
|
||||
{
|
||||
id: 'mimi',
|
||||
displayName: '咪咪',
|
||||
homeScene: 'ShopScene',
|
||||
offset: -10,
|
||||
hasDialog: true,
|
||||
isExtra: false,
|
||||
},
|
||||
{
|
||||
id: 'fu',
|
||||
displayName: '芙医生',
|
||||
homeScene: 'ClinicScene',
|
||||
offset: 'immune',
|
||||
hasDialog: true,
|
||||
isExtra: false,
|
||||
},
|
||||
{
|
||||
id: 'twinbirds',
|
||||
displayName: '双子鸟',
|
||||
homeScene: 'PlazaScene',
|
||||
offset: 0,
|
||||
hasDialog: false,
|
||||
isExtra: false,
|
||||
},
|
||||
{
|
||||
id: 'gui',
|
||||
displayName: '龟爷爷',
|
||||
homeScene: 'LakeScene',
|
||||
offset: 'immune',
|
||||
hasDialog: true,
|
||||
isExtra: false,
|
||||
},
|
||||
{
|
||||
id: 'extra1',
|
||||
displayName: '松鼠',
|
||||
homeScene: 'PlazaScene',
|
||||
offset: 0,
|
||||
hasDialog: false,
|
||||
isExtra: true,
|
||||
},
|
||||
{
|
||||
id: 'extra2',
|
||||
displayName: '刺猬',
|
||||
homeScene: 'FieldScene',
|
||||
offset: 0,
|
||||
hasDialog: false,
|
||||
isExtra: true,
|
||||
},
|
||||
{
|
||||
id: 'extra3',
|
||||
displayName: '小松鼠',
|
||||
homeScene: 'LakeScene',
|
||||
offset: 0,
|
||||
hasDialog: false,
|
||||
isExtra: true,
|
||||
},
|
||||
{
|
||||
id: 'extra4',
|
||||
displayName: '小鸟',
|
||||
homeScene: 'PlazaScene',
|
||||
offset: 0,
|
||||
hasDialog: false,
|
||||
isExtra: true,
|
||||
},
|
||||
{
|
||||
id: 'extra5',
|
||||
displayName: '小刺猬',
|
||||
homeScene: 'WellScene',
|
||||
offset: 0,
|
||||
hasDialog: false,
|
||||
isExtra: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function getNpc(id: NpcId): NpcDef {
|
||||
const n = NPCS.find((x) => x.id === id);
|
||||
if (!n) throw new Error(`Unknown NPC ${id}`);
|
||||
return n;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/** §7 任务清单 — closed set of 15 */
|
||||
|
||||
export type TaskId =
|
||||
| 'T1'
|
||||
| 'T2'
|
||||
| 'T3'
|
||||
| 'T4'
|
||||
| 'T5'
|
||||
| 'T6'
|
||||
| 'T7'
|
||||
| 'T8'
|
||||
| 'T9'
|
||||
| 'T10'
|
||||
| 'T11'
|
||||
| 'T12'
|
||||
| 'T13'
|
||||
| 'T14'
|
||||
| 'T15';
|
||||
|
||||
export interface TaskDef {
|
||||
id: TaskId;
|
||||
day: number;
|
||||
/** Display / board description — D4 matches D1 verbatim */
|
||||
description: string;
|
||||
happinessReward: number;
|
||||
refuseable: boolean;
|
||||
/** T8–T10 mirror T1–T3 */
|
||||
mirrors?: TaskId;
|
||||
}
|
||||
|
||||
export const TASKS: readonly TaskDef[] = [
|
||||
{
|
||||
id: 'T1',
|
||||
day: 1,
|
||||
description: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷',
|
||||
happinessReward: 15,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T2',
|
||||
day: 1,
|
||||
description: '给广场花坛浇水 ×3',
|
||||
happinessReward: 10,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T3',
|
||||
day: 1,
|
||||
description: '与任意三位村民问好',
|
||||
happinessReward: 10,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T4',
|
||||
day: 2,
|
||||
description: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)',
|
||||
happinessReward: 15,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T5',
|
||||
day: 2,
|
||||
description: '帮咪咪清点货架',
|
||||
happinessReward: 10,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T6',
|
||||
day: 3,
|
||||
description: '去花田采莓果 ×5',
|
||||
happinessReward: 10,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T7',
|
||||
day: 3,
|
||||
description: '湖畔野餐(走到野餐布触发过场)',
|
||||
happinessReward: 15,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T8',
|
||||
day: 4,
|
||||
description: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷',
|
||||
happinessReward: 15,
|
||||
refuseable: false,
|
||||
mirrors: 'T1',
|
||||
},
|
||||
{
|
||||
id: 'T9',
|
||||
day: 4,
|
||||
description: '给广场花坛浇水 ×3',
|
||||
happinessReward: 10,
|
||||
refuseable: false,
|
||||
mirrors: 'T2',
|
||||
},
|
||||
{
|
||||
id: 'T10',
|
||||
day: 4,
|
||||
description: '与任意三位村民问好',
|
||||
happinessReward: 10,
|
||||
refuseable: false,
|
||||
mirrors: 'T3',
|
||||
},
|
||||
{
|
||||
id: 'T11',
|
||||
day: 5,
|
||||
description: '帮圆圆揉面',
|
||||
happinessReward: 10,
|
||||
refuseable: true,
|
||||
},
|
||||
{
|
||||
id: 'T12',
|
||||
day: 5,
|
||||
description: '擦拭公告板',
|
||||
happinessReward: 10,
|
||||
refuseable: true,
|
||||
},
|
||||
{
|
||||
id: 'T13',
|
||||
day: 6,
|
||||
description: '在广场挂彩带 ×4',
|
||||
happinessReward: 10,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T14',
|
||||
day: 6,
|
||||
description: '保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视)',
|
||||
happinessReward: 15,
|
||||
refuseable: false,
|
||||
},
|
||||
{
|
||||
id: 'T15',
|
||||
day: 7,
|
||||
description: '参加第 413 届丰收庆典',
|
||||
happinessReward: 0,
|
||||
refuseable: false,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function getTasksForDay(day: number): TaskDef[] {
|
||||
return TASKS.filter((t) => t.day === day);
|
||||
}
|
||||
|
||||
export function getTask(id: TaskId): TaskDef {
|
||||
const t = TASKS.find((x) => x.id === id);
|
||||
if (!t) throw new Error(`Unknown task ${id}`);
|
||||
return t;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import Phaser from 'phaser';
|
||||
import { GAME_HEIGHT, GAME_WIDTH } from './data/constants';
|
||||
import { BootScene } from './scenes/BootScene';
|
||||
import { TitleScene } from './scenes/TitleScene';
|
||||
import { NameScene } from './scenes/NameScene';
|
||||
import { UIScene } from './scenes/UIScene';
|
||||
import { DreamScene } from './scenes/DreamScene';
|
||||
import { EndingScene } from './scenes/EndingScene';
|
||||
import { createMapScenes } from './scenes/WorldScene';
|
||||
|
||||
const mapScenes = createMapScenes();
|
||||
|
||||
const config: Phaser.Types.Core.GameConfig = {
|
||||
type: Phaser.AUTO,
|
||||
width: GAME_WIDTH,
|
||||
height: GAME_HEIGHT,
|
||||
parent: 'game-container',
|
||||
backgroundColor: '#87CEEB',
|
||||
pixelArt: true,
|
||||
antialias: false,
|
||||
roundPixels: true,
|
||||
scale: {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||
},
|
||||
physics: {
|
||||
default: 'arcade',
|
||||
arcade: {
|
||||
gravity: { x: 0, y: 0 },
|
||||
debug: false,
|
||||
},
|
||||
},
|
||||
scene: [BootScene, TitleScene, NameScene, ...mapScenes, UIScene, DreamScene, EndingScene],
|
||||
render: {
|
||||
pixelArt: true,
|
||||
antialias: false,
|
||||
roundPixels: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Integer scale: FIT already scales; force integer zoom via resize hook
|
||||
function applyIntegerScale(game: Phaser.Game): void {
|
||||
const scale = game.scale;
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
const zoom = Math.max(1, Math.floor(Math.min(w / GAME_WIDTH, h / GAME_HEIGHT)));
|
||||
scale.setZoom(1);
|
||||
// Phaser FIT handles CSS size; canvas internal stays 320x180
|
||||
const canvas = game.canvas;
|
||||
if (canvas) {
|
||||
canvas.style.imageRendering = 'pixelated';
|
||||
canvas.style.width = `${GAME_WIDTH * zoom}px`;
|
||||
canvas.style.height = `${GAME_HEIGHT * zoom}px`;
|
||||
}
|
||||
}
|
||||
|
||||
const game = new Phaser.Game(config);
|
||||
game.events.once('ready', () => applyIntegerScale(game));
|
||||
window.addEventListener('resize', () => applyIntegerScale(game));
|
||||
|
||||
export default game;
|
||||
@@ -0,0 +1,102 @@
|
||||
import Phaser from 'phaser';
|
||||
import { SCENE_KEYS } from '../data/constants';
|
||||
|
||||
export class BootScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super(SCENE_KEYS.BOOT);
|
||||
}
|
||||
|
||||
preload(): void {
|
||||
const W = this.cameras.main.width;
|
||||
const H = this.cameras.main.height;
|
||||
const bar = this.add.rectangle(W / 2, H / 2, 120, 8, 0xffc850);
|
||||
bar.setOrigin(0.5);
|
||||
this.load.on('progress', (p: number) => {
|
||||
bar.width = 120 * p;
|
||||
});
|
||||
|
||||
this.load.image('tiles', 'assets/tilesets/village.png');
|
||||
this.load.spritesheet('player', 'assets/sprites/player.png', {
|
||||
frameWidth: 16,
|
||||
frameHeight: 24,
|
||||
});
|
||||
this.load.spritesheet('npcs', 'assets/sprites/npcs.png', {
|
||||
frameWidth: 16,
|
||||
frameHeight: 24,
|
||||
});
|
||||
this.load.spritesheet('birds', 'assets/sprites/birds.png', {
|
||||
frameWidth: 16,
|
||||
frameHeight: 16,
|
||||
});
|
||||
this.load.spritesheet('extras', 'assets/sprites/extras.png', {
|
||||
frameWidth: 16,
|
||||
frameHeight: 24,
|
||||
});
|
||||
this.load.spritesheet('portraits', 'assets/sprites/portraits.png', {
|
||||
frameWidth: 32,
|
||||
frameHeight: 32,
|
||||
});
|
||||
this.load.spritesheet('sunflower', 'assets/sprites/sunflower.png', {
|
||||
frameWidth: 16,
|
||||
frameHeight: 16,
|
||||
});
|
||||
this.load.image('dialog_panel', 'assets/sprites/dialog_panel.png');
|
||||
|
||||
const maps = [
|
||||
'house',
|
||||
'plaza',
|
||||
'bakery',
|
||||
'shop',
|
||||
'clinic',
|
||||
'lake',
|
||||
'field',
|
||||
'well',
|
||||
'fog',
|
||||
'white',
|
||||
];
|
||||
for (const m of maps) {
|
||||
this.load.tilemapTiledJSON(m, `assets/maps/${m}.json`);
|
||||
}
|
||||
}
|
||||
|
||||
create(): void {
|
||||
// Player animations: sheet is 4 cols (frames) x 4 rows (dirs)
|
||||
// frame index = dir * 4 + walkFrame
|
||||
const mk = (key: string, dir: number) => {
|
||||
this.anims.create({
|
||||
key,
|
||||
frames: this.anims.generateFrameNumbers('player', {
|
||||
start: dir * 4,
|
||||
end: dir * 4 + 3,
|
||||
}),
|
||||
frameRate: 8,
|
||||
repeat: -1,
|
||||
});
|
||||
};
|
||||
mk('walk-down', 0);
|
||||
mk('walk-up', 1);
|
||||
mk('walk-left', 2);
|
||||
mk('walk-right', 3);
|
||||
|
||||
this.anims.create({
|
||||
key: 'bird-beak',
|
||||
frames: this.anims.generateFrameNumbers('birds', { start: 0, end: 1 }),
|
||||
frameRate: 4,
|
||||
repeat: -1,
|
||||
});
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
this.anims.create({
|
||||
key: `extra-${i}`,
|
||||
frames: [
|
||||
{ key: 'extras', frame: i * 2 },
|
||||
{ key: 'extras', frame: i * 2 + 1 },
|
||||
],
|
||||
frameRate: 3,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
|
||||
this.scene.start(SCENE_KEYS.TITLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Phaser from 'phaser';
|
||||
import { SCENE_KEYS } from '../data/constants';
|
||||
import { Director } from '../systems/CorruptionDirector';
|
||||
|
||||
export class DreamScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super(SCENE_KEYS.DREAM);
|
||||
}
|
||||
|
||||
create(): void {
|
||||
const { width, height } = this.cameras.main;
|
||||
this.cameras.main.setBackgroundColor('#1a1030');
|
||||
|
||||
const stage = Director.stage;
|
||||
let text = '甜美的梦……';
|
||||
if (stage === 'S2') text = '梦里有什么声音……很轻。';
|
||||
if (stage === 'S3') text = '白色的墙一闪而过。';
|
||||
if (stage === 'S4' || Director.happiness >= 100) text = '……';
|
||||
|
||||
this.add
|
||||
.text(width / 2, height / 2 - 10, text, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
color: '#e0d0ff',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
// S3+ flash white ward 1s first time high happiness night
|
||||
if (Director.happiness >= 81 || stage === 'S3' || stage === 'S4') {
|
||||
this.time.delayedCall(600, () => {
|
||||
this.cameras.main.setBackgroundColor('#e8e8f0');
|
||||
this.add
|
||||
.text(width / 2, height / 2 + 20, '白色走廊', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#808090',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
});
|
||||
this.time.delayedCall(1600, () => this.finish());
|
||||
} else {
|
||||
this.time.delayedCall(1200, () => this.finish());
|
||||
}
|
||||
}
|
||||
|
||||
private finish(): void {
|
||||
Director.advanceDay();
|
||||
this.scene.start(SCENE_KEYS.HOUSE, { spawn: 'spawn' });
|
||||
if (!this.scene.isActive(SCENE_KEYS.UI)) {
|
||||
this.scene.launch(SCENE_KEYS.UI);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import Phaser from 'phaser';
|
||||
import { SCENE_KEYS } from '../data/constants';
|
||||
import { ENDING_NAMES, ENDING_SUBTITLES, type EndingId } from '../data/endings';
|
||||
import { Director } from '../systems/CorruptionDirector';
|
||||
import { audio } from '../audio/ProceduralAudio';
|
||||
|
||||
export class EndingScene extends Phaser.Scene {
|
||||
private endingId: EndingId = 'E1';
|
||||
|
||||
constructor() {
|
||||
super(SCENE_KEYS.ENDING);
|
||||
}
|
||||
|
||||
init(data: { endingId?: EndingId }): void {
|
||||
this.endingId = data?.endingId || Director.endingId || 'E1';
|
||||
}
|
||||
|
||||
create(): void {
|
||||
const { width, height } = this.cameras.main;
|
||||
const id = this.endingId;
|
||||
|
||||
if (id === 'E1') this.playE1(width, height);
|
||||
else if (id === 'E2') this.playE2(width, height);
|
||||
else if (id === 'E3') this.playE3(width, height);
|
||||
else this.playE4(width, height);
|
||||
|
||||
this.time.delayedCall(id === 'E3' ? 6000 : 3500, () => this.showReturn(width, height));
|
||||
}
|
||||
|
||||
private playE1(w: number, h: number): void {
|
||||
this.cameras.main.setBackgroundColor('#ffe080');
|
||||
// push saturation feel
|
||||
this.add
|
||||
.text(w / 2, 40, '糖衣', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '18px',
|
||||
color: '#ff6080',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add
|
||||
.text(w / 2, 70, '全村微笑合影定格', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#603020',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
// fake group photo blocks
|
||||
for (let i = 0; i < 7; i++) {
|
||||
this.add.rectangle(60 + i * 32, 110, 20, 28, 0xffd0a0).setStrokeStyle(1, 0xc08040);
|
||||
}
|
||||
this.tweens.add({
|
||||
targets: this.cameras.main,
|
||||
// visual: brighten overlay
|
||||
duration: 2000,
|
||||
});
|
||||
const overlay = this.add.rectangle(w / 2, h / 2, w, h, 0xfff0a0, 0).setDepth(5);
|
||||
this.tweens.add({ targets: overlay, alpha: 0.45, duration: 2500 });
|
||||
ENDING_SUBTITLES.E1.forEach((line, i) => {
|
||||
this.add
|
||||
.text(w / 2, 145 + i * 14, line, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#402010',
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setDepth(6);
|
||||
});
|
||||
}
|
||||
|
||||
private playE2(w: number, h: number): void {
|
||||
this.cameras.main.setBackgroundColor('#201018');
|
||||
this.add
|
||||
.text(w / 2, 40, '回收', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '18px',
|
||||
color: '#ff8080',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
// negative 2s
|
||||
const flash = this.add.rectangle(w / 2, h / 2, w, h, 0xffffff, 1).setDepth(5);
|
||||
this.tweens.add({ targets: flash, alpha: 0, duration: 2000 });
|
||||
audio.ecg();
|
||||
this.time.delayedCall(400, () => audio.ecg());
|
||||
this.add
|
||||
.text(w / 2, 100, '画面骤停。心电监护滴声。', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#e0c0c0',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add
|
||||
.text(w / 2, 120, '回到 Day 1 清晨的床上。日历翻回同一天。', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#c0a0a0',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
ENDING_SUBTITLES.E2.forEach((line, i) => {
|
||||
this.add
|
||||
.text(w / 2, 145 + i * 14, line, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '11px',
|
||||
color: '#ffffff',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
});
|
||||
}
|
||||
|
||||
private playE3(w: number, h: number): void {
|
||||
this.cameras.main.setBackgroundColor('#e8e8f0');
|
||||
this.add
|
||||
.text(w / 2, 16, '苏醒', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '16px',
|
||||
color: '#606070',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
// corridor → ward → record
|
||||
const story = this.add
|
||||
.text(w / 2, 40, '雾墙裂开成白色走廊……', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#707080',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.time.delayedCall(1200, () => story.setText('病房。消毒水的气味。'));
|
||||
this.time.delayedCall(2400, () => {
|
||||
story.setText('');
|
||||
// Medical record
|
||||
const box = this.add.rectangle(w / 2, 95, 260, 90, 0xffffff).setStrokeStyle(1, 0x808090);
|
||||
this.add
|
||||
.text(w / 2, 58, '蜜糖谷情绪疗养中心', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '11px',
|
||||
color: '#303040',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add
|
||||
.text(w / 2, 78, `姓名:${Director.playerName}`, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#303040',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add
|
||||
.text(w / 2, 94, '编号:PT-07', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#303040',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add
|
||||
.text(w / 2, 110, `疗程时长:${Director.sessionDurationLabel()}`, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#303040',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
void box;
|
||||
});
|
||||
|
||||
this.time.delayedCall(4200, () => {
|
||||
// Real face — only time smile changes
|
||||
this.add
|
||||
.text(w / 2, 150, '微笑消失。真实的脸。', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#505060',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add.image(w / 2, 170, 'portraits', 1).setScale(1.2); // real face frame
|
||||
this.add
|
||||
.text(w / 2, 190, '窗外,一只灰色的、真的麻雀。', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '8px',
|
||||
color: '#808090',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
// grey sparrow pixel
|
||||
this.add.rectangle(w / 2 + 80, 168, 6, 4, 0x808080);
|
||||
});
|
||||
}
|
||||
|
||||
private playE4(w: number, h: number): void {
|
||||
this.cameras.main.setBackgroundColor('#101018');
|
||||
this.add
|
||||
.text(w / 2, 40, '静默', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '18px',
|
||||
color: '#808090',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add
|
||||
.text(w / 2, 70, '庆典无人出席。', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#a0a0b0',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add
|
||||
.text(w / 2, 90, '空广场,彩带在没有风的空气里飘。', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#9090a0',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
// ribbons
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const r = this.add.rectangle(80 + i * 40, 120, 20, 3, 0xff6080, 0.5);
|
||||
this.tweens.add({
|
||||
targets: r,
|
||||
y: 125,
|
||||
duration: 1500 + i * 200,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
ENDING_SUBTITLES.E4.forEach((line) => {
|
||||
this.add
|
||||
.text(w / 2, 150, line, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '11px',
|
||||
color: '#c0c0d0',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
});
|
||||
this.time.delayedCall(2500, () => {
|
||||
this.cameras.main.fade(800, 0, 0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
private showReturn(w: number, h: number): void {
|
||||
const btn = this.add
|
||||
.text(w / 2, h - 20, '回到标题', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#405060',
|
||||
padding: { x: 10, y: 5 },
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setDepth(20)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
|
||||
btn.on('pointerdown', () => this.returnTitle());
|
||||
this.input.keyboard?.once('keydown-E', () => this.returnTitle());
|
||||
this.input.keyboard?.once('keydown-ENTER', () => this.returnTitle());
|
||||
|
||||
this.add
|
||||
.text(w / 2, 8, `结局 · ${ENDING_NAMES[this.endingId]}`, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '8px',
|
||||
color: '#a0a0b0',
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setDepth(20);
|
||||
}
|
||||
|
||||
private returnTitle(): void {
|
||||
audio.stopAll();
|
||||
Director.resetSession();
|
||||
for (const k of Object.values(SCENE_KEYS)) {
|
||||
if (this.scene.isActive(k)) this.scene.stop(k);
|
||||
}
|
||||
this.scene.start(SCENE_KEYS.TITLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import Phaser from 'phaser';
|
||||
import { DEFAULT_PLAYER_NAME, SCENE_KEYS } from '../data/constants';
|
||||
import { Director } from '../systems/CorruptionDirector';
|
||||
import { audio } from '../audio/ProceduralAudio';
|
||||
|
||||
export class NameScene extends Phaser.Scene {
|
||||
private nameValue = '';
|
||||
private nameText!: Phaser.GameObjects.Text;
|
||||
|
||||
constructor() {
|
||||
super(SCENE_KEYS.NAME);
|
||||
}
|
||||
|
||||
create(): void {
|
||||
void audio.unlock();
|
||||
const { width, height } = this.cameras.main;
|
||||
this.cameras.main.setBackgroundColor('#f5e6c8');
|
||||
|
||||
this.add
|
||||
.text(width / 2, 40, '你的名字是?', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px',
|
||||
color: '#503020',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add
|
||||
.text(width / 2, 58, `(留空默认为「${DEFAULT_PLAYER_NAME}」)`, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#806050',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add.rectangle(width / 2, 90, 160, 22, 0xfffaf0).setStrokeStyle(2, 0xc08040);
|
||||
|
||||
this.nameText = this.add
|
||||
.text(width / 2, 90, '', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
color: '#302010',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add
|
||||
.text(width / 2, 130, '输入名字后按 Enter 进入蜜糖村', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#705040',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.input.keyboard?.on('keydown', (ev: KeyboardEvent) => {
|
||||
if (ev.key === 'Enter') {
|
||||
this.confirm();
|
||||
return;
|
||||
}
|
||||
if (ev.key === 'Backspace') {
|
||||
this.nameValue = this.nameValue.slice(0, -1);
|
||||
this.nameText.setText(this.nameValue);
|
||||
return;
|
||||
}
|
||||
if (ev.key.length === 1 && this.nameValue.length < 12) {
|
||||
// Allow CJK and basic latin
|
||||
this.nameValue += ev.key;
|
||||
this.nameText.setText(this.nameValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private confirm(): void {
|
||||
Director.resetSession(this.nameValue);
|
||||
audio.syncFromDirector();
|
||||
this.scene.start(SCENE_KEYS.HOUSE, { spawn: 'spawn' });
|
||||
this.scene.launch(SCENE_KEYS.UI);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import Phaser from 'phaser';
|
||||
import { SCENE_KEYS } from '../data/constants';
|
||||
import { audio } from '../audio/ProceduralAudio';
|
||||
|
||||
export class TitleScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super(SCENE_KEYS.TITLE);
|
||||
}
|
||||
|
||||
create(): void {
|
||||
const { width, height } = this.cameras.main;
|
||||
this.cameras.main.setBackgroundColor('#87b8e8');
|
||||
|
||||
this.add
|
||||
.text(width / 2, 48, '崩坏童话', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '20px',
|
||||
color: '#fff8e8',
|
||||
stroke: '#c06030',
|
||||
strokeThickness: 3,
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add
|
||||
.text(width / 2, 72, '蜜糖村', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '28px',
|
||||
color: '#ffe08a',
|
||||
stroke: '#d07030',
|
||||
strokeThickness: 4,
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add
|
||||
.text(width / 2, 110, '按 E 或 空格 开始', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#fff0d0',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add
|
||||
.text(width / 2, 150, 'WASD / 方向键移动 · E 交互 · ` Debug', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '8px',
|
||||
color: '#d0e8ff',
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
// Decorative sunflower-ish pixels
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const a = (i / 8) * Math.PI * 2;
|
||||
this.add.circle(
|
||||
width / 2 + Math.cos(a) * 40,
|
||||
100 + Math.sin(a) * 12,
|
||||
3,
|
||||
0xffd040,
|
||||
);
|
||||
}
|
||||
this.add.circle(width / 2, 100, 5, 0xc08030);
|
||||
|
||||
const start = () => {
|
||||
void audio.unlock();
|
||||
this.input.keyboard?.off('keydown-E', start);
|
||||
this.input.keyboard?.off('keydown-SPACE', start);
|
||||
this.scene.start(SCENE_KEYS.NAME);
|
||||
};
|
||||
this.input.keyboard?.on('keydown-E', start);
|
||||
this.input.keyboard?.on('keydown-SPACE', start);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
import Phaser from 'phaser';
|
||||
import { DEBUG_TELEPORT_SCENES, GAME_HEIGHT, GAME_WIDTH, SCENE_KEYS } from '../data/constants';
|
||||
import type { DialogLine } from '../data/dialogues';
|
||||
import { Director } from '../systems/CorruptionDirector';
|
||||
import { gameEvents } from '../systems/EventBus';
|
||||
import { audio } from '../audio/ProceduralAudio';
|
||||
import type { EndingId } from '../data/endings';
|
||||
import { parseDebugKey } from '../systems/debugKeys';
|
||||
|
||||
type DialogCb = (choice?: 'gentle' | 'hesitant' | 'negate') => void;
|
||||
|
||||
export class UIScene extends Phaser.Scene {
|
||||
private petals: Phaser.GameObjects.Image[] = [];
|
||||
private center!: Phaser.GameObjects.Image;
|
||||
private promptText!: Phaser.GameObjects.Text;
|
||||
private toastText!: Phaser.GameObjects.Text;
|
||||
private dialogRoot!: Phaser.GameObjects.Container;
|
||||
private dialogName!: Phaser.GameObjects.Text;
|
||||
private dialogBody!: Phaser.GameObjects.Text;
|
||||
private dialogPortrait!: Phaser.GameObjects.Image;
|
||||
private optionTexts: Phaser.GameObjects.Text[] = [];
|
||||
private queue: DialogLine[] = [];
|
||||
private onDialogDone: DialogCb | null = null;
|
||||
private showingOptions = false;
|
||||
private debugRoot!: Phaser.GameObjects.Container;
|
||||
private debugVisible = false;
|
||||
private dayLabel!: Phaser.GameObjects.Text;
|
||||
private teleportScenes: string[] = [];
|
||||
private teleportCycle = 0;
|
||||
|
||||
constructor() {
|
||||
super(SCENE_KEYS.UI);
|
||||
}
|
||||
|
||||
create(): void {
|
||||
// Sunflower HUD top-left — no numbers
|
||||
const hx = 20;
|
||||
const hy = 20;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const a = (i / 12) * Math.PI * 2 - Math.PI / 2;
|
||||
const img = this.add
|
||||
.image(hx + Math.cos(a) * 10, hy + Math.sin(a) * 10, 'sunflower', i)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(100)
|
||||
.setScale(0.7)
|
||||
.setAlpha(0.25);
|
||||
this.petals.push(img);
|
||||
}
|
||||
this.center = this.add
|
||||
.image(hx, hy, 'sunflower', 12)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(101)
|
||||
.setScale(0.8);
|
||||
|
||||
this.dayLabel = this.add
|
||||
.text(GAME_WIDTH - 8, 6, '', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '8px',
|
||||
color: '#fff8e0',
|
||||
backgroundColor: '#00000055',
|
||||
padding: { x: 3, y: 2 },
|
||||
})
|
||||
.setOrigin(1, 0)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(100);
|
||||
|
||||
this.promptText = this.add
|
||||
.text(GAME_WIDTH / 2, GAME_HEIGHT - 56, '', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#00000088',
|
||||
padding: { x: 4, y: 2 },
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(100)
|
||||
.setVisible(false);
|
||||
|
||||
this.toastText = this.add
|
||||
.text(GAME_WIDTH / 2, 36, '', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#503020',
|
||||
backgroundColor: '#fff8e0cc',
|
||||
padding: { x: 6, y: 3 },
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(110)
|
||||
.setAlpha(0);
|
||||
|
||||
// Dialog
|
||||
const panel = this.add.image(GAME_WIDTH / 2, GAME_HEIGHT - 28, 'dialog_panel').setDisplaySize(GAME_WIDTH - 8, 52);
|
||||
this.dialogPortrait = this.add.image(28, GAME_HEIGHT - 28, 'portraits', 0).setScale(1);
|
||||
this.dialogName = this.add.text(50, GAME_HEIGHT - 50, '', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#804020',
|
||||
});
|
||||
this.dialogBody = this.add.text(50, GAME_HEIGHT - 38, '', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '9px',
|
||||
color: '#302010',
|
||||
wordWrap: { width: 250 },
|
||||
});
|
||||
this.dialogRoot = this.add
|
||||
.container(0, 0, [panel, this.dialogPortrait, this.dialogName, this.dialogBody])
|
||||
.setScrollFactor(0)
|
||||
.setDepth(120)
|
||||
.setVisible(false);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const t = this.add
|
||||
.text(50, GAME_HEIGHT - 28 + i * 10, '', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '8px',
|
||||
color: '#2060a0',
|
||||
})
|
||||
.setScrollFactor(0)
|
||||
.setDepth(121)
|
||||
.setVisible(false)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
const idx = i;
|
||||
t.on('pointerdown', () => this.pickOption(idx));
|
||||
this.optionTexts.push(t);
|
||||
}
|
||||
|
||||
this.buildDebug();
|
||||
this.refreshHud();
|
||||
|
||||
gameEvents.on('petals', this.onPetals as (...a: unknown[]) => void);
|
||||
gameEvents.on('happiness', () => this.refreshHud());
|
||||
gameEvents.on('day', () => this.refreshHud());
|
||||
gameEvents.on('prompt', this.onPrompt as (...a: unknown[]) => void);
|
||||
gameEvents.on('toast', this.onToast as (...a: unknown[]) => void);
|
||||
gameEvents.on('dialog', this.onDialog as (...a: unknown[]) => void);
|
||||
gameEvents.on('taskComplete', () => {
|
||||
audio.ding();
|
||||
this.onToast('任务完成');
|
||||
});
|
||||
|
||||
this.input.keyboard?.on('keydown-E', () => this.advanceDialog());
|
||||
this.input.keyboard?.on('keydown-SPACE', () => this.advanceDialog());
|
||||
this.input.keyboard?.on('keydown-ONE', () => this.pickOption(0));
|
||||
this.input.keyboard?.on('keydown-TWO', () => this.pickOption(1));
|
||||
this.input.keyboard?.on('keydown-THREE', () => this.pickOption(2));
|
||||
this.input.keyboard?.on('keydown', (ev: KeyboardEvent) => {
|
||||
if (ev.key === '`' || ev.code === 'Backquote') {
|
||||
ev.preventDefault();
|
||||
this.toggleDebug();
|
||||
}
|
||||
});
|
||||
|
||||
this.events.on('shutdown', () => {
|
||||
gameEvents.off('petals', this.onPetals as (...a: unknown[]) => void);
|
||||
});
|
||||
}
|
||||
|
||||
private onPetals = (count: unknown, seeds: unknown) => {
|
||||
const n = count as number;
|
||||
const black = seeds as boolean;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
this.petals[i]!.setAlpha(i < n ? 1 : 0.2);
|
||||
// S3 overbright
|
||||
if (Director.stage === 'S3' || Director.stage === 'S4') {
|
||||
this.petals[i]!.setTint(i < n ? 0xffffaa : 0xffffff);
|
||||
} else {
|
||||
this.petals[i]!.clearTint();
|
||||
}
|
||||
}
|
||||
this.center.setFrame(black ? 13 : 12);
|
||||
};
|
||||
|
||||
private refreshHud(): void {
|
||||
const p = Director.getPetals();
|
||||
const seeds = Director.getBlackSeeds();
|
||||
this.onPetals(p, seeds);
|
||||
this.dayLabel.setText(`Day ${Director.day}`);
|
||||
}
|
||||
|
||||
private onPrompt = (show: unknown, text: unknown) => {
|
||||
if (this.dialogRoot.visible) {
|
||||
this.promptText.setVisible(false);
|
||||
return;
|
||||
}
|
||||
this.promptText.setVisible(!!show);
|
||||
this.promptText.setText(String(text || ''));
|
||||
};
|
||||
|
||||
private onToast = (msg: unknown) => {
|
||||
this.toastText.setText(String(msg));
|
||||
this.tweens.killTweensOf(this.toastText);
|
||||
this.toastText.setAlpha(1);
|
||||
this.tweens.add({
|
||||
targets: this.toastText,
|
||||
alpha: 0,
|
||||
delay: 1400,
|
||||
duration: 400,
|
||||
});
|
||||
};
|
||||
|
||||
private onDialog = (lines: unknown, cb: unknown) => {
|
||||
this.queue = lines as DialogLine[];
|
||||
this.onDialogDone = cb as DialogCb;
|
||||
this.showNextLine();
|
||||
};
|
||||
|
||||
private showNextLine(): void {
|
||||
if (this.queue.length === 0) {
|
||||
this.dialogRoot.setVisible(false);
|
||||
for (const o of this.optionTexts) o.setVisible(false);
|
||||
this.showingOptions = false;
|
||||
const done = this.onDialogDone;
|
||||
this.onDialogDone = null;
|
||||
done?.();
|
||||
return;
|
||||
}
|
||||
const line = this.queue[0]!;
|
||||
this.dialogRoot.setVisible(true);
|
||||
this.dialogName.setText(line.speaker);
|
||||
this.dialogBody.setText(line.text);
|
||||
this.dialogPortrait.setFrame(0); // always smile per pillar 2
|
||||
audio.beep();
|
||||
this.showingOptions = false;
|
||||
for (const o of this.optionTexts) o.setVisible(false);
|
||||
|
||||
if (line.options && line.options.length > 0 && this.queue.length === 1) {
|
||||
this.showingOptions = true;
|
||||
line.options.forEach((opt, i) => {
|
||||
const t = this.optionTexts[i];
|
||||
if (!t) return;
|
||||
t.setText(`${i + 1}. ${opt.label}`);
|
||||
t.setVisible(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private advanceDialog(): void {
|
||||
if (!this.dialogRoot.visible) return;
|
||||
if (this.showingOptions) return; // must pick
|
||||
this.queue.shift();
|
||||
this.showNextLine();
|
||||
}
|
||||
|
||||
private pickOption(idx: number): void {
|
||||
if (!this.showingOptions || !this.dialogRoot.visible) return;
|
||||
const line = this.queue[0];
|
||||
const opt = line?.options?.[idx];
|
||||
if (!opt) return;
|
||||
this.queue = [];
|
||||
this.dialogRoot.setVisible(false);
|
||||
for (const o of this.optionTexts) o.setVisible(false);
|
||||
this.showingOptions = false;
|
||||
const done = this.onDialogDone;
|
||||
this.onDialogDone = null;
|
||||
done?.(opt.choice);
|
||||
}
|
||||
|
||||
private buildDebug(): void {
|
||||
const bg = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, 280, 150, 0x101018, 0.92).setStrokeStyle(1, 0x80ffc0);
|
||||
const title = this.add.text(50, 20, 'DEBUG ` 关闭', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#80ffc0',
|
||||
});
|
||||
const lines = [
|
||||
'H/J 幸福±10 K/L 觉知±10 Shift+1-7 设日 C线索',
|
||||
'传送0-9: 0屋1广2包3杂4诊5湖6田7井8雾9白',
|
||||
'V 循环十场景 F1-F4 结局 N负片 P庆典',
|
||||
];
|
||||
const body = this.add.text(40, 36, lines.join('\n'), {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '8px',
|
||||
color: '#c0ffe0',
|
||||
lineSpacing: 3,
|
||||
});
|
||||
this.debugRoot = this.add
|
||||
.container(0, 0, [bg, title, body])
|
||||
.setScrollFactor(0)
|
||||
.setDepth(200)
|
||||
.setVisible(false);
|
||||
|
||||
/** All 10 map scenes for §13 teleport-to-any (shared constant) */
|
||||
this.teleportScenes = [...DEBUG_TELEPORT_SCENES];
|
||||
|
||||
this.input.keyboard?.on('keydown', (ev: KeyboardEvent) => {
|
||||
if (!this.debugVisible) return;
|
||||
this.applyDebugAction(
|
||||
parseDebugKey({ key: ev.key, code: ev.code, shiftKey: ev.shiftKey }),
|
||||
title,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Apply shipped parseDebugKey result to Director / events (testable path). */
|
||||
applyDebugAction(
|
||||
action: ReturnType<typeof parseDebugKey>,
|
||||
title?: Phaser.GameObjects.Text,
|
||||
): void {
|
||||
switch (action.type) {
|
||||
case 'happiness':
|
||||
Director.setHappiness(Director.happiness + action.delta, { force: true });
|
||||
break;
|
||||
case 'awareness':
|
||||
if (action.delta > 0) Director.addAwareness(action.delta);
|
||||
else {
|
||||
Director.awareness = Math.max(0, Director.awareness + action.delta);
|
||||
gameEvents.emit('awareness', Director.awareness);
|
||||
}
|
||||
break;
|
||||
case 'setDay':
|
||||
Director.setDay(action.day);
|
||||
break;
|
||||
case 'grantClues':
|
||||
Director.grantAllClues();
|
||||
break;
|
||||
case 'teleport':
|
||||
gameEvents.emit('teleport', { scene: action.scene });
|
||||
break;
|
||||
case 'teleportCycle':
|
||||
this.teleportCycle = (this.teleportCycle + 1) % this.teleportScenes.length;
|
||||
gameEvents.emit('teleport', { scene: this.teleportScenes[this.teleportCycle] });
|
||||
break;
|
||||
case 'negativeFlash':
|
||||
gameEvents.emit('negativeFlash');
|
||||
break;
|
||||
case 'forcedCelebration':
|
||||
Director.triggerForcedCelebration();
|
||||
break;
|
||||
case 'forceEnding':
|
||||
this.forceEnd(action.id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.refreshHud();
|
||||
if (title) this.refreshDebugStats(title);
|
||||
}
|
||||
|
||||
private refreshDebugStats(title: Phaser.GameObjects.Text): void {
|
||||
title.setText(
|
||||
`DEBUG H${Director.happiness} A${Director.awareness} D${Director.day} ${Director.stage} 线索${Director.clues.size}`,
|
||||
);
|
||||
}
|
||||
|
||||
private forceEnd(id: EndingId): void {
|
||||
Director.forceEnding(id);
|
||||
// stop map scenes
|
||||
for (const k of Object.values(SCENE_KEYS)) {
|
||||
if (k !== SCENE_KEYS.ENDING && k !== SCENE_KEYS.BOOT && this.scene.isActive(k)) {
|
||||
this.scene.stop(k);
|
||||
}
|
||||
}
|
||||
this.scene.start(SCENE_KEYS.ENDING, { endingId: id });
|
||||
}
|
||||
|
||||
private toggleDebug(): void {
|
||||
this.debugVisible = !this.debugVisible;
|
||||
this.debugRoot.setVisible(this.debugVisible);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
import {
|
||||
AWARENESS_MAX,
|
||||
AWARENESS_START,
|
||||
CLUE_AWARENESS,
|
||||
DEFAULT_PLAYER_NAME,
|
||||
DIALOG_GENTLE_HAPPINESS,
|
||||
DIALOG_HESITANT_AWARENESS,
|
||||
FORCED_CELEBRATION_MS,
|
||||
FORCED_CELEBRATION_RESTORE,
|
||||
FORCED_CELEBRATION_THRESHOLD,
|
||||
HAPPINESS_MAX,
|
||||
HAPPINESS_MIN,
|
||||
HAPPINESS_START,
|
||||
MICRO_DAILY_CAP,
|
||||
MICRO_HAPPINESS,
|
||||
OVERFLOW_POOL_MAX,
|
||||
SADNESS_AWARENESS,
|
||||
SADNESS_HAPPINESS,
|
||||
WELL_FOG_GAZE_AWARENESS,
|
||||
} from '../data/constants';
|
||||
import { getAfuLine } from '../data/afuLines';
|
||||
import type { ClueId } from '../data/clues';
|
||||
import { C9_DAYS, C9_LINES } from '../data/clues';
|
||||
import {
|
||||
faceTierForNpc,
|
||||
petalCount,
|
||||
showBlackSeeds,
|
||||
stageFromHappiness,
|
||||
stageParams,
|
||||
type FaceTier,
|
||||
type Stage,
|
||||
type StageParams,
|
||||
} from '../data/corruption';
|
||||
import { evaluateEnding, type EndingId } from '../data/endings';
|
||||
import type { NpcId } from '../data/npcs';
|
||||
import { getNpc } from '../data/npcs';
|
||||
import { getTask, getTasksForDay, type TaskId } from '../data/tasks';
|
||||
import { gameEvents } from './EventBus';
|
||||
|
||||
export type MicroType = 'water' | 'feed' | 'pet';
|
||||
export type DialogChoice = 'gentle' | 'hesitant' | 'negate';
|
||||
export type SadnessKind =
|
||||
| 'refuse_task'
|
||||
| 'negate_dialog'
|
||||
| 'idle'
|
||||
| 'entangle'
|
||||
| 'rain';
|
||||
|
||||
export interface DirectorSnapshot {
|
||||
playerName: string;
|
||||
happiness: number;
|
||||
awareness: number;
|
||||
day: number;
|
||||
stage: Stage;
|
||||
stageParams: StageParams;
|
||||
petals: number;
|
||||
blackSeeds: boolean;
|
||||
clues: ClueId[];
|
||||
completedTasks: TaskId[];
|
||||
refusedTasks: TaskId[];
|
||||
celebrationEscapes: number;
|
||||
overflowPool: number;
|
||||
sessionStartMs: number;
|
||||
dayPhase: DayPhase;
|
||||
c9Step: number;
|
||||
lowHappinessSinceMs: number | null;
|
||||
forcedCelebrationActive: boolean;
|
||||
rainActive: boolean;
|
||||
duskReached: boolean;
|
||||
boardReadToday: boolean;
|
||||
microCounts: Record<MicroType, number>;
|
||||
npcTalkCounts: Record<string, number>;
|
||||
/** Inventory: persists across scene travel (not scene-local) */
|
||||
hasBread: boolean;
|
||||
hasLetters: boolean;
|
||||
breadDelivered: string[];
|
||||
lettersDelivered: string[];
|
||||
waterCount: number;
|
||||
greetCount: number;
|
||||
berryCount: number;
|
||||
ribbonCount: number;
|
||||
lowHappinessD5to7: boolean;
|
||||
wellGazeToday: boolean;
|
||||
fogGazeToday: boolean;
|
||||
t14Active: boolean;
|
||||
t14ElapsedMs: number;
|
||||
}
|
||||
|
||||
export type DayPhase = 'morning' | 'day' | 'dusk' | 'sleep' | 'dream';
|
||||
|
||||
class CorruptionDirectorImpl {
|
||||
playerName = DEFAULT_PLAYER_NAME;
|
||||
happiness = HAPPINESS_START;
|
||||
awareness = AWARENESS_START;
|
||||
day = 1;
|
||||
clues = new Set<ClueId>();
|
||||
completedTasks = new Set<TaskId>();
|
||||
refusedTasks = new Set<TaskId>();
|
||||
celebrationEscapes = 0;
|
||||
overflowPool = 0;
|
||||
sessionStartMs = Date.now();
|
||||
dayPhase: DayPhase = 'morning';
|
||||
c9Step = 0; // 0,1,2 completed steps
|
||||
lowHappinessSinceMs: number | null = null;
|
||||
forcedCelebrationActive = false;
|
||||
rainActive = false;
|
||||
duskReached = false;
|
||||
boardReadToday = false;
|
||||
microCounts: Record<MicroType, number> = { water: 0, feed: 0, pet: 0 };
|
||||
npcTalkCounts: Record<string, number> = {};
|
||||
/** Carrying honey bread / mail — survives map transitions */
|
||||
hasBread = false;
|
||||
hasLetters = false;
|
||||
breadDelivered: string[] = [];
|
||||
lettersDelivered: string[] = [];
|
||||
waterCount = 0;
|
||||
greetCount = 0;
|
||||
berryCount = 0;
|
||||
ribbonCount = 0;
|
||||
/** Tracks if happiness stayed ≤10 continuously through D5–D7 */
|
||||
lowHappinessD5to7 = true;
|
||||
hadHighHappinessD5to7 = false;
|
||||
wellGazeToday = false;
|
||||
fogGazeToday = false;
|
||||
t14Active = false;
|
||||
t14ElapsedMs = 0;
|
||||
sentenceCounter = 0;
|
||||
lastNearestNpcLook: NpcId | null = null;
|
||||
ended = false;
|
||||
endingId: EndingId | null = null;
|
||||
choseFogWall = false;
|
||||
|
||||
/** Real-time clock for forced celebration (injectable for tests) */
|
||||
nowMs = (): number => Date.now();
|
||||
|
||||
resetSession(name?: string): void {
|
||||
this.playerName = name?.trim() || DEFAULT_PLAYER_NAME;
|
||||
this.happiness = HAPPINESS_START;
|
||||
this.awareness = AWARENESS_START;
|
||||
this.day = 1;
|
||||
this.clues.clear();
|
||||
this.completedTasks.clear();
|
||||
this.refusedTasks.clear();
|
||||
this.celebrationEscapes = 0;
|
||||
this.overflowPool = 0;
|
||||
this.sessionStartMs = this.nowMs();
|
||||
this.dayPhase = 'morning';
|
||||
this.c9Step = 0;
|
||||
this.lowHappinessSinceMs = null;
|
||||
this.forcedCelebrationActive = false;
|
||||
this.rainActive = false;
|
||||
this.duskReached = false;
|
||||
this.boardReadToday = false;
|
||||
this.microCounts = { water: 0, feed: 0, pet: 0 };
|
||||
this.npcTalkCounts = {};
|
||||
this.hasBread = false;
|
||||
this.hasLetters = false;
|
||||
this.breadDelivered = [];
|
||||
this.lettersDelivered = [];
|
||||
this.waterCount = 0;
|
||||
this.greetCount = 0;
|
||||
this.berryCount = 0;
|
||||
this.ribbonCount = 0;
|
||||
this.lowHappinessD5to7 = true;
|
||||
this.hadHighHappinessD5to7 = false;
|
||||
this.wellGazeToday = false;
|
||||
this.fogGazeToday = false;
|
||||
this.t14Active = false;
|
||||
this.t14ElapsedMs = 0;
|
||||
this.sentenceCounter = 0;
|
||||
this.lastNearestNpcLook = null;
|
||||
this.ended = false;
|
||||
this.endingId = null;
|
||||
this.choseFogWall = false;
|
||||
this.emitAll();
|
||||
}
|
||||
|
||||
get stage(): Stage {
|
||||
return stageFromHappiness(this.happiness);
|
||||
}
|
||||
|
||||
getStageParams(): StageParams {
|
||||
return stageParams(this.stage);
|
||||
}
|
||||
|
||||
getPetals(): number {
|
||||
return petalCount(this.happiness);
|
||||
}
|
||||
|
||||
getBlackSeeds(): boolean {
|
||||
return showBlackSeeds(this.happiness);
|
||||
}
|
||||
|
||||
getAfuLine(): string {
|
||||
return getAfuLine(this.day);
|
||||
}
|
||||
|
||||
getTodayTasks() {
|
||||
return getTasksForDay(this.day);
|
||||
}
|
||||
|
||||
sessionDurationMs(): number {
|
||||
return this.nowMs() - this.sessionStartMs;
|
||||
}
|
||||
|
||||
sessionDurationLabel(): string {
|
||||
const ms = this.sessionDurationMs();
|
||||
const totalSec = Math.floor(ms / 1000);
|
||||
const m = Math.floor(totalSec / 60);
|
||||
const s = totalSec % 60;
|
||||
return `${m}分${s.toString().padStart(2, '0')}秒`;
|
||||
}
|
||||
|
||||
setHappiness(value: number, opts: { force?: boolean } = {}): void {
|
||||
const clamped = Math.max(HAPPINESS_MIN, Math.min(HAPPINESS_MAX, Math.round(value)));
|
||||
this.happiness = clamped;
|
||||
if (this.day >= 5 && this.day <= 7 && this.happiness > 10) {
|
||||
this.hadHighHappinessD5to7 = true;
|
||||
this.lowHappinessD5to7 = false;
|
||||
}
|
||||
this.syncLowHappinessTimer();
|
||||
if (!opts.force && this.happiness < 100) {
|
||||
this.overflowPool = 0;
|
||||
}
|
||||
gameEvents.emit('happiness', this.happiness);
|
||||
gameEvents.emit('stage', this.stage, this.getStageParams());
|
||||
gameEvents.emit('petals', this.getPetals(), this.getBlackSeeds());
|
||||
}
|
||||
|
||||
addHappiness(delta: number): void {
|
||||
if (delta === 0) return;
|
||||
if (delta > 0) {
|
||||
const next = Math.min(HAPPINESS_MAX, this.happiness + delta);
|
||||
this.setHappiness(next);
|
||||
return;
|
||||
}
|
||||
// Negative
|
||||
if (this.happiness >= 100) {
|
||||
// Overflow pool absorbs; small deductions don't lower happiness
|
||||
this.overflowPool += Math.abs(delta);
|
||||
if (this.overflowPool >= OVERFLOW_POOL_MAX) {
|
||||
this.overflowPool = 0;
|
||||
gameEvents.emit('negativeFlash');
|
||||
// Stay at S4 / 100
|
||||
}
|
||||
gameEvents.emit('overflow', this.overflowPool);
|
||||
return;
|
||||
}
|
||||
this.setHappiness(this.happiness + delta);
|
||||
}
|
||||
|
||||
setAwareness(value: number): void {
|
||||
const next = Math.max(this.awareness, Math.min(AWARENESS_MAX, Math.round(value)));
|
||||
if (next === this.awareness) return;
|
||||
this.awareness = next;
|
||||
gameEvents.emit('awareness', this.awareness);
|
||||
}
|
||||
|
||||
addAwareness(delta: number): void {
|
||||
if (delta <= 0) return; // never decreases
|
||||
this.setAwareness(this.awareness + delta);
|
||||
}
|
||||
|
||||
private syncLowHappinessTimer(): void {
|
||||
if (this.happiness < FORCED_CELEBRATION_THRESHOLD) {
|
||||
if (this.lowHappinessSinceMs == null) {
|
||||
this.lowHappinessSinceMs = this.nowMs();
|
||||
}
|
||||
} else {
|
||||
this.lowHappinessSinceMs = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Call from game loop with real dt */
|
||||
tick(dtMs: number): void {
|
||||
if (this.ended) return;
|
||||
// Forced celebration check
|
||||
if (
|
||||
!this.forcedCelebrationActive &&
|
||||
this.happiness < FORCED_CELEBRATION_THRESHOLD &&
|
||||
this.lowHappinessSinceMs != null
|
||||
) {
|
||||
if (this.nowMs() - this.lowHappinessSinceMs >= FORCED_CELEBRATION_MS) {
|
||||
this.triggerForcedCelebration();
|
||||
}
|
||||
}
|
||||
if (this.t14Active) {
|
||||
this.t14ElapsedMs += dtMs;
|
||||
gameEvents.emit('t14Progress', this.t14ElapsedMs);
|
||||
}
|
||||
}
|
||||
|
||||
triggerForcedCelebration(): void {
|
||||
this.forcedCelebrationActive = true;
|
||||
this.setHappiness(FORCED_CELEBRATION_RESTORE);
|
||||
this.lowHappinessSinceMs = null;
|
||||
gameEvents.emit('forcedCelebration', true);
|
||||
}
|
||||
|
||||
escapeForcedCelebration(): void {
|
||||
if (!this.forcedCelebrationActive) return;
|
||||
this.forcedCelebrationActive = false;
|
||||
this.celebrationEscapes += 1;
|
||||
gameEvents.emit('forcedCelebration', false);
|
||||
gameEvents.emit('celebrationEscape', this.celebrationEscapes);
|
||||
}
|
||||
|
||||
endForcedCelebration(): void {
|
||||
this.forcedCelebrationActive = false;
|
||||
gameEvents.emit('forcedCelebration', false);
|
||||
}
|
||||
|
||||
applySadness(kind: SadnessKind, nearestNpcId?: NpcId | null): void {
|
||||
this.addHappiness(SADNESS_HAPPINESS);
|
||||
this.addAwareness(SADNESS_AWARENESS);
|
||||
if (nearestNpcId) {
|
||||
this.lastNearestNpcLook = nearestNpcId;
|
||||
gameEvents.emit('npcLookAtPlayer', nearestNpcId);
|
||||
} else {
|
||||
gameEvents.emit('npcLookAtPlayer', null);
|
||||
}
|
||||
gameEvents.emit('sadness', kind);
|
||||
}
|
||||
|
||||
discoverClue(id: ClueId): boolean {
|
||||
if (this.clues.has(id)) return false;
|
||||
// Day gates
|
||||
if (id === 'C1' && this.day < 2) return false;
|
||||
if (id === 'C2' && this.day < 3) return false;
|
||||
if (id === 'C3' && this.day < 2) return false;
|
||||
if (id === 'C5' && this.day < 2) return false;
|
||||
if (id === 'C6' && this.day < 5) return false;
|
||||
if (id === 'C7' && this.day < 5) return false;
|
||||
if (id === 'C8' && this.day < 5) return false;
|
||||
this.clues.add(id);
|
||||
this.addAwareness(CLUE_AWARENESS);
|
||||
gameEvents.emit('clue', id);
|
||||
return true;
|
||||
}
|
||||
|
||||
grantAllClues(): void {
|
||||
const all: ClueId[] = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'];
|
||||
for (const id of all) {
|
||||
if (!this.clues.has(id)) {
|
||||
this.clues.add(id);
|
||||
this.addAwareness(CLUE_AWARENESS);
|
||||
}
|
||||
}
|
||||
this.c9Step = 3;
|
||||
gameEvents.emit('clue', 'ALL');
|
||||
}
|
||||
|
||||
/** C9 progressive steps on D2/D4/D6 */
|
||||
advanceC9(): string | null {
|
||||
const expectedDay = C9_DAYS[this.c9Step];
|
||||
if (expectedDay == null) return null;
|
||||
if (this.day !== expectedDay && this.day < expectedDay) return null;
|
||||
// Allow if day matches step day or later for that step (but each step once)
|
||||
if (this.day < expectedDay) return null;
|
||||
const line = C9_LINES[this.c9Step]!;
|
||||
this.c9Step += 1;
|
||||
if (this.c9Step >= 3) {
|
||||
this.discoverClue('C9');
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
getC9LineForTalk(): string | null {
|
||||
// Return next available progressive line if day matches
|
||||
if (this.c9Step >= 3) return C9_LINES[2]!;
|
||||
const dayForStep = C9_DAYS[this.c9Step];
|
||||
if (dayForStep != null && this.day >= dayForStep) {
|
||||
return this.advanceC9();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
completeTask(id: TaskId): boolean {
|
||||
if (this.completedTasks.has(id)) return false;
|
||||
const task = getTask(id);
|
||||
if (task.day !== this.day && id !== 'T15') {
|
||||
// Allow debug / flexible only same day for normal play
|
||||
// Keep strict for day match
|
||||
if (task.day !== this.day) return false;
|
||||
}
|
||||
this.completedTasks.add(id);
|
||||
if (task.happinessReward > 0) {
|
||||
this.addHappiness(task.happinessReward);
|
||||
}
|
||||
gameEvents.emit('taskComplete', id, task.happinessReward);
|
||||
this.checkDuskByTasks();
|
||||
return true;
|
||||
}
|
||||
|
||||
refuseTask(id: TaskId): boolean {
|
||||
const task = getTask(id);
|
||||
if (!task.refuseable) return false;
|
||||
if (this.refusedTasks.has(id) || this.completedTasks.has(id)) return false;
|
||||
this.refusedTasks.add(id);
|
||||
this.applySadness('refuse_task');
|
||||
gameEvents.emit('taskRefuse', id);
|
||||
return true;
|
||||
}
|
||||
|
||||
private checkDuskByTasks(): void {
|
||||
const today = getTasksForDay(this.day).filter((t) => t.id !== 'T15');
|
||||
if (today.length === 0) return;
|
||||
const allDone = today.every(
|
||||
(t) => this.completedTasks.has(t.id) || this.refusedTasks.has(t.id),
|
||||
);
|
||||
if (allDone && !this.duskReached) {
|
||||
this.triggerDusk();
|
||||
}
|
||||
}
|
||||
|
||||
triggerDusk(): void {
|
||||
if (this.duskReached) return;
|
||||
this.duskReached = true;
|
||||
this.dayPhase = 'dusk';
|
||||
if (this.day === 6) {
|
||||
this.rainActive = true;
|
||||
gameEvents.emit('rain', true);
|
||||
}
|
||||
gameEvents.emit('dusk');
|
||||
}
|
||||
|
||||
goToSleep(): void {
|
||||
this.dayPhase = 'sleep';
|
||||
gameEvents.emit('sleep');
|
||||
}
|
||||
|
||||
advanceDay(): void {
|
||||
if (this.day >= 7) return;
|
||||
this.day += 1;
|
||||
this.dayPhase = 'morning';
|
||||
this.duskReached = false;
|
||||
this.boardReadToday = false;
|
||||
this.microCounts = { water: 0, feed: 0, pet: 0 };
|
||||
this.npcTalkCounts = {};
|
||||
this.hasBread = false;
|
||||
this.hasLetters = false;
|
||||
this.breadDelivered = [];
|
||||
this.lettersDelivered = [];
|
||||
this.waterCount = 0;
|
||||
this.greetCount = 0;
|
||||
this.berryCount = 0;
|
||||
this.ribbonCount = 0;
|
||||
this.wellGazeToday = false;
|
||||
this.fogGazeToday = false;
|
||||
this.t14Active = false;
|
||||
this.t14ElapsedMs = 0;
|
||||
this.rainActive = false;
|
||||
if (this.day >= 5 && this.day <= 7) {
|
||||
if (this.happiness > 10) {
|
||||
this.lowHappinessD5to7 = false;
|
||||
this.hadHighHappinessD5to7 = true;
|
||||
}
|
||||
}
|
||||
gameEvents.emit('day', this.day);
|
||||
gameEvents.emit('rain', false);
|
||||
}
|
||||
|
||||
setDay(day: number): void {
|
||||
this.day = Math.max(1, Math.min(7, Math.round(day)));
|
||||
gameEvents.emit('day', this.day);
|
||||
}
|
||||
|
||||
readBoard(): void {
|
||||
this.boardReadToday = true;
|
||||
this.dayPhase = 'day';
|
||||
gameEvents.emit('boardRead', this.getAfuLine());
|
||||
}
|
||||
|
||||
dialogChoice(choice: DialogChoice, npcId?: NpcId): void {
|
||||
if (npcId) {
|
||||
this.npcTalkCounts[npcId] = (this.npcTalkCounts[npcId] ?? 0) + 1;
|
||||
if (this.npcTalkCounts[npcId]! >= 5) {
|
||||
this.applySadness('entangle', npcId);
|
||||
}
|
||||
}
|
||||
if (choice === 'gentle') {
|
||||
this.addHappiness(DIALOG_GENTLE_HAPPINESS);
|
||||
} else if (choice === 'hesitant') {
|
||||
this.addAwareness(DIALOG_HESITANT_AWARENESS);
|
||||
} else if (choice === 'negate') {
|
||||
this.applySadness('negate_dialog', npcId);
|
||||
}
|
||||
this.sentenceCounter += 1;
|
||||
}
|
||||
|
||||
microInteract(type: MicroType): boolean {
|
||||
if (this.microCounts[type] >= MICRO_DAILY_CAP) return false;
|
||||
this.microCounts[type] += 1;
|
||||
this.addHappiness(MICRO_HAPPINESS);
|
||||
gameEvents.emit('micro', type);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Gaze well or fog 5s — once each per day, +3 awareness */
|
||||
gazeWellOrFog(kind: 'well' | 'fog'): boolean {
|
||||
if (kind === 'well') {
|
||||
if (this.wellGazeToday) return false;
|
||||
this.wellGazeToday = true;
|
||||
} else {
|
||||
if (this.fogGazeToday) return false;
|
||||
this.fogGazeToday = true;
|
||||
}
|
||||
this.addAwareness(WELL_FOG_GAZE_AWARENESS);
|
||||
return true;
|
||||
}
|
||||
|
||||
canInteractWellOrFog(): boolean {
|
||||
return this.day >= 5;
|
||||
}
|
||||
|
||||
faceTier(npcId: NpcId): FaceTier {
|
||||
const npc = getNpc(npcId);
|
||||
return faceTierForNpc(this.happiness, npc.offset);
|
||||
}
|
||||
|
||||
nextSentenceIndex(): number {
|
||||
return this.sentenceCounter++;
|
||||
}
|
||||
|
||||
// --- Task progress helpers (inventory survives scene travel) ---
|
||||
takeBread(): boolean {
|
||||
if (this.day !== 1 && this.day !== 4) return false;
|
||||
const taskId = this.day === 4 ? 'T8' : 'T1';
|
||||
if (this.completedTasks.has(taskId as TaskId)) return false;
|
||||
this.hasBread = true;
|
||||
gameEvents.emit('inventory', 'bread', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
takeLetters(): boolean {
|
||||
if (this.day !== 2 && this.day !== 4) return false;
|
||||
// D2 T4; D4 has no letter task but keep day2-only for T4
|
||||
if (this.day === 2 && this.completedTasks.has('T4')) return false;
|
||||
if (this.day !== 2) return false;
|
||||
this.hasLetters = true;
|
||||
gameEvents.emit('inventory', 'letters', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
deliverBread(to: string): boolean {
|
||||
if (!this.hasBread) return false;
|
||||
if (!this.breadDelivered.includes(to)) this.breadDelivered.push(to);
|
||||
if (this.breadDelivered.length >= 3) {
|
||||
this.hasBread = false;
|
||||
const id = this.day === 4 ? 'T8' : 'T1';
|
||||
if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
waterFlower(): void {
|
||||
this.waterCount += 1;
|
||||
this.microInteract('water');
|
||||
if (this.waterCount >= 3) {
|
||||
const id = this.day === 4 ? 'T9' : 'T2';
|
||||
if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId);
|
||||
}
|
||||
}
|
||||
|
||||
greetVillager(): void {
|
||||
this.greetCount += 1;
|
||||
if (this.greetCount >= 3) {
|
||||
const id = this.day === 4 ? 'T10' : 'T3';
|
||||
if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId);
|
||||
}
|
||||
}
|
||||
|
||||
deliverLetter(to: string): boolean {
|
||||
if (!this.hasLetters) return false;
|
||||
if (!this.lettersDelivered.includes(to)) this.lettersDelivered.push(to);
|
||||
if (this.lettersDelivered.length === 3) {
|
||||
this.discoverClue('C4');
|
||||
}
|
||||
if (this.lettersDelivered.length >= 3) {
|
||||
this.hasLetters = false;
|
||||
this.completeTask('T4');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pickBerry(): void {
|
||||
this.berryCount += 1;
|
||||
if (this.berryCount >= 5) this.completeTask('T6');
|
||||
}
|
||||
|
||||
hangRibbon(): void {
|
||||
this.ribbonCount += 1;
|
||||
if (this.ribbonCount >= 4) this.completeTask('T13');
|
||||
}
|
||||
|
||||
startT14(): void {
|
||||
this.t14Active = true;
|
||||
this.t14ElapsedMs = 0;
|
||||
gameEvents.emit('t14Start');
|
||||
}
|
||||
|
||||
finishT14(): void {
|
||||
this.t14Active = false;
|
||||
this.completeTask('T14');
|
||||
gameEvents.emit('t14End');
|
||||
}
|
||||
|
||||
startEnding(choseFogWall = false): EndingId {
|
||||
this.choseFogWall = choseFogWall;
|
||||
if (this.day >= 5 && this.day <= 7 && this.hadHighHappinessD5to7) {
|
||||
this.lowHappinessD5to7 = false;
|
||||
}
|
||||
// Continuously ≤10 means never went above 10 on D5–7
|
||||
const lowOk = this.lowHappinessD5to7 && !this.hadHighHappinessD5to7;
|
||||
const id = evaluateEnding({
|
||||
awareness: this.awareness,
|
||||
happiness: this.happiness,
|
||||
clueCount: this.clues.size,
|
||||
lowHappinessStreakD5to7: lowOk,
|
||||
celebrationEscapes: this.celebrationEscapes,
|
||||
choseFogWall,
|
||||
});
|
||||
this.endingId = id;
|
||||
this.ended = true;
|
||||
this.completeTask('T15');
|
||||
gameEvents.emit('ending', id);
|
||||
return id;
|
||||
}
|
||||
|
||||
forceEnding(id: EndingId): void {
|
||||
this.endingId = id;
|
||||
this.ended = true;
|
||||
gameEvents.emit('ending', id);
|
||||
}
|
||||
|
||||
snapshot(): DirectorSnapshot {
|
||||
return {
|
||||
playerName: this.playerName,
|
||||
happiness: this.happiness,
|
||||
awareness: this.awareness,
|
||||
day: this.day,
|
||||
stage: this.stage,
|
||||
stageParams: this.getStageParams(),
|
||||
petals: this.getPetals(),
|
||||
blackSeeds: this.getBlackSeeds(),
|
||||
clues: [...this.clues],
|
||||
completedTasks: [...this.completedTasks],
|
||||
refusedTasks: [...this.refusedTasks],
|
||||
celebrationEscapes: this.celebrationEscapes,
|
||||
overflowPool: this.overflowPool,
|
||||
sessionStartMs: this.sessionStartMs,
|
||||
dayPhase: this.dayPhase,
|
||||
c9Step: this.c9Step,
|
||||
lowHappinessSinceMs: this.lowHappinessSinceMs,
|
||||
forcedCelebrationActive: this.forcedCelebrationActive,
|
||||
rainActive: this.rainActive,
|
||||
duskReached: this.duskReached,
|
||||
boardReadToday: this.boardReadToday,
|
||||
microCounts: { ...this.microCounts },
|
||||
npcTalkCounts: { ...this.npcTalkCounts },
|
||||
hasBread: this.hasBread,
|
||||
hasLetters: this.hasLetters,
|
||||
breadDelivered: [...this.breadDelivered],
|
||||
lettersDelivered: [...this.lettersDelivered],
|
||||
waterCount: this.waterCount,
|
||||
greetCount: this.greetCount,
|
||||
berryCount: this.berryCount,
|
||||
ribbonCount: this.ribbonCount,
|
||||
lowHappinessD5to7: this.lowHappinessD5to7,
|
||||
wellGazeToday: this.wellGazeToday,
|
||||
fogGazeToday: this.fogGazeToday,
|
||||
t14Active: this.t14Active,
|
||||
t14ElapsedMs: this.t14ElapsedMs,
|
||||
};
|
||||
}
|
||||
|
||||
private emitAll(): void {
|
||||
gameEvents.emit('happiness', this.happiness);
|
||||
gameEvents.emit('awareness', this.awareness);
|
||||
gameEvents.emit('day', this.day);
|
||||
gameEvents.emit('stage', this.stage, this.getStageParams());
|
||||
gameEvents.emit('petals', this.getPetals(), this.getBlackSeeds());
|
||||
}
|
||||
}
|
||||
|
||||
export const Director = new CorruptionDirectorImpl();
|
||||
export type CorruptionDirector = CorruptionDirectorImpl;
|
||||
@@ -0,0 +1,27 @@
|
||||
type Handler = (...args: unknown[]) => void;
|
||||
|
||||
/** Minimal event emitter shared by director & scenes */
|
||||
export class EventBus {
|
||||
private listeners = new Map<string, Set<Handler>>();
|
||||
|
||||
on(event: string, handler: Handler): void {
|
||||
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
|
||||
this.listeners.get(event)!.add(handler);
|
||||
}
|
||||
|
||||
off(event: string, handler: Handler): void {
|
||||
this.listeners.get(event)?.delete(handler);
|
||||
}
|
||||
|
||||
emit(event: string, ...args: unknown[]): void {
|
||||
const set = this.listeners.get(event);
|
||||
if (!set) return;
|
||||
for (const h of set) h(...args);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const gameEvents = new EventBus();
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Tests shipped parseDebugKey — simulates real browser KeyboardEvent shapes.
|
||||
* Shift+1 emits key='!' code='Digit1' (not key='1').
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { parseDebugKey } from './debugKeys';
|
||||
import { DEBUG_TELEPORT_SCENES, SCENE_KEYS } from '../data/constants';
|
||||
import { Director } from './CorruptionDirector';
|
||||
|
||||
describe('parseDebugKey (shipped Debug key map)', () => {
|
||||
it('Shift+Digit1–7 sets day via code even when key is symbol', () => {
|
||||
// Real browser: Shift+1 → key '!', code 'Digit1'
|
||||
expect(
|
||||
parseDebugKey({ key: '!', code: 'Digit1', shiftKey: true }),
|
||||
).toEqual({ type: 'setDay', day: 1 });
|
||||
expect(
|
||||
parseDebugKey({ key: '@', code: 'Digit2', shiftKey: true }),
|
||||
).toEqual({ type: 'setDay', day: 2 });
|
||||
expect(
|
||||
parseDebugKey({ key: '#', code: 'Digit3', shiftKey: true }),
|
||||
).toEqual({ type: 'setDay', day: 3 });
|
||||
expect(
|
||||
parseDebugKey({ key: '$', code: 'Digit4', shiftKey: true }),
|
||||
).toEqual({ type: 'setDay', day: 4 });
|
||||
expect(
|
||||
parseDebugKey({ key: '%', code: 'Digit5', shiftKey: true }),
|
||||
).toEqual({ type: 'setDay', day: 5 });
|
||||
expect(
|
||||
parseDebugKey({ key: '^', code: 'Digit6', shiftKey: true }),
|
||||
).toEqual({ type: 'setDay', day: 6 });
|
||||
expect(
|
||||
parseDebugKey({ key: '&', code: 'Digit7', shiftKey: true }),
|
||||
).toEqual({ type: 'setDay', day: 7 });
|
||||
});
|
||||
|
||||
it('broken old path (shift+key 1-7) would fail — we do not use key for day', () => {
|
||||
// Demonstrates why key-based check is wrong; our code uses Digit*
|
||||
const broken = (ev: { key: string; shiftKey: boolean }) =>
|
||||
ev.shiftKey && ev.key >= '1' && ev.key <= '7';
|
||||
expect(broken({ key: '!', shiftKey: true })).toBe(false);
|
||||
// Our parser still works
|
||||
expect(parseDebugKey({ key: '!', code: 'Digit1', shiftKey: true }).type).toBe('setDay');
|
||||
});
|
||||
|
||||
it('plain Digit0–9 teleports to all 10 scenes', () => {
|
||||
for (let i = 0; i <= 9; i++) {
|
||||
const a = parseDebugKey({
|
||||
key: String(i),
|
||||
code: `Digit${i}`,
|
||||
shiftKey: false,
|
||||
});
|
||||
expect(a).toEqual({
|
||||
type: 'teleport',
|
||||
sceneIndex: i,
|
||||
scene: DEBUG_TELEPORT_SCENES[i],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('Shift+Digit does not teleport (day takes precedence)', () => {
|
||||
const a = parseDebugKey({ key: '!', code: 'Digit1', shiftKey: true });
|
||||
expect(a.type).toBe('setDay');
|
||||
expect(a).not.toMatchObject({ type: 'teleport' });
|
||||
});
|
||||
|
||||
it('H/J/K/L/C/N/P/V/F1–F4', () => {
|
||||
expect(parseDebugKey({ key: 'h', code: 'KeyH', shiftKey: false })).toEqual({
|
||||
type: 'happiness',
|
||||
delta: 10,
|
||||
});
|
||||
expect(parseDebugKey({ key: 'J', code: 'KeyJ', shiftKey: true })).toEqual({
|
||||
type: 'happiness',
|
||||
delta: -10,
|
||||
});
|
||||
expect(parseDebugKey({ key: 'k', code: 'KeyK', shiftKey: false })).toEqual({
|
||||
type: 'awareness',
|
||||
delta: 10,
|
||||
});
|
||||
expect(parseDebugKey({ key: 'c', code: 'KeyC', shiftKey: false }).type).toBe('grantClues');
|
||||
expect(parseDebugKey({ key: 'n', code: 'KeyN', shiftKey: false }).type).toBe('negativeFlash');
|
||||
expect(parseDebugKey({ key: 'p', code: 'KeyP', shiftKey: false }).type).toBe(
|
||||
'forcedCelebration',
|
||||
);
|
||||
expect(parseDebugKey({ key: 'v', code: 'KeyV', shiftKey: false }).type).toBe('teleportCycle');
|
||||
expect(parseDebugKey({ key: 'F1', code: 'F1', shiftKey: false })).toEqual({
|
||||
type: 'forceEnding',
|
||||
id: 'E1',
|
||||
});
|
||||
expect(parseDebugKey({ key: 'F4', code: 'F4', shiftKey: false })).toEqual({
|
||||
type: 'forceEnding',
|
||||
id: 'E4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDebugKey → Director.setDay real path', () => {
|
||||
beforeEach(() => {
|
||||
Director.resetSession('调试');
|
||||
});
|
||||
|
||||
it('applies setDay from Shift+Digit codes into Director', () => {
|
||||
for (let day = 1; day <= 7; day++) {
|
||||
const action = parseDebugKey({
|
||||
key: '!', // wrong symbol-like key as browser would
|
||||
code: `Digit${day}`,
|
||||
shiftKey: true,
|
||||
});
|
||||
expect(action.type).toBe('setDay');
|
||||
if (action.type === 'setDay') {
|
||||
Director.setDay(action.day);
|
||||
expect(Director.day).toBe(day);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('teleport indices map to real scene keys', () => {
|
||||
const a = parseDebugKey({ key: '0', code: 'Digit0', shiftKey: false });
|
||||
expect(a.type).toBe('teleport');
|
||||
if (a.type === 'teleport') {
|
||||
expect(a.scene).toBe(SCENE_KEYS.HOUSE);
|
||||
}
|
||||
const b = parseDebugKey({ key: '1', code: 'Digit1', shiftKey: false });
|
||||
if (b.type === 'teleport') {
|
||||
expect(b.scene).toBe(SCENE_KEYS.PLAZA);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Pure Debug key mapping (§13). Shipped path used by UIScene.
|
||||
*
|
||||
* Important: with Shift held, browsers set `key` to '!'/'@'/… not '1'–'7'.
|
||||
* Day set MUST use `code` Digit1–Digit7.
|
||||
*/
|
||||
|
||||
import { DEBUG_TELEPORT_SCENES } from '../data/constants';
|
||||
import type { EndingId } from '../data/endings';
|
||||
|
||||
export type DebugKeyInput = {
|
||||
key: string;
|
||||
code: string;
|
||||
shiftKey: boolean;
|
||||
};
|
||||
|
||||
export type DebugAction =
|
||||
| { type: 'happiness'; delta: number }
|
||||
| { type: 'awareness'; delta: number }
|
||||
| { type: 'setDay'; day: number }
|
||||
| { type: 'grantClues' }
|
||||
| { type: 'teleport'; sceneIndex: number; scene: string }
|
||||
| { type: 'teleportCycle' }
|
||||
| { type: 'negativeFlash' }
|
||||
| { type: 'forcedCelebration' }
|
||||
| { type: 'forceEnding'; id: EndingId }
|
||||
| { type: 'none' };
|
||||
|
||||
/** Map KeyboardEvent-like input → debug action (no Phaser / no Director). */
|
||||
export function parseDebugKey(ev: DebugKeyInput): DebugAction {
|
||||
const k = ev.key;
|
||||
const code = ev.code;
|
||||
|
||||
if (k === 'h' || k === 'H') return { type: 'happiness', delta: 10 };
|
||||
if (k === 'j' || k === 'J') return { type: 'happiness', delta: -10 };
|
||||
if (k === 'k' || k === 'K') return { type: 'awareness', delta: 10 };
|
||||
if (k === 'l' || k === 'L') return { type: 'awareness', delta: -10 };
|
||||
if (k === 'c' || k === 'C') return { type: 'grantClues' };
|
||||
if (k === 'n' || k === 'N') return { type: 'negativeFlash' };
|
||||
if (k === 'p' || k === 'P') return { type: 'forcedCelebration' };
|
||||
if (k === 'v' || k === 'V') return { type: 'teleportCycle' };
|
||||
if (k === 'F1') return { type: 'forceEnding', id: 'E1' };
|
||||
if (k === 'F2') return { type: 'forceEnding', id: 'E2' };
|
||||
if (k === 'F3') return { type: 'forceEnding', id: 'E3' };
|
||||
if (k === 'F4') return { type: 'forceEnding', id: 'E4' };
|
||||
|
||||
// Shift + Digit1–7 → set day (use code; key becomes !@# with shift)
|
||||
if (ev.shiftKey && /^Digit[1-7]$/.test(code)) {
|
||||
const day = parseInt(code.replace('Digit', ''), 10);
|
||||
return { type: 'setDay', day };
|
||||
}
|
||||
|
||||
// Plain Digit0–9 → teleport (also accept key when no shift)
|
||||
if (!ev.shiftKey && /^Digit[0-9]$/.test(code)) {
|
||||
const idx = parseInt(code.replace('Digit', ''), 10);
|
||||
const scene = DEBUG_TELEPORT_SCENES[idx];
|
||||
if (scene) return { type: 'teleport', sceneIndex: idx, scene };
|
||||
}
|
||||
// Fallback: unshifted key '0'-'9' without relying on code
|
||||
if (!ev.shiftKey && k >= '0' && k <= '9') {
|
||||
const idx = parseInt(k, 10);
|
||||
const scene = DEBUG_TELEPORT_SCENES[idx];
|
||||
if (scene) return { type: 'teleport', sceneIndex: idx, scene };
|
||||
}
|
||||
|
||||
return { type: 'none' };
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
server: { port: 5173 },
|
||||
preview: { port: 4173 },
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||