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

This commit is contained in:
2026-07-15 10:50:12 -04:00
commit 67253bded5
523 changed files with 58879 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
FROM node:22-alpine AS build
WORKDIR /app
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx vite build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
+226
View File
@@ -0,0 +1,226 @@
# PRD:《崩坏童话:蜜糖村》网页版
> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局
## 1. 游戏理念与设计支柱
一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。
四条设计支柱,所有实现争议回到这里裁决:
1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。
2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。
3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。
4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。
## 2. 世界观:三层结构
| 层 | 名称 | 玩家所见 | 实质 |
|---|---|---|---|
| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 |
| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" |
| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 |
命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。
## 3. 主角设定
- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。
- 性别中性像素小人,16×24 像素/帧,无内心独白。
- 对话头像永远同一帧微笑(见支柱 2)。
- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。
## 4. NPC 清单(封闭,7 组 + 群演)
每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。
| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 |
|---|---|---|---|---|---|
| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 |
| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) |
| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 |
| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) |
| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 |
| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 |
| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 |
| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — |
## 5. 七日故事线
每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。
| 日 | 主题 | 剧情要点 | 侵蚀预期 |
|---|---|---|---|
| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 |
| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 |
| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 |
| Day 4 | 完美的重复 | 任务 T8T10 与 Day 1 的 T1T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 |
| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 |
| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 |
| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 |
阿福每日公告(逐字实现,注意措辞逐日滑移):
- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」
- D2「早上好呀!新的一天,新的幸福。祝你开心!」
- D3「早上好!今天要把幸福装得满满的哦。要开心!」
- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同)
- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」
- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」
- D7「庆典开始了。来吧。」
## 6. 核心系统
### 6.1 幸福值(明线)
- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。
- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。
- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。
- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。
- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。
### 6.2 觉知值(暗线,永不显示)
- 范围 0–100,开局 0。只升不降。
- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。
### 6.3 悲伤行为(封闭清单,5 项)
拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。
### 6.4 侵蚀阶段矩阵(唯一真源)
阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。
| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5 | UI |
|---|---|---|---|---|---|---|
| S0 | 020 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 02 瓣 |
| S1 | 2150 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 36 瓣 |
| S2 | 5180 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 |
| S3 | 8199 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 |
| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 |
NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。
### 6.5 文本腐蚀
同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。
### 6.6 BGM 与音效
- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。
- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。
- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。
- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。
## 7. 任务清单(封闭,15 个)
| ID | 日 | 任务 | 幸福奖励 | 备注 |
|---|---|---|---|---|
| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 |
| T2 | D1 | 给广场花坛浇水 ×3 | +10 | |
| T3 | D1 | 与任意三位村民问好 | +10 | |
| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) |
| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) |
| T6 | D3 | 去花田采莓果 ×5 | +10 | |
| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 |
| T8T10 | D4 | 与 T1T3 逐字相同 | 同上 | 刻意恐怖谷 |
| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | |
| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) |
| T13 | D6 | 在广场挂彩带 ×4 | +10 | |
| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 |
| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 |
## 8. 线索清单(封闭,9 处,每处觉知 +8)
| ID | 位置/来源 | 内容 | 可发现时段 |
|---|---|---|---|
| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 |
| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 |
| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 |
| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 |
| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 |
| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5T12 中) |
| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 |
| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 |
| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 |
## 9. 结局(封闭,4 个)
Day 7 庆典按以下优先级判定(自上而下第一个满足者生效):
| 结局 | 条件 | 演出 |
|---|---|---|
| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 |
| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 |
| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 |
| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 |
任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。
## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON
| 场景 | 尺寸(瓦片) | 要点 |
|---|---|---|
| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) |
| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 |
| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) |
| 杂货店(室内) | 15×10 | 货架(C5 |
| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) |
| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 |
| 花田 | 20×14 | 莓果丛(T6)、浇花点 |
| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 |
| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 |
| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 |
统一结构:`ground` / `obstacles``collides=true`/ `objects``spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。
## 11. 美术规范(Aseprite 制作,全部原创)
- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。
- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。
- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。
- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。
- 禁止使用任何现成素材、素材包或开源 tileset。
## 12. 技术架构
- **Phaser 3 + TypeScript + Vite**`npm run build` 产出纯静态 `dist/``npm run preview` 本地游玩。
- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。
- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`
- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFXColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。
- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。
- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。
- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。
- 无 localStorage、无外部网络请求、无外部音频/字体文件。
## 13. Debug 面板(服务验收,必须实现)
按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。
## 14. 验收清单(共 20 条)
1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。
2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。
3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。
4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。
5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。
6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。
7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。
8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。
9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。
10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。
11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。
12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。
13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。
14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。
15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。
16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。
17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。
18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。
19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。
20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。
## 15. 明确不做
战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。
+122
View File
@@ -0,0 +1,122 @@
# 崩坏童话:蜜糖村(网页版)
> v2.0 · 纯单机网页像素游戏 · 无存档 / 无后端 / 无 localStorage · 浏览器打开即玩 · 完整流程约 25–35 分钟,含 4 个结局。
一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。
技术栈:**Phaser 3 + TypeScript + Vite**。逻辑分辨率 320×180,整数倍放大居中,`pixelArt`。全部像素素材与地图为本项目自制(见下文制作流程),无任何外部资产、外部音频或字体文件。
---
## 一、启动方式
```bash
npm install # 安装依赖(phaser / vite / typescript
npm run build # 类型检查 + 产出纯静态 dist/
npm run preview # 本地起服,浏览器打开提示的地址(默认 http://localhost:4173)游玩
# 开发热重载:
npm run dev
```
打开页面 → 输入名字(留空默认「小满」)→ 点「开始疗程」或回车 → 进入 Day 1 小屋。
音频在首次点击 / 按键后开始(浏览器自动播放策略)。
---
## 二、按键
| 操作 | 键 |
|---|---|
| 移动 | `WASD` 或 方向键(96 像素/秒,四向动画 + 待机帧) |
| 交互 / 推进对话 | `E`(靠近可交互对象 ≤24 像素出现「按 E」提示);对话也可鼠标点击推进 |
| 对话选项 | `W`/`S` 或 上/下 选择,`E` 确定(温柔 / 迟疑 / 否定三类,影响幸福与觉知) |
| 睡觉过日 | 走到床边按 `E` |
| Debug 面板 | 反引号键 `` ` `` 开关 |
核心玩法:每日「晨起 → 公告板接任务 → 完成任务 → 黄昏钟声 → 回屋睡觉 → 梦境过场 → 次日」。共 7 天、15 个任务、9 处线索,Day 7 庆典按状态走向四结局之一。
HUD 只有左上角一朵**向日葵表盘**:12 片花瓣按幸福值点亮,满格后花心浮现过熟的黑籽——**全程不显示任何数字**。幸福越高,世界饱和度越高、BGM 越变调变闷、台词越出错、NPC 表情从微笑滑向崩坏。
---
## 三、Debug 面板(`` ` `` 开关,服务验收)
默认关闭、无任何 UI 痕迹,不影响正常游玩。开启后(页面右上角)可:
- **直接设置幸福值**10 / 40 / 70 / 90 / 100 —— 对应侵蚀阶段 S0–S4,可肉眼/肉耳验证饱和度、晕影、NPC 表情档、文本腐蚀密度、BGM 变调与低通。
- **直接设置觉知值**0 / 40 / 70 / 100。
- **设置当前日**:1–7(同步激活当日任务、线索时段门槛)。
- **传送到任意场景**home / plaza / bakery / grocery / clinic / lake / field / well / fog(白色层由结局/梦境使用)。
- **置齐全部线索**:一次点亮 9 处线索。
- **强制触发四个结局**:E1 糖衣 / E2 回收 / E3 苏醒 / E4 静默。
E3 病历会显示你输入的名字、编号 `PT-07` 与**真实游玩时长**(读运行计时),最后一帧主角头像的微笑消失、换成一张疲惫而真实的脸。
---
## 四、素材与地图制作流程(全部原创,无外部资产)
一键重建全部素材与地图:
```bash
npm run assets # = bash tools/build_assets.sh
```
该脚本按顺序执行三步(需本机已装 Aseprite 与 Tiled):
### 1. 像素美术 —— Aseprite`tools/gen_all.lua`
用 Aseprite 命令行批处理 + Lua API **逐像素绘制**并导出 PNG`public/assets/sprites/`):
```bash
aseprite -b --script-param outdir=public/assets/sprites --script tools/gen_all.lua
```
产出(16×16 瓦片 / 16×24 角色,糖果色板 + 白色层独立灰白色板,无抗锯齿、无描边渐变):
- `tileset.png` —— 72 瓦片(草地/石板/湖水/花丛/莓果/树/房屋/室内家具/井/雾/白色层墙地/装饰,≥64)。
- `player.png` —— 主角 16×24,4 朝向 × 4 帧行走(每朝向第 1 帧兼待机)。
- `portraits.png` —— 主角对话头像(永恒微笑)+ E3 专用「真实的脸」。
- `npc_*.png` ×6 —— 阿福/圆圆/大宝/咪咪/芙医生/龟爷爷,各 3 档表情(smile/flat/off),`off` 与 `smile` 仅差 1–2 像素(一只眼睛低 1 像素、嘴角多 1 像素),不画成鬼脸。
- `birds.png` —— 双子鸟 2 帧张嘴;`extras.png` —— 群演 ×5 各 2 帧循环;`sparrow.png` —— E3 窗外的真麻雀。
预览用蒙太奇脚本:`tools/montage.lua`(放大拼图,仅供肉眼校对,不参与打包)。
### 2. 地图 —— Tiled`tools/gen_maps.mjs` → `.tmx` → Tiled 导出 `.json`
Node 脚本生成 10 张地图的 Tiled 源文件(`tools/tmx/*.tmx`,内嵌 tileset),再用 Tiled 命令行导出为 Phaser 可读 JSON`public/assets/maps/`):
```bash
node tools/gen_maps.mjs
tiled --export-map tools/tmx/plaza.tmx public/assets/maps/plaza.json # 每张地图一次
```
统一三层结构:`ground` / `obstacles`(有瓦片即碰撞)/ `objects``spawn`、`door`〔含 target/dest〕、`npc`、`interact`〔含 kind〕)。所有室外场景经村广场互通,门与出入口在成对地图两侧对齐。10 张:小满小屋 12×8、村广场 30×20、面包房 15×10、杂货店 15×10、诊所 12×8、湖畔 24×16、花田 20×14、古井空地 14×10、雾墙边界 20×8、白色层 20×14(走廊+病房)。
### 3. 音频 —— Web Audio 程序合成(`src/core/Audio.ts`
无任何音频文件。8 小节 8-bit 治愈循环(方波主旋律 + 三角波低音)渲染进 `AudioBuffer` 循环播放,`playbackRate` 与 `BiquadFilter` 低通由侵蚀阶段实时驱动(S2 起变慢,S3 加 8kHz 低通,S4 到 0.84 倍 + 4kHz 低通 + 鸟鸣声部静音而双子鸟仍张嘴)。另有 4 个程序合成音效:对话哔、任务完成叮、黄昏钟声、E2 心电滴声。Day 6「保持微笑 60 秒」任务 40–60 秒所有声音渐隐至零(突然的安静)。
---
## 五、代码结构
```
src/
main.ts Phaser 配置、整数缩放、音频阶段联动
core/
Director.ts CorruptionDirector 单例:幸福/觉知/日/任务/线索/侵蚀阶段的唯一真源 + 侵蚀矩阵 + 文本腐蚀
Audio.ts 程序合成 BGM/SFX
quests.ts 公告、NPC 对话装配、15 任务与 9 线索逻辑、结局判定
data/content.ts 全部逐字台词/数值/表格(公告、错字表、任务、线索、结局)
scenes/
BootScene / TitleScene / WorldScene(通用地图场景)/ UIScene(表盘·对话·Debug·梦境·雨·欢乐庆典)/ EndingScene
tools/
gen_all.lua Aseprite 绘制脚本
gen_maps.mjs Tiled .tmx 生成脚本
build_assets.sh 一键重建(Aseprite + Tiled
```
`CorruptionDirector` 是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFXColorMatrix / Vignette),S4 负片闪帧用 ColorMatrix negative 单帧切换。
---
## 六、明确不做(同 PRD §15)
战斗、背包/合成 UI、存档读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。
+34
View File
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ctext y='14' font-size='14'%3E🌻%3C/text%3E%3C/svg%3E" />
<title>崩坏童话:蜜糖村</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background: #101014;
overflow: hidden;
}
#game {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* pixel-perfect scaling handled by Phaser Scale.FIT + ZOOM; canvas stays crisp */
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
}
</style>
</head>
<body>
<div id="game"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+1059
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "honey-village",
"version": "2.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview --port 4173",
"assets": "bash tools/build_assets.sh"
},
"dependencies": {
"phaser": "^3.90.0"
},
"devDependencies": {
"typescript": "^5.6.3",
"vite": "^5.4.10"
}
}
@@ -0,0 +1,153 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[25, 25, 25, 25, 25, 25, 25, 25, 39, 25, 25, 25, 25, 25, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 33, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 25, 25, 25, 25, 25, 25, 40, 25, 25, 25, 25, 25, 25, 25],
"height":10,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":15,
"x":0,
"y":0
},
{
"data":[27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27,
27, 0, 32, 32, 32, 32, 32, 0, 0, 0, 0, 34, 34, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 27, 27, 27, 27, 27, 27, 0, 27, 27, 27, 27, 27, 27, 27],
"height":10,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":15,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"door_plaza",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_bakery"
},
{
"name":"target",
"type":"string",
"value":"plaza"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":112,
"y":144
},
{
"height":16,
"id":2,
"name":"sp_from_plaza",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":112,
"y":128
},
{
"height":16,
"id":3,
"name":"yuan",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"yuan"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":64,
"y":112
},
{
"height":16,
"id":4,
"name":"i_recipe",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"recipe"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":64,
"y":96
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":5,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":15
}
@@ -0,0 +1,149 @@
{ "compressionlevel":-1,
"height":8,
"infinite":false,
"layers":[
{
"data":[25, 25, 25, 25, 25, 25, 39, 25, 25, 25, 25, 25,
25, 26, 38, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 25, 25, 25, 25, 25, 40, 25, 25, 25, 25, 25],
"height":8,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":12,
"x":0,
"y":0
},
{
"data":[27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 27,
27, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 37, 0, 0, 0, 62, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 27, 27, 27, 27, 27, 0, 27, 27, 27, 27, 27],
"height":8,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":12,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"door_plaza",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_clinic"
},
{
"name":"target",
"type":"string",
"value":"plaza"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":96,
"y":112
},
{
"height":16,
"id":2,
"name":"sp_from_plaza",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":96,
"y":96
},
{
"height":16,
"id":3,
"name":"fu",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"fu"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":64,
"y":48
},
{
"height":16,
"id":4,
"name":"i_clinicform",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"clinicform"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":32,
"y":32
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":5,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":12
}
+276
View File
@@ -0,0 +1,276 @@
{ "compressionlevel":-1,
"height":14,
"infinite":false,
"layers":[
{
"data":[2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 40, 1, 65, 1, 1, 1, 1, 1, 2, 1,
65, 1, 9, 1, 1, 1, 2, 1, 1, 65, 3, 1, 1, 1, 1, 2, 1, 1, 65, 1,
1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1,
2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1,
65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1,
1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1,
2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1,
65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1,
1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1,
2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1,
65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1,
1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1,
2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1,
65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1, 1, 1, 1, 1, 2, 1, 1, 65, 1],
"height":14,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 51, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14],
"height":14,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"i_berry0",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"berry"
},
{
"name":"n",
"type":"string",
"value":"0"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":64,
"y":48
},
{
"height":16,
"id":2,
"name":"i_berry1",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"berry"
},
{
"name":"n",
"type":"string",
"value":"1"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":128,
"y":64
},
{
"height":16,
"id":3,
"name":"i_berry2",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"berry"
},
{
"name":"n",
"type":"string",
"value":"2"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":208,
"y":48
},
{
"height":16,
"id":4,
"name":"i_berry3",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"berry"
},
{
"name":"n",
"type":"string",
"value":"3"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":96,
"y":128
},
{
"height":16,
"id":5,
"name":"i_berry4",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"berry"
},
{
"name":"n",
"type":"string",
"value":"4"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":224,
"y":144
},
{
"height":16,
"id":6,
"name":"i_fieldbed",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"flowerbed"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":160,
"y":112
},
{
"height":16,
"id":7,
"name":"door_plaza",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_field"
},
{
"name":"target",
"type":"string",
"value":"plaza"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":160,
"y":0
},
{
"height":16,
"id":8,
"name":"sp_from_plaza",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":160,
"y":32
},
{
"height":16,
"id":9,
"name":"extra5",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"extra5"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":240,
"y":112
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":10,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":20
}
+131
View File
@@ -0,0 +1,131 @@
{ "compressionlevel":-1,
"height":8,
"infinite":false,
"layers":[
{
"data":[17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1,
1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2,
1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1,
1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 1, 1, 2, 1,
2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 40, 1, 1, 1, 2, 1, 1, 1, 1, 1],
"height":8,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14],
"height":8,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"door_plaza",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_fog"
},
{
"name":"target",
"type":"string",
"value":"plaza"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":160,
"y":112
},
{
"height":16,
"id":2,
"name":"sp_from_plaza",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":160,
"y":80
},
{
"height":16,
"id":3,
"name":"i_fog",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"fog"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":160,
"y":48
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":4,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":20
}
@@ -0,0 +1,153 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 39, 25, 25, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25,
25, 25, 25, 25, 25, 25, 25, 40, 25, 25, 25, 25, 25, 25, 25],
"height":10,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":15,
"x":0,
"y":0
},
{
"data":[27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27,
27, 0, 35, 35, 35, 35, 35, 35, 35, 35, 35, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 58, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 27, 27, 27, 27, 27, 27, 0, 27, 27, 27, 27, 27, 27, 27],
"height":10,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":15,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"door_plaza",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_grocery"
},
{
"name":"target",
"type":"string",
"value":"plaza"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":112,
"y":144
},
{
"height":16,
"id":2,
"name":"sp_from_plaza",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":112,
"y":128
},
{
"height":16,
"id":3,
"name":"mimi",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"mimi"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":64,
"y":112
},
{
"height":16,
"id":4,
"name":"i_shelf",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"shelf"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":96,
"y":32
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":5,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":15
}
+161
View File
@@ -0,0 +1,161 @@
{ "compressionlevel":-1,
"height":8,
"infinite":false,
"layers":[
{
"data":[25, 25, 30, 25, 39, 25, 25, 39, 25, 25, 25, 25,
25, 25, 25, 25, 25, 63, 63, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 40, 25, 25, 25, 25, 25],
"height":8,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":12,
"x":0,
"y":0
},
{
"data":[27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27,
27, 29, 0, 0, 0, 0, 0, 0, 62, 59, 0, 27,
27, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 36, 37, 0, 27,
27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27,
27, 27, 27, 27, 27, 27, 0, 27, 27, 27, 27, 27],
"height":8,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":12,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"door_plaza",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_home"
},
{
"name":"target",
"type":"string",
"value":"plaza"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":96,
"y":112
},
{
"height":16,
"id":2,
"name":"start",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":48,
"y":48
},
{
"height":16,
"id":3,
"name":"sp_from_plaza",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":96,
"y":96
},
{
"height":16,
"id":4,
"name":"i_bed",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"bed"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":32,
"y":32
},
{
"height":16,
"id":5,
"name":"i_calendar",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"calendar"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":32,
"y":16
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":6,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":12
}
+183
View File
@@ -0,0 +1,183 @@
{ "compressionlevel":-1,
"height":16,
"infinite":false,
"layers":[
{
"data":[2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 40, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 9, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1,
1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 9, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1,
1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1,
1, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 1,
1, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 1,
1, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 1,
1, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 1,
2, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 1,
1, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 1,
1, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 1],
"height":16,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":24,
"x":0,
"y":0
},
{
"data":[14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 5, 5, 55, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 55, 5, 5, 14,
14, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 54, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14,
14, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14,
14, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 14,
14, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 14,
14, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14],
"height":16,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":24,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"door_plaza",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_lake"
},
{
"name":"target",
"type":"string",
"value":"plaza"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":192,
"y":0
},
{
"height":16,
"id":2,
"name":"sp_from_plaza",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":192,
"y":32
},
{
"height":16,
"id":3,
"name":"gui",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"gui"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":80,
"y":112
},
{
"height":16,
"id":4,
"name":"extra4",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"extra4"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":240,
"y":48
},
{
"height":16,
"id":5,
"name":"i_picnic",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"picnic"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":256,
"y":96
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":6,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":24
}
+673
View File
@@ -0,0 +1,673 @@
{ "compressionlevel":-1,
"height":20,
"infinite":false,
"layers":[
{
"data":[2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1,
1, 18, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 13, 1, 1, 1, 1, 1, 13, 1, 1, 1, 1, 1, 1, 13, 1, 1, 1, 2, 1, 1, 13, 9, 1, 1, 1, 1, 1,
1, 2, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 2, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 9, 9, 9, 1, 1, 1, 1,
1, 1, 1, 3, 2, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 3, 1, 1, 2, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1,
1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 1,
40, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 40,
2, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1,
1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 72, 72, 72, 72, 72, 72, 72, 72, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 72, 72, 72, 72, 72, 72, 72, 72, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 72, 72, 72, 72, 72, 72, 72, 72, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 72, 72, 72, 72, 72, 72, 72, 72, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 3, 1, 1, 1, 1,
1, 1, 2, 1, 40, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 40, 1, 1, 1, 1],
"height":20,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"data":[14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 10, 12, 12, 12, 0, 0, 0, 12, 12, 12, 0, 0, 0, 0, 12, 12, 12, 0, 0, 0, 0, 12, 12, 12, 0, 0, 10, 10, 14,
14, 0, 11, 12, 11, 0, 0, 0, 11, 12, 11, 0, 0, 0, 0, 11, 12, 11, 0, 0, 0, 0, 11, 12, 11, 0, 0, 0, 0, 14,
14, 0, 11, 0, 11, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 48, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 14,
14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14],
"height":20,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"door_home",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_plaza"
},
{
"name":"target",
"type":"string",
"value":"home"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":48,
"y":48
},
{
"height":16,
"id":2,
"name":"sp_from_home",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":48,
"y":80
},
{
"height":16,
"id":3,
"name":"door_bakery",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_plaza"
},
{
"name":"target",
"type":"string",
"value":"bakery"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":144,
"y":48
},
{
"height":16,
"id":4,
"name":"sp_from_bakery",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":144,
"y":80
},
{
"height":16,
"id":5,
"name":"door_grocery",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_plaza"
},
{
"name":"target",
"type":"string",
"value":"grocery"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":256,
"y":48
},
{
"height":16,
"id":6,
"name":"sp_from_grocery",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":256,
"y":80
},
{
"height":16,
"id":7,
"name":"door_clinic",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_plaza"
},
{
"name":"target",
"type":"string",
"value":"clinic"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":368,
"y":48
},
{
"height":16,
"id":8,
"name":"sp_from_clinic",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":368,
"y":80
},
{
"height":16,
"id":9,
"name":"i_mailbox",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"mailbox"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":240,
"y":256
},
{
"height":16,
"id":10,
"name":"door_field",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_plaza"
},
{
"name":"target",
"type":"string",
"value":"field"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":0,
"y":160
},
{
"height":16,
"id":11,
"name":"sp_from_field",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":32,
"y":160
},
{
"height":16,
"id":12,
"name":"door_lake",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_plaza"
},
{
"name":"target",
"type":"string",
"value":"lake"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":464,
"y":160
},
{
"height":16,
"id":13,
"name":"sp_from_lake",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":432,
"y":160
},
{
"height":16,
"id":14,
"name":"door_well",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_plaza"
},
{
"name":"target",
"type":"string",
"value":"well"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":64,
"y":304
},
{
"height":16,
"id":15,
"name":"sp_from_well",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":64,
"y":272
},
{
"height":16,
"id":16,
"name":"door_fog",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_plaza"
},
{
"name":"target",
"type":"string",
"value":"fog"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":400,
"y":304
},
{
"height":16,
"id":17,
"name":"sp_from_fog",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":400,
"y":272
},
{
"height":16,
"id":18,
"name":"afu",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"afu"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":224,
"y":144
},
{
"height":16,
"id":19,
"name":"dabao",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"dabao"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":240,
"y":288
},
{
"height":16,
"id":20,
"name":"bird1",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"bird1"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":368,
"y":80
},
{
"height":16,
"id":21,
"name":"bird2",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"bird2"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":400,
"y":80
},
{
"height":16,
"id":22,
"name":"extra1",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"extra1"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":160,
"y":240
},
{
"height":16,
"id":23,
"name":"extra2",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"extra2"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":304,
"y":256
},
{
"height":16,
"id":24,
"name":"extra3",
"opacity":1,
"properties":[
{
"name":"npc",
"type":"string",
"value":"extra3"
}],
"rotation":0,
"type":"npc",
"visible":true,
"width":16,
"x":96,
"y":96
},
{
"height":16,
"id":25,
"name":"i_board",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"board"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":224,
"y":144
},
{
"height":16,
"id":26,
"name":"i_flowerbed",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"flowerbed"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":96,
"y":224
},
{
"height":16,
"id":27,
"name":"i_festival",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"festival"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":224,
"y":224
},
{
"height":16,
"id":28,
"name":"i_ribbon0",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"ribbon"
},
{
"name":"n",
"type":"string",
"value":"0"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":192,
"y":208
},
{
"height":16,
"id":29,
"name":"i_ribbon1",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"ribbon"
},
{
"name":"n",
"type":"string",
"value":"1"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":224,
"y":208
},
{
"height":16,
"id":30,
"name":"i_ribbon2",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"ribbon"
},
{
"name":"n",
"type":"string",
"value":"2"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":256,
"y":208
},
{
"height":16,
"id":31,
"name":"i_ribbon3",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"ribbon"
},
{
"name":"n",
"type":"string",
"value":"3"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":288,
"y":208
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":32,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":30
}
+135
View File
@@ -0,0 +1,135 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[2, 1, 1, 1, 1, 2, 1, 40, 1, 1, 2, 1, 1, 1,
1, 1, 1, 1, 2, 1, 1, 3, 1, 2, 1, 1, 1, 1,
1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2,
1, 1, 2, 1, 1, 24, 24, 24, 24, 1, 1, 1, 2, 1,
1, 2, 1, 1, 1, 24, 24, 24, 24, 1, 1, 2, 1, 1,
2, 1, 1, 1, 1, 24, 24, 24, 24, 1, 2, 1, 1, 1,
1, 1, 1, 1, 2, 24, 24, 24, 24, 2, 1, 1, 1, 1,
1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2,
1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1,
1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1],
"height":10,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":14,
"x":0,
"y":0
},
{
"data":[14, 14, 14, 14, 14, 14, 14, 0, 14, 14, 14, 14, 14, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 23, 0, 14,
14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14],
"height":10,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":14,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"door_plaza",
"opacity":1,
"properties":[
{
"name":"dest",
"type":"string",
"value":"sp_from_well"
},
{
"name":"target",
"type":"string",
"value":"plaza"
}],
"rotation":0,
"type":"door",
"visible":true,
"width":16,
"x":112,
"y":0
},
{
"height":16,
"id":2,
"name":"sp_from_plaza",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":112,
"y":32
},
{
"height":16,
"id":3,
"name":"i_well",
"opacity":1,
"properties":[
{
"name":"kind",
"type":"string",
"value":"well"
}],
"rotation":0,
"type":"interact",
"visible":true,
"width":16,
"x":112,
"y":80
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":4,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":14
}
+114
View File
@@ -0,0 +1,114 @@
{ "compressionlevel":-1,
"height":14,
"infinite":false,
"layers":[
{
"data":[43, 43, 43, 43, 46, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 46, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 45, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42],
"height":14,
"id":1,
"name":"ground",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 0, 41, 41, 41, 41, 41, 41, 41, 41, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41],
"height":14,
"id":2,
"name":"obstacles",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"objects",
"objects":[
{
"height":16,
"id":1,
"name":"sp_corridor",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":160,
"y":32
},
{
"height":16,
"id":2,
"name":"sp_ward",
"opacity":1,
"rotation":0,
"type":"spawn",
"visible":true,
"width":16,
"x":160,
"y":176
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":3,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.12.2",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"..\/sprites\/tileset.png",
"imageheight":144,
"imagewidth":128,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":72,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":"1.10",
"width":20
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+131
View File
@@ -0,0 +1,131 @@
import type { StageParams } from './Director';
// §6.6 程序合成:8 小节 8-bit 治愈循环(方波主旋律 + 三角波低音)+ 鸟鸣声部 + 4 音效。无外部音频文件。
const BPM = 100;
const BEAT = 60 / BPM; // 0.6s
const LOOP_BEATS = 32; // 8 小节 × 4 拍
const LOOP_SEC = LOOP_BEATS * BEAT; // 19.2s
const SR = 44100;
const midi = (n: number) => 440 * Math.pow(2, (n - 69) / 12);
// 主旋律(32 个四分音符,C 大调,明亮)
const LEAD = [72, 76, 79, 76, 77, 76, 74, 72, 74, 76, 77, 79, 76, 74, 72, 74,
79, 81, 79, 77, 76, 77, 79, 76, 72, 74, 76, 77, 79, 76, 72, 72].map(midi);
// 低音(每小节 2 个半音符,I–vi–IV–V)
const BASS_ROOTS = [48, 45, 41, 43, 48, 45, 41, 43].map(midi);
function square(ph: number) { return Math.sin(ph) >= 0 ? 1 : -1; }
function triangle(ph: number) { return (2 / Math.PI) * Math.asin(Math.sin(ph)); }
function renderMusic(): AudioBuffer {
const buf = new AudioBuffer({ length: Math.floor(LOOP_SEC * SR), sampleRate: SR, numberOfChannels: 1 });
const d = buf.getChannelData(0);
const addNote = (freq: number, start: number, dur: number, amp: number, wave: (p: number) => number) => {
const s0 = Math.floor(start * SR), s1 = Math.floor((start + dur) * SR);
for (let i = s0; i < s1 && i < d.length; i++) {
const t = (i - s0) / SR;
// 简单 AD 包络
const env = Math.min(1, t / 0.01) * Math.exp(-t * 3.2);
d[i] += amp * env * wave(2 * Math.PI * freq * (i / SR));
}
};
// lead: 四分音符
for (let i = 0; i < LEAD.length; i++) addNote(LEAD[i], i * BEAT, BEAT * 0.9, 0.12, square);
// bass: 每小节两个半音符
for (let b = 0; b < 8; b++) {
const root = BASS_ROOTS[b];
addNote(root, b * 4 * BEAT, BEAT * 2 * 0.95, 0.11, triangle);
addNote(root, b * 4 * BEAT + 2 * BEAT, BEAT * 2 * 0.95, 0.11, triangle);
}
return buf;
}
function renderBirds(): AudioBuffer {
const buf = new AudioBuffer({ length: Math.floor(LOOP_SEC * SR), sampleRate: SR, numberOfChannels: 1 });
const d = buf.getChannelData(0);
const chirp = (start: number) => {
const dur = 0.09;
const s0 = Math.floor(start * SR), s1 = Math.floor((start + dur) * SR);
for (let i = s0; i < s1 && i < d.length; i++) {
const t = (i - s0) / SR;
const f = 2400 + 900 * t / dur; // 上滑
const env = Math.min(1, t / 0.005) * Math.exp(-t * 22);
d[i] += 0.07 * env * Math.sin(2 * Math.PI * f * t);
}
};
// 每小节两声鸟鸣
for (let b = 0; b < 8; b++) { chirp(b * 4 * BEAT + 0.5); chirp(b * 4 * BEAT + 2.3); }
return buf;
}
class AudioEngine {
ctx: AudioContext | null = null;
private master!: GainNode;
private musicGain!: GainNode;
private birdGain!: GainNode;
private filter!: BiquadFilterNode;
private musicSrc: AudioBufferSourceNode | null = null;
private birdSrc: AudioBufferSourceNode | null = null;
private musicBuf!: AudioBuffer;
private birdBuf!: AudioBuffer;
private started = false;
private baseMaster = 0.5;
private birdLevel = 1.0;
start() {
if (this.started) { this.ctx?.resume(); return; }
this.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
const ctx = this.ctx;
this.master = ctx.createGain(); this.master.gain.value = this.baseMaster; this.master.connect(ctx.destination);
this.filter = ctx.createBiquadFilter(); this.filter.type = 'lowpass'; this.filter.frequency.value = 20000; this.filter.connect(this.master);
this.musicGain = ctx.createGain(); this.musicGain.gain.value = 1; this.musicGain.connect(this.filter);
this.birdGain = ctx.createGain(); this.birdGain.gain.value = this.birdLevel; this.birdGain.connect(this.filter);
this.musicBuf = renderMusic(); this.birdBuf = renderBirds();
this._spawnSources();
this.started = true;
}
private _spawnSources() {
if (!this.ctx) return;
this.musicSrc = this.ctx.createBufferSource(); this.musicSrc.buffer = this.musicBuf; this.musicSrc.loop = true; this.musicSrc.connect(this.musicGain); this.musicSrc.start();
this.birdSrc = this.ctx.createBufferSource(); this.birdSrc.buffer = this.birdBuf; this.birdSrc.loop = true; this.birdSrc.connect(this.birdGain); this.birdSrc.start();
}
setStage(p: StageParams) {
if (!this.ctx) return;
const now = this.ctx.currentTime;
this.musicSrc?.playbackRate.setTargetAtTime(p.playbackRate, now, 0.4);
this.birdSrc?.playbackRate.setTargetAtTime(p.playbackRate, now, 0.4);
this.filter.frequency.setTargetAtTime(p.lowpass > 0 ? p.lowpass : 20000, now, 0.4);
this.birdGain.gain.setTargetAtTime(p.birdMute ? 0 : this.birdLevel, now, 0.4);
}
/** T14「突然的安静」:淡入淡出主音量 */
fadeMaster(level: number, seconds: number) {
if (!this.ctx) return;
const now = this.ctx.currentTime;
this.master.gain.cancelScheduledValues(now);
this.master.gain.setValueAtTime(this.master.gain.value, now);
this.master.gain.linearRampToValueAtTime(level, now + seconds);
}
restoreMaster(seconds = 1) { this.fadeMaster(this.baseMaster, seconds); }
// ---- SFX(全部程序合成)----
private blip(freq: number, dur: number, type: OscillatorType, amp = 0.3, slideTo?: number) {
if (!this.ctx) return;
const ctx = this.ctx, now = ctx.currentTime;
const o = ctx.createOscillator(), g = ctx.createGain();
o.type = type; o.frequency.setValueAtTime(freq, now);
if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, now + dur);
g.gain.setValueAtTime(0.0001, now);
g.gain.exponentialRampToValueAtTime(amp, now + 0.008);
g.gain.exponentialRampToValueAtTime(0.0001, now + dur);
o.connect(g); g.connect(this.master); o.start(now); o.stop(now + dur + 0.02);
}
sfxBeep() { this.blip(620, 0.05, 'square', 0.12); } // 对话哔哔
sfxDing() { this.blip(880, 0.12, 'square', 0.22); setTimeout(() => this.blip(1320, 0.18, 'square', 0.2), 90); } // 任务完成叮
sfxBell() { this.blip(392, 1.6, 'triangle', 0.3); setTimeout(() => this.blip(261, 2.0, 'triangle', 0.25), 120); } // 黄昏钟声
sfxEcg() { this.blip(1000, 0.15, 'sine', 0.35); } // E2 心电监护滴声
}
export const Audio = new AudioEngine();
+195
View File
@@ -0,0 +1,195 @@
import Phaser from 'phaser';
import { NPCS, TYPO } from '../data/content';
// Corruption stage params (§6.4 唯一真源)
export interface StageParams {
stage: number;
saturation: number; // multiplier applied to ColorMatrix
vignette: number; // 0..1 strength
playbackRate: number;
lowpass: number; // Hz, 0 = off (open)
birdMute: boolean;
negativePerMin: number; // random single-frame negative
offFlashPerMin: number; // NPC off-expression flashes
}
const MATRIX: StageParams[] = [
{ stage: 0, saturation: 0.90, vignette: 0.0, playbackRate: 1.00, lowpass: 0, birdMute: false, negativePerMin: 0, offFlashPerMin: 0 },
{ stage: 1, saturation: 1.00, vignette: 0.0, playbackRate: 1.00, lowpass: 0, birdMute: false, negativePerMin: 0, offFlashPerMin: 0 },
{ stage: 2, saturation: 1.15, vignette: 0.0, playbackRate: 0.97, lowpass: 0, birdMute: false, negativePerMin: 0, offFlashPerMin: 0 },
{ stage: 3, saturation: 1.30, vignette: 0.30, playbackRate: 0.92, lowpass: 8000, birdMute: false, negativePerMin: 0, offFlashPerMin: 0 },
{ stage: 4, saturation: 1.40, vignette: 0.55, playbackRate: 0.84, lowpass: 4000, birdMute: true, negativePerMin: 0.4, offFlashPerMin: 0.6 },
];
export function stageFromHappiness(h: number): number {
if (h <= 20) return 0;
if (h <= 50) return 1;
if (h <= 80) return 2;
if (h <= 99) return 3;
return 4;
}
/** CorruptionDirector — single source of truth for all state + AV params (§12). */
class DirectorClass extends Phaser.Events.EventEmitter {
playerName = '小满';
day = 1;
happiness = 30;
awareness = 0;
clues = new Set<string>();
tasks: Record<string, 'idle' | 'active' | 'done' | 'refused'> = {};
flags: Record<string, any> = {};
escapeCount = 0;
overflowPool = 0;
startTime = 0;
// daily
greetedToday = new Set<string>();
talkCounts: Record<string, number> = {};
smallInteract: Record<string, number> = {}; // water/feed/pat -> count (max 3)
gazedToday = new Set<string>(); // well/fog
refusedTasks = new Set<string>();
lowHappySince = 0; // timestamp when happiness dropped below 30 (0 = not low)
endingShown = false;
reset(name: string) {
this.playerName = name && name.trim() ? name.trim() : '小满';
this.day = 1;
this.happiness = 30;
this.awareness = 0;
this.clues = new Set();
this.tasks = {};
this.flags = {};
this.escapeCount = 0;
this.overflowPool = 0;
this.startTime = performance.now();
this.greetedToday = new Set();
this.talkCounts = {};
this.smallInteract = {};
this.gazedToday = new Set();
this.refusedTasks = new Set();
this.lowHappySince = 0;
this.endingShown = false;
this.emit('change');
this.emit('stage', this.stage);
}
resetDaily() {
this.greetedToday = new Set();
this.talkCounts = {};
this.smallInteract = {};
this.gazedToday = new Set();
}
get stage(): number { return stageFromHappiness(this.happiness); }
get params(): StageParams { return MATRIX[this.stage]; }
/** petals lit 0..12 (§6.1 向日葵表盘) */
get petals(): number { return Phaser.Math.Clamp(Math.round((this.happiness / 100) * 12), 0, 12); }
get blackSeeds(): boolean { return this.happiness >= 100; }
private _clamp() {
this.happiness = Phaser.Math.Clamp(this.happiness, 0, 100);
this.awareness = Phaser.Math.Clamp(this.awareness, 0, 100);
}
addHappiness(n: number) {
const prevStage = this.stage;
if (this.happiness >= 100 && n < 0) {
// 满格强制维持:小额扣分累积进溢出池 (§6.1)
this.overflowPool += -n;
if (this.overflowPool >= 15) { this.overflowPool -= 15; this.emit('negativeFlash'); }
} else {
this.happiness += n;
this._clamp();
}
this._afterHappyChange(prevStage);
}
setHappiness(v: number) {
const prevStage = this.stage;
this.happiness = Phaser.Math.Clamp(v, 0, 100);
this._afterHappyChange(prevStage);
}
private _afterHappyChange(prevStage: number) {
this._clamp();
if (this.happiness < 30) { if (!this.lowHappySince) this.lowHappySince = performance.now(); }
else this.lowHappySince = 0;
this.emit('change');
if (this.stage !== prevStage) this.emit('stage', this.stage);
}
addAwareness(n: number) { this.awareness += n; this._clamp(); this.emit('change'); }
setAwareness(v: number) { this.awareness = Phaser.Math.Clamp(v, 0, 100); this.emit('change'); }
discoverClue(id: string): boolean {
if (this.clues.has(id)) return false;
this.clues.add(id);
this.addAwareness(8); // §8 每处 +8
this.emit('clue', id);
return true;
}
/** sad behavior (§6.3): 幸福 5, 觉知 +3, 最近 NPC 转头 */
sadBehavior(reason: string) {
this.addHappiness(-5);
this.addAwareness(3);
this.emit('sad', reason);
}
/** NPC effective stage with individual offset (§6.4) */
npcStage(npcId: string): number {
const npc = NPCS[npcId];
if (!npc || npc.participates === false) return 0;
const eff = Phaser.Math.Clamp(this.happiness - npc.offset, 0, 100);
return stageFromHappiness(eff);
}
/** expression for an NPC given current state (§6.4). off flashes handled by caller. */
npcExpression(npcId: string): 'smile' | 'flat' | 'off' {
const s = this.npcStage(npcId);
if (s <= 2) return 'smile';
return 'flat';
}
playtimeString(): string {
const ms = performance.now() - this.startTime;
const total = Math.floor(ms / 1000);
const mm = Math.floor(total / 60).toString().padStart(2, '0');
const ss = (total % 60).toString().padStart(2, '0');
return `${mm}:${ss}`;
}
// ---- text corruption (§6.5) applied at render time ----
private lineCounter = 0;
corrupt(text: string, speaker: string): string {
this.lineCounter++;
const s = this.stage;
const isMimi = speaker === '咪咪';
let out = text;
if (s >= 3) {
// homophone typo. Mimi always; others at density (~1 per 50 lines) unless S4.
const applyTypo = isMimi || s >= 4 || (this.lineCounter % 50 === 0);
if (applyTypo) {
const limit = s >= 4 ? TYPO.length : (isMimi ? TYPO.length : 1);
let done = 0;
for (const [from, to] of TYPO) {
if (done >= limit) break;
if (out.includes(from)) { out = out.split(from).join(to); done++; }
}
}
}
if (s >= 2) {
// 偶发标点重复 (S2 每80句1次); S4 咪咪句尾标点 x3
if (s >= 4 && isMimi) {
out = out.replace(/([。!?,])\s*$/g, (m, p) => p + p + p);
out = out.replace(/([。!?])/g, (m, p) => p + p + p);
} else if (this.lineCounter % 80 === 0) {
out = out.replace(/([。!?])/, (m, p) => p + p);
}
}
return out;
}
}
export const Director = new DirectorClass();
export type { DirectorClass };
+252
View File
@@ -0,0 +1,252 @@
import { Director } from './Director';
import { Audio } from './Audio';
import { ANNOUNCE, CLUE_TEXT, DAY_TASKS, GREET, NPCS, TASKS } from '../data/content';
export type DChoiceOpt = { label: string; tone: 'gentle' | 'hesitant' | 'deny'; effect?: () => void; then?: DNode[] };
export type DLine = { speaker: string; text: string; npcId?: string; raw?: boolean };
export type DChoice = { choice: DChoiceOpt[] };
export type DNode = DLine | DChoice;
function line(speaker: string, text: string, npcId?: string, raw = false): DLine { return { speaker, text, npcId, raw }; }
function narr(text: string): DLine { return { speaker: '', text }; }
export function taskActive(id: string) { return Director.tasks[id] === 'active'; }
export function completeTask(id: string) {
if (Director.tasks[id] !== 'active') return;
Director.tasks[id] = 'done';
Director.addHappiness(TASKS[id].reward);
Director.emit('toast', `任务完成 · ${TASKS[id].title} +${TASKS[id].reward}`);
Audio.sfxDing();
checkDusk();
}
export function refuseTask(id: string) {
if (Director.tasks[id] !== 'active') return;
Director.tasks[id] = 'refused';
Director.refusedTasks.add(id);
Director.sadBehavior('拒绝任务');
Director.emit('toast', '(拒绝被记录了。)');
checkDusk();
}
function checkDusk() {
const ts = DAY_TASKS[Director.day] || [];
if (ts.every((id) => Director.tasks[id] === 'done' || Director.tasks[id] === 'refused')) {
if (!Director.flags['dusk' + Director.day]) { Director.flags['dusk' + Director.day] = true; Director.emit('dusk'); }
}
}
export function activateDay(day: number) {
Director.day = day;
Director.resetDaily();
for (const id of DAY_TASKS[day] || []) if (!Director.tasks[id] || Director.tasks[id] === 'idle') Director.tasks[id] = 'active';
Director.emit('change');
}
const breadTask = () => (Director.day === 4 ? 'T8' : Director.day === 1 ? 'T1' : null);
const waterTask = () => (Director.day === 4 ? 'T9' : Director.day === 1 ? 'T2' : null);
const greetTask = () => (Director.day === 4 ? 'T10' : Director.day === 1 ? 'T3' : null);
// ---------------- 公告板 ----------------
export function boardScript(): DNode[] {
const s: DNode[] = [];
s.push(line('阿福', ANNOUNCE[Director.day], 'afu', true)); // 逐字,不腐蚀
const ts = DAY_TASKS[Director.day] || [];
const pending = ts.filter((id) => Director.tasks[id] === 'active');
if (pending.length) {
s.push(narr('今日幸福任务:'));
for (const id of pending) s.push(narr(`· ${TASKS[id].title}+${TASKS[id].reward}`));
} else if (Director.day < 7) {
s.push(narr('今天的任务都完成啦。'));
}
// Day5 擦公告板 (T12) —— 擦到一半浮现旧字迹 (C6)
if (Director.day === 5 && taskActive('T12')) {
s.push({
choice: [
{ label: '(擦拭公告板)', tone: 'gentle', effect: () => { discover('C6'); completeTask('T12'); } , then: CLUE_TEXT.C6.map(narr) },
{ label: '(不想擦)', tone: 'deny', effect: () => refuseTask('T12') },
],
});
}
return s;
}
function discover(id: string) { if (Director.discoverClue(id)) Director.emit('toast', '(你注意到了什么……)'); }
// ---------------- NPC 对话 ----------------
export function talkScript(npcId: string): DNode[] {
const def = NPCS[npcId];
const name = def?.name || npcId;
const s: DNode[] = [];
const first = !Director.greetedToday.has(npcId);
Director.greetedToday.add(npcId);
Director.talkCounts[npcId] = (Director.talkCounts[npcId] || 0) + 1;
const g = GREET[npcId] || ['……'];
s.push(line(name, g[(Director.talkCounts[npcId] - 1) % g.length], npcId));
// ---- 任务钩子 ----
const bt = breadTask();
const lettersActive = taskActive('T4');
if (npcId === 'yuan') {
if (bt && taskActive(bt)) {
const f = flag('bread' + Director.day, { taken: false, del: [] as string[] });
if (!f.taken) { f.taken = true; s.push(line('圆圆', '给,热乎的蜂蜜面包。送给大宝、咪咪和龟爷爷吧。', 'yuan')); }
}
if (lettersActive && letterCarry()) deliverLetter(s, 'yuan', '圆圆');
if (Director.day === 5 && taskActive('T11')) {
s.push({
choice: [
{ label: '好呀,我来帮你揉面。', tone: 'gentle', effect: () => completeTask('T11'), then: [line('圆圆', '谢谢你,面团都笑了呢。', 'yuan')] },
{ label: '今天……我想歇一歇。', tone: 'deny', effect: () => refuseTask('T11'), then: [line('圆圆', '没关系的,休息也很好。', 'yuan')] },
],
});
}
} else if (npcId === 'dabao') {
if (bt && taskActive(bt)) deliverBread(s, 'dabao', '大宝');
if (lettersActive) {
const f = flag('letters', { taken: false, del: [] as string[] });
if (!f.taken) { f.taken = true; s.push(line('大宝', '这三封信,麻烦你送给圆圆、咪咪和芙医生啦!', 'dabao')); }
}
if (Director.day >= 3) s.push(line('大宝', '奇怪……这些信的邮戳,怎么都是同一天呢?', 'dabao'));
} else if (npcId === 'mimi') {
if (bt && taskActive(bt)) deliverBread(s, 'mimi', '咪咪');
if (lettersActive && letterCarry()) deliverLetter(s, 'mimi', '咪咪');
} else if (npcId === 'gui') {
if (bt && taskActive(bt)) deliverBread(s, 'gui', '龟爷爷');
// C9 三段递进(D2/D4/D6 起各一段);C9 作为 9 处线索之一,最终段 +8
if (!Director.flags.seen_C9a && Director.day >= 2) c9(s, 'C9a');
else if (Director.flags.seen_C9a && !Director.flags.seen_C9b && Director.day >= 4) c9(s, 'C9b');
else if (Director.flags.seen_C9b && !Director.flags.seen_C9c && Director.day >= 6) c9(s, 'C9c');
} else if (npcId === 'fu') {
if (lettersActive && letterCarry()) {
// 第三封信(给芙医生)—— 署名乱码 C4
deliverLetter(s, 'fu', '芙医生', true);
}
}
// T3/T10 与三位村民问好
const gt = greetTask();
if (gt && taskActive(gt) && Director.greetedToday.size >= 3) completeTaskDeferred(gt, s);
// 纠缠:一天内对同一 NPC ≥5 次
if (Director.talkCounts[npcId] === 5) Director.sadBehavior('纠缠');
return s;
}
function completeTaskDeferred(id: string, s: DNode[]) { s.push({ choice: [{ label: '(继续)', tone: 'gentle', effect: () => completeTask(id) }] } as any); }
function c9(s: DNode[], id: 'C9a' | 'C9b' | 'C9c') {
for (const t of CLUE_TEXT[id]) s.push(line('龟爷爷', t, 'gui'));
Director.flags['seen_' + id] = true;
if (id === 'C9c') discover('C9'); // 链完成 = 第 9 处线索 +8
}
function letterCarry() { const f = Director.flags['letters']; return f && f.taken; }
function deliverBread(s: DNode[], target: string, disp: string) {
const bt = breadTask(); if (!bt) return;
const f = flag('bread' + Director.day, { taken: false, del: [] as string[] });
if (!f.taken || f.del.includes(target)) return;
f.del.push(target);
s.push(line(disp, '谢谢你的面包,好甜呀!', target));
if (f.del.length >= 3) completeTaskDeferred(bt, s);
}
function deliverLetter(s: DNode[], target: string, disp: string, glitch = false) {
const f = Director.flags['letters']; if (!f || !f.taken || f.del.includes(target)) return;
f.del.push(target);
if (glitch) { Director.emit('glitch', 'C4'); if (Director.day === 2) discover('C4'); s.push(line(disp, '……谢谢。信我收下了。', target)); }
else s.push(line(disp, '有我的信?谢谢你跑一趟!', target));
if (f.del.length >= 3) completeTaskDeferred('T4', s);
}
function flag<T>(k: string, init: T): T { if (!Director.flags[k]) Director.flags[k] = init; return Director.flags[k]; }
// ---------------- 交互点 ----------------
export function interactScript(kind: string, props: any): DNode[] | null {
switch (kind) {
case 'calendar':
if (Director.day >= 2) { discover('C1'); return CLUE_TEXT.C1.map(narr); }
return [narr('床头的日历,画着一朵向日葵。')];
case 'bed':
Director.emit('sleep');
return null;
case 'recipe':
if (Director.day >= 3) { discover('C2'); return CLUE_TEXT.C2.map(narr); }
return [narr('圆圆的配方纸,字迹娟秀。')];
case 'clinicform':
if (Director.day >= 5) { discover('C8'); return CLUE_TEXT.C8.map(narr); }
return [narr('医生桌上有一叠表格,看不太清。')];
case 'mailbox':
if (Director.day >= 2) { discover('C3'); return CLUE_TEXT.C3.map(narr); }
return [narr('大宝的邮筒,红彤彤的。')];
case 'shelf': {
const s: DNode[] = [];
if (Director.day >= 2) { discover('C5'); s.push(...CLUE_TEXT.C5.map(narr)); }
else s.push(narr('满满一货架的货物,真丰富呀。'));
if (taskActive('T5')) completeTaskDeferred('T5', s);
return s;
}
case 'flowerbed': {
const wt = waterTask();
if (wt && taskActive(wt)) {
const c = (Director.flags['water' + Director.day] = (Director.flags['water' + Director.day] || 0) + 1);
if (c >= 3) { completeTask(wt); return [narr('花坛浇好水了,花儿抬起了头。')]; }
return [narr(`浇水(${c}/3)……花儿咕嘟咕嘟地喝着。`)];
}
return small('water', '你给花浇了点水。心情好了一点点。');
}
case 'berry': {
if (taskActive('T6')) {
const set: number[] = flag('berries', [] as number[]);
if (!set.includes(props.n)) set.push(props.n);
if (set.length >= 5) { completeTask('T6'); return [narr('五颗莓果,红得像糖。采齐啦!')]; }
return [narr(`采到一颗莓果(${set.length}/5)。`)];
}
return [narr('莓果丛,红艳艳的。')];
}
case 'picnic':
if (taskActive('T7')) { Director.emit('picnic'); return null; }
return [narr('铺好的野餐布,格子花纹真可爱。')];
case 'ribbon': {
if (taskActive('T13')) {
const set: number[] = flag('ribbons', [] as number[]);
if (!set.includes(props.n)) set.push(props.n);
if (set.length >= 4) { completeTask('T13'); return [narr('四条彩带挂好了,广场热闹起来。')]; }
return [narr(`挂上一条彩带(${set.length}/4)。`)];
}
return [narr('一卷彩带,五颜六色的。')];
}
case 'festival':
Director.emit('festival');
return null;
case 'well':
if (Director.day < 5) return [narr('(一位村民温柔地把你引开)现在还不是看井的时候哦。')];
gaze('well');
discover('C7');
return CLUE_TEXT.C7.map(narr);
case 'fog':
if (Director.day < 5) return [narr('(一位村民温柔地把你引开)那边是村庄的尽头,别去啦。')];
gaze('fog');
Director.emit('fog');
return null;
}
return null;
}
function gaze(id: string) { if (!Director.gazedToday.has(id)) { Director.gazedToday.add(id); Director.addAwareness(3); } }
function small(kind: string, msg: string): DNode[] {
const n = Director.smallInteract[kind] || 0;
if (n < 3) { Director.smallInteract[kind] = n + 1; Director.addHappiness(1); }
return [narr(msg)];
}
// 对话选项语气效果 (§6.1/§6.2/§6.3)
export function applyTone(tone: 'gentle' | 'hesitant' | 'deny') {
if (tone === 'gentle') Director.addHappiness(2);
else if (tone === 'hesitant') Director.addAwareness(2);
else if (tone === 'deny') { Director.addAwareness(2); Director.sadBehavior('否定选项'); }
}
// 结局判定 (§9 自上而下第一个满足者)
export function evalEnding(fromFog: boolean): string | null {
const D = Director;
if (D.escapeCount >= 3 && D.happiness <= 10) return 'E4';
if (fromFog && D.awareness >= 70 && D.clues.size >= 6) return 'E3';
if (D.awareness >= 30 && D.awareness <= 69) return 'E2';
if (D.awareness <= 29 && D.happiness >= 90) return 'E1';
return null;
}
+88
View File
@@ -0,0 +1,88 @@
// All hardcoded, verbatim game text/data (§4,§5,§6.5,§7,§8,§9). Change nothing without PRD.
export interface NpcDef { name: string; offset: number; participates: boolean; sprite: string; }
export const NPCS: Record<string, NpcDef> = {
afu: { name: '阿福', offset: 0, participates: true, sprite: 'npc_afu' },
yuan: { name: '圆圆', offset: 15, participates: true, sprite: 'npc_yuan' }, // +15 最晚崩
dabao: { name: '大宝', offset: 0, participates: true, sprite: 'npc_dabao' },
mimi: { name: '咪咪', offset: -10, participates: true, sprite: 'npc_mimi' }, // -10 最早崩
fu: { name: '芙医生', offset: 0, participates: false, sprite: 'npc_fu' }, // 不参与侵蚀
gui: { name: '龟爷爷', offset: 0, participates: false, sprite: 'npc_gui' }, // 不参与侵蚀
};
// §6.5 同音错字对照表(全 10 组,逐字)
export const TYPO: [string, string][] = [
['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'],
['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'],
];
// §5 阿福每日公告(逐字,措辞逐日滑移)
export const ANNOUNCE: Record<number, string> = {
1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
2: '早上好呀!新的一天,新的幸福。祝你开心!',
3: '早上好!今天要把幸福装得满满的哦。要开心!',
4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // 与 D1 逐字相同
5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。',
6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。',
7: '庆典开始了。来吧。',
};
// §7 任务清单
export interface TaskDef { day: number; title: string; reward: number; }
export const TASKS: Record<string, TaskDef> = {
T1: { day: 1, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15 },
T2: { day: 1, title: '给广场花坛浇水 ×3', reward: 10 },
T3: { day: 1, title: '与任意三位村民问好', reward: 10 },
T4: { day: 2, title: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)', reward: 15 },
T5: { day: 2, title: '帮咪咪清点货架', reward: 10 },
T6: { day: 3, title: '去花田采莓果 ×5', reward: 10 },
T7: { day: 3, title: '湖畔野餐', reward: 15 },
T8: { day: 4, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15 },
T9: { day: 4, title: '给广场花坛浇水 ×3', reward: 10 },
T10: { day: 4, title: '与任意三位村民问好', reward: 10 },
T11: { day: 5, title: '帮圆圆揉面', reward: 10 },
T12: { day: 5, title: '擦拭公告板', reward: 10 },
T13: { day: 6, title: '在广场挂彩带 ×4', reward: 10 },
T14: { day: 6, title: '保持微笑 60 秒', reward: 15 },
T15: { day: 7, title: '参加第 413 届丰收庆典', reward: 0 },
};
export const DAY_TASKS: Record<number, string[]> = {
1: ['T1', 'T2', 'T3'], 2: ['T4', 'T5'], 3: ['T6', 'T7'],
4: ['T8', 'T9', 'T10'], 5: ['T11', 'T12'], 6: ['T13', 'T14'], 7: ['T15'],
};
// §8 线索文本
export const CLUE_TEXT: Record<string, string[]> = {
C1: ['床头的日历……', '每一页都是同一个日期。'],
C2: ['配方纸的背面有一行手写小字:', '「按疗程配比。甜度:最大。」'],
C3: ['邮筒旁散落着一些信。', '所有邮戳……都是同一天。'],
C4: ['第三封信的署名闪过一串乱码——', '「PT-0▓」'],
C5: ['货架上,同一件商品重复排了九格。', '标签全是空白。'],
C6: ['擦到一半,浮现出旧字迹:', '「第 412 届丰收庆典」', '下面还压着「第 411 届」「第 410 届」……'],
C7: ['你凑近井口,安静地听。', '……滴。', '极轻的一声,像监护仪。'],
C8: ['医生桌上的表格抬头被手肘挡住,', '只露出「…谷情绪疗…」几个字。'],
C9a: ['湖水一直很平静。'],
C9b: ['面包一直是那个味道。'],
C9c: ['孩子,今天是第几个今天?'],
};
// §9 结局字幕
export const ENDING_TEXT: Record<string, string[]> = {
E1: ['庆典圆满。', '全村在镜头前微笑合影,定格。', '从此,每一天都很幸福。', '每一天。'],
E2: ['心电滴声,一响。', '你回到了 Day 1 清晨的床上。', '日历翻回同一天。', '疗程重新开始。'],
E4: ['庆典无人出席。', '空荡的广场上,彩带在没有风的空气里飘着。', '系统检测到疗程无效。'],
// E3 组装在 EndingScene(含姓名与真实时长)
};
// 医生固定台词(§4
export const DOCTOR_LINE = '村里有诊所,但你不需要去那里哦。';
// NPC 日常问候(可腐蚀;原文保持干净)。key 为 npc id。
export const GREET: Record<string, string[]> = {
afu: ['你来啦!今天也要幸福地过哦。', '公告板上有新任务,别忘了看看。'],
yuan: ['面包刚出炉,闻闻,多甜呀。', '在蜜糖村,每天都是美好的一天呢。'],
dabao: ['有信要送吗?我最喜欢送信啦。', '大家都是好朋友,对吧?'],
mimi: ['要买点什么吗?今天阳光真好呀。', '我最喜欢和你聊天了,说个不停都可以。'],
fu: [DOCTOR_LINE, '你看起来很好。真的,很好。'],
gui: ['坐一会儿吧,湖边最舒服了。', '我这把年纪,什么都见过咯。'],
};
+49
View File
@@ -0,0 +1,49 @@
import Phaser from 'phaser';
import { Director } from './core/Director';
import { Audio } from './core/Audio';
import * as quests from './core/quests';
import { BootScene } from './scenes/BootScene';
import { TitleScene } from './scenes/TitleScene';
import { WorldScene } from './scenes/WorldScene';
import { UIScene } from './scenes/UIScene';
import { EndingScene } from './scenes/EndingScene';
const BASE_W = 320, BASE_H = 180;
const game = new Phaser.Game({
type: Phaser.AUTO,
parent: 'game',
width: BASE_W,
height: BASE_H,
pixelArt: true,
roundPixels: true,
backgroundColor: '#101014',
scale: { mode: Phaser.Scale.NONE, autoCenter: Phaser.Scale.CENTER_BOTH },
physics: { default: 'arcade', arcade: { debug: false } },
scene: [BootScene, TitleScene, WorldScene, UIScene, EndingScene],
});
// 整数倍缩放居中(§12
function fit() {
const z = Math.max(1, Math.floor(Math.min(window.innerWidth / BASE_W, window.innerHeight / BASE_H)));
game.scale.setZoom(z);
}
window.addEventListener('resize', fit);
game.events.once('ready', fit);
fit();
// 音频随侵蚀阶段联动(§6.4/§6.6)——全局唯一订阅
Director.on('stage', () => Audio.setStage(Director.params));
// 首个用户手势后恢复音频(浏览器策略)
const resume = () => { Audio.ctx?.resume(); };
window.addEventListener('pointerdown', resume);
window.addEventListener('keydown', resume);
// 测试/验收辅助句柄(无害只读引用;正常游玩不使用)
(window as any).HV = {
game, Director, Audio, quests,
ui: () => game.scene.getScene('UI'),
world: () => game.scene.getScene('World'),
say: (nodes: any[]) => (game.scene.getScene('UI') as any).showDialogue(nodes),
};
+41
View File
@@ -0,0 +1,41 @@
import Phaser from 'phaser';
const MAPS = ['home', 'plaza', 'bakery', 'grocery', 'clinic', 'lake', 'field', 'well', 'fog', 'white'];
const NPCS = ['afu', 'yuan', 'dabao', 'mimi', 'fu', 'gui'];
export class BootScene extends Phaser.Scene {
constructor() { super('Boot'); }
preload() {
const g = this.add.graphics();
const bar = this.add.graphics();
this.add.text(160, 78, '蜜糖村加载中……', { fontFamily: 'monospace', fontSize: '10px', color: '#ffe9b0' }).setOrigin(0.5);
this.load.on('progress', (p: number) => {
bar.clear(); bar.fillStyle(0xffd27a, 1); bar.fillRect(90, 96, 140 * p, 6);
});
g.lineStyle(1, 0x8a6a3a).strokeRect(90, 96, 140, 6);
this.load.image('tiles', 'assets/sprites/tileset.png');
this.load.spritesheet('player', 'assets/sprites/player.png', { frameWidth: 16, frameHeight: 24 });
this.load.spritesheet('portraits', 'assets/sprites/portraits.png', { frameWidth: 32, frameHeight: 32 });
this.load.spritesheet('birds', 'assets/sprites/birds.png', { frameWidth: 16, frameHeight: 24 });
this.load.spritesheet('extras', 'assets/sprites/extras.png', { frameWidth: 16, frameHeight: 16 });
this.load.image('sparrow', 'assets/sprites/sparrow.png');
for (const n of NPCS) this.load.spritesheet('npc_' + n, `assets/sprites/npc_${n}.png`, { frameWidth: 16, frameHeight: 24 });
for (const m of MAPS) this.load.tilemapTiledJSON('map_' + m, `assets/maps/${m}.json`);
}
create() {
// player walk anims (rows: 0 down,1 left,2 right,3 up; 4 frames each)
const mk = (key: string, row: number) =>
this.anims.create({ key, frames: this.anims.generateFrameNumbers('player', { start: row * 4, end: row * 4 + 3 }), frameRate: 8, repeat: -1 });
mk('walk-down', 0); mk('walk-left', 1); mk('walk-right', 2); mk('walk-up', 3);
// bird mouth
this.anims.create({ key: 'bird-flap', frames: this.anims.generateFrameNumbers('birds', { start: 0, end: 1 }), frameRate: 3, repeat: -1 });
// extras: 5 critters x 2 frames
for (let k = 0; k < 5; k++)
this.anims.create({ key: 'extra' + (k + 1), frames: this.anims.generateFrameNumbers('extras', { start: k * 2, end: k * 2 + 1 }), frameRate: 3, repeat: -1 });
this.scene.start('Title');
}
}
+143
View File
@@ -0,0 +1,143 @@
import Phaser from 'phaser';
import { Director } from '../core/Director';
import { Audio } from '../core/Audio';
import { ENDING_TEXT } from '../data/content';
const FONT = '"PingFang SC","Microsoft YaHei","Hiragino Sans GB",sans-serif';
export class EndingScene extends Phaser.Scene {
constructor() { super('Ending'); }
create(data: { id: string }) {
this.cameras.main.setBackgroundColor('#000000');
this.cameras.main.fadeIn(500, 0, 0, 0);
switch (data.id) {
case 'E1': this.e1(); break;
case 'E2': this.e2(); break;
case 'E3': this.e3(); break;
default: this.e4(); break;
}
}
// 字幕逐行(前一行淡出,避免重叠)
private prevCap?: Phaser.GameObjects.Text;
private captions(lines: string[], startDelay: number, gap: number, y = 150, cb?: () => void) {
let d = startDelay;
lines.forEach((ln, i) => {
this.time.delayedCall(d, () => {
if (this.prevCap) { const p = this.prevCap; this.tweens.add({ targets: p, alpha: 0, duration: 400, onComplete: () => p.destroy() }); }
const t = this.add.text(160, y, ln, { fontFamily: FONT, fontSize: '11px', color: '#f2ead6', align: 'center', wordWrap: { width: 300 } }).setOrigin(0.5).setAlpha(0).setDepth(100);
this.tweens.add({ targets: t, alpha: 1, duration: 600 });
this.prevCap = t;
if (i === lines.length - 1 && cb) this.time.delayedCall(gap, cb);
});
d += gap;
});
}
private homeButton(delay: number) {
this.time.delayedCall(delay, () => {
const b = this.add.text(160, 168, '回到标题', { fontFamily: FONT, fontSize: '11px', color: '#fff', backgroundColor: '#8a5a3a', padding: { x: 8, y: 3 } })
.setOrigin(0.5).setDepth(200).setInteractive({ useHandCursor: true }).setAlpha(0);
this.tweens.add({ targets: b, alpha: 1, duration: 500 });
b.on('pointerdown', () => this.scene.start('Title'));
});
}
// E1 糖衣:全村微笑合影定格,饱和度缓慢推到 1.5
private e1() {
this.cameras.main.setBackgroundColor('#f6e7b8');
const g = this.add.graphics(); g.fillStyle(0x8fd3f0, 1).fillRect(0, 0, 320, 110); g.fillStyle(0x7bc86c, 1).fillRect(0, 110, 320, 70);
const cast = ['npc_afu', 'npc_yuan', 'npc_dabao', 'npc_mimi', 'npc_fu', 'npc_gui'];
cast.forEach((c, i) => this.add.sprite(56 + i * 36, 108, c, 0).setOrigin(0.5, 1).setScale(1.4));
this.add.image(232, 108, 'portraits', 0).setOrigin(0.5, 1).setScale(0.9);
// 合影相框
const fr = this.add.graphics(); fr.lineStyle(4, 0xffffff, 1).strokeRect(20, 40, 280, 80);
this.add.text(160, 34, '📷', { fontSize: '12px' }).setOrigin(0.5);
const sat = this.cameras.main.postFX.addColorMatrix();
let s = 1.0; const ev = this.time.addEvent({ delay: 60, loop: true, callback: () => { s = Math.min(1.5, s + 0.006); sat.saturate((s - 1) * 1.5, false); } });
this.time.delayedCall(9000, () => ev.remove());
this.captions(ENDING_TEXT.E1, 1200, 2000, 150);
this.homeButton(1200 + 2000 * ENDING_TEXT.E1.length + 800);
}
// E2 回收:负片 2 秒 + 心电滴声 → 回到 Day1 床上,日历翻回同一天
private e2() {
const neg = this.cameras.main.postFX.addColorMatrix(); neg.negative();
Audio.sfxEcg();
const t = this.add.text(160, 90, '——', { fontFamily: FONT, fontSize: '14px', color: '#ffffff' }).setOrigin(0.5);
this.time.delayedCall(2000, () => {
neg.reset(); t.destroy();
// Day1 清晨的床(用室内地板 + 床 tile 感觉)
this.cameras.main.setBackgroundColor('#b09a78');
const g = this.add.graphics(); g.fillStyle(0xcaa06e, 1).fillRect(60, 70, 200, 60); g.fillStyle(0xf49ac1, 1).fillRect(70, 78, 180, 40); g.fillStyle(0xf5e6c0, 1).fillRect(70, 78, 60, 40);
this.add.image(150, 100, 'portraits', 0).setScale(1.4);
this.add.text(238, 60, '日\n历', { fontFamily: FONT, fontSize: '9px', color: '#8a5a3a', align: 'center' });
this.captions(ENDING_TEXT.E2, 600, 1900, 150);
this.homeButton(600 + 1900 * ENDING_TEXT.E2.length + 800);
});
}
// E3 苏醒:白色走廊→病房→病历特写→微笑消失变疲惫脸 + 灰麻雀(§9)
private e3() {
const map = this.make.tilemap({ key: 'map_white' });
map.layers.forEach((l: any) => (l.tilemapLayer = null));
const ts = map.addTilesetImage('tiles', 'tiles')!;
map.createLayer('ground', ts, 0, 0); map.createLayer('obstacles', ts, 0, 0);
const cam = this.cameras.main; cam.setBounds(0, 0, map.widthInPixels, map.heightInPixels); cam.centerOn(160, 40);
const hint = this.add.text(160, 20, '雾裂开了,成了一条白色的走廊……', { fontFamily: FONT, fontSize: '9px', color: '#5a5a60' }).setOrigin(0.5).setScrollFactor(0).setDepth(50);
this.tweens.add({ targets: cam, scrollY: 44, duration: 3200, ease: 'Sine.inOut', delay: 700, onComplete: () => hint.setText('……病房。') });
this.time.delayedCall(4600, () => {
cam.fadeOut(600, 240, 242, 245);
cam.once('camerafadeoutcomplete', () => { hint.destroy(); this.record(); });
});
}
private record() {
const cam = this.cameras.main; cam.setBounds(0, 0, 320, 180); cam.setScroll(0, 0); cam.setBackgroundColor('#e6e8ea'); cam.fadeIn(500, 240, 242, 245);
// 病历特写(放进容器,稍后整体淡出)
const c = this.add.container(0, 0).setDepth(60);
const p = this.add.graphics(); p.fillStyle(0xffffff, 1).fillRoundedRect(36, 24, 248, 128, 4); p.lineStyle(1, 0x9aa0a8).strokeRoundedRect(36, 24, 248, 128, 4);
const head = this.add.text(160, 42, '蜜糖谷情绪疗养中心', { fontFamily: FONT, fontSize: '13px', color: '#2a2a30', fontStyle: 'bold' }).setOrigin(0.5).setAlpha(0);
const rule = this.add.graphics(); rule.lineStyle(1, 0xccced2).lineBetween(56, 58, 264, 58);
c.add([p, head, rule]);
this.tweens.add({ targets: head, alpha: 1, duration: 700 });
const rows = [`姓名:${Director.playerName}`, `编号:PT-07`, `本次疗程时长:${Director.playtimeString()}`];
rows.forEach((r, i) => this.time.delayedCall(900 + i * 750, () => {
const t = this.add.text(64, 74 + i * 22, r, { fontFamily: FONT, fontSize: '12px', color: '#33333a' }).setAlpha(0); c.add(t);
this.tweens.add({ targets: t, alpha: 1, duration: 500 });
}));
// 最后一帧:微笑头像消失 → 疲惫真实的脸 + 灰麻雀(§9 唯一一次头像变化)
this.time.delayedCall(3800, () => {
this.tweens.add({ targets: c, alpha: 0, duration: 700, onComplete: () => c.destroy() });
this.time.delayedCall(800, () => this.finalFrame());
});
}
private finalFrame() {
const port = this.add.image(160, 82, 'portraits', 0).setScale(1.8).setDepth(60);
const cap = this.add.text(160, 128, '……', { fontFamily: FONT, fontSize: '11px', color: '#55555c' }).setOrigin(0.5).setDepth(60);
this.time.delayedCall(1400, () => {
this.cameras.main.flash(220, 255, 255, 255);
port.setTexture('portraits', 1); // 真实的、疲惫的脸
this.add.image(252, 54, 'sparrow').setScale(1.5).setDepth(60); // 窗外,一只真的麻雀
cap.setText('窗外,一只灰色的、真的麻雀。');
this.homeButton(1800);
});
}
// E4 静默:庆典无人出席,空广场,彩带在没有风里飘,字幕后黑屏
private e4() {
this.cameras.main.setBackgroundColor('#3a3d42');
const g = this.add.graphics(); g.fillStyle(0x4a4e54, 1).fillRect(0, 120, 320, 60); // 灰广场
// 无风飘的彩带
for (let i = 0; i < 6; i++) {
const x = 40 + i * 44, y = 60 + (i % 3) * 20;
const r = this.add.graphics(); r.fillStyle([0xaa6688, 0x88888a, 0x9a8a6a][i % 3], 0.8);
for (let k = 0; k < 12; k++) r.fillRect(x + k, y + Math.round(2 * Math.sin(k * 0.9)), 1, 2);
this.tweens.add({ targets: r, y: y + 6, duration: 3000 + i * 300, yoyo: true, repeat: -1, ease: 'Sine.inOut' });
}
this.captions(ENDING_TEXT.E4, 1000, 2200, 40, () => {
this.cameras.main.fadeOut(1500, 0, 0, 0);
this.homeButton(2000);
});
}
}
+47
View File
@@ -0,0 +1,47 @@
import Phaser from 'phaser';
import { Director } from '../core/Director';
import { Audio } from '../core/Audio';
import { activateDay } from '../core/quests';
export class TitleScene extends Phaser.Scene {
private nameInput?: HTMLInputElement;
constructor() { super('Title'); }
create() {
const W = 320, H = 180;
this.cameras.main.setBackgroundColor('#f6e7b8');
// sky / ground bands
const g = this.add.graphics();
g.fillStyle(0x8fd3f0, 1).fillRect(0, 0, W, 110);
g.fillStyle(0x7bc86c, 1).fillRect(0, 110, W, 70);
g.fillStyle(0xf2a94e, 1).fillCircle(260, 40, 22); // sun
// title
this.add.text(160, 44, '崩坏童话', { fontFamily: 'serif', fontSize: '30px', color: '#c85850', fontStyle: 'bold' }).setOrigin(0.5);
this.add.text(160, 74, '蜜 糖 村', { fontFamily: 'serif', fontSize: '18px', color: '#8a5a3a' }).setOrigin(0.5);
this.add.text(160, 150, 'WASD/方向键移动 · E 交互 · ` 调试面板', { fontFamily: 'monospace', fontSize: '8px', color: '#5a4a3a' }).setOrigin(0.5);
// DOM name input (centered overlay)
const inp = document.createElement('input');
inp.type = 'text'; inp.maxLength = 8; inp.placeholder = '小满';
inp.style.cssText = 'position:fixed;left:50%;top:58%;transform:translate(-50%,-50%);width:160px;padding:6px 8px;font-size:16px;text-align:center;border:2px solid #8a5a3a;border-radius:6px;background:#fffbe9;color:#5a4a3a;font-family:sans-serif;outline:none;';
document.body.appendChild(inp); this.nameInput = inp; setTimeout(() => inp.focus(), 50);
this.add.text(160, 96, '请输入名字(留空默认 小满):', { fontFamily: 'monospace', fontSize: '8px', color: '#5a4a3a' }).setOrigin(0.5);
const btn = this.add.text(160, 124, '▶ 开始疗程', { fontFamily: 'monospace', fontSize: '12px', color: '#ffffff', backgroundColor: '#c85850', padding: { x: 8, y: 4 } })
.setOrigin(0.5).setInteractive({ useHandCursor: true });
btn.on('pointerover', () => btn.setStyle({ backgroundColor: '#e0736b' }));
btn.on('pointerout', () => btn.setStyle({ backgroundColor: '#c85850' }));
btn.on('pointerdown', () => this.begin());
inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') this.begin(); });
}
private begin() {
const name = (this.nameInput?.value || '').trim() || '小满';
if (this.nameInput) { this.nameInput.remove(); this.nameInput = undefined; }
Audio.start();
Director.reset(name);
activateDay(1);
this.scene.launch('UI');
this.scene.start('World', { map: 'home', spawn: 'start' });
}
}
+254
View File
@@ -0,0 +1,254 @@
import Phaser from 'phaser';
import { Director } from '../core/Director';
import { Audio } from '../core/Audio';
import { activateDay, applyTone, DChoiceOpt, DNode } from '../core/quests';
const FONT = '"PingFang SC","Microsoft YaHei","Hiragino Sans GB",sans-serif';
const EXPR: Record<string, number> = { smile: 0, flat: 1, off: 2 };
export class UIScene extends Phaser.Scene {
busy = false;
private dial!: Phaser.GameObjects.Graphics;
private promptT!: Phaser.GameObjects.Text;
private toastT!: Phaser.GameObjects.Text;
// dialogue
private box!: Phaser.GameObjects.Container;
private nameT!: Phaser.GameObjects.Text;
private bodyT!: Phaser.GameObjects.Text;
private portrait!: Phaser.GameObjects.Image;
private portraitBg!: Phaser.GameObjects.Graphics;
private hintT!: Phaser.GameObjects.Text;
private dq: DNode[] = [];
private onDone?: () => void;
private choice: DChoiceOpt[] | null = null;
private choiceIdx = 0;
private openAt = 0;
// overlays
private rainG!: Phaser.GameObjects.Graphics; private raining = false; private rainY = 0;
private forcedC!: Phaser.GameObjects.Container; private forcedOn = false; private confetti: { x: number; y: number; c: number; s: number }[] = [];
private dbg!: HTMLDivElement;
private domKey!: (e: KeyboardEvent) => void;
constructor() { super('UI'); }
create() {
this.busy = false;
// dial
this.dial = this.add.graphics().setDepth(1000).setScrollFactor(0);
this.drawDial();
// prompt + toast
this.promptT = this.add.text(160, 116, '', { fontFamily: FONT, fontSize: '9px', color: '#fffbe9', backgroundColor: '#00000088', padding: { x: 3, y: 1 } }).setOrigin(0.5).setDepth(1000).setScrollFactor(0).setVisible(false);
this.toastT = this.add.text(160, 14, '', { fontFamily: FONT, fontSize: '9px', color: '#fff2c0', backgroundColor: '#00000099', padding: { x: 4, y: 2 }, align: 'center', wordWrap: { width: 280 } }).setOrigin(0.5, 0).setDepth(1200).setScrollFactor(0).setAlpha(0);
// dialogue box
const bg = this.add.graphics();
bg.fillStyle(0x2a2230, 0.94).fillRoundedRect(4, 128, 312, 48, 4);
bg.lineStyle(1, 0xffd27a, 0.8).strokeRoundedRect(4, 128, 312, 48, 4);
this.portraitBg = this.add.graphics();
this.portraitBg.fillStyle(0x14100e, 1).fillRoundedRect(8, 132, 40, 40, 3);
this.portrait = this.add.image(28, 152, 'portraits', 0).setScale(1.1);
this.nameT = this.add.text(54, 133, '', { fontFamily: FONT, fontSize: '10px', color: '#ffd27a', fontStyle: 'bold' });
this.bodyT = this.add.text(54, 148, '', { fontFamily: FONT, fontSize: '10px', color: '#fdf6e3', wordWrap: { width: 252 }, lineSpacing: 2 });
this.hintT = this.add.text(308, 166, '▶', { fontFamily: FONT, fontSize: '9px', color: '#ffd27a' }).setOrigin(1, 0.5);
this.box = this.add.container(0, 0, [bg, this.portraitBg, this.portrait, this.nameT, this.bodyT, this.hintT]).setDepth(900).setScrollFactor(0).setVisible(false);
// overlays
this.rainG = this.add.graphics().setDepth(800).setScrollFactor(0);
this.forcedC = this.add.container(0, 0).setDepth(850).setScrollFactor(0).setVisible(false);
const fbg = this.add.graphics(); fbg.fillStyle(0xff88bb, 0.10).fillRect(0, 0, 320, 180);
const ftxt = this.add.text(160, 30, '※ 欢乐庆典 ※', { fontFamily: FONT, fontSize: '14px', color: '#ff5599', fontStyle: 'bold' }).setOrigin(0.5);
this.forcedC.add([fbg, ftxt]);
// input
// 动作键走 DOM 监听(持久 UIScene 的 Phaser 键盘在初始场景下不可靠;DOM 与场景顺序无关)
this.domKey = (e: KeyboardEvent) => {
if (e.repeat) return;
if (e.code === 'Backquote') { this.toggleDebug(); e.preventDefault(); return; }
if (this.box.visible) {
if (e.code === 'KeyE' || e.code === 'Enter' || e.code === 'Space') { this.tryAdvance(); e.preventDefault(); }
else if (e.code === 'KeyW' || e.code === 'ArrowUp') this.moveChoice(-1);
else if (e.code === 'KeyS' || e.code === 'ArrowDown') this.moveChoice(1);
} else if (e.code === 'KeyE') {
(this.scene.get('World') as any)?.tryInteract?.();
}
};
window.addEventListener('keydown', this.domKey);
this.input.on('pointerdown', () => { if (this.box.visible) this.tryAdvance(); });
// Director listeners
const redraw = () => this.drawDial();
Director.on('change', redraw);
Director.on('stage', redraw);
Director.on('glitch', (t: string) => this.flashGlitch());
Director.on('toast', (m: string) => this.toast(m));
Director.on('dusk', () => { Audio.sfxBell(); this.toast('※ 黄昏钟声 ※ 天黑了……回小屋,睡一觉吧。'); });
this.events.once('shutdown', () => { Director.off('change', redraw); Director.off('stage', redraw); this.dbg?.remove(); window.removeEventListener('keydown', this.domKey); });
this.buildDebug();
}
// ---------------- sunflower dial (§6.1) ----------------
private drawDial() {
const g = this.dial; g.clear();
const cx = 28, cy = 26, R = 12;
g.fillStyle(0x3a2f22, 0.55).fillCircle(cx, cy, R + 6);
const petals = Director.petals, s = Director.stage;
for (let i = 0; i < 12; i++) {
const a = (i / 12) * Math.PI * 2 - Math.PI / 2;
const lit = i < petals;
const col = lit ? (s >= 3 ? 0xfff3b4 : 0xffd257) : 0xcabf9a;
g.fillStyle(col, lit ? 1 : 0.75).fillCircle(cx + Math.cos(a) * R, cy + Math.sin(a) * R, 3.3);
}
g.fillStyle(0xe8a44e, 1).fillCircle(cx, cy, 6.5);
g.fillStyle(0x9a6a34, 1).fillCircle(cx, cy, 4.8);
if (Director.blackSeeds) {
for (const [dx, dy] of [[-2, -1], [1, -2], [2, 1], [-1, 2], [0, 0]]) g.fillStyle(0x241c1c, 1).fillCircle(cx + dx, cy + dy, 1.1);
}
}
// ---------------- prompt / toast ----------------
showPrompt(text: string | null) { this.promptT.setVisible(!!text && !this.busy).setText(text || ''); }
toast(msg: string) {
this.toastT.setText(msg).setAlpha(1);
this.tweens.killTweensOf(this.toastT);
this.tweens.add({ targets: this.toastT, alpha: 0, delay: 2400, duration: 700 });
}
// ---------------- dialogue ----------------
showDialogue(nodes: DNode[], onDone?: () => void) {
this.dq = nodes.slice(); this.onDone = onDone; this.choice = null; this.busy = true;
this.box.setVisible(true); this.showPrompt(null); this.openAt = performance.now();
this.next();
}
private next() {
if (this.dq.length === 0) { this.close(); return; }
const node = this.dq.shift()! as any;
if (node.choice) this.renderChoice(node.choice);
else this.renderLine(node);
}
private renderLine(l: any) {
this.choice = null;
const txt = l.raw ? l.text : Director.corrupt(l.text, l.speaker);
const hasSpeaker = !!l.speaker || !!l.npcId;
this.nameT.setText(l.npcId ? (nameOf(l.npcId)) : (l.speaker || ''));
this.bodyT.setText(txt).setY(hasSpeaker ? 148 : 144);
this.setPortrait(l.npcId);
this.hintT.setText('▶');
Audio.sfxBeep();
}
private renderChoice(opts: DChoiceOpt[]) {
this.choice = opts; this.choiceIdx = 0;
this.nameT.setText('你');
this.setPortrait(undefined);
this.hintT.setText('W/S 选择 · E 确定');
this.drawChoices();
Audio.sfxBeep();
}
private drawChoices() {
if (!this.choice) return;
const lines = this.choice.map((o, i) => (i === this.choiceIdx ? '▶ ' : ' ') + o.label);
this.bodyT.setText(lines.join('\n')).setY(140);
}
private setPortrait(npcId?: string) {
if (npcId) {
const def = (Director as any);
const expr = Director.npcExpression(npcId);
const tex = spriteOf(npcId);
this.portrait.setVisible(true).setTexture(tex, EXPR[expr]).setScale(1.5);
this.portraitBg.setVisible(true);
} else {
// narration / player: 玩家永远微笑头像
this.portrait.setVisible(true).setTexture('portraits', 0).setScale(1.1);
this.portraitBg.setVisible(true);
}
}
private tryAdvance() {
if (!this.box.visible || performance.now() - this.openAt < 160) return;
if (this.choice) { this.chooseCurrent(); return; }
this.next();
}
private moveChoice(d: number) {
if (!this.box.visible || !this.choice) return;
this.choiceIdx = (this.choiceIdx + this.choice.length + d) % this.choice.length;
this.drawChoices(); Audio.sfxBeep();
}
private chooseCurrent() {
const opt = this.choice![this.choiceIdx];
this.choice = null;
applyTone(opt.tone);
opt.effect?.();
if (opt.then && opt.then.length) this.dq = [...opt.then, ...this.dq];
this.next();
}
private close() {
this.box.setVisible(false); this.busy = false;
const cb = this.onDone; this.onDone = undefined; cb?.();
}
// ---------------- overlays ----------------
setRain(on: boolean) { this.raining = on; if (!on) this.rainG.clear(); }
setForced(on: boolean) { this.forcedOn = on; this.forcedC.setVisible(on); if (on && this.confetti.length === 0) for (let i = 0; i < 40; i++) this.confetti.push({ x: Math.random() * 320, y: Math.random() * 180, c: [0xff5599, 0xffd257, 0x66ccff, 0x88ee99][i % 4], s: 20 + Math.random() * 40 }); }
flashGlitch() {
const t = this.add.text(160, 90, 'PT-0▓', { fontFamily: 'monospace', fontSize: '18px', color: '#ff4444', backgroundColor: '#000000' }).setOrigin(0.5).setDepth(2000).setScrollFactor(0);
let n = 0; const ev = this.time.addEvent({ delay: 40, repeat: 5, callback: () => { t.setX(160 + (Math.random() * 8 - 4)); t.setColor(n++ % 2 ? '#44ffee' : '#ff4444'); } });
this.time.delayedCall(200, () => { t.destroy(); ev.remove(); });
}
playDream(prevDay: number, cb: () => void) {
const c = this.add.container(0, 0).setDepth(2500).setScrollFactor(0);
const bg = this.add.graphics(); bg.fillStyle(0x000000, 1).fillRect(0, 0, 320, 180); c.add(bg);
const t = this.add.text(160, 90, '(睡着了……)', { fontFamily: FONT, fontSize: '11px', color: '#8a8a8a' }).setOrigin(0.5); c.add(t);
const leak = prevDay >= 3 || Director.stage >= 3;
const dur = leak ? (1000 + (Director.stage >= 4 ? 800 : 0)) : 0;
this.time.delayedCall(700, () => {
if (leak) {
const w = this.add.graphics(); w.fillStyle(0xdfe2e6, 1).fillRect(0, 0, 320, 180);
w.fillStyle(0xc2c6cc, 1).fillRect(40, 96, 90, 40); w.fillStyle(0x9aa0a8, 1).fillRect(40, 96, 90, 8); // 病床
const wt = this.add.text(160, 40, '……这里是……?', { fontFamily: FONT, fontSize: '10px', color: '#6a6a70' }).setOrigin(0.5);
c.add([w, wt]);
this.time.delayedCall(dur, () => { c.destroy(); cb(); });
} else { c.destroy(); cb(); }
});
}
update(_t: number, dt: number) {
// rain
if (this.raining) {
this.rainY = (this.rainY + dt * 0.35) % 20;
const g = this.rainG; g.clear(); g.lineStyle(1, 0xbcd6ee, 0.5);
for (let x = -20; x < 340; x += 12) for (let y = -20; y < 200; y += 20) { const yy = y + this.rainY; g.lineBetween(x + (yy % 40) * 0.2, yy, x + (yy % 40) * 0.2 - 3, yy + 6); }
}
// forced confetti
if (this.forcedOn) {
const g = this.rainG; // reuse layer when not raining
if (!this.raining) { g.clear(); for (const p of this.confetti) { p.y += (p.s / 60) * dt / 16; if (p.y > 180) p.y = -4; g.fillStyle(p.c, 0.9).fillRect(p.x, p.y, 2, 2); } }
}
}
// ---------------- debug panel (§13) ----------------
private buildDebug() {
const d = document.createElement('div');
d.style.cssText = 'position:fixed;right:8px;top:8px;z-index:9999;display:none;background:#1b1620ee;color:#eee;font:11px/1.5 monospace;padding:8px;border:1px solid #ffd27a;border-radius:6px;max-width:220px;';
d.innerHTML = `<b style="color:#ffd27a">DEBUG\` 开关)</b>
<div>幸福 ${btns('H', [10, 40, 70, 90, 100])}</div>
<div>觉知 ${btns('A', [0, 40, 70, 100])}</div>
<div>日 ${btns('D', [1, 2, 3, 4, 5, 6, 7])}</div>
<div>传送 ${['home', 'plaza', 'bakery', 'grocery', 'clinic', 'lake', 'field', 'well', 'fog'].map((m) => `<button data-tp="${m}">${m}</button>`).join('')}</div>
<div><button data-clues="1">置齐全部线索</button></div>
<div>结局 ${['E1', 'E2', 'E3', 'E4'].map((e) => `<button data-end="${e}">${e}</button>`).join('')}</div>`;
document.body.appendChild(d); this.dbg = d;
d.addEventListener('click', (e) => {
const el = e.target as HTMLElement; const world = () => this.scene.get('World') as any;
if (el.dataset.h) Director.setHappiness(+el.dataset.h);
else if (el.dataset.a) Director.setAwareness(+el.dataset.a);
else if (el.dataset.d) { activateDay(+el.dataset.d); this.toast('Day ' + el.dataset.d); }
else if (el.dataset.tp) world().debugTeleport(el.dataset.tp);
else if (el.dataset.clues) { ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'].forEach((c) => Director.discoverClue(c)); Director.flags.seen_C9a = Director.flags.seen_C9b = Director.flags.seen_C9c = true; this.toast('线索已置齐(9/9'); }
else if (el.dataset.end) world().startEnding(el.dataset.end);
});
}
private toggleDebug() { if (!this.dbg) return; this.dbg.style.display = this.dbg.style.display === 'none' ? 'block' : 'none'; }
}
function btns(kind: string, vals: number[]) { const a = kind === 'H' ? 'h' : kind === 'A' ? 'a' : 'd'; return vals.map((v) => `<button data-${a}="${v}">${v}</button>`).join(''); }
function nameOf(id: string): string { return ({ afu: '阿福', yuan: '圆圆', dabao: '大宝', mimi: '咪咪', fu: '芙医生', gui: '龟爷爷', bird1: '双子鸟', bird2: '双子鸟' } as any)[id] || id; }
function spriteOf(id: string): string { return ({ afu: 'npc_afu', yuan: 'npc_yuan', dabao: 'npc_dabao', mimi: 'npc_mimi', fu: 'npc_fu', gui: 'npc_gui' } as any)[id] || 'npc_afu'; }
+335
View File
@@ -0,0 +1,335 @@
import Phaser from 'phaser';
import { Director } from '../core/Director';
import { Audio } from '../core/Audio';
import { activateDay, evalEnding, interactScript, talkScript, taskActive, completeTask, DNode } from '../core/quests';
import { NPCS } from '../data/content';
import type { UIScene } from './UIScene';
const SPEED = 96;
const EXPR: Record<string, number> = { smile: 0, flat: 1, off: 2 };
const OUTDOOR = new Set(['plaza', 'lake', 'field', 'well', 'fog']);
const narr = (text: string): DNode => ({ speaker: '', text });
interface Ent { id: string; kind: 'named' | 'bird' | 'extra'; sprite: Phaser.GameObjects.Sprite; anim?: string; tween?: Phaser.Tweens.Tween; }
export class WorldScene extends Phaser.Scene {
mapKey!: string;
map!: Phaser.Tilemaps.Tilemap;
obs!: Phaser.Tilemaps.TilemapLayer;
player!: Phaser.Physics.Arcade.Sprite;
facing = 'down';
cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
wasd!: any;
ents: Ent[] = [];
interacts: { x: number; y: number; kind: string; props: any }[] = [];
doors: { rect: Phaser.Geom.Rectangle; target: string; dest: string }[] = [];
private sat!: any; private vig!: any; private neg!: any;
private target: any = null;
private doorCd = 0; private idle = 0; private rainT = 0; private trans = false;
private negFrames = 0; private offFlash = false;
private t14 = false; private t14t = 0; private t14fade = false;
private forced = false; private forcedT = 0;
private handlers: Record<string, Function> = {};
constructor() { super('World'); }
get ui(): UIScene { return this.scene.get('UI') as UIScene; }
create(data: { map: string; spawn: string }) {
this.mapKey = data.map;
this.ents = []; this.interacts = []; this.doors = [];
this.target = null; this.doorCd = 300; this.idle = 0; this.trans = false;
this.t14 = false; this.t14t = 0; this.t14fade = false; this.forced = false;
this.map = this.make.tilemap({ key: 'map_' + data.map });
// Tiled 地图数据被 Phaser 缓存并共享;重访地图前清掉上次的 tilemapLayer 引用,否则 createLayer 返回 null
this.map.layers.forEach((l: any) => (l.tilemapLayer = null));
const ts = this.map.addTilesetImage('tiles', 'tiles')!;
let ground = this.map.createLayer('ground', ts, 0, 0);
let obsL = this.map.createLayer('obstacles', ts, 0, 0);
if (!ground || !obsL) { this.map.layers.forEach((l: any) => (l.tilemapLayer = null)); ground = ground || this.map.createLayer('ground', ts, 0, 0); obsL = obsL || this.map.createLayer('obstacles', ts, 0, 0); }
ground!.setDepth(0);
this.obs = obsL!; this.obs.setDepth(1);
this.obs.setCollisionByExclusion([-1]);
const W = this.map.widthInPixels, H = this.map.heightInPixels;
this.physics.world.setBounds(0, 0, W, H);
this.cameras.main.setBounds(0, 0, W, H);
this.cameras.main.setBackgroundColor('#101014');
// spawn point
const objs = this.map.getObjectLayer('objects')?.objects || [];
const sp = objs.find((o) => o.name === data.spawn) || objs.find((o) => o.type === 'spawn');
const px = (sp?.x || 32) + 8, py = (sp?.y || 32) + 8;
this.player = this.physics.add.sprite(px, py, 'player', 0).setDepth(50);
this.player.body!.setSize(10, 7); (this.player.body as Phaser.Physics.Arcade.Body).setOffset(3, 16);
this.player.setCollideWorldBounds(true);
this.physics.add.collider(this.player, this.obs);
// 小地图整屏居中,大地图跟随并钳制边界(§12)
if (W <= 320 && H <= 180) this.cameras.main.centerOn(W / 2, H / 2);
else this.cameras.main.startFollow(this.player, true, 0.18, 0.18);
this.cameras.main.roundPixels = true;
// entities + interacts + doors
for (const o of objs) {
if (o.type === 'npc') this.spawnEntity(o);
else if (o.type === 'interact') this.interacts.push({ x: o.x! + 8, y: o.y! + 8, kind: prop(o, 'kind'), props: propsObj(o) });
else if (o.type === 'door') this.doors.push({ rect: new Phaser.Geom.Rectangle(o.x!, o.y!, o.width!, o.height!), target: prop(o, 'target'), dest: prop(o, 'dest') });
}
// postFX
const cam = this.cameras.main;
this.sat = cam.postFX.addColorMatrix();
this.vig = cam.postFX.addVignette(0.5, 0.5, 0.82, 0);
this.neg = cam.postFX.addColorMatrix();
this.updateFX();
// input
this.cursors = this.input.keyboard!.createCursorKeys();
this.wasd = this.input.keyboard!.addKeys('W,A,S,D');
// E 交互由持久 UIScene 的 DOM 监听转发到 tryInteract()(见 UIScene
// listeners
this.on('stage', () => this.updateFX());
this.on('negativeFlash', () => { this.negFrames = 2; });
this.on('sleep', () => this.startSleep());
this.on('festival', () => this.onFestival());
this.on('fog', () => this.onFog());
this.on('picnic', () => this.doPicnic());
this.on('sad', () => this.headTurn());
this.on('dusk', () => { if (Director.day === 6) { Director.flags.rain = true; this.ui.setRain(true); } });
this.cameras.main.fadeIn(400, 0, 0, 0);
if (this.mapKey === 'home' && data.spawn === 'start') this.ui.setRain(!!Director.flags.rain);
else this.ui.setRain(!!Director.flags.rain && OUTDOOR.has(this.mapKey));
this.events.once('shutdown', () => this.cleanup());
}
private on(ev: string, fn: (...a: any[]) => void) { this.handlers[ev] = fn; Director.on(ev, fn); }
private cleanup() { for (const [ev, fn] of Object.entries(this.handlers)) Director.off(ev, fn as any); this.handlers = {}; }
private spawnEntity(o: any) {
const id = prop(o, 'npc');
const x = o.x + 8, y = o.y + 16;
if (id === 'bird1' || id === 'bird2') {
const s = this.add.sprite(x, y, 'birds', 0).setOrigin(0.5, 1).setDepth(40); s.play('bird-flap');
this.ents.push({ id, kind: 'bird', sprite: s });
} else if (id.startsWith('extra')) {
const k = parseInt(id.slice(5), 10);
const s = this.add.sprite(x, y, 'extras', (k - 1) * 2).setOrigin(0.5, 1).setDepth(40); s.play('extra' + k);
const tw = this.tweens.add({ targets: s, x: x + 24, duration: 2200, yoyo: true, repeat: -1, onYoyo: () => s.setFlipX(true), onRepeat: () => s.setFlipX(false) });
this.ents.push({ id, kind: 'extra', sprite: s, anim: 'extra' + k, tween: tw });
} else {
const s = this.add.sprite(x, y, NPCS[id]?.sprite || 'npc_afu', 0).setOrigin(0.5, 1).setDepth(40);
this.ents.push({ id, kind: 'named', sprite: s });
}
}
update(_t: number, dtMs: number) {
const dt = dtMs;
this.doorCd -= dt;
if (this.negFrames > 0) { this.neg.negative(); this.negFrames--; if (this.negFrames === 0) this.neg.reset(); }
else if (Director.stage === 4 && Math.random() < 0.00011 * dt) { this.negFrames = 2; }
if (Director.stage === 4 && !this.offFlash && Math.random() < 0.00017 * dt) { this.offFlash = true; this.time.delayedCall(100, () => (this.offFlash = false)); }
this.updateEntities();
const busy = this.ui.busy;
if (this.t14) { this.tickT14(dt); return; }
if (busy) { this.player.setVelocity(0); this.setIdleAnim(); return; }
// movement
let vx = 0, vy = 0;
if (this.cursors.left.isDown || this.wasd.A.isDown) vx = -1;
else if (this.cursors.right.isDown || this.wasd.D.isDown) vx = 1;
if (this.cursors.up.isDown || this.wasd.W.isDown) vy = -1;
else if (this.cursors.down.isDown || this.wasd.S.isDown) vy = 1;
const moving = vx !== 0 || vy !== 0;
if (moving) { const l = Math.hypot(vx, vy); this.player.setVelocity((vx / l) * SPEED, (vy / l) * SPEED); }
else this.player.setVelocity(0);
if (vx < 0) this.facing = 'left'; else if (vx > 0) this.facing = 'right'; else if (vy < 0) this.facing = 'up'; else if (vy > 0) this.facing = 'down';
if (moving) this.player.anims.play('walk-' + this.facing, true); else this.setIdleAnim();
this.detectTarget();
this.checkDoors();
this.tickIdle(dt, moving);
this.tickRain(dt);
this.tickForced(dt);
}
private setIdleAnim() {
this.player.anims.stop();
this.player.setFrame({ down: 0, left: 4, right: 8, up: 12 }[this.facing]!);
}
private detectTarget() {
let best: any = null, bd = 24 * 24;
const test = (x: number, y: number, obj: any) => { const d = (x - this.player.x) ** 2 + (y - this.player.y) ** 2; if (d < bd) { bd = d; best = obj; } };
for (const it of this.interacts) test(it.x, it.y, { type: 'interact', it });
for (const e of this.ents) if (e.kind !== 'extra') test(e.sprite.x, e.sprite.y - 8, { type: 'npc', e });
this.target = best;
this.ui.showPrompt(best ? '按 E' : null);
}
tryInteract() { if (!this.ui.busy && !this.t14 && !this.trans && this.target) this.doInteract(); }
private doInteract() {
if (this.target.type === 'npc') {
const e: Ent = this.target.e;
if (e.kind === 'bird') { this.ui.showDialogue([narr('双子鸟:叽——叽——(音乐好像就是从它们那儿飘出来的。)')]); return; }
this.ui.showDialogue(talkScript(e.id));
} else {
const it = this.target.it;
const script = interactScript(it.kind, it.props);
if (script) this.ui.showDialogue(script);
}
}
private checkDoors() {
if (this.doorCd > 0 || this.trans) return;
const pr = new Phaser.Geom.Rectangle(this.player.x - 5, this.player.y - 3, 10, 8);
for (const d of this.doors) {
if (Phaser.Geom.Rectangle.Overlaps(pr, d.rect)) {
if (this.forced) { Director.escapeCount++; this.ui.toast('(你从欢乐庆典的边缘走开了……)'); this.endForced(); }
this.gotoMap(d.target, d.dest, 200);
return;
}
}
}
private gotoMap(map: string, spawn: string, fade = 150) {
if (this.trans) return;
this.trans = true; this.doorCd = 400; this.player.setVelocity(0);
this.cameras.main.fadeOut(fade, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => this.scene.restart({ map, spawn }));
}
private tickIdle(dt: number, moving: boolean) {
if (moving || this.target) { this.idle = 0; return; }
this.idle += dt;
if (this.idle >= 10000) { this.idle = 0; Director.sadBehavior('发呆'); }
}
private tickRain(dt: number) {
if (!Director.flags.rain) return;
if (OUTDOOR.has(this.mapKey)) { this.rainT += dt; if (this.rainT >= 10000) { this.rainT = 0; Director.sadBehavior('雨中久站'); } }
else this.rainT = 0;
}
private tickForced(dt: number) {
if (this.forced) { this.forcedT += dt; if (this.forcedT >= 10000) { Director.setHappiness(50); this.ui.toast('欢乐庆典把你的幸福拉回了 50。'); this.endForced(); } return; }
if (Director.day < 7 && Director.lowHappySince && performance.now() - Director.lowHappySince >= 120000) this.startForced();
}
private startForced() { this.forced = true; this.forcedT = 0; Director.lowHappySince = 0; this.ui.setForced(true); this.ui.toast('※ 欢乐庆典 ※ 大家都在等你开心起来!'); }
private endForced() { this.forced = false; this.ui.setForced(false); }
private headTurn() {
let best: Ent | null = null, bd = 1e9;
for (const e of this.ents) if (e.kind === 'named') { const d = (e.sprite.x - this.player.x) ** 2 + (e.sprite.y - this.player.y) ** 2; if (d < bd) { bd = d; best = e; } }
if (!best) return;
best.sprite.setFlipX(this.player.x < best.sprite.x);
this.tweens.add({ targets: best.sprite, scaleX: 1.15, scaleY: 1.15, duration: 120, yoyo: true });
}
private updateEntities() {
for (const e of this.ents) {
if (e.kind === 'named') {
let expr = Director.npcExpression(e.id);
if (this.offFlash && Director.npcStage(e.id) === 4) expr = 'off';
e.sprite.setFrame(EXPR[expr]);
if (this.t14 || Director.npcStage(e.id) >= 3) e.sprite.setFlipX(this.player.x < e.sprite.x);
else e.sprite.setFlipX(false);
} else if (e.kind === 'extra') {
if (Director.stage === 4 && e.id === 'extra1') { e.tween?.pause(); e.sprite.anims.stop(); e.sprite.setFlipX(this.player.x < e.sprite.x); }
else { e.tween?.resume(); if (!e.sprite.anims.isPlaying) e.sprite.play(e.anim!); }
}
}
}
private updateFX() {
const p = Director.params;
this.sat.saturate((p.saturation - 1) * 1.5, false);
this.vig.strength = p.vignette;
this.vig.radius = 0.82 - p.vignette * 0.25;
}
// ---------------- day flow ----------------
private startSleep() {
if (Director.day >= 7 || this.trans) return;
this.trans = true; this.ui.busy = true; this.player.setVelocity(0);
this.cameras.main.fadeOut(600, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
const prev = Director.day;
this.ui.playDream(prev, () => {
activateDay(prev + 1);
const nd = Director.day;
// 先在持久的 UIScene 上解锁再重启本场景;delayedCall 会随本场景 restart 被清除,绝不能放到 restart 之后
this.ui.busy = false;
Director.flags.rain = false; this.ui.setRain(false);
this.scene.restart({ map: 'home', spawn: 'start' });
this.ui.toast(`Day ${nd}`);
});
});
}
private onFestival() {
if (Director.day === 7) {
const e = evalEnding(false);
if (e) this.startEnding(e);
else if (Director.awareness >= 70 && Director.clues.size >= 6) this.ui.showDialogue([narr('广场很热闹……但村庄尽头那道雾,好像在等你。')]);
else this.startEnding('E2');
} else if (Director.day === 6 && taskActive('T14')) this.startT14();
else this.ui.showDialogue([narr('庆典的舞台,还没到时候呢。')]);
}
private onFog() {
if (Director.day === 7) {
if (Director.escapeCount >= 3 && Director.happiness <= 10) this.startEnding('E4');
else if (Director.awareness >= 70 && Director.clues.size >= 6) this.startEnding('E3');
else this.ui.showDialogue([narr('雾还是雾。你还没准备好。')]);
} else this.ui.showDialogue([narr('雾在轻轻涌动,像在呼吸。')]);
}
startEnding(id: string) {
if (Director.endingShown) return;
Director.endingShown = true;
this.cameras.main.fadeOut(500, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => { this.scene.stop('UI'); this.scene.start('Ending', { id }); });
}
private startT14() {
this.t14 = true; this.t14t = 0; this.t14fade = false;
this.ui.busy = true; this.player.setVelocity(0); this.setIdleAnim();
this.ui.toast('保持微笑……原地站好,别动。');
this.cameras.main.zoomTo(1.9, 60000, 'Linear');
}
private tickT14(dt: number) {
this.t14t += dt;
if (!this.t14fade && this.t14t >= 40000) { this.t14fade = true; Audio.fadeMaster(0, 20); } // 40→60s 渐隐至零
if (this.t14t >= 60000) {
this.t14 = false; this.ui.busy = false;
Audio.restoreMaster(1.5);
this.cameras.main.zoomTo(1, 700);
completeTask('T14');
}
}
private doPicnic() {
this.ui.busy = true; this.player.setVelocity(0);
this.cameras.main.fadeOut(400, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.cameras.main.fadeIn(400, 0, 0, 0);
this.ui.showDialogue([
narr('你在湖边铺好的野餐布上坐下。'),
narr('湖面倒映着你——'),
narr('倒影却慢了一拍才动。'),
narr('……大概是风吧。'),
], () => { completeTask('T7'); this.ui.busy = false; });
});
}
// debug hooks (called by UIScene)
debugTeleport(map: string) { this.gotoMap(map, 'sp_from_plaza'); }
}
function prop(o: any, k: string): string { const p = (o.properties || []).find((x: any) => x.name === k); return p ? p.value : ''; }
function propsObj(o: any): any { const r: any = {}; for (const p of (o.properties || [])) r[p.name] = p.value; return r; }
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Regenerates ALL art + maps from source scripts. Requires Aseprite + Tiled installed.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ASE="${ASEPRITE:-/Applications/Aseprite.app/Contents/MacOS/aseprite}"
TILED="${TILED:-/Applications/Tiled.app/Contents/MacOS/Tiled}"
SPRITES="$ROOT/public/assets/sprites"
MAPS="$ROOT/public/assets/maps"
mkdir -p "$SPRITES" "$MAPS"
echo "== [1/3] Aseprite: pixel art =="
"$ASE" -b --script-param outdir="$SPRITES" --script "$ROOT/tools/gen_all.lua"
echo "== [2/3] Node: Tiled .tmx sources =="
node "$ROOT/tools/gen_maps.mjs"
echo "== [3/3] Tiled: export .tmx -> .json =="
for f in "$ROOT/tools/tmx/"*.tmx; do
name="$(basename "$f" .tmx)"
"$TILED" --export-map "$f" "$MAPS/$name.json"
echo " exported $name.json"
done
echo "== assets built =="
+314
View File
@@ -0,0 +1,314 @@
-- gen_all.lua : draws EVERY pixel asset for 蜜糖村 from scratch (Aseprite Lua API).
-- No external art, no imported palettes. Run: aseprite -b --script-param outdir=... --script gen_all.lua
local outdir = app.params["outdir"]
if not outdir then error("need --script-param outdir=") end
local function pack(t) return app.pixelColor.rgba(t[1], t[2], t[3], t[4] or 255) end
local function px(img,x,y,c) if x>=0 and y>=0 and x<img.width and y<img.height then img:drawPixel(x,y,pack(c)) end end
local function rect(img,x,y,w,h,c) for j=0,h-1 do for i=0,w-1 do px(img,x+i,y+j,c) end end end
local function hline(img,x,y,w,c) for i=0,w-1 do px(img,x+i,y,c) end end
local function vline(img,x,y,h,c) for j=0,h-1 do px(img,x,y+j,c) end end
local function speck(img,ox,oy,w,h,c,mod,rem) for j=0,h-1 do for i=0,w-1 do if ((i*7+j*13+i*j)%mod)==rem then px(img,ox+i,oy+j,c) end end end end
local function newImg(w,h)
local s = Sprite(w,h, ColorMode.RGB)
return s, s.cels[1].image
end
local function save(s,name) s:saveAs(outdir.."/"..name) end
local P = {
grass1={124,200,110}, grass2={96,170,80}, grass3={158,222,138},
soil={150,110,70}, soilD={120,86,54},
path1={203,185,158}, path2={170,150,118}, path3={226,214,190},
water1={112,196,232}, water2={80,168,216}, water3={56,134,182}, foam={205,240,250}, waterDeep={44,110,158},
wall1={234,202,156}, wall2={210,170,122}, wallD={186,140,96},
roof1={224,116,108}, roof2={192,88,80}, roof3={240,152,142},
door1={140,92,60}, door2={106,64,40}, knob={240,208,120},
wood1={178,130,82}, wood2={140,98,58}, wood3={202,160,114},
pink1={244,156,194}, pink2={224,112,160}, pinkD={198,86,134},
red1={232,90,90}, red2={192,58,58},
yellow1={246,232,170}, yellow2={234,210,130}, yellow3={252,244,206},
orange1={242,170,80}, orange2={218,140,54},
sky={144,212,240},
trunk={140,98,58}, trunkD={108,74,44},
canopy1={80,170,70}, canopy2={64,128,58}, canopy3={100,198,88},
ifloor1={202,160,114}, ifloor2={178,134,88},
iwall1={152,118,86}, iwall2={122,92,66}, iwallTrim={212,182,142},
cloth={244,182,122}, clothW={252,236,214},
paper={248,244,232}, ink={90,70,60},
metal={150,158,166}, metalD={110,118,128}, glass={176,222,240},
seed={58,46,46}, black={40,34,34}, gray={120,120,124},
wbg={218,221,224}, wwall={196,200,206}, wwallD={168,174,182},
wfloor={208,212,216}, wfloorL={224,227,230}, wdark={150,156,164}, wbed={234,236,238}, wline={176,182,190},
skin={245,205,162}, skinD={214,168,128}, hair={92,64,42},
}
-- ============ TILESET (8x9 = 72 tiles) -> tileset.png ============
do
local s, img = newImg(128,144)
local function grassBase(ox,oy) rect(img,ox,oy,16,16,P.grass1); speck(img,ox,oy,16,16,P.grass2,5,0); speck(img,ox,oy,16,16,P.grass3,7,2) end
local T = {}
T[0]=function(ox,oy) grassBase(ox,oy) end
T[1]=function(ox,oy) grassBase(ox,oy); px(img,ox+4,oy+5,P.yellow2); px(img,ox+11,oy+10,P.pink1); px(img,ox+8,oy+13,P.pink1) end
T[2]=function(ox,oy) rect(img,ox,oy,16,16,P.path1); speck(img,ox,oy,16,16,P.path2,6,1); speck(img,ox,oy,16,16,P.path3,9,3); hline(img,ox,oy,16,P.path2); vline(img,ox,oy,16,P.path2) end
T[3]=function(ox,oy) grassBase(ox,oy); rect(img,ox+4,oy,8,16,P.path1); speck(img,ox+4,oy,8,16,P.path2,6,1) end
T[4]=function(ox,oy) rect(img,ox,oy,16,16,P.water1); speck(img,ox,oy,16,16,P.water2,5,0); hline(img,ox+2,oy+4,5,P.foam); hline(img,ox+9,oy+10,4,P.foam) end
T[5]=function(ox,oy) rect(img,ox,oy,16,16,P.water1); speck(img,ox,oy,16,16,P.water3,7,2); hline(img,ox+1,oy+2,14,P.foam); hline(img,ox+3,oy+8,10,P.water2) end
T[6]=function(ox,oy) grassBase(ox,oy); rect(img,ox+2,oy+6,12,8,P.canopy2); speck(img,ox+2,oy+6,12,8,P.canopy1,4,0); px(img,ox+4,oy+7,P.pink1); px(img,ox+9,oy+8,P.yellow2); px(img,ox+7,oy+11,P.pink2); px(img,ox+12,oy+11,P.pink1) end
T[7]=function(ox,oy) grassBase(ox,oy); rect(img,ox+2,oy+5,12,9,P.canopy2); speck(img,ox+2,oy+5,12,9,P.canopy1,3,0); for _,p in ipairs({{5,7},{9,7},{6,10},{11,9},{8,12}}) do px(img,ox+p[1],oy+p[2],P.red1); px(img,ox+p[1],oy+p[2]-1,P.red2) end end
T[8]=function(ox,oy) rect(img,ox+2,oy+1,12,12,P.canopy2); rect(img,ox+3,oy,10,2,P.canopy2); rect(img,ox+1,oy+3,14,8,P.canopy2); speck(img,ox,oy,16,14,P.canopy1,4,0); speck(img,ox,oy,16,14,P.canopy3,6,3) end
T[9]=function(ox,oy) grassBase(ox,oy); rect(img,ox+6,oy+2,4,13,P.trunk); vline(img,ox+6,oy+2,13,P.trunkD); px(img,ox+7,oy+7,P.trunkD) end
T[10]=function(ox,oy) rect(img,ox,oy,16,16,P.wall1); speck(img,ox,oy,16,16,P.wall2,7,2); hline(img,ox,oy+8,16,P.wallD); for i=0,3 do vline(img,ox+i*4+2,oy,8,P.wallD) end; for i=0,3 do vline(img,ox+i*4,oy+8,8,P.wallD) end end
T[11]=function(ox,oy) rect(img,ox,oy,16,16,P.roof1); for j=0,15 do hline(img,ox,oy+j,16,(j%3==0) and P.roof2 or P.roof1) end; speck(img,ox,oy,16,16,P.roof3,8,4) end
T[12]=function(ox,oy) rect(img,ox,oy,16,16,P.wall2); rect(img,ox+3,oy+2,10,14,P.door1); rect(img,ox+4,oy+3,8,13,P.door2); px(img,ox+10,oy+9,P.knob) end
T[13]=function(ox,oy) grassBase(ox,oy); rect(img,ox+1,oy+4,2,10,P.wood2); rect(img,ox+7,oy+4,2,10,P.wood2); rect(img,ox+13,oy+4,2,10,P.wood2); hline(img,ox,oy+6,16,P.wood1); hline(img,ox,oy+10,16,P.wood1) end
T[14]=function(ox,oy) rect(img,ox,oy,16,16,P.soilD); rect(img,ox+1,oy+1,14,14,P.soil); for _,p in ipairs({{3,3},{8,4},{12,6},{5,9},{10,11},{3,12}}) do px(img,ox+p[1],oy+p[2],P.pink1); px(img,ox+p[1]+1,oy+p[2],P.pink2); px(img,ox+p[1],oy+p[2]-1,P.yellow2) end end
T[15]=function(ox,oy) grassBase(ox,oy); rect(img,ox+2,oy+4,12,11,P.path2); rect(img,ox+3,oy+5,10,9,P.path1); rect(img,ox+5,oy+7,6,6,P.black); rect(img,ox+5,oy+7,6,3,P.waterDeep); vline(img,ox+2,oy+1,5,P.wood2); vline(img,ox+13,oy+1,5,P.wood2); hline(img,ox+2,oy+1,12,P.wood1) end
T[16]=function(ox,oy) rect(img,ox,oy,16,16,{225,228,232,235}); speck(img,ox,oy,16,16,{240,242,245,255},4,0); speck(img,ox,oy,16,16,{205,210,216,220},5,2) end
T[17]=function(ox,oy) rect(img,ox+1,oy+2,14,11,P.wood2); rect(img,ox+2,oy+3,12,9,P.paper); px(img,ox+4,oy+5,P.ink); px(img,ox+6,oy+5,P.ink); px(img,ox+8,oy+5,P.ink); hline(img,ox+4,oy+7,7,P.ink); hline(img,ox+4,oy+9,5,P.ink); rect(img,ox+2,oy+13,2,3,P.wood1); rect(img,ox+12,oy+13,2,3,P.wood1) end
T[18]=function(ox,oy) grassBase(ox,oy); rect(img,ox+7,oy+6,2,9,P.wood2); rect(img,ox+4,oy+2,8,6,P.red1); rect(img,ox+4,oy+2,8,2,P.red2); hline(img,ox+5,oy+5,6,P.black); rect(img,ox+11,oy+1,1,4,P.red2) end
T[19]=function(ox,oy) grassBase(ox,oy); for j=0,9 do for i=0,13 do local red=((i//2)+(j//2))%2==0; px(img,ox+1+i,oy+3+j, red and P.red1 or P.clothW) end end end
T[20]=function(ox,oy) grassBase(ox,oy); rect(img,ox+1,oy+7,14,3,P.wood1); rect(img,ox+1,oy+4,14,2,P.wood2); vline(img,ox+2,oy+9,5,P.wood2); vline(img,ox+13,oy+9,5,P.wood2) end
T[21]=function(ox,oy) grassBase(ox,oy); rect(img,ox+7,oy+4,2,11,P.metalD); rect(img,ox+5,oy+1,6,4,P.yellow3); rect(img,ox+6,oy+2,4,2,P.yellow2) end
T[22]=function(ox,oy) grassBase(ox,oy); rect(img,ox+4,oy+8,8,6,P.canopy2); speck(img,ox+4,oy+8,8,6,P.canopy1,3,0) end
T[23]=function(ox,oy) rect(img,ox,oy,16,16,P.wall1); speck(img,ox,oy,16,16,P.wall2,4,1); speck(img,ox,oy,16,16,P.wallD,9,3) end
T[24]=function(ox,oy) rect(img,ox,oy,16,16,P.ifloor1); for j=0,3 do hline(img,ox,oy+j*4,16,P.ifloor2) end; speck(img,ox,oy,16,16,P.ifloor2,11,4) end
T[25]=function(ox,oy) rect(img,ox,oy,16,16,P.ifloor1); for i=0,3 do vline(img,ox+i*4,oy,16,P.ifloor2) end end
T[26]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); speck(img,ox,oy,16,16,P.iwall2,7,3); hline(img,ox,oy,16,P.iwall2) end
T[27]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox,oy,16,5,P.iwallTrim); hline(img,ox,oy+5,16,P.iwall2) end
T[28]=function(ox,oy) rect(img,ox,oy,16,16,P.wood2); rect(img,ox+1,oy+4,14,11,P.pink1); rect(img,ox+1,oy+4,14,4,P.clothW); rect(img,ox+2,oy+4,5,3,P.clothW); hline(img,ox+1,oy+9,14,P.pinkD) end
T[29]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+3,oy+2,10,12,P.paper); hline(img,ox+3,oy+2,10,P.red1); px(img,ox+5,oy+6,P.ink); px(img,ox+7,oy+6,P.ink); px(img,ox+9,oy+6,P.ink); hline(img,ox+5,oy+9,6,P.ink); px(img,ox+7,oy+11,P.red2); px(img,ox+8,oy+11,P.red2) end
T[30]=function(ox,oy) rect(img,ox,oy+3,16,12,P.wood2); rect(img,ox,oy+3,16,3,P.wood3); hline(img,ox,oy+9,16,P.wood1); rect(img,ox+2,oy+11,3,3,P.wood1); rect(img,ox+11,oy+11,3,3,P.wood1) end
T[31]=function(ox,oy) rect(img,ox,oy+1,16,14,P.metalD); rect(img,ox+2,oy+3,12,8,P.black); rect(img,ox+3,oy+7,10,4,P.orange1); rect(img,ox+4,oy+8,8,2,P.yellow2); hline(img,ox+2,oy+2,12,P.metal); px(img,ox+11,oy+4,P.red1) end
T[32]=function(ox,oy) rect(img,ox,oy,16,16,P.wood1); rect(img,ox+2,oy+2,12,12,P.paper); hline(img,ox+4,oy+5,8,P.ink); hline(img,ox+4,oy+7,8,P.ink); hline(img,ox+4,oy+9,5,P.ink); px(img,ox+12,oy+11,P.red1) end
T[33]=function(ox,oy) rect(img,ox,oy,16,16,P.wood2); hline(img,ox,oy+5,16,P.wood1); hline(img,ox,oy+10,16,P.wood1); rect(img,ox+2,oy+1,3,3,P.orange1); rect(img,ox+7,oy+1,3,3,P.red1); rect(img,ox+11,oy+1,3,3,P.sky); rect(img,ox+2,oy+6,3,3,P.pink1); rect(img,ox+7,oy+6,3,3,P.yellow2) end
T[34]=function(ox,oy) rect(img,ox,oy,16,16,P.wood2); hline(img,ox,oy+8,16,P.wood1); rect(img,ox+3,oy+2,4,5,P.orange1); rect(img,ox+3,oy+2,4,2,P.orange2); rect(img,ox+9,oy+2,4,5,P.orange1); rect(img,ox+9,oy+2,4,2,P.orange2); rect(img,ox+3,oy+10,4,5,P.orange1); rect(img,ox+9,oy+10,4,5,P.orange1) end
T[35]=function(ox,oy) rect(img,ox+1,oy+4,14,4,P.wood3); rect(img,ox+1,oy+4,14,2,P.wood1); vline(img,ox+2,oy+8,6,P.wood2); vline(img,ox+13,oy+8,6,P.wood2) end
T[36]=function(ox,oy) rect(img,ox+4,oy+2,8,3,P.wood2); rect(img,ox+4,oy+7,8,3,P.wood3); vline(img,ox+4,oy+5,2,P.wood2); vline(img,ox+5,oy+10,4,P.wood2); vline(img,ox+10,oy+10,4,P.wood2) end
T[37]=function(ox,oy) rect(img,ox+1,oy+5,14,4,P.wood3); rect(img,ox+3,oy,10,7,P.paper); hline(img,ox+3,oy,10,P.water3); hline(img,ox+4,oy+2,8,P.ink); hline(img,ox+4,oy+4,5,P.ink); rect(img,ox+9,oy+1,4,3,P.wood3) end
T[38]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+2,oy+2,12,10,P.glass); rect(img,ox+2,oy+2,12,4,P.sky); vline(img,ox+8,oy+2,10,P.iwall2); hline(img,ox+2,oy+7,12,P.iwall2); rect(img,ox+1,oy+1,14,1,P.iwallTrim) end
T[39]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+3,oy+2,10,14,P.door1); rect(img,ox+4,oy+3,8,13,P.door2); px(img,ox+10,oy+9,P.knob) end
T[40]=function(ox,oy) rect(img,ox,oy,16,16,P.wwall); speck(img,ox,oy,16,16,P.wwallD,9,4); hline(img,ox,oy+11,16,P.wwallD) end
T[41]=function(ox,oy) rect(img,ox,oy,16,16,P.wfloor); hline(img,ox,oy,16,P.wline); vline(img,ox,oy,16,P.wline); speck(img,ox,oy,16,16,P.wfloorL,13,3) end
T[42]=function(ox,oy) rect(img,ox,oy,16,16,P.wfloorL); hline(img,ox+8,oy,8,P.wline); vline(img,ox,oy+8,8,P.wline) end
T[43]=function(ox,oy) rect(img,ox,oy,16,16,P.wfloor); rect(img,ox+1,oy+3,14,11,P.wbed); rect(img,ox+1,oy+3,14,3,P.wdark); hline(img,ox+1,oy+9,14,P.wline); rect(img,ox,oy+2,2,13,P.metal); rect(img,ox+14,oy+2,2,13,P.metal) end
T[44]=function(ox,oy) rect(img,ox,oy,16,16,P.wwall); rect(img,ox+3,oy+1,10,15,P.wfloorL); rect(img,ox+4,oy+2,8,14,P.wfloor); px(img,ox+10,oy+9,P.metalD) end
T[45]=function(ox,oy) rect(img,ox,oy,16,16,P.wwall); rect(img,ox+2,oy+2,12,10,{212,224,230}); vline(img,ox+8,oy+2,10,P.wdark); hline(img,ox+2,oy+7,12,P.wdark) end
T[46]=function(ox,oy) for i=0,15 do local j=math.floor(2*math.sin(i*0.9)+6); px(img,ox+i,oy+j,P.pink2); px(img,ox+i,oy+j+1,P.pink1) end; px(img,ox+3,oy+3,P.yellow2); px(img,ox+11,oy+9,P.sky) end
T[47]=function(ox,oy) rect(img,ox,oy+6,16,10,P.wood2); rect(img,ox,oy+6,16,2,P.wood3); rect(img,ox+2,oy,12,6,P.red1); for i=0,3 do px(img,ox+2+i*4,oy+5,P.yellow2) end end
T[48]=function(ox,oy) grassBase(ox,oy); for _,p in ipairs({{4,6},{9,4},{11,10},{6,11}}) do px(img,ox+p[1],oy+p[2],P.red1); px(img,ox+p[1]+1,oy+p[2],P.red2); px(img,ox+p[1],oy+p[2]+1,P.yellow2) end end
T[49]=function(ox,oy) grassBase(ox,oy); for _,p in ipairs({{5,5},{10,7},{7,11},{12,12}}) do px(img,ox+p[1],oy+p[2],P.sky); px(img,ox+p[1]+1,oy+p[2],P.water2); px(img,ox+p[1],oy+p[2]+1,P.yellow2) end end
T[50]=function(ox,oy) grassBase(ox,oy); rect(img,ox+6,oy+9,3,4,P.wall1); rect(img,ox+4,oy+6,7,4,P.red1); px(img,ox+5,oy+7,P.yellow3); px(img,ox+8,oy+8,P.yellow3) end
T[51]=function(ox,oy) grassBase(ox,oy); rect(img,ox+3,oy+7,10,6,P.gray); rect(img,ox+3,oy+7,10,2,{150,150,154}); px(img,ox+5,oy+10,P.metalD) end
T[52]=function(ox,oy) grassBase(ox,oy); rect(img,ox+4,oy+7,8,6,P.trunk); rect(img,ox+5,oy+6,6,2,P.wood3); px(img,ox+7,oy+7,P.trunkD); px(img,ox+8,oy+7,P.trunkD) end
T[53]=function(ox,oy) rect(img,ox,oy,16,16,P.water1); speck(img,ox,oy,16,16,P.water2,5,0); rect(img,ox+4,oy+5,8,6,P.canopy1); px(img,ox+7,oy+7,P.pink1); px(img,ox+8,oy+7,P.pink2) end
T[54]=function(ox,oy) rect(img,ox,oy,16,16,P.water1); speck(img,ox,oy,16,16,P.water2,5,0); vline(img,ox+5,oy+2,10,P.canopy2); vline(img,ox+9,oy+1,11,P.canopy2); rect(img,ox+4,oy+1,3,3,P.trunk); rect(img,ox+8,oy,3,3,P.trunk) end
T[55]=function(ox,oy) rect(img,ox+4,oy+4,8,10,P.orange1); rect(img,ox+4,oy+4,8,3,P.orange2); rect(img,ox+5,oy+1,6,3,P.wood2); rect(img,ox+6,oy+8,4,4,P.yellow2) end
T[56]=function(ox,oy) rect(img,ox+1,oy+2,14,13,P.wood2); rect(img,ox+2,oy+3,12,11,P.wood1); vline(img,ox+8,oy+3,11,P.wood2); hline(img,ox+2,oy+8,12,P.wood2) end
T[57]=function(ox,oy) rect(img,ox+3,oy+1,10,14,P.wood1); rect(img,ox+3,oy+1,10,2,P.wood2); hline(img,ox+3,oy+5,10,P.wood2); hline(img,ox+3,oy+10,10,P.wood2); vline(img,ox+7,oy+1,14,P.wood3) end
T[58]=function(ox,oy) rect(img,ox,oy,16,16,P.wood2); hline(img,ox,oy+8,16,P.wood1); for i=0,4 do vline(img,ox+1+i*3,oy+1,6,({P.red1,P.sky,P.canopy1,P.orange1,P.pink2})[i+1]) end; for i=0,4 do vline(img,ox+1+i*3,oy+9,6,({P.canopy1,P.pink2,P.red1,P.sky,P.yellow2})[i+1]) end end
T[59]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+4,oy+3,8,8,P.wall1); rect(img,ox+5,oy+4,6,6,P.paper); vline(img,ox+8,oy+5,3,P.ink); hline(img,ox+8,oy+7,3,P.ink) end
T[60]=function(ox,oy) rect(img,ox,oy,16,16,P.iwall1); rect(img,ox+2,oy+2,12,10,P.wood2); rect(img,ox+3,oy+3,10,8,P.sky); rect(img,ox+3,oy+8,10,3,P.canopy1); px(img,ox+10,oy+5,P.yellow3) end
T[61]=function(ox,oy) rect(img,ox+5,oy+10,6,5,P.orange2); rect(img,ox+4,oy+9,8,2,P.orange1); rect(img,ox+5,oy+4,6,6,P.canopy2); speck(img,ox+5,oy+4,6,6,P.canopy1,3,0) end
T[62]=function(ox,oy) rect(img,ox,oy,16,16,P.ifloor1); rect(img,ox+2,oy+3,12,10,P.pink2); rect(img,ox+4,oy+5,8,6,P.pink1); px(img,ox+7,oy+7,P.yellow2); px(img,ox+8,oy+8,P.yellow2) end
T[63]=function(ox,oy) rect(img,ox,oy,16,16,P.grass2); speck(img,ox,oy,16,16,P.canopy2,5,0); speck(img,ox,oy,16,16,P.grass1,7,2) end
T[64]=function(ox,oy) grassBase(ox,oy); px(img,ox+3,oy+3,P.orange1); px(img,ox+7,oy+6,P.pink2); px(img,ox+12,oy+4,P.yellow2); px(img,ox+9,oy+12,P.red1); px(img,ox+4,oy+11,P.sky) end
T[65]=function(ox,oy) rect(img,ox,oy,16,16,P.waterDeep); speck(img,ox,oy,16,16,P.water3,5,0); hline(img,ox+3,oy+6,8,P.water2) end
T[66]=function(ox,oy) rect(img,ox,oy,16,16,P.path1); speck(img,ox,oy,16,16,P.path2,6,1); rect(img,ox,oy,6,6,P.grass1); speck(img,ox,oy,6,6,P.grass2,4,0) end
T[67]=function(ox,oy) rect(img,ox,oy,16,16,P.roof1); rect(img,ox,oy+11,16,5,P.roof2); hline(img,ox,oy+11,16,P.wall1); speck(img,ox,oy,16,11,P.roof3,8,4) end
T[68]=function(ox,oy) rect(img,ox,oy,16,16,P.wall1); speck(img,ox,oy,16,16,P.wall2,7,2); rect(img,ox+3,oy+3,10,9,P.glass); rect(img,ox+3,oy+3,10,4,P.sky); vline(img,ox+8,oy+3,9,P.wallD); hline(img,ox+3,oy+7,10,P.wallD); rect(img,ox+2,oy+2,12,1,P.wallD) end
T[69]=function(ox,oy) rect(img,ox,oy,16,16,P.sky); rect(img,ox+7,oy+4,3,3,P.yellow2); px(img,ox+8,oy+3,P.yellow3); px(img,ox+8,oy+8,P.yellow3); px(img,ox+4,oy+6,P.yellow3); px(img,ox+12,oy+6,P.yellow3) end
T[70]=function(ox,oy) grassBase(ox,oy); vline(img,ox+7,oy+8,5,P.canopy2); rect(img,ox+6,oy+5,4,4,P.clothW); px(img,ox+7,oy+6,P.yellow2) end
T[71]=function(ox,oy) rect(img,ox,oy,16,16,P.path2); for j=0,3 do for i=0,3 do local c=((i+j)%2==0) and P.path1 or P.path3; rect(img,ox+i*4,oy+j*4,4,4,c) end end end
for idx=0,71 do T[idx]((idx%8)*16,(idx//8)*16) end
save(s,"tileset.png"); s:close(); print("tileset done")
end
-- ============ PLAYER walk sheet 64x96 (4 dir x 4 frame) ============
do
local skin=P.skin; local skinD=P.skinD; local hair=P.hair
local shirt={92,152,212}; local shirtD={64,116,176}
local pants={78,96,128}; local pantsD={56,72,102}; local shoe={70,58,58}
local s, img = newImg(64,96)
local function person(ox,oy,dir,frame)
local sw=0; if frame==1 then sw=1 elseif frame==3 then sw=-1 end
rect(img,ox+5,oy+10,6,7,shirt); hline(img,ox+5,oy+10,6,shirtD); vline(img,ox+5,oy+10,7,shirtD)
vline(img,ox+4,oy+11,4,skin); vline(img,ox+11,oy+11,4,skin)
rect(img,ox+5,oy+3,6,6,skin); hline(img,ox+5,oy+8,6,skinD)
rect(img,ox+4,oy+1,8,3,hair); px(img,ox+4,oy+4,hair); px(img,ox+11,oy+4,hair)
local lx,rx=ox+6,ox+9
if dir==0 then
px(img,ox+6,oy+5,P.black); px(img,ox+9,oy+5,P.black); px(img,ox+7,oy+7,skinD)
rect(img,lx,oy+17,2,4,pants); rect(img,rx,oy+17,2,4,pants)
rect(img,lx,oy+21+(sw>0 and 1 or 0),2,1,shoe); rect(img,rx,oy+21+(sw<0 and 1 or 0),2,1,shoe)
elseif dir==3 then
rect(img,ox+4,oy+1,8,5,hair)
rect(img,lx,oy+17,2,4,pants); rect(img,rx,oy+17,2,4,pants)
rect(img,lx,oy+21+(sw>0 and 1 or 0),2,1,shoe); rect(img,rx,oy+21+(sw<0 and 1 or 0),2,1,shoe)
else
px(img,ox+9,oy+5,P.black); px(img,ox+11,oy+6,skinD); rect(img,ox+4,oy+1,7,3,hair)
rect(img,ox+6+sw,oy+17,2,4,pants); rect(img,ox+8-sw,oy+17,2,4,pantsD)
rect(img,ox+6+sw,oy+21,2,1,shoe); rect(img,ox+8-sw,oy+21,2,1,shoe)
end
end
for f=0,3 do person(f*16,0,0,f); person(f*16,72,3,f); person(f*16,48,2,f) end
for f=0,3 do for yy=0,23 do for xx=0,15 do
img:drawPixel(f*16+(15-xx), 24+yy, img:getPixel(f*16+xx, 48+yy))
end end end
save(s,"player.png"); s:close(); print("player done")
end
-- ============ PLAYER PORTRAITS 64x32 (smile / real face) ============
do
local s, img = newImg(64,32)
local function drawPortrait(ox,tired)
local sk = tired and {216,199,185} or P.skin
local skd = tired and {186,170,158} or P.skinD
local hr = tired and {104,98,92} or P.hair
rect(img,ox+8,4,16,20,sk)
rect(img,ox+7,3,18,7,hr); px(img,ox+7,10,hr); px(img,ox+24,10,hr)
if tired then
rect(img,ox+11,13,2,2,P.black); rect(img,ox+19,13,2,2,P.black)
hline(img,ox+10,16,4,skd); hline(img,ox+18,16,4,skd)
hline(img,ox+13,20,6,skd); px(img,ox+12,21,skd); px(img,ox+19,21,skd)
else
rect(img,ox+11,12,2,3,P.black); rect(img,ox+19,12,2,3,P.black); px(img,ox+12,12,P.glass)
px(img,ox+12,19,skd); hline(img,ox+13,20,6,skd); px(img,ox+19,19,skd)
px(img,ox+14,13,{255,150,160}); px(img,ox+18,13,{255,150,160})
end
rect(img,ox+15,16,2,2,skd)
end
drawPortrait(0,false); drawPortrait(32,true)
save(s,"portraits.png"); s:close(); print("portraits done")
end
-- ============ NPCS 48x24 (smile/flat/off) ============
local function drawAnimal(img, ox, expr, spec)
local body=spec.body; local bodyD=spec.bodyD; local muzzle=spec.muzzle or {250,244,232}
-- torso
rect(img,ox+3,9,10,12,body); rect(img,ox+3,9,10,2,bodyD)
rect(img,ox+5,13,6,7,muzzle) -- belly
-- feet
rect(img,ox+4,21,3,2,bodyD); rect(img,ox+9,21,3,2,bodyD)
-- head
rect(img,ox+4,2,8,8,body)
rect(img,ox+5,5,6,5,muzzle) -- face patch
-- ears
if spec.ears=="floppy" then
rect(img,ox+2,3,2,5,bodyD); rect(img,ox+12,3,2,5,bodyD)
elseif spec.ears=="long" then
rect(img,ox+4,-0+0,2,4,body); rect(img,ox+10,0,2,4,body); px(img,ox+4,1,P.pink1); px(img,ox+10,1,P.pink1)
rect(img,ox+4,0,2,3,body); rect(img,ox+10,0,2,3,body)
elseif spec.ears=="round" then
rect(img,ox+3,1,3,3,body); rect(img,ox+10,1,3,3,body); px(img,ox+4,2,bodyD); px(img,ox+11,2,bodyD)
elseif spec.ears=="pointy" then
px(img,ox+4,1,body); rect(img,ox+3,2,3,2,body); px(img,ox+11,1,body); rect(img,ox+10,2,3,2,body); px(img,ox+4,2,P.pink1); px(img,ox+11,2,P.pink1)
elseif spec.ears=="antler" then
px(img,ox+4,0,P.wood2); px(img,ox+4,1,P.wood2); px(img,ox+3,0,P.wood2); px(img,ox+11,0,P.wood2); px(img,ox+11,1,P.wood2); px(img,ox+12,0,P.wood2)
end
-- eyes (base y=6). off lowers RIGHT eye by 1px.
local ey=6
px(img,ox+6,ey,P.black)
px(img,ox+9, expr=="off" and ey+1 or ey, P.black)
-- nose
px(img,ox+7,8,bodyD); px(img,ox+8,8,bodyD)
-- mouth: smile curve / flat line / off = smile + 1px corner
if expr=="flat" then
hline(img,ox+6,10,4,bodyD)
else
px(img,ox+5,9,bodyD); hline(img,ox+6,10,4,bodyD); px(img,ox+10,9,bodyD)
if expr=="off" then px(img,ox+11,9,bodyD) end -- 1px extra corner
end
-- extras
if spec.whiskers then hline(img,ox+1,7,3,P.gray); hline(img,ox+12,7,3,P.gray) end
if spec.glasses then rect(img,ox+5,5,3,3,P.metal); rect(img,ox+8,5,3,3,P.metal); px(img,ox+6,6,P.glass); px(img,ox+9, expr=="off" and 7 or 6,P.glass) end
if spec.shell then rect(img,ox+3,10,10,9,{86,140,80}); rect(img,ox+5,12,6,5,{110,168,100}); px(img,ox+7,14,{70,116,66}); px(img,ox+8,15,{70,116,66}); rect(img,ox+5,13,6,7,muzzle,{}); rect(img,ox+3,10,10,9,{86,140,80}); px(img,ox+6,13,{70,116,66}); px(img,ox+9,16,{70,116,66}) end
if spec.coat then rect(img,ox+3,11,10,10,P.clothW); rect(img,ox+3,11,10,2,{230,230,236}); rect(img,ox+7,13,2,7,P.red1); rect(img,ox+6,15,4,2,P.red1); rect(img,ox+4,20,3,1,bodyD); rect(img,ox+9,20,3,1,bodyD) end
if spec.head then rect(img,ox+4,0,8,3,spec.head) end
end
do
local specs = {
afu ={body={168,120,72}, bodyD={128,90,52}, ears="floppy"}, -- 狗 村长
yuan ={body={244,236,222}, bodyD={206,196,180}, ears="long"}, -- 兔 面包师
dabao ={body={128,90,60}, bodyD={98,68,44}, ears="round"}, -- 熊 邮差
mimi ={body={236,152,80}, bodyD={200,120,56}, ears="pointy", whiskers=true}, -- 猫 杂货
fu ={body={216,176,124}, bodyD={176,138,92}, ears="antler", coat=true}, -- 鹿 医生
gui ={body={120,180,110}, bodyD={90,140,84}, ears=nil, shell=true, glasses=true}, -- 龟 老者
afu2 =nil,
}
local order = {"afu","yuan","dabao","mimi","fu","gui"}
for _,name in ipairs(order) do
local s, img = newImg(48,24)
local exprs = {"smile","flat","off"}
for i,e in ipairs(exprs) do drawAnimal(img,(i-1)*16,e,specs[name]) end
save(s,"npc_"..name..".png"); s:close()
end
print("npcs done")
end
-- ============ TWIN BIRDS 32x24 (beak closed / open) ============
do
local s, img = newImg(32,24)
local function drawBird(ox,open)
local body={110,170,220}; local bodyD={78,132,186}
rect(img,ox+4,6,8,10,body); rect(img,ox+4,6,8,2,{150,200,240})
rect(img,ox+3,9,3,5,bodyD) -- wing
rect(img,ox+5,3,6,5,body) -- head
px(img,ox+7,5,P.black); px(img,ox+8,5,P.black) -- eyes
-- beak
if open then
px(img,ox+11,6,P.orange2); px(img,ox+12,6,P.orange2); px(img,ox+11,8,P.orange2); px(img,ox+12,8,P.orange2); px(img,ox+13,7,P.orange1)
else
px(img,ox+11,7,P.orange2); px(img,ox+12,7,P.orange2); px(img,ox+13,7,P.orange1)
end
-- feet
px(img,ox+6,16,P.orange2); px(img,ox+9,16,P.orange2)
-- little tuft
px(img,ox+7,2,bodyD); px(img,ox+8,2,body)
end
drawBird(0,false); drawBird(16,true)
save(s,"birds.png"); s:close(); print("birds done")
end
-- ============ EXTRAS (群演) 32x80 = 5 critters x 2 frames, 16x16 ============
do
local s, img = newImg(32,80)
local function critter(ox,oy,f,spec)
local bob = (f==1) and 1 or 0
local b=spec.body; local bd=spec.bodyD
rect(img,ox+4,oy+7+bob,8,6,b); rect(img,ox+4,oy+7+bob,8,2,bd) -- body
rect(img,ox+5,oy+3+bob,6,5,b) -- head
px(img,ox+6,oy+5+bob,P.black); px(img,ox+9,oy+5+bob,P.black)
if spec.tail=="bushy" then rect(img,ox+11,oy+4+bob,3,7,b); rect(img,ox+12,oy+3+bob,2,3,bd) end
if spec.tail=="thin" then vline(img,ox+12,oy+9+bob,3,bd) end
if spec.spikes then for i=0,4 do px(img,ox+4+i*2,oy+6+bob,bd); px(img,ox+5+i*2,oy+8+bob,bd) end end
if spec.ears=="mouse" then px(img,ox+5,oy+2+bob,b); px(img,ox+10,oy+2+bob,b); rect(img,ox+4,oy+2+bob,2,2,b); rect(img,ox+10,oy+2+bob,2,2,b) end
if spec.ears=="pointy" then px(img,ox+5,oy+2+bob,b); px(img,ox+10,oy+2+bob,b) end
rect(img,ox+5,oy+13+bob,2,2,bd); rect(img,ox+9,oy+13+bob,2,2,bd) -- feet
end
local specs = {
{body={214,142,72}, bodyD={176,108,52}, tail="bushy", ears="pointy"}, -- squirrel
{body={150,120,90}, bodyD={110,86,64}, spikes=true, ears="pointy"}, -- hedgehog
{body={200,160,100}, bodyD={160,120,70}, tail="bushy", ears="pointy"}, -- squirrel2
{body={188,190,196}, bodyD={150,152,158},tail="thin", ears="mouse"}, -- mouse
{body={230,200,120}, bodyD={196,164,90}, tail="thin", ears="pointy"}, -- chipmunk
}
for k=0,4 do critter(0,k*16,0,specs[k+1]); critter(16,k*16,1,specs[k+1]) end
save(s,"extras.png"); s:close(); print("extras done")
end
-- ============ SPARROW (E3 real bird) 16x16 gray ============
do
local s, img = newImg(16,16)
local g1={128,124,118}; local g2={98,94,90}; local g3={160,156,150}; local beak={90,80,60}
rect(img,4,6,8,6,g1); rect(img,4,6,8,2,g3)
rect(img,3,8,3,4,g2) -- wing
rect(img,10,4,4,4,g1) -- head
px(img,12,5,P.black); px(img,13,6,beak); px(img,14,6,beak)
px(img,5,12,beak); px(img,8,12,beak) -- feet
speck(img,4,6,8,6,g2,3,0)
save(s,"sparrow.png"); s:close(); print("sparrow done")
end
print("ALL ASSETS DONE")
+330
View File
@@ -0,0 +1,330 @@
// gen_maps.mjs : builds all 10 Tiled maps as .tmx (embedded tileset), for export via Tiled CLI.
// Original layouts, no external tilesets. Run with node; then Tiled exports each .tmx -> .json.
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dir = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dir, '..');
const TMX_DIR = resolve(__dir, 'tmx');
const PNG = resolve(ROOT, 'public/assets/sprites/tileset.png');
mkdirSync(TMX_DIR, { recursive: true });
// tile index (0-based); GID = idx+1
const G = (i) => i + 1;
const t = {
grass: G(0), grassF: G(1), path: G(2), pathEdge: G(3), water: G(4), waterL: G(5),
flowerBush: G(6), berry: G(7), treeTop: G(8), treeTrunk: G(9), wall: G(10), roof: G(11),
door: G(12), fence: G(13), flowerbed: G(14), well: G(15), fog: G(16), board: G(17),
mailbox: G(18), picnic: G(19), bench: G(20), lamp: G(21), bushS: G(22), sand: G(23),
wfloor: G(24), wfloorB: G(25), iwall: G(26), iwallTop: G(27), bed: G(28), calendar: G(29),
counter: G(30), oven: G(31), recipe: G(32), shelf: G(33), shelfItem: G(34), table: G(35),
chair: G(36), clinicForm: G(37), windowI: G(38), doorI: G(39), wwall: G(40), wfloorW: G(41),
wfloorWb: G(42), hbed: G(43), wdoor: G(44), wwindow: G(45), ribbon: G(46), stage: G(47),
flowersRed: G(48), flowersBlue: G(49), mushroom: G(50), rock: G(51), stump: G(52),
lilypad: G(53), reed: G(54), honey: G(55), crate: G(56), barrel: G(57), bookshelf: G(58),
clock: G(59), painting: G(60), plantPot: G(61), rug: G(62), darkGrass: G(63), grassF2: G(64),
waterDeep: G(65), pathCorner: G(66), roofEdge: G(67), houseWindow: G(68), star: G(69),
snowdrop: G(70), cobble: G(71),
};
function M(w, h, base) {
return {
w, h,
ground: new Array(w * h).fill(base),
obs: new Array(w * h).fill(0),
objects: [],
_oid: 1,
};
}
const idx = (m, c, r) => r * m.w + c;
function setG(m, c, r, g) { if (c >= 0 && c < m.w && r >= 0 && r < m.h) m.ground[idx(m, c, r)] = g; }
function setO(m, c, r, g) { if (c >= 0 && c < m.w && r >= 0 && r < m.h) m.obs[idx(m, c, r)] = g; }
function rectG(m, c, r, w, h, g) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) setG(m, c + i, r + j, g); }
function rectO(m, c, r, w, h, g) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) setO(m, c + i, r + j, g); }
function borderO(m, g) {
for (let c = 0; c < m.w; c++) { setO(m, c, 0, g); setO(m, c, m.h - 1, g); }
for (let r = 0; r < m.h; r++) { setO(m, 0, r, g); setO(m, m.w - 1, r, g); }
}
function obj(m, name, type, c, r, props = {}, wTiles = 1, hTiles = 1) {
m.objects.push({ id: m._oid++, name, type, x: c * 16, y: r * 16, w: wTiles * 16, h: hTiles * 16, props });
}
// ---- interior room helper (wood floor + walls) ----
function room(m) {
rectG(m, 0, 0, m.w, m.h, t.wfloor);
for (let c = 0; c < m.w; c++) { setO(m, c, 0, t.iwallTop); setO(m, c, m.h - 1, t.iwall); }
for (let r = 0; r < m.h; r++) { setO(m, 0, r, t.iwall); setO(m, m.w - 1, r, t.iwall); }
}
function exitDoor(m, c, r, target, dest) { setG(m, c, r, t.doorI); setO(m, c, r, 0); obj(m, 'door_' + target, 'door', c, r, { target, dest }); }
const maps = {};
// ============ 1. HOME 12x8 ============
{
const m = M(12, 8, t.wfloor); room(m);
setG(m, 5, 1, t.rug); setG(m, 6, 1, t.rug);
setO(m, 1, 1, t.bed); setO(m, 1, 2, t.bed); // bed (2 tiles)
setG(m, 2, 0, t.calendar); // 床头日历 on top wall
setO(m, 9, 1, t.bookshelf); setO(m, 8, 1, t.plantPot);
setO(m, 8, 5, t.table); setO(m, 9, 5, t.chair);
setG(m, 4, 0, t.windowI); setG(m, 7, 0, t.windowI);
exitDoor(m, 6, 7, 'plaza', 'sp_from_home');
obj(m, 'start', 'spawn', 3, 3);
obj(m, 'sp_from_plaza', 'spawn', 6, 6);
obj(m, 'i_bed', 'interact', 2, 2, { kind: 'bed' });
obj(m, 'i_calendar', 'interact', 2, 1, { kind: 'calendar' });
maps.home = m;
}
// ============ 2. PLAZA 30x20 ============
{
const m = M(30, 20, t.grass);
// scatter grass flowers
for (let r = 0; r < 20; r++) for (let c = 0; c < 30; c++) if (((c * 5 + r * 7) % 11) === 0) setG(m, c, r, t.grassF);
borderO(m, t.fence);
// trees along corners
for (const [c, r] of [[1, 1], [2, 1], [27, 1], [28, 1], [1, 18], [28, 18]]) { setO(m, c, r, t.treeTrunk); setG(m, c, r - 1 < 0 ? 0 : c, r); }
// ---- houses along top (home/bakery/grocery/clinic) ----
function house(c, target, dest) {
rectO(m, c, 1, 3, 2, t.roof);
rectO(m, c, 3, 3, 1, t.wall);
setO(m, c, 2, t.wall); setO(m, c + 2, 2, t.wall);
setG(m, c + 1, 3, t.door); setO(m, c + 1, 3, 0);
obj(m, 'door_' + target, 'door', c + 1, 3, { target, dest });
// path down from door
setG(m, c + 1, 4, t.path); setG(m, c + 1, 5, t.path);
obj(m, 'sp_from_' + target, 'spawn', c + 1, 5);
}
house(2, 'home', 'sp_from_plaza');
house(8, 'bakery', 'sp_from_plaza');
house(15, 'grocery', 'sp_from_plaza');
house(22, 'clinic', 'sp_from_plaza');
// central cobble plaza + main path
rectG(m, 3, 9, 24, 3, t.path);
rectG(m, 11, 13, 8, 4, t.cobble);
// festival stage (center) — walkable cobble around; stage tile as decor obstacle at back
rectO(m, 12, 12, 4, 1, t.stage);
// bulletin board (afu)
setO(m, 14, 8, t.board);
// flowerbed (T2 water) cluster left-center
setO(m, 5, 13, t.flowerbed); setO(m, 6, 13, t.flowerbed); setO(m, 7, 13, t.flowerbed);
// twin-bird tree (right)
setO(m, 24, 5, t.treeTrunk); setG(m, 23, 4, t.treeTop); setG(m, 24, 4, t.treeTop); setG(m, 25, 4, t.treeTop); setG(m, 24, 3, t.treeTop);
// mailbox (dabao) bottom-center
setO(m, 15, 17, t.mailbox);
obj(m, 'i_mailbox', 'interact', 15, 16, { kind: 'mailbox' }); // C3 散落信件
// decorative
setO(m, 3, 16, t.bushS); setO(m, 26, 16, t.bushS); setO(m, 9, 8, t.lamp); setO(m, 20, 8, t.lamp);
// edge exits (leave gaps in fence)
setO(m, 0, 10, 0); exitDoor(m, 0, 10, 'field', 'sp_from_plaza'); setG(m, 1, 10, t.path); obj(m, 'sp_from_field', 'spawn', 2, 10);
setO(m, 29, 10, 0); exitDoor(m, 29, 10, 'lake', 'sp_from_plaza'); setG(m, 28, 10, t.path); obj(m, 'sp_from_lake', 'spawn', 27, 10);
setO(m, 4, 19, 0); exitDoor(m, 4, 19, 'well', 'sp_from_plaza'); setG(m, 4, 18, t.path); obj(m, 'sp_from_well', 'spawn', 4, 17);
setO(m, 25, 19, 0); exitDoor(m, 25, 19, 'fog', 'sp_from_plaza'); setG(m, 25, 18, t.path); obj(m, 'sp_from_fog', 'spawn', 25, 17);
// NPCs
obj(m, 'afu', 'npc', 14, 9, { npc: 'afu' });
obj(m, 'dabao', 'npc', 15, 18, { npc: 'dabao' });
obj(m, 'bird1', 'npc', 23, 5, { npc: 'bird1' });
obj(m, 'bird2', 'npc', 25, 5, { npc: 'bird2' });
obj(m, 'extra1', 'npc', 10, 15, { npc: 'extra1' });
obj(m, 'extra2', 'npc', 19, 16, { npc: 'extra2' });
obj(m, 'extra3', 'npc', 6, 6, { npc: 'extra3' });
// interacts
obj(m, 'i_board', 'interact', 14, 9, { kind: 'board' });
obj(m, 'i_flowerbed', 'interact', 6, 14, { kind: 'flowerbed' });
obj(m, 'i_festival', 'interact', 14, 14, { kind: 'festival' });
obj(m, 'i_ribbon0', 'interact', 12, 13, { kind: 'ribbon', n: 0 });
obj(m, 'i_ribbon1', 'interact', 14, 13, { kind: 'ribbon', n: 1 });
obj(m, 'i_ribbon2', 'interact', 16, 13, { kind: 'ribbon', n: 2 });
obj(m, 'i_ribbon3', 'interact', 18, 13, { kind: 'ribbon', n: 3 });
maps.plaza = m;
}
// ============ 3. BAKERY 15x10 ============
{
const m = M(15, 10, t.wfloor); room(m);
rectG(m, 1, 1, 13, 8, t.wfloorB);
rectO(m, 2, 1, 5, 1, t.oven);
rectO(m, 1, 6, 8, 1, t.counter);
setG(m, 4, 5, t.recipe); // 配方纸 (C2 on back)
setO(m, 11, 1, t.shelf); setO(m, 12, 1, t.shelf);
setO(m, 12, 7, t.barrel); setO(m, 13, 4, t.plantPot);
setG(m, 8, 0, t.windowI);
exitDoor(m, 7, 9, 'plaza', 'sp_from_bakery');
obj(m, 'sp_from_plaza', 'spawn', 7, 8);
obj(m, 'yuan', 'npc', 4, 7, { npc: 'yuan' });
obj(m, 'i_recipe', 'interact', 4, 6, { kind: 'recipe' });
maps.bakery = m;
}
// ============ 4. GROCERY 15x10 ============
{
const m = M(15, 10, t.wfloor); room(m);
rectG(m, 1, 1, 13, 8, t.wfloorB);
// shelves: same item repeated 9 (C5)
for (let c = 2; c <= 10; c++) setO(m, c, 1, t.shelfItem); // 9 identical
rectO(m, 1, 6, 8, 1, t.counter);
setO(m, 12, 3, t.crate); setO(m, 13, 6, t.barrel); setO(m, 12, 7, t.crate);
setG(m, 11, 0, t.windowI);
exitDoor(m, 7, 9, 'plaza', 'sp_from_grocery');
obj(m, 'sp_from_plaza', 'spawn', 7, 8);
obj(m, 'mimi', 'npc', 4, 7, { npc: 'mimi' });
obj(m, 'i_shelf', 'interact', 6, 2, { kind: 'shelf' });
maps.grocery = m;
}
// ============ 5. CLINIC 12x8 ============
{
const m = M(12, 8, t.wfloor); room(m);
rectG(m, 1, 1, 10, 6, t.wfloorB);
setO(m, 2, 2, t.table); setG(m, 2, 1, t.clinicForm); // 表格 (C8)
setO(m, 9, 1, t.bookshelf); setO(m, 9, 5, t.plantPot);
setO(m, 5, 5, t.chair);
setG(m, 6, 0, t.windowI);
exitDoor(m, 6, 7, 'plaza', 'sp_from_clinic');
obj(m, 'sp_from_plaza', 'spawn', 6, 6);
obj(m, 'fu', 'npc', 4, 3, { npc: 'fu' });
obj(m, 'i_clinicform', 'interact', 2, 2, { kind: 'clinicform' });
maps.clinic = m;
}
// ============ 6. LAKE 24x16 ============
{
const m = M(24, 16, t.grass);
for (let r = 0; r < 16; r++) for (let c = 0; c < 24; c++) if (((c * 3 + r * 5) % 13) === 0) setG(m, c, r, t.grassF);
// lake fills bottom rows 9-15
for (let r = 9; r < 16; r++) for (let c = 1; c < 23; c++) {
const deep = r >= 12;
setG(m, c, r, deep ? t.waterDeep : (((c + r) % 2) ? t.water : t.waterL));
setO(m, c, r, deep ? t.waterDeep : t.water);
}
// lilypads / reeds on shore
setO(m, 3, 9, t.reed); setO(m, 20, 9, t.reed); setO(m, 12, 10, t.lilypad);
borderO(m, t.fence);
// top exit to plaza
setO(m, 12, 0, 0); exitDoor(m, 12, 0, 'plaza', 'sp_from_lake'); setG(m, 12, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 12, 2);
// bench (gui) at shore
setO(m, 5, 8, t.bench);
// picnic cloth (T7)
setG(m, 16, 6, t.picnic);
// trees
setO(m, 2, 2, t.treeTrunk); setG(m, 2, 1, t.treeTop); setO(m, 21, 3, t.treeTrunk); setG(m, 21, 2, t.treeTop);
setO(m, 8, 3, t.rock); setO(m, 18, 2, t.bushS);
obj(m, 'gui', 'npc', 5, 7, { npc: 'gui' });
obj(m, 'extra4', 'npc', 15, 3, { npc: 'extra4' });
obj(m, 'i_picnic', 'interact', 16, 6, { kind: 'picnic' });
maps.lake = m;
}
// ============ 7. FIELD 20x14 ============
{
const m = M(20, 14, t.grass);
for (let r = 0; r < 14; r++) for (let c = 0; c < 20; c++) {
const k = (c * 7 + r * 3) % 9;
if (k === 0) setG(m, c, r, t.grassF); else if (k === 3) setG(m, c, r, t.grassF2);
}
borderO(m, t.fence);
// berry bushes (T6, pick x5) — 5 spread
const berries = [[4, 3], [8, 4], [13, 3], [6, 8], [14, 9]];
berries.forEach(([c, r], i) => { setO(m, c, r, t.berry); obj(m, 'i_berry' + i, 'interact', c, r, { kind: 'berry', n: i }); });
// flowerbed (浇花点)
setO(m, 10, 6, t.flowerbed); setO(m, 11, 6, t.flowerbed);
obj(m, 'i_fieldbed', 'interact', 10, 7, { kind: 'flowerbed' });
setO(m, 2, 2, t.treeTrunk); setG(m, 2, 1, t.treeTop); setO(m, 17, 11, t.stump); setO(m, 16, 4, t.mushroom);
// top exit to plaza
setO(m, 10, 0, 0); exitDoor(m, 10, 0, 'plaza', 'sp_from_field'); setG(m, 10, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 10, 2);
obj(m, 'extra5', 'npc', 15, 7, { npc: 'extra5' });
maps.field = m;
}
// ============ 8. WELL 14x10 ============
{
const m = M(14, 10, t.grass);
for (let r = 0; r < 10; r++) for (let c = 0; c < 14; c++) if (((c + r) % 5) === 0) setG(m, c, r, t.grassF);
rectG(m, 5, 3, 4, 4, t.sand);
borderO(m, t.fence);
setO(m, 6, 4, t.well); setO(m, 7, 4, t.well);
setO(m, 2, 2, t.rock); setO(m, 11, 7, t.bushS); setO(m, 3, 7, t.stump);
// top exit to plaza
setO(m, 7, 0, 0); exitDoor(m, 7, 0, 'plaza', 'sp_from_well'); setG(m, 7, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 7, 2);
obj(m, 'i_well', 'interact', 7, 5, { kind: 'well' });
maps.well = m;
}
// ============ 9. FOG 20x8 ============
{
const m = M(20, 8, t.grass);
for (let r = 0; r < 8; r++) for (let c = 0; c < 20; c++) if (((c * 2 + r) % 7) === 0) setG(m, c, r, t.grassF);
// fog wall fills top rows 0-2 (collides)
for (let r = 0; r < 3; r++) for (let c = 0; c < 20; c++) { setG(m, c, r, t.fog); setO(m, c, r, t.fog); }
for (let r = 3; r < 8; r++) { setO(m, 0, r, t.fence); setO(m, 19, r, t.fence); }
for (let c = 0; c < 20; c++) setO(m, c, 7, t.fence);
setO(m, 3, 5, t.bushS); setO(m, 16, 5, t.bushS);
// bottom exit to plaza
setO(m, 10, 7, 0); exitDoor(m, 10, 7, 'plaza', 'sp_from_fog'); setG(m, 10, 6, t.path); obj(m, 'sp_from_plaza', 'spawn', 10, 5);
obj(m, 'i_fog', 'interact', 10, 3, { kind: 'fog' });
maps.fog = m;
}
// ============ 10. WHITE (corridor 20x6 stacked over ward) 20x14 ============
{
const m = M(20, 14, t.wfloorW);
rectG(m, 0, 0, 20, 14, t.wfloorW);
rectG(m, 0, 0, 20, 6, t.wfloorWb); // corridor top band
borderO(m, t.wwall);
// ward divider wall (row 6) with a doorway
for (let c = 0; c < 20; c++) setO(m, c, 6, t.wwall);
setO(m, 10, 6, 0); setG(m, 10, 6, t.wdoor);
// corridor windows
setG(m, 4, 0, t.wwindow); setG(m, 15, 0, t.wwindow);
// ward: hospital beds
setO(m, 3, 9, t.hbed); setO(m, 3, 11, t.hbed); setO(m, 16, 9, t.hbed);
obj(m, 'sp_corridor', 'spawn', 10, 2);
obj(m, 'sp_ward', 'spawn', 10, 11);
maps.white = m;
}
// ================= TMX serialization =================
function tmx(m) {
const csv = (arr) => {
let out = '';
for (let r = 0; r < m.h; r++) {
out += arr.slice(r * m.w, r * m.w + m.w).join(',');
if (r < m.h - 1) out += ',';
out += '\n';
}
return out;
};
const objXml = m.objects.map((o) => {
const props = Object.entries(o.props);
const p = props.length
? `\n <properties>\n${props.map(([k, v]) => ` <property name="${k}" value="${v}"/>`).join('\n')}\n </properties>\n `
: '';
return ` <object id="${o.id}" name="${o.name}" type="${o.type}" x="${o.x}" y="${o.y}" width="${o.w}" height="${o.h}">${p}</object>`;
}).join('\n');
const nextId = Math.max(1, ...m.objects.map((o) => o.id)) + 1;
return `<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${m.w}" height="${m.h}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="${nextId}">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="${PNG}" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="${m.w}" height="${m.h}">
<data encoding="csv">
${csv(m.ground)}</data>
</layer>
<layer id="2" name="obstacles" width="${m.w}" height="${m.h}">
<data encoding="csv">
${csv(m.obs)}</data>
</layer>
<objectgroup id="3" name="objects">
${objXml}
</objectgroup>
</map>
`;
}
for (const [name, m] of Object.entries(maps)) {
writeFileSync(resolve(TMX_DIR, name + '.tmx'), tmx(m));
console.log('wrote', name + '.tmx', m.w + 'x' + m.h, m.objects.length + ' objects');
}
console.log('MAPS TMX DONE');
+35
View File
@@ -0,0 +1,35 @@
-- montage.lua : upscale generated assets into preview PNGs for visual QA
local dir = app.params["outdir"]
local function open(name) return app.open(dir.."/"..name) end
local function blit(dst, src, dx, dy, sc)
for y=0,src.height-1 do for x=0,src.width-1 do
local p = src:getPixel(x,y)
for j=0,sc-1 do for i=0,sc-1 do dst:drawPixel(dx+x*sc+i, dy+y*sc+j, p) end end
end end
end
-- tiles preview
do
local ts = open("tileset.png"); local si = ts.cels[1].image
local out = Sprite(128*5, 144*5, ColorMode.RGB)
local oi = out.cels[1].image
for y=0,oi.height-1 do for x=0,oi.width-1 do oi:drawPixel(x,y, app.pixelColor.rgba(30,30,36,255)) end end
blit(oi, si, 0, 0, 5)
out:saveAs(dir.."/_preview_tiles.png"); out:close(); ts:close()
end
-- chars preview
do
local out = Sprite(760, 420, ColorMode.RGB)
local oi = out.cels[1].image
for y=0,oi.height-1 do for x=0,oi.width-1 do oi:drawPixel(x,y, app.pixelColor.rgba(40,44,52,255)) end end
local pl = open("player.png"); blit(oi, pl.cels[1].image, 0, 0, 4); pl:close()
local po = open("portraits.png"); blit(oi, po.cels[1].image, 280, 0, 4); po:close()
local bi = open("birds.png"); blit(oi, bi.cels[1].image, 560, 0, 5); bi:close()
local sp = open("sparrow.png"); blit(oi, sp.cels[1].image, 680, 0, 5); sp:close()
local ex = open("extras.png"); blit(oi, ex.cels[1].image, 560, 140, 4); ex:close()
local names = {"afu","yuan","dabao","mimi","fu","gui"}
for i,n in ipairs(names) do
local np = open("npc_"..n..".png"); blit(oi, np.cels[1].image, (i-1)*84, 200, 5); np:close()
end
out:saveAs(dir.."/_preview_chars.png"); out:close()
end
print("montage done")
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="15" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="5">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="15" height="10">
<data encoding="csv">
25,25,25,25,25,25,25,25,39,25,25,25,25,25,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,33,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,25,25,25,25,25,25,40,25,25,25,25,25,25,25
</data>
</layer>
<layer id="2" name="obstacles" width="15" height="10">
<data encoding="csv">
27,28,28,28,28,28,28,28,28,28,28,28,28,28,27,
27,0,32,32,32,32,32,0,0,0,0,34,34,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,62,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,0,27,
27,31,31,31,31,31,31,31,31,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,58,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,0,27,
27,27,27,27,27,27,27,0,27,27,27,27,27,27,27
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="door_plaza" type="door" x="112" y="144" width="16" height="16">
<properties>
<property name="target" value="plaza"/>
<property name="dest" value="sp_from_bakery"/>
</properties>
</object>
<object id="2" name="sp_from_plaza" type="spawn" x="112" y="128" width="16" height="16"></object>
<object id="3" name="yuan" type="npc" x="64" y="112" width="16" height="16">
<properties>
<property name="npc" value="yuan"/>
</properties>
</object>
<object id="4" name="i_recipe" type="interact" x="64" y="96" width="16" height="16">
<properties>
<property name="kind" value="recipe"/>
</properties>
</object>
</objectgroup>
</map>
+49
View File
@@ -0,0 +1,49 @@
<?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="5" nextobjectid="5">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="12" height="8">
<data encoding="csv">
25,25,25,25,25,25,39,25,25,25,25,25,
25,26,38,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,25,
25,25,25,25,25,25,40,25,25,25,25,25
</data>
</layer>
<layer id="2" name="obstacles" width="12" height="8">
<data encoding="csv">
27,28,28,28,28,28,28,28,28,28,28,27,
27,0,0,0,0,0,0,0,0,59,0,27,
27,0,36,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,37,0,0,0,62,0,27,
27,0,0,0,0,0,0,0,0,0,0,27,
27,27,27,27,27,27,0,27,27,27,27,27
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="door_plaza" type="door" x="96" y="112" width="16" height="16">
<properties>
<property name="target" value="plaza"/>
<property name="dest" value="sp_from_clinic"/>
</properties>
</object>
<object id="2" name="sp_from_plaza" type="spawn" x="96" y="96" width="16" height="16"></object>
<object id="3" name="fu" type="npc" x="64" y="48" width="16" height="16">
<properties>
<property name="npc" value="fu"/>
</properties>
</object>
<object id="4" name="i_clinicform" type="interact" x="32" y="32" width="16" height="16">
<properties>
<property name="kind" value="clinicform"/>
</properties>
</object>
</objectgroup>
</map>
+91
View File
@@ -0,0 +1,91 @@
<?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="5" nextobjectid="10">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="20" height="14">
<data encoding="csv">
2,1,1,65,1,1,1,1,1,2,40,1,65,1,1,1,1,1,2,1,
65,1,9,1,1,1,2,1,1,65,3,1,1,1,1,2,1,1,65,1,
1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,
2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,
65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,
1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,
2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,
65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,
1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,
2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,
65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,
1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,
2,1,1,65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,
65,1,1,1,1,1,2,1,1,65,1,1,1,1,1,2,1,1,65,1
</data>
</layer>
<layer id="2" name="obstacles" width="20" height="14">
<data encoding="csv">
14,14,14,14,14,14,14,14,14,14,0,14,14,14,14,14,14,14,14,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,8,0,0,0,0,0,0,0,0,8,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,51,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,15,15,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="i_berry0" type="interact" x="64" y="48" width="16" height="16">
<properties>
<property name="kind" value="berry"/>
<property name="n" value="0"/>
</properties>
</object>
<object id="2" name="i_berry1" type="interact" x="128" y="64" width="16" height="16">
<properties>
<property name="kind" value="berry"/>
<property name="n" value="1"/>
</properties>
</object>
<object id="3" name="i_berry2" type="interact" x="208" y="48" width="16" height="16">
<properties>
<property name="kind" value="berry"/>
<property name="n" value="2"/>
</properties>
</object>
<object id="4" name="i_berry3" type="interact" x="96" y="128" width="16" height="16">
<properties>
<property name="kind" value="berry"/>
<property name="n" value="3"/>
</properties>
</object>
<object id="5" name="i_berry4" type="interact" x="224" y="144" width="16" height="16">
<properties>
<property name="kind" value="berry"/>
<property name="n" value="4"/>
</properties>
</object>
<object id="6" name="i_fieldbed" type="interact" x="160" y="112" width="16" height="16">
<properties>
<property name="kind" value="flowerbed"/>
</properties>
</object>
<object id="7" name="door_plaza" type="door" x="160" y="0" width="16" height="16">
<properties>
<property name="target" value="plaza"/>
<property name="dest" value="sp_from_field"/>
</properties>
</object>
<object id="8" name="sp_from_plaza" type="spawn" x="160" y="32" width="16" height="16"></object>
<object id="9" name="extra5" type="npc" x="240" y="112" width="16" height="16">
<properties>
<property name="npc" value="extra5"/>
</properties>
</object>
</objectgroup>
</map>
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="4">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="20" 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,
1,1,2,1,1,1,1,1,1,2,1,1,1,1,1,1,2,1,1,1,
1,1,1,1,1,2,1,1,1,1,1,1,2,1,1,1,1,1,1,2,
1,2,1,1,1,1,1,1,2,1,1,1,1,1,1,2,1,1,1,1,
1,1,1,1,2,1,1,1,1,1,3,2,1,1,1,1,1,1,2,1,
2,1,1,1,1,1,1,2,1,1,40,1,1,1,2,1,1,1,1,1
</data>
</layer>
<layer id="2" name="obstacles" width="20" 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,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,14,14,14,14,14,14,14,14,14,0,14,14,14,14,14,14,14,14,14
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="door_plaza" type="door" x="160" y="112" width="16" height="16">
<properties>
<property name="target" value="plaza"/>
<property name="dest" value="sp_from_fog"/>
</properties>
</object>
<object id="2" name="sp_from_plaza" type="spawn" x="160" y="80" width="16" height="16"></object>
<object id="3" name="i_fog" type="interact" x="160" y="48" width="16" height="16">
<properties>
<property name="kind" value="fog"/>
</properties>
</object>
</objectgroup>
</map>
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="15" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="5">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="15" height="10">
<data encoding="csv">
25,25,25,25,25,25,25,25,25,25,25,39,25,25,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,26,26,26,26,26,26,26,26,26,26,26,26,26,25,
25,25,25,25,25,25,25,40,25,25,25,25,25,25,25
</data>
</layer>
<layer id="2" name="obstacles" width="15" height="10">
<data encoding="csv">
27,28,28,28,28,28,28,28,28,28,28,28,28,28,27,
27,0,35,35,35,35,35,35,35,35,35,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,57,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,0,27,
27,31,31,31,31,31,31,31,31,0,0,0,0,58,27,
27,0,0,0,0,0,0,0,0,0,0,0,57,0,27,
27,0,0,0,0,0,0,0,0,0,0,0,0,0,27,
27,27,27,27,27,27,27,0,27,27,27,27,27,27,27
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="door_plaza" type="door" x="112" y="144" width="16" height="16">
<properties>
<property name="target" value="plaza"/>
<property name="dest" value="sp_from_grocery"/>
</properties>
</object>
<object id="2" name="sp_from_plaza" type="spawn" x="112" y="128" width="16" height="16"></object>
<object id="3" name="mimi" type="npc" x="64" y="112" width="16" height="16">
<properties>
<property name="npc" value="mimi"/>
</properties>
</object>
<object id="4" name="i_shelf" type="interact" x="96" y="32" width="16" height="16">
<properties>
<property name="kind" value="shelf"/>
</properties>
</object>
</objectgroup>
</map>
+50
View File
@@ -0,0 +1,50 @@
<?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="5" nextobjectid="6">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="12" height="8">
<data encoding="csv">
25,25,30,25,39,25,25,39,25,25,25,25,
25,25,25,25,25,63,63,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,25,25,25,25,25,25,
25,25,25,25,25,25,40,25,25,25,25,25
</data>
</layer>
<layer id="2" name="obstacles" width="12" height="8">
<data encoding="csv">
27,28,28,28,28,28,28,28,28,28,28,27,
27,29,0,0,0,0,0,0,62,59,0,27,
27,29,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,0,0,0,27,
27,0,0,0,0,0,0,0,36,37,0,27,
27,0,0,0,0,0,0,0,0,0,0,27,
27,27,27,27,27,27,0,27,27,27,27,27
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="door_plaza" type="door" x="96" y="112" width="16" height="16">
<properties>
<property name="target" value="plaza"/>
<property name="dest" value="sp_from_home"/>
</properties>
</object>
<object id="2" name="start" type="spawn" x="48" y="48" width="16" height="16"></object>
<object id="3" name="sp_from_plaza" type="spawn" x="96" y="96" width="16" height="16"></object>
<object id="4" name="i_bed" type="interact" x="32" y="32" width="16" height="16">
<properties>
<property name="kind" value="bed"/>
</properties>
</object>
<object id="5" name="i_calendar" type="interact" x="32" y="16" width="16" height="16">
<properties>
<property name="kind" value="calendar"/>
</properties>
</object>
</objectgroup>
</map>
+70
View File
@@ -0,0 +1,70 @@
<?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="5" nextobjectid="6">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="24" height="16">
<data encoding="csv">
2,1,1,1,1,1,1,1,1,1,1,1,40,2,1,1,1,1,1,1,1,1,1,1,
1,1,9,1,1,1,1,2,1,1,1,1,3,1,1,1,1,1,1,1,2,1,1,1,
1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,9,1,1,
1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,
1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,
1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,20,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,
1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,
1,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,1,
1,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,1,
1,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,1,
1,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,1,
2,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,1,
1,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,1,
1,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,1
</data>
</layer>
<layer id="2" name="obstacles" width="24" height="16">
<data encoding="csv">
14,14,14,14,14,14,14,14,14,14,14,14,0,14,14,14,14,14,14,14,14,14,14,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,0,0,14,
14,0,0,0,0,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,10,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,5,5,55,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,55,5,5,14,
14,5,5,5,5,5,5,5,5,5,5,5,54,5,5,5,5,5,5,5,5,5,5,14,
14,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,14,
14,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,14,
14,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,14,
14,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,14,
14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="door_plaza" type="door" x="192" y="0" width="16" height="16">
<properties>
<property name="target" value="plaza"/>
<property name="dest" value="sp_from_lake"/>
</properties>
</object>
<object id="2" name="sp_from_plaza" type="spawn" x="192" y="32" width="16" height="16"></object>
<object id="3" name="gui" type="npc" x="80" y="112" width="16" height="16">
<properties>
<property name="npc" value="gui"/>
</properties>
</object>
<object id="4" name="extra4" type="npc" x="240" y="48" width="16" height="16">
<properties>
<property name="npc" value="extra4"/>
</properties>
</object>
<object id="5" name="i_picnic" type="interact" x="256" y="96" width="16" height="16">
<properties>
<property name="kind" value="picnic"/>
</properties>
</object>
</objectgroup>
</map>
+191
View File
@@ -0,0 +1,191 @@
<?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="5" nextobjectid="32">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="30" height="20">
<data encoding="csv">
2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,
1,18,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,
1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,
1,1,1,13,1,1,1,1,1,13,1,1,1,1,1,1,13,1,1,1,2,1,1,13,9,1,1,1,1,1,
1,2,1,3,1,1,1,1,1,3,1,1,2,1,1,1,3,1,1,1,1,1,1,9,9,9,1,1,1,1,
1,1,1,3,2,1,1,1,1,3,1,1,1,1,1,2,3,1,1,1,1,1,1,3,1,1,2,1,1,1,
1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,
1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,
1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,
1,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1,1,
40,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,40,
2,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,1,
1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,
1,1,1,1,1,1,2,1,1,1,1,72,72,72,72,72,72,72,72,1,1,1,1,1,1,1,1,1,2,1,
1,1,1,1,1,1,1,1,1,2,1,72,72,72,72,72,72,72,72,1,2,1,1,1,1,1,1,1,1,1,
1,2,1,1,1,1,1,1,1,1,1,72,72,72,72,72,72,72,72,1,1,1,1,2,1,1,1,1,1,1,
1,1,1,1,2,1,1,1,1,1,1,72,72,72,72,72,72,72,72,1,1,1,1,1,1,1,2,1,1,1,
1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,
1,1,1,1,3,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,3,1,1,1,1,
1,1,2,1,40,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,40,1,1,1,1
</data>
</layer>
<layer id="2" name="obstacles" width="30" height="20">
<data encoding="csv">
14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,
14,10,12,12,12,0,0,0,12,12,12,0,0,0,0,12,12,12,0,0,0,0,12,12,12,0,0,10,10,14,
14,0,11,12,11,0,0,0,11,12,11,0,0,0,0,11,12,11,0,0,0,0,11,12,11,0,0,0,0,14,
14,0,11,0,11,0,0,0,11,0,11,0,0,0,0,11,0,11,0,0,0,0,11,0,11,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,22,0,0,0,0,18,0,0,0,0,0,22,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,48,48,48,48,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,15,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,0,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,14,
14,14,14,14,0,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,0,14,14,14,14
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="door_home" type="door" x="48" y="48" width="16" height="16">
<properties>
<property name="target" value="home"/>
<property name="dest" value="sp_from_plaza"/>
</properties>
</object>
<object id="2" name="sp_from_home" type="spawn" x="48" y="80" width="16" height="16"></object>
<object id="3" name="door_bakery" type="door" x="144" y="48" width="16" height="16">
<properties>
<property name="target" value="bakery"/>
<property name="dest" value="sp_from_plaza"/>
</properties>
</object>
<object id="4" name="sp_from_bakery" type="spawn" x="144" y="80" width="16" height="16"></object>
<object id="5" name="door_grocery" type="door" x="256" y="48" width="16" height="16">
<properties>
<property name="target" value="grocery"/>
<property name="dest" value="sp_from_plaza"/>
</properties>
</object>
<object id="6" name="sp_from_grocery" type="spawn" x="256" y="80" width="16" height="16"></object>
<object id="7" name="door_clinic" type="door" x="368" y="48" width="16" height="16">
<properties>
<property name="target" value="clinic"/>
<property name="dest" value="sp_from_plaza"/>
</properties>
</object>
<object id="8" name="sp_from_clinic" type="spawn" x="368" y="80" width="16" height="16"></object>
<object id="9" name="i_mailbox" type="interact" x="240" y="256" width="16" height="16">
<properties>
<property name="kind" value="mailbox"/>
</properties>
</object>
<object id="10" name="door_field" type="door" x="0" y="160" width="16" height="16">
<properties>
<property name="target" value="field"/>
<property name="dest" value="sp_from_plaza"/>
</properties>
</object>
<object id="11" name="sp_from_field" type="spawn" x="32" y="160" width="16" height="16"></object>
<object id="12" name="door_lake" type="door" x="464" y="160" width="16" height="16">
<properties>
<property name="target" value="lake"/>
<property name="dest" value="sp_from_plaza"/>
</properties>
</object>
<object id="13" name="sp_from_lake" type="spawn" x="432" y="160" width="16" height="16"></object>
<object id="14" name="door_well" type="door" x="64" y="304" width="16" height="16">
<properties>
<property name="target" value="well"/>
<property name="dest" value="sp_from_plaza"/>
</properties>
</object>
<object id="15" name="sp_from_well" type="spawn" x="64" y="272" width="16" height="16"></object>
<object id="16" name="door_fog" type="door" x="400" y="304" width="16" height="16">
<properties>
<property name="target" value="fog"/>
<property name="dest" value="sp_from_plaza"/>
</properties>
</object>
<object id="17" name="sp_from_fog" type="spawn" x="400" y="272" width="16" height="16"></object>
<object id="18" name="afu" type="npc" x="224" y="144" width="16" height="16">
<properties>
<property name="npc" value="afu"/>
</properties>
</object>
<object id="19" name="dabao" type="npc" x="240" y="288" width="16" height="16">
<properties>
<property name="npc" value="dabao"/>
</properties>
</object>
<object id="20" name="bird1" type="npc" x="368" y="80" width="16" height="16">
<properties>
<property name="npc" value="bird1"/>
</properties>
</object>
<object id="21" name="bird2" type="npc" x="400" y="80" width="16" height="16">
<properties>
<property name="npc" value="bird2"/>
</properties>
</object>
<object id="22" name="extra1" type="npc" x="160" y="240" width="16" height="16">
<properties>
<property name="npc" value="extra1"/>
</properties>
</object>
<object id="23" name="extra2" type="npc" x="304" y="256" width="16" height="16">
<properties>
<property name="npc" value="extra2"/>
</properties>
</object>
<object id="24" name="extra3" type="npc" x="96" y="96" width="16" height="16">
<properties>
<property name="npc" value="extra3"/>
</properties>
</object>
<object id="25" name="i_board" type="interact" x="224" y="144" width="16" height="16">
<properties>
<property name="kind" value="board"/>
</properties>
</object>
<object id="26" name="i_flowerbed" type="interact" x="96" y="224" width="16" height="16">
<properties>
<property name="kind" value="flowerbed"/>
</properties>
</object>
<object id="27" name="i_festival" type="interact" x="224" y="224" width="16" height="16">
<properties>
<property name="kind" value="festival"/>
</properties>
</object>
<object id="28" name="i_ribbon0" type="interact" x="192" y="208" width="16" height="16">
<properties>
<property name="kind" value="ribbon"/>
<property name="n" value="0"/>
</properties>
</object>
<object id="29" name="i_ribbon1" type="interact" x="224" y="208" width="16" height="16">
<properties>
<property name="kind" value="ribbon"/>
<property name="n" value="1"/>
</properties>
</object>
<object id="30" name="i_ribbon2" type="interact" x="256" y="208" width="16" height="16">
<properties>
<property name="kind" value="ribbon"/>
<property name="n" value="2"/>
</properties>
</object>
<object id="31" name="i_ribbon3" type="interact" x="288" y="208" width="16" height="16">
<properties>
<property name="kind" value="ribbon"/>
<property name="n" value="3"/>
</properties>
</object>
</objectgroup>
</map>
+48
View File
@@ -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="14" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="4">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="14" height="10">
<data encoding="csv">
2,1,1,1,1,2,1,40,1,1,2,1,1,1,
1,1,1,1,2,1,1,3,1,2,1,1,1,1,
1,1,1,2,1,1,1,1,2,1,1,1,1,2,
1,1,2,1,1,24,24,24,24,1,1,1,2,1,
1,2,1,1,1,24,24,24,24,1,1,2,1,1,
2,1,1,1,1,24,24,24,24,1,2,1,1,1,
1,1,1,1,2,24,24,24,24,2,1,1,1,1,
1,1,1,2,1,1,1,1,2,1,1,1,1,2,
1,1,2,1,1,1,1,2,1,1,1,1,2,1,
1,2,1,1,1,1,2,1,1,1,1,2,1,1
</data>
</layer>
<layer id="2" name="obstacles" width="14" height="10">
<data encoding="csv">
14,14,14,14,14,14,14,0,14,14,14,14,14,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,52,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,16,16,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,0,0,53,0,0,0,0,0,0,0,23,0,14,
14,0,0,0,0,0,0,0,0,0,0,0,0,14,
14,14,14,14,14,14,14,14,14,14,14,14,14,14
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="door_plaza" type="door" x="112" y="0" width="16" height="16">
<properties>
<property name="target" value="plaza"/>
<property name="dest" value="sp_from_well"/>
</properties>
</object>
<object id="2" name="sp_from_plaza" type="spawn" x="112" y="32" width="16" height="16"></object>
<object id="3" name="i_well" type="interact" x="112" y="80" width="16" height="16">
<properties>
<property name="kind" value="well"/>
</properties>
</object>
</objectgroup>
</map>
+46
View File
@@ -0,0 +1,46 @@
<?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="5" nextobjectid="3">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Opus4-8-xhigh/public/assets/sprites/tileset.png" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="20" height="14">
<data encoding="csv">
43,43,43,43,46,43,43,43,43,43,43,43,43,43,43,46,43,43,43,43,
43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,
43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,
43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,
43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,
43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,
42,42,42,42,42,42,42,42,42,42,45,42,42,42,42,42,42,42,42,42,
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42
</data>
</layer>
<layer id="2" name="obstacles" width="20" height="14">
<data encoding="csv">
41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,41,41,41,41,41,41,41,41,41,0,41,41,41,41,41,41,41,41,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,44,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,44,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41
</data>
</layer>
<objectgroup id="3" name="objects">
<object id="1" name="sp_corridor" type="spawn" x="160" y="32" width="16" height="16"></object>
<object id="2" name="sp_ward" type="spawn" x="160" y="176" width="16" height="16"></object>
</objectgroup>
</map>
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": false,
"noImplicitAny": false,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": []
},
"include": ["src"]
}
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
// ponytail: single-page pixel game, no framework plugin needed.
export default defineConfig({
base: './',
build: {
target: 'es2020',
assetsInlineLimit: 0, // keep pixel PNGs & map JSON as real files
},
});