refactor: harm interactions!

This commit is contained in:
cha0s 2020-01-17 02:30:03 -06:00
parent e6f5046b55
commit 762820dc7c
6 changed files with 282 additions and 171 deletions

122
common/combat/harm.hooks.js Normal file
View File

@ -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',
];
}

32
common/combat/harm.js Normal file
View File

@ -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];
}

View File

@ -8,11 +8,9 @@ export class HarmPacket extends Packet {
data: [
{
amount: 'varuint',
harmSpec: {
affinity: 'uint8',
},
from: 'uint32',
isHarm: 'bool',
type: 'string',
},
]
}

View File

@ -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);
}
});
}
}

View File

@ -0,0 +1,8 @@
export function vulnerabilityTypes() {
return [
'bio',
'rock',
'machine',
'wood',
];
}

View File

@ -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));
},
};
}
}