feat(minecraft): hurt source
limitations: the yaml rule doesn't support conditioning...
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user