更新饥荒
This commit is contained in:
202
_campsmoke.mjs
202
_campsmoke.mjs
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user