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

218 lines
7.9 KiB
JavaScript
Raw 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.
// 切斯特:荒野某处藏着眼骨,拾起后切斯特现身跟随;
// 它会自动吞掉身边的掉落物(9 格随身仓),点击它把肚子里的东西全吐出来
const TAU = Math.PI * 2
const STORE_SLOTS = 9
function ownerOf(g) {
return g.players.find((p) => !p.dead && g.countItem(p, 'eyebone') > 0) || null
}
function stackInto(store, code, n, cap) {
let left = n
// 先叠进已有同类
for (const s of store) {
if (!s || s.code !== code || s.n >= cap) continue
const add = Math.min(cap - s.n, left)
s.n += add
left -= add
if (!left) return 0
}
// 再开新格
for (let i = 0; i < store.length && left; i++) {
if (store[i]) continue
const add = Math.min(cap, left)
store[i] = { code, n: add }
left -= add
}
return left
}
export default {
id: 'chester',
name: '切斯特',
desc: '寻获眼骨召来跟班箱子,自动吞附近掉落物',
factor: 1,
tag: 'content',
items: {
eyebone: { name: '眼骨', stack: 1 },
},
icons: {
eyebone: (c) => {
c.strokeStyle = '#ece7db'
c.lineWidth = 4
c.beginPath(); c.moveTo(-4, 12); c.lineTo(4, -4); c.stroke()
c.fillStyle = '#ece7db'
c.strokeStyle = '#1d1409'
c.lineWidth = 1.6
;[[-7, 12], [-1, 14]].forEach(([x, y]) => {
c.beginPath(); c.arc(x, y, 3.4, 0, TAU); c.fill(); c.stroke()
})
c.beginPath(); c.arc(5, -8, 7, 0, TAU); c.fill(); c.stroke()
c.fillStyle = '#fff'
c.beginPath(); c.arc(5, -8, 4.6, 0, TAU); c.fill()
c.fillStyle = '#4a90c2'
c.beginPath(); c.arc(5, -8, 2.6, 0, TAU); c.fill()
c.fillStyle = '#1d1409'
c.beginPath(); c.arc(5, -8, 1.2, 0, TAU); c.fill()
},
},
mobs: {
chester: {
conf: { hp: 300, speed: 160, dmg: 0, score: 0, name: '切斯特' },
friendly: true,
drops: [],
update(m, dt, g) {
if (!m.store) m.store = Array(STORE_SLOTS).fill(null)
// 优先扑向附近掉落物吞掉(眼骨除外;刚吐出的东西有几秒宽限期不回吞)
const loot = m.cd <= 0
? g.entities.find((e) => e.loot && e.code !== 'eyebone' && !(e.shyUntil > g.time) && Math.hypot(e.x - m.x, e.y - m.y) < 150)
: null
if (loot) {
const d = Math.hypot(loot.x - m.x, loot.y - m.y)
if (d > 26) {
g.moveToward(m, loot.x, loot.y, 175, dt)
} else {
const cap = g.items[loot.code]?.stack || g.tune.stackMax
const left = stackInto(m.store, loot.code, loot.n, cap)
if (left < loot.n) {
m.cd = 0.35
m.gulpT = 0.5
if (left > 0) loot.n = left
else g.removeEntity(loot)
} else {
m.cd = 2 // 仓满吞不下,歇会儿别原地抽搐
}
}
if (m.gulpT > 0) m.gulpT -= dt
return
}
const owner = ownerOf(g)
if (owner) {
const d = Math.hypot(owner.x - m.x, owner.y - m.y)
if (d > 64) {
const sprint = d > 220 ? 1.5 : 1
g.moveToward(m, owner.x - owner.dir * 40, owner.y + 16, 160 * sprint, dt)
}
}
if (m.gulpT > 0) m.gulpT -= dt
},
interact(p, m, g) {
const has = (m.store || []).filter(Boolean)
if (!has.length) {
g.ptip(p, '切斯特打了个哈欠(肚子是空的)')
return
}
has.forEach((s) => {
const l = g.dropLoot(m.x + g.rand(-24, 24), m.y + g.rand(10, 30), s.code, s.n)
if (l) l.shyUntil = g.time + 6
})
m.store = Array(STORE_SLOTS).fill(null)
m.gulpT = 0.5
g.ptip(p, `切斯特吐出了 ${has.length} 样东西`)
},
draw(ctx, m, time) {
const hop = m.moving ? Math.abs(Math.sin(time * 9 + m.bob)) * 6 : Math.sin(time * 2.2 + m.bob) * 1.5
const squash = m.gulpT > 0 ? 1.15 : 1
ctx.save()
ctx.translate(0, -hop)
ctx.scale(m.dir, 1)
// 阴影
ctx.restore()
ctx.fillStyle = 'rgba(0,0,0,0.25)'
ctx.beginPath()
ctx.ellipse(0, 2, 16, 5, 0, 0, TAU)
ctx.fill()
ctx.save()
ctx.translate(0, -hop)
ctx.scale(m.dir * squash, 1 / squash)
// 小短腿
ctx.strokeStyle = '#4a2c1a'
ctx.lineWidth = 4
const leg = m.moving ? Math.sin(time * 12) * 3 : 0
ctx.beginPath(); ctx.moveTo(-8, -4); ctx.lineTo(-8 + leg, 2); ctx.moveTo(8, -4); ctx.lineTo(8 - leg, 2); ctx.stroke()
// 圆滚滚的身体
ctx.fillStyle = '#a85a28'
ctx.strokeStyle = '#1d1409'
ctx.lineWidth = 2
ctx.beginPath()
ctx.ellipse(0, -16, 15, 14, 0, 0, TAU)
ctx.fill()
ctx.stroke()
// 大嘴(张合)
const mouth = m.gulpT > 0 ? 5 : 2 + Math.sin(time * 3 + m.bob) * 1
ctx.fillStyle = '#5d2a12'
ctx.beginPath()
ctx.ellipse(4, -12, 9, mouth, 0.15, 0, TAU)
ctx.fill()
// 牙
ctx.fillStyle = '#ece7db'
ctx.beginPath(); ctx.moveTo(-2, -14); ctx.lineTo(0, -10); ctx.lineTo(2, -14); ctx.closePath(); ctx.fill()
ctx.beginPath(); ctx.moveTo(7, -14); ctx.lineTo(9, -10); ctx.lineTo(11, -14); ctx.closePath(); ctx.fill()
// 眼睛
ctx.fillStyle = '#fff'
ctx.strokeStyle = '#1d1409'
ctx.lineWidth = 1.4
ctx.beginPath(); ctx.arc(-2, -24, 3.4, 0, TAU); ctx.fill(); ctx.stroke()
ctx.beginPath(); ctx.arc(7, -23, 3, 0, TAU); ctx.fill(); ctx.stroke()
ctx.fillStyle = '#1d1409'
ctx.beginPath(); ctx.arc(-1.4, -23.6, 1.4, 0, TAU); ctx.fill()
ctx.beginPath(); ctx.arc(7.6, -22.6, 1.2, 0, TAU); ctx.fill()
// 角耳朵
ctx.strokeStyle = '#7a3f1c'
ctx.lineWidth = 3.4
ctx.beginPath(); ctx.moveTo(-10, -27); ctx.quadraticCurveTo(-14, -34, -10, -38); ctx.stroke()
ctx.beginPath(); ctx.moveTo(4, -28); ctx.quadraticCurveTo(8, -35, 4, -39); ctx.stroke()
ctx.restore()
},
},
},
hooks: {
onWorldGen(g) {
// 眼骨落在远离出生点的荒野
const cx = g.worldW / 2
const cy = g.worldH / 2
let best = null
for (let tries = 0; tries < 300; tries++) {
const x = 80 + Math.random() * (g.worldW - 160)
const y = 80 + Math.random() * (g.worldH - 160)
if (!g.isLand(x, y)) continue
const d = Math.hypot(x - cx, y - cy)
if (d > 480 && d < 1100) { best = { x, y }; break }
}
if (!best) best = g.findLand(cx + 500, cy, 0, 200)
g.addEntity('loot', best.x, best.y, { code: 'eyebone', n: 1, fresh: 1 })
g.tip('传说荒野某处躺着一根奇怪的眼骨……')
},
onTick(g) {
const owner = ownerOf(g)
if (!owner) return
if (g.monsters.some((m) => m.kind === 'chester')) return
const spot = g.findLand(owner.x + 60, owner.y + 40, 20, 60)
g.spawnMob('chester', spot.x, spot.y)
g.tip('切斯特闻着眼骨的味道蹦了出来!(它会吞附近的掉落物,点它取回)')
},
onUseItem(g, p, s) {
if (s.code !== 'eyebone') return
const ch = g.monsters.find((m) => m.kind === 'chester')
if (ch) {
const n = (ch.store || []).filter(Boolean).length
g.ptip(p, n ? `切斯特肚子里有 ${n} 样东西(点击它取回)` : '切斯特跟在你身边(会自动吞掉落物)')
} else {
g.ptip(p, '眼骨微微颤动着……切斯特正在赶来')
}
return true
},
// 切斯特与仓内物品随存档走(生物本身不入档)
save(g) {
const ch = g.monsters.find((m) => m.kind === 'chester')
if (!ch) return null
return { x: Math.round(ch.x), y: Math.round(ch.y), store: (ch.store || []).map((s) => (s ? [s.code, s.n] : 0)) }
},
load(g, d) {
const m = g.spawnMob('chester', d.x, d.y)
m.store = (d.store || []).map((v) => (v ? { code: v[0], n: v[1] } : null))
while (m.store.length < STORE_SLOTS) m.store.push(null)
},
},
}