Files
nl-game/test-starve.mjs
李琦 cd1ab90013 初始化
需要注意的是饥荒不够完善:动作、手持工具、细节交互、系统逻辑
2026-08-15 07:24:34 +08:00

182 lines
9.9 KiB
JavaScript
Raw Permalink 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.
// 临时冒烟测试:纯逻辑模块(不触 DOM——世界生成 / 季节 / 烹饪锅 / 数据表一致性 / mod 注册表
import { genIsland, planEntities, isLand, biomeAt, WORLD, GW, GH, findLandNear } from './src/games/starve/world.js'
import { seasonOf, daySplit, phaseOf, ambientTemp, rollWeather } from './src/games/starve/climate.js'
import { cookPot, RECIPES, ITEMS, MON, MOB_DROPS, RES, SEG_SPLIT, SEASONS } from './src/games/starve/data.js'
import { activateMods, modMeta, callAllows, callHandled } from './src/games/starve/mods/registry.js'
import { MODS } from './src/games/starve/mods/index.js'
let fails = 0
function ok(cond, msg) {
if (cond) console.log(' PASS', msg)
else { fails++; console.error(' FAIL', msg) }
}
console.log('== 世界生成 ==')
for (let run = 0; run < 5; run++) {
const { tiles, road } = genIsland()
const landN = tiles.reduce((n, v) => n + (v > 0 ? 1 : 0), 0)
ok(landN > 300 && landN < 1000, `岛屿陆地占比合理 (${landN}/1200)`)
ok(isLand(tiles, WORLD.w / 2, WORLD.h / 2), '出生点在陆地上')
const biomes = new Set(Array.from(tiles).filter(Boolean))
ok(biomes.size >= 5, `群系种类 >=5 (${[...biomes].join(',')})`)
const roadN = road.reduce((a, b) => a + b, 0)
ok(roadN > 10, `道路瓦片存在 (${roadN})`)
const plan = planEntities(tiles, road)
const count = (t) => plan.placed.filter((p) => p.type === t).length
ok(count('tree') > 20, `树数量 (${count('tree')})`)
ok(count('spidernest') >= 2, `蜘蛛巢 >=2 (${count('spidernest')})`)
ok(count('beehive') >= 2, `蜂巢 >=2 (${count('beehive')})`)
ok(count('goldrock') >= 4, `金矿 >=4 (${count('goldrock')})`)
ok(count('rabbithole') >= 5, `兔子洞 >=5 (${count('rabbithole')})`)
ok(count('tentacle') >= 4, `触手 >=4 (${count('tentacle')})`)
ok(count('pighouse') >= 3, `猪屋 >=3 (${count('pighouse')})`)
ok(plan.herds.length >= 1, `牛群锚点 (${plan.herds.length})`)
ok(plan.frogs.length >= 4, `青蛙出生点 (${plan.frogs.length})`)
// 所有放置点都在陆地
const offLand = plan.placed.filter((p) => !isLand(tiles, p.x, p.y)).length
ok(offLand === 0, `没有实体落在海里 (${offLand})`)
// 最小间距抽查
let overlaps = 0
for (let i = 0; i < plan.placed.length; i++) {
for (let j = i + 1; j < plan.placed.length; j++) {
const a = plan.placed[i]
const b = plan.placed[j]
if (Math.hypot(a.x - b.x, a.y - b.y) < 24) overlaps++
}
}
ok(overlaps === 0, `实体间距无重叠 (${overlaps})`)
const spot = findLandNear(tiles, WORLD.w / 2, WORLD.h / 2, 400, 600)
ok(isLand(tiles, spot.x, spot.y), 'findLandNear 返回陆地')
}
console.log('== 季节与昼夜 ==')
ok(seasonOf(1).code === 'autumn' && seasonOf(1).dayIn === 1, '第 1 天 = 秋 1')
ok(seasonOf(20).code === 'autumn', '第 20 天仍是秋')
ok(seasonOf(21).code === 'winter' && seasonOf(21).dayIn === 1, '第 21 天入冬(官方节奏)')
ok(seasonOf(36).code === 'spring', '第 36 天入春')
ok(seasonOf(56).code === 'summer', '第 56 天入夏')
ok(seasonOf(71).code === 'autumn' && seasonOf(71).dayIn === 1, '第 71 天新一年秋')
Object.entries(SEG_SPLIT).forEach(([code, [d, k, n]]) => {
ok(d + k + n === 16, `${code} 段数合计 16`)
})
const [duskAt, nightAt] = daySplit('winter')
ok(duskAt === 50 && nightAt === 90, `冬季昼 50s / 夜起 90s (${duskAt},${nightAt})`)
ok(phaseOf('winter', 100) === 'night', '冬季 100s 是夜')
ok(phaseOf('summer', 100) === 'day', '夏季 100s 是昼')
ok(ambientTemp('winter', 'night') < -15, `冬夜气温 ${ambientTemp('winter', 'night')}`)
ok(rollWeather('winter').type === 'snow', '冬季必然飘雪')
console.log('== 烹饪锅 ==')
ok(cookPot(['meat', 'meat', 'meat', 'berry']) === 'meatystew', '3 大肉 = 炖肉汤')
ok(cookPot(['morsel', 'berry', 'berry', 'berry']) === 'meatballs', '小肉+果 = 肉丸')
ok(cookPot(['meat', 'meat', 'honey', 'berry']) === 'honeyham', '2 肉 1 蜜 = 蜜汁火腿')
ok(cookPot(['morsel', 'honey', 'berry', 'berry']) === 'honeynuggets', '半肉 1 蜜 = 蜜糖块')
ok(cookPot(['honey', 'honey', 'honey', 'berry']) === 'taffy', '3 蜜 = 太妃糖')
ok(cookPot(['monstermeat', 'monstermeat', 'berry', 'berry']) === 'monsterlasagna', '2 怪物肉 = 千层饼')
ok(cookPot(['berry', 'berry', 'berry', 'berry']) === 'wetgoop', '全果 = 湿哒哒糊糊')
console.log('== 数据表一致性 ==')
RECIPES.forEach((r) => {
Object.keys(r.cost).forEach((c) => ok(!!ITEMS[c], `配方 ${r.code} 材料 ${c} 存在`))
const isStruct = ['campfire', 'firepit', 'sciencemachine', 'alchemyengine', 'crockpot'].includes(r.code)
ok(isStruct || !!ITEMS[r.code], `配方产物 ${r.code} 在物品表或建筑`)
})
Object.entries(MOB_DROPS).forEach(([kind, drops]) => {
ok(!!MON[kind], `掉落表生物 ${kind} 存在`)
drops.forEach(([p, code]) => ok(!!ITEMS[code], `${kind} 掉落 ${code} 存在`))
})
Object.entries(RES).forEach(([type, conf]) => {
if (conf.drops) Object.keys(conf.drops).forEach((c) => ok(!!ITEMS[c], `资源 ${type} 产出 ${c} 存在`))
})
ok(SEASONS.reduce((n, s) => n + s.days, 0) === 70, '一年 70 天')
console.log('== Mod 注册表 ==')
ok(MODS.length === 8, `内置 8 个 mod (${MODS.length})`)
ok(modMeta().every((m) => m.id && m.name && m.desc), '元数据齐全')
// 空激活 = 基表原样
const rt0 = activateMods([])
ok(rt0.scoreFactor === 1, '无 mod 系数 1')
ok(Object.keys(rt0.items).length === Object.keys(ITEMS).length, '无 mod 物品表与基表一致')
ok(rt0.recipes.length === RECIPES.length, '无 mod 配方表与基表一致')
// 全量激活
const allIds = MODS.map((m) => m.id)
const rt = activateMods(allIds)
ok(rt.ids.length === 8, '全量激活 8 个')
// 系数0.7(便利包) * 0.7(和平) * 1.3(噩梦) = 0.637 → 0.64;夹在 0.5~1.5
ok(rt.scoreFactor === 0.64, `全开系数 0.64 (${rt.scoreFactor})`)
ok(activateMods(['qol']).scoreFactor === 0.7, '便利包 0.7')
ok(activateMods(['nightmare']).scoreFactor === 1.3, '噩梦 1.3')
ok(activateMods(['farm', 'chest', 'chester', 'revive', 'magic']).scoreFactor === 1, '内容 mod 不影响系数')
ok(activateMods(['qol', 'peaceful']).scoreFactor === 0.5, '0.49 夹到下限 0.5')
// 配方一致性:材料与产物都能解析
rt.recipes.forEach((r) => {
Object.keys(r.cost).forEach((c) => ok(!!rt.items[c], `mod 配方 ${r.code} 材料 ${c} 存在`))
const isStruct = ['campfire', 'firepit', 'sciencemachine', 'alchemyengine', 'crockpot'].includes(r.code) || r.build
ok(isStruct ? !!rt.res[r.code] || !!rt.entKinds[r.code] || !rt.items[r.code] : !!rt.items[r.code], `mod 配方产物 ${r.code} 可解析`)
})
// 注册内容点名
ok(['farmplot', 'chest', 'revivestatue', 'flamefx', 'boomerang'].every((t) => rt.entKinds[t] && rt.res[t]), '自定义实体齐全')
ok(rt.mobKinds.chester && rt.mon.chester?.name === '切斯特', '切斯特生物注册')
ok(rt.items.hoe && rt.items.eyebone && rt.items.firestaff && rt.items.boomerang && rt.items.carrot, 'mod 物品齐全')
ok(rt.itemCodes.length === Object.keys(rt.items).length && rt.itemCodes.every((c, i) => rt.itemIdx[c] === i), '物品索引一致(联机打包用)')
// 调参与旗标
ok(rt.tune.stackMax === 99 && rt.tune.gatherMul === 0.5, '便利包调参生效')
ok(rt.tune.houndGapMul === 0.7 && rt.tune.houndExtra === 1, '噩梦猎犬调参生效')
ok(rt.flags.noFog === true, '无迷雾旗标')
// 噩梦倍率只改运行时表,不污染基表
ok(rt.mon.spider.hp === 150 && rt.mon.spider.dmg === 30, `噩梦蜘蛛 150/30 (${rt.mon.spider.hp}/${rt.mon.spider.dmg})`)
ok(MON.spider.hp === 100 && MON.spider.dmg === 20, '基表蜘蛛不受污染')
ok(activateMods(['farm']).mon.spider.hp === 100, '不开噩梦不缩放')
// 合并顺序确定性(主客机一致)
ok(JSON.stringify(activateMods([...allIds].reverse()).itemCodes) === JSON.stringify(rt.itemCodes), '激活顺序无关,物品索引确定')
// 钩子语义
ok(callAllows(rt, 'onSpawnWave', {}, 'hound') === false, '和平模式否决猎犬波')
ok(callAllows(activateMods(['farm']), 'onSpawnWave', {}, 'hound') === true, '无和平模式放行')
// 行为级:农田三阶段生长(纯函数拨表)
{
const plot = { state: 'grow0', mx: { seed: 'carrot', t: 0 }, id: 1 }
const g = { season: 'autumn', markDirty() {}, tip() {} }
const farm = rt.entKinds.farmplot
farm.update(plot, 108, g)
ok(plot.state === 'grow1', `农田 108s 二阶段 (${plot.state})`)
farm.update(plot, 107, g)
ok(plot.state === 'grow2', `农田 215s 三阶段 (${plot.state})`)
farm.update(plot, 200, g)
ok(plot.state === 'ready', `农田 320s 成熟 (${plot.state})`)
const winterPlot = { state: 'grow0', mx: { seed: 'berry', t: 0 }, id: 2 }
farm.update(winterPlot, 500, { ...g, season: 'winter' })
ok(winterPlot.state === 'grow0' && winterPlot.mx.t === 0, '冬季生长暂停')
}
// 行为级:宝箱开箱按战利品表发放
{
const got = []
const g = {
rand: (a, b) => a, addItem: (p, c, n) => got.push([c, n]), items: rt.items,
markDirty() {}, swing() {}, addScore() {}, ptip() {}, shake() {},
}
const box = { state: 'closed', id: 3 }
rt.entKinds.chest.interact({}, box, g)
ok(box.state === 'open' && got.length >= 2, `开箱得 ${got.length}`)
ok(got.every(([c]) => !!rt.items[c]), '战利品都在物品表内')
got.length = 0
rt.entKinds.chest.interact({}, box, g)
ok(got.length === 0, '空箱不再出货')
}
// 行为级:复活雕像拦截死亡并消耗
{
const statue = { type: 'revivestatue', id: 9 }
const world = { entities: [statue] }
const g = {
get entities() { return world.entities },
removeEntity: (e) => { world.entities = world.entities.filter((x) => x !== e) },
ptip() {}, tip() {}, shake() {},
}
const p = { hp: 0, hunger: 3, san: 5, temp: -2, name: '测试' }
ok(callHandled(rt, 'onPlayerDeath', g, p) === true, '复活雕像拦截死亡')
ok(p.hp === 75 && world.entities.length === 0, `复活半血且雕像消耗 (hp=${p.hp})`)
ok(callHandled(rt, 'onPlayerDeath', g, p) === false, '没有雕像不再拦截')
}
console.log(fails ? `\n${fails} 项失败` : '\n全部通过')
process.exit(fails ? 1 : 0)