更新饥荒
This commit is contained in:
@@ -375,9 +375,9 @@ console.log('== 新 mod 行为级验证 ==')
|
||||
{
|
||||
const g = makeG()
|
||||
const p = { san: 50 }
|
||||
const well = { type: 'well', cd: 0, id: 1 }
|
||||
const well = { type: 'well', mx: { cd: 0 }, id: 1 }
|
||||
rt.entKinds.well.interact(p, well, g)
|
||||
ok(p.san === 55 && well.cd === 60, `打水 san+5 冷却 60s (${p.san})`)
|
||||
ok(p.san === 55 && well.mx.cd === 60, `打水 san+5 冷却 60s (${p.san})`)
|
||||
rt.entKinds.well.interact(p, well, g)
|
||||
ok(p.san === 55, '冷却中不再加理智')
|
||||
}
|
||||
@@ -409,7 +409,7 @@ console.log('== 新 mod 行为级验证 ==')
|
||||
const g = makeG({ grant: [['husbshears', 1]], day: 3 })
|
||||
const m = { kind: 'sheep', id: 1 }
|
||||
rt.mobKinds.sheep.interact({}, m, g)
|
||||
ok(g.inv.wool === 2 && m.shearDay === 3, `剪毛得羊毛2 (wool=${g.inv.wool})`)
|
||||
ok(g.inv.wool === 2 && m.state === 'sheared:3', `剪毛得羊毛2 且冷却记在 m.state (wool=${g.inv.wool}, state=${m.state})`)
|
||||
g.received.length = 0
|
||||
rt.mobKinds.sheep.interact({}, m, g)
|
||||
ok(g.inv.wool === 2 && g.received.length === 0, '同日重复剪毛不再产出')
|
||||
@@ -453,15 +453,15 @@ console.log('== 新 mod 行为级验证 ==')
|
||||
const g = makeG({ grant: [['ingot', 2]] })
|
||||
rt.entKinds.factory.interact({}, { x: 0, y: 0, id: 1 }, g)
|
||||
ok(!g.inv.gear && g.inv.ingot === 2, '无供能蒸汽机不出齿轮')
|
||||
const eng = { type: 'steamengine', on: true, x: 0, y: 0, id: 9 }
|
||||
const eng = { type: 'steamengine', mx: { fuel: 2 }, x: 0, y: 0, id: 9 }
|
||||
const g2 = makeG({ entities: [eng], grant: [['ingot', 2]] })
|
||||
rt.entKinds.factory.interact({}, { x: 0, y: 0, id: 2 }, g2)
|
||||
ok(g2.inv.gear === 1 && g2.inv.ingot === 0, '有蒸汽且 ingot 够→齿轮×1')
|
||||
const engOff = { type: 'steamengine', on: false, x: 0, y: 0, id: 10 }
|
||||
const engOff = { type: 'steamengine', mx: { fuel: 0 }, x: 0, y: 0, id: 10 }
|
||||
const g3 = makeG({ entities: [engOff], grant: [['ingot', 2]] })
|
||||
rt.entKinds.factory.interact({}, { x: 0, y: 0, id: 3 }, g3)
|
||||
ok(!g3.inv.gear, '蒸汽机停转不出齿轮')
|
||||
const farEng = { type: 'steamengine', on: true, x: 999, y: 0, id: 11 }
|
||||
const farEng = { type: 'steamengine', mx: { fuel: 2 }, x: 999, y: 0, id: 11 }
|
||||
const g4 = makeG({ entities: [farEng], grant: [['ingot', 2]] })
|
||||
rt.entKinds.factory.interact({}, { x: 0, y: 0, id: 4 }, g4)
|
||||
ok(!g4.inv.gear, '蒸汽机超出 300px 不出齿轮')
|
||||
@@ -476,7 +476,7 @@ console.log('== 新 mod 行为级验证 ==')
|
||||
}))
|
||||
ok(sum(cap1, 'rock') === 2 && sum(cap1, 'flint') === 1, `无蒸汽矿机 rock×2 flint×1 (${sum(cap1, 'rock')}/${sum(cap1, 'flint')})`)
|
||||
const cap2 = []
|
||||
const eng = { type: 'steamengine', on: true, fuel: 5, x: 0, y: 0, id: 9 }
|
||||
const eng = { type: 'steamengine', mx: { fuel: 5 }, x: 0, y: 0, id: 9 }
|
||||
hookFn(rt, 'onNewDay', 'industry')(makeG({
|
||||
entities: [{ type: 'miner', x: 0, y: 0, id: 2 }, eng],
|
||||
extra: { dropLoot: (x, y, c, n) => cap2.push([c, n]) },
|
||||
@@ -486,20 +486,22 @@ console.log('== 新 mod 行为级验证 ==')
|
||||
// --- 工业蒸汽机 ---
|
||||
{
|
||||
const g = makeG({ grant: [['log', 1]] })
|
||||
const eng = { type: 'steamengine', fuel: 0, on: false, id: 1 }
|
||||
const eng = { type: 'steamengine', mx: { fuel: 0 }, id: 1 }
|
||||
rt.entKinds.steamengine.interact({}, eng, g)
|
||||
ok(eng.fuel === 3 && g.inv.log === 0, `投 log×1 燃料+3 (fuel=${eng.fuel})`)
|
||||
ok(eng.mx.fuel === 3 && g.inv.log === 0, `投 log×1 燃料+3 且存进 mx (fuel=${eng.mx.fuel})`)
|
||||
const gNo = makeG()
|
||||
rt.entKinds.steamengine.interact({}, { type: 'steamengine', fuel: 0, id: 2 }, gNo)
|
||||
rt.entKinds.steamengine.interact({}, { type: 'steamengine', mx: { fuel: 0 }, id: 2 }, gNo)
|
||||
ok(gNo.inv.log === undefined, '无燃料不消耗')
|
||||
hookFn(rt, 'onNewDay', 'industry')(makeG({ entities: [eng] }))
|
||||
ok(eng.fuel === 2, `蒸汽机每日耗 1 (fuel=${eng.fuel})`)
|
||||
const eng2 = { type: 'steamengine', fuel: 1, on: true, id: 3 }
|
||||
ok(eng.mx.fuel === 2, `蒸汽机每日耗 1 (fuel=${eng.mx.fuel})`)
|
||||
const eng2 = { type: 'steamengine', mx: { fuel: 1 }, id: 3 }
|
||||
hookFn(rt, 'onNewDay', 'industry')(makeG({ entities: [eng2] }))
|
||||
ok(eng2.fuel === 0 && eng2.on === false, `燃料归零停转 (fuel=${eng2.fuel}, on=${eng2.on})`)
|
||||
eng.fuel = 2; eng.on = false
|
||||
ok(eng2.mx.fuel === 0 && eng2.on === false, `燃料归零停转 (fuel=${eng2.mx.fuel}, on=${eng2.on})`)
|
||||
eng.mx.fuel = 2; eng.on = false
|
||||
rt.entKinds.steamengine.update(eng, 0.1, makeG())
|
||||
ok(eng.on === true, 'fuel>0 时 update 置 on=true')
|
||||
// 燃料必须落在 e.mx:核心只序列化 e.mx,落在 e.fuel 会存档/联机丢失
|
||||
ok(eng.fuel === undefined, '燃料不再占用 e.fuel(避免存档丢失)')
|
||||
}
|
||||
|
||||
console.log('== 跨 mod 独立性 ==')
|
||||
@@ -514,7 +516,7 @@ console.log('== 跨 mod 独立性 ==')
|
||||
try {
|
||||
hookFn(rtInd, 'onNewDay', 'industry')(makeG({
|
||||
items: rtInd.items,
|
||||
entities: [{ type: 'miner', x: 0, y: 0, id: 1 }, { type: 'steamengine', on: true, fuel: 2, x: 0, y: 0, id: 2 }],
|
||||
entities: [{ type: 'miner', x: 0, y: 0, id: 1 }, { type: 'steamengine', mx: { fuel: 2 }, x: 0, y: 0, id: 2 }],
|
||||
}))
|
||||
} catch (e) { err2 = e.message }
|
||||
ok(err2 === null, `单激活 industry onNewDay 不崩 (${err2 || 'ok'})`)
|
||||
@@ -822,9 +824,14 @@ const campHooks = (name) => hookFn(rt, name, 'camp')
|
||||
ok(hook(gIn, { kind: 'spider' }, { x: 419, y: 0 }) === false, '边界 419 内 → false')
|
||||
ok(hook(gIn, { kind: 'spider' }, { x: 421, y: 0 }) !== false, '边界 421 外 → 不否决')
|
||||
ok(hook(gIn, { kind: 'beefalo' }, { x: 10, y: 0 }) !== false, '非敌对生物不处理')
|
||||
// 没有营地房屋 = 没有安全区(不应凭空在世界中心生成无敌区)
|
||||
const gNo = mkCG({ worldW: 1000, worldH: 800 })
|
||||
ok(hook(gNo, { kind: 'spider' }, { x: 500, y: 400 }) === false, '无房屋时以世界中心判定 → false')
|
||||
ok(hook(gNo, { kind: 'spider' }, { x: 920, y: 400 }) !== false, '无房屋时中心外 → 不否决')
|
||||
ok(hook(gNo, { kind: 'spider' }, { x: 500, y: 400 }) !== false, '无房屋时不否决(不在世界中心凭空生成安全区)')
|
||||
// 自建的分基地同样生效:任意一座房屋的 CAMP_R 内都算营地
|
||||
const gTwo = mkCG({ entities: [{ type: 'camphouse', x: 0, y: 0, id: 1 }, { type: 'camphouse', x: 5000, y: 0, id: 2 }] })
|
||||
ok(hook(gTwo, { kind: 'spider' }, { x: 300, y: 0 }) === false, '一号房屋内 → false')
|
||||
ok(hook(gTwo, { kind: 'spider' }, { x: 5300, y: 0 }) === false, '二号房屋(自建分基地)内 → 同样 false')
|
||||
ok(hook(gTwo, { kind: 'spider' }, { x: 2500, y: 0 }) !== false, '两座房屋之间 → 不否决')
|
||||
}
|
||||
{
|
||||
const g = mkCG({ entities: [{ type: 'camphouse', x: 0, y: 0, id: 1 }], monsters: [] })
|
||||
@@ -1243,5 +1250,48 @@ console.log('== F. 边界与错误路径 ==')
|
||||
ok(err === null, `守卫/农民/牧工 draw 假 ctx 不崩 (${err || 'ok'})`)
|
||||
}
|
||||
|
||||
console.log('== 核心钩子契约(防回归)==')
|
||||
{
|
||||
const fs = await import('node:fs')
|
||||
const url = (p) => new URL(p, import.meta.url)
|
||||
const vue = fs.readFileSync(url('./src/games/components/StarveGame.vue'), 'utf8')
|
||||
const mobsSrc = fs.readFileSync(url('./src/games/starve/mobs.js'), 'utf8')
|
||||
|
||||
// 1) g.allows 必须把 gCtx 作为钩子首参,否则 mod 侧 (g, ...) 签名整体错位
|
||||
// 真实事故:onMobVictim 实际收到 (怪, 玩家, undefined),camp.js 里 g.mon 变成 undefined 而崩
|
||||
const ad = vue.match(/allows:\s*\(name,\s*\.\.\.args\)\s*=>\s*callAllows\([^\n]*/)
|
||||
ok(!!ad && ad[0].includes('gCtx'), `g.allows 前置 gCtx (${ad ? ad[0].trim() : '未找到 allows 定义'})`)
|
||||
|
||||
// 2) 所有 core→mod 钩子调用都必须以 gCtx 作为第一个可变参
|
||||
const bad = []
|
||||
let hookCalls = 0
|
||||
for (const m of vue.matchAll(/call(?:Hook|Handled|Allows)\(\s*modRt\s*,\s*'([^']+)'\s*,\s*([^)]*)/g)) {
|
||||
hookCalls++
|
||||
if (!m[2].trim().startsWith('gCtx')) bad.push(m[1])
|
||||
}
|
||||
ok(bad.length === 0, `核心钩子全部传 gCtx (${bad.length ? '缺: ' + bad.join(',') : hookCalls + ' 处'})`)
|
||||
|
||||
// 3) mod 侧每个钩子的首参必须是 g(位置契约一致)
|
||||
const modFiles = fs.readdirSync(url('./src/games/starve/mods'))
|
||||
.filter((f) => f.endsWith('.js') && f !== 'index.js' && f !== 'registry.js')
|
||||
const badSig = []
|
||||
let hookDefs = 0
|
||||
for (const f of modFiles) {
|
||||
const src = fs.readFileSync(url('./src/games/starve/mods/' + f), 'utf8')
|
||||
for (const m of src.matchAll(/^\s{4}(on[A-Z][A-Za-z]*)\(([^)]*)\)/gm)) {
|
||||
hookDefs++
|
||||
const first = (m[2].split(',')[0] || '').trim()
|
||||
if (first !== 'g') badSig.push(`${f}:${m[1]}(${m[2]})`)
|
||||
}
|
||||
}
|
||||
ok(badSig.length === 0, `mod 钩子首参都是 g (${badSig.length ? badSig.join('; ') : hookDefs + ' 个钩子 / ' + modFiles.length + ' 个 mod'})`)
|
||||
|
||||
// 4) 否决钩子的调用点必须真实存在,否则 onMobVictim 形同虚设
|
||||
ok(/g\.allows\(/.test(mobsSrc), 'mobs.js 里有 g.allows 调用点')
|
||||
// 5) 实体/生物回调签名(核心侧)
|
||||
ok(/modEnt\.interact\(p, e, gCtx\)|mk\.interact\(p, ref, gCtx\)/.test(vue), '核心以 (p, e, g) 调实体 interact')
|
||||
ok(/mk\.update\(m, dt, gCtx\)|modEnt\.update\?\.\(e, dt, gCtx\)/.test(vue), '核心以 (m/e, dt, g) 调 update')
|
||||
}
|
||||
|
||||
console.log(fails ? `\n${fails} 项失败` : '\n全部通过')
|
||||
process.exit(fails ? 1 : 0)
|
||||
|
||||
Reference in New Issue
Block a user