diff --git a/common/combat/harm.hooks.js b/common/combat/harm.hooks.js new file mode 100644 index 0000000..7de1fa8 --- /dev/null +++ b/common/combat/harm.hooks.js @@ -0,0 +1,122 @@ +import { + behaviorItemFromJSON, + buildInvoke, + buildTraversal, + Context, +} from '@avocado/behavior'; + +const bloodJson = { + rate: 0.025, + traits: { + emitted: { + params: { + alpha: { + start: 1, + end: 0.4, + }, + force: [0, 3], + velocity: { + angle: { + min: 338.5, + max: 382.5, + }, + magnitude: { + min: 0.7, + max: 0.9, + }, + }, + scale: { + start: 1, + end: 1.25, + }, + transient: false, + ttl: 0.5, + }, + }, + existent: {}, + layered: {}, + listed: {}, + perishable: { + params: { + ttl: 10, + }, + }, + positioned: {}, + primitive: { + params: { + primitives: [ + { + type: 'circle', + radius: 0.5, + line: { + rgba: [255, 0, 0], + }, + fill: { + rgba: [255, 0, 0], + }, + }, + ], + }, + }, + roomed: {}, + visible: {}, + }, +}; + +function logScaledCountedParticle(json, count) { + return buildInvoke( + ['Utility', 'merge'], + [ + buildInvoke( + ['Utility', 'makeObject'], + [ + 'count', + buildInvoke( + ['Math', 'mul'], + [ + buildInvoke( + ['Math', 'log10'], + [ + buildTraversal(['harm', 'amount']), + ], + ), + count, + ], + ), + ], + ), + json, + ], + ); +} + +export function harmInteractions() { + return [ + { + from: 'blunt', + to: 'bio', + actions: { + client: { + type: 'actions', + traversals: [ + buildInvoke(['entity', 'emitParticleJson'], [ + logScaledCountedParticle(bloodJson, 5), + ]), + buildInvoke(['from', 'playSound'], [ + buildTraversal(['from', 'harmfulSound']), + ]), + ], + }, + }, + }, + ]; +} + +export function harmTypes() { + return [ + 'blunt', + 'sharp', + 'fire', + 'ice', + ]; +} diff --git a/common/combat/harm.js b/common/combat/harm.js new file mode 100644 index 0000000..1b7aec3 --- /dev/null +++ b/common/combat/harm.js @@ -0,0 +1,32 @@ +import {behaviorItemFromJSON} from '@avocado/behavior'; +import {invokeHookFlat} from '@avocado/core'; + +let _harmInteractions; +export function harmInteractions(vulnerabilityType, harmType) { + if (!_harmInteractions) { + _harmInteractions = {}; + const interactionss = invokeHookFlat('harmInteractions'); + for (let i = 0; i < interactionss.length; i++) { + const interactions = interactionss[i]; + for (let j = 0; j < interactions.length; j++) { + const interaction = interactions[j]; + const {from, to} = interaction; + if ('undefined' === typeof _harmInteractions[from]) { + _harmInteractions[from] = {}; + } + if ('undefined' === typeof _harmInteractions[from][to]) { + _harmInteractions[from][to] = []; + } + _harmInteractions[from][to].push(interaction); + } + } + } + if ('undefined' === typeof _harmInteractions[harmType]) { + return []; + } + if ('undefined' === typeof _harmInteractions[harmType][vulnerabilityType]) { + return []; + } + return _harmInteractions[harmType][vulnerabilityType]; +} + diff --git a/common/combat/harm.packet.js b/common/combat/harm.packet.js index aa33626..36bb3de 100644 --- a/common/combat/harm.packet.js +++ b/common/combat/harm.packet.js @@ -8,11 +8,9 @@ export class HarmPacket extends Packet { data: [ { amount: 'varuint', - harmSpec: { - affinity: 'uint8', - }, from: 'uint32', isHarm: 'bool', + type: 'string', }, ] } diff --git a/common/combat/harmful.trait.js b/common/combat/harmful.trait.js index d793618..1a0fafa 100644 --- a/common/combat/harmful.trait.js +++ b/common/combat/harmful.trait.js @@ -1,6 +1,6 @@ import * as I from 'immutable'; -import {compose, merge} from '@avocado/core'; +import {compose, invokeHookFlat, iterateForEach, merge} from '@avocado/core'; import {StateProperty, Trait} from '@avocado/entity'; import {fromRad, normalizeAngleRange, Vector} from '@avocado/math'; @@ -14,7 +14,8 @@ export class Harmful extends decorate(Trait) { static defaultParams() { return { - harmfulSound: '', + harmKnockback: 500, + harmLock: 0.1, harmSpecs: [], }; } @@ -32,16 +33,19 @@ export class Harmful extends decorate(Trait) { constructor(entity, params, state) { super(entity, params, state); this._doesNotHarm = []; - const harmSpecsJSON = this.params.harmSpecs; - this._harmSpecs = harmSpecsJSON.map((harmSpec) => { + this.locks = new Map(); + this._harmSpecs = this.params.harmSpecs.map((harmSpec) => { return { - lock: 0.1, power: 0, + type: 'idfk', variance: 0.2, ...harmSpec, }; }); - this._harmfulSound = this.params.harmfulSound; + } + + destroy() { + this.locks.clear(); } doesNotHarmEntity(entity) { @@ -51,21 +55,13 @@ export class Harmful extends decorate(Trait) { return -1 !== this._doesNotHarm.indexOf(entity); } - get harmSpecs() { - return this._harmSpecs; - } - - get harmfulSound() { - return this._harmfulSound; - } - tryHarmingEntity(entity) { if (this.doesNotHarmEntity(entity)) { return; } if (entity.is('vulnerable')) { if (!entity.isInvulnerable) { - entity.takeHarmFrom(this.entity); + this.entity.harm(entity); } } } @@ -123,33 +119,53 @@ export class Harmful extends decorate(Trait) { methods() { return { - emitHarmfulParticles: (other, json = {}) => { - const diff = Vector.sub(this.entity.position, other.position); - const velocityAngle = Vector.toAngle(Vector.normalize(diff)); - const [fromAngle, toAngle] = normalizeAngleRange( - velocityAngle - Math.PI / 8, - velocityAngle + Math.PI / 8, - ); - this.entity.emitParticle('harmful', merge( - {}, - { - traits: { - emitted: { - params: { - position: other.position, - velocity: { - angle: { - min: 450 - fromRad(fromAngle), - max: 450 - fromRad(toAngle), - }, - magnitude: 0.5, - }, - }, - }, - }, - }, - json, - )); + harm: (entity) => { + if (this.locks.has(entity)) { + return; + } + this.locks.set(entity, this.params.harmLock); + for (let i = 0; i < this._harmSpecs.length; ++i) { + const {power, type, variance} = this._harmSpecs[i]; + // Give or take. + const delta = Math.random() * variance * 2 - variance; + let amount = Math.round(power + power * delta); + // Overflow. + if (power < 0) { + if (amount > 0) { + amount = 0; + } + } + else { + if (amount < 0) { + amount = 0; + } + } + amount = Math.abs(amount); + const harm = { + amount, + isHarm: power >= 0, + from: this.entity.numericUid, + type, + }; + entity.emit('tookHarm', harm); + } + // Give any knockback. + if (this.params.harmKnockback) { + if ( + entity.is('mobile') + && entity.is('positioned') + && this.entity.is('positioned') + ) { + // Yay, trig. + const difference = Vector.sub( + entity.position, + this.entity.position, + ); + const unit = Vector.normalize(difference); + const knockback = Vector.scale(unit, this.params.harmKnockback); + entity.applyMovement(knockback); + } + } }, setDoesHarm: (entity) => { @@ -176,6 +192,15 @@ export class Harmful extends decorate(Trait) { this.tryHarmingEntity(isCollidingWith[i]); } } + iterateForEach(this.locks.keys(), (key) => { + const remaining = this.locks.get(key) - elapsed; + if (remaining <= 0) { + this.locks.delete(key); + } + else { + this.locks.set(key, remaining); + } + }); } } diff --git a/common/combat/vulnerable.hooks.js b/common/combat/vulnerable.hooks.js new file mode 100644 index 0000000..fc977ec --- /dev/null +++ b/common/combat/vulnerable.hooks.js @@ -0,0 +1,8 @@ +export function vulnerabilityTypes() { + return [ + 'bio', + 'rock', + 'machine', + 'wood', + ]; +} diff --git a/common/combat/vulnerable.trait.js b/common/combat/vulnerable.trait.js index 526c806..9305170 100644 --- a/common/combat/vulnerable.trait.js +++ b/common/combat/vulnerable.trait.js @@ -5,30 +5,19 @@ import { buildTraversal, Context, } from '@avocado/behavior'; -import {flatten, invokeHookFlat, iterateForEach} from '@avocado/core'; +import {arrayUnique, flatten, invokeHookFlat} from '@avocado/core'; import {hasGraphics, TextNodeRenderer} from '@avocado/graphics'; import {randomNumber, Vector} from '@avocado/math'; +import {harmInteractions} from './harm'; import {HarmPacket} from './harm.packet'; export class Vulnerable extends Trait { static defaultParams() { - const emitHarm = buildInvoke(['entity', 'emitHarmParticles'], [ - buildTraversal(['harm']), - ]); - const playHarmfulSound = buildInvoke(['harm', 'from', 'playSound'], [ - buildTraversal(['harm', 'from', 'harmfulSound']), - ]); return { - tookHarmActions: { - type: 'actions', - traversals: [ - emitHarm, - playHarmfulSound, - ], - }, modifiers: undefined, + types: [], } } @@ -38,31 +27,19 @@ export class Vulnerable extends Trait { constructor(entity, params, state) { super(entity, params, state); - this.harms = []; + this._harms = []; this._isInvulnerable = false; - this.locks = new Map(); - this._tookHarmActions = behaviorItemFromJSON( - this.params.tookHarmActions - ); - } - - destroy() { - this.locks.clear(); } acceptHarm(harm) { + const {amount, from, isHarm, type} = harm; const context = new Context({ - harm, entity: this.entity, + from, + harm, }); - const tickingPromise = this._tookHarmActions.tickingPromise(context); - tickingPromise.then(() => { - context.destroy(); - }); - this.entity.addTickingPromise(tickingPromise); - if (harm.from) { - harm.from.emitHarmfulParticles(this.entity); - } + this.processHarmInteractions(this.entity, type, context, 'client'); + this.entity.emit('acceptedHarm', harm); } acceptPacket(packet) { @@ -80,8 +57,23 @@ export class Vulnerable extends Trait { } } + processHarmInteractions(harmed, harmType, context, side) { + const interactions = []; + const vulnerabilityTypes = harmed.vulnerabilityTypes(); + for (let i = 0; i < vulnerabilityTypes.length; i++) { + const type = vulnerabilityTypes[i]; + interactions.push(...harmInteractions(vulnerabilityTypes[i], harmType)); + } + for (let i = 0; i < interactions.length; i++) { + if (side in interactions[i].actions) { + const actions = behaviorItemFromJSON(interactions[i].actions[side]); + harmed.addTickingPromise(actions.tickingPromise(context)); + } + } + } + cleanPackets() { - this.harms = []; + this._harms = []; } harmTextSize(amount) { @@ -111,8 +103,8 @@ export class Vulnerable extends Trait { } packets(informed) { - if (this.harms.length > 0) { - return new HarmPacket(this.harms); + if (this._harms.length > 0) { + return new HarmPacket(this._harms); } } @@ -236,24 +228,7 @@ export class Vulnerable extends Trait { listeners() { return { - isDyingChanged: (_, isDying) => { - this._isInvulnerable = isDying; - }, - - tookHarm: (harm) => { - if (AVOCADO_SERVER) { - this.harms.push(harm); - this.setDirty(); - } - }, - - }; - } - - methods() { - return { - - emitHarmParticles: (harm) => { + acceptedHarm: (harm) => { const {amount, isHarm} = harm; const fill = isHarm ? '#FF0000' : '#00FF77'; this.entity.emitParticle('harm', { @@ -271,87 +246,38 @@ export class Vulnerable extends Trait { }, }, }); - this.entity.emitParticle('blood', { - rate: 0.025, - count: 8 * Math.log10(amount), - }); }, - takeHarmFrom: (entity) => { - const harmSpecs = entity.harmSpecs; - for (let i = 0; i < harmSpecs.length; ++i) { - const harmSpec = harmSpecs[i]; - if (this.locks.has(harmSpec)) { - continue; - } - this.locks.set(harmSpec, harmSpec.lock); - let power = harmSpec.power; - // Check if vulnerable to this affinity. - if (this.params.modifiers) { - if (harmSpec.affinity in this.params.modifiers) { - power *= this.params.modifiers[harmSpec.affinity]; - } - } - // Give any knockback. - if (harmSpec.knockback) { - if ( - this.entity.is('mobile') - && this.entity.is('positioned') - && entity.is('positioned') - ) { - const difference = Vector.sub( - this.entity.position, - entity.position, - ); - const unit = Vector.normalize(difference) - const knockback = Vector.scale(unit, harmSpec.knockback); - this.entity.applyMovement(knockback); - } - } - const variance = Math.random() * harmSpec.variance * 2 - harmSpec.variance; - const difference = power * variance; - // Account for variance past 0, so track if it's harm or not. - let amount = Math.round(power + difference); - let isHarm; - if (power < 0) { - isHarm = false; - if (amount > 0) { - amount = 0; - } - } - else { - isHarm = true; - if (amount < 0) { - amount = 0; - } - } - amount = Math.abs(amount); - const harm = { - isHarm, - amount, - harmSpec, - from: entity.numericUid, - }; - this.entity.emit('tookHarm', harm); + isDyingChanged: (_, isDying) => { + this._isInvulnerable = isDying; + }, + + tookHarm: (harm) => { + if (AVOCADO_SERVER) { + this._harms.push(harm); + const {amount, from, isHarm, type} = harm; + const context = new Context({ + entity: this.entity, + from: this.entity.list.findEntity(from), + harm, + }); + this.processHarmInteractions(this.entity, type, context, 'server'); + this.setDirty(); } }, }; } - tick(elapsed) { - if (AVOCADO_SERVER) { - iterateForEach(this.locks.keys(), (key) => { - const remaining = this.locks.get(key) - elapsed; - if (remaining <= 0) { - this.locks.delete(key); - } - else { - this.locks.set(key, remaining); - } - }); - } + methods() { + return { + + vulnerabilityTypes: () => { + const hookTypes = this.entity.invokeHookFlat('vulnerabilityTypes'); + return arrayUnique(hookTypes.concat(this.params.types)); + }, + + }; } } -