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