Browse Source

Merge branch 'master' into 'release'

feat #1866: Moved save to after delete and ensured that deleted objects aren't...

See merge request Isleward/isleward!580
tags/v0.10.6
Big Bad Waffle 2 years ago
parent
commit
680ae2e86f
88 changed files with 4210 additions and 1118 deletions
  1. BIN
     
  2. BIN
     
  3. BIN
     
  4. BIN
     
  5. +2
    -2
      src/client/js/components/components.js
  6. +13
    -2
      src/client/js/misc/physics.js
  7. +1
    -1
      src/client/js/misc/statTranslations.js
  8. +1
    -1
      src/client/js/objects/objBase.js
  9. +14
    -16
      src/client/js/objects/objects.js
  10. +45
    -0
      src/client/js/rendering/helpers/resetRenderer.js
  11. +5
    -2
      src/client/js/rendering/renderer.js
  12. +78
    -25
      src/client/js/system/client.js
  13. +1
    -1
      src/client/package.json
  14. +38
    -26
      src/client/ui/factory.js
  15. +0
    -59
      src/client/ui/templates/buffs/buffs.js
  16. +0
    -1
      src/client/ui/templates/buffs/styles.css
  17. +0
    -1
      src/client/ui/templates/buffs/template.html
  18. +0
    -1
      src/client/ui/templates/characters/characters.js
  19. +0
    -1
      src/client/ui/templates/createCharacter/createCharacter.js
  20. +2
    -2
      src/client/ui/templates/death/death.js
  21. +54
    -0
      src/client/ui/templates/effects/effects.js
  22. +1
    -0
      src/client/ui/templates/effects/styles.css
  23. +2
    -2
      src/client/ui/templates/effects/styles.less
  24. +1
    -0
      src/client/ui/templates/effects/template.html
  25. +1
    -1
      src/client/ui/templates/effects/templateEffect.html
  26. +2
    -2
      src/client/ui/templates/events/events.js
  27. +9
    -9
      src/client/ui/templates/inventory/inventory.js
  28. +2
    -2
      src/client/ui/templates/login/template.html
  29. +7
    -7
      src/client/ui/templates/mainMenu/mainMenu.js
  30. +2
    -2
      src/client/ui/templates/quests/quests.js
  31. +45
    -22
      src/client/ui/templates/stash/stash.js
  32. +1
    -5
      src/client/ui/templates/talk/talk.js
  33. +3
    -1
      src/server/.eslintrc
  34. +83
    -91
      src/server/clientComponents/effects.js
  35. +136
    -0
      src/server/clientComponents/effects/auras.js
  36. +0
    -8
      src/server/clientComponents/gatherer.js
  37. +4
    -4
      src/server/clientComponents/pather.js
  38. +2
    -2
      src/server/clientComponents/player.js
  39. +4
    -0
      src/server/clientComponents/serverActions.js
  40. +2
    -44
      src/server/clientComponents/stash.js
  41. +4
    -0
      src/server/components/aggro.js
  42. +16
    -4
      src/server/components/auth.js
  43. +73
    -77
      src/server/components/effects.js
  44. +1
    -1
      src/server/components/extensions/socialCommands.js
  45. +5
    -8
      src/server/components/inventory.js
  46. +31
    -23
      src/server/components/mob.js
  47. +2
    -10
      src/server/components/player.js
  48. +11
    -9
      src/server/components/portal.js
  49. +42
    -19
      src/server/components/portal/sendObjToZone.js
  50. +1
    -1
      src/server/components/social.js
  51. +3
    -0
      src/server/components/social/rezone.js
  52. +27
    -31
      src/server/components/spellbook.js
  53. +131
    -0
      src/server/components/spellbook/rotationManager.js
  54. +99
    -69
      src/server/components/stash.js
  55. +5
    -1
      src/server/components/stats.js
  56. +9
    -3
      src/server/config/clientConfig.js
  57. +2
    -1
      src/server/config/effects/effectStunned.js
  58. +15
    -1
      src/server/config/effects/effectTemplate.js
  59. +1
    -1
      src/server/config/serverConfig.js
  60. +0
    -5
      src/server/config/skins.js
  61. +51
    -0
      src/server/config/spells.js
  62. +4
    -5
      src/server/config/spells/spellAura.js
  63. +3
    -12
      src/server/config/spells/spellCharge.js
  64. +2
    -3
      src/server/config/spells/spellFireblast.js
  65. +1
    -9
      src/server/config/spells/spellIceSpear.js
  66. +1
    -1
      src/server/config/spells/spellReflectDamage.js
  67. +41
    -0
      src/server/config/spells/spellSingleTargetHeal.js
  68. +5
    -3
      src/server/config/spells/spellStealth.js
  69. +1
    -1
      src/server/config/spells/spellTrailDash.js
  70. +19
    -7
      src/server/config/spellsConfig.js
  71. +2
    -2
      src/server/events/phases/phaseGiveRewards.js
  72. +2
    -1
      src/server/misc/messages.js
  73. +1
    -1
      src/server/mods/class-necromancer/index.js
  74. +8
    -18
      src/server/objects/objBase.js
  75. +71
    -41
      src/server/objects/objects.js
  76. +2772
    -367
      src/server/package-lock.json
  77. +2
    -2
      src/server/package.json
  78. +5
    -2
      src/server/security/connections.js
  79. +4
    -3
      src/server/security/routerConfig.js
  80. +7
    -6
      src/server/world/atlas.js
  81. +22
    -9
      src/server/world/instancer.js
  82. +49
    -0
      src/server/world/instancer/handshakes.js
  83. +7
    -2
      src/server/world/map.js
  84. +2
    -15
      src/server/world/mobBuilder.js
  85. +55
    -0
      src/server/world/rezoneManager.js
  86. +0
    -1
      src/server/world/spawners.js
  87. +29
    -0
      src/server/world/syncer.js
  88. +5
    -0
      src/server/world/worker.js

BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 2
- 2
src/client/js/components/components.js View File

@@ -31,7 +31,7 @@ define([
if (cpn.type)
templates.push(tpl);
if (cpn.extends)
extenders.push(tpl);
extenders.push({ extends: cpn.extends, tpl });
res();
});
@@ -49,7 +49,7 @@ define([
templates.forEach(t => {
const extensions = extenders.filter(e => e.extends === t.type);

extensions.forEach(e => $.extend(true, t, e));
extensions.forEach(e => $.extend(true, t, e.tpl));

t.eventList = {};
t.hookEvent = hookEvent;


+ 13
- 2
src/client/js/misc/physics.js View File

@@ -1,7 +1,9 @@
define([
'js/misc/distanceToPolygon'
'js/misc/distanceToPolygon',
'js/system/events'
], function (
distanceToPolygon
distanceToPolygon,
events
) {
return {
grid: null,
@@ -10,6 +12,8 @@ define([
height: 0,

init: function (collisionMap) {
events.on('resetPhysics', this.reset.bind(this));

this.width = collisionMap.length;
this.height = collisionMap[0].length;

@@ -22,6 +26,13 @@ define([
}
},

reset: function () {
this.width = 0;
this.height = 0;

this.grid = [];
},

isTileBlocking: function (x, y, mob, obj) {
if ((x < 0) || (y < 0) || (x >= this.width) | (y >= this.height))
return true;


+ 1
- 1
src/client/js/misc/statTranslations.js View File

@@ -52,7 +52,7 @@ define([
attackSpeed: 'attack speed',
castSpeed: 'cast speed',

lifeOnHit: 'life gained on hit',
lifeOnHit: 'life gained on dealing physical damage',

auraReserveMultiplier: 'aura mana reservation multiplier',



+ 1
- 1
src/client/js/objects/objBase.js View File

@@ -174,7 +174,7 @@ define([

offEvents: function () {
if (this.pather)
this.pather.onDeath();
this.pather.resetPath();

for (let e in this.eventCallbacks)
this.eventCallbacks[e].forEach(c => events.off(e, c));


+ 14
- 16
src/client/js/objects/objects.js View File

@@ -15,11 +15,15 @@ define([
objects: [],

init: function () {
events.on('onGetObject', this.onGetObject.bind(this));
events.on('onRezone', this.onRezone.bind(this));
events.on('onChangeHoverTile', this.getLocation.bind(this));
events.on('onTilesVisible', this.onTilesVisible.bind(this));
events.on('onToggleNameplates', this.onToggleNameplates.bind(this));

[
'onGetObject',
'onTilesVisible',
'onToggleNameplates',
'destroyAllObjects'
]
.forEach(e => events.on(e, this[e].bind(this)));
},

getLocation: function (x, y) {
@@ -87,20 +91,14 @@ define([
return list[fromIndex];
},

onRezone: function (oldZone) {
let objects = this.objects;
let oLen = objects.length;
for (let i = 0; i < oLen; i++) {
let o = objects[i];
destroyAllObjects: function () {
this.objects.forEach(o => {
o.destroy();
});

if (oldZone === null)
o.destroy();
else if (o.zoneId === oldZone)
o.destroy();
}
this.objects.length = 0;

if (window.player)
window.player.offEvents();
window?.player?.offEvents();
},

onGetObject: function (obj) {


+ 45
- 0
src/client/js/rendering/helpers/resetRenderer.js View File

@@ -0,0 +1,45 @@
define([
'js/rendering/spritePool'
], function (
spritePool
) {
return function () {
let map = this.map;
let w = this.w = map.length;
let h = this.h = map[0].length;

this.stage.removeChild(this.layers.hiders);
this.layers.hiders = new PIXI.Container();
this.layers.hiders.layer = 'hiders';
this.stage.addChild(this.layers.hiders);

let container = this.layers.tileSprites;
this.stage.removeChild(container);

this.layers.tileSprites = container = new PIXI.Container();
container.layer = 'tiles';
this.stage.addChild(container);

this.stage.children.sort((a, b) => {
if (a.layer === 'hiders')
return 1;
else if (b.layer === 'hiders')
return -1;
else if (a.layer === 'tiles')
return -1;
else if (b.layer === 'tiles')
return 1;
return 0;
});

spritePool.clean();

this.sprites = _.get2dArray(w, h, 'array');

this.map = [];
this.w = 0;
this.h = 0;

delete this.moveTo;
};
});

+ 5
- 2
src/client/js/rendering/renderer.js View File

@@ -8,7 +8,8 @@ define([
'js/rendering/shaders/outline',
'js/rendering/spritePool',
'js/system/globals',
'js/rendering/renderLoginBackground'
'js/rendering/renderLoginBackground',
'js/rendering/helpers/resetRenderer'
], function (
resources,
events,
@@ -19,7 +20,8 @@ define([
shaderOutline,
spritePool,
globals,
renderLoginBackground
renderLoginBackground,
resetRenderer
) {
const mRandom = Math.random.bind(Math);

@@ -84,6 +86,7 @@ define([
events.on('onGetMap', this.onGetMap.bind(this));
events.on('onToggleFullscreen', this.toggleScreen.bind(this));
events.on('onMoveSpeedChange', this.adaptCameraMoveSpeed.bind(this));
events.on('resetRenderer', resetRenderer.bind(this));

this.width = $('body').width();
this.height = $('body').height();


+ 78
- 25
src/client/js/system/client.js View File

@@ -18,7 +18,38 @@ define([
this.socket.on('event', this.onEvent.bind(this));
this.socket.on('events', this.onEvents.bind(this));
this.socket.on('dc', this.onDisconnect.bind(this));

Object.entries(this.processAction).forEach(([k, v]) => {
this.processAction[k] = v.bind(this);
});
},

onRezoneStart: function () {
//Fired for mods to listen to
events.emit('rezoneStart');

events.emit('destroyAllObjects');
events.emit('resetRenderer');
events.emit('resetPhysics');
events.emit('clearUis');

client.request({
threadModule: 'rezoneManager',
method: 'clientAck',
data: {}
});
},

onGetMap: function ([msg]) {
events.emit('onGetMap', msg);

client.request({
threadModule: 'instancer',
method: 'clientAck',
data: {}
});
},

onConnected: function (onReady) {
if (this.doneConnect)
this.onDisconnect();
@@ -28,45 +59,67 @@ define([
if (onReady)
onReady();
},

onDisconnect: function () {
window.location = window.location;
},

onHandshake: function () {
events.emit('onHandshake');
this.socket.emit('handshake');
},

request: function (msg) {
this.socket.emit('request', msg, msg.callback);
},
onEvent: function (response) {
events.emit(response.event, response.data);
},
onEvents: function (response) {
//If we get objects, self needs to be first
// otherwise we might create the object (setting his position or attack animation)
// before instantiating it
let oList = response.onGetObject;
if (oList) {
let prepend = oList.filter(o => o.self);
oList.spliceWhere(o => prepend.some(p => p === o));
oList.unshift.apply(oList, prepend);
}

for (let e in response) {
let r = response[e];
processAction: {
default: function (eventName, msgs) {
msgs.forEach(m => events.emit(eventName, m));
},

//Certain messages expect to be performed last (because the object they act on hasn't been created when they get queued)
r.sort(function (a, b) {
if (a.performLast)
return 1;
else if (b.performLast)
return -1;
return 0;
});
rezoneStart: function (eventName, msgs) {
events.emit('rezoneStart');

events.emit('destroyAllObjects');
events.emit('resetRenderer');
events.emit('resetPhysics');
events.emit('clearUis');

r.forEach(function (o) {
events.emit(e, o);
client.request({
threadModule: 'rezoneManager',
method: 'clientAck',
data: {}
});
},

getMap: function (eventName, msgs) {
events.emit('onBuildIngameUis');
events.emit('onGetMap', msgs[0]);
},

onGetObject: function (eventName, msgs) {
const prepend = msgs.filter(o => o.self);
msgs.spliceWhere(o => prepend.some(p => p === o));
msgs.unshift.apply(msgs, prepend);

this.processAction.default(eventName, msgs);
}
},

onEvent: function ({ event: eventName, data: eventData }) {
const handler = this.processAction[eventName] || this.processAction.default;

handler(eventName, [eventData]);
},

onEvents: function (response) {
for (let eventName in response) {
const eventMsgs = response[eventName];

const handler = this.processAction[eventName] || this.processAction.default;

handler(eventName, eventMsgs);
}
}
};


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

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


+ 38
- 26
src/client/ui/factory.js View File

@@ -14,12 +14,13 @@ define([
return {
uis: [],
root: '',
ingameUisBuilt: false,

init: function (root) {
if (root)
this.root = root + '/';

events.on('onEnterGame', this.onEnterGame.bind(this));
events.on('onBuildIngameUis', this.onBuildIngameUis.bind(this));
events.on('onUiKeyDown', this.onUiKeyDown.bind(this));
events.on('onResize', this.onResize.bind(this));

@@ -31,39 +32,40 @@ define([
});
},

onEnterGame: async function () {
events.clearQueue();
onBuildIngameUis: async function () {
if (!this.ingameUisBuilt) {
events.clearQueue();

await Promise.all(
globals.clientConfig.uiList.map(u => {
const uiType = u.path ? u.path.split('/').pop() : u;
await Promise.all(
globals.clientConfig.uiList.map(u => {
const uiType = u.path ? u.path.split('/').pop() : u;

return new Promise(res => {
const doneCheck = () => {
const isDone = this.uis.some(ui => ui.type === uiType);
if (isDone) {
res();
return new Promise(res => {
const doneCheck = () => {
const isDone = this.uis.some(ui => ui.type === uiType);
if (isDone) {
res();

return;
}
return;
}

setTimeout(doneCheck, 100);
};
setTimeout(doneCheck, 100);
};

this.build(uiType, { path: u.path });
this.build(uiType, { path: u.path });

doneCheck();
});
})
);
doneCheck();
});
})
);

this.ingameUisBuilt = true;
}

client.request({
cpn: 'player',
method: 'performAction',
data: {
cpn: 'player',
method: 'notifyServerUiReady'
}
threadModule: 'instancer',
method: 'clientAck',
data: {}
});
},

@@ -169,6 +171,16 @@ define([
}
},

exitGame: function () {
$('[class^="ui"]:not(.ui-container)').toArray().forEach(el => {
let ui = $(el).data('ui');
if (ui && ui.destroy)
ui.destroy();
});

this.ingameUisBuilt = false;
},

getUi: function (type) {
return this.uis.find(u => u.type === type);
}


+ 0
- 59
src/client/ui/templates/buffs/buffs.js View File

@@ -1,59 +0,0 @@
define([
'js/system/events',
'html!ui/templates/buffs/template',
'css!ui/templates/buffs/styles',
'html!ui/templates/buffs/templateBuff'
], function (
events,
template,
styles,
templateBuff
) {
let icons = {
stunned: [4, 0],
regenHp: [3, 1],
regenMana: [4, 1],
swiftness: [5, 1],
stealth: [7, 0],
reflectDamage: [2, 1],
holyVengeance: [4, 0]
};

return {
tpl: template,

icons: {},

postRender: function () {
this.onEvent('onGetBuff', this.onGetBuff.bind(this));
this.onEvent('onRemoveBuff', this.onRemoveBuff.bind(this));
},

onGetBuff: function (buff) {
let icon = icons[buff.type];
if (!icon)
return;

let imgX = icon[0] * -32;
let imgY = icon[1] * -32;

let html = templateBuff;
let el = $(html).appendTo(this.el)
.find('.inner')
.css({
background: 'url(../../../images/statusIcons.png) ' + imgX + 'px ' + imgY + 'px'
});

this.icons[buff.id] = el.parent();
},

onRemoveBuff: function (buff) {
let el = this.icons[buff.id];
if (!el)
return;

el.remove();
delete this.icons[buff.id];
}
};
});

+ 0
- 1
src/client/ui/templates/buffs/styles.css View File

@@ -1 +0,0 @@
.uiBuffs{position:absolute;left:16px;top:104px}.uiBuffs .icon{width:40px;height:40px;padding:4px;background-color:rgba(49,33,54,.75);margin-right:16px;float:left}.uiBuffs .icon .inner{width:32px;height:32px;background:url(../../../images/statusIcons.png) 0 0}.mobile .uiBuffs{left:316px;top:10px}

+ 0
- 1
src/client/ui/templates/buffs/template.html View File

@@ -1 +0,0 @@
<div class="uiBuffs"></div>

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

@@ -97,7 +97,6 @@ define([
onPlay: function () {
this.el.removeClass('disabled');
this.destroy();
events.emit('onEnterGame');
},

onNewClick: function () {


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

@@ -151,7 +151,6 @@ define([
if (!result) {
this.clear();
this.destroy();
events.emit('onEnterGame');
} else
this.el.find('.message').html(result);
},


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

@@ -20,14 +20,14 @@ define([
this.onEvent('onPermadeath', this.onPermadeath.bind(this));

this.find('.btn-logout').on('click', this.onLogout.bind(this));
this.find('.btn-respawn').on('click', this.onRespawn.bind(this));
this.find('.btn-respawn').on('click', this.performRespawn.bind(this));
},

onLogout: function () {
$('.uiMainMenu').data('ui').charSelect();
},

onRespawn: function () {
performRespawn: function () {
events.emit('onHideOverlay', this.el);
this.hide(true);



+ 54
- 0
src/client/ui/templates/effects/effects.js View File

@@ -0,0 +1,54 @@
define([
'html!ui/templates/effects/template',
'css!ui/templates/effects/styles',
'html!ui/templates/effects/templateEffect'
], function (
template,
styles,
templateEffect
) {
return {
tpl: template,

icons: {},

postRender: function () {
this.onEvent('onGetEffectIcon', this.onGetEffectIcon.bind(this));
this.onEvent('onRemoveEffectIcon', this.onRemoveEffectIcon.bind(this));
},

buildIcon: function (config) {
let { icon, url } = config;

if (!url)
url = '../../../images/statusIcons.png';

let imgX = icon[0] * -32;
let imgY = icon[1] * -32;

let html = templateEffect;
let el = $(html).appendTo(this.el)
.find('.inner')
.css({
background: `url(${url}) ${imgX}px ${imgY}px`
});

return el.parent();
},

onGetEffectIcon: function (config) {
let el = this.buildIcon(config);

this.icons[config.id] = el;
},

onRemoveEffectIcon: function (config) {
let el = this.icons[config.id];
if (!el)
return;

el.remove();
delete this.icons[config.id];
}
};
});

+ 1
- 0
src/client/ui/templates/effects/styles.css View File

@@ -0,0 +1 @@
.uiEffects{position:absolute;left:16px;top:104px}.uiEffects .icon{width:40px;height:40px;padding:4px;background-color:rgba(49,33,54,.75);margin-right:16px;float:left}.uiEffects .icon .inner{width:32px;height:32px;background:url(../../../images/statusIcons.png) 0 0}.mobile .uiEffects{left:316px;top:10px}

src/client/ui/templates/buffs/styles.less → src/client/ui/templates/effects/styles.less View File

@@ -1,6 +1,6 @@
@import "../../../css/colors.less";

.uiBuffs {
.uiEffects {
position: absolute;
left: 16px;
top: 104px;
@@ -23,7 +23,7 @@

}

.mobile .uiBuffs {
.mobile .uiEffects {
left: 316px;
top: 10px;
}

+ 1
- 0
src/client/ui/templates/effects/template.html View File

@@ -0,0 +1 @@
<div class="uiEffects"></div>

src/client/ui/templates/buffs/templateBuff.html → src/client/ui/templates/effects/templateEffect.html View File

@@ -1,3 +1,3 @@
<div class="icon buff">
<div class="icon effect">
<div class="inner"></div>
</div>

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

@@ -26,7 +26,7 @@ define([
this.find('.btnCollapse').on('click', this.toggleButtons.bind(this));
}
this.onEvent('onRezone', this.onRezone.bind(this));
this.onEvent('clearUis', this.clear.bind(this));

this.onEvent('onObtainEvent', this.onObtainEvent.bind(this));
this.onEvent('onRemoveEvent', this.onRemoveEvent.bind(this));
@@ -37,7 +37,7 @@ define([
this.onToggleEventsVisibility(config.showEvents);
},

onRezone: function () {
clear: function () {
this.list = [];
this.el.find('.list').empty();
},


+ 9
- 9
src/client/ui/templates/inventory/inventory.js View File

@@ -328,17 +328,17 @@ define([
ctxConfig.push(menuItems.divider);
}

if ((!item.eq) && (!item.active)) {
if (!item.quest) {
if ((window.player.stash.active) && (!item.noStash))
ctxConfig.push(menuItems.stash);
if (!item.eq && !item.active && !item.quest) {
const isAtStash = window.player.serverActions.hasAction('openStash');

if (!item.noDrop)
ctxConfig.push(menuItems.drop);
if (isAtStash && !item.noStash)
ctxConfig.push(menuItems.stash);

if ((!item.material) && (!item.noSalvage))
ctxConfig.push(menuItems.salvage);
}
if (!item.noDrop)
ctxConfig.push(menuItems.drop);

if (!item.material && !item.noSalvage)
ctxConfig.push(menuItems.salvage);
}

if (item.quantity > 1 && !item.quest)


+ 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.10.5-Release-Notes">[ Latest Release Notes ]</div>
<div class="news" location="https://gitlab.com/Isleward/play.isleward.com/-/wikis/v0.10.6-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.10.5-Release-Notes">v0.10.5</div>
<div class="version" location="https://gitlab.com/Isleward/play.isleward.com/-/wikis/v0.10.5-Release-Notes">v0.10.6</div>
</div>

+ 7
- 7
src/client/ui/templates/mainMenu/mainMenu.js View File

@@ -58,17 +58,17 @@ define([
},

onCharSelect: function () {
renderer.clean();
objects.onRezone();
events.emit('destroyAllObjects');
events.emit('resetRenderer');
events.emit('resetPhysics');

renderer.buildTitleScreen();
sound.unload();

events.emit('onShowCharacterSelect');
$('[class^="ui"]:not(.ui-container)').toArray().forEach(el => {
let ui = $(el).data('ui');
if (ui && ui.destroy)
ui.destroy();
});

factory.exitGame();

factory.build('characters', {});
},



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

@@ -25,7 +25,7 @@ define([
this.find('.btnCollapse').on('click', this.toggleButtons.bind(this));
}

this.onEvent('onRezone', this.onRezone.bind(this));
this.onEvent('clearUis', this.clear.bind(this));

this.onEvent('onObtainQuest', this.onObtainQuest.bind(this));
this.onEvent('onUpdateQuest', this.onUpdateQuest.bind(this));
@@ -35,7 +35,7 @@ define([
this.onToggleQuestsVisibility(config.showQuests);
},

onRezone: function () {
clear: function () {
this.quests = [];
this.el.find('.list').empty();
},


+ 45
- 22
src/client/ui/templates/stash/stash.js View File

@@ -22,33 +22,38 @@ define([
hoverItem: null,

items: [],
maxItems: null,

modal: true,
hasClose: true,

postRender: function () {
this.onEvent('onGetStashItems', this.onGetStashItems.bind(this));
this.onEvent('onDestroyStashItems', this.onDestroyStashItems.bind(this));
this.onEvent('onKeyDown', this.onKeyDown.bind(this));
this.onEvent('onKeyUp', this.onKeyUp.bind(this));
this.onEvent('onOpenStash', this.toggle.bind(this));
[
'onKeyUp',
'onKeyDown',
'onOpenStash',
'onAddStashItems',
'onRemoveStashItems'
]
.forEach(e => {
this.onEvent(e, this[e].bind(this));
});
},

build: function () {
this.el.removeClass('scrolls');
if (window.player.stash.maxItems > 50)
this.el.addClass('scrolls');
const { el, maxItems, items } = this;

let container = this.el.find('.grid')
.empty();
el.removeClass('scrolls');
if (maxItems > 50)
el.addClass('scrolls');

let items = this.items;
let iLen = Math.max(items.length, window.player.stash.maxItems);
const container = this.el.find('.grid').empty();

for (let i = 0; i < iLen; i++) {
let item = items[i];
const renderItemCount = Math.max(items.length, maxItems);

let itemEl = renderItem(container, item);
for (let i = 0; i < renderItemCount; i++) {
const item = items[i];
const itemEl = renderItem(container, item);

if (!item)
continue;
@@ -126,14 +131,28 @@ define([
this.build();
},

onDestroyStashItems: function (itemIds) {
itemIds.forEach(function (id) {
let item = this.items.find(i => i.id === id);
onAddStashItems: function (addItems) {
const { items } = this;

addItems.forEach(newItem => {
const existIndex = items.findIndex(i => i.id === newItem.id);
if (existIndex !== -1)
items.splice(existIndex, 1, newItem);
else
items.push(newItem);
});
},

onRemoveStashItems: function (removeItemIds) {
const { items } = this;

removeItemIds.forEach(id => {
const item = items.find(i => i.id === id);
if (item === this.hoverItem)
this.hideTooltip();

this.items.spliceWhere(i => i.id === id);
}, this);
items.spliceWhere(i => i.id === id);
});

if (this.shown)
this.build();
@@ -155,8 +174,12 @@ define([
events.emit('onHideContextMenu');
},

onOpenStash: function () {
this.build();
onOpenStash: function ({ items, maxItems }) {
this.maxItems = maxItems;

this.show();

this.onGetStashItems(items);
},

beforeDestroy: function () {


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

@@ -18,11 +18,7 @@ define([

postRender: function () {
this.onEvent('onGetTalk', this.onGetTalk.bind(this));
this.onEvent('onRezone', this.onRezone.bind(this));
},

onRezone: function () {
this.hide();
this.onEvent('clearUis', this.hide.bind(this));
},

onGetTalk: function (dialogue) {


+ 3
- 1
src/server/.eslintrc View File

@@ -58,7 +58,9 @@
"leaderboard": false,
"clientConfig": false,
"random": false,
"consts": false
"consts": false,
"rezoneManager": false,
"eventManager": false
},

"rules": {


+ 83
- 91
src/server/clientComponents/effects.js View File

@@ -1,135 +1,127 @@
define([
'js/rendering/renderer'
'js/system/events',
'js/rendering/numbers'
], function (
renderer
events,
numbers
) {
let auras = {
reflectDamage: 0,
stealth: 1,
regenHp: 9,
regenMana: 10,
swiftness: 11,
holyVengeance: 8,
rare: 16
const defaultBuffIcons = {
stunned: [4, 0]
};

return {
type: 'effects',
const effectBase = {
init: function () {
this.defaultDamageText(false);

alpha: 0,
alphaDir: 0.0025,
if (this.self && defaultBuffIcons[this.type]) {
events.emit('onGetEffectIcon', {
id: this.id,
icon: defaultBuffIcons[this.type]
});
}
},

alphaMax: 0.6,
alphaMin: 0.35,
destroy: function () {
if (!this.obj.destroyed)
this.defaultDamageText(true);

alphaCutoff: 0.4,
if (this.self && defaultBuffIcons[this.type]) {
events.emit('onRemoveEffectIcon', {
id: this.id
});
}
},

defaultDamageText: function (removing) {
numbers.onGetDamage({
id: this.obj.id,
event: true,
text: (removing ? '-' : '+') + this.type
});
}
};

return {
type: 'effects',

effects: [],

templates: {
},

init: function (blueprint) {
this.effects = this.effects
.filter(e => auras[e] !== null)
.map(e => {
return {
name: e,
sprite: renderer.buildObject({
layerName: 'effects',
sheetName: 'auras',
x: this.obj.x,
y: this.obj.y + 1,
w: scale * 3,
h: scale * 3,
cell: auras[e]
})
};
});
this.effects = this.effects.map(e => this.buildEffect(e));
},

buildEffect: function (data) {
let template = this.templates[data.type] || {};

let effect = $.extend(true, {}, effectBase, template, data);

effect.self = !!this.obj.self;
effect.obj = this.obj;

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

extend: function (blueprint) {
if (blueprint.addEffects) {
blueprint.addEffects = blueprint.addEffects
.filter(e => {
return (auras[e] !== null);
})
.map(e => {
return {
name: e,
sprite: renderer.buildObject({
layerName: 'effects',
sheetName: 'auras',
x: this.obj.x,
y: this.obj.y + 1,
w: scale * 3,
h: scale * 3,
cell: auras[e]
})
};
});
blueprint.addEffects = blueprint.addEffects.map(e => this.buildEffect(e));

this.effects.push.apply(this.effects, blueprint.addEffects || []);
}
if (blueprint.removeEffects) {
blueprint.removeEffects.forEach(r => {
let effect = this.effects.find(e => e.name === r);
blueprint.removeEffects.forEach(removeId => {
let effect = this.effects.find(e => e.id === removeId);

if (!effect)
return;

renderer.destroyObject({
layerName: 'effects',
sprite: effect.sprite
});
if (effect.destroy)
effect.destroy();

this.effects.spliceFirstWhere(e => e.name === r);
this.effects.spliceFirstWhere(e => e.id === removeId);
});
}
},

update: function () {
this.alpha += this.alphaDir;
if ((this.alphaDir > 0) && (this.alpha >= this.alphaMax)) {
this.alpha = this.alphaMax;
this.alphaDir *= -1;
} else if ((this.alphaDir < 0) && (this.alpha <= this.alphaMin)) {
this.alpha = this.alphaMin;
this.alphaDir *= -1;
}
if (blueprint.extendEffects) {
blueprint.extendEffects.forEach(u => {
let effect = this.effects.find(e => e.id === u.id);

let x = this.obj.x;
let y = this.obj.y;
if (!effect)
return;

let useAlpha = this.alpha;
if (useAlpha < this.alphaCutoff)
useAlpha = 0;
else {
useAlpha -= this.alphaCutoff;
useAlpha /= (this.alphaMax - this.alphaCutoff);
if (effect.extend)
effect.extend(u.data);
else {
for (let p in u.data)
effect[p] = u.data[p];
}
});
}
},

update: function () {
this.effects.forEach(e => {
renderer.setSpritePosition({
x,
y: y + 1,
sprite: e.sprite
});

e.sprite.alpha = useAlpha;

e.sprite.visible = this.obj.isVisible;
if (e.update)
e.update();
});
},

setVisible: function (visible) {
this.effects.forEach(e => {
e.sprite.visible = visible;
if (e.setVisible)
e.setVisible(visible);
});
},

destroy: function () {
this.effects.forEach(e => {
renderer.destroyObject({
layerName: 'effects',
sprite: e.sprite
});
if (e.destroy)
e.destroy();
});
}
};


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

@@ -0,0 +1,136 @@
define([
'js/rendering/renderer',
'js/system/events'
], function (
renderer,
events
) {
const auras = {
reflectDamage: 0,
stealth: 1,
regenHp: 9,
regenMana: 10,
swiftness: 11,
holyVengeance: 8,
rare: 16
};
const buffIcons = {
regenHp: [3, 1],
regenMana: [4, 1],
swiftness: [5, 1],
stealth: [7, 0],
reflectDamage: [2, 1],
holyVengeance: [4, 0]
};

let templates = {};

Object.keys(auras).forEach(type => {
let cell = auras[type];

templates[type] = {
sprite: null,

alpha: 0,
alphaDir: 0.0025,

alphaMax: 0.6,
alphaMin: 0.35,

alphaCutoff: 0.4,
init: function () {
this.sprite = renderer.buildObject({
layerName: 'effects',
sheetName: 'auras',
x: this.obj.x,
y: this.obj.y + 1,
w: scale * 3,
h: scale * 3,
cell: cell
});

this.defaultDamageText();

if (this.self && buffIcons[type]) {
events.emit('onGetEffectIcon', {
id: this.id,
icon: buffIcons[type]
});
}
},

getAlpha: function () {
let listAuras = this.obj.effects.effects.filter(e => auras[e.type]);
let first = listAuras[0];

// The first aura in the list should do all the updating so that all auras pulse together
if (first === this) {
this.alpha += this.alphaDir;
if ((this.alphaDir > 0) && (this.alpha >= this.alphaMax)) {
this.alpha = this.alphaMax;
this.alphaDir *= -1;
} else if ((this.alphaDir < 0) && (this.alpha <= this.alphaMin)) {
this.alpha = this.alphaMin;
this.alphaDir *= -1;
}
} else {
this.alpha = first.alpha;
this.alphaDir = first.alphaDir;
}

let useAlpha = this.alpha;
if (useAlpha < this.alphaCutoff)
useAlpha = 0;
else {
useAlpha -= this.alphaCutoff;
useAlpha /= (this.alphaMax - this.alphaCutoff);
}

return useAlpha;
},

update: function () {
let useAlpha = this.getAlpha();

let x = this.obj.x;
let y = this.obj.y;

renderer.setSpritePosition({
x,
y: y + 1,
sprite: this.sprite
});

this.sprite.alpha = useAlpha;

this.sprite.visible = this.obj.isVisible;
},

destroy: function () {
renderer.destroyObject({
layerName: 'effects',
sprite: this.sprite
});

this.defaultDamageText(true);

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

setVisible: function (visible) {
this.sprite.visible = visible;
}
};
});

return {
templates: {
...templates
}
};
});

+ 0
- 8
src/server/clientComponents/gatherer.js View File

@@ -13,7 +13,6 @@ define([

init: function () {
this.obj.on('onKeyDown', this.onKeyDown.bind(this));
this.hookEvent('onRezone', this.onRezone.bind(this));
},

extend: function (msg) {
@@ -53,13 +52,6 @@ define([
}
},

onRezone: function () {
this.extend({
progress: 100,
action: 'Fishing'
});
},

onKeyDown: function (key) {
if (key !== 'g')
return;


+ 4
- 4
src/server/clientComponents/pather.js View File

@@ -27,9 +27,9 @@ define([
lastY: 0,

init: function () {
events.on('onRespawn', this.onDeath.bind(this));
events.on('onDeath', this.onDeath.bind(this));
events.on('onClearQueue', this.onDeath.bind(this));
events.on('teleportToPosition', this.resetPath.bind(this));
events.on('onDeath', this.resetPath.bind(this));
events.on('onClearQueue', this.resetPath.bind(this));

this.pathPos.x = round(this.obj.x);
this.pathPos.y = round(this.obj.y);
@@ -46,7 +46,7 @@ define([
this.path = [];
},

onDeath: function () {
resetPath: function () {
this.clearPath();
this.pathPos.x = round(this.obj.x);


+ 2
- 2
src/server/clientComponents/player.js View File

@@ -29,7 +29,7 @@ define([
obj.addComponent('serverActions');
obj.addComponent('pather');

this.hookEvent('onRespawn', this.onRespawn.bind(this));
this.hookEvent('teleportToPosition', this.teleportToPosition.bind(this));

events.emit('onGetPortrait', obj.portrait);
},
@@ -74,7 +74,7 @@ define([
}, instant);
},

onRespawn: function ({ x, y }) {
teleportToPosition: function ({ x, y }) {
this.positionCamera(x, y, true);
sound.update(x, y);
},


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

@@ -14,6 +14,10 @@ define([
this.hookEvent('onKeyUp', this.onKeyUp.bind(this));
},

hasAction: function (actionId) {
return this.actions.some(a => a.id === actionId);
},

onKeyUp: function (key) {
this.actions.forEach(function (a) {
if (a.key !== key)


+ 2
- 44
src/server/clientComponents/stash.js View File

@@ -1,49 +1,7 @@
define([
'js/system/events'
], function (
events
) {
define([], function () {
return {
type: 'stash',

active: false,

items: null,

init: function () {
events.emit('onGetStashItems', this.items);
},

extend: function (blueprint) {
if (blueprint.has('active'))
this.active = blueprint.active;

if (blueprint.getItems) {
let items = this.items;
let newItems = blueprint.getItems || [];
let nLen = newItems.length;

for (let i = 0; i < nLen; i++) {
let nItem = newItems[i];
let nId = nItem.id;

let findItem = items.find(f => f.id === nId);
if (findItem) {
$.extend(true, findItem, nItem);

newItems.splice(i, 1);
i--;
nLen--;
}
}

this.items.push.apply(this.items, blueprint.getItems || []);

events.emit('onGetStashItems', this.items);
}

if (blueprint.destroyItems)
events.emit('onDestroyStashItems', blueprint.destroyItems);
}
init: function () {}
};
});

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

@@ -403,5 +403,9 @@ module.exports = {

clearIgnoreList: function () {
this.ignoreList = [];
},

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

+ 16
- 4
src/server/components/auth.js View File

@@ -93,6 +93,9 @@ module.exports = {

doSave: async function (callback, saveStash = true) {
const simple = this.obj.getSimple(true, true);
delete simple.destroyed;
delete simple.forceDestroy;

simple.components.spliceWhere(f => (f.type === 'stash'));

await io.setAsync({
@@ -111,10 +114,15 @@ module.exports = {
},

doSaveStash: async function () {
const { username, obj: { stash } } = this;

if (!stash.changed)
return;

await io.setAsync({
key: this.username,
key: username,
table: 'stash',
value: this.obj.stash.serialize(),
value: stash.serialize(),
clean: true,
serialize: true
});
@@ -174,7 +182,6 @@ module.exports = {
this.characters[charName] = character;

await this.getCustomChannels(character);
await this.getStash();

await this.verifySkin(character);

@@ -341,8 +348,13 @@ module.exports = {
register: async function (msg) {
let credentials = msg.data;

if ((credentials.username === '') || (credentials.password === '')) {
if (credentials.username === '' || credentials.password === '') {
msg.callback(messages.login.allFields);

return;
} else if (credentials.username.length > 32) {
msg.callback(messages.login.maxUsernameLength);

return;
}



+ 73
- 77
src/server/components/effects.js View File

@@ -110,14 +110,14 @@ module.exports = {
let effect = effects[i];
if (!forceDestroy) {
if (effect.persist) {
this.syncRemove(effect.id, effect.type);
this.syncRemove(effect.id);
continue;
}
}

this.destroyEffect(effect);

this.syncRemove(effect.id, effect.type);
this.syncRemove(effect.id);
effects.splice(i, 1);
eLen--;
i--;
@@ -137,36 +137,40 @@ module.exports = {
},

addEffect: function (options, source) {
//Skip 0-duration effects
if ((options.has('ttl')) && (options.ttl === 0))
return;

options.caster = options.caster || source;

//"X of Y in Z" cc resist check
if (!options.force && !this.canApplyEffect(options.type))
return;

if (!options.new) {
let exists = this.effects.find(e => e.type === options.type);
if (exists) {
exists.ttl += options.ttl;
let oldEffect = this.effects.find(e => e.type === options.type);

for (let p in options) {
if (p === 'ttl')
continue;
exists[p] = options[p];
}
return exists;
}
//If there is no existing effect or the effect is not stackable, make a new effect
if (!oldEffect || !oldEffect.shouldStack)
return this.buildEffect(options);
//If the effect is stackable and the new effect should stack, stack with the old effect
let shouldStack = oldEffect.shouldStack(options);
if (shouldStack && oldEffect.incrementStack) {
oldEffect.incrementStack(options);
return oldEffect;
}

//Otherwise make a new effect
return this.buildEffect(options);
},

getTypeTemplate: function (type) {
let typeTemplate = null;
if (options.type) {
let type = options.type[0].toUpperCase() + options.type.substr(1);
if (type) {
let capitalizedType = type[0].toUpperCase() + type.substr(1);
let result = {
type: type,
url: 'config/effects/effect' + type + '.js'
url: 'config/effects/effect' + capitalizedType + '.js'
};
this.obj.instance.eventEmitter.emit('onBeforeGetEffect', result);

@@ -174,84 +178,77 @@ module.exports = {
}

let builtEffect = extend({}, effectTemplate, typeTemplate);
return builtEffect;
},

buildEffect: function (options) {
let builtEffect = this.getTypeTemplate(options.type);

for (let p in options)
builtEffect[p] = options[p];
builtEffect.obj = this.obj;
builtEffect.id = this.nextId++;
builtEffect.noMsg = options.noMsg;
builtEffect.silent = options.silent;

if (builtEffect.init)
builtEffect.init(options.source);

this.effects.push(builtEffect);

if (!options.noMsg) {
this.obj.instance.syncer.queue('onGetBuff', {
type: options.type,
id: builtEffect.id
}, [this.obj.serverId]);

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

this.obj.syncer.setArray(false, 'effects', 'addEffects', options.type);
}
if (!options.silent)
this.obj.syncer.setArray(false, 'effects', 'addEffects', builtEffect.simplify());

this.obj.instance.eventEmitter.emit('onAddEffect', this.obj, builtEffect);

return builtEffect;
},

syncRemove: function (id, type, noMsg) {
if ((noMsg) || (!type))
syncExtend: function (id, data) {
let effect = this.effects.find(e => e.id === id);
if (!effect)
return;

//Never sync silent effects
if (effect.silent)
return;

this.obj.instance.syncer.queue('onRemoveBuff', {
id: id
}, [this.obj.serverId]);
this.obj.syncer.setArray(true, 'effects', 'extendEffects', {
id,
data
});
},

syncRemove: function (id) {
let effect = this.effects.find(e => e.id === id);

if (!effect)
return;

this.obj.instance.syncer.queue('onGetDamage', {
id: this.obj.id,
event: true,
text: '-' + type
}, -1);
if (effect.silent)
return;

this.obj.syncer.setArray(false, 'effects', 'removeEffects', type);
this.obj.syncer.setArray(false, 'effects', 'removeEffects', id);
},

removeEffect: function (checkEffect, noMsg) {
let effects = this.effects;
let eLen = effects.length;
for (let i = 0; i < eLen; i++) {
let effect = effects[i];
if (effect === checkEffect) {
this.destroyEffect(effect);
removeEffect: function (id) {
const effect = this.effects.find(e => e.id === id);

this.syncRemove(effect.id, effect.type, noMsg || effect.noMsg);
effects.splice(i, 1);
//It's possible that something else has removed the effect
if (!effect)
return;

return;
}
}
this.destroyEffect(effect);

this.syncRemove(effect.id);
this.effects.spliceWhere(e => e.id === id);
},
removeEffectByName: function (effectName, noMsg) {
let effects = this.effects;
let eLen = effects.length;
for (let i = 0; i < eLen; i++) {
let effect = effects[i];
if (effect.type === effectName) {
this.destroyEffect(effect);

this.syncRemove(effect.id, effect.type, noMsg || effects.noMsg);
effects.splice(i, 1);
return effect;
}
}
removeEffectByType: function (type) {
const effects = this.effects.filter(e => e.type === type);

effects.forEach(e => this.removeEffect(e.id));
},

getEffectByType: function (effectType) {
@@ -293,23 +290,22 @@ module.exports = {
for (let i = 0; i < eLen; i++) {
let e = effects[i];

if (e.ttl > 0) {
if (e.ttl > 0)
e.ttl--;
if (e.ttl === 0)
e.destroyed = true;
}
else if (e.ttl === 0)
e.destroyed = true;

if (e.update)
e.update();

if (e.destroyed) {
this.destroyEffect(e);

this.syncRemove(e.id);

effects.splice(i, 1);
eLen--;
i--;

this.destroyEffect(e);

this.syncRemove(e.id, e.type, e.noMsg);
}
}



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

@@ -549,7 +549,7 @@ module.exports = {

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



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

@@ -272,19 +272,16 @@ module.exports = {
this.obj.syncer.setArray(true, 'inventory', 'getItems', item);
},

stashItem: function (id) {
let item = this.findItem(id);
stashItem: async function (id) {
const item = this.findItem(id);
if (!item || item.quest || item.noStash)
return;

delete item.pos;

let stash = this.obj.stash;
if (!stash.active)
return;

let clonedItem = extend({}, item);
const success = stash.deposit(clonedItem);
const stash = this.obj.stash;
const clonedItem = extend({}, item);
const success = await stash.deposit(clonedItem);
if (!success)
return;



+ 31
- 23
src/server/components/mob.js View File

@@ -98,6 +98,8 @@ module.exports = {
patrol: null,
patrolTargetNode: 0,

needLos: null,

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

@@ -111,6 +113,7 @@ module.exports = {
this.maxChaseDistance = blueprint.maxChaseDistance;
},

/* eslint-disable-next-line max-lines-per-function */
update: function () {
let obj = this.obj;

@@ -121,20 +124,22 @@ module.exports = {
//Have we reached home?
if (this.goHome) {
let distanceFromHome = Math.max(abs(this.originX - obj.x), abs(this.originY - obj.y));
if (!distanceFromHome)
if (!distanceFromHome) {
this.goHome = false;
}

//Are we too far from home?
if ((!this.goHome) && (!obj.follower) && (target)) {
if (!this.canChase(target)) {
obj.clearQueue();
obj.aggro.unAggro(target);
target = obj.aggro.getHighest();
obj.spellbook.resetRotation();
}
}

if (!this.goHome) {
//Are we too far from home?
if (!obj.follower && target) {
if (!this.canChase(target)) {
obj.clearQueue();
obj.aggro.unAggro(target);
target = obj.aggro.getHighest();
}
}

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) {
@@ -145,10 +150,11 @@ module.exports = {
//Are we in fight mode?
this.fight(target);
return;
} else if ((!target) && (this.target)) {
} else if (!target && this.target) {
//Is fight mode over?
this.target = null;
obj.clearQueue();
obj.spellbook.resetRotation();

if (canPathHome(this))
this.goHome = true;
@@ -251,8 +257,8 @@ module.exports = {
let ty = ~~target.y;

let distance = max(abs(x - tx), abs(y - ty));
let furthestAttackRange = obj.spellbook.getFurthestRange(null, true);
let furthestStayRange = obj.spellbook.getFurthestRange(null, false);
let furthestAttackRange = obj.spellbook.getFurthestRange(target, true);
let furthestStayRange = obj.spellbook.getFurthestRange(target, false);

let doesCollide = null;
let hasLos = null;
@@ -263,18 +269,20 @@ module.exports = {
hasLos = this.physics.hasLos(x, y, tx, ty);
//Maybe we don't care if the mob has LoS
if (hasLos || this.needLos === false) {
if (((obj.follower) && (obj.follower.master.player)) || (rnd() < 0.65)) {
let spell = obj.spellbook.getRandomSpell(target);
let success = obj.spellbook.cast({
spell: spell,
target: target
});
//null means we don't have LoS
if (success !== null)
return;
hasLos = false;
} else
let spell = obj.spellbook.getSpellToCast(target);
if (!spell)
return;

let success = obj.spellbook.cast({
spell: spell.id,
target
});

//null means we don't have LoS
if (success !== null)
return;

hasLos = false;
}
}
} else if (furthestAttackRange === 0) {


+ 2
- 10
src/server/components/player.js View File

@@ -70,9 +70,7 @@ module.exports = {
faction: 'players'
});
obj.addComponent('gatherer');
obj.addComponent('stash', {
items: character.stash
});
obj.addComponent('stash');

let blueprintEffects = character.components.find(c => c.type === 'effects') || {};
if (blueprintEffects.effects) {
@@ -235,7 +233,7 @@ module.exports = {

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

obj.instance.syncer.queue('onRespawn', {
obj.instance.syncer.queue('teleportToPosition', {
x: obj.x,
y: obj.y
}, [obj.serverId]);
@@ -276,11 +274,5 @@ module.exports = {
msg.data.data.callbackId = atlas.registerCallback(msg.callback);

atlas.performAction(this.obj, msg.data);
},

notifyServerUiReady: function () {
this.obj.instance.eventEmitter.emit('onPlayerUiReady', {
obj: this.obj
});
}
};

+ 11
- 9
src/server/components/portal.js View File

@@ -16,18 +16,20 @@ module.exports = {
this.patronLevel = ~~blueprint.patron;
},

collisionEnter: async function (obj) {
collisionEnter: function (obj) {
if (!obj.player)
return;

const { toZone: zoneName, toPos, toRelativePos } = this;
(async () => {
const { toZone: zoneName, toPos, toRelativePos } = this;

await sendObjToZone({
obj,
invokingObj: this,
zoneName,
toPos,
toRelativePos
});
await sendObjToZone({
obj,
invokingObj: this,
zoneName,
toPos,
toRelativePos
});
})();
}
};

+ 42
- 19
src/server/components/portal/sendObjToZone.js View File

@@ -1,9 +1,19 @@
const sendObjToZone = async ({ obj, invokingObj, zoneName, toPos, toRelativePos }) => {
const { serverId, instance: { physics, syncer: globalSyncer } } = obj;
const fixPosition = (obj, toPos, toRelativePos, invokingObj) => {
if (toPos) {
obj.x = toPos.x;
obj.y = toPos.y;
} else if (toRelativePos) {
obj.x = invokingObj.obj.x + toRelativePos.x;
obj.y = invokingObj.obj.y + toRelativePos.y;
}
};

globalSyncer.flushForTarget(serverId);
const sendObjToZone = async ({ obj, invokingObj, zoneName, toPos, toRelativePos }) => {
const { serverId, instance: { syncer: globalSyncer, physics } } = obj;

if (obj.zoneName === zoneName) {
globalSyncer.flushForTarget(serverId);

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

if (toRelativePos) {
@@ -18,7 +28,7 @@ const sendObjToZone = async ({ obj, invokingObj, zoneName, toPos, toRelativePos

physics.addObject(obj, obj.x, obj.y);

globalSyncer.queue('onRespawn', {
globalSyncer.queue('teleportToPosition', {
x: obj.x,
y: obj.y
}, [obj.serverId]);
@@ -26,28 +36,41 @@ const sendObjToZone = async ({ obj, invokingObj, zoneName, toPos, toRelativePos
return;
}

obj.fireEvent('beforeRezone');
obj.destroyed = true;
//We set this before saving so that objects aren't saved ON portals
obj.zoneName = zoneName;
fixPosition(obj, toPos, toRelativePos, invokingObj);

//Destroy, flush events and notify other objects
globalSyncer.processDestroyedObject(obj);
await obj.auth.doSave();

//We have to do this again. This is because onCollisionEnter in portal is not blocking (even though it is async)
// So physics will carry on and allow the obj to move onto the next tile (changing the position while we save above)
fixPosition(obj, toPos, toRelativePos, invokingObj);

//Test code, remove later
Object.entries(globalSyncer.buffer).forEach(([k, v]) => {
v.forEach(e => {
if (e.to.includes(serverId)) {
/* eslint-disable-next-line */
console.log('Found event', k, 'for rezoning object');
}
});
});

const simpleObj = obj.getSimple(true, false, true);
simpleObj.destroyed = false;
simpleObj.forceDestroy = false;

if (toPos) {
simpleObj.x = toPos.x;
simpleObj.y = toPos.y;
} else if (toRelativePos) {
simpleObj.x = invokingObj.obj.x + toRelativePos.x;
simpleObj.y = invokingObj.obj.y + toRelativePos.y;
}
rezoneManager.stageRezone(simpleObj, zoneName);

process.send({
method: 'rezone',
id: obj.serverId,
args: {
obj: simpleObj,
newZone: zoneName
method: 'events',
data: {
rezoneStart: [{
obj: { msg: {} },
to: [serverId]
}]
}
});
};


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

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

let source = cons.players.find(c => c.id === sourceId);
if (!source)
if (!source || !source.social)
return;

source.social.sendMessage('invite sent', 'color-yellowB');


+ 3
- 0
src/server/components/social/rezone.js View File

@@ -3,6 +3,9 @@ const sendObjToZone = require('../portal/sendObjToZone');
module.exports = (cpnSocial, targetZone) => {
const { obj } = cpnSocial;

if (obj.zoneName === targetZone)
return;

sendObjToZone({
obj,
zoneName: targetZone


+ 27
- 31
src/server/components/spellbook.js View File

@@ -3,6 +3,11 @@ let animations = require('../config/animations');
let playerSpells = require('../config/spells');
let playerSpellsConfig = require('../config/spellsConfig');

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

//Component

module.exports = {
type: 'spellbook',

@@ -16,6 +21,8 @@ module.exports = {

callbacks: [],

rotation: null,

init: function (blueprint) {
this.objects = this.obj.instance.objects;
this.physics = this.obj.instance.physics;
@@ -24,7 +31,22 @@ module.exports = {

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

if (blueprint.rotation) {
const { duration, spells } = blueprint.rotation;

this.rotation = {
currentTick: 0,
duration,
spells
};
}

delete blueprint.spells;

//External helpers that should form part of the component
this.getSpellToCast = rotationManager.getSpellToCast.bind(null, this);
this.getFurthestRange = rotationManager.getFurthestRange.bind(null, this);
this.resetRotation = rotationManager.resetRotation.bind(null, this);
},

transfer: function () {
@@ -243,17 +265,6 @@ module.exports = {
});
},

getRandomSpell: function (target) {
const valid = this.spells.filter(s => {
return (!s.selfCast && !s.procCast && !s.castOnDeath && s.canCast(target));
});

if (!valid.length)
return null;

return valid[~~(Math.random() * valid.length)].id;
},

getTarget: function (spell, action) {
let target = action.target;

@@ -289,7 +300,7 @@ module.exports = {
}
}

if (spell.spellType === 'buff') {
if (spell.spellType === 'buff' || spell.spellType === 'heal') {
if (this.obj.aggro.faction !== target.aggro.faction)
return;
} else if (target.aggro && !this.obj.aggro.canAttack(target)) {
@@ -339,6 +350,7 @@ module.exports = {
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;
@@ -445,25 +457,6 @@ module.exports = {
return this.closestRange;
},

getFurthestRange: function (spellNum, checkCanCast) {
if (spellNum)
return this.spells[spellNum].range;

let spells = this.spells;
let sLen = spells.length;
let furthest = 0;
for (let i = 0; i < sLen; i++) {
let spell = spells[i];
if (spell.procCast || spell.castOnDeath)
continue;

if (spell.range > furthest && (!checkCanCast || spell.canCast()))
furthest = spell.range;
}

return furthest;
},

getCooldowns: function () {
let cds = [];
this.spells.forEach(
@@ -480,6 +473,9 @@ module.exports = {
let didCast = false;
const isCasting = this.isCasting();

if (this.rotation)
rotationManager.tick(this);

this.spells.forEach(s => {
let auto = s.autoActive;
if (auto) {


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

@@ -0,0 +1,131 @@
const getDefaultRotationSpell = rotationSpells => {
const spells = rotationSpells.filter(s => !s.atRotationTicks);

if (!spells.length)
return;

if (spells.length === 1)
return spells[0];

const randomSpell = spells[~~(Math.random() * spells.length)];

return randomSpell;
};

//Mobs that define rotations (normally bosses) use this method to determine their spell choices
const getRotationSpell = (source, target) => {
const { spells, rotation: { currentTick, spells: rotationSpells } } = source;

//Find spell matching current tick
let rotationEntry = rotationSpells.find(s => s.atRotationTicks?.includes(currentTick));

if (!rotationEntry)
rotationEntry = getDefaultRotationSpell(rotationSpells);

if (!rotationEntry)
return;

//Don't cast anything
if (rotationEntry.spellIndex === -1)
return;

const useSpell = spells[rotationEntry.spellIndex];

//Todo: We should set cdMax and manaCost to 0 of rotation spells (unless there's a mana drain mechanic)
// later and we want to allow that on bosses
useSpell.cd = 0;
useSpell.manaCost = 0;
if (!useSpell.selfCast && !useSpell.canCast(target))
return getDefaultRotationSpell(rotationSpells);

return useSpell;
};

//Mobs without rune rotations (normally the case) simple select any random spell that is valid
const getRandomSpell = (source, target) => {
const valid = source.spells.filter(s => {
return (!s.selfCast && !s.procCast && !s.castOnDeath && s.canCast(target));
});

if (!valid.length)
return null;

return valid[~~(Math.random() * valid.length)];
};

const getSpellToCast = (source, target) => {
if (source.rotation)
return getRotationSpell(source, target);

const { obj: { follower } } = source;

//Mobs don't cast all the time but player followers do
if (!follower?.master?.player && Math.random() >= 0.65)
return;

return getRandomSpell(source, target);
};

const tick = source => {
if (!source.obj.aggro.isInCombat())
return;

const { rotation } = source;

rotation.currentTick++;

if (rotation.currentTick === rotation.duration)
rotation.currentTick = 1;
};

//Gets the range we need to be at to cast a specific rotation spell
const getFurthestRangeRotation = (source, target, checkCanCast) => {
const spell = getRotationSpell(source, target);

if (!spell)
return 0;

return spell.range;
};

/*
This is used by mobs when in combat mode,
* When checkCanCast is true, we want to see if we can cast right now
* When checkCanCast is false, we want to see if there is a spell we could cast in the near future
-> This could be a spell that is currently on cooldown, or that the mob has insufficient mana for
---> Even though mobs don't need mana for spells at the moment
-> Ultimately, this means the mob should not move, just wait
*/
const getFurthestRange = (source, target, checkCanCast) => {
const { spells, rotation } = source;

if (rotation)
return getFurthestRangeRotation(source, target, checkCanCast);

let sLen = spells.length;
let furthest = 0;
for (let i = 0; i < sLen; i++) {
let spell = spells[i];
if (spell.procCast || spell.castOnDeath)
continue;

if (spell.range > furthest && (!checkCanCast || spell.canCast()))
furthest = spell.range;
}

return furthest;
};

const resetRotation = source => {
if (!source.rotation)
return;

source.rotation.currentTick = 0;
};

module.exports = {
tick,
resetRotation,
getSpellToCast,
getFurthestRange
};

+ 99
- 69
src/server/components/stash.js View File

@@ -1,127 +1,138 @@
//System
const eventEmitter = require('../misc/events');

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

//Config
const maxItemsBase = 50;

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

active: false,
items: [],
items: null,
changed: false,

maxItems: maxItemsBase,

init: function (blueprint) {
let items = blueprint.items || [];
let iLen = items.length;
for (let i = 0; i < iLen; i++)
this.getItem(items[i]);
init: function (blueprint) {},

getItemsFromDb: async function () {
const { obj } = this;

this.items = await io.getAsync({
key: obj.account,
table: 'stash',
isArray: true,
clean: true
});

delete blueprint.items;
fixes.fixStash(this.items);

this.blueprint = blueprint;
await eventEmitter.emit('onAfterGetStash', {
obj: obj,
stash: this.items
});
},

getItem: function (item) {
//Material?
let exists = false;
const { items } = this;
if (isItemStackable(item)) {
let existItem = this.items.find(i => i.name === item.name);
const existItem = items.find(i => i.name === item.name);
if (existItem) {
exists = true;
if (!existItem.quantity)
existItem.quantity = 1;
existItem.quantity += (item.quantity || 1);

existItem.quantity += (+item.quantity || 1);

//We modify the old object because it gets sent to the client
item.id = existItem.id;
item.quantity = existItem.quantity;

item = existItem;

return;
}
}

//Get next id
if (!exists) {
let id = 0;
let items = this.items;
let iLen = items.length;
for (let i = 0; i < iLen; i++) {
let fItem = items[i];
if (fItem.id >= id)
id = fItem.id + 1;
}
item.id = id;
}
let id = 0;
items.forEach(i => {
if (i.id >= id)
id = i.id + 1;
});
item.id = id;

if (!exists)
this.items.push(item);
items.push(item);
},

deposit: function (item) {
if (!this.active)
deposit: async function (item) {
if (!this.items)
await this.getItemsFromDb();

const { active, items, maxItems, obj } = this;

if (!active)
return;
else if (this.items.length >= this.maxItems) {
let isStackable = this.items.some(stashedItem => item.name === stashedItem.name && (isItemStackable(stashedItem)));

else if (items.length >= maxItems) {
const isStackable = items.some(stashedItem => item.name === stashedItem.name && isItemStackable(stashedItem));
if (!isStackable) {
const message = 'You do not have room in your stash to deposit that item';
this.obj.social.notifySelf({ message });
obj.social.notifySelf({ message });

return;
}
}
this.getItem(item);

this.obj.syncer.setArray(true, 'stash', 'getItems', item);

this.changed = true;

return true;
},

destroyItem: function (id) {
let item = this.items.find(i => i.id === id);
if (!item)
return;
this.getItem(item);

this.items.spliceWhere(i => i === item);
const sendItem = cpnInventory.simplifyItem.call({ obj: {} }, item);

this.obj.syncer.setArray(true, 'stash', 'destroyItems', id);
obj.instance.syncer.queue('onAddStashItems', [sendItem], [obj.serverId]);

this.changed = true;
return true;
},

withdraw: function (id) {
if (!this.active)
const { active, items, obj } = this;

if (!active)
return;

let item = this.items.find(i => i.id === id);
let item = items.find(i => i.id === id);
if (!item)
return;
else if (!this.obj.inventory.hasSpace(item)) {
else if (!obj.inventory.hasSpace(item)) {
const message = 'You do not have room in your inventory to withdraw that item';
this.obj.social.notifySelf({ message });
obj.social.notifySelf({ message });
return;
}

this.obj.inventory.getItem(item);
this.items.spliceWhere(i => i === item);
this.changed = true;

this.obj.syncer.setArray(true, 'stash', 'destroyItems', id);
obj.inventory.getItem(item);
items.spliceWhere(i => i === item);

this.changed = true;
obj.instance.syncer.queue('onRemoveStashItems', [id], [obj.serverId]);
},

setActive: function (active) {
let obj = this.obj;
const { obj } = this;

this.active = active;
obj.syncer.set(true, 'stash', 'active', this.active);

const actionType = active ? 'addActions' : 'removeActions';
obj.syncer.setArray(true, 'serverActions', actionType, {
id: 'openStash',
key: 'u',
action: {
targetId: obj.id,
@@ -130,34 +141,53 @@ module.exports = {
}
});

if (!this.active)
return;

let msg = 'Press U to access your Shared Stash';
this.obj.instance.syncer.queue('onGetAnnouncement', {
src: this.obj.id,
obj.instance.syncer.queue('onGetAnnouncement', {
src: obj.id,
msg: msg
}, [obj.serverId]);

if (this.active && this.items.length > this.maxItems) {
const message = `You have more than ${this.maxItems} items in your stash. In the future, these items will be lost.`;
obj.social.notifySelf({ message });
}
},

open: function () {
const { active, obj } = this;
open: async function () {
if (!this.items)
await this.getItemsFromDb();

if (active)
obj.instance.syncer.queue('onOpenStash', {}, [obj.serverId]);
const { obj, active, maxItems, items } = this;

if (!active)
return;

const sendItems = items.map(i => cpnInventory.simplifyItem.call({ obj: {} }, i));

const msg = {
maxItems,
items: sendItems
};

obj.instance.syncer.queue('onOpenStash', msg, [obj.serverId]);

if (items.length > maxItems) {
const message = `You have more than ${maxItems} items in your stash. In the future, these items will be lost.`;
obj.social.notifySelf({ message });
}
},

simplify: function (self) {
if (!self)
return null;

return { type: 'stash' };
},

simplifyTransfer: function () {
const { type, items } = this;

return {
type: 'stash',
active: this.active,
items: this.items,
maxItems: this.maxItems
type,
items
};
},



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

@@ -629,7 +629,8 @@ module.exports = {
source: source.id,
heal: true,
amount: amount,
crit: heal.crit
crit: heal.crit,
element: heal.element
}, recipients);
}

@@ -776,6 +777,9 @@ module.exports = {
},

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

const { obj, values: { lifeOnHit } } = this;

if (target === obj || !lifeOnHit)


+ 9
- 3
src/server/config/clientConfig.js View File

@@ -44,7 +44,8 @@ const config = {
'images/tiles.png': 8,
'images/walls.png': 8,
'images/objects.png': 8,
'images/mobs.png': 8
'images/mobs.png': 8,
'images/characters.png': 8
},
blockingTileIndices: [
6, 7, 54, 55, 62, 63, 154, 189, 190, 192, 193, 194, 195, 196, 197
@@ -147,7 +148,7 @@ const config = {
//Table Sides
103, 110, 118, 126,
//Wall-mounted plants
120, 122, 140,
120, 121,
//Ship oars
140, 143,
//Ship Cannons
@@ -173,7 +174,7 @@ const config = {
'party',
'help',
'dialogue',
'buffs',
'effects',
'tooltips',
'tooltipInfo',
'tooltipItem',
@@ -221,6 +222,11 @@ module.exports = {
});
});

config.clientComponents.push({
extends: 'effects',
path: 'server/clientComponents/effects/auras.js'
});

events.emit('onBeforeGetClientConfig', config);

//Deprecated


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

@@ -2,7 +2,8 @@ module.exports = {
type: 'stunned',

init: function () {
this.obj.spellbook.stopCasting();
if (this.obj.spellbook)
this.obj.spellbook.stopCasting();
},

events: {


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

@@ -1,4 +1,15 @@
module.exports = {
syncExtend: function (data) {
let effects = this.obj.effects;
effects.syncExtend(this.id, data);
},

isFirstOfType: function () {
let effects = this.obj.effects;
let firstOfType = effects.find(f => f.type === this.type);
return (firstOfType.id === this);
},

save: function () {
if (!this.persist)
return null;
@@ -19,6 +30,9 @@ module.exports = {
},

simplify: function () {
return this.type;
return {
id: this.id,
type: this.type
};
}
};

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

@@ -1,5 +1,5 @@
module.exports = {
version: '0.10.5',
version: '0.10.6',
port: 4000,
startupMessage: 'Server: ready',
defaultZone: 'fjolarok',


+ 0
- 5
src/server/config/skins.js View File

@@ -18,11 +18,6 @@ const config = {
sprite: [2, 0],
defaultSpirit: 'bear',
default: true
},
//Faction Skins
'gaekatlan-druid': {
name: 'Gaekatlan Druid',
sprite: [0, 1]
}
};



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

@@ -212,6 +212,57 @@ let spells = [{
randomSpeed: true,
chance: 0.02
}
}, {
name: 'Healing Touch',
description: 'Restore health to a friendly target.',
type: 'singleTargetHeal',
spellType: 'heal',
icon: [0, 3],
animation: 'raiseStaff',
particles: {
scale: {
start: {
min: 6,
max: 16
},
end: {
min: 0,
max: 4
}
},
speed: {
start: {
min: 2,
max: 12
},
end: {
min: 0,
max: 4
}
},
lifetime: {
min: 1,
max: 3
},
alpha: {
start: 0.45,
end: 0
},
color: {
start: ['ffeb38', 'fcfcfc'],
end: ['fcfcfc', 'faac45']
},
spawnType: 'circle',
spawnCircle: {
x: 0,
y: 0,
r: 12
},
randomScale: true,
randomColor: true,
randomSpeed: true,
chance: 0.02
}
}, {
name: 'Holy Vengeance',
description: 'Grants holy vengeance to a friendly target. For the duration of the effect, dealing damage will also heal the attacker.',


+ 4
- 5
src/server/config/spells/spellAura.js View File

@@ -69,7 +69,7 @@ module.exports = {
if (distance > range) {
if (effect) {
delete effects[m];
obj.effects.removeEffect(effect);
obj.effects.removeEffect(effect.id);
}

return;
@@ -85,8 +85,7 @@ module.exports = {
type: this.effect,
amount: amount,
caster: this.obj,
ttl: -1,
new: true
ttl: -1
});
});

@@ -96,7 +95,7 @@ module.exports = {
delete effects[serverId];
const obj = objects.find(f => ~~f.serverId === ~~serverId);
if (obj)
obj.effects.removeEffect(effect);
obj.effects.removeEffect(effect.id);
}
});
},
@@ -117,7 +116,7 @@ module.exports = {
return;
}

obj.effects.removeEffect(effect);
obj.effects.removeEffect(effect.id);
delete effects[m];
}, this);
}


+ 3
- 12
src/server/config/spells/spellCharge.js View File

@@ -55,19 +55,10 @@ module.exports = {
type: 'stunned'
});

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

let selfEffect = this.obj.effects.addEffect({
type: 'stunned',
noMsg: true,
force: true,
new: true
silent: true,
force: true
});

const moveAnimationEffect = {
@@ -115,7 +106,7 @@ module.exports = {

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

obj.effects.removeEffect(selfEffect, true);
obj.effects.removeEffect(selfEffect.id);

this.obj.aggro.move();



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

@@ -115,8 +115,7 @@ module.exports = {

const targetEffect = m.effects.addEffect({
type: 'stunned',
noMsg: true,
new: true
silent: true
});

//If targetEffect is undefined, it means that the target has become resistant
@@ -147,7 +146,7 @@ module.exports = {
},

endEffect: function (target, targetPos, targetEffect) {
target.effects.removeEffect(targetEffect, true);
target.effects.removeEffect(targetEffect.id);

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



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

@@ -54,19 +54,11 @@ module.exports = {
if (this.obj.destroyed)
return;

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

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

let damage = this.getDamage(target);
target.stats.takeDamage(damage, this.threatMult, this.obj);
}


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

@@ -36,6 +36,6 @@ module.exports = {

let obj = this.obj;

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

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

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

cdMax: 20,
manaCost: 0,
range: 9,

healing: 1,

needLos: true,
targetFriendly: true,

spellType: 'heal',
particleDuration: 10,

cast: function (action) {
const target = action.target;
const { x, y } = target;

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

const effect = {
x,
y,
components: [{
type: 'particles',
//This ttl is in frames (each frame is roughly 1000 / 60 ms)
ttl: (1000 / 60) * this.particleDuration,
destroyObject: true,
blueprint: this.particles
}]
};

this.obj.instance.syncer.queue('onGetObject', effect, -1);

this.sendBump(target);

return true;
}
};

+ 5
- 3
src/server/config/spells/spellStealth.js View File

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

targetGround: true,

effect: null,

cast: function (action) {
//Clear Aggro
this.obj.aggro.die();
@@ -15,10 +17,10 @@ module.exports = {
let ttl = this.duration * consts.tickTime;
let endCallback = this.queueCallback(this.endEffect.bind(this), ttl - 50);

this.obj.effects.addEffect({
this.effect = this.obj.effects.addEffect({
type: 'stealth',
endCallback: endCallback
});
});

return true;
},
@@ -28,7 +30,7 @@ module.exports = {

let obj = this.obj;

obj.effects.removeEffectByName('stealth');
obj.effects.removeEffect(this.effect.id);
this.obj.aggro.move();
}
};

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

@@ -164,7 +164,7 @@ module.exports = {

this.castingEffect = this.obj.effects.addEffect({
type: 'casting',
noMsg: true
silent: true
});

this.casting = true;


+ 19
- 7
src/server/config/spellsConfig.js View File

@@ -76,9 +76,9 @@ let spells = {
statType: 'int',
statMult: 1,
element: 'holy',
cdMax: 5,
cdMax: 15,
castTimeMax: 4,
manaCost: 8,
manaCost: 12,
range: 9,
radius: 3,
random: {
@@ -87,6 +87,19 @@ let spells = {
}
},

'healing touch': {
statType: 'int',
statMult: 1,
element: 'holy',
cdMax: 5,
castTimeMax: 3,
manaCost: 8,
range: 9,
random: {
healing: [1, 3]
}
},

slash: {
statType: 'str',
statMult: 1,
@@ -130,7 +143,6 @@ let spells = {
cdMax: 12,
castTimeMax: 2,
manaCost: 7,
noDrop: true,
random: {
i_range: [1, 2.5],
damage: [4, 18]
@@ -156,7 +168,6 @@ let spells = {
castTimeMax: 3,
range: 10,
manaCost: 7,
noDrop: true,
random: {
damage: [8, 35],
i_stunDuration: [4, 7]
@@ -167,11 +178,12 @@ let spells = {
statMult: 1,
manaCost: 14,
needLos: true,
cdMax: 20,
cdMax: 15,
castTimeMax: 0,
range: 9,
isAttack: true,
random: {
damage: [3, 16],
damage: [3, 18],
i_delay: [1, 4]
},
negativeStats: [
@@ -219,7 +231,7 @@ let spells = {
auraRange: 9,
effect: 'swiftness',
random: {
chance: [5, 10]
chance: [8, 20]
}
}



+ 2
- 2
src/server/events/phases/phaseGiveRewards.js View File

@@ -1,6 +1,6 @@
module.exports = {
init: function (event) {
const { config, rewards, eventManager } = event;
const { config, rewards, eventManager: eManager } = event;

const { name: eventName, rewardSenderName } = config;

@@ -26,7 +26,7 @@ module.exports = {
});

if ((config.events) && (config.events.afterGiveRewards))
config.events.afterGiveRewards(eventManager, config);
config.events.afterGiveRewards(eManager, config);

this.end = true;
}


+ 2
- 1
src/server/misc/messages.js View File

@@ -5,7 +5,8 @@ module.exports = {
allFields: 'please complete all fields',
illegal: 'illegal characters in username',
incorrect: 'invalid username and password',
charExists: 'character name is taken'
charExists: 'character name is taken',
maxUsernameLength: 'username may not be longer than 32 characters'
},
createCharacter: {
nameLength: 'name must be between 3 and 12 characters'


+ 1
- 1
src/server/mods/class-necromancer/index.js View File

@@ -126,7 +126,7 @@ module.exports = {
range: 1,
random: {
damage: [4, 14],
healPercent: [10, 30]
healPercent: [2, 15]
}
};



+ 8
- 18
src/server/objects/objBase.js View File

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

performMove: function (action) {
const { x: xOld, y: yOld, syncer, aggro, mob, instance: { physics } } = this;
const { x: xOld, y: yOld, syncer, aggro, instance: { physics } } = this;

const { maxDistance = 1, force, data } = action;
const { x: xNew, y: yNew } = data;
@@ -309,26 +309,16 @@ module.exports = {
return false;
}

//Don't allow mob overlap during combat
if (mob && mob.target) {
this.x = xNew;
this.y = yNew;
this.x = xNew;
this.y = yNew;

if (physics.addObject(this, xNew, yNew))
physics.removeObject(this, xOld, yOld);
else {
this.x = xOld;
this.y = yOld;

return false;
}
} else {
if (physics.addObject(this, xNew, yNew, xOld, yOld))
physics.removeObject(this, xOld, yOld, xNew, yNew);
else {
this.x = xOld;
this.y = yOld;

this.x = xNew;
this.y = yNew;

physics.addObject(this, xNew, yNew, xOld, yOld);
return false;
}

//We can't use xNew and yNew because addObject could have changed the position (like entering a building interior with stairs)


+ 71
- 41
src/server/objects/objects.js View File

@@ -203,64 +203,87 @@ module.exports = {

return newO;
},
sendEvent: function (msg) {
let player = this.objects.find(p => p.id === msg.id);
if (!player)

sendEvent: function (msg, { name: sourceZone }) {
const { id, data } = msg;

const player = this.objects.find(p => p.id === id);
if (!player || player.zoneName !== sourceZone)
return;

player.socket.emit('event', {
event: msg.data.event,
data: msg.data.data
event: data.event,
data: data.data
});
},
sendEvents: function (msg) {
let players = {};
let objects = this.objects;

let data = msg.data;
sendEvents: function ({ data }, { name: sourceZone }) {
const { objects } = this;

//Store will contain all events to be sent to players
const store = {};

for (let e in data) {
let event = data[e];
let eLen = event.length;
const event = data[e];
const eLen = event.length;

for (let j = 0; j < eLen; j++) {
let eventEntry = event[j];

let obj = eventEntry.obj;

if (e !== 'serverModule') {
let to = eventEntry.to;
let toLen = to.length;
for (let i = 0; i < toLen; i++) {
let toId = to[i];

let player = players[toId];
if (!player) {
let findPlayer = objects.find(o => o.id === toId);
if (!findPlayer)
continue;
else {
player = (players[toId] = {
socket: findPlayer.socket,
events: {}
});
}
const eventEntry = event[j];

const { obj: eventObj, to } = eventEntry;

if (e === 'serverModule') {
const { method, msg } = eventObj;

if (Array.isArray(msg))
global[eventObj.module][method](...msg);
else
global[eventObj.module][method](msg);

continue;
}

const toLen = to.length;
for (let i = 0; i < toLen; i++) {
const toId = to[i];

let storeEntry = store[toId];
if (!storeEntry) {
const playerObj = objects.find(o => o.id === toId);

if (!playerObj || playerObj.zoneName !== sourceZone) {
io.setAsync({
key: new Date(),
table: 'error',
value: `ignoring ${e}`
});

continue;
}

let eventList = player.events[e] || (player.events[e] = []);
eventList.push(obj);
store[toId] = {
obj: playerObj,
events: { [e]: [eventObj] }
};

continue;
}
} else if (obj.msg instanceof Array)
global[obj.module][obj.method](...obj.msg);
else
global[obj.module][obj.method](obj.msg);

if (!storeEntry.events[e])
storeEntry.events[e] = [];

storeEntry.events[e].push(eventObj);
}
}
}

for (let p in players) {
let player = players[p];
player.socket.emit('events', player.events);
for (let p in store) {
const { obj: { socket }, events } = store[p];

socket.emit('events', events);
}
},

updateObject: async function (msg) {
let player = this.objects.find(p => p.id === msg.serverId);
if (!player)
@@ -324,6 +347,13 @@ module.exports = {
if ((o.update) && (!o.destroyed))
o.update();

//When objects are sent to other zones, we destroy them immediately (thhrough sendObjToZone)
if (o.forceDestroy) {
i--;
len--;
continue;
}

if (o.ttl) {
o.ttl--;
if (!o.ttl)


+ 2772
- 367
src/server/package-lock.json
File diff suppressed because it is too large
View File


+ 2
- 2
src/server/package.json View File

@@ -1,6 +1,6 @@
{
"name": "isleward_server",
"version": "0.10.5",
"version": "0.10.6",
"description": "isleward",
"dependencies": {
"axios": "^0.22.0",
@@ -11,7 +11,7 @@
"image-size": "^1.0.0",
"rethinkdbdash": "^2.3.31",
"socket.io": "^4.2.0",
"universal-analytics": "^0.4.23"
"universal-analytics": "^0.5.1"
},
"devDependencies": {
"babel-eslint": "^10.1.0",


+ 5
- 2
src/server/security/connections.js View File

@@ -77,8 +77,11 @@ module.exports = {
(['getCharacterList', 'getCharacter', 'deleteCharacter'].indexOf(msg.method) === -1)
) ||
(
(player.dead) &&
(msg.data.method !== 'respawn')
player.dead &&
!(
(msg.method === 'performAction' && ['respawn'].includes(msg.data.method)) ||
(msg.method === 'clientAck')
)
)
)
return;


+ 4
- 3
src/server/security/routerConfig.js View File

@@ -18,13 +18,14 @@ const routerConfig = {
wardrobe: ['open', 'apply'],
stats: ['respawn'],
passives: ['tickNode', 'untickNode'],
workbench: ['open', 'craft', 'getRecipe'],
player: ['notifyServerUiReady']
workbench: ['open', 'craft', 'getRecipe']
},
globalAllowed: {
clientConfig: ['getClientConfig'],
leaderboard: ['requestList'],
cons: ['unzone']
cons: ['unzone'],
rezoneManager: ['clientAck'],
instancer: ['clientAck']
},
allowTargetId: {
door: ['lock', 'unlock'],


+ 7
- 6
src/server/world/atlas.js View File

@@ -128,15 +128,16 @@ module.exports = {
mapList.mapList.filter(m => !m.disabled).forEach(m => this.spawnMap(m));
},
spawnMap: function (map) {
let worker = childProcess.fork('./world/worker');
let thread = {
const worker = childProcess.fork('./world/worker', [map.name]);

const thread = {
id: this.nextId++,
name: map.name,
path: map.path,
worker: worker
worker
};

let onMessage = this.onMessage.bind(this, thread);
const onMessage = this.onMessage.bind(this, thread);
worker.on('message', function (m) {
onMessage(m);
});
@@ -174,11 +175,11 @@ module.exports = {
},

event: function (thread, message) {
objects.sendEvent(message);
objects.sendEvent(message, thread);
},

events: function (thread, message) {
objects.sendEvents(message);
objects.sendEvents(message, thread);
},

object: function (thread, message) {


+ 22
- 9
src/server/world/instancer.js View File

@@ -13,6 +13,9 @@ let eventEmitter = require('../misc/events');
const mods = require('../misc/mods');
const transactions = require('../security/transactions');

//Own helpers
const { stageZoneIn, unstageZoneIn, clientAck } = require('./instancer/handshakes');

module.exports = {
instances: [],
zoneId: -1,
@@ -65,6 +68,9 @@ module.exports = {
[resourceSpawner, syncer, objects, questBuilder, events].forEach(i => i.init(fakeInstance));

this.tick();

this.clientAck = clientAck;
eventEmitter.on('removeObject', unstageZoneIn);
},

startRegen: function (respawnMap, respawnPos) {
@@ -211,21 +217,24 @@ module.exports = {

obj.spawn = map.spawn;

syncer.queue('onGetMap', map.clientMap, [obj.serverId]);
stageZoneIn(msg);

if (!msg.transfer)
objects.addObject(obj, this.onAddObject.bind(this));
else {
let o = objects.transferObject(obj);
questBuilder.obtain(o);
eventEmitter.emit('onAfterPlayerEnterZone', o);
}
process.send({
method: 'events',
data: {
getMap: [{
obj: map.clientMap,
to: [obj.serverId]
}]
}
});
},

//This function fires when the player logs in the first time, not upon rezone
onAddObject: function (obj) {
if (obj.player) {
obj.stats.onLogin();
eventEmitter.emit('onAfterPlayerEnterZone', obj);
eventEmitter.emit('onAfterPlayerEnterZone', obj, { isTransfer: false });
}

questBuilder.obtain(obj);
@@ -302,6 +311,10 @@ module.exports = {
return;
}

//We fire this event because even though an object might be destroyed already,
// mods and modules might have staged events/actions we need to clear
eventEmitter.emit('removeObject', { obj: msg.obj });

let obj = msg.obj;
obj = objects.find(o => o.serverId === obj.id);
if (!obj) {


+ 49
- 0
src/server/world/instancer/handshakes.js View File

@@ -0,0 +1,49 @@
//Local State
const stagedZoneIns = [];

//Methods

//Fired when an object is removed through a socket dc
// We do this because a client might DC during rezone handshake
const unstageZoneIn = msg => {
stagedZoneIns.spliceWhere(s => s.obj.serverId === msg.obj.id);
};

const stageZoneIn = msg => {
const { serverId } = msg.obj;

stagedZoneIns.spliceWhere(o => o.obj.serverId === serverId);

stagedZoneIns.push(msg);
};

const doZoneIn = function (staged) {
const { onAddObject, instances: [ { objects, questBuilder, eventEmitter } ] } = instancer;

const { transfer: isTransfer, obj } = staged;

if (!isTransfer)
objects.addObject(obj, onAddObject.bind(instancer));
else {
let o = objects.transferObject(obj);
questBuilder.obtain(o);
eventEmitter.emit('onAfterPlayerEnterZone', o, { isTransfer });
}
};

const clientAck = msg => {
const staged = stagedZoneIns.find(s => s.obj.serverId === msg.sourceId);
if (!staged)
return;

stagedZoneIns.spliceWhere(s => s === staged);

doZoneIn(staged);
};

//Exports
module.exports = {
unstageZoneIn,
stageZoneIn,
clientAck
};

+ 7
- 2
src/server/world/map.js View File

@@ -85,8 +85,13 @@ module.exports = {
try {
chats = require('../' + this.path + '/' + this.name + '/chats');
} catch (e) {}
if (chats)
this.zone.chats = chats;

if (chats) {
if (this.zone.chats)
extend(this.zone.chats, chats);
else
this.zone.chats = chats;
}

let dialogues = null;
try {


+ 2
- 15
src/server/world/mobBuilder.js View File

@@ -50,6 +50,8 @@ module.exports = {
if (cpnMob.patrol)
cpnMob.walkDistance = 1;

cpnMob.needLos = blueprint.needLos;

let spells = extend([], blueprint.spells);
spells.forEach(s => {
if (!s.animation && mob.sheetName === 'mobs' && animations.mobs[mob.cell])
@@ -179,21 +181,6 @@ module.exports = {
s.dmgMult = s.name ? dmgMult / 3 : dmgMult;
s.statType = preferStat;
s.manaCost = 0;

/*if (mob.name.toLowerCase().includes('stinktooth')) {
mob.stats.values.critChance = 0;
mob.stats.values.attackCritChance = 0;
mob.stats.values.spellCritChance = 0;

const n = mob.name + '-' + s.type;
if (!track[n])
track[n] = [];

track[n].push(~~s.getDamage(mob, true).amount);
track[n].sort((a, b) => a - b);
console.log(track);
console.log('');
}*/
});

//Hack to disallow low level mobs from having any lifeOnHit


+ 55
- 0
src/server/world/rezoneManager.js View File

@@ -0,0 +1,55 @@
//Imports
const eventEmitter = require('../misc/events');

//Local State
const stagedRezones = [];

//Methods

//Fired when an object is removed through a socket dc
// We do this because a client might DC during rezone handshake
const unstageRezone = msg => {
stagedRezones.spliceWhere(s => s.simplifiedObj.serverId === msg.obj.id);
};

const stageRezone = (simplifiedObj, targetZone) => {
const { serverId } = simplifiedObj;

stagedRezones.spliceWhere(o => o.simplifiedObj.serverId === serverId);

stagedRezones.push({ simplifiedObj, targetZone });
};

const doRezone = stagedRezone => {
const { simplifiedObj, targetZone } = stagedRezone;

process.send({
method: 'rezone',
id: simplifiedObj.serverId,
args: {
obj: simplifiedObj,
newZone: targetZone
}
});
};

const clientAck = msg => {
const staged = stagedRezones.find(s => s.simplifiedObj.serverId === msg.sourceId);
if (!staged)
return;

stagedRezones.spliceWhere(s => s === staged);

doRezone(staged);
};

const init = () => {
eventEmitter.on('removeObject', unstageRezone);
};

//Exports
module.exports = {
init,
stageRezone,
clientAck
};

+ 0
- 1
src/server/world/spawners.js View File

@@ -60,7 +60,6 @@ module.exports = {

this.syncer.queue('onGetObject', {
id: obj.id,
performLast: true,
components: [spawnAnimation]
}, -1);
}


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

@@ -180,6 +180,35 @@ module.exports = {
}
},

processDestroyedObject: function (obj) {
const { objects, queue } = this;
const { id, serverId } = obj;

obj.destroyed = true;

//We mark forceDestroy to tell objects that we're destroying an object outside of the
// syncer's update method
obj.forceDestroy = true;

const msg = {
id: id,
destroyed: true
};

objects.removeObject(obj);

this.flushForTarget(serverId);

const fnQueueMsg = queue.bind(this, 'onGetObject');

//Find any players that have seen this obj
objects
.filter(o => !o.destroyed && o?.player?.hasSeen(id))
.forEach(o => {
fnQueueMsg(msg, [o.serverId]);
});
},

send: function () {
if (!this.dirty)
return;


+ 5
- 0
src/server/world/worker.js View File

@@ -5,6 +5,7 @@ global.consts = require('../config/consts');
global.instancer = require('./instancer');
global.eventManager = require('../events/events');
global.clientConfig = require('../config/clientConfig');
global.rezoneManager = require('./rezoneManager');

const components = require('../components/components');
const mods = require('../misc/mods');
@@ -21,6 +22,8 @@ const itemEffects = require('../items/itemEffects');
const profanities = require('../misc/profanities');
const eventEmitter = require('../misc/events');

instancer.mapName = process.argv[2];

let onCpnsReady = async function () {
factions.init();
skins.init();
@@ -33,6 +36,8 @@ let onCpnsReady = async function () {
recipes.init();
itemEffects.init();
profanities.init();
rezoneManager.init();

await clientConfig.init();

process.send({


Loading…
Cancel
Save