182 lines
9.9 KiB
JavaScript
182 lines
9.9 KiB
JavaScript
// 临时冒烟测试:纯逻辑模块(不触 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)
|