Browse Source

Merge branch '1935-aggro-event' into 'master'

Notify the client when entering/exiting combat

Closes #1935

See merge request Isleward/isleward!599
merge-requests/599/merge
kckckc 1 year ago
parent
commit
8779fc7b80
18 changed files with 306 additions and 408 deletions
  1. +5
    -5
      src/client/js/main.js
  2. +183
    -0
      src/client/js/rendering/floatingText.js
  3. +0
    -118
      src/client/js/rendering/numbers.js
  4. +12
    -12
      src/server/clientComponents/effects.js
  5. +2
    -2
      src/server/clientComponents/effects/auras.js
  6. +24
    -0
      src/server/components/aggro.js
  7. +6
    -5
      src/server/components/inventory/getItem.js
  8. +1
    -1
      src/server/components/player.js
  9. +28
    -39
      src/server/components/stats.js
  10. +0
    -6
      src/server/components/stats/die.js
  11. +9
    -0
      src/server/config/combatEvents.js
  12. +0
    -83
      src/server/config/effects/effectCocoon.js
  13. +0
    -18
      src/server/config/effects/effectReflectDamage.js
  14. +0
    -6
      src/server/config/spells.js
  15. +1
    -9
      src/server/config/spells/spellAmbush.js
  16. +0
    -63
      src/server/config/spells/spellCocoon.js
  17. +0
    -41
      src/server/config/spells/spellReflectDamage.js
  18. +35
    -0
      src/server/world/syncer.js

+ 5
- 5
src/client/js/main.js View File

@@ -4,7 +4,7 @@ define([
'js/rendering/renderer',
'js/objects/objects',
'js/rendering/effects',
'js/rendering/numbers',
'js/rendering/floatingText',
'js/input',
'js/system/events',
'js/resources',
@@ -19,7 +19,7 @@ define([
renderer,
objects,
effects,
numbers,
floatingText,
input,
events,
resources,
@@ -72,7 +72,7 @@ define([

await resources.init();
await components.init();
events.emit('onResourcesLoaded');

this.start();
@@ -90,7 +90,7 @@ define([
renderer.init();
input.init();

numbers.init();
floatingText.init();

uiFactory.init(null);

@@ -127,7 +127,7 @@ define([
objects.update();
renderer.update();
uiFactory.update();
numbers.update();
floatingText.update();

renderer.render();



+ 183
- 0
src/client/js/rendering/floatingText.js View File

@@ -0,0 +1,183 @@
define([
'js/system/events',
'js/objects/objects',
'js/rendering/renderer',
'js/config'
], function (
events,
objects,
renderer,
config
) {
//Create an object of the form: { elementName: elementIntegerColor, ... } from corresponding variable values.
// These variables are defiend in main.less and take the form: var(--color-element-elementName)
const elementColors = Object.fromEntries(
['default', 'arcane', 'frost', 'fire', 'holy', 'poison'].map(e => {
const variableName = `--color-element-${e}`;
const variableValue = getComputedStyle(document.documentElement).getPropertyValue(variableName);

const integerColor = `0x${variableValue.replace('#', '')}`;

return [e, integerColor];
})
);

const shouldShowEvent = msg => {
const { target, source, targetMaster, sourceMaster } = msg;
const { player: { id } } = window;

const shouldShow = (
(id === target) ||
(id === source) ||
(id === targetMaster) ||
(id === sourceMaster)
);

return shouldShow;
};

const getObjectPosition = options => {
let { targetId } = options;

if (typeof targetId === 'undefined')
targetId = window.player.id;

//Find target from id
const target = objects.objects.find(o => o.id === targetId);
if (!target || !target.isVisible)
return { success: false };

return { x: target.x, y: target.y, success: true };
};

return {
list: [],

init: function () {
events.on('onGetDamage', this.onGetDamage.bind(this));
events.on('onCombatEvent', this.onCombatEvent.bind(this));
},

onGetDamage: function (msg) {
if (config.damageNumbers === 'off' || !shouldShowEvent(msg))
return;

const { target, amount, crit, heal, element = 'default' } = msg;

const div = ((~~(amount * 10) / 10) > 0) ? 10 : 100;
const text = (heal ? '+' : '') + (~~(amount * div) / div);

const colorVariableName = config.damageNumbers === 'element' ? element : 'default';

const textConfig = {
targetId: target,
text,
color: elementColors[colorVariableName],
fontSize: crit ? 22 : 18,
tileOffsetX: heal ? -1 : 1,
tileOffsetY: 0.75,
floatDirection: -1
};

this.build(textConfig);
},

onCombatEvent: function (msg) {
if (!msg.floatingText)
return;

if (msg.type === 'damageAvoided' && !shouldShowEvent(msg))
return;

const textConfig = {
targetId: msg.target,
text: msg.floatingText,
tileOffsetX: 0,
tileOffsetY: 1.25,
floatDirection: 1
};

this.build(textConfig);
},

//Options:
// text
// color -- Color string in the form of "0xFFFFFF". Defaults to some gray
// fontSize -- Defaults to 18
// x, y -- Position to create the text at
// targetId -- Create the text at this object if no x or y is provided. Defaults to the current player
// tileOffsetX -- Offset in tiles. Defaults to (1, 0.75)
// tileOffsetY
// ttl -- Defaults to 35
// floatDirection -- Defaults to -1 (up)
build: function (options) {
let { x, y } = options;
if (typeof x === 'undefined' || typeof y === 'undefined') {
let success;
({ x, y, success } = getObjectPosition(options));

if (!success)
return;
}

const {
text,
tileOffsetX = 1,
tileOffsetY = 0.75,
ttl = 35,
fontSize = 18,
floatDirection = -1,
color = elementColors.default
} = options;

const textObj = {
text,
x: (x * scale) + (tileOffsetX * scale),
y: (y * scale) + (tileOffsetY * scale),
ttl,
ttlMax: ttl,
floatDirection
};

textObj.sprite = renderer.buildText({
fontSize,
layerName: 'effects',
x: textObj.x,
y: textObj.y,
text,
color
});

this.list.push(textObj);
},

update: function () {
const list = this.list;
let lLen = list.length;

for (let i = 0; i < lLen; i++) {
const l = list[i];
l.ttl--;

if (l.ttl === 0) {
renderer.destroyObject({
layerName: 'effects',
sprite: l.sprite
});
list.splice(i, 1);
i--;
lLen--;
continue;
}

l.y += l.floatDirection;

const alpha = l.ttl / l.ttlMax;

l.sprite.x = ~~(l.x / scaleMult) * scaleMult;
l.sprite.y = ~~(l.y / scaleMult) * scaleMult;
l.sprite.alpha = alpha;
}
}
};
});

+ 0
- 118
src/client/js/rendering/numbers.js View File

@@ -1,118 +0,0 @@
define([
'js/system/events',
'js/objects/objects',
'js/rendering/renderer',
'js/config'
], function (
events,
objects,
renderer,
config
) {
//Create an object of the form: { elementName: elementIntegerColor, ... } from corresponding variable values.
// These variables are defiend in main.less and take the form: var(--color-element-elementName)
const elementColors = Object.fromEntries(
['default', 'arcane', 'frost', 'fire', 'holy', 'poison'].map(e => {
const variableName = `--color-element-${e}`;
const variableValue = getComputedStyle(document.documentElement).getPropertyValue(variableName);

const integerColor = `0x${variableValue.replace('#', '')}`;

return [e, integerColor];
})
);

return {
list: [],

init: function () {
events.on('onGetDamage', this.onGetDamage.bind(this));
},

onGetDamage: function (msg) {
if (config.damageNumbers === 'off')
return;

let target = objects.objects.find(function (o) {
return (o.id === msg.id);
});
if (!target || !target.isVisible)
return;

let ttl = 35;

let numberObj = {
obj: target,
amount: msg.amount,
x: (target.x * scale),
y: (target.y * scale) + scale - (scale / 4),
ttl: ttl,
ttlMax: ttl,
event: msg.event,
text: msg.text,
crit: msg.crit,
heal: msg.heal,
element: msg.element
};

if (numberObj.event)
numberObj.y += (scale / 2);
else if (numberObj.heal)
numberObj.x -= scale;
else
numberObj.x += scale;

let text = numberObj.text;
if (!numberObj.event) {
let amount = numberObj.amount;
let div = ((~~(amount * 10) / 10) > 0) ? 10 : 100;
text = (numberObj.heal ? '+' : '') + (~~(amount * div) / div);
}

const colorVariableName = config.damageNumbers === 'element' ? numberObj.element : 'default';
numberObj.sprite = renderer.buildText({
fontSize: numberObj.crit ? 22 : 18,
layerName: 'effects',
x: numberObj.x,
y: numberObj.y,
text: text,
color: elementColors[colorVariableName]
});

this.list.push(numberObj);
},

update: function () {
let list = this.list;
let lLen = list.length;

for (let i = 0; i < lLen; i++) {
let l = list[i];
l.ttl--;

if (l.ttl === 0) {
renderer.destroyObject({
layerName: 'effects',
sprite: l.sprite
});
list.splice(i, 1);
i--;
lLen--;
continue;
}

if (l.event)
l.y += 1;
else
l.y -= 1;

let alpha = l.ttl / l.ttlMax;

l.sprite.x = ~~(l.x / scaleMult) * scaleMult;
l.sprite.y = ~~(l.y / scaleMult) * scaleMult;
l.sprite.alpha = alpha;
}
}
};
});

+ 12
- 12
src/server/clientComponents/effects.js View File

@@ -1,9 +1,9 @@
define([
'js/system/events',
'js/rendering/numbers'
'js/rendering/floatingText'
], function (
events,
numbers
floatingText
) {
const defaultBuffIcons = {
stunned: [4, 0]
@@ -11,7 +11,7 @@ define([

const effectBase = {
init: function () {
this.defaultDamageText(false);
this.showDefaultCombatEvent(false);

if (this.self && defaultBuffIcons[this.type]) {
events.emit('onGetEffectIcon', {
@@ -23,7 +23,7 @@ define([

destroy: function () {
if (!this.obj.destroyed)
this.defaultDamageText(true);
this.showDefaultCombatEvent(true);

if (this.self && defaultBuffIcons[this.type]) {
events.emit('onRemoveEffectIcon', {
@@ -32,11 +32,11 @@ define([
}
},

defaultDamageText: function (removing) {
numbers.onGetDamage({
id: this.obj.id,
event: true,
text: (removing ? '-' : '+') + this.type
showDefaultCombatEvent: function (removing) {
floatingText.onCombatEvent({
target: this.obj.id,
type: 'effect',
floatingText: (removing ? '-' : '+') + this.type
});
}
};
@@ -47,7 +47,7 @@ define([
effects: [],

templates: {
},

init: function (blueprint) {
@@ -64,7 +64,7 @@ define([

if (effect.init)
effect.init();
return effect;
},

@@ -97,7 +97,7 @@ define([
if (effect.extend)
effect.extend(u.data);
else {
for (let p in u.data)
for (let p in u.data)
effect[p] = u.data[p];
}
});


+ 2
- 2
src/server/clientComponents/effects/auras.js View File

@@ -50,7 +50,7 @@ define([
cell: cell
});

this.defaultDamageText();
this.showDefaultCombatEvent();

if (this.self && buffIcons[type]) {
events.emit('onGetEffectIcon', {
@@ -113,7 +113,7 @@ define([
sprite: this.sprite
});

this.defaultDamageText(true);
this.showDefaultCombatEvent(true);

if (this.self && buffIcons[type]) {
events.emit('onRemoveEffectIcon', {


+ 24
- 0
src/server/components/aggro.js View File

@@ -1,3 +1,5 @@
const combatEvents = require('../config/combatEvents');

const configThreatCeiling = {
regular: 1,
rare: 0.5
@@ -21,6 +23,8 @@ module.exports = {
//Certain summoned minions need to despawn when they lose their last target
dieOnAggroClear: false,

inCombat: false,

init: function (blueprint) {
this.physics = this.obj.instance.physics;

@@ -227,6 +231,8 @@ module.exports = {
if (exists.threat > this.threatCeiling)
exists.threat = this.threatCeiling;

this.updateInCombat();

return true;
},

@@ -294,6 +300,8 @@ module.exports = {

if ((this.list.length === 0) && (this.obj.mob) && (!this.obj.follower))
this.obj.stats.resetHp();

this.updateInCombat();
},

sortThreat: function () {
@@ -406,5 +414,21 @@ module.exports = {

isInCombat: function () {
return this.list.length > 0;
},

updateInCombat: function () {
const prev = this.inCombat;
this.inCombat = this.isInCombat();

if (!this.obj.player)
return;

if (prev !== this.inCombat) {
this.obj.instance.syncer.queueInRange('onCombatEvent', {
type: combatEvents.aggro,
id: this.obj.id,
inCombat: this.inCombat
}, this.obj.id);
}
}
};

+ 6
- 5
src/server/components/inventory/getItem.js View File

@@ -1,5 +1,6 @@
const events = require('../../misc/events');
const { isItemStackable } = require('./helpers');
const combatEvents = require('../../config/combatEvents');

const getNextId = items => {
let id = 0;
@@ -101,11 +102,11 @@ module.exports = (cpnInv, item, hideMessage, noStack, hideAlert, createBagIfFull
}];

if (!hideAlert) {
obj.instance.syncer.queue('onGetDamage', {
id: obj.id,
event: true,
text: 'loot'
}, -1);
obj.instance.syncer.queue('onCombatEvent', {
target: obj.id,
type: combatEvents.loot,
floatingText: 'loot'
}, [obj.serverId]);
}

if (!hideMessage) {


+ 1
- 1
src/server/components/player.js View File

@@ -137,7 +137,7 @@ module.exports = {
},

hasSeen: function (id) {
return (this.seen.indexOf(id) > -1);
return this.seen.includes(id);
},

see: function (id) {


+ 28
- 39
src/server/components/stats.js View File

@@ -4,6 +4,7 @@ const die = require('./stats/die');
let animations = require('../config/animations');
let spirits = require('../config/spirits');
let scheduler = require('../misc/scheduler');
const combatEvents = require('../config/combatEvents');

let baseStats = {
mana: 20,
@@ -278,11 +279,11 @@ module.exports = {

obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);

this.syncer.queue('onGetDamage', {
id: obj.id,
event: true,
text: '+' + amount + ' xp'
}, -1);
this.syncer.queue('onCombatEvent', {
target: obj.id,
type: combatEvents.xp,
floatingText: '+' + amount + ' xp'
}, [obj.serverId]);

let syncO = {};
let didLevelUp = false;
@@ -306,11 +307,11 @@ module.exports = {

obj.spellbook.calcDps();

this.syncer.queue('onGetDamage', {
id: obj.id,
event: true,
text: 'level up'
}, -1);
this.syncer.queue('onCombatEvent', {
target: obj.id,
type: combatEvents.levelUp,
floatingText: 'level up'
}, [obj.serverId]);

syncO.level = values.level;

@@ -537,7 +538,7 @@ module.exports = {
damage.dealt = amount;

let msg = {
id: obj.id,
target: obj.id,
source: source.id,
crit: damage.crit,
amount: amount,
@@ -545,34 +546,22 @@ module.exports = {
};

this.values.hp -= amount;
let recipients = [];
if (obj.serverId)
recipients.push(obj.serverId);
if (source.serverId)
recipients.push(source.serverId);

if (source.follower && source.follower.master.serverId) {
recipients.push(source.follower.master.serverId);
msg.masterSource = source.follower.master.id;
}
if (obj.follower && obj.follower.master.serverId) {
recipients.push(obj.follower.master.serverId);
msg.masterId = obj.follower.master.id;
}
if (source.follower)
msg.sourceMaster = source.follower.master.id;

if (recipients.length) {
if (!damage.blocked && !damage.dodged)
this.syncer.queue('onGetDamage', msg, recipients);
else {
this.syncer.queue('onGetDamage', {
id: obj.id,
source: source.id,
event: true,
text: damage.blocked ? 'blocked' : 'dodged'
}, recipients);
}
}
if (obj.follower)
msg.targetMaster = obj.follower.master.id;

if (damage.blocked || damage.dodged) {
this.syncer.queueInRange('onCombatEvent', {
source: source.id,
target: obj.id,
type: combatEvents.damageAvoided,
floatingText: damage.blocked ? 'blocked' : 'dodged'
}, obj.id, source.id);
} else
this.syncer.queueInRange('onGetDamage', msg, obj.id, source.id);

obj.aggro.tryEngage(source, amount, threatMult);

@@ -624,14 +613,14 @@ module.exports = {
if (source.serverId)
recipients.push(source.serverId);
if (recipients.length > 0) {
this.syncer.queue('onGetDamage', {
this.syncer.queueInRange('onGetDamage', {
id: this.obj.id,
source: source.id,
heal: true,
amount: amount,
crit: heal.crit,
element: heal.element
}, recipients);
}, this.obj.id, source.id);
}

//Add aggro to all our attackers


+ 0
- 6
src/server/components/stats/die.js View File

@@ -26,12 +26,6 @@ const die = (cpnStats, deathSource) => {
const { obj, syncer: syncerGlobal } = cpnStats;
const { x, y, serverId, syncer } = obj;

syncerGlobal.queue('onGetDamage', {
id: obj.id,
event: true,
text: 'death'
}, -1);

syncer.set(true, null, 'dead', true);

const syncO = syncer.o;


+ 9
- 0
src/server/config/combatEvents.js View File

@@ -0,0 +1,9 @@
const types = {
damageAvoided: 'damageAvoided',
loot: 'loot',
xp: 'xp',
levelUp: 'levelUp',
aggro: 'aggro'
};

module.exports = types;

+ 0
- 83
src/server/config/effects/effectCocoon.js View File

@@ -1,83 +0,0 @@
module.exports = {
type: 'cocoon',
cocoon: null,

persist: true,

init: function (source) {
let obj = this.obj;
let syncO = obj.syncer.o;

obj.hidden = true;
obj.nonSelectable = true;
syncO.hidden = true;
syncO.nonSelectable = true;

this.cocoon = obj.instance.objects.buildObjects([{
name: 'cocoon',
sheetName: 'objects',
cell: 54,
x: obj.x,
y: obj.y,
properties: {
cpnAggro: {
faction: source.aggro.faction
},
cpnStats: {
values: {
hpMax: 10,
hp: 10
}
},
cpnEffects: {}
}
}]);

this.cocoon.effects.addEffect({
events: {
afterDeath: this.onDestroyCocoon.bind(this)
}
});
},

onDestroyCocoon: function () {
this.destroyed = true;
},

destroy: function () {
let obj = this.obj;
let syncO = obj.syncer.o;

obj.hidden = false;
obj.nonSelectable = false;
syncO.hidden = false;
syncO.nonSelectable = false;

this.cocoon.destroyed = true;
},

simplify: function () {
return {
type: 'cocoon',
ttl: this.ttl
};
},

events: {
beforeMove: function (targetPos) {
let obj = this.obj;

targetPos.x = obj.x;
targetPos.y = obj.y;
},

beforeDealDamage: function (damage) {
if (damage)
damage.failed = true;
},

beforeCastSpell: function (successObj) {
successObj.success = false;
}
}
};

+ 0
- 18
src/server/config/effects/effectReflectDamage.js View File

@@ -1,18 +0,0 @@
module.exports = {
type: 'reflectDamage',

events: {
beforeTakeDamage: function (damage, source) {
damage.amount *= 0.5;
source.stats.takeDamage(damage, this.threatMult, this.obj);

damage.failed = true;

this.obj.instance.syncer.queue('onGetDamage', {
id: this.obj.id,
event: true,
text: 'reflect'
}, -1);
}
}
};

+ 0
- 6
src/server/config/spells.js View File

@@ -283,12 +283,6 @@ let spells = [{
description: 'Charges at a foe, dealing damage and stunning them for a short period.',
icon: [3, 1],
animation: 'raiseShield'
}, {
name: 'Reflect Damage',
type: 'reflectdamage',
description: 'Gain an ethereal shield that reflects damage until the buff wears off.',
icon: [3, 2],
animation: 'raiseShield'
}, {
name: 'Flurry',
type: 'flurry',


+ 1
- 9
src/server/config/spells/spellAmbush.js View File

@@ -103,19 +103,11 @@ module.exports = {
targetPos.y += offsetY;
}

let targetEffect = target.effects.addEffect({
target.effects.addEffect({
type: 'stunned',
ttl: this.stunDuration
});

if (targetEffect) {
this.obj.instance.syncer.queue('onGetDamage', {
id: target.id,
event: true,
text: 'stunned'
}, -1);
}

if (this.animation) {
this.obj.instance.syncer.queue('onGetObject', {
id: this.obj.id,


+ 0
- 63
src/server/config/spells/spellCocoon.js View File

@@ -1,63 +0,0 @@
module.exports = {
type: 'cocoon',

cdMax: 7,
manaCost: 0,

range: 9,

speed: 200,
damage: 0,

needLos: true,

ttl: 75,

cast: function (action) {
let obj = this.obj;
let target = action.target;

let ttl = Math.sqrt(Math.pow(target.x - obj.x, 2) + Math.pow(target.y - obj.y, 2)) * this.speed;

this.sendAnimation({
caster: this.obj.id,
components: [{
idSource: this.obj.id,
idTarget: target.id,
type: 'projectile',
ttl: ttl,
particles: this.particles
}, {
type: 'attackAnimation',
layer: 'projectiles',
loop: -1,
row: this.row,
col: this.col
}]
});

this.sendBump(target);

this.queueCallback(this.explode.bind(this, target), ttl);

return true;
},
explode: function (target) {
if (this.obj.destroyed)
return;

target.effects.addEffect({
type: 'cocoon',
ttl: this.ttl,
source: this.obj
});

this.obj.instance.syncer.queue('onGetDamage', {
id: target.id,
event: true,
text: 'cocooned'
}, -1);

target.aggro.tryEngage(this.obj, this.damage, this.threatMult);
}
};

+ 0
- 41
src/server/config/spells/spellReflectDamage.js View File

@@ -1,41 +0,0 @@
module.exports = {
type: 'reflectDamage',

cdMax: 0,
manaCost: 0,

duration: 10,

targetGround: true,

cast: function (action) {
let selfEffect = this.obj.effects.addEffect({
type: 'reflectDamage',
threatMult: this.threatMult
});

let ttl = this.duration * consts.tickTime;

if (this.animation) {
this.obj.instance.syncer.queue('onGetObject', {
id: this.obj.id,
components: [{
type: 'animation',
template: this.animation
}]
}, -1);
}

this.queueCallback(this.endEffect.bind(this, selfEffect), ttl - 50);

return true;
},
endEffect: function (selfEffect) {
if (this.obj.destroyed)
return;

let obj = this.obj;

obj.effects.removeEffect(selfEffect.id);
}
};

+ 35
- 0
src/server/world/syncer.js View File

@@ -155,6 +155,9 @@ module.exports = {
this.send();
},

//Queues an event of type `event` with data `obj` to a list of players.
// `to` is an array of the serverIds of players to send the event to.
// If `to` is -1, the event will be sent to all players in the zone.
queue: function (event, obj, to) {
//Send to all players in zone?
if (to === -1) {
@@ -175,6 +178,38 @@ module.exports = {
});
},

//Queues an event to all players who are close enough.
// `eventTarget` and `eventSource` are optional object IDs.
// If a player has seen either object, they are sent the event.
// We pass object IDs instead of serverIds because we check
// player.hasSeen which checks a list of object IDs.
queueInRange: function (event, obj, eventTarget, eventSource) {
const to = [];
const oLen = this.objects.objects.length;
for (let i = 0; i < oLen; i++) {
const o = this.objects.objects[i];

const isMatch = (
o.player &&
(
(
eventTarget &&
o.player.hasSeen(eventTarget)
) ||
(
eventSource &&
o.player.hasSeen(eventSource)
)
)
);

if (isMatch)
to.push(o.serverId);
}

this.queue(event, obj, to);
},

flushForTarget: function (targetServerId) {
const buffer = this.buffer;



Loading…
Cancel
Save