Browse Source

merged master for v0.12.0

tags/v0.12.0
Shaun 1 year ago
parent
commit
9e12f8c6c8
100 changed files with 1841 additions and 981 deletions
  1. +1
    -1
      src/client/index.html
  2. +6
    -0
      src/client/js/objects/objects.js
  3. +5
    -3
      src/client/js/rendering/lightningBuilder.js
  4. +194
    -83
      src/client/js/rendering/numbers.js
  5. +22
    -12
      src/client/js/rendering/renderer.js
  6. +58
    -55
      src/client/js/rendering/shaders/outline.js
  7. +31
    -18
      src/client/js/rendering/shaders/outline/frag.js
  8. +20
    -4
      src/client/js/rendering/shaders/outline/vert.js
  9. +1
    -1
      src/client/package.json
  10. +7
    -2
      src/client/ui/factory.js
  11. +0
    -1
      src/client/ui/templates/hud/styles.less
  12. +2
    -2
      src/client/ui/templates/login/template.html
  13. +4
    -2
      src/client/ui/templates/messages/messages.js
  14. +3
    -1
      src/client/ui/templates/messages/mobile.js
  15. +9
    -2
      src/client/ui/templates/messages/styles.less
  16. +16
    -14
      src/client/ui/templates/party/party.js
  17. +10
    -2
      src/client/ui/templates/tooltipItem/buildTooltip/lineBuilders.js
  18. +3
    -3
      src/client/ui/templates/tooltipItem/buildTooltip/stringifyStatValue.js
  19. +1
    -1
      src/client/ui/templates/tooltipItem/styles.less
  20. +1
    -1
      src/client/ui/templates/trade/trade.js
  21. +314
    -183
      src/server/.eslintrc
  22. +0
    -3
      src/server/clientComponents/.eslintrc
  23. +2
    -0
      src/server/clientComponents/effects.js
  24. +2
    -0
      src/server/clientComponents/effects/auras.js
  25. +7
    -8
      src/server/clientComponents/inventory.js
  26. +22
    -13
      src/server/clientComponents/lightningEffect.js
  27. +2
    -0
      src/server/clientComponents/pather.js
  28. +4
    -0
      src/server/clientComponents/reputation.js
  29. +2
    -0
      src/server/clientComponents/spellbook.js
  30. +8
    -5
      src/server/combat/scale.js
  31. +14
    -5
      src/server/components/aggro.js
  32. +1
    -1
      src/server/components/effects.js
  33. +2
    -20
      src/server/components/equipment.js
  34. +4
    -2
      src/server/components/extensions/socialCommands.js
  35. +10
    -0
      src/server/components/follower.js
  36. +1
    -1
      src/server/components/gatherer.js
  37. +58
    -54
      src/server/components/inventory.js
  38. +17
    -7
      src/server/components/inventory/dropBag.js
  39. +37
    -0
      src/server/components/inventory/simplifyItem.js
  40. +10
    -1
      src/server/components/inventory/useItem.js
  41. +10
    -11
      src/server/components/mob.js
  42. +10
    -30
      src/server/components/reputation.js
  43. +7
    -1
      src/server/components/social.js
  44. +5
    -1
      src/server/components/social/startEvent.js
  45. +5
    -1
      src/server/components/social/stopEvent.js
  46. +7
    -120
      src/server/components/spellbook.js
  47. +131
    -0
      src/server/components/spellbook/cast.js
  48. +64
    -118
      src/server/components/stats.js
  49. +84
    -0
      src/server/components/stats/takeDamage.js
  50. +6
    -0
      src/server/config/consts.js
  51. +0
    -3
      src/server/config/effects/effectChampion.js
  52. +1
    -1
      src/server/config/effects/effectCocoon.js
  53. +6
    -2
      src/server/config/effects/effectHolyVengeance.js
  54. +2
    -2
      src/server/config/effects/effectInvulnerability.js
  55. +28
    -12
      src/server/config/effects/effectLifeDrain.js
  56. +9
    -2
      src/server/config/effects/effectReflectDamage.js
  57. +1
    -1
      src/server/config/effects/effectStunned.js
  58. +8
    -2
      src/server/config/factions/akarei.js
  59. +30
    -0
      src/server/config/factions/helpers.js
  60. +107
    -26
      src/server/config/itemEffects/castSpellOnHit.js
  61. +9
    -4
      src/server/config/itemEffects/damageSelf.js
  62. +1
    -1
      src/server/config/itemEffects/freezeOnHit.js
  63. +7
    -3
      src/server/config/itemEffects/gainStat.js
  64. +7
    -3
      src/server/config/itemEffects/healOnCrit.js
  65. +0
    -19
      src/server/config/maps/mapList.js
  66. +2
    -2
      src/server/config/prophecies/crushable.js
  67. +6
    -4
      src/server/config/quests/questBuilder.js
  68. +1
    -1
      src/server/config/serverConfig.js
  69. +8
    -1
      src/server/config/spells/spellAmbush.js
  70. +6
    -2
      src/server/config/spells/spellArcaneBarrier.js
  71. +8
    -1
      src/server/config/spells/spellChainLightning.js
  72. +20
    -5
      src/server/config/spells/spellCharge.js
  73. +23
    -3
      src/server/config/spells/spellFireblast.js
  74. +6
    -2
      src/server/config/spells/spellHealingCircle.js
  75. +8
    -1
      src/server/config/spells/spellIceSpear.js
  76. +8
    -1
      src/server/config/spells/spellMelee.js
  77. +8
    -1
      src/server/config/spells/spellProjectile.js
  78. +12
    -2
      src/server/config/spells/spellSingleTargetHeal.js
  79. +8
    -1
      src/server/config/spells/spellSlash.js
  80. +8
    -1
      src/server/config/spells/spellSlowBlast.js
  81. +8
    -1
      src/server/config/spells/spellSmite.js
  82. +11
    -3
      src/server/config/spells/spellSmokeBomb.js
  83. +7
    -3
      src/server/config/spells/spellSummonConsumableFollower.js
  84. +2
    -1
      src/server/config/spells/spellTemplate.js
  85. +10
    -2
      src/server/config/spells/spellTrailDash.js
  86. +8
    -1
      src/server/config/spells/spellWarnBlast.js
  87. +8
    -2
      src/server/config/spells/spellWhirlwind.js
  88. +0
    -11
      src/server/config/zoneBase.js
  89. +30
    -4
      src/server/db/ioRethink.js
  90. +5
    -6
      src/server/db/tableNames.js
  91. +9
    -4
      src/server/events/events.js
  92. +1
    -1
      src/server/events/phases/phaseKillMob.js
  93. +61
    -1
      src/server/fixes/fixes.js
  94. +19
    -16
      src/server/index.js
  95. +8
    -0
      src/server/items/generator.js
  96. +39
    -16
      src/server/items/generators/effects.js
  97. +1
    -1
      src/server/items/generators/spellbook.js
  98. +4
    -1
      src/server/items/generators/types.js
  99. +10
    -2
      src/server/items/salvager.js
  100. +12
    -3
      src/server/mods/class-necromancer/spells/spellBloodBarrier.js

+ 1
- 1
src/client/index.html View File

@@ -11,7 +11,7 @@
<link rel="stylesheet" href="css/main.css">
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="js/system/addons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/6.1.2/browser/pixi.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/6.5.9/browser/pixi.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js" data-main="js/app"></script>
<script>if (window.module) module = window.module;</script>
<link rel="icon" href="images/favicon.ico">


+ 6
- 0
src/client/js/objects/objects.js View File

@@ -144,6 +144,9 @@ define([
});
}

if (template.filters && obj.sprite)
renderer.addFilter(obj.sprite, template.filters[0]);

//We need to set visibility before components kick in as they sometimes need access to isVisible
obj.updateVisibility();

@@ -241,6 +244,9 @@ define([
if ((!obj.sprite) && (template.sheetName))
obj.sprite = renderer.buildObject(obj);

if (template.filters && !obj.sprite?.filters?.length)
renderer.addFilter(obj.sprite, template.filters[0]);

if (template.name) {
if (obj.nameSprite)
renderer.destroyObject({ sprite: obj.nameSprite });


+ 5
- 3
src/client/js/rendering/lightningBuilder.js View File

@@ -17,9 +17,11 @@ define([
let tx = config.toX * scale;
let ty = config.toY * scale;

const linkSize = config.linkSize ?? 3;

let angle = Math.atan2(ty - fy, tx - fx);
let distance = Math.sqrt(Math.pow(tx - fx, 2) + Math.pow(ty - fy, 2));
let divDistance = Math.min(20, distance);
let divDistance = config.divDistance ?? Math.min(20, distance);
let divisions = config.divisions || Math.max(1, distance / divDistance);

let x = fx;
@@ -75,8 +77,8 @@ define([
lightPatch.tint = '0xffffff';
lightPatch.x = ~~((xx - scaleMult) / scaleMult) * scaleMult;
lightPatch.y = ~~((yy - scaleMult) / scaleMult) * scaleMult;
lightPatch.width = scaleMult * 3;
lightPatch.height = scaleMult * 3;
lightPatch.width = scaleMult * linkSize;
lightPatch.height = scaleMult * linkSize;

lightPatch.blendMode = PIXI.BLEND_MODES.ADD;



+ 194
- 83
src/client/js/rendering/numbers.js View File

@@ -9,8 +9,28 @@ define([
renderer,
config
) {
//Defaults
const MOVE_SPEED = 0.5;

const TTL = 35;
const FONT_SIZE = 18;
const FONT_SIZE_CRIT = 22;
const LAYER_NAME = 'effects';

const PADDING = scaleMult;

const POSITION = {
BOTTOM_CENTER: 0,
LEFT_BOTTOM: 1,
RIGHT_BOTTOM: 2,
TOP_CENTER: 3
};

//Internals
const list = [];

//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)
// These variables are defined 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}`;
@@ -22,97 +42,188 @@ define([
})
);

return {
list: [],
//Helpers
const getColor = ({ color, element }) => {
if (color)
return color;

return elementColors[element];
};

const getText = ({ amount, text, heal }) => {
if (amount === undefined)
return text;

const div = ((~~(amount * 10) / 10) > 0) ? 10 : 100;
let result = ~~(amount * div) / div;

if (heal)
result = `+${result}`;

return result;
};

const getPosition = ({ position, event: isEvent, heal }) => {
if (position)
return position;

//Events render under the target, centered
if (isEvent)
return POSITION.BOTTOM_CENTER;
else if (heal)
return POSITION.LEFT_BOTTOM;

return POSITION.RIGHT_BOTTOM;
};

const getXY = (msg, position, sprite) => {
let x = 0;
let y = 0;

if (position === POSITION.TOP_CENTER) {
x = (scale / 2) - (sprite.width / 2);
y = -PADDING - sprite.height;
} else if (position === POSITION.BOTTOM_CENTER) {
x = (scale / 2) - (sprite.width / 2);
y = scale + PADDING;
} else if (position === POSITION.RIGHT_BOTTOM) {
x = scale;
y = scale - sprite.height + (scaleMult * 2);
} else if (position === POSITION.LEFT_BOTTOM) {
x = -sprite.width - PADDING;
y = scale - sprite.height + (scaleMult * 2);
}

return { x, y };
};

init: function () {
events.on('onGetDamage', this.onGetDamage.bind(this));
},
const getMovementDelta = ({ movementDelta }, position) => {
if (movementDelta)
return movementDelta;

onGetDamage: function (msg) {
if (config.damageNumbers === 'off')
if (position === POSITION.BOTTOM_CENTER)
return [0, 1];

return [0, -1];
};

const getFontSize = ({ fontSize, crit }) => {
if (fontSize)
return fontSize;
else if (crit)
return FONT_SIZE_CRIT;

return FONT_SIZE;
};

//Events
const onGetDamage = msg => {
const { ttl = TTL } = msg;

if (config.damageNumbers === 'off')
return;

const target = objects.objects.find(o => o.id === msg.id);
if (!target || !target.isVisible)
return;

const sprite = renderer.buildText({
fontSize: getFontSize(msg),
layerName: LAYER_NAME,
text: getText(msg),
color: getColor(msg),
visible: false
});

const position = getPosition(msg);
const movementDelta = getMovementDelta(msg, position);
const { x, y } = getXY(msg, position, sprite);

sprite.x = (target.x * scale) + x;
sprite.y = (target.y * scale) + y;
sprite.visible = true;

const numberObj = {
obj: target,
x,
y,
ttl,
ttlMax: ttl,
movementDelta,
sprite
};

list.push(numberObj);
};

//Call this method from inside update to generate test numbers
// around the player
/* eslint-disable-next-line no-unused-vars */
const test = () => {
objects.objects.forEach(o => {
if (!o.player)
return;

let target = objects.objects.find(function (o) {
return (o.id === msg.id);
const amount = Math.random() < 0.5 ? ~~(Math.random() * 100) : undefined;
const isEvent = amount ? false : Math.random() < 0.5;
const text = amount ? undefined : 'text';
const heal = Math.random() < 0.5;
let position;
if (!amount)
position = Math.random() < 0.5 ? POSITION.TOP_CENTER : POSITION.BOTTOM_CENTER;
const element = ['default', 'arcane', 'frost', 'fire', 'holy', 'poison'][~~(Math.random() * 6)];
const crit = amount > 50;

onGetDamage({
id: o.id,
event: isEvent,
text,
amount,
element,
heal,
position,
crit
});
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 update = () => {
let lLen = list.length;
for (let i = 0; i < lLen; i++) {
const l = list[i];

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]
});
l.ttl--;
if (l.ttl === 0) {
list.splice(i, 1);
lLen--;

renderer.destroyObject({
layerName: 'effects',
sprite: l.sprite
});

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;
continue;
}

l.x += l.movementDelta[0] * MOVE_SPEED;
l.y += l.movementDelta[1] * MOVE_SPEED;

l.sprite.x = (l.obj.x * scale) + l.x;
l.sprite.y = (l.obj.y * scale) + l.y;

l.sprite.alpha = l.ttl / l.ttlMax;
}
};

const init = () => {
events.on('onGetDamage', onGetDamage);
};

//Exports
return {
init,
update,
onGetDamage,
POSITION
};
});

+ 22
- 12
src/client/js/rendering/renderer.js View File

@@ -326,6 +326,12 @@ define([
c.zoneId = this.zoneId;
events.emit('onGetObject', c);
});

//Normally, the mounts mod queues this event when unmounting.
// If we rezone, our effects are destroyed, so the event is queued,
// but flushForTarget clears the event right after and the event is never received.
// We emit it again here to make sure the speed is reset after entering the new zone.
events.emit('onMoveSpeedChange', 0);
},

setPosition: function (pos, instant) {
@@ -734,14 +740,13 @@ define([
return sprite;
},

addFilter: function (sprite) {
let thickness = (sprite.width > scale) ? 8 : 16;
addFilter: function (sprite, config) {
const filter = new shaderOutline(config);

let filter = new shaderOutline(this.renderer.width, this.renderer.height, thickness, '0xffffff');
if (!sprite.filters)
sprite.filters = [filter];
else
sprite.filters.push();
sprite.filters.push(filter);

return filter;
},
@@ -752,19 +757,24 @@ define([
},

buildText: function (obj) {
let textSprite = new PIXI.Text(obj.text, {
const { text, visible, x, y, parent: spriteParent, layerName } = obj;
const { fontSize = 14, color = 0xF2F5F5 } = obj;

const textSprite = new PIXI.Text(text, {
fontFamily: 'bitty',
fontSize: (obj.fontSize || 14),
fill: obj.color || 0xF2F5F5,
fontSize: fontSize,
fill: color,
stroke: 0x2d2136,
strokeThickness: 4,
align: 'center'
strokeThickness: 4
});

textSprite.x = obj.x - (textSprite.width / 2);
textSprite.y = obj.y;
if (visible === false)
textSprite.visible = false;

textSprite.x = x - (textSprite.width / 2);
textSprite.y = y;

let parentSprite = obj.parent || this.layers[obj.layerName];
const parentSprite = spriteParent ?? this.layers[layerName];
parentSprite.addChild(textSprite);

return textSprite;


+ 58
- 55
src/client/js/rendering/shaders/outline.js View File

@@ -2,62 +2,65 @@ define([
'js/rendering/shaders/outline/vert',
'js/rendering/shaders/outline/frag'
], function (
vert,
frag
vertex,
fragment
) {
let OutlineFilter = function (viewWidth, viewHeight, thickness, color) {
thickness = thickness || 1;
PIXI.Filter.call(this,
vert,
frag.replace(/%THICKNESS%/gi, (1.0 / thickness).toFixed(7))
);

this.uniforms.pixelWidth = 0.002;
this.uniforms.pixelHeight = 1.0 / (viewHeight || 1);
this.uniforms.thickness = thickness;
this.uniforms.outlineColor = new Float32Array([0, 0, 0, 1]);
this.alpha = 0;
if (color)
this.color = color;
};

OutlineFilter.prototype = Object.create(PIXI.Filter.prototype);
OutlineFilter.prototype.constructor = OutlineFilter;

Object.defineProperties(OutlineFilter.prototype, {
color: {
get: function () {
return PIXI.utils.rgb2hex(this.uniforms.outlineColor);
},
set: function (value) {
PIXI.utils.hex2rgb(value, this.uniforms.outlineColor);
}
},

alpha: {
set: function (value) {
this.uniforms.alpha = value;
}
},

viewWidth: {
get: function () {
return 1 / this.uniforms.pixelWidth;
},
set: function (value) {
this.uniforms.pixelWidth = 1 / value;
}
},

viewHeight: {
get: function () {
return 1 / this.uniforms.pixelHeight;
},
set: function (value) {
this.uniforms.pixelHeight = 1 / value;
}
}
});
class OutlineFilter extends PIXI.Filter {
constructor ({ thickness = 5, color = 0xFFFFFF, quality = 0.1, alpha = 1.0, knockout = false }) {
const angleStep = Math.PI / 2;

super(vertex, fragment.replace('$angleStep$', angleStep));

this.uniforms.uThickness = new Float32Array([thickness, thickness]);
this.uniforms.uColor = new Float32Array([1, 1, 1, 1]);
this.uniforms.uAlpha = alpha;
this.uniforms.uKnockout = knockout;

const rgbColor = PIXI.utils.hex2rgb(color);
this.uniforms.uColor = PIXI.utils.hex2rgb(rgbColor, this.uniforms.uColor);

Object.assign(this, { thickness, color, quality, alpha, knockout });
}

apply (filterManager, input, output, clear) {
this.uniforms.uThickness[0] = this.thickness / input._frame.width;
this.uniforms.uThickness[1] = this.thickness / input._frame.height;
this.uniforms.uAlpha = this.alpha;
this.uniforms.uKnockout = this.knockout;
this.uniforms.uColor = PIXI.utils.hex2rgb(this.color, this.uniforms.uColor);

filterManager.applyFilter(this, input, output, clear);
}

get alpha () {
return this._alpha;
}
set alpha (value) {
this._alpha = value;
}

get color () {
return PIXI.utils.rgb2hex(this.uniforms.uColor);
}
set color (value) {
PIXI.utils.hex2rgb(value, this.uniforms.uColor);
}

get knockout () {
return this._knockout;
}
set knockout (value) {
this._knockout = value;
}

get thickness () {
return this._thickness;
}
set thickness (value) {
this._thickness = value;
this.padding = value;
}
}

return OutlineFilter;
});

+ 31
- 18
src/client/js/rendering/shaders/outline/frag.js View File

@@ -6,28 +6,41 @@ define([
return `
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform vec4 filterClamp;

uniform float thickness;
uniform vec4 outlineColor;
uniform float pixelWidth;
uniform float alpha;
vec2 px = vec2(pixelWidth, pixelWidth);
uniform float uAlpha;
uniform vec2 uThickness;
uniform vec4 uColor;
uniform bool uKnockout;

void main(void) {
const float PI = 3.14159265358979323846264;
vec4 ownColor = texture2D(uSampler, vTextureCoord);
vec4 curColor;
const float DOUBLE_PI = 2. * 3.14159265358979323846264;
const float ANGLE_STEP = $angleStep$;

float outlineMaxAlphaAtPos(vec2 pos) {
if (uThickness.x == 0. || uThickness.y == 0.) {
return 0.;
}

vec4 displacedColor;
vec2 displacedPos;
float maxAlpha = 0.;
for (float angle = 0.; angle < PI * 2.; angle += %THICKNESS% ) {
curColor = texture2D(uSampler, vec2(vTextureCoord.x + thickness * px.x * cos(angle), vTextureCoord.y + thickness * px.y * sin(angle)));
maxAlpha = max(maxAlpha, curColor.a);

for (float angle = 0.; angle <= DOUBLE_PI; angle += ANGLE_STEP) {
displacedPos.x = vTextureCoord.x + uThickness.x * cos(angle);
displacedPos.y = vTextureCoord.y + uThickness.y * sin(angle);
displacedColor = texture2D(uSampler, clamp(displacedPos, filterClamp.xy, filterClamp.zw));
maxAlpha = max(maxAlpha, displacedColor.a);
}
if (maxAlpha > 0.1)
maxAlpha = alpha;
else
maxAlpha = 0.0;
float resultAlpha = max(maxAlpha, ownColor.a);
gl_FragColor = vec4((ownColor.rgb + outlineColor.rgb * (1. - ownColor.a)) * resultAlpha, resultAlpha);

return maxAlpha;
}

void main(void) {
vec4 sourceColor = texture2D(uSampler, vTextureCoord);
vec4 contentColor = sourceColor * float(!uKnockout);
float outlineAlpha = uAlpha * outlineMaxAlphaAtPos(vTextureCoord.xy) * (1.-sourceColor.a);
vec4 outlineColor = vec4(vec3(uColor) * outlineAlpha, outlineAlpha);
gl_FragColor = contentColor + outlineColor;
}
`;
});

+ 20
- 4
src/client/js/rendering/shaders/outline/vert.js View File

@@ -5,14 +5,30 @@ define([
) {
return `
attribute vec2 aVertexPosition;
attribute vec2 aTextureCoord;

uniform mat3 projectionMatrix;

varying vec2 vTextureCoord;

void main(void){
gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
vTextureCoord = aTextureCoord;
uniform vec4 inputSize;
uniform vec4 outputFrame;

vec4 filterVertexPosition( void )
{
vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy;

return vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0);
}

vec2 filterTextureCoord( void )
{
return aVertexPosition * (outputFrame.zw * inputSize.zw);
}

void main(void)
{
gl_Position = filterVertexPosition();
vTextureCoord = filterTextureCoord();
}
`;
});

+ 1
- 1
src/client/package.json View File

@@ -1,6 +1,6 @@
{
"name": "isleward_client",
"version": "0.11.0",
"version": "0.12.0",
"description": "isleward",
"dependencies": {
},


+ 7
- 2
src/client/ui/factory.js View File

@@ -90,8 +90,13 @@ define([
let path = null;
if (options && options.path)
path = options.path + `\\${type}.js`;
else
path = this.root + 'ui/templates/' + type + '/' + type;
else {
const entryInClientConfig = globals.clientConfig.uiList.find(u => u.type === type);
if (entryInClientConfig)
path = entryInClientConfig.path;
else
path = this.root + 'ui/templates/' + type + '/' + type;
}
require([path], this.onGetTemplate.bind(this, options, type));
},


+ 0
- 1
src/client/ui/templates/hud/styles.less View File

@@ -112,7 +112,6 @@
width: 80px;
height: 80px;
background-color: fade(#3a3b4a, 90%);
margin-bottom: 8px;
cursor: pointer;
padding: 8px;
position: relative;


+ 2
- 2
src/client/ui/templates/login/template.html View File

@@ -11,11 +11,11 @@
</div>
<div class="message"></div>
</div>
<div class="news" location="https://gitlab.com/Isleward/play.isleward.com/-/wikis/v0.11.0-Release-Notes">[ Latest Release Notes ]</div>
<div class="news" location="https://gitlab.com/Isleward/play.isleward.com/-/wikis/v0.12.0-Release-Notes">[ Latest Release Notes ]</div>
<div class="extra">
<div class="el btn btnPatreon monetization" location="https://patreon.com/bigbadwaffle">Pledge on Patreon</div>
<div class="el btn btnPaypal monetization" location="https://www.paypal.com/donate?hosted_button_id=NEQAV3NG9PWXA">Donate on Paypal</div>
<div class="el btn btnWiki" location="http://wiki.isleward.com/Main_Page">Access the Wiki</div>
</div>
<div class="version" location="https://gitlab.com/Isleward/play.isleward.com/-/wikis/v0.11.0-Release-Notes">v0.11.0</div>
<div class="version" location="https://gitlab.com/Isleward/play.isleward.com/-/wikis/v0.12.0-Release-Notes">v0.12.0</div>
</div>

+ 4
- 2
src/client/ui/templates/messages/messages.js View File

@@ -242,10 +242,12 @@ define([
}

if (m.type) {
let isChannel = (['info', 'chat', 'loot', 'rep'].indexOf(m.type) === -1);
const isChannel = m?.subType === 'custom';
if (isChannel) {
if (this.find('.filter[filter="' + m.type + '"]').hasClass('active'))
if (this.find('.filter[filter="' + m.channel + '"]').hasClass('active'))
el.show();
else
el.hide();
}

if (isMobile && ['loot', 'info'].indexOf(m.type) !== -1) {


+ 3
- 1
src/client/ui/templates/messages/mobile.js View File

@@ -7,8 +7,10 @@ define([
renderKeyboard: function () {
this.find('.keyboard').remove();

let mainContainer = this.find('.main');

let container = $('<div class="keyboard"></div>')
.appendTo(this.el);
.appendTo(mainContainer);

let keyboard = {
0: 'qwertyuiop|asdfghjkl|zxcvbnm',


+ 9
- 2
src/client/ui/templates/messages/styles.less View File

@@ -318,13 +318,20 @@
&.typing {
left: 0px;
top: 0px;
width: 100%;
height: 100%;
width: 100vw;
height: 100vh;
background-color: fade(@blackC, 90%);
display: flex;
flex-direction: column;
z-index: 999999998;

.main {
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0px;
}

.channelPicker {
display: none;
}


+ 16
- 14
src/client/ui/templates/party/party.js View File

@@ -54,28 +54,30 @@ define([
msg.forEach(m => {
const { id: mId, zoneId: mZone } = m;

if (party.indexOf(m.id) === -1)
if (!party.includes(m.id))
return;

if (mId === playerId) {
party.forEach(p => {
const mObj = globals.onlineList.find(o => o.id === p);

let el = this.find('.member[memberId="' + p + '"]');
el.removeClass('differentZone');

if (mObj.mZone !== mZone)
el.addClass('differentZone');
});
} else {
let el = this.find('.member[memberId="' + m.id + '"]');
if (mId !== playerId) {
const el = this.find('.member[memberId="' + m.id + '"]');
el.removeClass('differentZone');

if (m.mZone !== playerZone)
if (m.zoneId !== playerZone)
el.addClass('differentZone');

el.find('.txtLevel').html('level: ' + m.level);

return;
}

party.forEach(p => {
const mObj = globals.onlineList.find(o => o.id === p);

const el = this.find('.member[memberId="' + p + '"]');
el.removeClass('differentZone');

if (mObj.zoneId !== mZone)
el.addClass('differentZone');
});
});
},



+ 10
- 2
src/client/ui/templates/tooltipItem/buildTooltip/lineBuilders.js View File

@@ -214,7 +214,15 @@ define([
html += '<br />';
});

return html;
const result = (
lineBuilders.div('space', ' ') +
(!!item.spell?.values ? lineBuilders.div('line', ' ') + lineBuilders.div('space', ' ') : '') +
html +
lineBuilders.div('space', ' ') +
lineBuilders.div('line', ' ')
);

return result;
},

material: () => {
@@ -314,7 +322,7 @@ define([

item.factions.forEach((f, i) => {
let htmlF = f.name + ': ' + f.tierName;
if (f.noEquip)
if (f.tier > window.player.reputation.getTier(f.id))
htmlF = '<font class="color-red">' + htmlF + '</font>';

htmlFactions += htmlF;


+ 3
- 3
src/client/ui/templates/tooltipItem/buildTooltip/stringifyStatValue.js View File

@@ -5,11 +5,11 @@ define([
) {
const percentageStats = [
'addCritChance',
'addCritemultiplier',
'addCritMultiplier',
'addAttackCritChance',
'addAttackCritemultiplier',
'addAttackCritMultiplier',
'addSpellCritChance',
'addSpellCritemultiplier',
'addSpellCritMultiplier',
'sprintChance',
'xpIncrease',
'blockAttackChance',


+ 1
- 1
src/client/ui/templates/tooltipItem/styles.less View File

@@ -99,7 +99,7 @@
}

.effects {
color: @white;
color: @blueB;
}

.faction {


+ 1
- 1
src/client/ui/templates/trade/trade.js View File

@@ -93,7 +93,7 @@ define([
noAfford = (~~(item.worth * this.itemList.markup) > window.player.trade.gold);

if (!noAfford && item.factions)
noAfford = item.factions.some(f => f.noEquip);
noAfford = item.factions.some(f => f.tier > window.player.reputation.getTier(f.id));

if (noAfford)
$('<div class="no-afford"></div>').appendTo(itemEl);


+ 314
- 183
src/server/.eslintrc View File

@@ -1,183 +1,314 @@
{
"root": true,

"parser": "babel-eslint",

"env": {
"es6": true,
"node": true
},

"plugins": [
"prettier"
],

// Map from global var to bool specifying if it can be redefined
"globals": {
"__DEV__": true,
"__dirname": false,
"__fbBatchedBridgeConfig": false,
"alert": false,
"cancelAnimationFrame": false,
"cancelIdleCallback": false,
"clearImmediate": true,
"clearInterval": false,
"clearTimeout": false,
"console": false,
"document": false,
"escape": false,
"Event": false,
"EventTarget": false,
"exports": false,
"fetch": false,
"FormData": false,
"global": false,
"jest": false,
"Map": true,
"module": false,
"navigator": false,
"process": false,
"Promise": true,
"requestAnimationFrame": true,
"requestIdleCallback": true,
"require": false,
"Set": true,
"setImmediate": true,
"setInterval": false,
"setTimeout": false,
"window": false,
"XMLHttpRequest": false,
"pit": false,

"extend": false,
"_": false,
"io": false,
"cons": false,
"atlas": false,
"instancer": false,
"leaderboard": false,
"clientConfig": false,
"random": false,
"consts": false,
"rezoneManager": false,
"eventManager": false
},

"rules": {
"comma-dangle": [2,"never"],
"no-cond-assign": [2,"always"],
"no-console": 1,
"no-constant-condition": 2,
"no-control-regex": 2,
"no-debugger": 1,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-empty": [2, { "allowEmptyCatch": true }],
"no-ex-assign": 2,
"no-extra-semi": 2,
"no-func-assign": 2,
"no-inner-declarations": [2,"functions"],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-regex-spaces": 2,
"no-sparse-arrays": 2,
"no-unreachable": 1,
"use-isnan": 2,
"valid-typeof": 2,
"block-scoped-var": 2,
"curly": [2,"multi-or-nest"],
"dot-notation": 2,
"dot-location": [2,"property"],
"eqeqeq": [2,"always", {"null": "ignore"}],
"no-alert": 2,
"no-caller": 2,
"no-else-return": 2,
"no-eq-null": 1,
"no-eval": 2,
"no-extend-native": 1,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-multi-spaces": 2,
"no-native-reassign": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-new": 2,
"no-octal-escape": 2,
"no-octal": 2,
"no-process-env": 1,
"no-proto": 2,
"no-redeclare": 2,
"no-return-assign": [2,"always"],
"no-script-url": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-throw-literal": 2,
"no-unused-expressions": 2,
"no-useless-call": 2,
"no-void": 2,
"no-with": 2,
"wrap-iife": [2,"inside"],
"yoda": [2,"never",{}],
"strict": [2,"global"],
"no-catch-shadow": 2,
"no-delete-var": 2,
"no-label-var": 2,
"no-shadow-restricted-names": 2,
"no-shadow": [2,{"builtinGlobals":true,"hoist":"all"}],
"no-undef-init": 2,
"no-undef": 1,
"no-unused-vars": [1, {"args": "none"}],
"no-use-before-define": 2,
"no-mixed-requires": [2,false],
"no-new-require": 2,
"no-path-concat": 2,
"brace-style": [2,"1tbs",{}],
"camelcase": [1,{"properties":"never"}],
"comma-spacing": [2,{"before": false, "after":true}],
"comma-style": 2,
"eol-last": 2,
"func-style": [1,"expression"],
"indent": [2, "tab"],
"key-spacing": [2,{"afterColon":true}],
"max-lines-per-function": [
2,
{
"max": 120,
"skipBlankLines": false,
"skipComments": false
}
],
"new-parens": 2,
"no-inline-comments": 2,
"no-lonely-if": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": [2,{"max":1}],
"no-nested-ternary": 1,
"no-new-object": 2,
"no-spaced-func": 2,
"no-underscore-dangle": 1,
"no-unneeded-ternary": 2,
"object-curly-spacing": [2,"always",{"arraysInObjects":true,"objectsInObjects":true}],
"padded-blocks": [2,"never"],
"quote-props": [2,"as-needed"],
"quotes": [2,"single","avoid-escape"],
"semi-spacing": [2,{"after":true}],
"semi": [2,"always"],
"space-before-blocks": [2,"always"],
"space-before-function-paren": [2,"always"],
"space-infix-ops": 2,
"keyword-spacing": [2,{"before":true,"after":true}],
"arrow-parens": [2,"as-needed"],
"arrow-spacing": [2,{"before":true,"after":true}],
"no-const-assign": 2,
"no-var": 2
}
}
{
"root": true,
"parser": "babel-eslint",
"env": {
"es6": true,
"node": true
},
"plugins": [
"prettier"
],
"globals": {
"__DEV__": true,
"__dirname": false,
"__fbBatchedBridgeConfig": false,
"alert": false,
"cancelAnimationFrame": false,
"cancelIdleCallback": false,
"clearImmediate": true,
"clearInterval": false,
"clearTimeout": false,
"console": false,
"document": false,
"escape": false,
"Event": false,
"EventTarget": false,
"exports": false,
"fetch": false,
"FormData": false,
"global": false,
"jest": false,
"Map": true,
"module": false,
"navigator": false,
"process": false,
"Promise": true,
"requestAnimationFrame": true,
"requestIdleCallback": true,
"require": false,
"Set": true,
"setImmediate": true,
"setInterval": false,
"setTimeout": false,
"window": false,
"XMLHttpRequest": false,
"pit": false,
"extend": false,
"_": false,
"io": false,
"cons": false,
"atlas": false,
"instancer": false,
"leaderboard": false,
"clientConfig": false,
"random": false,
"consts": false,
"rezoneManager": false,
"eventManager": false
},
"rules": {
"comma-dangle": [
2,
"never"
],
"no-cond-assign": [
2,
"always"
],
"no-console": 1,
"no-constant-condition": 2,
"no-control-regex": 2,
"no-debugger": 1,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-empty": [
2,
{
"allowEmptyCatch": true
}
],
"no-ex-assign": 2,
"no-extra-semi": 2,
"no-func-assign": 2,
"no-inner-declarations": [
2,
"functions"
],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-regex-spaces": 2,
"no-sparse-arrays": 2,
"no-unreachable": 1,
"use-isnan": 2,
"valid-typeof": 2,
"block-scoped-var": 2,
"curly": [
2,
"multi-or-nest"
],
"dot-notation": 2,
"dot-location": [
2,
"property"
],
"eqeqeq": [
2,
"always",
{
"null": "ignore"
}
],
"no-alert": 2,
"no-caller": 2,
"no-else-return": 2,
"no-eq-null": 1,
"no-eval": 2,
"no-extend-native": 1,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-multi-spaces": 2,
"no-native-reassign": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-new": 2,
"no-octal-escape": 2,
"no-octal": 2,
"no-process-env": 1,
"no-proto": 2,
"no-redeclare": 2,
"no-return-assign": [
2,
"always"
],
"no-script-url": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-throw-literal": 2,
"no-unused-expressions": 2,
"no-useless-call": 2,
"no-void": 2,
"no-with": 2,
"wrap-iife": [
2,
"inside"
],
"yoda": [
2,
"never",
{}
],
"strict": [
2,
"global"
],
"no-catch-shadow": 2,
"no-delete-var": 2,
"no-label-var": 2,
"no-shadow-restricted-names": 2,
"no-shadow": [
2,
{
"builtinGlobals": true,
"hoist": "all"
}
],
"no-undef-init": 2,
"no-undef": 1,
"no-unused-vars": [
1,
{
"args": "none"
}
],
"no-use-before-define": 2,
"no-mixed-requires": [
2,
false
],
"no-new-require": 2,
"no-path-concat": 2,
"brace-style": [
2,
"1tbs",
{}
],
"camelcase": [
1,
{
"properties": "never"
}
],
"comma-spacing": [
2,
{
"before": false,
"after": true
}
],
"comma-style": 2,
"eol-last": 2,
"func-style": [
1,
"expression"
],
"indent": [
2,
"tab"
],
"key-spacing": [
2,
{
"afterColon": true
}
],
"max-lines-per-function": [
2,
{
"max": 120,
"skipBlankLines": false,
"skipComments": false
}
],
"new-parens": 2,
"no-inline-comments": 2,
"no-lonely-if": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": [
2,
{
"max": 1
}
],
"no-nested-ternary": 1,
"no-new-object": 2,
"no-spaced-func": 2,
"no-underscore-dangle": 1,
"no-unneeded-ternary": 2,
"object-curly-spacing": [
2,
"always",
{
"arraysInObjects": true,
"objectsInObjects": true
}
],
"padded-blocks": [
2,
"never"
],
"quote-props": [
2,
"as-needed"
],
"quotes": [
2,
"single",
"avoid-escape"
],
"semi-spacing": [
2,
{
"after": true
}
],
"semi": [
2,
"always"
],
"space-before-blocks": [
2,
"always"
],
"space-before-function-paren": [
2,
"always"
],
"space-infix-ops": 2,
"keyword-spacing": [
2,
{
"before": true,
"after": true
}
],
"arrow-parens": [
2,
"as-needed"
],
"arrow-spacing": [
2,
{
"before": true,
"after": true
}
],
"no-const-assign": 2,
"no-var": 2
},
"overrides": [
{
"files": [
"**/clientComponents/**/*.js"
],
"extends": "../client/.eslintrc"
}
]
}

+ 0
- 3
src/server/clientComponents/.eslintrc View File

@@ -1,3 +0,0 @@
{
"extends": "../../client/.eslintrc"
}

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

@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */

define([
'js/system/events',
'js/rendering/numbers'


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

@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */

define([
'js/rendering/renderer',
'js/system/events'


+ 7
- 8
src/server/clientComponents/inventory.js View File

@@ -59,19 +59,18 @@ define([
},

equipItemErrors: function (item) {
let errors = [];
let stats = this.obj.stats.values;
const { obj: { reputation, stats: { values: statValues } } } = this;

if (item.level > stats.level)
const errors = [];

if (item.level > statValues.level)
errors.push('level');

if (item.requires && item.requires[0] && stats[item.requires[0].stat] < item.requires[0].value)
if (item.requires && item.requires[0] && statValues[item.requires[0].stat] < item.requires[0].value)
errors.push('stats');

if (item.factions) {
if (item.factions.some(f => f.noEquip))
errors.push('faction');
}
if (item.factions?.some(f => reputation.getTier(f.id) < f.tier))
errors.push('faction');

return errors;
},


+ 22
- 13
src/server/clientComponents/lightningEffect.js View File

@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */

define([
'js/rendering/lightningBuilder',
'js/rendering/effects'
@@ -23,13 +25,13 @@ define([
init: function () {
effects.register(this);

let xOffset = (this.toX >= this.obj.x) ? 1 : 0;
let { toX = this.target.x, toY = this.target.y } = this;

let fromX = this.obj.x + xOffset;
let fromY = this.obj.y + 0.5;
const fromX = this.obj.x + ((toX >= this.obj.x) ? 1 : 0);
const fromY = this.obj.y + 0.5;

let toX = this.lineGrow ? fromX : this.toX + 0.5;
let toY = this.lineGrow ? fromY : this.toY + 0.5;
toX = this.lineGrow ? fromX : toX + 0.5;
toY = this.lineGrow ? fromY : toY + 0.5;

this.effect = lightningBuilder.build({
fromX: fromX,
@@ -38,7 +40,9 @@ define([
toY: toY,
divisions: this.divisions,
colors: this.colors,
maxDeviate: this.maxDeviate
maxDeviate: this.maxDeviate,
divDistance: this.divDistance,
linkSize: this.linkSize
});
},

@@ -56,8 +60,10 @@ define([

this.cd = cdMax;

lightningBuilder.destroy(this.effect);
this.effect = null;
if (this.effect) {
lightningBuilder.destroy(this.effect);
this.effect = null;
}

if (!this.shrinking) {
this.ttl--;
@@ -67,14 +73,15 @@ define([
}
}

let xOffset = (this.toX >= this.obj.x) ? 1 : 0;
let { toX = this.target.x, toY = this.target.y } = this;
toX += 0.5;
toY += 0.5;

let xOffset = (toX >= this.obj.x) ? 1 : 0;

let fromX = this.obj.x + xOffset;
let fromY = this.obj.y + 0.5;

let toX = this.toX + 0.5;
let toY = this.toY + 0.5;

let changeTo = (
(
(this.lineGrow) &&
@@ -108,7 +115,9 @@ define([
toY: toY,
divisions: this.divisions,
colors: this.colors,
maxDeviate: this.maxDeviate
maxDeviate: this.maxDeviate,
divDistance: this.divDistance,
linkSize: this.linkSize
});

if ((this.shrinking) && (linePercentage < 0.1))


+ 2
- 0
src/server/clientComponents/pather.js View File

@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */

define([
'js/rendering/renderer',
'js/system/events',


+ 4
- 0
src/server/clientComponents/reputation.js View File

@@ -29,6 +29,10 @@ define([

events.emit('onGetReputations', this.list);
}
},

getTier: function (factionId) {
return this.factions.find(f => f.id === factionId)?.tier ?? 3;
}
};
});

+ 2
- 0
src/server/clientComponents/spellbook.js View File

@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */

define([
'js/system/client',
'js/rendering/renderer',


+ 8
- 5
src/server/combat/scale.js View File

@@ -4,9 +4,9 @@ const max = Math.max.bind(Math);

//Helpers
const scaleStatType = (config, result) => {
const { statType, statMult = 1, srcValues } = config;
const { statType, statMult = 1, srcValues, scaleConfig } = config;

if (!statType)
if (!statType || scaleConfig?.statMult === false)
return;
let statValue = 0;
@@ -24,7 +24,10 @@ const scaleStatType = (config, result) => {
result.amount *= statValue * statMult;
};

const scalePercentMultipliers = ({ isAttack, elementName, srcValues }, result) => {
const scalePercentMultipliers = ({ isAttack, elementName, srcValues, scaleConfig }, result) => {
if (scaleConfig?.percentMult === false)
return;

const { dmgPercent = 0, physicalPercent = 0, spellPercent = 0 } = srcValues;

let totalPercent = 100 + dmgPercent;
@@ -40,8 +43,8 @@ const scalePercentMultipliers = ({ isAttack, elementName, srcValues }, result) =
result.amount *= (totalPercent / 100);
};

const scaleCrit = ({ noCrit, isAttack, crit: forceCrit, srcValues }, result) => {
if (noCrit)
const scaleCrit = ({ noCrit, isAttack, crit: forceCrit, srcValues, scaleConfig }, result) => {
if (noCrit || scaleConfig?.critMult === false)
return;

const { critChance, attackCritChance, spellCritChance } = srcValues;


+ 14
- 5
src/server/components/aggro.js View File

@@ -1,7 +1,6 @@
const configThreatCeiling = {
regular: 1,
rare: 0.5,
champion: 0.2
rare: 0.5
};

module.exports = {
@@ -179,7 +178,7 @@ module.exports = {
this.ignoreList.spliceWhere(o => o === obj);
},

tryEngage: function (source, amount, threatMult) {
tryEngage: function (source, amount, threatMult = 1) {
let obj = this.obj;

//Don't aggro yourself, stupid
@@ -205,7 +204,7 @@ module.exports = {
let list = this.list;

amount = (amount || 0);
let threat = (amount / obj.stats.values.hpMax) * (threatMult || 1);
let threat = (amount / obj.stats.values.hpMax) * threatMult;

let exists = list.find(l => l.obj.id === oId);
if (!exists) {
@@ -238,7 +237,7 @@ module.exports = {
return null;
},

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

@@ -260,6 +259,10 @@ module.exports = {
this.list = [];
},

die: function () {
this.reset();
},

unAggro: function (obj, amount) {
let list = this.list;
let lLen = list.length;
@@ -407,5 +410,11 @@ module.exports = {

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

setAllAmounts: function (amount) {
this.list.forEach(l => {
l.amount = amount;
});
}
};

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

@@ -213,7 +213,7 @@ module.exports = {
if (effect.silent)
return;

this.obj.syncer.setArray(true, 'effects', 'extendEffects', {
this.obj.syncer.setArray(false, 'effects', 'extendEffects', {
id,
data
});


+ 2
- 20
src/server/components/equipment.js View File

@@ -4,7 +4,6 @@ module.exports = {
type: 'equipment',

eq: {},
doAutoEq: true,

quickSlots: {},

@@ -32,24 +31,6 @@ module.exports = {
return !this.eq.has(slot);
},

autoEquip: function (itemId) {
if (!this.doAutoEq)
return;

let item = this.obj.inventory.findItem(itemId);
if (!item)
return;
else if ((!item.slot) || (item.material) || (item.quest) || (item.ability) || (!this.obj.inventory.canEquipItem(item))) {
item.eq = false;
return;
}

if (!this.eq.has(item.slot)) {
this.equip({ itemId });
return true;
}
},

equip: function (itemId) {
let slot = null;
if (typeof (itemId) === 'object') {
@@ -122,7 +103,8 @@ module.exports = {
this.eq[slot] = itemId;
item.equipSlot = slot;

obj.spellbook.calcDps();
if (obj.spellbook)
obj.spellbook.calcDps();

if ((!obj.mob) || (item.ability)) {
if (item.spell)


+ 4
- 2
src/server/components/extensions/socialCommands.js View File

@@ -465,8 +465,10 @@ module.exports = {

die: function () {
this.obj.stats.takeDamage({
amount: 20000000
}, 1, this.obj);
damage: { amount: 20000000 },
source: this.obj,
target: this.obj
});
},

setPassword: async function (config) {


+ 10
- 0
src/server/components/follower.js View File

@@ -169,5 +169,15 @@ module.exports = {
type: 'follower',
master: this.master.id
};
},

events: {
afterDeath: function (deathEvent) {
this.master.fireEvent('afterFollowerDeath', {
deathEvent,
follower: this.obj,
master: this.master
});
}
}
};

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

@@ -311,7 +311,7 @@ module.exports = {
this.events.beforeMove.call(this);
},

beforeTakeDamage: function () {
beforeTakeDamage: function (damageEvent) {
this.events.beforeMove.call(this);
},



+ 58
- 54
src/server/components/inventory.js View File

@@ -1,16 +1,21 @@
//System
const events = require('../misc/events');

//External Helpers
let generator = require('../items/generator');
let salvager = require('../items/salvager');
let classes = require('../config/spirits');
let factions = require('../config/factions');
let itemEffects = require('../items/itemEffects');
const events = require('../misc/events');

const { isItemStackable } = require('./inventory/helpers');

//Helpers
const simplifyItem = require('./inventory/simplifyItem');
const getItem = require('./inventory/getItem');
const dropBag = require('./inventory/dropBag');
const useItem = require('./inventory/useItem');
const { isItemStackable } = require('./inventory/helpers');

//Component
module.exports = {
type: 'inventory',

@@ -91,44 +96,7 @@ module.exports = {
},

simplifyItem: function (item) {
let result = extend({}, item);

if (result.effects) {
result.effects = result.effects.map(e => ({
factionId: e.factionId || null,
text: e.text || null,
properties: e.properties || null,
type: e.type || null,
rolls: e.rolls || null
}));
}

let reputation = this.obj.reputation;
if (result.factions) {
result.factions = result.factions.map(function (f) {
let res = {
id: f.id,
tier: f.tier,
tierName: ['Hated', 'Hostile', 'Unfriendly', 'Neutral', 'Friendly', 'Honored', 'Revered', 'Exalted'][f.tier]
};

if (reputation) {
let faction = reputation.getBlueprint(f.id);
let factionTier = reputation.getTier(f.id);

let noEquip = null;
if (factionTier < f.tier)
noEquip = true;

res.name = faction.name;
res.noEquip = noEquip;
}

return res;
}, this);
}

return result;
return simplifyItem(this, item);
},

update: function () {
@@ -403,13 +371,53 @@ module.exports = {
try {
let effectModule = require('../' + effectUrl);
e.events = effectModule.events;
if (effectModule.events.onGetText)

const { rolls } = e;

if (rolls.textTemplate) {
let text = rolls.textTemplate;

while (text.includes('((')) {
Object.entries(rolls).forEach(([k, v]) => {
text = text.replaceAll(`((${k}))`, v);
});

if (rolls.applyEffect) {
Object.entries(rolls.applyEffect).forEach(([k, v]) => {
text = text.replaceAll(`((applyEffect.${k}))`, v);
});
}

if (rolls.castSpell) {
Object.entries(rolls.castSpell).forEach(([k, v]) => {
text = text.replaceAll(`((castSpell.${k}))`, v);
});
}

if (rolls.applyEffect?.scaleDamage) {
Object.entries(rolls.applyEffect.scaleDamage).forEach(([k, v]) => {
text = text.replaceAll(`((applyEffect.scaleDamage.${k}))`, v);
});
}

if (rolls.castSpell?.scaleDamage) {
Object.entries(rolls.castSpell.scaleDamage).forEach(([k, v]) => {
text = text.replaceAll(`((castSpell.scaleDamage.${k}))`, v);
});
}
}

e.text = text;
} else if (effectModule.events.onGetText)
e.text = effectModule.events.onGetText(item, e);
} catch (error) {
_.log(`Effect not found: ${e.type}`);
_.log(error);
}
}
});

item.effects.spliceWhere(e => !e.events);
}

if (!item.has('pos') && !item.eq) {
@@ -682,25 +690,21 @@ module.exports = {
},

equipItemErrors: function (item) {
let errors = [];
const { obj: { player, stats: { values: statValues }, reputation } } = this;

if (!this.obj.player)
return [];
const errors = [];

let stats = this.obj.stats.values;
if (!player)
return errors;

if (item.level > stats.level)
if (item.level > statValues.level)
errors.push('level');

if ((item.requires) && (stats[item.requires[0].stat] < item.requires[0].value))
if (item.requires && statValues[item.requires[0].stat] < item.requires[0].value)
errors.push(item.requires[0].stat);

if (item.factions) {
if (item.factions.some(function (f) {
return f.noEquip;
}))
errors.push('faction');
}
if (item.factions?.some(f => reputation.getTier(f.id) < f.tier))
errors.push('faction');

return errors;
},


+ 17
- 7
src/server/components/inventory/dropBag.js View File

@@ -1,13 +1,15 @@
let generator = require('../../items/generator');

module.exports = (cpnInv, ownerName, killSource) => {
if (!cpnInv.blueprint)
const { obj, blueprint } = cpnInv;

if (!blueprint)
return;

const obj = cpnInv.obj;
const { stats, instance: { objects, eventEmitter } } = obj;

//Only drop loot if this player is in the zone
let playerObject = obj.instance.objects.find(o => o.player && o.name === ownerName);
let playerObject = objects.find(o => o.player && o.name === ownerName);
if (!playerObject)
return;

@@ -18,7 +20,6 @@ module.exports = (cpnInv, ownerName, killSource) => {
delete items[i].pos;
}

let blueprint = cpnInv.blueprint;
let magicFind = (blueprint.magicFind || 0);

let savedItems = extend([], cpnInv.items);
@@ -44,7 +45,7 @@ module.exports = (cpnInv, ownerName, killSource) => {
continue;

let itemBlueprint = {
level: obj.stats.values.level,
level: stats.values.level,
magicFind: magicFind,
bonusMagicFind: bonusMagicFind,
noCurrency: i > 0
@@ -64,7 +65,7 @@ module.exports = (cpnInv, ownerName, killSource) => {
else if ((drop.chance) && (~~(Math.random() * 100) >= drop.chance * dropEvent.chanceMultiplier))
continue;

drop.level = drop.level || obj.stats.values.level;
drop.level = drop.level || stats.values.level;
drop.magicFind = magicFind;

let item = drop;
@@ -80,8 +81,17 @@ module.exports = (cpnInv, ownerName, killSource) => {

playerObject.fireEvent('beforeTargetDeath', obj, cpnInv.items);

obj.instance.eventEmitter.emit('onBeforeDropBag', obj, cpnInv.items, killSource);
//Deprecated
eventEmitter.emit('onBeforeDropBag', obj, cpnInv.items, killSource);
obj.fireEvent('onBeforeDropBag', cpnInv.items, killSource);
//New
const eventMsg = {
objDropper: obj,
objLooter: playerObject,
objKillSource: killSource,
items: cpnInv.items
};
eventEmitter.emit('beforeDropBag', eventMsg);

if (cpnInv.items.length > 0)
cpnInv.createBag(obj.x, obj.y, cpnInv.items, ownerName);


+ 37
- 0
src/server/components/inventory/simplifyItem.js View File

@@ -0,0 +1,37 @@
//Helpers
const { getFactionBlueprint } = require('../../config/factions/helpers');

//Internals
const tierNames = ['Hated', 'Hostile', 'Unfriendly', 'Neutral', 'Friendly', 'Honored', 'Revered', 'Exalted'];

//Method
const simplifyItem = (cpnInventory, item) => {
const result = extend({}, item);

if (result.effects) {
result.effects = result.effects.map(e => ({
factionId: e.factionId ?? null,
text: e.text ?? null,
properties: e.properties ?? null,
type: e.type ?? null,
rolls: e.rolls ?? null
}));
}

if (result.factions) {
result.factions = result.factions.map(f => {
const res = {
id: f.id,
tier: f.tier,
tierName: tierNames[f.tier],
name: getFactionBlueprint(f.id).name
};

return res;
});
}

return result;
};

module.exports = simplifyItem;

+ 10
- 1
src/server/components/inventory/useItem.js View File

@@ -56,10 +56,19 @@ module.exports = async (cpnInv, itemId) => {
success: true,
cdMax: item.cdMax
};
//Deprecated
obj.instance.eventEmitter.emit('onBeforeUseItem', obj, item, result);
obj.fireEvent('onBeforeUseItem', item, result);
//New
const eventMsg = {
obj,
item,
cdMax: item.cdMax,
success: true
};
obj.instance.eventEmitter.emit('beforeUseItem', eventMsg);

if (!result.success)
if (!result.success || !eventMsg.success)
return;

placeItemOnCooldown(obj, cpnInv, item, result);


+ 10
- 11
src/server/components/mob.js View File

@@ -126,19 +126,14 @@ module.exports = {
let distanceFromHome = Math.max(abs(this.originX - obj.x), abs(this.originY - obj.y));
if (!distanceFromHome) {
this.goHome = false;
if (!obj.spellbook) {
/* eslint-disable-next-line no-console */
console.log('MOB HAS NO SPELLBOOK BUT WANTS TO RESET ROTATION');
/* eslint-disable-next-line no-console */
console.log(obj.name, obj.zone, obj.zoneName, obj.x, obj.y, obj.components.map(c => c.type).join(','));
}

obj.spellbook.resetRotation();
if (obj.spellbook)
obj.spellbook.resetRotation();
}
}

if (!this.goHome) {
//Are we too far from home?
//Are we chasing a target too far from home?
if (!obj.follower && target) {
if (!this.canChase(target)) {
obj.clearQueue();
@@ -147,7 +142,11 @@ module.exports = {
}
}

if ((target) && (target !== obj) && ((!obj.follower) || (obj.follower.master !== target))) {
//Are we too far from home?
let distanceFromHome = Math.max(abs(this.originX - obj.x), abs(this.originY - obj.y));
if (distanceFromHome > this.maxChaseDistance || (distanceFromHome > this.walkDistance && !target && !this.patrol))
this.goHome = true;
else if (target && target !== obj && (!obj.follower || obj.follower.master !== target)) {
//If we just started attacking, patrols need to know where home is
if (!this.target && this.patrol) {
this.originX = obj.x;
@@ -361,9 +360,9 @@ module.exports = {
},

events: {
beforeTakeDamage: function (msg) {
beforeTakeDamage: function ({ damage }) {
if (this.goHome)
msg.failed = true;
damage.failed = true;
}
}
};

+ 10
- 30
src/server/components/reputation.js View File

@@ -1,6 +1,7 @@
let factionBase = require('../config/factionBase');
let factions = require('../config/factions');
//Helpers
const { getFactionBlueprint } = require('../config/factions/helpers');

//Component
module.exports = {
type: 'reputation',

@@ -13,7 +14,7 @@ module.exports = {
delete blueprint.list;

list.forEach(function (l) {
let bpt = this.getBlueprint(l.id);
let bpt = getFactionBlueprint(l.id);
if (!bpt)
return;

@@ -27,25 +28,6 @@ module.exports = {
}, this);
},

getBlueprint: function (factionId) {
if (this.factions[factionId])
return this.factions[factionId];

let factionBlueprint = null;
try {
factionBlueprint = factions.getFaction(factionId);
} catch (e) {}

if (!factionBlueprint)
return;

factionBlueprint = extend({}, factionBase, factionBlueprint);

this.factions[factionBlueprint.id] = factionBlueprint;

return factionBlueprint;
},

getTier: function (factionId) {
let faction = this.list.find(l => l.id === factionId);
if (!faction) {
@@ -53,9 +35,7 @@ module.exports = {
faction = this.list.find(l => l.id === factionId);
}

return (faction || {
tier: 3
}).tier;
return faction?.tier ?? 3;
},

canEquipItem: function (item) {
@@ -71,7 +51,7 @@ module.exports = {
},

calculateTier: function (factionId) {
let blueprint = this.getBlueprint(factionId);
let blueprint = getFactionBlueprint(factionId);

let faction = this.list.find(l => l.id === factionId);
let rep = faction.rep;
@@ -99,7 +79,7 @@ module.exports = {

getReputation: function (factionId, gain) {
let fullSync = false;
let blueprint = this.getBlueprint(factionId);
let blueprint = getFactionBlueprint(factionId);

let faction = this.list.find(l => l.id === factionId);
if (!faction) {
@@ -147,7 +127,7 @@ module.exports = {
if (this.list.some(l => l.id === factionId))
return;

let blueprint = this.getBlueprint(factionId);
let blueprint = getFactionBlueprint(factionId);

if (!blueprint)
return;
@@ -185,7 +165,7 @@ module.exports = {
let sendList = this.list
.map(function (l) {
let result = {};
let blueprint = this.getBlueprint(l.id);
let blueprint = getFactionBlueprint(l.id);
extend(result, l, blueprint);

return result;
@@ -206,7 +186,7 @@ module.exports = {
};

if (full) {
let blueprint = this.getBlueprint(factionId);
let blueprint = getFactionBlueprint(factionId);
extend(faction, l, blueprint);
}



+ 7
- 1
src/server/components/social.js View File

@@ -276,7 +276,13 @@ module.exports = {
//Sends a notification to yourself
// arg1 = { message, className, type }
notifySelf: function ({ message, className = 'color-redA', type = 'info', subType }) {
const { obj: { id, serverId, instance: { syncer } } } = this;
const { obj: { id, serverId, instance } } = this;

//Maybe we are in the main thread
if (!instance)
return;

const { syncer } = instance;

syncer.queue('onGetMessages', {
id,


+ 5
- 1
src/server/components/social/startEvent.js View File

@@ -1,5 +1,9 @@
//Imports
const { messageAllThreads } = require('../../world/threadManager');

//Exports
module.exports = async (cpnSocial, eventName) => {
atlas.messageAllThreads({
messageAllThreads({
threadModule: 'eventManager',
method: 'startEventByCode',
data: eventName


+ 5
- 1
src/server/components/social/stopEvent.js View File

@@ -1,5 +1,9 @@
//Imports
const { messageAllThreads } = require('../../world/threadManager');

//Exports
module.exports = async (cpnSocial, eventName) => {
atlas.messageAllThreads({
messageAllThreads({
threadModule: 'eventManager',
method: 'stopEventByCode',
data: eventName


+ 7
- 120
src/server/components/spellbook.js View File

@@ -1,13 +1,14 @@
let spellTemplate = require('../config/spells/spellTemplate');
let animations = require('../config/animations');
let playerSpells = require('../config/spells');
let playerSpellsConfig = require('../config/spellsConfig');
//Imports
const spellTemplate = require('../config/spells/spellTemplate');
const animations = require('../config/animations');
const playerSpells = require('../config/spells');
const playerSpellsConfig = require('../config/spellsConfig');

//Helpers
const rotationManager = require('./spellbook/rotationManager');
const cast = require('./spellbook/cast');

//Component

module.exports = {
type: 'spellbook',

@@ -27,8 +28,6 @@ module.exports = {
this.objects = this.obj.instance.objects;
this.physics = this.obj.instance.physics;

this.dmgMult = blueprint.dmgMult;

(blueprint.spells || []).forEach(s => this.addSpell(s, -1));

if (blueprint.rotation) {
@@ -340,119 +339,7 @@ module.exports = {
},

cast: function (action, isAuto) {
if (!action.has('spell')) {
const isCasting = this.isCasting();
this.stopCasting();

const consumeTick = isCasting;

return consumeTick;
}

let spell = this.spells.find(s => (s.id === action.spell));
if (!spell)
return false;

action.target = this.getTarget(spell, action);

//If a target has become nonSelectable, we need to stop attacks that are queued/auto
if (!action.target || action.target.nonSelectable)
return false;

action.auto = spell.auto;

let success = true;
if (spell.cd > 0) {
if (!isAuto) {
let type = (spell.auto) ? 'Weapon' : 'Spell';
this.sendAnnouncement(`${type} is on cooldown`);
}
success = false;
} else if (spell.manaCost > this.obj.stats.values.mana) {
if (!isAuto)
this.sendAnnouncement('Insufficient mana to cast spell');
success = false;
} else if (spell.has('range')) {
let distance = Math.max(Math.abs(action.target.x - this.obj.x), Math.abs(action.target.y - this.obj.y));
let range = spell.range;
if ((spell.useWeaponRange) && (this.obj.player)) {
let weapon = this.obj.inventory.findItem(this.obj.equipment.eq.oneHanded) || this.obj.inventory.findItem(this.obj.equipment.eq.twoHanded);
if (weapon)
range = weapon.range || 1;
}

if (distance > range) {
if (!isAuto)
this.sendAnnouncement('Target out of range');
success = false;
}
}

//LoS check
//Null means we don't have LoS and as such, we should move
if (spell.needLos && success) {
if (!this.physics.hasLos(~~this.obj.x, ~~this.obj.y, ~~action.target.x, ~~action.target.y)) {
if (!isAuto)
this.sendAnnouncement('Target not in line of sight');
action.auto = false;
success = null;
}
}

if (!success) {
this.queueAuto(action, spell);
return success;
} else if (!this.queueAuto(action, spell))
return false;

let castSuccess = {
success: true
};
this.obj.fireEvent('beforeCastSpell', castSuccess);
if (!castSuccess.success)
return false;

if (spell.manaReserve) {
let reserve = spell.manaReserve;

if (reserve.percentage) {
let reserveEvent = {
spell: spell.name,
reservePercent: reserve.percentage
};
this.obj.fireEvent('onBeforeReserveMana', reserveEvent);

if (!spell.active) {
if (1 - this.obj.stats.values.manaReservePercent < reserve.percentage) {
this.sendAnnouncement('Insufficient mana to cast spell');
return;
} this.obj.stats.addStat('manaReservePercent', reserveEvent.reservePercent);
} else
this.obj.stats.addStat('manaReservePercent', -reserveEvent.reservePercent);
}
}

if (spell.targetFurthest)
spell.target = this.obj.aggro.getFurthest();
else if (spell.targetRandom)
spell.target = this.obj.aggro.getRandom();

success = spell.castBase(action);
this.stopCasting(spell, true);

if (success) {
spell.consumeMana();
spell.setCd();
}

this.obj.fireEvent('afterCastSpell', {
castSuccess: success,
spell,
action
});

//Null means we didn't fail but are initiating casting
return (success === null || success === true);
return cast(this, action, isAuto);
},

getClosestRange: function (spellNum) {


+ 131
- 0
src/server/components/spellbook/cast.js View File

@@ -0,0 +1,131 @@
/* eslint-disable-next-line max-lines-per-function */
const cast = (cpnSpellbook, action, isAuto) => {
const { obj, physics, spells } = cpnSpellbook;

//Stop casting
if (!action.has('spell')) {
const wasCasting = cpnSpellbook.isCasting();
cpnSpellbook.stopCasting();

//Consume a tick if we were casting
return wasCasting;
}

const spell = spells.find(s => (s.id === action.spell));
if (!spell)
return false;

action.target = cpnSpellbook.getTarget(spell, action);
action.auto = spell.auto;

//If a target has become nonSelectable, we need to stop attacks that are queued/auto
if (!action.target || action.target.nonSelectable)
return false;

let success = true;
if (spell.cd > 0) {
if (!isAuto) {
const type = (spell.auto) ? 'Weapon' : 'Spell';
cpnSpellbook.sendAnnouncement(`${type} is on cooldown`);
}
success = false;
} else if (spell.manaCost > obj.stats.values.mana) {
if (!isAuto)
cpnSpellbook.sendAnnouncement('Insufficient mana to cast spell');
success = false;
} else if (spell.has('range')) {
const distance = Math.max(Math.abs(action.target.x - obj.x), Math.abs(action.target.y - obj.y));
let range = spell.range;
if ((spell.useWeaponRange) && (obj.player)) {
const weapon = obj.inventory.findItem(obj.equipment.eq.oneHanded) || obj.inventory.findItem(obj.equipment.eq.twoHanded);
if (weapon)
range = weapon.range || 1;
}

if (distance > range) {
if (!isAuto)
cpnSpellbook.sendAnnouncement('Target out of range');
success = false;
}
}

//LoS check
//Null means we don't have LoS and as such, we should move
if (spell.needLos && success) {
if (!physics.hasLos(~~obj.x, ~~obj.y, ~~action.target.x, ~~action.target.y)) {
if (!isAuto)
cpnSpellbook.sendAnnouncement('Target not in line of sight');
action.auto = false;
success = null;
}
}

if (!success) {
cpnSpellbook.queueAuto(action, spell);
return success;
} else if (!cpnSpellbook.queueAuto(action, spell))
return false;

const eventBeforeCastSpell = {
success: true,
action
};
obj.fireEvent('beforeCastSpell', eventBeforeCastSpell);
if (!eventBeforeCastSpell.success)
return false;

if (spell.manaReserve) {
const reserve = spell.manaReserve;

if (reserve.percentage) {
const reserveEvent = {
spell: spell.name,
reservePercent: reserve.percentage
};
obj.fireEvent('onBeforeReserveMana', reserveEvent);

if (!spell.active) {
if (1 - obj.stats.values.manaReservePercent < reserve.percentage) {
cpnSpellbook.sendAnnouncement('Insufficient mana to cast spell');
return;
} obj.stats.addStat('manaReservePercent', reserveEvent.reservePercent);
} else
obj.stats.addStat('manaReservePercent', -reserveEvent.reservePercent);
}
}

if (spell.targetFurthest)
spell.target = obj.aggro.getFurthest();
else if (spell.targetRandom)
spell.target = obj.aggro.getRandom();

if (!!eventBeforeCastSpell.action.target?.effects) {
const eventBeforeIsSpellTarget = {
source: obj,
spell,
target: eventBeforeCastSpell.action.target
};
eventBeforeIsSpellTarget.target.fireEvent('beforeIsSpellTarget', eventBeforeIsSpellTarget);
eventBeforeCastSpell.action.target = eventBeforeIsSpellTarget.target;
}

success = spell.castBase(eventBeforeCastSpell.action);
cpnSpellbook.stopCasting(spell, true);

if (success) {
spell.consumeMana();
spell.setCd();
}

obj.fireEvent('afterCastSpell', {
castSuccess: success,
spell,
action: eventBeforeCastSpell.action
});

//Null means we didn't fail but are initiating casting
return (success === null || success === true);
};

module.exports = cast;

+ 64
- 118
src/server/components/stats.js View File

@@ -1,10 +1,13 @@
//Imports
const animations = require('../config/animations');
const spirits = require('../config/spirits');
const scheduler = require('../misc/scheduler');

//Methods
const die = require('./stats/die');
const takeDamage = require('./stats/takeDamage');

let animations = require('../config/animations');
let spirits = require('../config/spirits');
let scheduler = require('../misc/scheduler');

//Internals
let baseStats = {
mana: 20,
manaMax: 20,
@@ -82,6 +85,7 @@ let baseStats = {
fishItems: 0
};

//Exports
module.exports = {
type: 'stats',

@@ -348,8 +352,6 @@ module.exports = {
let mobDiffMult = 1;
if (target.isRare)
mobDiffMult = 2;
else if (target.isChampion)
mobDiffMult = 5;

//Who should get xp?
let aggroList = target.aggro.list;
@@ -520,85 +522,23 @@ module.exports = {
this.addStat(s, gainStats[s] * count);
},

takeDamage: function (damage, threatMult, source) {
if (this.values.hp <= 0)
return;

let obj = this.obj;

if (!damage.noEvents) {
source.fireEvent('beforeDealDamage', damage, obj);
obj.fireEvent('beforeTakeDamage', damage, source);
}

if (damage.failed || obj.destroyed)
return;

let amount = Math.min(this.values.hp, damage.amount);

damage.dealt = amount;

let msg = {
id: obj.id,
source: source.id,
crit: damage.crit,
amount: amount,
element: damage.element
};

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

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

let died = (this.values.hp <= 0);

if (died) {
let death = {
success: true
};
obj.instance.eventEmitter.emit('onBeforeActorDies', death, obj, source);
obj.fireEvent('beforeDeath', death);

if (death.success)
this.preDeath(source);
} else {
source.aggro.tryEngage(obj, 0);
obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp);
}

if (!damage.noEvents)
source.fireEvent('afterDealDamage', damage, obj);
takeDamage: function (eventDamage) {
takeDamage(this, eventDamage);
},

getHp: function (heal, source) {
/*
Gives hp to heal.target
heal: Damage object returned by combat.getDamage
source: Source object
event: Optional config object. We want to eventually phase out the first 2 args.
heal: Same as 1st parameter
source: Same as 2nd parameter
target: Target object (heal.target)
spell: Optional spell object that caused this event
*/
getHp: function (event) {
const { heal, source } = event;

let amount = heal.amount;
if (amount === 0)
return;
@@ -610,44 +550,46 @@ module.exports = {
let values = this.values;
let hpMax = values.hpMax;

if (values.hp >= hpMax)
return;

if (hpMax - values.hp < amount)
amount = hpMax - values.hp;
if (values.hp < hpMax) {
if (hpMax - values.hp < amount)
amount = hpMax - values.hp;

values.hp += amount;
if (values.hp > hpMax)
values.hp = hpMax;
values.hp += amount;
if (values.hp > hpMax)
values.hp = hpMax;

let recipients = [];
if (this.obj.serverId)
recipients.push(this.obj.serverId);
if (source.serverId)
recipients.push(source.serverId);
if (recipients.length > 0) {
this.syncer.queue('onGetDamage', {
id: this.obj.id,
source: source.id,
heal: true,
amount: amount,
crit: heal.crit,
element: heal.element
}, recipients);
}
let recipients = [];
if (this.obj.serverId)
recipients.push(this.obj.serverId);
if (source.serverId)
recipients.push(source.serverId);
if (recipients.length > 0) {
this.syncer.queue('onGetDamage', {
id: this.obj.id,
source: source.id,
heal: true,
amount: amount,
crit: heal.crit,
element: heal.element
}, recipients);
}

//Add aggro to all our attackers
let threat = amount * 0.4 * threatMult;
if (threat !== 0) {
let aggroList = this.obj.aggro.list;
let aLen = aggroList.length;
for (let i = 0; i < aLen; i++) {
let a = aggroList[i].obj;
a.aggro.tryEngage(source, threat);
//Add aggro to all our attackers
let threat = amount * 0.4 * threatMult;
if (threat !== 0) {
let aggroList = this.obj.aggro.list;
let aLen = aggroList.length;
for (let i = 0; i < aLen; i++) {
let a = aggroList[i].obj;
a.aggro.tryEngage(source, threat);
}
}

this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
}

this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
if (!heal.noEvents)
source.fireEvent('afterGiveHp', event);
},

save: function () {
@@ -778,8 +720,8 @@ module.exports = {
}
},

afterDealDamage: function (damageEvent, target) {
if (damageEvent.element)
afterDealDamage: function ({ damage, target }) {
if (damage.element)
return;

const { obj, values: { lifeOnHit } } = this;
@@ -787,7 +729,11 @@ module.exports = {
if (target === obj || !lifeOnHit)
return;

this.getHp({ amount: lifeOnHit }, obj);
this.getHp({
heal: { amount: lifeOnHit },
source: obj,
target: obj
});
}
}
};

+ 84
- 0
src/server/components/stats/takeDamage.js View File

@@ -0,0 +1,84 @@
const takeDamage = (cpnStats, eventDamage) => {
const { obj, values, syncer } = cpnStats;

if (values.hp <= 0)
return;

if (!eventDamage.noEvents) {
eventDamage.source.fireEvent('beforeDealDamage', eventDamage);
obj.fireEvent('beforeTakeDamage', eventDamage);
}

const { source, damage, threatMult = 1 } = eventDamage;
const { noEvents, failed, blocked, dodged, crit, element } = damage;

if (failed || obj.destroyed)
return;

const amount = Math.min(values.hp, damage.amount);

damage.dealt = amount;

const msg = {
id: obj.id,
source: source.id,
crit,
amount,
element
};

values.hp -= amount;
const 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 (recipients.length) {
if (!blocked && !dodged)
syncer.queue('onGetDamage', msg, recipients);
else {
syncer.queue('onGetDamage', {
id: obj.id,
source: source.id,
event: true,
text: blocked ? 'blocked' : 'dodged'
}, recipients);
}
}

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

let died = (values.hp <= 0);

if (died) {
let death = {
success: true
};
obj.instance.eventEmitter.emit('onBeforeActorDies', death, obj, source);
obj.fireEvent('beforeDeath', death);

if (death.success)
cpnStats.preDeath(source);
} else {
source.aggro.tryEngage(obj, 0);
obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
}

if (!noEvents) {
source.fireEvent('afterDealDamage', eventDamage);
obj.fireEvent('afterTakeDamage', eventDamage);
}
};

module.exports = takeDamage;

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

@@ -5,6 +5,12 @@ module.exports = {
//The maximum level a player can reach
maxLevel: 20,

//Rune damage is multiplied by nth entry from this array where n = level - 1
dmgMults: [0.25, 0.4, 0.575, 0.8, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2, 2.1, 2.2, 2.3, 2.4, 2.5],

//Mob HP is multiplied by nth entry from this array where n = level - 1
hpMults: [0.1, 0.2, 0.4, 0.7, 0.78, 0.91, 1.16, 1.19, 1.65, 2.36, 3.07, 3.55, 4.1, 4.85, 5.6, 5.9, 6.5, 7.1, 7.9, 12],

//How far a player can see objects horizontally
viewDistanceX: 25,



+ 0
- 3
src/server/config/effects/effectChampion.js View File

@@ -1,3 +0,0 @@
module.exports = {
type: 'champion'
};

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

@@ -71,7 +71,7 @@ module.exports = {
targetPos.y = obj.y;
},

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


+ 6
- 2
src/server/config/effects/effectHolyVengeance.js View File

@@ -4,9 +4,13 @@ module.exports = {
persist: true,

events: {
afterDealDamage: function (damage, target) {
afterDealDamage: function ({ damage, target }) {
damage.dealt *= 0.5;
this.obj.stats.getHp(damage, this.obj);
this.obj.stats.getHp({
heal: damage,
source: this.obj,
target: this.obj
});
}
}
};

+ 2
- 2
src/server/config/effects/effectInvulnerability.js View File

@@ -2,12 +2,12 @@ module.exports = {
type: 'invulnerability',

events: {
beforeDealDamage: function (damage) {
beforeDealDamage: function ({ damage }) {
if (damage)
damage.amount = 0;
},

beforeTakeDamage: function (damage, source) {
beforeTakeDamage: function ({ damage }) {
damage.amount = 0;
}
}


+ 28
- 12
src/server/config/effects/effectLifeDrain.js View File

@@ -3,23 +3,39 @@ let combat = require('../../combat/combat');
module.exports = {
type: 'lifeDrain',

amount: 0,

scaleDamage: {
isAttack: false,
damage: 1,
element: undefined,
noScale: false,
noMitigate: false,
noCrit: false
},

events: {
afterTick: function () {
let newDamage = combat.getDamage({
source: {
stats: {
values: {}
}
},
isAttack: false,
const { isAttack, damage, element, noScale, noMitigate, noCrit } = this.scaleDamage;

const damageEvent = combat.getDamage({
source: this.caster,
target: this.obj,
damage: this.amount,
element: this.element,
noScale: this.noScale,
noCrit: true
isAttack,
damage,
element,
noScale,
noMitigate,
noCrit
});
this.obj.stats.takeDamage(newDamage, 1, this.caster);
this.obj.stats.takeDamage({
damage: damageEvent,
threatMult: 1,
source: this.caster,
target: this.obj,
effectName: 'lifeDrain'
});
}
}
};

+ 9
- 2
src/server/config/effects/effectReflectDamage.js View File

@@ -2,9 +2,16 @@ module.exports = {
type: 'reflectDamage',

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

source.stats.takeDamage({
damage,
threatMult: 1,
source: this.obj,
target: source,
effectName: 'reflectDamage'
});

damage.failed = true;



+ 1
- 1
src/server/config/effects/effectStunned.js View File

@@ -11,7 +11,7 @@ module.exports = {
targetPos.success = false;
},

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


+ 8
- 2
src/server/config/factions/akarei.js View File

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

events: {
beforeDealDamage: function (item, damage, target) {
beforeDealDamage: function (item, { damage, target }) {
if (!damage.crit)
return;

@@ -69,7 +69,13 @@ module.exports = {
noCrit: true
});

boundTarget.stats.takeDamage(damageConfig, 1, this);
boundTarget.stats.takeDamage({
damage: damageConfig,
threatMult: 1,
source: this,
target: boundTarget,
effectName: 'akareiZap'
});
};

this.instance.syncer.queue('onGetObject', {


+ 30
- 0
src/server/config/factions/helpers.js View File

@@ -0,0 +1,30 @@
//Imports
let factionBase = require('../factionBase');
let factions = require('../factions');

//Internals
const cache = {};

//Method
const getFactionBlueprint = factionId => {
if (cache[factionId])
return cache[factionId];

let res = null;
try {
res = factions.getFaction(factionId);
} catch (e) {}

if (!res)
return;

res = extend({}, factionBase, res);

cache[factionId] = res;

return res;
};

module.exports = {
getFactionBlueprint
};

+ 107
- 26
src/server/config/itemEffects/castSpellOnHit.js View File

@@ -1,37 +1,118 @@
//Imports
const spellBaseTemplate = require('../spells/spellTemplate');

//Helpers
const getItemEffect = item => {
return item.effects.find(e => (e.type === 'castSpellOnHit'));
};

const doesEventMatch = (firedEvent, eventCondition) => {
if (
!firedEvent ||
(
eventCondition.targetNotSelf === false &&
firedEvent.target === firedEvent.source
)
)
return false;

const foundNonMatch = Object.entries(eventCondition).some(([k, v]) => {
if (v !== null && typeof(v) === 'object') {
if (!doesEventMatch(firedEvent[k], v))
return true;

return false;
}

return firedEvent[k] !== v;
});

return !foundNonMatch;
};

const shouldApplyEffect = (itemEffect, firedEvent, firedEventName) => {
const { rolls: { chance, combatEvent: { [firedEventName]: eventCondition } } } = itemEffect;

if (!eventCondition || !doesEventMatch(firedEvent, eventCondition))
return false;

if (chance !== undefined && Math.random() * 100 >= chance)
return false;

return true;
};

const handler = (obj, item, event, firedEventName) => {
const itemEffect = getItemEffect(item);

if (!shouldApplyEffect(itemEffect, event, firedEventName))
return;

const { rolls: { castSpell, castTarget } } = itemEffect;

const spellConfig = extend({}, castSpell);
spellConfig.noEvents = true;

const scaleDamage = spellConfig.scaleDamage;
delete spellConfig.scaleDamage;

if (scaleDamage) {
if (scaleDamage.useOriginal) {
scaleDamage.useOriginal.forEach(s => {
spellConfig[s] = event.spell[s];
});
}

if (scaleDamage.percentage)
spellConfig.damage *= (scaleDamage.percentage / 100);
}

const typeTemplate = {
type: spellConfig.type[0].toUpperCase() + spellConfig.type.substr(1),
template: null
};
obj.instance.eventEmitter.emit('onBeforeGetSpellTemplate', typeTemplate);

if (!typeTemplate.template)
typeTemplate.template = require('../spells/spell' + typeTemplate.type);

const builtSpell = extend({ obj }, spellBaseTemplate, typeTemplate.template, spellConfig);

let target = event.target;
if (castTarget === 'self')
target = obj;
else if (castTarget === 'none')
target = undefined;
//Need to write a generic way to apply these
else if (castTarget === '{{event.oldPos}}')
target = extend({}, event.oldPos);
else if (JSON.stringify(castTarget) === '{"x":"{{event.follower.x}}","y":"{{event.follower.y}}"}') {
target = {
x: event.follower.x,
y: event.follower.y
};
}

builtSpell.cast({ target });
};

//Effect
module.exports = {
events: {
onGetText: function (item) {
const { rolls: { chance, spell, damage = 1 } } = item.effects.find(e => (e.type === 'castSpellOnHit'));
afterGiveHp: function (item, event) {
handler(this, item, event, 'afterGiveHp');
},

return `${chance}% chance to cast a ${damage} damage ${spell} on hit`;
afterDealDamage: function (item, event) {
handler(this, item, event, 'afterDealDamage');
},

afterDealDamage: function (item, damage, target) {
//Should only proc for attacks...this is kind of a hack
const { element } = damage;
if (element)
return;

const { rolls: { chance, spell, statType = 'dex', damage: spellDamage = 1 } } = item.effects.find(e => (e.type === 'castSpellOnHit'));

const chanceRoll = Math.random() * 100;
if (chanceRoll >= chance)
return;

const spellName = 'spell' + spell.replace(/./, spell.toUpperCase()[0]);
const spellTemplate = require(`../spells/${spellName}`);
const builtSpell = extend({ obj: this }, spellBaseTemplate, spellTemplate, {
name: spellName,
noEvents: true,
statType,
damage: spellDamage,
duration: 5,
radius: 1
});
afterPositionChange: function (item, event) {
handler(this, item, event, 'afterPositionChange');
},

builtSpell.cast();
afterFollowerDeath: function (item, event) {
handler(this, item, event, 'afterFollowerDeath');
}
}
};

+ 9
- 4
src/server/config/itemEffects/damageSelf.js View File

@@ -10,7 +10,7 @@ module.exports = {
return `you take ${rolls.percentage}% of the damage you deal`;
},

afterDealDamage: function (item, damage, target) {
afterDealDamage: function (item, { damage, target }) {
let effect = item.effects.find(e => (e.type === 'damageSelf'));
let rolls = effect.rolls;

@@ -29,9 +29,14 @@ module.exports = {
noCrit: true
});

newDamage.noEvents = true;

this.stats.takeDamage(newDamage, 1, this);
this.stats.takeDamage({
damage: newDamage,
threatMult: 1,
source: this,
target: this,
effectName: 'damageSelf',
noEvents: true
});
}
}
};

+ 1
- 1
src/server/config/itemEffects/freezeOnHit.js View File

@@ -6,7 +6,7 @@ module.exports = {
return `${rolls.chance}% chance on hit to freeze target for ${rolls.duration} ticks`;
},

afterDealDamage: function (item, damage, target) {
afterDealDamage: function (item, { damage, target }) {
let rolls = item.effects.find(e => (e.type === 'freezeOnHit')).rolls;

let chanceRoll = Math.random() * 100;


+ 7
- 3
src/server/config/itemEffects/gainStat.js View File

@@ -18,9 +18,13 @@ module.exports = {
amount = (cpnStats.values.hpMax / 100) * ~~amount.replace('%', '');

cpnStats.getHp({
amount: amount,
threatMult: 0
}, item);
heal: {
amount,
threatMult: 0
},
source: cpnStats.obj,
target: cpnStats.obj
});
} else
cpnStats.addStat(stat, amount);
},


+ 7
- 3
src/server/config/itemEffects/healOnCrit.js View File

@@ -21,7 +21,7 @@ module.exports = {
return text;
},

afterDealDamage: function (item, damage, target) {
afterDealDamage: function (item, { damage, target }) {
if (!damage.crit)
return;

@@ -34,8 +34,12 @@ module.exports = {
let amount = rolls.amount || ((damage.dealt / 100) * rolls.percentage);

this.stats.getHp({
amount: amount
}, this);
heal: {
amount
},
source: this,
target: this
});
}
}
};

+ 0
- 19
src/server/config/maps/mapList.js View File

@@ -1,19 +0,0 @@
let events = require('../../misc/events');

let config = [
{
name: 'cave',
path: 'config/maps'
},
{
name: 'fjolarok',
path: 'config/maps'
}
];

module.exports = {
init: function () {
events.emit('onBeforeGetMapList', config);
this.mapList = config;
}
};

+ 2
- 2
src/server/config/prophecies/crushable.js View File

@@ -10,8 +10,8 @@ module.exports = {
},

events: {
beforeTakeDamage: function (dmg, source) {
dmg.amount *= 4;
beforeTakeDamage: function ({ damage }) {
damage.amount *= 4;
}
}
};

+ 6
- 4
src/server/config/quests/questBuilder.js View File

@@ -1,7 +1,9 @@
let questTemplate = require('./templates/questTemplate');
let globalQuests = require('../questsBase');
let mapList = require('../maps/mapList');
//Imports
const questTemplate = require('./templates/questTemplate');
const globalQuests = require('../questsBase');
const { mapList } = require('../../world/mapManager');

//Exports
module.exports = {
instance: null,

@@ -11,7 +13,7 @@ module.exports = {

obtain: function (obj, template) {
let zoneName = template?.zoneName ?? obj.zoneName;
let zone = mapList.mapList.find(m => m.name === zoneName);
let zone = mapList.find(m => m.name === zoneName);

//Zone doesn't exist any more. Probably been renamed
if (!zone)


+ 1
- 1
src/server/config/serverConfig.js View File

@@ -1,7 +1,7 @@
/* eslint-disable no-process-env */

module.exports = {
version: '0.11.0',
version: '0.12.0',
port: 4000,
startupMessage: 'Server: ready',



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

@@ -183,7 +183,14 @@ module.exports = {
this.obj.aggro.move();

let damage = this.getDamage(target);
target.stats.takeDamage(damage, this.threatMult, obj);
target.stats.takeDamage({
damage,
threatMult: this.threatMult,
source: this.obj,
target,
spellName: 'ambush',
noEvents: this.noEvents
});
},

isTileValid: function (physics, fromX, fromY, toX, toY) {


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

@@ -38,8 +38,12 @@ let cpnArcanePatch = {
for (let i = 0; i < cLen; i++) {
let c = contents[i];

let amount = this.spell.getDamage(c, true);
c.stats.getHp(amount, this.caster);
let heal = this.spell.getDamage(c, true);
c.stats.getHp({
heal,
source: this.caster,
target: c
});
}
}
};


+ 8
- 1
src/server/config/spells/spellChainLightning.js View File

@@ -31,6 +31,13 @@ module.exports = {
return;
let damage = this.getDamage(target);
target.stats.takeDamage(damage, this.threatMult, this.obj);
target.stats.takeDamage({
damage,
threatMult: this.threatMult,
source: this.obj,
target,
spellName: 'chainLightning',
noEvents: this.noEvents
});
}
};

+ 20
- 5
src/server/config/spells/spellCharge.js View File

@@ -95,6 +95,18 @@ module.exports = {

let obj = this.obj;

const moveEvent = {
oldPos: {
x: obj.x,
y: obj.y
},
newPos: targetPos,
source: this,
target: this,
spellName: 'charge',
spell: this
};

obj.instance.physics.removeObject(obj, obj.x, obj.y);

obj.x = targetPos.x;
@@ -114,12 +126,15 @@ module.exports = {
targetEffect.ttl = this.stunDuration;

let damage = this.getDamage(target);
target.stats.takeDamage(damage, this.threatMult, obj);
target.stats.takeDamage({
damage,
threatMult: this.threatMult,
source: this.obj,
target,
spellName: 'charge',
noEvents: this.noEvents
});

const moveEvent = {
newPos: targetPos,
source: this
};
this.obj.fireEvent('afterPositionChange', moveEvent);

if (this.castOnEnd)


+ 23
- 3
src/server/config/spells/spellFireblast.js View File

@@ -48,9 +48,15 @@ module.exports = {
damage: 1,

cast: function (action) {
let obj = this.obj;
const { obj, targetPlayerPos } = this;

let { x, y, instance: { physics, syncer } } = obj;

if (!targetPlayerPos) {
x = action.target.x;
y = action.target.y;
}

let radius = this.radius;

const particleEvent = {
@@ -99,7 +105,14 @@ module.exports = {
m.clearQueue();

let damage = this.getDamage(m);
m.stats.takeDamage(damage, 1, obj);
m.stats.takeDamage({
damage,
threatMult: 1,
source: this.obj,
target: m,
spellName: 'fireblast',
noEvents: this.noEvents
});

if (m.destroyed)
continue;
@@ -173,11 +186,18 @@ module.exports = {
syncer.o.y = yFinal;

const moveEvent = {
oldPos: {
x: xOld,
y: yOld
},
newPos: {
x: xFinal,
y: yFinal
},
source: this
source: this.obj,
target,
spellName: 'fireblast',
spell: this
};
target.fireEvent('afterPositionChange', moveEvent);
}


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

@@ -8,8 +8,12 @@ let cpnHealPatch = {
this[p] = blueprint[p];
},

applyHeal: function (o, amount) {
o.stats.getHp(amount, this.caster);
applyHeal: function (target, heal) {
target.stats.getHp({
heal,
source: this.caster,
target
});
},

collisionEnter: function (o) {


+ 8
- 1
src/server/config/spells/spellIceSpear.js View File

@@ -60,6 +60,13 @@ module.exports = {
});

let damage = this.getDamage(target);
target.stats.takeDamage(damage, this.threatMult, this.obj);
target.stats.takeDamage({
damage,
threatMult: this.threatMult,
source: this.obj,
target,
spellName: 'iceSpear',
noEvents: this.noEvents
});
}
};

+ 8
- 1
src/server/config/spells/spellMelee.js View File

@@ -43,6 +43,13 @@ module.exports = {
return;

let damage = this.getDamage(target);
target.stats.takeDamage(damage, this.threatMult, this.obj);
target.stats.takeDamage({
damage,
threatMult: this.threatMult,
source: this.obj,
target,
spellName: 'melee',
noEvents: this.noEvents
});
}
};

+ 8
- 1
src/server/config/spells/spellProjectile.js View File

@@ -138,6 +138,13 @@ module.exports = {
if (this.applyEffect)
target.effects.addEffect(this.applyEffect, this.obj);

target.stats.takeDamage(damage, this.threatMult, this.obj);
target.stats.takeDamage({
damage,
threatMult: this.threatMult,
source: this.obj,
target,
spellName: 'projectile',
noEvents: this.noEvents
});
}
};

+ 12
- 2
src/server/config/spells/spellSingleTargetHeal.js View File

@@ -17,8 +17,18 @@ module.exports = {
const target = action.target;
const { x, y } = target;

const amount = this.getDamage(target, true);
target.stats.getHp(amount, this.obj);
const heal = this.getDamage(target, true);

const event = {
heal,
source: this.obj,
target,
spellName: 'singleTargetHeal',
spell: this,
noEvents: this.noEvents
};

target.stats.getHp(event);

const effect = {
x,


+ 8
- 1
src/server/config/spells/spellSlash.js View File

@@ -43,6 +43,13 @@ module.exports = {
return;

let damage = this.getDamage(target);
target.stats.takeDamage(damage, this.threatMult, this.obj);
target.stats.takeDamage({
damage,
threatMult: this.threatMult,
source: this.obj,
target,
spellName: 'slash',
noEvents: this.noEvents
});
}
};

+ 8
- 1
src/server/config/spells/spellSlowBlast.js View File

@@ -89,7 +89,14 @@ module.exports = {
continue;

let damage = this.getDamage(m);
m.stats.takeDamage(damage, 1, obj);
m.stats.takeDamage({
damage,
threatMult: 1,
source: obj,
target: m,
spellName: 'slowBlast',
noEvents: this.noEvents
});
}
}
}


+ 8
- 1
src/server/config/spells/spellSmite.js View File

@@ -42,7 +42,14 @@ module.exports = {
return;

let damage = this.getDamage(target);
target.stats.takeDamage(damage, this.threatMult, this.obj);
target.stats.takeDamage({
damage,
threatMult: this.threatMult,
source: this.obj,
target,
spellName: 'smite',
noEvents: this.noEvents
});

target.effects.addEffect({
type: 'stunned',


+ 11
- 3
src/server/config/spells/spellSmokeBomb.js View File

@@ -4,8 +4,15 @@ let cpnSmokePatch = {
contents: [],
ttl: 0,

applyDamage: function (o, amount) {
o.stats.takeDamage(amount, 1, this.caster);
applyDamage: function (target, damage) {
target.stats.takeDamage({
damage,
threatMult: 1,
source: this.caster,
target: target,
spellName: 'smokeBomb',
noEvents: this.noEvents
});
},

collisionEnter: function (o) {
@@ -171,7 +178,8 @@ module.exports = {
caster: obj,
statType: this.statType,
getDamage: this.getDamage.bind(this),
ttl: this.duration
ttl: this.duration,
noEvents: this.noEvents
}
}
}]);


+ 7
- 3
src/server/config/spells/spellSummonConsumableFollower.js View File

@@ -96,9 +96,13 @@ module.exports = {
mLen--;
} else if ((Math.abs(x - m.x) <= 1) && (Math.abs(y - m.y) <= 1)) {
m.destroyed = true;
this.obj.stats.getHp({
amount: obj.stats.values.hpMax / 10
}, obj);
obj.stats.getHp({
heal: {
amount: obj.stats.values.hpMax / 10
},
source: obj,
target: obj
});

obj.instance.syncer.queue('onGetObject', {
x: m.x,


+ 2
- 1
src/server/config/spells/spellTemplate.js View File

@@ -292,7 +292,8 @@ module.exports = {
isAttack: this.isAttack,
noScale: this.noScale,
noMitigate: noMitigate,
spell: this
spell: this,
scaleConfig: this.scaleConfig
};

if (this.obj.mob)


+ 10
- 2
src/server/config/spells/spellTrailDash.js View File

@@ -3,8 +3,15 @@ let cpnSpikePatch = {

contents: [],

applyDamage: function (o, amount) {
o.stats.takeDamage(amount, 1, this.caster);
applyDamage: function (target, damage) {
target.stats.takeDamage({
damage,
threatMult: 1,
source: this.caster,
target: target,
spellName: 'smokeBomb',
noEvents: this.noEvents
});
},

collisionEnter: function (o) {
@@ -116,6 +123,7 @@ module.exports = {
}]);
spike.spikePatch.caster = this.obj;
spike.spikePatch.damage = this.damage;
spike.spikePatch.noEvents = this.noEvents;

this.queueCallback(null, this.spikeDuration * consts.tickTime, this.endEffect.bind(this, spike), null, true);



+ 8
- 1
src/server/config/spells/spellWarnBlast.js View File

@@ -124,7 +124,14 @@ module.exports = {
continue;

let damage = this.getDamage(m);
m.stats.takeDamage(damage, 1, obj);
m.stats.takeDamage({
damage,
threatMult: 1,
source: obj,
target: m,
spellName: 'warnBlast',
noEvents: this.noEvents
});
}
}
};

+ 8
- 2
src/server/config/spells/spellWhirlwind.js View File

@@ -3,8 +3,14 @@ const coordinateDeltas = [
[[0, -2], [0, -1], [1, -2], [2, -2], [1, -1], [2, -1], [2, 0], [1, 0], [2, 1], [2, 2], [1, 1], [1, 2], [0, 2], [0, 1], [-1, 2], [-2, 2], [-2, 1], [-1, 1], [-2, 0], [-1, 0], [-2, -1], [-2, -2], [-1, -1], [-1, -2]]
];

const applyDamage = (target, damage, threat, source) => {
target.stats.takeDamage(damage, threat, source);
const applyDamage = (target, damage, threatMult, source) => {
target.stats.takeDamage({
damage,
threatMult,
source,
target,
spellName: 'whirlwind'
});
};

const dealDamage = (spell, obj, coords) => {


+ 0
- 11
src/server/config/zoneBase.js View File

@@ -36,17 +36,6 @@ module.exports = {
rolls: 2,
magicFind: 2000
}
},

champion: {
hpMult: 5,
dmgMult: 2,

drops: {
chance: 100,
rolls: 2,
magicFind: [2000, 175]
}
}
}
}


+ 30
- 4
src/server/db/ioRethink.js View File

@@ -77,11 +77,23 @@ module.exports = {
return res;
},

getFilterAsync: async function ({ table, noDefault, filter }) {
const res = await r
getFilterAsync: async function (
{ table, noDefault, filter, limit, offset, orderAsc, orderDesc }
) {
let res = r
.table(table)
.filter(filter)
.run();
.filter(filter);

if (orderAsc)
res = res.orderBy(orderAsc);
if (orderDesc)
res = res.orderBy(r.desc(orderDesc));
if (offset)
res = res.skip(offset);
if (limit)
res = res.limit(limit);

await res.run();

if (res)
return res;
@@ -129,6 +141,20 @@ module.exports = {
}
},

setFlat: async function ({
table,
value,
conflict = 'update'
}) {
try {
await r.table(table)
.insert(value, { conflict })
.run();
} catch (e) {
this.logError(e, table, JSON.stringify(value));
}
},

deleteAsync: async function ({
key,
table


+ 5
- 6
src/server/db/tableNames.js View File

@@ -1,15 +1,14 @@
const tableNames = [
'accountInfo',
'character',
'characterList',
'stash',
'login',
'leaderboard',
'customMap',
'customChannels',
'error',
'leaderboard',
'login',
'modLog',
'accountInfo',
'recipes'
'recipes',
'stash'
];

module.exports = tableNames;

+ 9
- 4
src/server/events/events.js View File

@@ -1,7 +1,11 @@
let phaseTemplate = require('./phases/phaseTemplate');
let fs = require('fs');
let mapList = require('../config/maps/mapList');
//System Imports
const fs = require('fs');

//Imports
const phaseTemplate = require('./phases/phaseTemplate');
const { mapList } = require('../world/mapManager');

//Helpers
const applyVariablesToDescription = (desc, variables) => {
if (!variables)
return desc;
@@ -14,6 +18,7 @@ const applyVariablesToDescription = (desc, variables) => {
return desc;
};

//Exports
module.exports = {
configs: [],
nextId: 0,
@@ -22,7 +27,7 @@ module.exports = {
this.instance = instance;

const zoneName = this.instance.map.name;
const zonePath = mapList.mapList.find(z => z.name === zoneName).path;
const zonePath = mapList.find(z => z.name === zoneName).path;
const zoneEventPath = zonePath + '/' + zoneName + '/events';

const paths = ['config/globalEvents', zoneEventPath];


+ 1
- 1
src/server/events/phases/phaseKillMob.js View File

@@ -4,7 +4,7 @@ let cpnDeathStopper = {
end: false,

events: {
beforeTakeDamage: function (damage, source) {
beforeTakeDamage: function ({ damage }) {
let statValues = this.obj.stats.values;
let minHp = statValues.hpMax * this.percentage;
if (statValues.hp - damage.amount < minHp) {


+ 61
- 1
src/server/fixes/fixes.js View File

@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */

module.exports = {
fixDb: async function () {
await io.deleteAsync({
@@ -117,7 +119,65 @@ module.exports = {
});

items
.filter(f => ((f.effects) && (f.effects[0].factionId === 'akarei') && (!f.effects[0].properties)))
.filter(i => i.name === 'Gourdhowl')
.forEach(i => {
const effect = i.effects[0];

if (!effect.rolls.castSpell) {
effect.rolls = {
castSpell: {
type: 'whirlwind',
damage: effect.rolls.damage,
range: 1,
statType: 'str',
statMult: 1,
isAttack: true
},
castTarget: 'none',
chance: effect.rolls.chance,
textTemplate: 'Grants you a ((chance))% chance to cast a ((castSpell.damage)) damage whirlwind on hit',
combatEvent: {
name: 'afterDealDamage',
afterDealDamage: {
spellName: 'melee'
}
}
};
}
});

items
.filter(i => i.name === 'Putrid Shank')
.forEach(i => {
const effect = i.effects[0];

if (!effect.rolls.castSpell) {
effect.rolls = {
chance: effect.rolls.chance,
textTemplate: 'Grants you a ((chance))% chance to cast a ((castSpell.damage)) damage smokebomb on hit',
combatEvent: {
name: 'afterDealDamage',
afterDealDamage: {
spellName: 'melee'
}
},
castTarget: 'none',
castSpell: {
type: 'smokebomb',
damage: 1,
range: 1,
element: 'poison',
statType: 'dex',
statMult: 1,
duration: 5,
isAttack: true
}
};
}
});

items
.filter(f => f.effects?.[0]?.factionId === 'akarei' && !f.effects[0].properties)
.forEach(function (i) {
let effect = i.effects[0];
let chance = parseFloat(effect.text.split(' ')[0].replace('%', ''));


+ 19
- 16
src/server/index.js View File

@@ -1,20 +1,22 @@
require('./globals');

let server = require('./server/index');
let components = require('./components/components');
let mods = require('./misc/mods');
let animations = require('./config/animations');
let skins = require('./config/skins');
let factions = require('./config/factions');
let classes = require('./config/spirits');
let spellsConfig = require('./config/spellsConfig');
let spells = require('./config/spells');
let itemTypes = require('./items/config/types');
let recipes = require('./config/recipes/recipes');
let mapList = require('./config/maps/mapList');
let fixes = require('./fixes/fixes');
let profanities = require('./misc/profanities');
const server = require('./server/index');
const components = require('./components/components');
const mods = require('./misc/mods');
const animations = require('./config/animations');
const skins = require('./config/skins');
const factions = require('./config/factions');
const classes = require('./config/spirits');
const spellsConfig = require('./config/spellsConfig');
const spells = require('./config/spells');
const itemTypes = require('./items/config/types');
const salvager = require('./items/salvager');
const recipes = require('./config/recipes/recipes');
const mapManager = require('./world/mapManager');
const fixes = require('./fixes/fixes');
const profanities = require('./misc/profanities');
const routerConfig = require('./security/routerConfig');
const { spawnMapThreads } = require('./world/threadManager');

let startup = {
init: function () {
@@ -40,8 +42,9 @@ let startup = {
spells.init();
recipes.init();
itemTypes.init();
salvager.init();
profanities.init();
mapList.init();
mapManager.init();
components.init(this.onComponentsReady.bind(this));
},

@@ -55,7 +58,7 @@ let startup = {

await leaderboard.init();

atlas.init();
await spawnMapThreads();
},

onError: async function (e) {


+ 8
- 0
src/server/items/generator.js View File

@@ -75,6 +75,14 @@ module.exports = {
if (blueprint.isSpell)
isSpell = true;

const beforeGenerateItemEvent = {
blueprint,
item: null
};
global.instancer.instances[0].eventEmitter.emit('beforeGenerateItem', beforeGenerateItemEvent);
if (beforeGenerateItemEvent.item)
return beforeGenerateItemEvent.item;

if (isSpell)
spellGenerators.forEach(g => g.generate(item, blueprint));
else if (isCurrency)


+ 39
- 16
src/server/items/generators/effects.js View File

@@ -1,3 +1,40 @@
const rollValues = (rollsDefinition, result) => {
for (let p in rollsDefinition) {
const entry = rollsDefinition[p];

if (typeof(entry) === 'object' && entry !== null && !Array.isArray(entry) ) {
const newResult = {};

result[p] = newResult;

rollValues(entry, newResult);

continue;
}

const range = entry;
const isInt = (p.indexOf('i_') === 0);
const fieldName = p.replace('i_', '');

//Keys that start with s_ indicate that they shouldn't be rolled
// We use this to allow arrays inside rolls to be hardcoded
if (!Array.isArray(entry) || p.indexOf('s_') === 0) {
if (p.indexOf('s_') === 0)
result[p.substr(2)] = range;
else
result[fieldName] = range;

continue;
}

let value = range[0] + (Math.random() * (range[1] - range[0]));
if (isInt)
value = ~~value;

result[fieldName] = value;
}
};

module.exports = {
generate: function (item, blueprint) {
if (!blueprint.effects)
@@ -6,22 +43,8 @@ module.exports = {
item.effects = blueprint.effects.map(function (e) {
let rolls = e.rolls;
let newRolls = {};
for (let p in rolls) {
let isInt = (p.indexOf('i_') === 0);
let fieldName = p.replace('i_', '');

let range = rolls[p];
if (!range.push) {
newRolls[fieldName] = range;
continue;
}

let value = range[0] + (Math.random() * (range[1] - range[0]));
if (isInt)
value = ~~value;

newRolls[fieldName] = value;
}

rollValues(rolls, newRolls);

return {
type: e.type,


+ 1
- 1
src/server/items/generators/spellbook.js View File

@@ -47,7 +47,7 @@ module.exports = {
generate: function (item, blueprint) {
blueprint = blueprint || {};
let spellQuality = blueprint ? blueprint.spellQuality : '';
let spellName = blueprint.spellName;
let spellName = blueprint.spellName?.replaceAll('_', ' ');

if (!spellName) {
let spellList = Object.keys(spellsConfig.spells).filter(s => !spellsConfig.spells[s].auto && !spellsConfig.spells[s].noDrop);


+ 4
- 1
src/server/items/generators/types.js View File

@@ -8,7 +8,10 @@ module.exports = {
if (!type || !configTypes.types[item.slot][type]) {
//Pick a material type first
const types = configTypes.types[item.slot];
const typeArray = Object.entries(types);
const typeArray = Object
.entries(types)
.filter(t => t.noDrop !== true);

const materials = Object.values(types)
.map(t => {
return t.material;


+ 10
- 2
src/server/items/salvager.js View File

@@ -1,4 +1,7 @@
let mappings = {
//Imports
const events = require('../misc/events');

const mappings = {
rune: [{
materials: [{
name: 'Essence',
@@ -68,7 +71,7 @@ let mappings = {
}]
};

let materialItems = {
const materialItems = {
'Iron Bar': {
sprite: [0, 0]
},
@@ -99,6 +102,11 @@ let materialItems = {
};
module.exports = {
init: function () {
events.emit('onBeforeGetSalvagerMappings', {
mappings
});
},
salvage: function (item, maxRoll) {
let result = [];



+ 12
- 3
src/server/mods/class-necromancer/spells/spellBloodBarrier.js View File

@@ -35,12 +35,21 @@ module.exports = {
return;

let amount = (obj.stats.values.hpMax / 100) * this.drainPercentage;
const damage = { amount };
obj.stats.takeDamage(damage, 0, obj);
obj.stats.takeDamage({
damage: { amount },
threatMult: 0,
source: obj,
target: obj,
spellName: 'bloodBarrier'
});

amount = amount * this.shieldMultiplier;
const heal = { amount };
target.stats.getHp(heal, obj);
target.stats.getHp({
heal,
source: obj,
target
});

//Only reset the first spell's cooldown if it's an auto attack and not a spell
const firstSpell = target.spellbook.spells[0];


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save