218 lines
7.9 KiB
JavaScript
218 lines
7.9 KiB
JavaScript
// 切斯特:荒野某处藏着眼骨,拾起后切斯特现身跟随;
|
||
// 它会自动吞掉身边的掉落物(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)
|
||
},
|
||
},
|
||
}
|