diff --git a/_campsmoke.mjs b/_campsmoke.mjs deleted file mode 100644 index 001eca0..0000000 --- a/_campsmoke.mjs +++ /dev/null @@ -1,202 +0,0 @@ -// 临时冒烟:验证 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) diff --git a/_campveto.mjs b/_campveto.mjs deleted file mode 100644 index af4f2ca..0000000 --- a/_campveto.mjs +++ /dev/null @@ -1,77 +0,0 @@ -// 临时:验证 onMobVictim 否决钩子语义(跑完删除) -import camp from './src/games/starve/mods/camp.js' - -let fails = 0 -const ok = (c, m) => { if (c) console.log(' PASS', m); else { fails++; console.error(' FAIL', m) } } - -const HOOK = camp.hooks.onMobVictim -function makeG() { - const g = { - mon: { hound: { hostile: true }, icehound: { hostile: true }, pig: { friendly: true } }, - players: [], - monsters: [], - entities: [{ type: 'camphouse', x: 0, y: 0 }], - worldW: 0, - worldH: 0, - } - // 模拟核心 g.allows:任一 mod 返回 false 即 false - g.allows = (name, mm, vv) => HOOK(g, mm, vv) !== false - return g -} - -console.log('== onMobVictim 否决语义 ==') -const g = makeG() -const hound = { kind: 'hound', x: 800, y: 800 } -const pig = { kind: 'pig', x: 800, y: 800 } - -const inCamp = { x: 100, y: 100 } // 距房(0,0) ~141 < 420 -const outside = { x: 800, y: 800 } // 距房 ~1131 > 420 - -ok(HOOK(g, hound, inCamp) === false, '营地内玩家 → 返回 false(怪不选他)') -ok(HOOK(g, hound, outside) !== false, '营地外玩家 → 不返回 false') -ok(HOOK(g, pig, inCamp) !== false, '非敌对生物 → 不返回 false(保留打守卫/猪人)') -ok(g.allows('onMobVictim', hound, inCamp) === false, 'g.allows 透传 false 正确') -ok(g.allows('onMobVictim', hound, outside) === true, 'g.allows 透传 true 正确') - -console.log('== 端点边界 ==') -ok(HOOK(g, hound, { x: 419, y: 0 }) === false, '半径内 419 → false') -ok(HOOK(g, hound, { x: 421, y: 0 }) !== false, '半径外 421 → 允许') - -console.log('== 无房屋时退回世界中心 ==') -const g2 = makeG() -g2.entities = [] -g2.worldW = 1000 -g2.worldH = 1000 -ok(HOOK(g2, hound, { x: 600, y: 500 }) === false, '无房屋→以世界中心(500,500)为圆心,100 距离内 → false') -ok(HOOK(g2, hound, { x: 1200, y: 500 }) !== false, '无房屋→距世界中心 700 → 允许') -ok(HOOK(g2, hound, { x: 500, y: 500 }) === false, '世界中心本身 → false') - -console.log('== 模拟核心 nearestVictim:营地内玩家被排除 ==') -{ - // 复刻 mobs.js nearestVictim 的玩家循环(含 g.allows 否决) - const pick = (gg, m) => { - let best = null - let bd = Infinity - gg.players.forEach((p) => { - if (p.dead) return - if (gg.allows && gg.allows('onMobVictim', m, p) === false) return - const d = Math.hypot(p.x - m.x, p.y - m.y) - if (d < bd) { bd = d; best = p } - }) - return best - } - const gg = makeG() - const m = { kind: 'hound', x: 300, y: 300 } - const pIn = { dead: false, x: 100, y: 100 } // 营地内,离怪更近 (~283) - const pOut = { dead: false, x: 900, y: 900 } // 营地外,离怪更远 (~849) - gg.players = [pIn, pOut] - ok(pick(gg, m) === pOut, '营地内玩家被否决,目标落到营地外玩家') - - // 若营地内玩家是唯一玩家 → 无目标(怪待命) - const gg2 = makeG() - gg2.players = [pIn] - ok(pick(gg2, { kind: 'hound', x: 300, y: 300 }) === null, '仅营地内玩家 → 无目标(怪待命,符合 updHound if(!near)return)') -} - -console.log(`\n${fails} 项失败`) -process.exit(fails ? 1 : 0) diff --git a/_qa_debug.mjs b/_qa_debug.mjs deleted file mode 100644 index f017707..0000000 --- a/_qa_debug.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import { activateMods } from './src/games/starve/mods/registry.js' - -const laborRt2 = activateMods(['labor']) -console.log('berry item =', JSON.stringify(laborRt2.items.berry)) -console.log('wheat item =', !!laborRt2.items.wheat) - -const office = { type: 'laboroffice', id: 5 } -laborRt2.entKinds.laboroffice.init(office); office.mx.farmer = 1 -const wh = { type: 'laborwarehouse', x: 0, y: 0, id: 6 } -laborRt2.entKinds.laborwarehouse.init(wh) -wh.mx.boxes[0].items.berry = 10 -console.log('boxes[0] =', JSON.stringify(wh.mx.boxes[0].items)) - -const players = [{ x: 0, y: 0, dead: false, seat: 0, slots: [], pack: null }] -const g = { - entities: [office, wh], - monsters: [{ kind: 'laborfarmer', x: 0, y: 0, id: 901, office: 5 }], - players, - mon: laborRt2.mon, - items: laborRt2.items, - me: () => players[0], - markDirty() {}, tip() {}, ptip() {}, -} -const hooks = (laborRt2.hooks.onNewDay || []) -console.log('onNewDay hooks =', hooks.map((h) => h.id)) -hooks.forEach((h) => h.fn(g)) -console.log('after onNewDay strike =', g.monsters[0].strike, 'berry =', wh.mx.boxes.reduce((n, b) => n + (b.items.berry || 0), 0)) diff --git a/src/games/components/StarveGame.vue b/src/games/components/StarveGame.vue index 56727f7..c360aeb 100644 --- a/src/games/components/StarveGame.vue +++ b/src/games/components/StarveGame.vue @@ -10,7 +10,7 @@ import { CAP, DAY_LEN, cookPot, DECOR } from '../starve/data' import { seasonOf, daySplit, phaseOf, ambientTemp, rollWeather, tickWeather, updateBodyTemp } from '../starve/climate' import { TILE, GW, GH, WORLD, BIOMES, tileIdx, isLand, biomeAt, onRoad, findLandNear, - genIsland, planEntities, genDecor, buildTilePatterns, drawTiles, + genIsland, planEntities, genDecor, buildTilePatterns, drawTiles, configureWorld, } from '../starve/world' import { buildIcons, drawEnt, drawMob, drawFirePainter, drawDecor } from '../starve/paint' import { updateMob, moveMob } from '../starve/mobs'