import * as planck from 'planck' import { AnimalDefinition, CreatureDefinition } from '../data/animals' import { playSong, playSound } from '../shared/audio' import { Action, adjustAction } from "./Action" import { Entity, Sensor } from "./Entity" import { GibletEntity } from './GibletEntity' import { PuddleEntity } from './PuddleEntity' import { WorldContext } from './World' export class AnimalEntity extends Entity { action?: Action puddleTimer: number = 0 isPlayer: boolean = false isMonster: boolean = false thinkTimer: number = 0 // Time since last thought, used by AI. wanderTimer: number = 0 // Time since last wander. nextWander: number = 100 // Time to next wander. smelledTarget?: Entity seenTarget?: Entity def: AnimalDefinition mode: CreatureDefinition desiredActions: Action[] = [] stepSoundElapsed: number = 0 lastYellElapsed: number = 0 nextYell: number = 1000 dead: boolean = false shouldGib: boolean = false constructor(def: AnimalDefinition) { super(`${def.name}.animal.stand.west.0`) this.def = def this.mode = def.animal this.maxSpeed = def.animal.maxSpeed this.acceleration = def.animal.acceleration this.turnRate = def.animal.turnRate } act(actions: Action[]) { this.action = actions.sort((a, b) => { if (a.priority < b.priority) { return 1 } if (a.priority > b.priority) { return -1 } return 0 })[0] } update(delta: number, ctx: WorldContext) { super.update(delta) if (this.dead) { if (this.shouldGib) { this.gib(ctx) this.shouldGib = false } this.shouldRemove = true return } this.lastYellElapsed += delta let waterZones = this.zones.filter(v=>v.type==='fluid') if (waterZones.length) { this.puddleTimer += delta if (this.puddleTimer >= 600) { if (ctx) { let p = this.position p[0] += (this.sprite.frame?.originX??0) + ((this.sprite.frame?.width??0)/2??0) p[1] += (this.sprite.frame?.originY??0) + ((this.sprite.frame?.height??0)/2??0) ctx.addEntity(new PuddleEntity('effects.water.ripple.small.0'), 'ground', p[0]+4, p[1]+6) } this.puddleTimer = 0 } } let shouldMove = false if (this.action) { // FIXME: Use physics. switch(this.action.type) { case 'west': if (this.direction !== 0) { if (this.direction < 180) { this.direction -= this.mode.turnRate } else { this.direction += this.mode.turnRate } } shouldMove = true break case 'east': if (this.direction !== 180) { if (this.direction < 180) { this.direction += this.mode.turnRate } else { this.direction -= this.mode.turnRate } } shouldMove = true break case 'north': if (this.direction !== 90) { if (this.direction < 90 || this.direction > 270) { this.direction += this.mode.turnRate } else { this.direction -= this.mode.turnRate } } shouldMove = true break case 'south': if (this.direction !== 270) { if (this.direction > 90 && this.direction < 270) { this.direction += this.mode.turnRate } else { this.direction -= this.mode.turnRate } } shouldMove = true break case 'attack': this.yell(1) break } if (this.direction > 360) { this.direction = 0 } else if (this.direction < 0) { this.direction = 360 } } if (shouldMove) { this.stepSoundElapsed += delta let r = this.direction * (Math.PI/180) if (Math.abs(this.velocity[0]) < this.mode.maxSpeed) { this.velocity[0] -= Math.cos(r) * this.mode.acceleration } if (Math.abs(this.velocity[1]) < this.mode.maxSpeed) { this.velocity[1] -= Math.sin(r) * this.mode.acceleration } let cardinal = this.getCardinal(this.direction) this.sprite.setKey = 'run' if (this.sprite.subsetKey !== cardinal || this.sprite.subsetKey !== 'run') { this.sprite.setCtor(`${this.sprite.spriteKey}.${this.sprite.animationKey}.${this.sprite.setKey}.${cardinal}.${this.sprite.frameIndex}`) } if (this.stepSoundElapsed >= 200) { let v = 1 + Math.floor(Math.random() * 3) playSound('action/footstep-tiny-v'+v, 0.5) this.stepSoundElapsed = 0 } this.sprite.animate = true } else { this.sprite.animate = false this.sprite.setKey = 'stand' this.sprite.setCtor(`${this.sprite.spriteKey}.${this.sprite.animationKey}.${this.sprite.setKey}.${this.getCardinal(this.direction)}.0`) } // this.velocity[0] *= 0.65 this.velocity[1] *= 0.65 // Eh... let's manually handle velocity this.body?.setLinearVelocity(planck.Vec2(this.velocity[0], this.velocity[1])) } getCardinal(direction: number): string { const degreesPerDirection = 360 / 4 const angle = direction + degreesPerDirection / 2 if (angle >= 0 * degreesPerDirection && angle < 1 * degreesPerDirection) { return 'west' } else if (angle >= 1 * degreesPerDirection && angle < 2 * degreesPerDirection) { return 'north' } else if (angle >= 2 * degreesPerDirection && angle < 3 * degreesPerDirection) { return 'east' } return 'south' } think(delta: number) { this.thinkTimer += delta while (this.thinkTimer >= 100) { if (!this.isMonster) { if (this.seenTarget) { let r = Math.atan2(this.y-(this.seenTarget.y+10), this.x-this.seenTarget.x) // FIXME: 10 ain't right son let a = r * (180 / Math.PI) if (a < 0) { a += 360 } a -= 180 if (a < 0) { a = 360 - a } this.desiredActions = adjustAction([], this.getCardinal(a), 0.85) this.desiredActions = adjustAction(this.desiredActions, this.getCardinal(Math.random()*360), Math.random()) if (Math.random() > 1 - this.mode.erratic) { this.desiredActions = adjustAction(this.desiredActions, this.getCardinal(Math.random()*360), 1) } this.yell(1) } else if (this.smelledTarget) { let r = Math.atan2(this.y-this.smelledTarget.y, this.x-this.smelledTarget.x) let a = r * (180 / Math.PI) if (a < 0) { a += 360 } a -= 180 if (a < 0) { a = 360 - a } // 5% to walk away, otherwise just chill. if (Math.random() > 0.95) { this.desiredActions = adjustAction([], this.getCardinal(a), 0.85) } } else { this.wanderTimer += delta if (this.wanderTimer >= this.nextWander) { this.nextWander = Math.max(10, Math.random() * 100) if (Math.random() > 1 - this.mode.laziness) { this.desiredActions = [] } else { this.desiredActions = adjustAction([], this.getCardinal(Math.random()*360), 1) } if (Math.random() > 1 - this.mode.noisiness) { this.yell(0.1) } this.wanderTimer = 0 } } } this.thinkTimer -= 100 this.act(this.desiredActions) } } sense(sensor: Sensor, entity: AnimalEntity) { if (this.isPlayer) return if (sensor.type === 'long') { this.smell(entity) } else { this.see(entity) } } lost(sensor: Sensor, entity: AnimalEntity) { if (this.isPlayer) return if (sensor.type === 'long') { this.unsmell(entity) } else { this.unsee(entity) } } smell(entity: AnimalEntity) { if (entity.isPlayer) { this.smelledTarget = entity } } unsmell(entity: AnimalEntity) { if (entity.isPlayer) { this.smelledTarget = undefined } } see(entity: AnimalEntity) { if (entity.isPlayer) { this.seenTarget = entity } } unsee(entity: AnimalEntity) { if (entity.isPlayer) { this.seenTarget = undefined } } yell(volume: number) { if (this.lastYellElapsed > this.nextYell) { this.nextYell = 500 + Math.random()*3000 this.lastYellElapsed = 0 let v = 1 + Math.floor(Math.random() * 2) if (this.isMonster) { playSound(`monsters/evil-${this.def.name}-v${v}`, volume) } else { playSound(`animals/${this.def.name}-v${v}`, volume) } } } gib(ctx: WorldContext) { let p = this.position p[0] += this.sprite.container.width/2 p[1] += this.sprite.container.height/2 this.dead = true let force = -1 + Math.random() * 2 let dir = Math.random() * 320 // 50% head gib if (Math.random() > 0.5) { ctx.addEntity(new GibletEntity(`giblets.${this.sprite.spriteKey}.head.default.0`, dir+Math.random()*40, force+Math.random()*10), 'gibs', p[0], p[1]) } // 44% leg gib per leg for (let i = 0; i < 4; i++) { if (Math.random() > 0.66) { ctx.addEntity(new GibletEntity(`giblets.${this.sprite.spriteKey}.leg.default.0`, dir+Math.random()*40, force+Math.random()*10), 'gibs', p[0], p[1]) } } // 35% extra gibs per 8 for (let i = 0; i < 8; i++) { if (Math.random() > 0.65) { ctx.addEntity(new GibletEntity(`giblets.any.chunk${Math.floor(1+Math.random()*4)}.default.0`, dir+Math.random()*40, force+Math.random()*10), 'gibs', p[0], p[1]) } } playSound(`action/splat-v${1+Math.floor(Math.random()*7)}`, 0.5) } } export function isAnimalEntity(o: any): o is AnimalEntity { return o.act }