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(), }; } initialize() { this.damageId = 0; this.damageList = {}; this._hasAddedEmitter = false; this._hasAddedEmitterRenderer = false; this._isHydrating = false; this._isInvulnerable = false; this.locks = new Map(); this._tookDamageActionsJSON = this.params.get('tookDamageActions').toJS(); this.tookDamageActions = []; } hydrate() { this._isHydrating = true; this.addEmitter(); this.addEmitterRenderer(); } patchStateStep(key, step) { if ('state' !== key) { return; } const stateKey = step.path.substr(1); const value = this.transformPatchValue(stateKey, step.value); const stateKeyParts = stateKey.split('/'); switch (stateKeyParts[0]) { case 'damageList': for (let i = 0; i < value.length; ++i) { const damage = value[i]; 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: super.patchStateStep(key, step); break; } } 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; } set isInvulnerable(isInvulnerable) { this._isInvulnerable = isInvulnerable; } 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.addEmitterRenderer(); }, traitsChanged: () => { this.addEmitter(); this.addEmitterRenderer(); }, }; } methods() { return { takeDamageFrom: (entity) => { const damageSpecs = entity.damageSpecs; for (let i = 0; i < damageSpecs.length; ++i) { const damageSpec = damageSpecs[i]; if (this.locks.has(damageSpec)) { return; } this.locks.set(damageSpec, damageSpec.lock); const variance = Math.random() * damageSpec.variance * 2 - damageSpec.variance; const difference = damageSpec.power * variance; // Account for variance past 0, so track if it's damage or not. let amount = Math.round(damageSpec.power + difference); let isDamage; if (damageSpec.power < 0) { isDamage = false; if (amount > 0) { amount = 0; } } else { isDamage = true; if (amount < 0) { amount = 0; } } amount = Math.abs(amount); if (!this.damageList[entity.instanceUuid]) { this.damageList[entity.instanceUuid] = []; } const damage = { id: this.damageId++, isDamage, amount, damageSpec, from: entity.instanceUuid, }; this.damageList[entity.instanceUuid].push(damage); this.entity.emit('tookDamage', damage, 'server'); } }, }; } 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()); } if (Object.keys(this.damageList).length > 0) { this.state = this.state.set('damageList', I.Map(this.damageList)); this.isDirty = true; this.damageList = {}; } const keys = Array.from(this.locks.keys()); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const remaining = this.locks.get(key) - elapsed; if (remaining <= 0) { this.locks.delete(key); } else { this.locks.set(key, remaining); } } } }