diff --git a/common/combat/alive.trait.js b/common/combat/alive.trait.js index 2e87ed7..b0a4ca0 100644 --- a/common/combat/alive.trait.js +++ b/common/combat/alive.trait.js @@ -18,6 +18,37 @@ export class Alive extends decorate(Trait) { deathActions: { type: 'actions', traversals: [ + { + type: 'traversal', + steps: [ + { + type: 'key', + key: 'entity', + }, + { + type: 'key', + key: 'playSound', + }, + { + type: 'invoke', + args: [ + { + type: 'traversal', + steps: [ + { + type: 'key', + key: 'entity', + }, + { + type: 'key', + key: 'deathSound', + }, + ], + }, + ], + }, + ], + }, { type: 'traversal', steps: [ @@ -74,6 +105,7 @@ export class Alive extends decorate(Trait) { }, ], }, + deathSound: 'deathSound', }; } @@ -89,15 +121,20 @@ export class Alive extends decorate(Trait) { this._context.add('entity', this.entity); const actionsJSON = this.params.get('deathActions').toJS(); this._deathActions = behaviorItemFromJSON(actionsJSON); + this._deathSound = this.params.get('deathSound'); const conditionJSON = this.params.get('deathCondition').toJS(); this._deathCondition = behaviorItemFromJSON(conditionJSON); this._isDying = false; } + get deathSound() { + return this._deathSound; + } + listeners() { return { - tookDamage: (damage) => { + tookDamage: (damage, source) => { this.entity.life -= damage.amount; // Clamp health between 0 and max. this.entity.life = Math.min( @@ -112,11 +149,19 @@ export class Alive extends decorate(Trait) { methods() { return { - die: () => { + dieIfPossible: () => { + if (this._deathCondition.check(this._context)) { + this.entity.forceDeath(); + } + }, + + forceDeath: () => { this._isDying = true; this.entity.emit('dying'); this._deathActions.on('actionsFinished', () => { - this.entity.destroy(); + if (this.entity.is('existent')) { + this.entity.destroy(); + } }); }, @@ -128,10 +173,7 @@ export class Alive extends decorate(Trait) { this._deathActions.tick(this._context, elapsed); } else { - if (this._deathCondition.check(this._context)) { - // It's a good day to die. - this.entity.die(); - } + this.entity.dieIfPossible(); } } diff --git a/common/combat/damaging.trait.js b/common/combat/damaging.trait.js index 9461618..30303ef 100644 --- a/common/combat/damaging.trait.js +++ b/common/combat/damaging.trait.js @@ -6,6 +6,7 @@ export class Damaging extends Trait { static defaultParams() { return { + damagingSound: '', damageSpecs: [], }; } @@ -22,12 +23,17 @@ export class Damaging extends Trait { ...damageSpec, }; }); + this._damagingSound = this.params.get('damagingSound'); } get damageSpecs() { return this._damageSpecs; } + get damagingSound() { + return this._damagingSound; + } + tick(elapsed) { const isCollidingWith = this.entity.isCollidingWith; for (let i = 0; i < isCollidingWith.length; ++i) { diff --git a/common/combat/vulnerable.trait.js b/common/combat/vulnerable.trait.js index 9958393..d76b3b3 100644 --- a/common/combat/vulnerable.trait.js +++ b/common/combat/vulnerable.trait.js @@ -1,12 +1,106 @@ import * as I from 'immutable'; import {Trait} from '@avocado/entity'; +import {behaviorItemFromJSON, createContext} from '@avocado/behavior'; import {hasGraphics, TextNodeRenderer} from '@avocado/graphics'; import {DamageEmitter} from './emitter'; export class Vulnerable extends Trait { + static defaultParams() { + return { + tookDamageActions: { + type: 'actions', + traversals: [ + { + type: 'traversal', + steps: [ + { + type: 'key', + key: 'entity', + }, + { + type: 'key', + key: 'emitParticle', + }, + { + type: 'invoke', + args: [ + { + type: 'literal', + value: 'damage', + }, + { + type: 'traversal', + steps: [ + { + type: 'key', + key: 'entity', + }, + { + type: 'key', + key: 'position', + }, + ], + }, + { + type: 'traversal', + steps: [ + { + type: 'key', + key: 'damage', + }, + ], + }, + ], + }, + ], + }, + { + type: 'traversal', + steps: [ + { + type: 'key', + key: 'damage', + }, + { + type: 'key', + key: 'from', + }, + { + type: 'key', + key: 'playSound', + }, + { + type: 'invoke', + args: [ + { + type: 'traversal', + steps: [ + { + type: 'key', + key: 'damage', + }, + { + type: 'key', + key: 'from', + }, + { + type: 'key', + key: 'damagingSound', + }, + ], + }, + ], + }, + ], + }, + ], + }, + } + } + static defaultState() { return { damageList: I.Map(), @@ -16,12 +110,19 @@ export class Vulnerable extends Trait { initialize() { this.damageId = 0; this.damageList = {}; + this._hasAddedEmitter = false; + this._hasAddedEmitterRenderer = false; + this._isHydrating = false; this._isInvulnerable = false; this.locks = new Map(); - if (hasGraphics) { - this.emitter = new DamageEmitter(); - this.setRenderer(); - } + this._tookDamageActionsJSON = this.params.get('tookDamageActions').toJS(); + this.tookDamageActions = []; + } + + hydrate() { + this._isHydrating = true; + this.addEmitter(); + this.addEmitterRenderer(); } patchStateStep(key, step) { @@ -35,7 +136,13 @@ export class Vulnerable extends Trait { case 'damageList': for (let i = 0; i < value.length; ++i) { const damage = value[i]; - this.emitter.emit(this.entity.position, damage); + if (this.entity.is('listed')) { + damage.from = this.entity.list.findEntity(damage.from); + } + else { + damage.from = undefined; + } + this.entity.emit('tookDamage', damage, 'client'); } break; default: @@ -44,6 +151,38 @@ export class Vulnerable extends Trait { } } + addEmitter() { + if (!this._isHydrating) { + return; + } + if (this._hasAddedEmitter) { + return; + } + if (!this.entity.is('emitter')) { + return; + } + this.entity.addEmitter('damage', new DamageEmitter()); + this._hasAddedEmitter = true; + } + + addEmitterRenderer() { + if (!this._isHydrating) { + return; + } + if (this._hasAddedEmitterRenderer) { + return; + } + if (!this.entity.is('emitter')) { + return; + } + if (!this.entity.is('staged') || !this.entity.stage) { + return; + } + const renderer = new TextNodeRenderer('.damage', this.entity.stage); + this.entity.addEmitterRenderer('damage', renderer); + this._hasAddedEmitterRenderer = true; + } + get isInvulnerable() { return this._isInvulnerable; } @@ -52,38 +191,41 @@ export class Vulnerable extends Trait { this._isInvulnerable = isInvulnerable; } - setRenderer() { - if (this.entity.is('staged') && this.entity.stage) { - const renderer = new TextNodeRenderer('.damage', this.entity.stage); - this.emitter.addRenderer(renderer); - } - } - - hooks() { - return { - - afterDestructionTickers: () => { - return (elapsed) => { - if (!hasGraphics) { - return true; - } - this.emitter.tick(elapsed); - return !this.emitter.hasParticles(); - }; - }, - - } - } - listeners() { return { + tookDamage: (damage, source) => { + if ('server' === source) { + return; + } + const context = createContext(); + context.add('entity', this.entity); + context.add('damage', damage); + const actions = behaviorItemFromJSON( + this._tookDamageActionsJSON + ); + const tuple = { + context, + actions, + }; + this.tookDamageActions.push(tuple); + actions.on('actionsFinished', () => { + const index = this.tookDamageActions.indexOf(tuple); + this.tookDamageActions.splice(tuple); + }); + }, + dying: () => { this._isInvulnerable = true; }, stageChanged: () => { - this.setRenderer(); + this.addEmitterRenderer(); + }, + + traitsChanged: () => { + this.addEmitter(); + this.addEmitterRenderer(); }, }; @@ -126,9 +268,10 @@ export class Vulnerable extends Trait { isDamage, amount, damageSpec, + from: entity.instanceUuid, }; this.damageList[entity.instanceUuid].push(damage); - this.entity.emit('tookDamage', damage); + this.entity.emit('tookDamage', damage, 'server'); } }, @@ -136,8 +279,9 @@ export class Vulnerable extends Trait { } tick(elapsed) { - if (hasGraphics) { - this.emitter.tick(elapsed); + for (let i = 0; i < this.tookDamageActions.length; ++i) { + const {context, actions} = this.tookDamageActions[i]; + actions.tick(context, elapsed); } if (this.state.get('damageList').size > 0) { this.state = this.state.set('damageList', I.Map()); diff --git a/package.json b/package.json index 13491c2..71fa250 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@avocado/physics": "1.x", "@avocado/resource": "1.x", "@avocado/server": "1.x", + "@avocado/sound": "1.x", "@avocado/state": "1.x", "@avocado/timing": "1.x", "@avocado/topdown": "1.x", diff --git a/resource/aria-math2.mp3 b/resource/aria-math2.mp3 new file mode 100755 index 0000000..63d0742 Binary files /dev/null and b/resource/aria-math2.mp3 differ diff --git a/resource/ded.wav b/resource/ded.wav new file mode 100755 index 0000000..49b3f6e Binary files /dev/null and b/resource/ded.wav differ diff --git a/resource/fire.wav b/resource/fire.wav new file mode 100755 index 0000000..3d2bc44 Binary files /dev/null and b/resource/fire.wav differ diff --git a/resource/shatter.mp3 b/resource/shatter.mp3 new file mode 100755 index 0000000..ed7f7ea Binary files /dev/null and b/resource/shatter.mp3 differ diff --git a/resource/step-grass.ogg b/resource/step-grass.ogg new file mode 100755 index 0000000..d2376be Binary files /dev/null and b/resource/step-grass.ogg differ diff --git a/server/create-server-room.js b/server/create-server-room.js index c419a30..806e8f7 100644 --- a/server/create-server-room.js +++ b/server/create-server-room.js @@ -4,6 +4,16 @@ import {Room} from '@avocado/topdown'; function fireJSON(position) { return { traits: { + audible: { + params: { + sounds: { + fire: { + src: '/fire.wav', + volume: 0.05, + }, + } + } + }, collider: { params: { isSensor: true, @@ -11,6 +21,7 @@ function fireJSON(position) { }, damaging: { params: { + damagingSound: 'fire', damageSpecs: [ { affinity: 'fire', @@ -142,6 +153,16 @@ function kittyJSON(position) { } }, }, + audible: { + params: { + sounds: { + deathSound: { + src: '/ded.wav', + volume: 0.1, + }, + } + } + }, behaved: { params: { routines: { @@ -243,6 +264,7 @@ function kittyJSON(position) { direction: 2, }, }, + emitter: {}, existent: {}, visible: {}, mobile: { diff --git a/yarn.lock b/yarn.lock index b78496c..58bdaa2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,6 +106,12 @@ dependencies: socket.io "2.2.0" +"@avocado/sound@1.x": + version "1.0.0" + resolved "https://npm.i12e.cha0s.io/@avocado%2fsound/-/sound-1.0.0.tgz#348939934234e1966dcc92bee1bae4829924c95d" + dependencies: + howler "2.1.2" + "@avocado/state@1.x": version "1.0.2" resolved "https://npm.i12e.cha0s.io/@avocado%2fstate/-/state-1.0.2.tgz#aef7928bcd512874a31567c7133759708ae6b32f" @@ -2385,6 +2391,10 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" +howler@2.1.2: + version "2.1.2" + resolved "https://npm.i12e.cha0s.io/howler/-/howler-2.1.2.tgz#8433a09d8fe84132a3e726e05cb2bd352ef8bd49" + hpack.js@^2.1.6: version "2.1.6" resolved "https://npm.i12e.cha0s.io/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"