2022-01-29 18:25:51 -08:00
|
|
|
import * as planck from 'planck'
|
2022-01-29 23:48:13 -08:00
|
|
|
import { AnimalDefinition, CreatureDefinition } from '../data/animals'
|
2022-01-30 01:18:03 -08:00
|
|
|
import { playSong, playSound } from '../shared/audio'
|
2022-01-30 00:24:56 -08:00
|
|
|
import { Action, adjustAction } from "./Action"
|
2022-01-29 23:48:13 -08:00
|
|
|
import { Entity, Sensor } from "./Entity"
|
2022-01-30 06:23:50 -08:00
|
|
|
import { GibletEntity } from './GibletEntity'
|
2022-01-29 21:43:20 -08:00
|
|
|
import { PuddleEntity } from './PuddleEntity'
|
|
|
|
import { WorldContext } from './World'
|
2022-01-29 18:25:51 -08:00
|
|
|
|
2022-01-29 22:39:11 -08:00
|
|
|
export class AnimalEntity extends Entity {
|
2022-01-29 18:25:51 -08:00
|
|
|
action?: Action
|
2022-01-29 21:43:20 -08:00
|
|
|
puddleTimer: number = 0
|
2022-01-29 22:39:11 -08:00
|
|
|
isPlayer: boolean = false
|
2022-01-29 23:48:13 -08:00
|
|
|
isMonster: boolean = false
|
|
|
|
thinkTimer: number = 0 // Time since last thought, used by AI.
|
2022-01-30 00:24:56 -08:00
|
|
|
wanderTimer: number = 0 // Time since last wander.
|
|
|
|
nextWander: number = 100 // Time to next wander.
|
|
|
|
smelledTarget?: Entity
|
|
|
|
seenTarget?: Entity
|
2022-01-29 23:48:13 -08:00
|
|
|
def: AnimalDefinition
|
|
|
|
mode: CreatureDefinition
|
2022-01-30 00:24:56 -08:00
|
|
|
desiredActions: Action[] = []
|
2022-01-30 01:18:03 -08:00
|
|
|
stepSoundElapsed: number = 0
|
|
|
|
lastYellElapsed: number = 0
|
|
|
|
nextYell: number = 1000
|
2022-01-30 06:23:50 -08:00
|
|
|
dead: boolean = false
|
|
|
|
shouldGib: boolean = false
|
2022-01-29 22:39:11 -08:00
|
|
|
|
2022-01-29 23:48:13 -08:00
|
|
|
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
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
act(actions: Action[]) {
|
|
|
|
this.action = actions.sort((a, b) => {
|
|
|
|
if (a.priority < b.priority) {
|
2022-01-29 20:30:56 -08:00
|
|
|
return 1
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
if (a.priority > b.priority) {
|
2022-01-29 20:30:56 -08:00
|
|
|
return -1
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
return 0
|
|
|
|
})[0]
|
|
|
|
}
|
2022-01-29 21:43:20 -08:00
|
|
|
update(delta: number, ctx: WorldContext) {
|
|
|
|
super.update(delta)
|
2022-01-30 06:23:50 -08:00
|
|
|
|
|
|
|
if (this.dead) {
|
|
|
|
if (this.shouldGib) {
|
|
|
|
this.gib(ctx)
|
|
|
|
this.shouldGib = false
|
|
|
|
}
|
|
|
|
this.shouldRemove = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-30 01:18:03 -08:00
|
|
|
this.lastYellElapsed += delta
|
2022-01-29 21:43:20 -08:00
|
|
|
|
|
|
|
let waterZones = this.zones.filter(v=>v.type==='fluid')
|
|
|
|
if (waterZones.length) {
|
|
|
|
this.puddleTimer += delta
|
|
|
|
if (this.puddleTimer >= 600) {
|
|
|
|
if (ctx) {
|
2022-01-29 22:13:35 -08:00
|
|
|
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)
|
2022-01-30 06:30:49 -08:00
|
|
|
ctx.addEntity(new PuddleEntity('effects.water.ripple.small.0'), 'ground', p[0]+4, p[1]+6)
|
2022-01-29 21:43:20 -08:00
|
|
|
}
|
|
|
|
this.puddleTimer = 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let shouldMove = false
|
2022-01-29 18:25:51 -08:00
|
|
|
if (this.action) {
|
|
|
|
// FIXME: Use physics.
|
|
|
|
switch(this.action.type) {
|
|
|
|
case 'west':
|
|
|
|
if (this.direction !== 0) {
|
|
|
|
if (this.direction < 180) {
|
2022-01-29 23:48:13 -08:00
|
|
|
this.direction -= this.mode.turnRate
|
2022-01-29 18:25:51 -08:00
|
|
|
} else {
|
2022-01-29 23:48:13 -08:00
|
|
|
this.direction += this.mode.turnRate
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
shouldMove = true
|
|
|
|
break
|
|
|
|
case 'east':
|
|
|
|
if (this.direction !== 180) {
|
|
|
|
if (this.direction < 180) {
|
2022-01-29 23:48:13 -08:00
|
|
|
this.direction += this.mode.turnRate
|
2022-01-29 18:25:51 -08:00
|
|
|
} else {
|
2022-01-29 23:48:13 -08:00
|
|
|
this.direction -= this.mode.turnRate
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
shouldMove = true
|
|
|
|
break
|
|
|
|
case 'north':
|
|
|
|
if (this.direction !== 90) {
|
|
|
|
if (this.direction < 90 || this.direction > 270) {
|
2022-01-29 23:48:13 -08:00
|
|
|
this.direction += this.mode.turnRate
|
2022-01-29 18:25:51 -08:00
|
|
|
} else {
|
2022-01-29 23:48:13 -08:00
|
|
|
this.direction -= this.mode.turnRate
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
shouldMove = true
|
|
|
|
break
|
|
|
|
case 'south':
|
|
|
|
if (this.direction !== 270) {
|
|
|
|
if (this.direction > 90 && this.direction < 270) {
|
2022-01-29 23:48:13 -08:00
|
|
|
this.direction += this.mode.turnRate
|
2022-01-29 18:25:51 -08:00
|
|
|
} else {
|
2022-01-29 23:48:13 -08:00
|
|
|
this.direction -= this.mode.turnRate
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
shouldMove = true
|
|
|
|
break
|
2022-01-30 01:18:03 -08:00
|
|
|
case 'attack':
|
|
|
|
this.yell(1)
|
|
|
|
break
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
if (this.direction > 360) {
|
|
|
|
this.direction = 0
|
|
|
|
} else if (this.direction < 0) {
|
|
|
|
this.direction = 360
|
|
|
|
}
|
2022-01-29 21:43:20 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (shouldMove) {
|
2022-01-30 01:18:03 -08:00
|
|
|
this.stepSoundElapsed += delta
|
2022-01-29 21:43:20 -08:00
|
|
|
let r = this.direction * (Math.PI/180)
|
2022-01-29 23:48:13 -08:00
|
|
|
if (Math.abs(this.velocity[0]) < this.mode.maxSpeed) {
|
|
|
|
this.velocity[0] -= Math.cos(r) * this.mode.acceleration
|
2022-01-29 21:43:20 -08:00
|
|
|
}
|
2022-01-29 23:48:13 -08:00
|
|
|
if (Math.abs(this.velocity[1]) < this.mode.maxSpeed) {
|
|
|
|
this.velocity[1] -= Math.sin(r) * this.mode.acceleration
|
2022-01-29 21:43:20 -08:00
|
|
|
}
|
2022-01-30 00:24:56 -08:00
|
|
|
let cardinal = this.getCardinal(this.direction)
|
2022-01-29 21:43:20 -08:00
|
|
|
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}`)
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
2022-01-30 01:18:03 -08:00
|
|
|
if (this.stepSoundElapsed >= 200) {
|
2022-01-30 02:27:17 -08:00
|
|
|
let v = 1 + Math.floor(Math.random() * 3)
|
2022-01-30 01:18:03 -08:00
|
|
|
playSound('action/footstep-tiny-v'+v, 0.5)
|
|
|
|
this.stepSoundElapsed = 0
|
|
|
|
}
|
2022-01-29 21:43:20 -08:00
|
|
|
this.sprite.animate = true
|
|
|
|
} else {
|
|
|
|
this.sprite.animate = false
|
|
|
|
this.sprite.setKey = 'stand'
|
2022-01-30 00:24:56 -08:00
|
|
|
this.sprite.setCtor(`${this.sprite.spriteKey}.${this.sprite.animationKey}.${this.sprite.setKey}.${this.getCardinal(this.direction)}.0`)
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
2022-01-29 22:24:41 -08:00
|
|
|
this.velocity[0] *= 0.65
|
|
|
|
this.velocity[1] *= 0.65
|
2022-01-29 18:25:51 -08:00
|
|
|
|
|
|
|
// Eh... let's manually handle velocity
|
|
|
|
this.body?.setLinearVelocity(planck.Vec2(this.velocity[0], this.velocity[1]))
|
|
|
|
}
|
2022-01-30 00:24:56 -08:00
|
|
|
getCardinal(direction: number): string {
|
2022-01-29 21:43:20 -08:00
|
|
|
const degreesPerDirection = 360 / 4
|
2022-01-30 00:24:56 -08:00
|
|
|
const angle = direction + degreesPerDirection / 2
|
2022-01-29 18:25:51 -08:00
|
|
|
|
|
|
|
if (angle >= 0 * degreesPerDirection && angle < 1 * degreesPerDirection) {
|
2022-01-29 21:43:20 -08:00
|
|
|
return 'west'
|
2022-01-29 18:25:51 -08:00
|
|
|
} else if (angle >= 1 * degreesPerDirection && angle < 2 * degreesPerDirection) {
|
2022-01-29 21:43:20 -08:00
|
|
|
return 'north'
|
2022-01-29 18:25:51 -08:00
|
|
|
} else if (angle >= 2 * degreesPerDirection && angle < 3 * degreesPerDirection) {
|
2022-01-29 21:43:20 -08:00
|
|
|
return 'east'
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
2022-01-29 21:43:20 -08:00
|
|
|
return 'south'
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
2022-01-29 23:48:13 -08:00
|
|
|
think(delta: number) {
|
|
|
|
this.thinkTimer += delta
|
2022-01-30 00:24:56 -08:00
|
|
|
while (this.thinkTimer >= 100) {
|
2022-01-29 23:48:13 -08:00
|
|
|
if (!this.isMonster) {
|
2022-01-30 00:24:56 -08:00
|
|
|
if (this.seenTarget) {
|
2022-01-30 02:27:17 -08:00
|
|
|
let r = Math.atan2(this.y-(this.seenTarget.y+10), this.x-this.seenTarget.x) // FIXME: 10 ain't right son
|
2022-01-30 00:24:56 -08:00
|
|
|
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())
|
2022-01-30 02:27:17 -08:00
|
|
|
if (Math.random() > 1 - this.mode.erratic) {
|
|
|
|
this.desiredActions = adjustAction(this.desiredActions, this.getCardinal(Math.random()*360), 1)
|
|
|
|
}
|
2022-01-30 01:18:03 -08:00
|
|
|
this.yell(1)
|
2022-01-30 00:24:56 -08:00
|
|
|
} 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)
|
|
|
|
}
|
2022-01-29 23:48:13 -08:00
|
|
|
} else {
|
2022-01-30 00:24:56 -08:00
|
|
|
this.wanderTimer += delta
|
|
|
|
if (this.wanderTimer >= this.nextWander) {
|
|
|
|
this.nextWander = Math.max(10, Math.random() * 100)
|
2022-01-30 00:37:05 -08:00
|
|
|
if (Math.random() > 1 - this.mode.laziness) {
|
2022-01-30 00:24:56 -08:00
|
|
|
this.desiredActions = []
|
|
|
|
} else {
|
|
|
|
this.desiredActions = adjustAction([], this.getCardinal(Math.random()*360), 1)
|
|
|
|
}
|
2022-01-30 01:18:03 -08:00
|
|
|
if (Math.random() > 1 - this.mode.noisiness) {
|
2022-01-30 03:23:57 -08:00
|
|
|
this.yell(0.1)
|
2022-01-30 01:18:03 -08:00
|
|
|
}
|
2022-01-30 00:24:56 -08:00
|
|
|
this.wanderTimer = 0
|
|
|
|
}
|
2022-01-29 23:48:13 -08:00
|
|
|
}
|
|
|
|
}
|
2022-01-30 00:24:56 -08:00
|
|
|
this.thinkTimer -= 100
|
|
|
|
this.act(this.desiredActions)
|
2022-01-29 23:48:13 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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) {
|
2022-01-30 00:24:56 -08:00
|
|
|
this.smelledTarget = entity
|
2022-01-29 23:48:13 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
unsmell(entity: AnimalEntity) {
|
|
|
|
if (entity.isPlayer) {
|
2022-01-30 00:24:56 -08:00
|
|
|
this.smelledTarget = undefined
|
2022-01-29 23:48:13 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
see(entity: AnimalEntity) {
|
|
|
|
if (entity.isPlayer) {
|
2022-01-30 00:24:56 -08:00
|
|
|
this.seenTarget = entity
|
2022-01-29 23:48:13 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
unsee(entity: AnimalEntity) {
|
|
|
|
if (entity.isPlayer) {
|
2022-01-30 00:24:56 -08:00
|
|
|
this.seenTarget = undefined
|
2022-01-29 23:48:13 -08:00
|
|
|
}
|
|
|
|
}
|
2022-01-30 01:18:03 -08:00
|
|
|
yell(volume: number) {
|
|
|
|
if (this.lastYellElapsed > this.nextYell) {
|
|
|
|
this.nextYell = 500 + Math.random()*3000
|
|
|
|
this.lastYellElapsed = 0
|
2022-01-30 02:27:17 -08:00
|
|
|
let v = 1 + Math.floor(Math.random() * 2)
|
2022-01-30 01:18:03 -08:00
|
|
|
if (this.isMonster) {
|
|
|
|
playSound(`monsters/evil-${this.def.name}-v${v}`, volume)
|
|
|
|
} else {
|
|
|
|
playSound(`animals/${this.def.name}-v${v}`, volume)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-01-30 06:23:50 -08:00
|
|
|
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) {
|
2022-01-30 06:42:07 -08:00
|
|
|
ctx.addEntity(new GibletEntity(`giblets.${this.sprite.spriteKey}.head.default.0`, dir+Math.random()*40, force+Math.random()*10), 'gibs', p[0], p[1])
|
2022-01-30 06:23:50 -08:00
|
|
|
}
|
|
|
|
// 44% leg gib per leg
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
if (Math.random() > 0.66) {
|
2022-01-30 06:42:07 -08:00
|
|
|
ctx.addEntity(new GibletEntity(`giblets.${this.sprite.spriteKey}.leg.default.0`, dir+Math.random()*40, force+Math.random()*10), 'gibs', p[0], p[1])
|
2022-01-30 06:23:50 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// 35% extra gibs per 8
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
if (Math.random() > 0.65) {
|
2022-01-30 06:42:07 -08:00
|
|
|
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])
|
2022-01-30 06:23:50 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
playSound(`action/splat-v${1+Math.floor(Math.random()*7)}`, 0.5)
|
|
|
|
}
|
2022-01-29 18:25:51 -08:00
|
|
|
}
|
|
|
|
|
2022-01-29 22:39:11 -08:00
|
|
|
export function isAnimalEntity(o: any): o is AnimalEntity {
|
2022-01-29 18:25:51 -08:00
|
|
|
return o.act
|
|
|
|
}
|