Files
nl-game/_campsmoke.mjs
2026-09-24 10:24:51 +08:00

203 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 临时冒烟:验证 camp.js / wideworld.js 运行时行为(不加入正式测试)
import camp from './src/games/starve/mods/camp.js'
import wide from './src/games/starve/mods/wideworld.js'
let fails = 0
const ok = (c, m) => { if (c) console.log(' PASS', m); else { fails++; console.error(' FAIL', m) } }
const makeCtx = () => new Proxy({}, { get: (t, k) => (k in t ? t[k] : () => {}), set: (t, k, v) => { t[k] = v; return true } })
const items = {
log: { name: '木料', stack: 20 },
rock: { name: '石头', stack: 20 },
spear: { name: '长矛', stack: 1, equip: 'hand' },
}
function makeG(opts = {}) {
const world = { entities: opts.entities || [], monsters: opts.monsters || [], fires: [] }
const p = opts.player || { x: 1200, y: 1000, slots: [], pack: null, hp: 100, san: 100, dir: 1, dead: false }
const count = (code) => (p.slots || []).reduce((n, s) => n + (s && s.code === code ? s.n : 0), 0)
+ (p.pack || []).reduce((n, s) => n + (s && s.code === code ? s.n : 0), 0)
const g = {
get players() { return [p] },
get monsters() { return world.monsters },
get entities() { return world.entities },
get fires() { return world.fires },
mon: {
guard: { hp: 9999, speed: 150, dmg: 60, score: 0, friendly: true, name: '守卫' },
hound: { hp: 150, speed: 195, dmg: 20, score: 20, name: '猎犬', hostile: true },
},
items, tune: { stackMax: 20 },
worldW: 2400, worldH: 2000, day: 1, season: 'autumn', phase: 'day', time: 5,
isLand: () => true,
litAt: () => true,
findLand: (x, y) => ({ x, y }),
countItem: (pp, c) => count(c),
hasCost: (pp, cost) => Object.entries(cost).every(([c, n]) => count(c) >= n),
payCost: (pp, cost) => {
if (!Object.entries(cost).every(([c, n]) => count(c) >= n)) return false
Object.entries(cost).forEach(([c, need]) => {
let n = need
;[p.slots, p.pack].filter(Boolean).forEach((area) => {
for (let i = 0; i < area.length && n > 0; i++) {
const s = area[i]
if (s && s.code === c) { const t = Math.min(n, s.n); s.n -= t; n -= t; if (s.n <= 0) area[i] = null }
}
})
})
return true
},
addItem: (pp, c, n) => {
for (let i = 0; i < p.slots.length; i++) if (!p.slots[i]) { p.slots[i] = { code: c, n }; return }
p.slots.push({ code: c, n })
},
consumeAt: () => {},
me: () => p,
isHost: () => true,
rand: (a, b) => a,
addEntity: (type, x, y, extra) => { const e = { id: world.entities.length + 1, type, x, y, ...(extra || {}) }; world.entities.push(e); return e },
removeEntity: () => {},
markDirty: () => {},
spawnMob: (kind, x, y) => { const m = { id: world.monsters.length + 1, kind, x, y, hp: 100, cd: 0 }; world.monsters.push(m); return m },
removeMob: () => {},
killMob: () => {},
damageMob: (m, d) => { m.hp -= d },
moveToward: (m, x, y, sp, dt) => { const dd = Math.hypot(x - m.x, y - m.y) || 1; m.x += (x - m.x) / dd * sp * dt; m.y += (y - m.y) / dd * sp * dt },
smashStructure: () => {},
addFire: (x, y, o) => { const f = { fid: world.fires.length + 1, x, y, ...(o || {}) }; world.fires.push(f); return f },
addScore: () => {}, ptip: () => {}, swing: () => {}, tip: () => {}, shake: () => {}, invalidate: () => {},
...(opts.extra || {}),
}
g._player = p
g._world = world
return g
}
console.log('== wideworld ==')
ok(wide.id === 'wideworld' && wide.flags.worldScale === 1.5, `worldScale=${wide.flags && wide.flags.worldScale}`)
ok(wide.factor === 0.9 && wide.tag === 'rule', 'factor/tag 正确')
console.log('== camp 结构 ==')
ok(camp.recipes.every((r) => camp.icons[r.code]), '每个配方都有图标')
ok(camp.recipes.filter((r) => r.build).every((r) => camp.ents[r.code]), '每个 build 配方都有同名实体')
ok(camp.tabs[0].code === 'camp' && !!camp.icons[camp.tabs[0].icon], '页签图标存在')
const uniq = (arr) => new Set(arr).size === arr.length
ok(
uniq(camp.recipes.map((r) => r.code)) && uniq(Object.keys(camp.ents)) && uniq(Object.keys(camp.mobs)) && uniq(Object.keys(camp.icons)),
'各自命名空间内 code 唯一'
)
console.log('== onWorldGen ==')
{
const g = makeG()
camp.hooks.onWorldGen(g)
const house = g._world.entities.find((e) => e.type === 'camphouse')
const guard = g._world.monsters.find((m) => m.kind === 'guard')
ok(!!house && !!guard && g._world.fires.some((f) => f.eternal), '房屋/守卫/长明篝火就位')
const pd = Math.hypot(house.x - g._player.x, house.y - g._player.y)
ok(pd > 30, `房屋与出生点错开 (d=${pd.toFixed(0)})`)
}
console.log('== 房屋休息 ==')
{
const g = makeG()
const house = { id: 1, type: 'camphouse', mx: { restDay: 0 } }
camp.ents.camphouse.init(house)
const p = { san: 100, hp: 100 }
camp.ents.camphouse.interact(p, house, g)
ok(p.san === 130 && p.hp === 120 && house.mx.restDay === g.day, `休息 san+30 hp+20 (${p.san}/${p.hp})`)
camp.ents.camphouse.interact(p, house, g)
ok(p.san === 130 && p.hp === 120, '同日重复休息不再回复')
}
console.log('== 营地仓储 ==')
{
const p = { x: 1300, y: 1000, slots: [{ code: 'log', n: 5 }, { code: 'spear', n: 1 }, null], pack: null }
const g = makeG({ player: p })
const store = g.addEntity('campstore', 1300, 1000)
camp.ents.campstore.init(store)
camp.ents.campstore.interact(p, store, g)
const ctx = makeCtx()
const env = { camX: 0, camY: 0, W: 900, H: 620, time: 1 }
camp.hooks.onDrawUI(g, ctx, env)
// 点击某行“存入”按钮(复刻面板坐标:dep.x=572, sy≈198.5+30i)
let deposited = false
for (let i = 0; i < 8 && !deposited; i++) {
// 行内按钮 x 位置:右缘附近的“存入”钮
const sx = 900 / 2 + 470 / 2 - 10 - 4 - 60 - 6 - 33 // 近似“存入”按钮中心
const sy = Math.round((620 - (40 + 8 * 30 + 42 + 10)) / 2) + 40 + i * 30 + 14
const handled = camp.hooks.onClick(g, sx, sy)
if (handled === true) {
const box0 = store.mx.boxes[i]
if (Object.keys(box0.items).length) deposited = true
}
}
const anyBox = store.mx.boxes.find((b) => Object.keys(b.items).length)
ok(!!anyBox && anyBox.items.log === 5, `存入 log×5 (${JSON.stringify(store.mx.boxes.map((b) => b.items))})`)
ok(p.slots.find((s) => s && s.code === 'spear'), '工具(长矛)不被存入')
// 取出全部
const p2 = { x: 1300, y: 1000, slots: [], pack: null }
const g2 = makeG({ player: p2 })
g2._world.entities.push(store)
camp.hooks.onDrawUI(g2, ctx, env)
// 找出有内容的箱子行按钮
const idx = store.mx.boxes.findIndex((b) => Object.keys(b.items).length)
const sx = 900 / 2 + 470 / 2 - 10 - 4 - 30 // “取出”按钮中心
const sy = Math.round((620 - (40 + 8 * 30 + 42 + 10)) / 2) + 40 + idx * 30 + 14
const h = camp.hooks.onClick(g2, sx, sy)
ok(h === true && (p2.slots.some((s) => s && s.code === 'log')), '取出物资到背包')
// 面板外点击 → 关闭面板并吃掉
const outside = camp.hooks.onClick(g2, -9999, -9999)
ok(outside === true, '面板外点击返回 true(关闭)')
const noPanel = camp.hooks.onClick(g2, -9999, -9999)
ok(noPanel === undefined, '无面板时返回 undefined(不干扰)')
}
console.log('== 领地:敌对生物被推离 ==')
{
const g = makeG()
camp.hooks.onWorldGen(g)
const house = g._world.entities.find((e) => e.type === 'camphouse')
const hound = g.spawnMob('hound', house.x + 40, house.y + 20) // 营地内
camp.hooks.onTick(g)
const d = Math.hypot(hound.x - house.x, hound.y - house.y)
ok(d >= 420, `营地内猎犬被推到边界外 (d=${d.toFixed(0)})`)
}
console.log('== 守卫拦截 ==')
{
const g = makeG()
camp.hooks.onWorldGen(g)
const house = g._world.entities.find((e) => e.type === 'camphouse')
const guard = g._world.monsters.find((m) => m.kind === 'guard')
const hound = g.spawnMob('hound', house.x + 200, house.y + 60)
const before = Math.hypot(hound.x - guard.x, hound.y - guard.y)
for (let i = 0; i < 40; i++) { hound.x = house.x + 200; camp.mobs.guard.update(guard, 0.05, g) }
const after = Math.hypot(hound.x - guard.x, hound.y - guard.y)
ok(guard.hp === 1e9, '守卫每帧回满血(无限血)')
ok(after < before, `守卫向敌人靠拢 (${before.toFixed(0)}→${after.toFixed(0)})`)
}
console.log('== 绘制不抛异常 ==')
{
const g = makeG()
camp.hooks.onWorldGen(g)
const house = g._world.entities.find((e) => e.type === 'camphouse')
const store = g.addEntity('campstore', house.x + 40, house.y)
const ctx = makeCtx()
let err = null
try {
camp.ents.camphouse.draw(ctx, house, { time: 3 })
camp.ents.campstore.draw(ctx, store, { time: 3 })
camp.mobs.guard.draw(ctx, { dir: 1, moving: true, bob: 0 }, 3)
camp.hooks.onDraw(g, ctx, { camX: 100, camY: 60, W: 800, H: 600, time: 3 })
camp.ents.campstore.interact(g._player, store, g)
camp.hooks.onDrawUI(g, ctx, { camX: 100, camY: 60, W: 800, H: 600, time: 3 })
camp.icons.camphouse(ctx)
camp.icons.campstore(ctx)
} catch (e) { err = e.message }
ok(err === null, `绘制/UI 无异常 (${err || 'ok'})`)
}
console.log(`\n${fails} 项失败`)
process.exit(fails ? 1 : 0)