From fc24f6e6369d188142779326fb477a634bc87f79 Mon Sep 17 00:00:00 2001 From: Rin Date: Wed, 21 Jan 2026 01:13:48 +0800 Subject: [PATCH] feat(minecraft): hurt source limitations: the yaml rule doesn't support conditioning... --- .../events/definitions/damage-taken.ts | 99 ++++++++++++++++++- .../perception/rules/danger/damage.yaml | 6 +- .../cognitive/perception/saliency-rules.ts | 38 +++++-- .../cognitive/perception/types/raw-events.ts | 22 ++++- 4 files changed, 151 insertions(+), 14 deletions(-) diff --git a/services/minecraft/src/cognitive/perception/events/definitions/damage-taken.ts b/services/minecraft/src/cognitive/perception/events/definitions/damage-taken.ts index ef0096d72..e842b1c86 100644 --- a/services/minecraft/src/cognitive/perception/events/definitions/damage-taken.ts +++ b/services/minecraft/src/cognitive/perception/events/definitions/damage-taken.ts @@ -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 }, 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, }), }, diff --git a/services/minecraft/src/cognitive/perception/rules/danger/damage.yaml b/services/minecraft/src/cognitive/perception/rules/danger/damage.yaml index 2fd11a054..1f78c3377 100644 --- a/services/minecraft/src/cognitive/perception/rules/danger/damage.yaml +++ b/services/minecraft/src/cognitive/perception/rules/danger/damage.yaml @@ -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 }}' diff --git a/services/minecraft/src/cognitive/perception/saliency-rules.ts b/services/minecraft/src/cognitive/perception/saliency-rules.ts index 4ba1d6176..28f632970 100644 --- a/services/minecraft/src/cognitive/perception/saliency-rules.ts +++ b/services/minecraft/src/cognitive/perception/saliency-rules.ts @@ -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 + 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, diff --git a/services/minecraft/src/cognitive/perception/types/raw-events.ts b/services/minecraft/src/cognitive/perception/types/raw-events.ts index 37e8e1fec..e8915a53d 100644 --- a/services/minecraft/src/cognitive/perception/types/raw-events.ts +++ b/services/minecraft/src/cognitive/perception/types/raw-events.ts @@ -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 }