feat(minecraft): hurt source

limitations: the yaml rule doesn't support conditioning...
This commit is contained in:
Rin
2026-02-18 11:12:46 +08:00
committed by Neko Ayaka
parent e31d5e8cdf
commit fc24f6e636
4 changed files with 151 additions and 14 deletions
@@ -1,13 +1,108 @@
import type { DamageSourceCause, DamageSourceMetadata } from '../../types/raw-events'
import { definePerceptionEvent } from '..'
interface DamageTakenExtract {
amount: number
damageSource: DamageSourceMetadata
}
let lastHealth: number | null = null
let pendingDamageAmount: number | null = null
function inferDamageSource(ctx: { bot: { entity?: any, entities?: Record<string, any> }, distanceTo: (entity: any) => number | null, maxDistance: number, isSelf: (entity: any) => boolean, entityId: (entity: any) => string }): DamageSourceMetadata {
const entity = ctx.bot.entity as any
const isInLava = Boolean(entity?.isInLava)
if (isInLava)
return { cause: 'lava' }
const isInWater = Boolean(entity?.isInWater)
if (isInWater)
return { cause: 'drown' }
const isOnFire = Boolean(entity?.isOnFire ?? entity?.fire)
if (isOnFire)
return { cause: 'fire' }
const velocityY = typeof entity?.velocity?.y === 'number' ? entity.velocity.y : null
const onGround = typeof entity?.onGround === 'boolean' ? entity.onGround : null
if (velocityY !== null && velocityY < -0.2 && onGround === false)
return { cause: 'gravity' }
const entities = Object.values(ctx.bot.entities ?? {})
let nearest: any | null = null
let nearestDistance: number | null = null
for (const candidate of entities) {
if (!candidate || ctx.isSelf(candidate))
continue
const distance = ctx.distanceTo(candidate)
if (distance === null || distance > ctx.maxDistance)
continue
if (nearestDistance === null || distance < nearestDistance) {
nearest = candidate
nearestDistance = distance
}
}
if (nearest) {
const entityType = nearest.type === 'player' ? 'player' : nearest.type === 'mob' ? 'mob' : null
if (entityType) {
return {
cause: entityType,
name: nearest.username ?? nearest.displayName ?? nearest.name,
entityId: ctx.entityId(nearest),
distance: nearestDistance ?? undefined,
}
}
const name = String(nearest.name ?? '').toLowerCase()
const cause = inferCauseFromName(name)
if (cause !== 'unknown') {
return {
cause,
name: nearest.name,
entityId: ctx.entityId(nearest),
distance: nearestDistance ?? undefined,
}
}
}
return { cause: 'unknown' }
}
function inferCauseFromName(name: string): DamageSourceCause {
if (!name)
return 'unknown'
if (name.includes('anvil'))
return 'anvil'
if (name.includes('tnt') || name.includes('creeper') || name.includes('explosion'))
return 'explosion'
if (name.includes('arrow') || name.includes('trident') || name.includes('snowball'))
return 'projectile'
return 'unknown'
}
function describeDamageTaken(extracted: DamageTakenExtract): string {
const amount = extracted.amount
const ds = extracted.damageSource
const cause = ds?.cause ?? 'unknown'
const name = ds?.name
const distance = ds?.distance
const details = [
`amount=${amount}`,
`cause=${cause}`,
typeof name === 'string' && name.length > 0 ? `name=${name}` : null,
typeof distance === 'number' ? `distance=${distance.toFixed(1)}` : null,
].filter(Boolean).join(', ')
return `Taken damage (${details}).`
}
export const damageTakenEvent = definePerceptionEvent<[], DamageTakenExtract>({
id: 'damage_taken',
modality: 'felt',
@@ -39,6 +134,7 @@ export const damageTakenEvent = definePerceptionEvent<[], DamageTakenExtract>({
const prev = lastHealth ?? current
return {
amount: pendingDamageAmount ?? Math.max(0, prev - current),
damageSource: inferDamageSource(ctx),
}
},
},
@@ -50,11 +146,12 @@ export const damageTakenEvent = definePerceptionEvent<[], DamageTakenExtract>({
signal: {
type: 'saliency_high',
description: () => 'Taken damage!',
description: extracted => describeDamageTaken(extracted),
metadata: extracted => ({
kind: 'felt',
action: 'damage',
amount: extracted.amount,
damageSource: extracted.damageSource,
}),
},
@@ -11,7 +11,11 @@ accumulator:
signal:
type: saliency_high
description: 'Taken damage!'
description: 'Taken damage (amount={{ amount }}, cause={{ damageSource.cause }}).'
confidence: 1.0
metadata:
action: damage
amount: '{{ amount }}'
cause: '{{ damageSource.cause }}'
source_name: '{{ damageSource.name }}'
source_distance: '{{ damageSource.distance }}'
@@ -144,16 +144,34 @@ export const SALIENCY_RULES: SaliencyRuleBook = {
damage_taken: {
threshold: 1, // Damage is immediately salient
key: 'felt:damage',
buildSignal: () => ({
type: 'saliency_high',
description: 'Taken damage!',
confidence: 1.0,
timestamp: Date.now(),
metadata: {
kind: 'felt',
action: 'damage',
},
}),
buildSignal: (event) => {
const e = event as Extract<RawPerceptionEvent, { modality: 'felt', kind: 'damage_taken' }>
const ds = (e as any).damageSource as any
const cause = typeof ds?.cause === 'string' ? ds.cause : 'unknown'
const name = typeof ds?.name === 'string' && ds.name.length > 0 ? ds.name : undefined
const distance = typeof ds?.distance === 'number' ? ds.distance : undefined
const amount = typeof (e as any).amount === 'number' ? (e as any).amount : undefined
const details = [
amount !== undefined ? `amount=${amount}` : null,
`cause=${cause}`,
name ? `name=${name}` : null,
distance !== undefined ? `distance=${distance.toFixed(1)}` : null,
].filter(Boolean).join(', ')
return {
type: 'saliency_high',
description: `Taken damage (${details}).`,
confidence: 1.0,
timestamp: Date.now(),
metadata: {
kind: 'felt',
action: 'damage',
amount: e.amount,
damageSource: e.damageSource,
},
}
},
},
item_collected: {
threshold: 3,
@@ -57,8 +57,26 @@ export interface FeltDamageTakenEvent extends RawPerceptionEventBase {
modality: 'felt'
kind: 'damage_taken'
amount?: number
attackerEntityType?: 'player' | 'mob'
attackerEntityId?: string
damageSource?: DamageSourceMetadata
}
export type DamageSourceCause
= | 'player'
| 'mob'
| 'fall'
| 'lava'
| 'fire'
| 'drown'
| 'anvil'
| 'gravity'
| 'explosion'
| 'projectile'
| 'unknown'
export interface DamageSourceMetadata {
cause: DamageSourceCause
name?: string
entityId?: string
distance?: number
}