|
|
@@ -0,0 +1,884 @@
|
|
|
+# 技术设计文档:分层架构重构
|
|
|
+
|
|
|
+## 概述
|
|
|
+
|
|
|
+本文档描述对 LÖVE2D Lua 农场游戏进行分层架构重构的技术方案。
|
|
|
+
|
|
|
+### 当前问题
|
|
|
+
|
|
|
+| 问题 | 位置 | 影响 |
|
|
|
+|------|------|------|
|
|
|
+| 上帝对象 | `game.lua` | 同时持有 systems 层和 render 层引用,承担过多职责 |
|
|
|
+| 反向依赖 | `render/ui_renderer.lua` | 顶层 `require("systems/farm")`、`require("systems/state")` |
|
|
|
+| 跨层调用 | `systems/player.lua` | 通过注入的 `_world` 变量调用 `WorldRenderer` 方法 |
|
|
|
+| love 直接调用 | `render/` 所有文件 | 直接调用 `love.graphics.*`,无法在无 LÖVE 环境下测试渲染逻辑 |
|
|
|
+| love 直接调用 | `engine/particles.lua`、`engine/engine.lua` | 部分 Engine 层模块直接调用 `love.*`,未通过适配器隔离 |
|
|
|
+| 缺少层注释 | 全部文件 | 无法快速定位模块所属层次 |
|
|
|
+| OOP 不统一 | 全部文件 | 部分用 `Class.__index = Class`,部分用 classic,不一致 |
|
|
|
+
|
|
|
+### 重构目标
|
|
|
+
|
|
|
+引入清晰的五层架构,使用 `classic` 库统一接口定义,通过接口隔离各层,消除跨层调用和反向依赖。**只有最底层的 Love 适配层(`love_adapter/`)允许直接调用 `love.*` API;Engine 层通过适配层接口访问框架能力;Render 层通过 `IGraphics` 接口调用绘图;Systems 层完全不接触 love。更换游戏引擎时,只需重写 `love_adapter/` 目录下的实现,其余所有层无需改动。**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 架构
|
|
|
+
|
|
|
+### 五层架构总览
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────────────┐
|
|
|
+│ 协调层 (game.lua) │
|
|
|
+│ 负责组装各层对象、注入接口实现、转发框架事件 │
|
|
|
+└──────────────────────┬──────────────────────────────────┘
|
|
|
+ │ 创建 + 注入
|
|
|
+ ┌────────────┴────────────┐
|
|
|
+ ▼ ▼
|
|
|
+┌─────────────────┐ ┌─────────────────────┐
|
|
|
+│ Systems 层 │ │ Render 层 │
|
|
|
+│ 业务逻辑 │◄──────│ 通过接口读取状态 │
|
|
|
+│ farm / player │ 接口 │ 通过 IGraphics 绘图 │
|
|
|
+│ world / pet │ │ 不调用任何 love.* │
|
|
|
+└────────┬────────┘ └──────────┬────────────┘
|
|
|
+ │ 依赖 │ 依赖 IGraphics
|
|
|
+ ▼ ▼
|
|
|
+┌─────────────────────────────────────────────────────────┐
|
|
|
+│ Engine 层 │
|
|
|
+│ EventBus / Camera / Save / Audio / Registry / Anim │
|
|
|
+│ 通过 Love 适配层接口访问框架能力,不直接调用 love.* │
|
|
|
+└──────────────────────────────────────────────────────────┘
|
|
|
+ │ 依赖(通过接口)
|
|
|
+ ▼
|
|
|
+┌─────────────────────────────────────────────────────────┐
|
|
|
+│ Love 适配层 (love_adapter/) │
|
|
|
+│ IGraphics / IFilesystem / IAudio / IWindow 的实现 │
|
|
|
+│ ★ 唯一允许直接调用 love.* 的层 │
|
|
|
+│ 更换引擎时只需重写此层 │
|
|
|
+└──────────────────────────────────────────────────────────┘
|
|
|
+ ▲
|
|
|
+ │ 只读数据
|
|
|
+┌─────────────────────────────────────────────────────────┐
|
|
|
+│ Data 层 │
|
|
|
+│ crops / tools / items / blocks / config(纯静态表) │
|
|
|
+└──────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### love 隔离原则
|
|
|
+
|
|
|
+**只有 Love 适配层(`love_adapter/`)允许直接调用 `love.*` API。** 各层隔离方式:
|
|
|
+
|
|
|
+| 层 | love 隔离方式 | 现状 |
|
|
|
+|----|--------------|------|
|
|
|
+| Love 适配层 | 唯一调用 `love.*` 的层,实现各框架接口 | 待创建 |
|
|
|
+| Engine 层 | 通过注入的适配层接口访问框架能力,不直接调用 `love.*` | 部分满足(save/audio 已注入隔离,particles/engine.lua 待整理) |
|
|
|
+| Systems 层 | 完全不使用 love | 已满足(world_system、lighting_system 等均无 love 调用) |
|
|
|
+| Render 层 | 通过 `IGraphics` 接口调用绘图,不直接调用 `love.*` | 待重构 |
|
|
|
+| Data 层 | 纯静态表,无任何 love 调用 | 已满足 |
|
|
|
+
|
|
|
+### 依赖方向规则
|
|
|
+
|
|
|
+```
|
|
|
+Love 适配层 ← Engine 层(Engine 通过接口依赖适配层)
|
|
|
+Engine 层 ← Systems 层(Systems 依赖 Engine + Data,不调用 love)
|
|
|
+Engine 层 ← Render 层(Render 依赖 Engine,通过 IGraphics 绘图)
|
|
|
+Systems 层 ✗ Render 层(禁止)
|
|
|
+Render 层 ✗ Systems 层(禁止直接 require)
|
|
|
+Engine 层 ✗ love.*(禁止,通过适配层接口访问)
|
|
|
+Systems 层 ✗ love.*(禁止)
|
|
|
+Render 层 ✗ love.*(禁止)
|
|
|
+```
|
|
|
+
|
|
|
+### 接口隔离机制
|
|
|
+
|
|
|
+```
|
|
|
+Systems 层 Render 层
|
|
|
+┌──────────────┐ ┌──────────────────┐
|
|
|
+│ WorldSystem │ │ WorldRenderer │
|
|
|
+│ implements │◄─────────────│ 构造时注入 │
|
|
|
+│ IWorldQuery │ 接口引用 │ IWorldQuery │
|
|
|
+└──────────────┘ └──────────────────┘
|
|
|
+
|
|
|
+┌──────────────┐ ┌──────────────────┐
|
|
|
+│ FarmSystem │◄─────────────│ UIRenderer │
|
|
|
+│ implements │ 接口引用 │ FarmRenderer │
|
|
|
+│ IFarmQuery │ │ 构造时注入 │
|
|
|
+└──────────────┘ │ IFarmQuery │
|
|
|
+ └──────────────────┘
|
|
|
+
|
|
|
+Love 适配层 Engine 层 / Render 层
|
|
|
+┌──────────────────────┐ ┌──────────────────────┐
|
|
|
+│ LoveGraphics │ │ Engine 模块(如 Anim)│
|
|
|
+│ implements IGraphics │◄─────│ Render 层所有渲染器 │
|
|
|
+│ 唯一调用 love.graphics│ │ 构造时注入 IGraphics │
|
|
|
+└──────────────────────┘ └──────────────────────┘
|
|
|
+
|
|
|
+┌──────────────────────┐ ┌──────────────────────┐
|
|
|
+│ LoveFilesystem │ │ engine/save.lua │
|
|
|
+│ implements │◄─────│ engine/mod_loader.lua │
|
|
|
+│ IFilesystem │ │ 通过注入接口访问文件系统│
|
|
|
+└──────────────────────┘ └──────────────────────┘
|
|
|
+
|
|
|
+┌──────────────────────┐ ┌──────────────────────┐
|
|
|
+│ LoveAudio │ │ engine/audio.lua │
|
|
|
+│ implements IAudio │◄─────│ engine/sfx.lua │
|
|
|
+└──────────────────────┘ └──────────────────────┘
|
|
|
+
|
|
|
+┌──────────────────────┐ ┌──────────────────────┐
|
|
|
+│ LoveWindow │ │ engine/engine.lua │
|
|
|
+│ implements IWindow │◄─────│ engine/camera.lua │
|
|
|
+└──────────────────────┘ └──────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 组件与接口
|
|
|
+
|
|
|
+### Love 适配层(love_adapter/)
|
|
|
+
|
|
|
+**这是整个项目唯一允许调用 `love.*` 的目录。** 包含四个适配器文件,每个文件实现一个框架接口。更换引擎时,只需重写此目录下的文件。
|
|
|
+
|
|
|
+#### love_adapter/graphics.lua — IGraphics 实现
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Love适配层] IGraphics 的 LÖVE2D 实现,封装所有 love.graphics 调用
|
|
|
+local IGraphics = require("engine/interface").IGraphics
|
|
|
+
|
|
|
+local LoveGraphics = IGraphics:extend()
|
|
|
+
|
|
|
+function LoveGraphics:setColor(r, g, b, a) love.graphics.setColor(r, g, b, a) end
|
|
|
+function LoveGraphics:getWidth() return love.graphics.getWidth() end
|
|
|
+function LoveGraphics:getHeight() return love.graphics.getHeight() end
|
|
|
+function LoveGraphics:setScissor(x, y, w, h) love.graphics.setScissor(x, y, w, h) end
|
|
|
+function LoveGraphics:rectangle(mode, x, y, w, h, rx) love.graphics.rectangle(mode, x, y, w, h, rx) end
|
|
|
+function LoveGraphics:circle(mode, x, y, r) love.graphics.circle(mode, x, y, r) end
|
|
|
+function LoveGraphics:print(text, x, y, r, sx, sy) love.graphics.print(text, x, y, r, sx, sy) end
|
|
|
+function LoveGraphics:printf(text, x, y, lim, align) love.graphics.printf(text, x, y, lim, align) end
|
|
|
+function LoveGraphics:newImage(path) return love.graphics.newImage(path) end
|
|
|
+function LoveGraphics:newQuad(x, y, w, h, sw, sh) return love.graphics.newQuad(x, y, w, h, sw, sh) end
|
|
|
+function LoveGraphics:draw(drawable, x, y, r, sx, sy, ox, oy)
|
|
|
+ love.graphics.draw(drawable, x, y, r, sx, sy, ox, oy)
|
|
|
+end
|
|
|
+function LoveGraphics:push() love.graphics.push() end
|
|
|
+function LoveGraphics:pop() love.graphics.pop() end
|
|
|
+function LoveGraphics:translate(x, y) love.graphics.translate(x, y) end
|
|
|
+function LoveGraphics:scale(sx, sy) love.graphics.scale(sx, sy) end
|
|
|
+function LoveGraphics:newCanvas(w, h) return love.graphics.newCanvas(w, h) end
|
|
|
+function LoveGraphics:setCanvas(canvas) love.graphics.setCanvas(canvas) end
|
|
|
+function LoveGraphics:clear(r, g, b, a) love.graphics.clear(r, g, b, a) end
|
|
|
+function LoveGraphics:newShader(code) return love.graphics.newShader(code) end
|
|
|
+function LoveGraphics:setShader(shader) love.graphics.setShader(shader) end
|
|
|
+function LoveGraphics:newFont(path, size) return love.graphics.newFont(path, size) end
|
|
|
+function LoveGraphics:setFont(font) love.graphics.setFont(font) end
|
|
|
+function LoveGraphics:getFont() return love.graphics.getFont() end
|
|
|
+
|
|
|
+return LoveGraphics
|
|
|
+```
|
|
|
+
|
|
|
+#### love_adapter/filesystem.lua — IFilesystem 实现
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Love适配层] IFilesystem 的 LÖVE2D 实现,封装所有 love.filesystem 调用
|
|
|
+local IFilesystem = require("engine/interface").IFilesystem
|
|
|
+
|
|
|
+local LoveFilesystem = IFilesystem:extend()
|
|
|
+
|
|
|
+function LoveFilesystem:read(path) return love.filesystem.read(path) end
|
|
|
+function LoveFilesystem:write(path, data) return love.filesystem.write(path, data) end
|
|
|
+function LoveFilesystem:exists(path) return love.filesystem.getInfo(path) ~= nil end
|
|
|
+function LoveFilesystem:remove(path) return love.filesystem.remove(path) end
|
|
|
+function LoveFilesystem:getInfo(path) return love.filesystem.getInfo(path) end
|
|
|
+function LoveFilesystem:getDirectoryItems(p) return love.filesystem.getDirectoryItems(p) end
|
|
|
+function LoveFilesystem:createDirectory(p) return love.filesystem.createDirectory(p) end
|
|
|
+function LoveFilesystem:load(path) return love.filesystem.load(path) end
|
|
|
+
|
|
|
+return LoveFilesystem
|
|
|
+```
|
|
|
+
|
|
|
+#### love_adapter/audio.lua — IAudio 实现
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Love适配层] IAudio 的 LÖVE2D 实现,封装所有 love.audio / love.sound 调用
|
|
|
+local IAudio = require("engine/interface").IAudio
|
|
|
+
|
|
|
+local LoveAudio = IAudio:extend()
|
|
|
+
|
|
|
+function LoveAudio:newSoundData(samples, rate, bits, channels)
|
|
|
+ return love.sound.newSoundData(samples, rate, bits, channels)
|
|
|
+end
|
|
|
+function LoveAudio:newSource(soundData)
|
|
|
+ return love.audio.newSource(soundData)
|
|
|
+end
|
|
|
+
|
|
|
+return LoveAudio
|
|
|
+```
|
|
|
+
|
|
|
+#### love_adapter/window.lua — IWindow 实现
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Love适配层] IWindow 的 LÖVE2D 实现,封装 love.graphics 屏幕尺寸查询
|
|
|
+local IWindow = require("engine/interface").IWindow
|
|
|
+
|
|
|
+local LoveWindow = IWindow:extend()
|
|
|
+
|
|
|
+function LoveWindow:getWidth() return love.graphics.getWidth() end
|
|
|
+function LoveWindow:getHeight() return love.graphics.getHeight() end
|
|
|
+
|
|
|
+return LoveWindow
|
|
|
+```
|
|
|
+
|
|
|
+### engine/interface.lua — 接口基础设施
|
|
|
+
|
|
|
+所有跨层接口的基类,依赖 `classic.lua`,不依赖任何其他项目模块。同时定义所有框架接口(`IGraphics`、`IFilesystem`、`IAudio`、`IWindow`)和业务接口(`IWorldQuery`、`IFarmQuery`、`IGameStateQuery`)。
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Engine层] 接口基类定义,所有跨层接口继承自此
|
|
|
+local Object = require("classic")
|
|
|
+
|
|
|
+local Interface_Base = Object:extend()
|
|
|
+
|
|
|
+local function makeUnimplemented(interfaceName, methodName)
|
|
|
+ return function()
|
|
|
+ error(interfaceName .. "." .. methodName .. " 未实现")
|
|
|
+ end
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+**框架接口**(供 Engine 层模块使用,由 Love 适配层实现):
|
|
|
+
|
|
|
+**IGraphics 接口**(覆盖 Render 层和 Engine 层所需的绘图方法):
|
|
|
+
|
|
|
+```lua
|
|
|
+local IGraphics = Interface_Base:extend()
|
|
|
+function IGraphics:setColor(r, g, b, a) end
|
|
|
+function IGraphics:getWidth() end
|
|
|
+function IGraphics:getHeight() end
|
|
|
+function IGraphics:setScissor(x, y, w, h) end
|
|
|
+function IGraphics:rectangle(mode, x, y, w, h, rx, ry) end
|
|
|
+function IGraphics:circle(mode, x, y, r) end
|
|
|
+function IGraphics:line(...) end
|
|
|
+function IGraphics:print(text, x, y, r, sx, sy) end
|
|
|
+function IGraphics:printf(text, x, y, limit, align) end
|
|
|
+function IGraphics:newImage(path) end
|
|
|
+function IGraphics:newQuad(x, y, w, h, sw, sh) end
|
|
|
+function IGraphics:draw(drawable, x, y, r, sx, sy, ox, oy) end
|
|
|
+function IGraphics:push() end
|
|
|
+function IGraphics:pop() end
|
|
|
+function IGraphics:translate(x, y) end
|
|
|
+function IGraphics:scale(sx, sy) end
|
|
|
+function IGraphics:newCanvas(w, h) end
|
|
|
+function IGraphics:setCanvas(canvas) end
|
|
|
+function IGraphics:clear(r, g, b, a) end
|
|
|
+function IGraphics:newShader(code) end
|
|
|
+function IGraphics:setShader(shader) end
|
|
|
+function IGraphics:newFont(path, size) end
|
|
|
+function IGraphics:setFont(font) end
|
|
|
+function IGraphics:getFont() end
|
|
|
+```
|
|
|
+
|
|
|
+**IFilesystem 接口**(供 `engine/save.lua`、`engine/mod_loader.lua` 使用):
|
|
|
+
|
|
|
+```lua
|
|
|
+local IFilesystem = Interface_Base:extend()
|
|
|
+function IFilesystem:read(path) end
|
|
|
+function IFilesystem:write(path, data) end
|
|
|
+function IFilesystem:exists(path) end
|
|
|
+function IFilesystem:remove(path) end
|
|
|
+function IFilesystem:getInfo(path) end
|
|
|
+function IFilesystem:getDirectoryItems(path) end
|
|
|
+function IFilesystem:createDirectory(path) end
|
|
|
+function IFilesystem:load(path) end
|
|
|
+```
|
|
|
+
|
|
|
+**IAudio 接口**(供 `engine/audio.lua`、`engine/sfx.lua` 使用):
|
|
|
+
|
|
|
+```lua
|
|
|
+local IAudio = Interface_Base:extend()
|
|
|
+function IAudio:newSoundData(samples, rate, bits, channels) end
|
|
|
+function IAudio:newSource(soundData) end
|
|
|
+```
|
|
|
+
|
|
|
+**IWindow 接口**(供 `engine/camera.lua`、`engine/engine.lua` 使用):
|
|
|
+
|
|
|
+```lua
|
|
|
+local IWindow = Interface_Base:extend()
|
|
|
+function IWindow:getWidth() end
|
|
|
+function IWindow:getHeight() end
|
|
|
+```
|
|
|
+
|
|
|
+**MockGraphics**(测试用,不依赖 love,定义于 `engine/interface.lua` 或独立测试辅助文件):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Engine层] IGraphics 的测试 mock 实现,不调用任何 love API
|
|
|
+local MockGraphics = IGraphics:extend()
|
|
|
+function MockGraphics:new()
|
|
|
+ self.calls = {}
|
|
|
+ self._w, self._h = 480, 854
|
|
|
+end
|
|
|
+function MockGraphics:setColor(r, g, b, a)
|
|
|
+ table.insert(self.calls, {"setColor", r, g, b, a})
|
|
|
+end
|
|
|
+function MockGraphics:getWidth() return self._w end
|
|
|
+function MockGraphics:getHeight() return self._h end
|
|
|
+-- ... 其余方法均记录调用
|
|
|
+```
|
|
|
+
|
|
|
+### classic.lua — OOP 基础库
|
|
|
+
|
|
|
+来自 rxi/classic,放置于项目根目录。提供 `Object:extend()`、`Object:new()` 等方法。所有 Systems 层和 Render 层的主要类通过 `classic` 定义。
|
|
|
+
|
|
|
+### Engine 层模块说明
|
|
|
+
|
|
|
+Engine 层模块通过构造时注入的适配层接口访问框架能力,不直接调用 `love.*`:
|
|
|
+
|
|
|
+- `engine/save.lua`:已通过 `Save.setFS(IFilesystem)` 注入文件系统接口,无需修改
|
|
|
+- `engine/audio.lua`:已通过 `Audio.setLove(IAudio)` 注入音频接口,无需修改
|
|
|
+- `engine/camera.lua`:已通过 `Camera.setScreenSize(w, h)` 注入屏幕尺寸,无需修改
|
|
|
+- `engine/particles.lua`:当前直接调用 `love.graphics.*`,需重构为接收 `IGraphics` 注入
|
|
|
+- `engine/engine.lua`:当前直接调用 `love.*`,需重构为通过 `IGraphics`、`IFilesystem`、`IWindow` 接口访问
|
|
|
+- `engine/mod_loader.lua`:当前直接调用 `love.filesystem.*`,需重构为通过 `IFilesystem` 接口访问
|
|
|
+
|
|
|
+### Systems 层类定义
|
|
|
+
|
|
|
+**WorldSystem**(实现 IWorldQuery):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Systems层] 世界地图数据与逻辑
|
|
|
+local Object = require("classic")
|
|
|
+local IWorldQuery = require("engine/interface").IWorldQuery
|
|
|
+
|
|
|
+local WorldSystem = IWorldQuery:extend()
|
|
|
+
|
|
|
+function WorldSystem:new(seed)
|
|
|
+ -- 初始化地图数据
|
|
|
+ self._baseData = nil
|
|
|
+ self._overlayData = nil
|
|
|
+ -- ... 生成地图
|
|
|
+ self:init(seed)
|
|
|
+end
|
|
|
+
|
|
|
+-- 实现 IWorldQuery 的所有方法
|
|
|
+function WorldSystem:isPath(row, col) ... end
|
|
|
+function WorldSystem:isCollision(row, col) ... end
|
|
|
+-- ... 其余 12 个方法
|
|
|
+```
|
|
|
+
|
|
|
+**FarmSystem**(实现 IFarmQuery):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Systems层] 农场业务逻辑
|
|
|
+local Object = require("classic")
|
|
|
+local IFarmQuery = require("engine/interface").IFarmQuery
|
|
|
+
|
|
|
+local FarmSystem = IFarmQuery:extend()
|
|
|
+
|
|
|
+function FarmSystem:new()
|
|
|
+ self.farmData = {}
|
|
|
+end
|
|
|
+
|
|
|
+-- 实现 IFarmQuery
|
|
|
+function FarmSystem:getTile(row, col) ... end
|
|
|
+function FarmSystem:getProgress(row, col, speedMult) ... end
|
|
|
+function FarmSystem:getRemaining(row, col, speedMult) ... end
|
|
|
+function FarmSystem:countNeighborCrops(row, col) ... end
|
|
|
+
|
|
|
+-- FarmSystem 自有方法(不在接口中)
|
|
|
+function FarmSystem:till(row, col) ... end
|
|
|
+function FarmSystem:plant(row, col, cropId, gene) ... end
|
|
|
+function FarmSystem:water(row, col) ... end
|
|
|
+function FarmSystem:harvest(row, col, stats) ... end
|
|
|
+function FarmSystem:setWorldPave(fn) ... end
|
|
|
+```
|
|
|
+
|
|
|
+**PlayerSystem**(接收 IWorldQuery):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Systems层] 玩家控制与工具操作
|
|
|
+local Object = require("classic")
|
|
|
+
|
|
|
+local PlayerSystem = Object:extend()
|
|
|
+
|
|
|
+-- 构造函数接收 IWorldQuery 接口实例
|
|
|
+function PlayerSystem:new(farmSystem, gameState, blockSystem, worldQuery)
|
|
|
+ self.farm = farmSystem
|
|
|
+ self.state = gameState
|
|
|
+ self.blocks = blockSystem
|
|
|
+ self._world = worldQuery -- IWorldQuery 实例,替代模块级 _world 变量
|
|
|
+ self.character = Character:new(50, 51)
|
|
|
+ -- ...
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+**Character**(接收 IWorldQuery):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Systems层] 角色状态机与寻路移动
|
|
|
+local Object = require("classic")
|
|
|
+
|
|
|
+local Character = Object:extend()
|
|
|
+
|
|
|
+function Character:new(startRow, startCol, worldQuery)
|
|
|
+ -- worldQuery 用于 isPath 查询(替代注入函数 _isPath)
|
|
|
+ self._world = worldQuery
|
|
|
+ -- ...
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+**Pet**(接收 IWorldQuery):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Systems层] 宠物跟随与离线收益
|
|
|
+local Object = require("classic")
|
|
|
+
|
|
|
+local Pet = Object:extend()
|
|
|
+
|
|
|
+function Pet:new(worldQuery)
|
|
|
+ self._world = worldQuery -- 替代模块级 _isCollision 函数
|
|
|
+ -- ...
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+### Render 层类定义
|
|
|
+
|
|
|
+Render 层所有类通过构造函数接收 `IGraphics` 实例,**不直接调用 `love.graphics.*`**。
|
|
|
+
|
|
|
+**WorldRenderer**(接收 IWorldQuery + IGraphics):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Render层] 世界地图渲染,通过 IGraphics 绘图,不直接调用 love
|
|
|
+local Object = require("classic")
|
|
|
+
|
|
|
+local WorldRenderer = Object:extend()
|
|
|
+
|
|
|
+function WorldRenderer:new(worldQuery, graphics)
|
|
|
+ self._world = worldQuery -- IWorldQuery
|
|
|
+ self._gfx = graphics -- IGraphics,替代直接调用 love.graphics
|
|
|
+ self._animTime = 0
|
|
|
+ self._tileImgs = {}
|
|
|
+end
|
|
|
+
|
|
|
+function WorldRenderer:update(dt) ... end
|
|
|
+function WorldRenderer:drawBackground(brightnessMap, baseBrightness)
|
|
|
+ -- 使用 self._gfx:setColor(...)、self._gfx:draw(...) 等
|
|
|
+ -- 不出现任何 love.graphics.* 调用
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+**FarmRenderer**(接收 IFarmQuery + IGraphics):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Render层] 农场格子渲染
|
|
|
+local Object = require("classic")
|
|
|
+
|
|
|
+local FarmRenderer = Object:extend()
|
|
|
+
|
|
|
+function FarmRenderer:new(farmQuery, graphics)
|
|
|
+ self._farm = farmQuery -- IFarmQuery
|
|
|
+ self._gfx = graphics -- IGraphics
|
|
|
+end
|
|
|
+
|
|
|
+function FarmRenderer:draw(camera) ... end
|
|
|
+function FarmRenderer.getTileCenter(row, col) ... end
|
|
|
+```
|
|
|
+
|
|
|
+**UIRenderer**(接收 IFarmQuery + IGameStateQuery + IGraphics):
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [Render层] HUD、商店、消息等 UI 渲染
|
|
|
+local Object = require("classic")
|
|
|
+
|
|
|
+local UIRenderer = Object:extend()
|
|
|
+
|
|
|
+function UIRenderer:new(farmQuery, gameStateQuery, graphics)
|
|
|
+ self._farm = farmQuery -- IFarmQuery
|
|
|
+ self._gameState = gameStateQuery -- IGameStateQuery
|
|
|
+ self._gfx = graphics -- IGraphics
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+所有其他 Render 层类(`BlockRenderer`、`CharacterRenderer`、`CropRenderer`、`DropsRenderer`、`EffectRenderer`、`InventoryRenderer`、`PetRenderer`、`TalentRenderer`、`WorkbenchRenderer`、`DungeonRenderer`、`ForgeRenderer`)均同样接收 `IGraphics` 实例,不直接调用 `love.graphics.*`。
|
|
|
+
|
|
|
+### 协调层 game.lua
|
|
|
+
|
|
|
+`game.lua` 重构为薄协调层,职责:
|
|
|
+1. 创建 Love 适配层实例(`LoveGraphics`、`LoveFilesystem`、`LoveAudio`、`LoveWindow`)
|
|
|
+2. 将适配层实例注入到 Engine 层模块
|
|
|
+3. 创建所有 Systems 层对象
|
|
|
+4. 将 Systems 对象包装为接口实现
|
|
|
+5. 将接口(含 `IGraphics`)注入到 Render 层模块
|
|
|
+6. 转发框架事件给对应系统
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [协调层] 游戏主控制器,负责组装各层对象并传递接口
|
|
|
+function Game:new()
|
|
|
+ -- 1. 创建 Love 适配层实例(唯一 require love_adapter/ 的地方)
|
|
|
+ local LoveGraphics = require("love_adapter/graphics")
|
|
|
+ local LoveFilesystem = require("love_adapter/filesystem")
|
|
|
+ local LoveAudio = require("love_adapter/audio")
|
|
|
+ local LoveWindow = require("love_adapter/window")
|
|
|
+
|
|
|
+ self.graphics = LoveGraphics:new()
|
|
|
+ self.filesystem = LoveFilesystem:new()
|
|
|
+ self.audio = LoveAudio:new()
|
|
|
+ self.window = LoveWindow:new()
|
|
|
+
|
|
|
+ -- 2. 将适配层注入 Engine 层模块
|
|
|
+ local Save = require("engine/save")
|
|
|
+ local Audio = require("engine/audio")
|
|
|
+ local ModLoader = require("engine/mod_loader")
|
|
|
+ Save.setFS(self.filesystem)
|
|
|
+ Audio.setLove(self.audio)
|
|
|
+ ModLoader.setFS(self.filesystem)
|
|
|
+
|
|
|
+ -- 3. 创建 Systems 层对象
|
|
|
+ self.worldSystem = WorldSystem:new(os.time())
|
|
|
+ self.farmSystem = FarmSystem:new()
|
|
|
+ -- ...
|
|
|
+
|
|
|
+ -- 4. 创建 Render 层对象,注入接口(含 IGraphics)
|
|
|
+ self.worldRenderer = WorldRenderer:new(self.worldSystem, self.graphics)
|
|
|
+ self.farmRenderer = FarmRenderer:new(self.farmSystem, self.graphics)
|
|
|
+ self.uiRenderer = UIRenderer:new(
|
|
|
+ self.farmSystem,
|
|
|
+ GameStateAdapter:new(self.gameState),
|
|
|
+ self.graphics
|
|
|
+ )
|
|
|
+
|
|
|
+ -- 5. 创建需要接口的 Systems 对象
|
|
|
+ self.playerSystem = PlayerSystem:new(
|
|
|
+ self.farmSystem, self.gameState, self.blockSystem, self.worldSystem
|
|
|
+ )
|
|
|
+ -- ...
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+**GameStateAdapter**(IGameStateQuery 适配器):
|
|
|
+
|
|
|
+由于 `systems/state.lua` 是纯数据表(非 classic 类),需要一个适配器将其包装为 `IGameStateQuery`:
|
|
|
+
|
|
|
+```lua
|
|
|
+-- [协调层] GameState 到 IGameStateQuery 的适配器
|
|
|
+local IGameStateQuery = require("engine/interface").IGameStateQuery
|
|
|
+
|
|
|
+local GameStateAdapter = IGameStateQuery:extend()
|
|
|
+
|
|
|
+function GameStateAdapter:new(gs)
|
|
|
+ self._gs = gs
|
|
|
+end
|
|
|
+
|
|
|
+function GameStateAdapter:getMoney() return self._gs.money end
|
|
|
+function GameStateAdapter:getLevel() return self._gs.level end
|
|
|
+-- ... 其余方法
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 数据模型
|
|
|
+
|
|
|
+### 接口继承层次
|
|
|
+
|
|
|
+```
|
|
|
+classic.Object
|
|
|
+ └── Interface_Base
|
|
|
+ ├── IGraphics
|
|
|
+ │ ├── LoveGraphics(Love适配层实现,唯一调用 love.graphics)
|
|
|
+ │ └── MockGraphics(测试用,不依赖 love)
|
|
|
+ ├── IFilesystem
|
|
|
+ │ ├── LoveFilesystem(Love适配层实现)
|
|
|
+ │ └── MockFilesystem(测试用)
|
|
|
+ ├── IAudio
|
|
|
+ │ └── LoveAudio(Love适配层实现)
|
|
|
+ ├── IWindow
|
|
|
+ │ └── LoveWindow(Love适配层实现)
|
|
|
+ ├── IWorldQuery
|
|
|
+ │ └── WorldSystem(实现)
|
|
|
+ ├── IFarmQuery
|
|
|
+ │ └── FarmSystem(实现)
|
|
|
+ └── IGameStateQuery
|
|
|
+ └── GameStateAdapter(适配器实现)
|
|
|
+
|
|
|
+classic.Object
|
|
|
+ ├── PlayerSystem(持有 IWorldQuery)
|
|
|
+ ├── Character(持有 IWorldQuery)
|
|
|
+ ├── Pet(持有 IWorldQuery)
|
|
|
+ ├── WorldRenderer(持有 IWorldQuery + IGraphics)
|
|
|
+ ├── FarmRenderer(持有 IFarmQuery + IGraphics)
|
|
|
+ ├── UIRenderer(持有 IFarmQuery + IGameStateQuery + IGraphics)
|
|
|
+ └── 其他 Renderer(持有 IGraphics)
|
|
|
+```
|
|
|
+
|
|
|
+### 目录结构与层归属
|
|
|
+
|
|
|
+```
|
|
|
+love_adapter/ ← Love 适配层(唯一调用 love.* 的目录)
|
|
|
+ graphics.lua -- [Love适配层] IGraphics 的 LÖVE2D 实现
|
|
|
+ filesystem.lua -- [Love适配层] IFilesystem 的 LÖVE2D 实现
|
|
|
+ audio.lua -- [Love适配层] IAudio 的 LÖVE2D 实现
|
|
|
+ window.lua -- [Love适配层] IWindow 的 LÖVE2D 实现
|
|
|
+engine/ ← Engine 层(不调用 love.*,通过接口访问适配层)
|
|
|
+ interface.lua -- [Engine层] 所有接口定义(含框架接口和业务接口)
|
|
|
+ camera.lua -- [Engine层] 摄像机(通过 setScreenSize 注入)
|
|
|
+ save.lua -- [Engine层] 存档(通过 setFS 注入 IFilesystem)
|
|
|
+ audio.lua -- [Engine层] 音频合成(通过 setLove 注入 IAudio)
|
|
|
+ ...
|
|
|
+data/ ← Data 层(纯静态表)
|
|
|
+systems/ ← Systems 层(不调用 love.*)
|
|
|
+render/ ← Render 层(通过 IGraphics 绘图,不调用 love.*)
|
|
|
+game.lua ← 协调层(创建适配层实例并注入)
|
|
|
+main.lua ← 协调层(LÖVE2D 入口,转发事件给 engine/engine.lua)
|
|
|
+```
|
|
|
+
|
|
|
+### 层归属注释格式
|
|
|
+
|
|
|
+每个文件第一行必须包含:
|
|
|
+
|
|
|
+| 目录 | 注释格式 | 示例 |
|
|
|
+|------|----------|------|
|
|
|
+| `love_adapter/` | `-- [Love适配层] 说明` | `-- [Love适配层] IGraphics 的 LÖVE2D 实现` |
|
|
|
+| `engine/` | `-- [Engine层] 说明` | `-- [Engine层] 事件总线,解耦模块间通信` |
|
|
|
+| `data/` | `-- [Data层] 说明` | `-- [Data层] 作物静态数据表` |
|
|
|
+| `systems/` | `-- [Systems层] 说明` | `-- [Systems层] 农场业务逻辑` |
|
|
|
+| `render/` | `-- [Render层] 说明` | `-- [Render层] 世界地图渲染` |
|
|
|
+| 根目录协调 | `-- [协调层] 说明` | `-- [协调层] 游戏主控制器,组装各层对象` |
|
|
|
+
|
|
|
+### 依赖检查规则(静态分析)
|
|
|
+
|
|
|
+```
|
|
|
+love_adapter/ 下文件 → 允许调用 love.*,不得 require 其他层(除 engine/interface)
|
|
|
+engine/ 下文件 → 不得包含 love. 字符串(通过适配层接口访问)
|
|
|
+engine/ 下文件 → 不得包含 require("systems/ 或 require("render/
|
|
|
+systems/ 下文件 → 不得包含 love. 字符串
|
|
|
+systems/ 下文件 → 不得包含 require("render/
|
|
|
+render/ 下文件 → 不得包含 love. 字符串(通过 IGraphics 接口调用)
|
|
|
+render/ 下文件 → 不得包含顶层 require("systems/
|
|
|
+data/ 下文件 → 不得包含任何 require(除 data/ 内部)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 正确性属性
|
|
|
+
|
|
|
+*属性是在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*
|
|
|
+
|
|
|
+### 属性 1:接口未实现方法抛出格式化错误
|
|
|
+
|
|
|
+*对于任意* 接口名称和方法名称,当通过未实现该方法的子类调用时,系统应抛出包含 `"接口名.方法名 未实现"` 格式的错误信息。
|
|
|
+
|
|
|
+**验证:需求 1.3**
|
|
|
+
|
|
|
+### 属性 2:IWorldQuery 接口方法完整性
|
|
|
+
|
|
|
+*对于* IWorldQuery 接口声明的每一个方法,当通过未实现的子类调用时,系统应抛出包含该方法名的错误(证明方法已被声明且有默认未实现行为)。
|
|
|
+
|
|
|
+**验证:需求 1.4**
|
|
|
+
|
|
|
+### 属性 3:IFarmQuery 接口方法完整性
|
|
|
+
|
|
|
+*对于* IFarmQuery 接口声明的每一个方法,当通过未实现的子类调用时,系统应抛出包含该方法名的错误。
|
|
|
+
|
|
|
+**验证:需求 1.5**
|
|
|
+
|
|
|
+### 属性 4:IGameStateQuery 接口方法完整性
|
|
|
+
|
|
|
+*对于* IGameStateQuery 接口声明的每一个方法,当通过未实现的子类调用时,系统应抛出包含该方法名的错误。
|
|
|
+
|
|
|
+**验证:需求 1.6**
|
|
|
+
|
|
|
+### 属性 5:所有 Lua 源文件包含层归属注释
|
|
|
+
|
|
|
+*对于* 项目中 `engine/`、`data/`、`systems/`、`render/` 目录下的任意 Lua 源文件,其第一行应匹配 `-- [*层] ` 格式的层归属注释。
|
|
|
+
|
|
|
+**验证:需求 2.1、2.2、2.3、2.4、2.5**
|
|
|
+
|
|
|
+### 属性 6:Systems 层不依赖 Render 层
|
|
|
+
|
|
|
+*对于* `systems/` 目录下的任意 Lua 源文件,其文件内容不应包含 `require("render/` 字符串。
|
|
|
+
|
|
|
+**验证:需求 3.1、3.6、9.3**
|
|
|
+
|
|
|
+### 属性 7:Render 层不直接 require Systems 层
|
|
|
+
|
|
|
+*对于* `render/` 目录下的任意 Lua 源文件,其文件内容不应包含顶层(文件加载时执行的)`require("systems/` 字符串。
|
|
|
+
|
|
|
+**验证:需求 4.1、4.5、9.4**
|
|
|
+
|
|
|
+### 属性 11:Render 层不直接调用 love API
|
|
|
+
|
|
|
+*对于* `render/` 目录下的任意 Lua 源文件,其文件内容不应包含 `love.` 字符串(所有绘图调用均通过 `IGraphics` 接口)。
|
|
|
+
|
|
|
+**验证:需求 9.4(扩展)**
|
|
|
+
|
|
|
+### 属性 12:Systems 层不直接调用 love API
|
|
|
+
|
|
|
+*对于* `systems/` 目录下的任意 Lua 源文件,其文件内容不应包含 `love.` 字符串。
|
|
|
+
|
|
|
+**验证:需求 9.3(扩展)**
|
|
|
+
|
|
|
+### 属性 8:通过接口访问与直接调用等价
|
|
|
+
|
|
|
+*对于任意* 有效的地图坐标 `(row, col)`,通过 `IWorldQuery` 接口调用 `WorldSystem` 的查询方法,应与直接调用 `WorldSystem` 对应方法返回相同的结果。
|
|
|
+
|
|
|
+**验证:需求 8.4**
|
|
|
+
|
|
|
+### 属性 9:农场操作状态机转换正确性
|
|
|
+
|
|
|
+*对于任意* 有效的农场格子初始状态和操作序列(耕地→播种→浇水→收获),FarmSystem 的状态转换应满足:`EMPTY →(till)→ TILLED →(plant)→ SEEDED →(harvest when ready)→ EMPTY`,且每步操作返回值与状态变更一致。
|
|
|
+
|
|
|
+**验证:需求 8.3**
|
|
|
+
|
|
|
+### 属性 10:Engine 层不依赖上层模块且不直接调用 love
|
|
|
+
|
|
|
+*对于* `engine/` 目录下的任意 Lua 源文件,其文件内容不应包含 `require("systems/`、`require("render/` 或 `require("data/` 字符串;同时不应包含 `love.` 字符串(Engine 层通过适配层接口访问框架能力)。
|
|
|
+
|
|
|
+**验证:需求 9.1**
|
|
|
+
|
|
|
+### 属性 13:Love 适配层是唯一调用 love 的层
|
|
|
+
|
|
|
+*对于* `engine/`、`systems/`、`render/`、`data/` 目录下的任意 Lua 源文件,其文件内容不应包含 `love.` 字符串。只有 `love_adapter/` 目录下的文件允许包含 `love.` 字符串。
|
|
|
+
|
|
|
+**验证:love 隔离原则**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 错误处理
|
|
|
+
|
|
|
+### 接口未实现错误
|
|
|
+
|
|
|
+当调用未被子类覆盖的接口方法时,`Interface_Base` 的默认实现抛出 Lua error:
|
|
|
+
|
|
|
+```lua
|
|
|
+error("IWorldQuery.isPath 未实现")
|
|
|
+```
|
|
|
+
|
|
|
+调用方(Systems 层或 Render 层)不需要捕获此错误——它代表编程错误(忘记实现接口),应在开发阶段暴露。
|
|
|
+
|
|
|
+### 接口注入缺失
|
|
|
+
|
|
|
+若 `game.lua` 忘记注入接口实例,相关模块在首次调用接口方法时会因 `nil` 调用而报错。为提前发现此类问题,各模块构造函数应做断言:
|
|
|
+
|
|
|
+```lua
|
|
|
+function PlayerSystem:new(farmSystem, gameState, blockSystem, worldQuery)
|
|
|
+ assert(worldQuery, "PlayerSystem:new 需要 IWorldQuery 实例")
|
|
|
+ -- ...
|
|
|
+end
|
|
|
+```
|
|
|
+
|
|
|
+### 依赖方向违规检测
|
|
|
+
|
|
|
+在 CI/测试阶段,通过静态扫描脚本检测违规 `require`。违规时输出文件名和行号,阻止合并。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 测试策略
|
|
|
+
|
|
|
+### 双轨测试方法
|
|
|
+
|
|
|
+- **单元测试**:验证具体示例、边界条件、错误情况
|
|
|
+- **属性测试**:验证跨所有输入的通用属性
|
|
|
+
|
|
|
+使用 [busted](https://lunarmodules.github.io/busted/) 框架(已配置于 `.busted`),属性测试使用 [lua-quickcheck](https://github.com/mcmtroffaes/lua-quickcheck) 或手动生成随机输入。
|
|
|
+
|
|
|
+### 属性测试配置
|
|
|
+
|
|
|
+每个属性测试最少运行 **100 次迭代**,标签格式:
|
|
|
+
|
|
|
+```lua
|
|
|
+-- Feature: layered-architecture-refactor, Property N: 属性描述
|
|
|
+```
|
|
|
+
|
|
|
+### 测试文件组织
|
|
|
+
|
|
|
+```
|
|
|
+spec/
|
|
|
+ helpers/
|
|
|
+ love_mock.lua -- 已有 LÖVE2D mock(仅用于 love_adapter 层测试)
|
|
|
+ interface_helpers.lua -- 接口测试辅助函数(新增)
|
|
|
+ mock_graphics.lua -- MockGraphics 实现(新增,供 Engine/Render 层测试使用)
|
|
|
+ mock_filesystem.lua -- MockFilesystem 实现(新增,供 Engine 层测试使用)
|
|
|
+ layered_arch/
|
|
|
+ property_interface_spec.lua -- 属性 1-4:接口正确性(含框架接口)
|
|
|
+ property_layer_deps_spec.lua -- 属性 5-7、10-13:层依赖约束(含 love 隔离检查)
|
|
|
+ property_equivalence_spec.lua -- 属性 8:接口等价性
|
|
|
+ property_farm_spec.lua -- 属性 9:农场状态机
|
|
|
+```
|
|
|
+
|
|
|
+### 属性测试示例
|
|
|
+
|
|
|
+**属性 1 — 接口未实现方法抛出格式化错误**:
|
|
|
+
|
|
|
+```lua
|
|
|
+-- Feature: layered-architecture-refactor, Property 1: 接口未实现方法抛出格式化错误
|
|
|
+it("对任意方法名,未实现时调用应抛出包含方法名的错误", function()
|
|
|
+ local Interface_Base = require("engine/interface").Interface_Base
|
|
|
+ -- 生成随机方法名列表
|
|
|
+ local methodNames = { "foo", "bar", "doSomething", "query_x", "get123" }
|
|
|
+ for _ = 1, 100 do
|
|
|
+ local name = methodNames[math.random(#methodNames)] .. tostring(math.random(1000))
|
|
|
+ local TestIface = Interface_Base:extend()
|
|
|
+ TestIface[name] = function() error("TestIface." .. name .. " 未实现") end
|
|
|
+ local impl = TestIface:extend()
|
|
|
+ local obj = impl:new()
|
|
|
+ local ok, err = pcall(function() obj[name](obj) end)
|
|
|
+ assert.is_false(ok)
|
|
|
+ assert.truthy(err:find(name))
|
|
|
+ end
|
|
|
+end)
|
|
|
+```
|
|
|
+
|
|
|
+**属性 8 — 接口访问与直接调用等价**:
|
|
|
+
|
|
|
+```lua
|
|
|
+-- Feature: layered-architecture-refactor, Property 8: 通过接口访问与直接调用等价
|
|
|
+it("对任意坐标,IWorldQuery 接口调用与 WorldSystem 直接调用结果相同", function()
|
|
|
+ local WorldSystem = require("systems/world_system")
|
|
|
+ WorldSystem.init(42) -- 固定 seed
|
|
|
+ for _ = 1, 100 do
|
|
|
+ local row = math.random(1, 100)
|
|
|
+ local col = math.random(1, 100)
|
|
|
+ assert.equal(WorldSystem:isPath(row, col), WorldSystem.isPath(row, col))
|
|
|
+ assert.equal(WorldSystem:isCollision(row, col), WorldSystem.isCollision(row, col))
|
|
|
+ assert.equal(WorldSystem:isObstacle(row, col), WorldSystem.isObstacle(row, col))
|
|
|
+ end
|
|
|
+end)
|
|
|
+```
|
|
|
+
|
|
|
+**属性 9 — 农场状态机转换**:
|
|
|
+
|
|
|
+```lua
|
|
|
+-- Feature: layered-architecture-refactor, Property 9: 农场操作状态机转换正确性
|
|
|
+it("对任意坐标,农场状态机转换 EMPTY→TILLED→SEEDED→READY 正确", function()
|
|
|
+ local FarmSystem = require("systems/farm")
|
|
|
+ for _ = 1, 100 do
|
|
|
+ local farm = FarmSystem:new()
|
|
|
+ local row, col = math.random(1, 100), math.random(1, 100)
|
|
|
+ -- 先开垦
|
|
|
+ farm:clearGrass(row, col)
|
|
|
+ assert.equal("empty", farm:getTile(row, col).state)
|
|
|
+ -- 耕地
|
|
|
+ local ok = farm:till(row, col)
|
|
|
+ assert.is_true(ok)
|
|
|
+ assert.equal("tilled", farm:getTile(row, col).state)
|
|
|
+ -- 播种
|
|
|
+ ok = farm:plant(row, col, "carrot", nil)
|
|
|
+ assert.is_true(ok)
|
|
|
+ assert.equal("seeded", farm:getTile(row, col).state)
|
|
|
+ end
|
|
|
+end)
|
|
|
+```
|
|
|
+
|
|
|
+### 单元测试覆盖
|
|
|
+
|
|
|
+- `engine/interface.lua`:接口定义、错误格式(含 IGraphics、IFilesystem、IAudio、IWindow)
|
|
|
+- `love_adapter/graphics.lua`:LoveGraphics 委托行为(需 love mock)
|
|
|
+- `systems/world_system.lua`:地图查询方法
|
|
|
+- `systems/farm.lua`:农场操作、状态转换
|
|
|
+- `systems/player.lua`:通过 MockGraphics + mock IWorldQuery 验证注入
|
|
|
+- `render/ui_renderer.lua`:通过 MockGraphics + mock IFarmQuery/IGameStateQuery 验证注入,不依赖 love
|
|
|
+- 层依赖静态检查:扫描所有 `.lua` 文件的 `require` 调用和 `love.` 字符串,验证只有 `love_adapter/` 下文件包含 `love.`
|
|
|
+
|
|
|
+### 回归测试
|
|
|
+
|
|
|
+重构完成后,运行现有 `spec/` 下所有测试套件,确保全部通过:
|
|
|
+
|
|
|
+```bash
|
|
|
+busted --run
|
|
|
+```
|