Browse Source

merged master for v0.4

tags/v0.4
Big Bad Waffle 4 years ago
parent
commit
ab9071c629
100 changed files with 6875 additions and 4566 deletions
  1. +1
    -1
      .gitlab-ci.yml
  2. +4
    -4
      Dockerfile
  3. BIN
     
  4. +1
    -1
      src/client/css/colors.less
  5. +2
    -2
      src/client/css/main.less
  6. BIN
     
  7. BIN
     
  8. BIN
     
  9. BIN
     
  10. BIN
     
  11. BIN
     
  12. BIN
     
  13. BIN
     
  14. BIN
     
  15. BIN
     
  16. BIN
     
  17. BIN
     
  18. BIN
     
  19. BIN
     
  20. BIN
     
  21. BIN
     
  22. +5
    -0
      src/client/js/components/social.js
  23. +1
    -1
      src/client/js/components/spellbook.js
  24. +5
    -3
      src/client/js/components/whirlwind.js
  25. +2
    -0
      src/client/js/misc/statTranslations.js
  26. +11
    -2
      src/client/js/objects/objects.js
  27. +91
    -63
      src/client/js/rendering/renderer.js
  28. +2
    -0
      src/client/js/rendering/spritePool.js
  29. +10
    -10
      src/client/js/rendering/tileOpacity.js
  30. +0
    -106
      src/client/js/spriteBuilder.js
  31. +4
    -2
      src/client/ui/templates/equipment/equipment.js
  32. +1
    -1
      src/client/ui/templates/inventory/inventory.js
  33. +2
    -2
      src/client/ui/templates/login/template.html
  34. +29
    -8
      src/client/ui/templates/online/online.js
  35. +5
    -3
      src/client/ui/templates/reputation/reputation.js
  36. +4
    -4
      src/client/ui/templates/spells/spells.js
  37. +14
    -6
      src/client/ui/templates/spells/styles.less
  38. +10
    -1
      src/client/ui/templates/tooltipItem/tooltipItem.js
  39. +1
    -1
      src/client/ui/templates/trade/trade.js
  40. +1
    -0
      src/client/ui/templates/wardrobe/wardrobe.js
  41. +1
    -0
      src/client/ui/templates/workbench/workbench.js
  42. +7
    -1
      src/server/components/aggro.js
  43. +6
    -44
      src/server/components/auth.js
  44. +65
    -0
      src/server/components/auth/checkLoginRewards.js
  45. +34
    -2
      src/server/components/extensions/socialCommands.js
  46. +10
    -283
      src/server/components/inventory.js
  47. +88
    -0
      src/server/components/inventory/dropBag.js
  48. +127
    -0
      src/server/components/inventory/getItem.js
  49. +50
    -0
      src/server/components/inventory/learnRecipe.js
  50. +98
    -0
      src/server/components/inventory/useItem.js
  51. +19
    -93
      src/server/components/social.js
  52. +27
    -0
      src/server/components/social/ban.js
  53. +101
    -0
      src/server/components/social/chat.js
  54. +12
    -11
      src/server/components/spellbook.js
  55. +12
    -10
      src/server/components/stash.js
  56. +12
    -1
      src/server/components/stats.js
  57. +14
    -3
      src/server/components/workbench.js
  58. +4
    -1
      src/server/config/effects/effectFrenzy.js
  59. +7
    -2
      src/server/config/herbs.js
  60. +36
    -0
      src/server/config/itemEffects/castSpellOnHit.js
  61. +11
    -11
      src/server/config/maps/cave/map.json
  62. +6
    -6
      src/server/config/maps/cave/zone.js
  63. +18
    -12
      src/server/config/maps/dungeon/zone.js
  64. +44
    -13
      src/server/config/maps/fjolarok/dialogues.js
  65. +2032
    -1603
      src/server/config/maps/fjolarok/map.json
  66. +186
    -10
      src/server/config/maps/fjolarok/zone.js
  67. +0
    -39
      src/server/config/maps/newSewer/zone.js
  68. +0
    -0
      src/server/config/maps/oldsewer/chats.js
  69. +917
    -1440
      src/server/config/maps/oldsewer/map.json
  70. +246
    -0
      src/server/config/maps/oldsewer/zone.js
  71. +1651
    -620
      src/server/config/maps/sewer/map.json
  72. +269
    -36
      src/server/config/maps/sewer/zone.js
  73. +4
    -0
      src/server/config/quests/templates/questGatherResource.js
  74. +8
    -3
      src/server/config/recipes/alchemy.js
  75. +37
    -0
      src/server/config/recipes/etching.js
  76. +12
    -1
      src/server/config/recipes/recipes.js
  77. +1
    -1
      src/server/config/serverConfig.js
  78. +6
    -0
      src/server/config/skins.js
  79. +20
    -0
      src/server/config/spells.js
  80. +191
    -0
      src/server/config/spells/spellAmbush.js
  81. +4
    -1
      src/server/config/spells/spellCharge.js
  82. +4
    -1
      src/server/config/spells/spellFireblast.js
  83. +1
    -0
      src/server/config/spells/spellFlurry.js
  84. +35
    -0
      src/server/config/spells/spellSmokeBomb.js
  85. +8
    -1
      src/server/config/spells/spellTemplate.js
  86. +60
    -44
      src/server/config/spells/spellWhirlwind.js
  87. +26
    -1
      src/server/config/spellsConfig.js
  88. +8
    -0
      src/server/db/io.js
  89. +3
    -17
      src/server/db/ioRethink.js
  90. +5
    -22
      src/server/db/ioSqlite.js
  91. +16
    -0
      src/server/db/tableNames.js
  92. +1
    -1
      src/server/globals.js
  93. +2
    -1
      src/server/index.js
  94. +15
    -1
      src/server/items/enchanter.js
  95. +8
    -4
      src/server/items/generator.js
  96. +5
    -0
      src/server/items/generators/effects.js
  97. +12
    -0
      src/server/items/generators/recipeBook.js
  98. +37
    -2
      src/server/items/generators/stats.js
  99. +28
    -2
      src/server/items/generators/types.js
  100. +2
    -1
      src/server/misc/scheduler.js

+ 1
- 1
.gitlab-ci.yml View File

@@ -1,4 +1,4 @@
image: node:10
image: node:12

stages:
- test


+ 4
- 4
Dockerfile View File

@@ -1,5 +1,5 @@
# Base image on Node.js 10.x LTS (dubnium)
FROM node:10-alpine
FROM node:12-alpine

# Create app directory
WORKDIR /usr/src/isleward
@@ -10,11 +10,11 @@ COPY . .
# Change directory to src/server/
WORKDIR /usr/src/isleward/src/server/

# Install npm modules specified in package.json
RUN npm install --only-production
# Install only production npm modules specified in package.json
RUN npm install --only=production

# Expose container's port 4000
EXPOSE 4000

# Launch Isleward server
CMD ["node", "index.js"]
CMD ["node", "index.js"]

BIN
View File


+ 1
- 1
src/client/css/colors.less View File

@@ -55,4 +55,4 @@

@grayB: #c0c3cf;
@grayC: #929398;
@grayD: #69696e;
@grayD: #69696e;

+ 2
- 2
src/client/css/main.less View File

@@ -26,7 +26,7 @@ body {
> .right {
position: absolute;
right: 10px;
top: 100px;
top: 92px;
}

&.mobile {
@@ -133,7 +133,7 @@ body {
padding-left: 2px;

&:hover {
background-color: lighten(@black, 10%);
background-color: lighten(@blackA, 10%);
}

}


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 5
- 0
src/client/js/components/social.js View File

@@ -17,6 +17,11 @@ define([
this.blockedList = blueprint.blockedList;
events.emit('onGetBlockedPlayers', this.blockedPlayers);
}

if (blueprint.actions) {
this.actions = blueprint.actions;
events.emit('onGetSocialActions', this.actions);
}
}
};
});

+ 1
- 1
src/client/js/components/spellbook.js View File

@@ -149,7 +149,7 @@ define([
let isShiftDown = input.isKeyDown('shift');

let oldTarget = null;
if (isShiftDown) {
if (isShiftDown || spell.targetPlayerPos) {
oldTarget = this.target;
this.target = this.obj;
}


+ 5
- 3
src/client/js/components/whirlwind.js View File

@@ -10,8 +10,9 @@ define([

row: null,
col: null,
frames: 4,

delay: 40,
delay: 32,
coordinates: [],

objects: null,
@@ -43,7 +44,7 @@ define([
},

spawnThing: function (x, y) {
const { row, col } = this;
const { frames: frameCount, row, col } = this;

this.objects.buildObject({
x,
@@ -51,7 +52,8 @@ define([
components: [{
type: 'attackAnimation',
row,
col
col,
frames: frameCount
}]
});
},


+ 2
- 0
src/client/js/misc/statTranslations.js View File

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

lifeOnHit: 'life gained on hit',

auraReserveMultiplier: 'aura mana reservation multiplier',

//This stat is used for gambling when you can't see the stats


+ 11
- 2
src/client/js/objects/objects.js View File

@@ -197,7 +197,15 @@ define([
}

if (renderer.sprites) {
let isVisible = ((obj.self) || ((renderer.sprites[obj.x]) && (renderer.sprites[obj.x][obj.y].length > 0)));
let isVisible = (
obj.self ||
(
renderer.sprites[obj.x] &&
renderer.sprites[obj.x][obj.y].length > 0 &&
!renderer.isHidden(obj.x, obj.y)
)
);

obj.setVisible(isVisible);
}

@@ -309,7 +317,8 @@ define([
(
renderer.sprites[ix] &&
renderer.sprites[ix][iy] &&
renderer.sprites[ix][iy].length > 0
renderer.sprites[ix][iy].length > 0 &&
!renderer.isHidden(obj.x, obj.y)
)
);
obj.setVisible(isVisible);


+ 91
- 63
src/client/js/rendering/renderer.js View File

@@ -415,52 +415,44 @@ define([
if (!hLen)
return false;

let player = window.player;
let px = player.x;
let py = player.y;

let hidden = false;
for (let i = 0; i < hLen; i++) {
let h = hiddenRooms[i];

let outsideHider = (
x < h.x ||
x >= h.x + h.width ||
y < h.y ||
y >= h.y + h.height
);

if (outsideHider)
continue;

let inHider = physics.isInPolygon(x, y, h.area);

if (!inHider)
continue;
const { player: { x: px, y: py } } = window;

const isVisible = hiddenRooms.every(h => {
if (h.discovered)
return true;

const { x: hx, y: hy, width, height, area } = h;

//Is the tile outside the hider
if (
x < hx ||
x >= hx + width ||
y < hy ||
y >= hy + height
)
return true;

//Is the tile inside the hider
if (!physics.isInPolygon(x, y, area))
return true;

//Is the player outside the hider
if (
px < hx ||
px >= hx + width ||
py < hy ||
py >= hy + height
)
return false;

outsideHider = (
px < h.x ||
px >= h.x + h.width ||
py < h.y ||
py >= h.y + h.height
);

if (outsideHider) {
hidden = true;
continue;
}

inHider = physics.isInPolygon(px, py, h.area);

if (inHider)
//Is the player inside the hider
if (!physics.isInPolygon(px, py, area))
return false;
hidden = true;
}

return hidden;
return true;
});

return !isVisible;
},

updateSprites: function () {
@@ -514,38 +506,71 @@ define([

let rendered = spriteRow[j];
let isHidden = checkHidden(i, j);
if (rendered.length > 0) {
if (!isHidden)
continue;
else {
newHidden.push({
x: i,
y: j
});

let rLen = rendered.length;
for (let k = 0; k < rLen; k++) {
let sprite = rendered[k];
sprite.visible = false;
spritePool.store(sprite);
}
spriteRow[j] = [];

if (isHidden) {
const nonFakeRendered = rendered.filter(r => !r.isFake);

let rLen = nonFakeRendered.length;
for (let k = 0; k < rLen; k++) {
let sprite = nonFakeRendered[k];

sprite.visible = false;
spritePool.store(sprite);
rendered.spliceWhere(s => s === sprite);
}

newHidden.push({
x: i,
y: j
});

const hasFake = cell.some(c => c[0] === '-');
if (hasFake) {
const isFakeRendered = rendered.some(r => r.isFake);
if (isFakeRendered)
continue;
} else
continue;
} else {
const fakeRendered = rendered.filter(r => r.isFake);

let rLen = fakeRendered.length;
for (let k = 0; k < rLen; k++) {
let sprite = fakeRendered[k];

sprite.visible = false;
spritePool.store(sprite);
rendered.spliceWhere(s => s === sprite);
}
} else if (isHidden)
continue;
newVisible.push({
x: i,
y: j
});

newVisible.push({
x: i,
y: j
});

const hasNonFake = cell.some(c => c[0] !== '-');
if (hasNonFake) {
const isNonFakeRendered = rendered.some(r => !r.isFake);
if (isNonFakeRendered)
continue;
} else
continue;
}

for (let k = 0; k < cLen; k++) {
let c = cell[k];
if (c === '0' || c === '')
continue;

const isFake = +c < 0;
if (isFake && !isHidden)
continue;
else if (!isFake && isHidden)
continue;

if (isFake)
c = -c;

c--;

let flipped = '';
@@ -569,6 +594,9 @@ define([
tile.visible = true;
}

if (isFake)
tile.isFake = isFake;

tile.z = k;

rendered.push(tile);


+ 2
- 0
src/client/js/rendering/spritePool.js View File

@@ -28,6 +28,8 @@ define([
if (!list)
list = pool[type] = [];

delete sprite.isFake;

list.push(sprite);
}
};


+ 10
- 10
src/client/js/rendering/tileOpacity.js View File

@@ -106,9 +106,9 @@ define([
],

getSheetNum: function (tile) {
if (tile < 192)
if (tile < 224)
return 0;
else if (tile < 448)
else if (tile < 480)
return 1;
return 2;
},
@@ -116,13 +116,13 @@ define([
map: function (tile) {
let sheetNum;

if (tile < 192)
if (tile < 224)
sheetNum = 0;
else if (tile < 448) {
tile -= 192;
else if (tile < 480) {
tile -= 224;
sheetNum = 1;
} else {
tile -= 448;
tile -= 480;
sheetNum = 2;
}

@@ -140,13 +140,13 @@ define([
canFlip: function (tile) {
let sheetNum;

if (tile < 192)
if (tile < 224)
sheetNum = 0;
else if (tile < 448) {
tile -= 192;
else if (tile < 480) {
tile -= 224;
sheetNum = 1;
} else {
tile -= 448;
tile -= 480;
sheetNum = 2;
}



+ 0
- 106
src/client/js/spriteBuilder.js View File

@@ -1,106 +0,0 @@
define([
'js/resources',
'js/rendering/tileOpacity'
], function (
resources,
tileOpacity
) {
let tileSize = 32;
let width = 0;
let height = 0;

let canvas = null;
let ctx = null;

return {
buildSprite: function (layers, maps, opacities) {
width = maps[0].length;
height = maps[0][0].length;

if (canvas)
canvas.remove();

canvas = $('<canvas></canvas>')
.appendTo('body')
.css('display', 'none');

canvas[0].width = width * tileSize;
canvas[0].height = height * tileSize;

ctx = canvas[0].getContext('2d');

this.build(layers, maps, opacities);

return canvas[0];
},

build: function (layers, maps, opacities) {
let random = Math.random.bind(Math);

for (let m = 0; m < maps.length; m++) {
let map = maps[m];
if (!map)
continue;

let layer = layers[m];
let sprite = resources.sprites[layer].image;

let opacity = opacities[m];

for (let i = 0; i < width; i++) {
let x = i * tileSize;
for (let j = 0; j < height; j++) {
let y = j * tileSize;

let cell = map[i][j];
if (cell === 0)
continue;

cell--;

let tileY = ~~(cell / 8);
let tileX = cell - (tileY * 8);

let tileO = tileOpacity[layer];
if (tileO) {
if (tileO[cell])
ctx.globalAlpha = tileO[cell];
else
ctx.globalAlpha = opacity;
} else
ctx.globalAlpha = opacity;

if (random() > 0.5) {
ctx.drawImage(
sprite,
tileX * tileSize,
tileY * tileSize,
tileSize,
tileSize,
x,
y,
tileSize,
tileSize
);
} else {
ctx.save();
ctx.scale(-1, 1);
ctx.drawImage(
sprite,
tileX * tileSize,
tileY * tileSize,
tileSize,
tileSize,
-x,
y,
-tileSize,
tileSize
);
ctx.restore();
}
}
}
}
}
};
});

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

@@ -115,7 +115,7 @@ define([
if (slot.indexOf('finger') === 0)
slot = 'finger';
else if (slot === 'oneHanded')
return (['oneHanded', 'twoHanded'].includes(slot) && i.isNew);
return (['oneHanded', 'twoHanded'].includes(i.slot) && i.isNew);

return (i.slot === slot && i.isNew);
});
@@ -449,7 +449,9 @@ define([
'holy resist': stats.elementHolyResist,
'poison resist': stats.elementPoisonResist,
gap3: '',
'all resist': stats.elementAllResist
'all resist': stats.elementAllResist,
gap4: '',
'life gained on hit': stats.lifeOnHit
},
misc: {
'item quality': stats.magicFind + '%',


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

@@ -329,7 +329,7 @@ define([
config.push(menuItems.learn);
else if (item.type === 'mtx')
config.push(menuItems.activate);
else if (item.type === 'toy' || item.type === 'consumable' || item.useText) {
else if (item.type === 'toy' || item.type === 'consumable' || item.useText || item.type === 'recipe') {
if (item.useText)
menuItems.use.text = item.useText;
config.push(menuItems.use);


+ 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/isleward/tags/v0.3.3">[ Latest Release Notes ]</div>
<div class="news" location="https://gitlab.com/Isleward/isleward/tags/v0.4.0">[ Latest Release Notes ]</div>
<div class="extra">
<div class="el btn btnPatreon" location="https://patreon.com/bigbadwaffle">Pledge on Patreon</div>
<div class="el btn btnPaypal" location="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=BR2CC82WUAVEA">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/isleward/tags/v0.3.3">v0.3.3</div>
<div class="version" location="https://gitlab.com/Isleward/isleward/tags/v0.4.0">v0.4.0</div>
</div>

+ 29
- 8
src/client/ui/templates/online/online.js View File

@@ -23,6 +23,8 @@ define([
modal: true,
hasClose: true,

actions: [],

postRender: function () {
globals.onlineList = this.onlineList;

@@ -30,11 +32,16 @@ define([
this.onEvent('onGetDisconnectedPlayer', this.onGetDisconnectedPlayer.bind(this));

this.onEvent('onGetBlockedPlayers', this.onGetBlockedPlayers.bind(this));
this.onEvent('onGetSocialActions', this.onGetSocialActions.bind(this));

this.onEvent('onKeyDown', this.onKeyDown.bind(this));
this.onEvent('onShowOnline', this.toggle.bind(this));
},

onGetSocialActions: function (actions) {
this.actions = actions;
},

onGetBlockedPlayers: function (list) {
this.blockedPlayers = list;
},
@@ -117,8 +124,16 @@ define([

showContext: function (char, e) {
if (char.name !== window.player.name) {
let isBlocked = this.blockedPlayers.includes(char.name);
events.emit('onContextMenu', [{
const extraActions = this.actions.map(({ command, text }) => {
return {
text,
callback: this.performAction.bind(this, command, char.name)
};
});

const isBlocked = this.blockedPlayers.includes(char.name);

const actions = [{
text: 'invite to party',
callback: this.invite.bind(this, char.id)
}, {
@@ -127,26 +142,32 @@ define([
}, {
text: isBlocked ? 'unblock' : 'block',
callback: this.block.bind(this, char.name)
}], e);
}, ...extraActions];
events.emit('onContextMenu', actions, e);
}

e.preventDefault();
return false;
},

block: function (charName) {
let isBlocked = this.blockedPlayers.includes(charName);
let method = isBlocked ? 'unblock' : 'block';

performAction: function (command, charName) {
client.request({
cpn: 'social',
method: 'chat',
data: {
message: `/${method} ${charName}`
message: `/${command} ${charName}`
}
});
},

block: function (charName) {
let isBlocked = this.blockedPlayers.includes(charName);
let method = isBlocked ? 'unblock' : 'block';

this.performAction(method, charName);
},

invite: function (charId) {
this.hide();



+ 5
- 3
src/client/ui/templates/reputation/reputation.js View File

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

build: function () {
let list = this.list;
this.find('.info .heading-bottom').html('');
this.find('.info .description').html('');
this.find('.bar-outer').hide();
@@ -83,14 +83,16 @@ define([

onGetReputations: function (list) {
this.list = list;

this.list.sort(function (a, b) {
if (a.name[0] < b.name[0])
return -1;
return 1;
});

if (this.el.is(':visible'))
this.build();
let selElement = this.find('.selected');
if (this.el.is(':visible') && selElement.index() !== -1)
this.onSelectFaction(selElement, list[selElement.index() + 1]);
},

toggle: function () {


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

@@ -90,10 +90,10 @@ define([
if (isMobile)
return false;

let pos = el.offset();
let pos = el.parent().offset();
pos = {
x: pos.left + 56,
y: pos.top + el.height() + 16
x: pos.left - 26,
y: pos.top
};

let values = Object.keys(spell.values).filter(function (v) {
@@ -122,7 +122,7 @@ define([
.replace('range', 'range hidden');
}

events.emit('onShowTooltip', tooltip, el[0], pos, 200, false, true, this.el.css('z-index'));
events.emit('onShowTooltip', tooltip, el[0], pos, 250, false, true, this.el.css('z-index'));
},
onHideTooltip: function (el) {
events.emit('onHideTooltip', el[0]);


+ 14
- 6
src/client/ui/templates/spells/styles.less View File

@@ -8,21 +8,29 @@
right: 10px;
top: 10px;

height: (@btnSize + (@pad * 2));
padding: @pad;
background-color: fade(#3a3b4a, 90%);
height: (@btnSize + @pad);

.spell {
width: @btnSize;
height: @btnSize;
float: left;
margin: 0px 8px;
margin-left: 10px;
background-color: #2d2136;
cursor: pointer;
position: relative;
box-sizing: content-box;
border: 5px solid fade(#3a3b4a, 90%);
transition-property: filter;
transition-duration: 0.1s;

&:first-child {
margin-left: 0px;
}

&:hover {
background-color: fade(@white, 10%);
filter: brightness(160%);

-moz-filter: brightness(160%);
}

&.active .hotkey {


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

@@ -187,7 +187,7 @@ define([
tempImplicitStats.forEach(s => {
let statValue = s.value;

let f = compareImplicitStats.find(c => c.stat === statValue);
let f = compareImplicitStats.find(c => c.stat === s.stat);

if (f) {
let delta = statValue - f.value;
@@ -472,6 +472,15 @@ define([
if (bottomAlign)
pos.y -= this.tooltip.height();

//correct tooltips that are appearing offscreen
// arbitrary constant -30 is there to stop resize code
// completely squishing the popup
if ((pos.x + this.tooltip.width()) > window.innerWidth)
pos.x = window.innerWidth - this.tooltip.width() - 30;

if ((pos.y + this.tooltip.height()) > window.innerHeight)
pos.y = window.innerHeight - this.tooltip.height() - 30;

this.tooltip.css({
left: pos.x,
top: pos.y


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

@@ -119,7 +119,7 @@ define([
let currencyItems = window.player.inventory.items.find(f => f.name === item.worth.currency);
noAfford = ((!currencyItems) || (currencyItems.quantity < item.worth.amount));
} else
noAfford = (item.worth * this.itemList.markup > window.player.trade.gold);
noAfford = (~~(item.worth * this.itemList.markup) > window.player.trade.gold);

if (!noAfford && item.factions)
noAfford = item.factions.some(f => f.noEquip);


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

@@ -15,6 +15,7 @@ define([
centered: true,

modal: true,
hasClose: true,

skin: null,
wardrobeId: null,


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

@@ -15,6 +15,7 @@ define([
centered: true,

modal: true,
hasClose: true,

workbenchId: null,



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

@@ -19,6 +19,9 @@ module.exports = {
threatDecay: 0.9,
threatCeiling: 1,

//Certain summoned minions need to despawn when they lose their last target
dieOnAggroClear: false,

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

@@ -201,7 +204,7 @@ module.exports = {
let oId = source.id;
let list = this.list;

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

let exists = list.find(l => l.obj.id === oId);
@@ -281,6 +284,9 @@ module.exports = {
}
}

if (list.length !== lLen && this.dieOnAggroClear)
this.obj.destroyed = true;

this.ignoreList.spliceWhere(o => o === obj);

//Stuff like cocoons don't have spellbooks


+ 6
- 44
src/server/components/auth.js View File

@@ -4,12 +4,12 @@ let skins = require('../config/skins');
let roles = require('../config/roles');
let profanities = require('../misc/profanities');
let fixes = require('../fixes/fixes');
let loginRewards = require('../config/loginRewards');
let mail = require('../mail/mail');
let scheduler = require('../misc/scheduler');
let spirits = require('../config/spirits');
let ga = require('../security/ga');

const checkLoginRewards = require('./auth/checkLoginRewards');

module.exports = {
type: 'auth',

@@ -22,15 +22,14 @@ module.exports = {

customChannels: [],

play: function (data) {
play: async function (data) {
if (!this.username)
return;

let character = this.characters[data.data.name];
if (!character)
return;

if (character.permadead)
else if (character.permadead)
return;

character.stash = this.stash;
@@ -38,50 +37,13 @@ module.exports = {

this.charname = character.name;

this.checkLoginReward(data, character);
checkLoginRewards(this, data, character, this.onSendRewards.bind(this, data, character));

cons.modifyPlayerCount(1);
},

checkLoginReward: function (data, character) {
let accountInfo = this.accountInfo;

let time = scheduler.getTime();
let lastLogin = accountInfo.lastLogin;
if (!lastLogin || lastLogin.day !== time.day) {
let daysSkipped = 1;
if (lastLogin) {
if (time.day > lastLogin.day)
daysSkipped = time.day - lastLogin.day;
else {
let daysInMonth = scheduler.daysInMonth(lastLogin.month);
daysSkipped = (daysInMonth - lastLogin.day) + time.day;

for (let i = lastLogin.month + 1; i < time.month - 1; i++)
daysSkipped += scheduler.daysInMonth(i);
}
}

if (daysSkipped === 1) {
accountInfo.loginStreak++;
if (accountInfo.loginStreak > 21)
accountInfo.loginStreak = 21;
} else {
accountInfo.loginStreak -= (daysSkipped - 1);
if (accountInfo.loginStreak < 1)
accountInfo.loginStreak = 1;
}

let rewards = loginRewards.generate(accountInfo.loginStreak);
mail.sendMail(character.name, rewards, this.onSendRewards.bind(this, data, character));
} else
this.onSendRewards(data, character);

accountInfo.lastLogin = time;
},

onSendRewards: async function (data, character) {
//Bit of a hack. Rethink doesn't havve a busy list
//Bit of a hack. Rethink doesn't have a busy list
if (mail.busy)
delete mail.busy[character.name];



+ 65
- 0
src/server/components/auth/checkLoginRewards.js View File

@@ -0,0 +1,65 @@
const scheduler = require('../../misc/scheduler');
const loginRewards = require('../../config/loginRewards');
const mail = require('../../mail/mail');

const calculateDaysSkipped = (oldTime, newTime) => {
let daysSkipped = 1;

if (oldTime.year === newTime.year && oldTime.month === newTime.month) {
//Same year and month
daysSkipped = newTime.day - oldTime.day;
} else if (oldTime.year === newTime.year) {
//Same month
let daysInMonth = scheduler.daysInMonth(oldTime.month);
daysSkipped = (daysInMonth - oldTime.day) + newTime.day;

for (let i = oldTime.month + 1; i < newTime.month - 1; i++)
daysSkipped += scheduler.daysInMonth(i);
} else {
//Different year and month
const daysInMonth = scheduler.daysInMonth(oldTime.month);
daysSkipped = (daysInMonth - oldTime.day) + newTime.day;

for (let i = oldTime.year + 1; i < newTime.year - 1; i++)
daysSkipped += 365;

for (let i = oldTime.month + 1; i < 12; i++)
daysSkipped += scheduler.daysInMonth(i);

for (let i = 0; i < newTime.month - 1; i++)
daysSkipped += scheduler.daysInMonth(i);
}

return daysSkipped;
};

module.exports = async (cpnAuth, data, character, cbDone) => {
const accountInfo = cpnAuth.accountInfo;

const time = scheduler.getTime();
const lastLogin = accountInfo.lastLogin;
accountInfo.lastLogin = time;

if (
!lastLogin ||
(
lastLogin.day === time.day &&
lastLogin.month === time.month &&
lastLogin.year === time.year
)
) {
cbDone();
return;
}

const daysSkipped = calculateDaysSkipped(lastLogin, time);
if (daysSkipped === 1)
accountInfo.loginStreak++;
else
accountInfo.loginStreak -= (daysSkipped - 1);

accountInfo.loginStreak = Math.min(1, Math.max(21, accountInfo.loginStreak));

const rewards = loginRewards.generate(accountInfo.loginStreak);
mail.sendMail(character.name, rewards, cbDone);
};

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

@@ -5,6 +5,8 @@ let configMaterials = require('../../items/config/materials');
let factions = require('../../config/factions');
let connections = require('../../security/connections');

const ban = require('../social/ban');

let commandRoles = {
//Regular players
join: 0,
@@ -15,6 +17,7 @@ let commandRoles = {
unblock: 0,

//Mods
ban: 5,
mute: 5,
unmute: 5,

@@ -39,7 +42,9 @@ let commandRoles = {
getMaterials: 10
};

let localCommands = [
//Commands that should be run on the main thread (not the zone thread)
const localCommands = [
'ban',
'join',
'leave',
'mute',
@@ -50,7 +55,24 @@ let localCommands = [
'block',
'unblock',
'broadcast',
'saveAll'
'saveAll',
'ban'
];

//Actions that should appear when a player is right clicked
const contextActions = [
{
command: 'mute',
text: 'mute'
},
{
command: 'unmute',
text: 'unmute'
},
{
command: 'ban',
text: 'ban'
}
];

module.exports = {
@@ -64,6 +86,12 @@ module.exports = {
}

this.roleLevel = roles.getRoleLevel(this.obj);
this.calculateActions();
},

calculateActions: function () {
this.actions = contextActions
.filter(c => this.roleLevel >= commandRoles[c.command]);
},

onBeforeChat: function (msg) {
@@ -642,5 +670,9 @@ module.exports = {

saveAll: function () {
connections.forceSaveAll();
},

ban: function (msg) {
ban(this, msg);
}
};

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

@@ -5,8 +5,13 @@ let classes = require('../config/spirits');
let mtx = require('../mtx/mtx');
let factions = require('../config/factions');
let itemEffects = require('../items/itemEffects');

const { applyItemStats } = require('./equipment/helpers');

const getItem = require('./inventory/getItem');
const dropBag = require('./inventory/dropBag');
const useItem = require('./inventory/useItem');

module.exports = {
type: 'inventory',

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

if (item.slot !== slot)
obj.equipment.unequip(itemId);
else
obj.spellbook.calcDps();
} else
enchanter.enchant(obj, item, msg);

@@ -296,87 +303,7 @@ module.exports = {
},

useItem: function (itemId) {
let item = this.findItem(itemId);
if (!item)
return;

let obj = this.obj;

if (item.cdMax) {
if (item.cd) {
process.send({
method: 'events',
data: {
onGetAnnouncement: [{
obj: {
msg: 'That item is on cooldown'
},
to: [obj.serverId]
}]
}
});

return;
}

item.cd = item.cdMax;

//Find similar items and put them on cooldown too
this.items.forEach(function (i) {
if ((i.name === item.name) && (i.cdMax === item.cdMax))
i.cd = i.cdMax;
});
}

let result = {};
obj.instance.eventEmitter.emit('onBeforeUseItem', obj, item, result);

let effects = (item.effects || []);
let eLen = effects.length;
for (let j = 0; j < eLen; j++) {
let effect = effects[j];
if (!effect.events)
continue;

let effectEvent = effect.events.onConsumeItem;
if (!effectEvent)
continue;

let effectResult = {
success: true,
errorMessage: null
};

effectEvent.call(obj, effectResult, item, effect);

if (!effectResult.success) {
obj.instance.syncer.queue('onGetMessages', {
id: obj.id,
messages: [{
class: 'color-redA',
message: effectResult.errorMessage,
type: 'info'
}]
}, [obj.serverId]);

return;
}
}

if (item.type === 'consumable') {
if (item.uses) {
item.uses--;

if (item.uses) {
obj.syncer.setArray(true, 'inventory', 'getItems', item);
return;
}
}

this.destroyItem(itemId, 1);
if (item.has('quickSlot'))
this.obj.equipment.replaceQuickSlot(item);
}
useItem(this, itemId);
},

unlearnAbility: function (itemId) {
@@ -773,211 +700,11 @@ module.exports = {
},

getItem: function (item, hideMessage, noStack, hideAlert) {
this.obj.instance.eventEmitter.emit('onBeforeGetItem', item, this.obj);

//We need to know if a mob dropped it for quest purposes
let fromMob = item.fromMob;

if (!item.has('quality'))
item.quality = 0;

//Players can't have fromMob items in their inventory but bags can (dropped by a mob)
if (this.obj.player)
delete item.fromMob;

//Store the quantity to send to the player
let quantity = item.quantity;

let exists = false;
if ((item.material || item.quest || item.quantity) && !item.noStack && !item.uses && !noStack) {
let existItem = this.items.find(i => i.name === item.name);
if (existItem) {
exists = true;
existItem.quantity = ~~(existItem.quantity || 1) + ~~(item.quantity || 1);
item = existItem;
}
}

if (!exists)
delete item.pos;

//Get next id
if (!exists) {
let id = 0;
let items = this.items;
let iLen = items.length;

if (!this.hasSpace(item)) {
if (!hideMessage)
this.notifyNoBagSpace();

return false;
}

for (let i = 0; i < iLen; i++) {
let fItem = items[i];
if (fItem.id >= id)
id = fItem.id + 1;
}
item.id = id;

if (item.eq)
delete item.pos;

if (!item.has('pos') && !item.eq) {
let pos = iLen;
for (let i = 0; i < iLen; i++) {
if (!items.some(fi => (fi.pos === i))) {
pos = i;
break;
}
}
item.pos = pos;
}
}

if (this.obj.player) {
let msg = item.name;
if (quantity)
msg += ' x' + quantity;
else if ((item.stats) && (item.stats.weight))
msg += ` ${item.stats.weight}lb`;
const messages = [{
class: 'q' + item.quality,
message: 'loot: {' + msg + '}',
item: item,
type: 'loot'
}];

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

if (!hideMessage) {
this.obj.instance.syncer.queue('onGetMessages', {
id: this.obj.id,
messages: messages
}, [this.obj.serverId]);
}
}

if (item.effects)
this.hookItemEvents([item]);

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

if (item.eq) {
if (item.ability)
this.learnAbility(item.id, item.runeSlot);
else
this.obj.equipment.equip(item.id);
} else if (item.has('quickSlot')) {
this.obj.equipment.setQuickSlot({
itemId: item.id,
slot: item.quickSlot
});
} else {
this.obj.syncer.deleteFromArray(true, 'inventory', 'getItems', i => i.id === item.id);
this.obj.syncer.setArray(true, 'inventory', 'getItems', this.simplifyItem(item), true);
}

if (!hideMessage && fromMob)
this.obj.fireEvent('afterLootMobItem', item);

return item;
return getItem(this, item, hideMessage, noStack, hideAlert);
},

dropBag: function (ownerName, killSource) {
if (!this.blueprint)
return;

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

let items = this.items;
let iLen = items.length;
for (let i = 0; i < iLen; i++) {
delete items[i].eq;
delete items[i].pos;
}

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

let savedItems = extend([], this.items);
this.items = [];

let dropEvent = {
chanceMultiplier: 1,
source: this.obj
};
playerObject.fireEvent('beforeGenerateLoot', dropEvent);

if ((!blueprint.noRandom) || (blueprint.alsoRandom)) {
let bonusMagicFind = killSource.stats.values.magicFind;

let rolls = blueprint.rolls;
let itemQuantity = killSource.stats.values.itemQuantity;
rolls += ~~(itemQuantity / 100);
if ((Math.random() * 100) < (itemQuantity % 100))
rolls++;

for (let i = 0; i < rolls; i++) {
if (Math.random() * 100 >= (blueprint.chance || 35) * dropEvent.chanceMultiplier)
continue;

let itemBlueprint = {
level: this.obj.stats.values.level,
magicFind: magicFind,
bonusMagicFind: bonusMagicFind,
noCurrency: i > 0
};

let useItem = generator.generate(itemBlueprint, playerObject.stats.values.level);
this.getItem(useItem);
}
}

if (blueprint.noRandom) {
let blueprints = blueprint.blueprints;
for (let i = 0; i < blueprints.length; i++) {
let drop = blueprints[i];
if ((blueprint.chance) && (~~(Math.random() * 100) >= blueprint.chance * dropEvent.chanceMultiplier))
continue;
else if ((drop.maxLevel) && (drop.maxLevel < killSource.stats.values.level))
continue;
else if ((drop.chance) && (~~(Math.random() * 100) >= drop.chance * dropEvent.chanceMultiplier))
continue;

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

let item = drop;
if ((!item.quest) && (item.type !== 'key'))
item = generator.generate(drop);

if (!item.slot)
delete item.level;

this.getItem(item, true);
}
}

playerObject.fireEvent('beforeTargetDeath', this.obj, this.items);
this.obj.instance.eventEmitter.emit('onBeforeDropBag', this.obj, this.items, killSource);

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

this.items = savedItems;
dropBag(this, ownerName, killSource);
},

giveItems: function (obj, hideMessage) {


+ 88
- 0
src/server/components/inventory/dropBag.js View File

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

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

const obj = cpnInv.obj;

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

let items = cpnInv.items;
let iLen = items.length;
for (let i = 0; i < iLen; i++) {
delete items[i].eq;
delete items[i].pos;
}

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

let savedItems = extend([], cpnInv.items);
cpnInv.items = [];

let dropEvent = {
chanceMultiplier: 1,
source: obj
};
playerObject.fireEvent('beforeGenerateLoot', dropEvent);

if ((!blueprint.noRandom) || (blueprint.alsoRandom)) {
let bonusMagicFind = killSource.stats.values.magicFind;

let rolls = blueprint.rolls;
let itemQuantity = Math.min(200, killSource.stats.values.itemQuantity);
rolls += ~~(itemQuantity / 100);
if ((Math.random() * 100) < (itemQuantity % 100))
rolls++;

for (let i = 0; i < rolls; i++) {
if (Math.random() * 100 >= (blueprint.chance || 35) * dropEvent.chanceMultiplier)
continue;

let itemBlueprint = {
level: obj.stats.values.level,
magicFind: magicFind,
bonusMagicFind: bonusMagicFind,
noCurrency: i > 0
};

let useItem = generator.generate(itemBlueprint, playerObject.stats.values.level);
cpnInv.getItem(useItem);
}
}

if (blueprint.noRandom) {
let blueprints = blueprint.blueprints;
for (let i = 0; i < blueprints.length; i++) {
let drop = blueprints[i];
if ((drop.maxLevel) && (drop.maxLevel < killSource.stats.values.level))
continue;
else if ((drop.chance) && (~~(Math.random() * 100) >= drop.chance * dropEvent.chanceMultiplier))
continue;

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

let item = drop;
if ((!item.quest) && (item.type !== 'key'))
item = generator.generate(drop);

if (!item.slot)
delete item.level;

cpnInv.getItem(item, true);
}
}

playerObject.fireEvent('beforeTargetDeath', obj, cpnInv.items);
obj.instance.eventEmitter.emit('onBeforeDropBag', obj, cpnInv.items, killSource);

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

cpnInv.items = savedItems;
};

+ 127
- 0
src/server/components/inventory/getItem.js View File

@@ -0,0 +1,127 @@
const getNextId = items => {
let id = 0;
let iLen = items.length;

for (let i = 0; i < iLen; i++) {
let fItem = items[i];
if (fItem.id >= id)
id = fItem.id + 1;
}

return id;
};

module.exports = (cpnInv, item, hideMessage, noStack, hideAlert) => {
const obj = cpnInv.obj;
obj.instance.eventEmitter.emit('onBeforeGetItem', item, obj);

//We need to know if a mob dropped it for quest purposes
let fromMob = item.fromMob;

if (!item.has('quality'))
item.quality = 0;

//Store the quantity to send to the player
let quantity = item.quantity;

let exists = false;
if ((item.material || item.quest || item.quantity) && !item.noStack && !item.uses && !noStack) {
let existItem = cpnInv.items.find(i => i.name === item.name);
if (existItem) {
exists = true;
existItem.quantity = ~~(existItem.quantity || 1) + ~~(item.quantity || 1);
item = existItem;
}
}

if (!exists)
delete item.pos;

//Get next id
if (!exists) {
if (!cpnInv.hasSpace(item)) {
if (!hideMessage)
cpnInv.notifyNoBagSpace();

return false;
}

const items = cpnInv.items;
item.id = getNextId(items);

if (item.eq)
delete item.pos;

if (!item.has('pos') && !item.eq) {
const iLen = items.length;
let pos = iLen;
for (let i = 0; i < iLen; i++) {
if (!items.some(fi => fi.pos === i)) {
pos = i;
break;
}
}
item.pos = pos;
}
}

//Players can't have fromMob items in their inventory but bags can (dropped by a mob)
if (obj.player)
delete item.fromMob;

if (obj.player) {
let msg = item.name;
if (quantity)
msg += ' x' + quantity;
else if ((item.stats) && (item.stats.weight))
msg += ` ${item.stats.weight}lb`;
const messages = [{
class: 'q' + item.quality,
message: 'loot: {' + msg + '}',
item: item,
type: 'loot'
}];

if (!hideAlert) {
obj.instance.syncer.queue('onGetDamage', {
id: obj.id,
event: true,
text: 'loot'
}, -1);
}

if (!hideMessage) {
obj.instance.syncer.queue('onGetMessages', {
id: obj.id,
messages: messages
}, [obj.serverId]);
}
}

if (item.effects)
cpnInv.hookItemEvents([item]);

if (!exists)
cpnInv.items.push(item);

if (item.eq) {
if (item.ability)
cpnInv.learnAbility(item.id, item.runeSlot);
else
obj.equipment.equip(item.id);
} else if (item.has('quickSlot')) {
obj.equipment.setQuickSlot({
itemId: item.id,
slot: item.quickSlot
});
} else {
obj.syncer.deleteFromArray(true, 'inventory', 'getItems', i => i.id === item.id);
obj.syncer.setArray(true, 'inventory', 'getItems', cpnInv.simplifyItem(item), true);
}

if (!hideMessage && fromMob)
obj.fireEvent('afterLootMobItem', item);

return item;
};

+ 50
- 0
src/server/components/inventory/learnRecipe.js View File

@@ -0,0 +1,50 @@
module.exports = async ({ serverId, name }, { recipe: { profession, teaches } }) => {
const recipes = await io.getAsync({
key: name,
table: 'recipes',
isArray: true
});

const known = recipes.some(r => r.profession === profession && r.teaches === teaches);
if (known) {
process.send({
method: 'events',
data: {
onGetAnnouncement: [{
obj: {
msg: 'You already know that recipe'
},
to: [serverId]
}]
}
});

return false;
}

recipes.push({
profession,
teaches
});

await io.setAsync({
key: name,
table: 'recipes',
value: recipes,
serialize: true
});

process.send({
method: 'events',
data: {
onGetAnnouncement: [{
obj: {
msg: 'The recipe imprints itself in your mind, then vanishes'
},
to: [serverId]
}]
}
});

return true;
};

+ 98
- 0
src/server/components/inventory/useItem.js View File

@@ -0,0 +1,98 @@
const learnRecipe = require('./learnRecipe');

const isOnCooldown = (obj, cpnInv, item) => {
if (item.cdMax) {
if (item.cd) {
process.send({
method: 'events',
data: {
onGetAnnouncement: [{
obj: {
msg: 'That item is on cooldown'
},
to: [obj.serverId]
}]
}
});

return;
}

item.cd = item.cdMax;

//Find similar items and put them on cooldown too
cpnInv.items.forEach(function (i) {
if (i.name === item.name && i.cdMax === item.cdMax)
i.cd = i.cdMax;
});
}
};

module.exports = async (cpnInv, itemId) => {
let item = cpnInv.findItem(itemId);
if (!item)
return;

let obj = cpnInv.obj;

if (isOnCooldown(obj, cpnInv, item))
return;

let result = {};
obj.instance.eventEmitter.emit('onBeforeUseItem', obj, item, result);

if (item.recipe) {
const didLearn = await learnRecipe(obj, item);
if (didLearn)
cpnInv.destroyItem(itemId, 1);

return;
}

let effects = (item.effects || []);
let eLen = effects.length;
for (let j = 0; j < eLen; j++) {
let effect = effects[j];
if (!effect.events)
continue;

let effectEvent = effect.events.onConsumeItem;
if (!effectEvent)
continue;

let effectResult = {
success: true,
errorMessage: null
};

effectEvent.call(obj, effectResult, item, effect);

if (!effectResult.success) {
obj.instance.syncer.queue('onGetMessages', {
id: obj.id,
messages: [{
class: 'color-redA',
message: effectResult.errorMessage,
type: 'info'
}]
}, [obj.serverId]);

return;
}
}

if (item.type === 'consumable') {
if (item.uses) {
item.uses--;

if (item.uses) {
obj.syncer.setArray(true, 'inventory', 'getItems', item);
return;
}
}

cpnInv.destroyItem(itemId, 1);
if (item.has('quickSlot'))
cpnInv.obj.equipment.replaceQuickSlot(item);
}
};

+ 19
- 93
src/server/components/social.js View File

@@ -1,6 +1,4 @@
let roles = require('../config/roles');
let events = require('../misc/events');
const profanities = require('../misc/profanities');
const chat = require('./social/chat');

module.exports = {
type: 'social',
@@ -12,6 +10,8 @@ module.exports = {
customChannels: null,
blockedPlayers: [],

actions: null,

messageHistory: [],

maxChatLength: 255,
@@ -24,13 +24,23 @@ module.exports = {
},

simplify: function (self) {
return {
const { party, customChannels, blockedPlayers, actions, muted } = this;

const res = {
type: 'social',
party: this.party,
customChannels: self ? this.customChannels : null,
blockedPlayers: self ? this.blockedPlayers : null,
muted: this.muted
party,
muted
};

if (self) {
Object.assign(res, {
actions,
customChannels,
blockedPlayers
});
}

return res;
},

save: function () {
@@ -181,91 +191,7 @@ module.exports = {
},

chat: function (msg) {
if (!msg.data.message)
return;

msg.data.message = msg.data.message
.split('<')
.join('&lt;')
.split('>')
.join('&gt;');

if (!msg.data.message)
return;

if (msg.data.message.trim() === '')
return;

if (this.muted) {
this.sendMessage('You have been muted from talking', 'color-redA');
return;
}

let messageString = msg.data.message;
if (messageString.length > this.maxChatLength)
return;

let history = this.messageHistory;

let time = +new Date();
history.spliceWhere(h => ((time - h.time) > 5000));

if (history.length > 0) {
if (history[history.length - 1].msg === messageString) {
this.sendMessage('You have already sent that message', 'color-redA');
return;
} else if (history.length >= 3) {
this.sendMessage('You are sending too many messages', 'color-redA');
return;
}
}

this.onBeforeChat(msg.data);
if (msg.data.ignore)
return;

if (!msg.data.item && !profanities.isClean(messageString)) {
this.sendMessage('Profanities detected in message. Blocked.', 'color-redA');
return;
}

history.push({
msg: messageString,
time: time
});

let charname = this.obj.auth.charname;

let msgStyle = roles.getRoleMessageStyle(this.obj) || ('color-grayB');

let msgEvent = {
source: charname,
msg: messageString
};
events.emit('onBeforeSendMessage', msgEvent);
messageString = msgEvent.msg;
if (messageString[0] === '@')
this.sendPrivateMessage(messageString);
else if (messageString[0] === '$')
this.sendCustomChannelMessage(msg);
else if (messageString[0] === '%')
this.sendPartyMessage(msg);
else {
let prefix = roles.getRoleMessagePrefix(this.obj) || '';

cons.emit('event', {
event: 'onGetMessages',
data: {
messages: [{
class: msgStyle,
message: prefix + charname + ': ' + msg.data.message,
item: msg.data.item,
type: 'chat',
source: this.obj.name
}]
}
});
}
chat(this, msg);
},

dc: function () {


+ 27
- 0
src/server/components/social/ban.js View File

@@ -0,0 +1,27 @@
const roles = require('../../config/roles');

module.exports = async (cpnSocial, playerName) => {
let o = cons.players.find(f => (f.name === playerName));
if (!o)
return;

const { username } = o.auth;

let role = roles.getRoleLevel(o);
if (role >= cpnSocial.roleLevel)
return;

await io.setAsync({
key: username,
table: 'login',
value: '{banned}'
});

cons.logOut({
auth: {
username
}
});

cpnSocial.sendMessage('Successfully banned ' + playerName, 'color-yellowB');
};

+ 101
- 0
src/server/components/social/chat.js View File

@@ -0,0 +1,101 @@
let roles = require('../../config/roles');
let events = require('../../misc/events');
const profanities = require('../../misc/profanities');

module.exports = (cpnSocial, msg) => {
if (!msg.data.message)
return;

msg.data.message = msg.data.message
.split('<')
.join('&lt;')
.split('>')
.join('&gt;');

if (!msg.data.message)
return;

if (msg.data.message.trim() === '')
return;

if (cpnSocial.muted) {
cpnSocial.sendMessage('You have been muted from talking', 'color-redA');
return;
}

let messageString = msg.data.message;
if (messageString.length > cpnSocial.maxChatLength)
return;

let history = cpnSocial.messageHistory;

let time = +new Date();
history.spliceWhere(h => ((time - h.time) > 5000));

if (history.length > 0) {
if (history[history.length - 1].msg === messageString) {
cpnSocial.sendMessage('You have already sent that message', 'color-redA');
return;
} else if (history.length >= 3) {
cpnSocial.sendMessage('You are sending too many messages', 'color-redA');
return;
}
}

cpnSocial.onBeforeChat(msg.data);
if (msg.data.ignore)
return;

if (!msg.data.item && !profanities.isClean(messageString)) {
cpnSocial.sendMessage('Profanities detected in message. Blocked.', 'color-redA');
return;
}

let playerLevel = cpnSocial.obj.level;
let playedTime = cpnSocial.obj.stats.stats.played * 1000;
let sessionStart = cpnSocial.obj.player.sessionStart;
let sessionDelta = time - sessionStart;

if (playerLevel < 3 || playedTime + sessionDelta < 180000) {
cpnSocial.sendMessage('Your character needs to be played for at least 3 minutes or be at least level 3 to be able to send messages in chat.', 'color-redA');
return;
}

history.push({
msg: messageString,
time: time
});

let charname = cpnSocial.obj.auth.charname;

let msgStyle = roles.getRoleMessageStyle(cpnSocial.obj) || ('color-grayB');

let msgEvent = {
source: charname,
msg: messageString
};
events.emit('onBeforeSendMessage', msgEvent);
messageString = msgEvent.msg;
if (messageString[0] === '@')
cpnSocial.sendPrivateMessage(messageString);
else if (messageString[0] === '$')
cpnSocial.sendCustomChannelMessage(msg);
else if (messageString[0] === '%')
cpnSocial.sendPartyMessage(msg);
else {
let prefix = roles.getRoleMessagePrefix(cpnSocial.obj) || '';

cons.emit('event', {
event: 'onGetMessages',
data: {
messages: [{
class: msgStyle,
message: prefix + charname + ': ' + msg.data.message,
item: msg.data.item,
type: 'chat',
source: cpnSocial.obj.name
}]
}
});
}
};

+ 12
- 11
src/server/components/spellbook.js View File

@@ -133,7 +133,9 @@ module.exports = {
}

builtSpell.id = !options.has('id') ? spellId : options.id;
if (builtSpell.cdMax)

//Mobs don't get abilities put on CD when they learn them
if (!this.obj.mob && builtSpell.cdMax)
builtSpell.cd = builtSpell.cdMax;

this.spells.push(builtSpell);
@@ -242,18 +244,14 @@ module.exports = {
},

getRandomSpell: function (target) {
let valid = [];
this.spells.forEach(function (s) {
if (s.castOnDeath)
return;

if (s.canCast(target))
valid.push(s.id);
const valid = this.spells.filter(s => {
return (!s.selfCast && !s.procCast && !s.castOnDeath && s.canCast(target));
});

if (valid.length > 0)
return valid[~~(Math.random() * valid.length)];
return null;
if (!valid.length)
return null;

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

getTarget: function (spell, action) {
@@ -444,6 +442,9 @@ module.exports = {
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;
}


+ 12
- 10
src/server/components/stash.js View File

@@ -58,16 +58,18 @@ module.exports = {
if (!this.active)
return;
else if (this.items.length >= 50) {
this.obj.instance.syncer.queue('onGetMessages', {
id: this.obj.id,
messages: [{
class: 'color-redA',
message: 'You do not have room in your stash to deposit that item',
type: 'info'
}]
}, [this.obj.serverId]);

return;
let isMaterial = this.items.some(stashedItem => item.name === stashedItem.name && item.quantity);
if (!isMaterial) {
this.obj.instance.syncer.queue('onGetMessages', {
id: this.obj.id,
messages: [{
class: 'color-redA',
message: 'You do not have room in your stash to deposit that item',
type: 'info'
}]
}, [this.obj.serverId]);
return;
}
}

this.getItem(item);


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

@@ -69,6 +69,8 @@ let baseStats = {

xpIncrease: 0,

lifeOnHit: 0,

//Fishing stats
catchChance: 0,
catchSpeed: 0,
@@ -469,7 +471,7 @@ module.exports = {
syncO.hidden = true;
syncO.nonSelectable = true;

let xpLoss = ~~Math.min(values.xp, values.xpMax / 10);
let xpLoss = ~~Math.min(values.xp, values.xpMax * 0.05);

values.xp -= xpLoss;
obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);
@@ -796,6 +798,15 @@ module.exports = {
if (mobKillStreaks[p] <= 0)
delete mobKillStreaks[p];
}
},

afterDealDamage: function (damageEvent, target) {
const { obj, values: { lifeOnHit } } = this;

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

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

+ 14
- 3
src/server/components/workbench.js View File

@@ -1,4 +1,5 @@
const recipes = require('../config/recipes/recipes');
const generator = require('../items/generator');

module.exports = {
type: 'workbench',
@@ -72,7 +73,7 @@ module.exports = {
}, [obj.serverId]);
},

open: function (msg) {
open: async function (msg) {
if (!msg.has('sourceId'))
return;

@@ -84,10 +85,16 @@ module.exports = {
if ((Math.abs(thisObj.x - obj.x) > 1) || (Math.abs(thisObj.y - obj.y) > 1))
return;

const unlocked = await io.getAsync({
key: obj.name,
table: 'recipes',
isArray: true
});

this.obj.instance.syncer.queue('onOpenWorkbench', {
workbenchId: this.obj.id,
name: this.obj.name,
recipes: recipes.getList(this.craftType)
recipes: recipes.getList(this.craftType, unlocked)
}, [obj.serverId]);
},

@@ -159,7 +166,11 @@ module.exports = {

let outputItems = recipe.item ? [ recipe.item ] : recipe.items;
outputItems.forEach(itemBpt => {
let item = extend({}, itemBpt);
let item = null;
if (itemBpt.generate)
item = generator.generate(itemBpt);
else
item = extend({}, itemBpt);
if (item.description)
item.description += `<br /><br />(Crafted by ${obj.name})`;


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

@@ -4,7 +4,10 @@ module.exports = {

events: {
beforeSetSpellCooldown: function (msg, spell) {
if (!spell.auto)
if (!spell.auto || !spell.isAttack)
return;

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

msg.cd = this.newCd;


+ 7
- 2
src/server/config/herbs.js View File

@@ -26,9 +26,14 @@ module.exports = {
baseWeight: 3,
ttl: 30
},
'Stink Carp': {
Stinkcap: {
sheetName: 'tiles',
cell: 57,
itemSprite: [2, 0]
},
Mudfish: {
sheetName: 'objects',
itemSprite: [11, 0],
itemSprite: [11, 3],
baseWeight: 5,
ttl: 30
}


+ 36
- 0
src/server/config/itemEffects/castSpellOnHit.js View File

@@ -0,0 +1,36 @@
const spellBaseTemplate = require('../spells/spellTemplate');

module.exports = {
events: {
onGetText: function (item) {
const { rolls: { chance, spell } } = item.effects.find(e => (e.type === 'castSpellOnHit'));

return `${chance}% chance to cast ${spell} on hit`;
},

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 } } = 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, {
noEvents: true,
statType: 'dex',
damage: 1,
duration: 5,
radius: 1
});

builtSpell.cast();
}
}
};

+ 11
- 11
src/server/config/maps/cave/map.json View File

@@ -2037,7 +2037,7 @@
"y":224
},
{
"gid":801,
"gid":833,
"height":24,
"id":870,
"name":"|bigPortal",
@@ -2249,7 +2249,7 @@
"y":320
},
{
"gid":721,
"gid":753,
"height":24,
"id":886,
"name":"|bigGem",
@@ -2261,7 +2261,7 @@
"y":72
},
{
"gid":722,
"gid":754,
"height":24,
"id":889,
"name":"|bigGem",
@@ -2273,7 +2273,7 @@
"y":184
},
{
"gid":724,
"gid":756,
"height":24,
"id":890,
"name":"|bigGem",
@@ -2285,7 +2285,7 @@
"y":16
},
{
"gid":725,
"gid":757,
"height":24,
"id":896,
"name":"|bigGem",
@@ -2466,7 +2466,7 @@
"value":"[{\"x\":16,\"y\":136}, {\"source\": \"radulos\", \"x\": 163, \"y\": 35}]"
}],
"renderorder":"right-down",
"tiledversion":"1.2.2",
"tiledversion":"1.2.5",
"tileheight":8,
"tilesets":[
{
@@ -2558,12 +2558,12 @@
"columns":8,
"firstgid":345,
"image":"..\/..\/..\/..\/client\/images\/tiles.png",
"imageheight":192,
"imageheight":224,
"imagewidth":64,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":192,
"tilecount":224,
"tileheight":8,
"tiles":[
{
@@ -2658,7 +2658,7 @@
},
{
"columns":8,
"firstgid":537,
"firstgid":569,
"image":"..\/..\/..\/..\/client\/images\/objects.png",
"imageheight":176,
"imagewidth":64,
@@ -2676,7 +2676,7 @@
},
{
"columns":8,
"firstgid":713,
"firstgid":745,
"image":"..\/..\/..\/..\/client\/images\/bigObjects.png",
"imageheight":240,
"imagewidth":192,
@@ -2689,7 +2689,7 @@
},
{
"columns":8,
"firstgid":793,
"firstgid":825,
"image":"..\/..\/..\/..\/client\/images\/animBigObjects.png",
"imageheight":240,
"imagewidth":192,


+ 6
- 6
src/server/config/maps/cave/zone.js View File

@@ -23,11 +23,11 @@ module.exports = {

regular: {
drops: {
chance: 30,
rolls: 1,
noRandom: true,
alsoRandom: true,
blueprints: [{
chance: 30,
name: 'Digested Crystal',
quality: 0,
quest: true,
@@ -97,11 +97,11 @@ module.exports = {

regular: {
drops: {
chance: 30,
rolls: 1,
noRandom: true,
alsoRandom: true,
blueprints: [{
chance: 35,
name: 'Digested Crystal',
quality: 0,
quest: true,
@@ -269,14 +269,14 @@ module.exports = {
deathRep: -3
},
biorn: {
level: 22,
level: 20,
attackable: false,
walkDistance: 0,
faction: 'akarei',
deathRep: -3
},
veleif: {
level: 22,
level: 20,
attackable: false,
walkDistance: 0,
faction: 'akarei',
@@ -284,13 +284,13 @@ module.exports = {
},

'akarei artificer': {
level: 24,
level: 20,
attackable: false,
faction: 'akarei',
deathRep: -6
},
'thaumaturge yala': {
level: 30,
level: 20,
attackable: false,
walkDistance: 0,
faction: 'akarei',


+ 18
- 12
src/server/config/maps/dungeon/zone.js View File

@@ -10,7 +10,7 @@ let balance = {
level: 20,
hpMult: hpMult * 1,

meleeDmg: 0.25,
meleeDmg: 0.02,
meleeCd: 5,
meleeElement: null,
slowDmg: 0.2,
@@ -24,12 +24,13 @@ let balance = {
level: 20,
hpMult: hpMult * 1.25,

meleeDmg: 0.25,
meleeDmg: 0.035,
meleeCd: 5,
meleeElement: null,
chargeDmg: 0.2,
chargeDmg: 0.4,
chargeCd: 25,
chargeElement: null
chargeElement: null,
chargeStunDuration: 0
},

viridianSerpent: {
@@ -42,7 +43,7 @@ let balance = {
spitDotAmount: 20,
spitElement: 'poison',
poolDuration: 40,
poolDmg: 5,
poolDmg: 0.01,
poolElement: 'poison'
}
}
@@ -60,6 +61,11 @@ module.exports = {
gaekatla: 15
},

spells: [{
type: 'melee',
statMult: 1
}],

regular: {
hpMult: balance.hpMult,
dmgMult: balance.dmgMult,
@@ -93,10 +99,10 @@ module.exports = {
spells: [{
type: 'melee',
element: balance.mobs.violetSerpent.meleeElement,
statMult: balance.mobs.violetSerpent.meleeDmg,
damage: balance.mobs.violetSerpent.meleeDmg,
cdMax: balance.mobs.violetSerpent.meleeCd
}, {
statMult: balance.mobs.violetSerpent.slowDmg,
damage: balance.mobs.violetSerpent.slowDmg,
element: balance.mobs.violetSerpent.slowElement,
cdMax: balance.mobs.violetSerpent.slowCd,
type: 'projectile',
@@ -172,14 +178,14 @@ module.exports = {
spells: [{
type: 'melee',
element: balance.mobs.scarletSerpent.meleeElement,
statMult: balance.mobs.scarletSerpent.meleeDmg,
damage: balance.mobs.scarletSerpent.meleeDmg,
cdMax: balance.mobs.scarletSerpent.meleeCd
}, {
type: 'charge',
targetFurthest: true,
element: balance.mobs.scarletSerpent.chargeElement,
stunDuration: 0,
statMult: balance.mobs.scarletSerpent.chargeDmg,
stunDuration: balance.mobs.scarletSerpent.chargeStunDuration,
damage: balance.mobs.scarletSerpent.chargeDmg,
cdMax: balance.mobs.scarletSerpent.chargeCd
}]
},
@@ -196,9 +202,9 @@ module.exports = {
element: balance.mobs.viridianSerpent.poolElement,
castOnDeath: true,
duration: balance.mobs.viridianSerpent.poolDuration,
cdMult: balance.mobs.viridianSerpent.poolDmg
damage: balance.mobs.viridianSerpent.poolDmg
}, {
statMult: balance.mobs.viridianSerpent.spitDmg,
damage: balance.mobs.viridianSerpent.spitDmg,
element: balance.mobs.viridianSerpent.spitElement,
cdMax: balance.mobs.viridianSerpent.spitCd,
type: 'projectile',


+ 44
- 13
src/server/config/maps/fjolarok/dialogues.js View File

@@ -180,7 +180,7 @@ module.exports = {
1: {
msg: [{
msg: 'Is there anything I can help you with today?',
options: [1.1, 1.2, 1.3, 1.4]
options: [1.1]
}],
options: {
1.1: {
@@ -190,18 +190,6 @@ module.exports = {
return !!fullSet;
},
goto: 'tradeCards'
},
1.2: {
msg: 'I would like to buy some runes',
goto: 'tradeBuy'
},
1.3: {
msg: 'I have some items I would like to sell',
goto: 'tradeSell'
},
1.4: {
msg: 'Could I see the items I sold to you?',
goto: 'tradeBuyback'
}
}
},
@@ -254,6 +242,49 @@ module.exports = {
}]
}
},
asvald: {
1: {
msg: [{
msg: 'Is there anything I can help you with today?',
options: [1.1, 1.2, 1.3]
}],
options: {
1.1: {
msg: 'I would like to buy some runes',
goto: 'tradeBuy'
},
1.2: {
msg: 'I have some items I would like to sell',
goto: 'tradeSell'
},
1.3: {
msg: 'Could I see the items I sold to you?',
goto: 'tradeBuyback'
}
}
},
tradeBuy: {
cpn: 'trade',
method: 'startBuy',
args: [{
targetName: 'asvald'
}]
},
tradeSell: {
cpn: 'trade',
method: 'startSell',
args: [{
targetName: 'asvald'
}]
},
tradeBuyback: {
cpn: 'trade',
method: 'startBuyback',
args: [{
targetName: 'asvald'
}]
}
},
priest: {
1: {
msg: [{


+ 2032
- 1603
src/server/config/maps/fjolarok/map.json
File diff suppressed because it is too large
View File


+ 186
- 10
src/server/config/maps/fjolarok/zone.js View File

@@ -83,6 +83,25 @@ module.exports = {
}
}
},
shopasvald: {
properties: {
cpnNotice: {
actions: {
enter: {
cpn: 'dialogue',
method: 'talk',
args: [{
targetName: 'asvald'
}]
},
exit: {
cpn: 'dialogue',
method: 'stopTalk'
}
}
}
}
},
shoppriest: {
properties: {
cpnNotice: {
@@ -154,6 +173,63 @@ module.exports = {
}
}
},
gas: {
components: {
cpnParticles: {
simplify: function () {
return {
type: 'particles',
blueprint: {
color: {
start: ['c0c3cf', '80f643'],
end: ['386646', '69696e']
},
scale: {
start: {
min: 18,
max: 64
},
end: {
min: 8,
max: 24
}
},
speed: {
start: {
min: 2,
max: 6
},
end: {
min: 0,
max: 4
}
},
lifetime: {
min: 4,
max: 24
},
alpha: {
start: 0.05,
end: 0
},
randomScale: true,
randomSpeed: true,
chance: 0.02,
randomColor: true,
spawnType: 'rect',
blendMode: 'screen',
spawnRect: {
x: -80,
y: -80,
w: 160,
h: 160
}
}
};
}
}
}
},
greencandle: {
components: {
cpnLight: {
@@ -386,6 +462,61 @@ module.exports = {
}
}
},
etchbench: {
components: {
cpnParticles: {
simplify: function () {
return {
type: 'particles',
blueprint: {
color: {
start: ['ff4252', 'ff4252'],
end: ['a82841', 'a82841']
},
scale: {
start: {
min: 2,
max: 10
},
end: {
min: 0,
max: 2
}
},
speed: {
start: {
min: 4,
max: 16
},
end: {
min: 2,
max: 8
}
},
lifetime: {
min: 1,
max: 4
},
randomScale: true,
randomSpeed: true,
chance: 0.2,
randomColor: true,
spawnType: 'rect',
spawnRect: {
x: -15,
y: -28,
w: 30,
h: 8
}
}
};
}
},
cpnWorkbench: {
type: 'etching'
}
}
},
fireplace: {
components: {
cpnWorkbench: {
@@ -412,10 +543,10 @@ module.exports = {

regular: {
drops: {
chance: 100,
rolls: 1,
noRandom: true,
blueprints: [{
chance: 100,
maxLevel: 2,
name: 'Family Heirloom',
quality: 1,
@@ -452,7 +583,28 @@ module.exports = {
}
},
rare: {
name: 'Thumper'
count: 0
},
questItem: {
name: "Rabbit's Foot",
sprite: [0, 1]
}
},
thumper: {
level: 5,
cron: '0 * * * *',
regular: {
hpMult: 3,
dmgMult: 3,

drops: {
chance: 100,
rolls: 2,
magicFind: [1300]
}
},
rare: {
count: 0
},
questItem: {
name: "Rabbit's Foot",
@@ -513,6 +665,20 @@ module.exports = {
eagle: {
level: 10,
faction: 'hostile',
regular: {
drops: {
rolls: 1,
noRandom: true,
alsoRandom: true,
blueprints: [{
chance: 3,
name: 'Eagle Feather',
material: true,
sprite: [0, 0],
spritesheet: 'images/questItems.png'
}]
}
},
rare: {
name: 'Fleshripper'
}
@@ -601,6 +767,23 @@ module.exports = {
}
},
vikar: {
level: 15,
walkDistance: 0,
attackable: false,
rare: {
count: 0
}
},
luta: {
level: 15,
walkDistance: 0,
attackable: false,
rare: {
count: 0
}
},
asvald: {
level: 15,
walkDistance: 0,
attackable: false,
rare: {
@@ -681,13 +864,6 @@ module.exports = {
}
}
},
luta: {
walkDistance: 0,
attackable: false,
rare: {
count: 0
}
},
rodriguez: {
attackable: false,
level: 10,
@@ -717,7 +893,7 @@ module.exports = {
}
},
priest: {
level: 50,
level: 20,
attackable: false,
walkDistance: 0,
rare: {


+ 0
- 39
src/server/config/maps/newSewer/zone.js View File

@@ -1,39 +0,0 @@
module.exports = {
name: 'Sewer',
level: [11, 13],

mobs: {
default: {
faction: 'fjolgard',
deathRep: -5
},

stinktooth: {
faction: 'fjolgard',
grantRep: {
fjolgard: 15
},
level: 13,

regular: {

},

rare: {
count: 0
},

spells: [{
type: 'whirlwind',
range: 1
}]
}
},
objects: {
'stink carp school': {
max: 3000,
type: 'fish',
quantity: [1, 3]
}
}
};

src/server/config/maps/sewer/chats.js → src/server/config/maps/oldsewer/chats.js View File


src/server/config/maps/oldsewer/map.json
File diff suppressed because it is too large
View File


+ 246
- 0
src/server/config/maps/oldsewer/zone.js View File

@@ -0,0 +1,246 @@
module.exports = {
name: 'Sewer',
level: [11, 13],

resources: {
Stinkcap: {
type: 'herb',
max: 100
}
},

mobs: {
default: {
faction: 'fjolgard',
deathRep: -5
},

rat: {
faction: 'fjolgard',
grantRep: {
fjolgard: 6
},
level: 11,

regular: {
drops: {
rolls: 2,
noRandom: true,
alsoRandom: true,
blueprints: [{
chance: 2,
type: 'key',
noSalvage: true,
name: 'Rusted Key',
keyId: 'rustedSewer',
singleUse: true,
sprite: [12, 1],
quantity: 1
}, {
chance: 200,
name: 'Muddy Runestone',
material: true,
sprite: [6, 0],
spritesheet: 'images/materials.png'
}, {
chance: 100,
type: 'recipe',
name: 'Recipe: Noxious Oil',
profession: 'alchemy',
teaches: 'noxiousOil'
}]
}
},

rare: {
count: 0
}
},

stinktooth: {
faction: 'fjolgard',
grantRep: {
fjolgard: 15
},
level: 13,

regular: {
drops: {
rolls: 1,
noRandom: true,
chance: 100,
alsoRandom: true,
blueprints: [{
chance: 0.5,
type: 'key',
noSalvage: true,
name: 'Rusted Key',
keyId: 'rustedSewer',
singleUse: true,
sprite: [12, 1],
quantity: 1
}, {
chance: 100,
type: 'recipe',
name: 'Recipe: Rune of Whirlwind',
profession: 'etching',
teaches: 'runeWhirlwind'
}]
}
},

rare: {
chance: 4,
name: 'Steelclaw',
cell: 59
}
},

bandit: {
faction: 'hostile',
grantRep: {
fjolgard: 18
},
level: 11,

rare: {
count: 0
}
},

whiskers: {
level: 13,
faction: 'hostile',
grantRep: {
fjolgard: 22
},

rare: {
count: 0
}
},

'bera the blade': {
faction: 'hostile',
grantRep: {
fjolgard: 25
},
level: 14,

regular: {
drops: {
rolls: 1,
noRandom: true,
alsoRandom: true,
blueprints: [{
chance: 100,
type: 'key',
noSalvage: true,
name: 'Rusted Key',
keyId: 'rustedSewer',
singleUse: true,
sprite: [12, 1],
quantity: 1
}, {
chance: 100,
type: 'recipe',
name: 'Recipe: Rune of Ambush',
profession: 'etching',
teaches: 'runeAmbush'
}]
}
},

rare: {
count: 0
}
}
},
objects: {
'mudfish school': {
max: 9,
type: 'fish',
quantity: [6, 12]
},
sewerdoor: {
properties: {
cpnDoor: {
autoClose: 171,
locked: true,
key: 'rustedSewer',
destroyKey: true
}
}
},
banditdoor: {
properties: {
cpnDoor: {}
}
},
vaultdoor: {
properties: {
cpnDoor: {}
}
},

etchbench: {
components: {
cpnParticles: {
simplify: function () {
return {
type: 'particles',
blueprint: {
color: {
start: ['ff4252', 'ff4252'],
end: ['a82841', 'a82841']
},
scale: {
start: {
min: 2,
max: 10
},
end: {
min: 0,
max: 2
}
},
speed: {
start: {
min: 4,
max: 16
},
end: {
min: 2,
max: 8
}
},
lifetime: {
min: 1,
max: 4
},
randomScale: true,
randomSpeed: true,
chance: 0.2,
randomColor: true,
spawnType: 'rect',
spawnRect: {
x: -15,
y: -28,
w: 30,
h: 8
}
}
};
}
},
cpnWorkbench: {
type: 'etching'
}
}
},

treasure: {
cron: '0 2 * * *'
}
}
};

+ 1651
- 620
src/server/config/maps/sewer/map.json
File diff suppressed because it is too large
View File


+ 269
- 36
src/server/config/maps/sewer/zone.js View File

@@ -1,7 +1,32 @@
const balance = {
rat: {
clawChance: 3
},
stinktooth: {
runestoneChance: 10,
recipeChance: 3,
shankChance: 0.1
},
bandit: {
keyChance: 1
},
bera: {
recipeChance: 3,
keyChance: 3
}
};

module.exports = {
name: 'Sewer',
level: [11, 13],

resources: {
Stinkcap: {
type: 'herb',
max: 1
}
},

mobs: {
default: {
faction: 'fjolgard',
@@ -21,53 +46,120 @@ module.exports = {
noRandom: true,
alsoRandom: true,
blueprints: [{
chance: 2,
type: 'key',
noSalvage: true,
name: 'Rusted Key',
keyId: 'rustedSewer',
singleUse: true,
sprite: [12, 1],
quantity: 1
chance: balance.rat.clawChance,
name: 'Rat Claw',
material: true,
sprite: [3, 0],
spritesheet: 'images/materials.png'
}]
}
},

rare: {
count: 0
name: 'Enraged Rat',
cell: 24
}
},

stinktooth: {
faction: 'fjolgard',
faction: 'hostile',
grantRep: {
fjolgard: 15
},
level: 13,
cron: '*/10 * * * *',

regular: {
hpMult: 10,
dmgMult: 3,

drops: {
rolls: 1,
rolls: 3,
noRandom: true,
alsoRandom: true,
magicFind: [2000, 125],
blueprints: [{
chance: 0.5,
type: 'key',
noSalvage: true,
name: 'Rusted Key',
keyId: 'rustedSewer',
singleUse: true,
sprite: [12, 1],
quantity: 1
chance: balance.stinktooth.shankChance,
name: 'Putrid Shank',
level: 13,
quality: 4,
slot: 'oneHanded',
type: 'Dagger',
implicitStat: {
stat: 'lifeOnHit',
value: [5, 20]
},
effects: [{
type: 'castSpellOnHit',
rolls: {
i_chance: [5, 20],
spell: 'smokeBomb'
}
}]
}, {
chance: balance.stinktooth.recipeChance,
type: 'recipe',
name: 'Recipe: Rune of Whirlwind',
profession: 'etching',
teaches: 'runeWhirlwind'
}, {
chance: balance.stinktooth.runestoneChance,
name: 'Muddy Runestone',
material: true,
sprite: [6, 0],
spritesheet: 'images/materials.png'
}]
}
},

rare: {
chance: 4,
name: 'Steelclaw',
cell: 59
}
count: 0
},

spells: [{
type: 'melee',
statMult: 1,
damage: 0.08
}, {
type: 'whirlwind',
range: 2,
damage: 0.2,
cdMax: 40
}, {
type: 'summonSkeleton',
killMinionsOnDeath: false,
killMinionsBeforeSummon: false,
minionsDieOnAggroClear: true,
needLos: false,
sheetName: 'mobs',
cdMax: 60,
positions: [[30, 30], [40, 30], [30, 40], [40, 40]],
summonTemplates: [{
name: 'Biter Rat',
cell: 16,
hpPercent: 20,
dmgPercent: 0.1,
basicSpell: 'melee'
}, {
name: 'Spitter Rat',
cell: 24,
hpPercent: 10,
dmgPercent: 0.2,
basicSpell: 'projectile'
}]
}, {
type: 'charge',
castOnEnd: 1,
cdMax: 50,
targetRandom: true,
damage: 0.3
}, {
type: 'fireblast',
range: 2,
damage: 0.001,
pushback: 2,
procCast: true
}]
},

bandit: {
@@ -77,18 +169,41 @@ module.exports = {
},
level: 11,

regular: {
drops: {
noRandom: true,
alsoRandom: true,
blueprints: [{
chance: balance.bandit.keyChance,
type: 'key',
noSalvage: true,
name: 'Rusted Key',
keyId: 'rustedSewer',
singleUse: true,
sprite: [12, 1],
quantity: 1
}]
}
},

rare: {
count: 0
name: 'Cutthroat'
}
},

whiskers: {
'dire rat': {
level: 13,
walkDistance: 0,
faction: 'hostile',
grantRep: {
fjolgard: 22
},

regular: {
hpMult: 5,
dmgMult: 1.2
},

rare: {
count: 0
}
@@ -100,14 +215,24 @@ module.exports = {
fjolgard: 25
},
level: 14,
walkDistance: 0,

regular: {
hpMult: 3,
dmgMult: 1.5,

drops: {
rolls: 1,
noRandom: true,
alsoRandom: true,
blueprints: [{
chance: 100,
chance: balance.bera.recipeChance,
type: 'recipe',
name: 'Recipe: Rune of Ambush',
profession: 'etching',
teaches: 'runeAmbush'
}, {
chance: balance.bera.keyChance,
type: 'key',
noSalvage: true,
name: 'Rusted Key',
@@ -125,6 +250,12 @@ module.exports = {
}
},
objects: {
'mudfish school': {
max: 9,
type: 'fish',
quantity: [6, 12]
},

sewerdoor: {
properties: {
cpnDoor: {
@@ -135,19 +266,121 @@ module.exports = {
}
}
},
banditdoor: {
properties: {
cpnDoor: {}
}
},
vaultdoor: {
properties: {
cpnDoor: {}

bubbles: {
components: {
cpnParticles: {
simplify: function () {
return {
type: 'particles',
blueprint: {
color: {
start: ['51fc9a', '80f643'],
end: ['386646', '44cb95']
},
scale: {
start: {
min: 2,
max: 8
},
end: {
min: 2,
max: 4
}
},
speed: {
start: {
min: 2,
max: 6
},
end: {
min: 0,
max: 4
}
},
lifetime: {
min: 1,
max: 3
},
alpha: {
start: 0.5,
end: 0
},
randomScale: true,
randomSpeed: true,
chance: 0.2,
randomColor: true,
spawnType: 'rect',
blendMode: 'screen',
spawnRect: {
x: -40,
y: -40,
w: 80,
h: 80
}
}
};
}
}
}
},

treasure: {
cron: '0 2 * * *'
gas: {
components: {
cpnParticles: {
simplify: function () {
return {
type: 'particles',
blueprint: {
color: {
start: ['c0c3cf', '80f643'],
end: ['386646', '69696e']
},
scale: {
start: {
min: 18,
max: 64
},
end: {
min: 8,
max: 24
}
},
speed: {
start: {
min: 2,
max: 6
},
end: {
min: 0,
max: 4
}
},
lifetime: {
min: 4,
max: 24
},
alpha: {
start: 0.05,
end: 0
},
randomScale: true,
randomSpeed: true,
chance: 0.02,
randomColor: true,
spawnType: 'rect',
blendMode: 'screen',
spawnRect: {
x: -80,
y: -80,
w: 160,
h: 160
}
}
};
}
}
}
}
}
};

+ 4
- 0
src/server/config/quests/templates/questGatherResource.js View File

@@ -60,6 +60,10 @@ module.exports = {
return;
else if ((this.requiredQuality) && (gatherResult.items[0].quality < this.requiredQuality))
return;
else if (gatherResult.items[0].name.toLowerCase() === 'cerulean pearl') {
//This is a hack but we have no other way to tell fish from pearls at the moment
return;
}

if ((this.obj.zoneName !== this.zoneName) || (this.have >= this.need))
return;


+ 8
- 3
src/server/config/recipes/alchemy.js View File

@@ -27,10 +27,12 @@ module.exports = [{
quantity: 1
}]
}, {
name: 'Stinky Oil',
id: 'noxiousOil',
name: 'Noxious Oil',
description: 'Makes your weapon both stinkier, and hurtier.',
default: false,
item: {
name: 'Stinky Oil',
name: 'Noxious Oil',
type: 'consumable',
sprite: [0, 1],
description: 'Makes your weapon both stinkier, and hurtier.',
@@ -47,7 +49,10 @@ module.exports = [{
}]
},
materials: [{
name: 'Stink Carp',
name: 'Mudfish',
quantity: 3
}, {
name: 'Stinkcap',
quantity: 3
}, {
name: 'Empty Vial',


+ 37
- 0
src/server/config/recipes/etching.js View File

@@ -0,0 +1,37 @@
module.exports = [{
id: 'runeWhirlwind',
name: 'Rune of Whirlwind',
default: false,
description: 'Wiggle-wiggly woo-woo.',
item: {
name: 'Rune of Whirlwind',
generate: true,
spell: true,
spellName: 'whirlwind'
},
materials: [{
name: 'Muddy Runestone',
quantity: 1
}, {
name: 'Eagle Feather',
quantity: 1
}]
}, {
id: 'runeAmbush',
name: 'Rune of Ambush',
default: false,
description: 'Wiggle-wiggly woo-woo.',
item: {
name: 'Rune of Ambush',
generate: true,
spell: true,
spellName: 'ambush'
},
materials: [{
name: 'Muddy Runestone',
quantity: 1
}, {
name: 'Rat Claw',
quantity: 1
}]
}];

+ 12
- 1
src/server/config/recipes/recipes.js View File

@@ -2,6 +2,7 @@ let events = require('../../misc/events');

const recipesAlchemy = require('./alchemy');
const recipesCooking = require('./cooking');
const recipesEtching = require('./etching');

let recipes = {
alchemy: [
@@ -9,6 +10,9 @@ let recipes = {
],
cooking: [
...recipesCooking
],
etching: [
...recipesEtching
]
};

@@ -17,8 +21,15 @@ module.exports = {
events.emit('onBeforeGetRecipes', recipes);
},

getList: function (type) {
getList: function (type, unlocked) {
return (recipes[type] || [])
.filter(r => {
let hasUnlocked = (r.default !== false);
if (!hasUnlocked)
hasUnlocked = unlocked.some(u => u.profession === type && u.teaches === r.id);

return hasUnlocked;
})
.map(r => r.name);
},



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

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


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

@@ -104,10 +104,16 @@ let config = {
sprite: [5, 0]
},

//Frozen Pack
'12.0': {
name: 'Frozen Lance Knight',
spritesheet: 'images/skins/0012.png',
sprite: [0, 0]
},
12.1: {
name: 'Frozen Invoker',
spritesheet: 'images/skins/0012.png',
sprite: [1, 0]
}
};



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

@@ -284,6 +284,20 @@ let spells = [{
randomScale: true,
blendMode: 'screen'
}
}, {
name: 'Whirlwind',
description: 'You furiously spin in a circle, striking all foes around you.',
type: 'whirlwind',
icon: [5, 0],
row: 5,
col: 0,
frames: 3
}, {
name: 'Ambush',
type: 'ambush',
description: 'Step into the shadows and reappear behind your target before delivering a concussing blow.',
icon: [2, 4],
animation: 'raiseShield'
}, {
name: 'Stealth',
description: 'The thief slips into the shadows and becomes undetectable by foes. Performing an attack removes this effect.',
@@ -369,6 +383,12 @@ let spells = [{
chance: 0.075,
randomColor: true
}
}, {
name: 'Whirlwind',
description: 'Wooooo, bitches!.',
type: 'whirlwind',
icon: [0, 1],
animation: 'raiseStaff'
}, {
name: 'Chain Lightning',
description: 'Creates a circle of pure holy energy that heals allies for a brief period.',


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

@@ -0,0 +1,191 @@
const particlePatch = {
type: 'particlePatch',

ttl: 0,

update: function () {
this.ttl--;
if (this.ttl <= 0)
this.obj.destroyed = true;
}
};

module.exports = {
type: 'ambush',

cdMax: 40,
manaCost: 10,
range: 9,

damage: 1,
speed: 70,
isAttack: true,

stunDuration: 20,
needLos: true,

tickParticles: {
ttl: 5,
blueprint: { color: {
start: ['a24eff', '7a3ad3'],
end: ['533399', '393268']
},
scale: {
start: {
min: 2,
max: 12
},
end: {
min: 0,
max: 6
}
},
lifetime: {
min: 1,
max: 2
},
alpha: {
start: 0.8,
end: 0
},
spawnType: 'rect',
spawnRect: {
x: -12,
y: -12,
w: 24,
h: 24
},
randomScale: true,
randomColor: true,
frequency: 0.25 }
},

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

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

let dx = target.x - x;
let dy = target.y - y;

//This calculation is much like the charge one except we land on the
// furthest side of the target instead of the closest. Hence, we multiply
// the delta by -1
let offsetX = 0;
if (dx !== 0)
offsetX = (dx / Math.abs(dx)) * -1;

let offsetY = 0;
if (dy !== 0)
offsetY = (dy / Math.abs(dy)) * -1;

let targetPos = {
x: target.x,
y: target.y
};

let physics = obj.instance.physics;
//Check where we should land
if (!this.isTileValid(physics, x, y, targetPos.x - offsetX, targetPos.y - offsetY)) {
if (!this.isTileValid(physics, x, y, targetPos.x - offsetX, targetPos.y))
targetPos.y -= offsetY;
else
targetPos.x -= offsetX;
} else {
targetPos.x -= offsetX;
targetPos.y -= offsetY;
}

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

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

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

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

this.reachDestination(target, targetPos);

return true;
},

onCastTick: function (particleFrequency) {
const { obj, tickParticles } = this;
const { x, y, instance: { objects } } = obj;

const particleBlueprint = extend({}, tickParticles.blueprint, {
frequency: particleFrequency
});

objects.buildObjects([{
x,
y,
properties: {
cpnParticlePatch: particlePatch,
cpnParticles: {
simplify: function () {
return {
type: 'particles',
blueprint: particleBlueprint
};
},
blueprint: this.particles
}
},
extraProperties: {
particlePatch: {
ttl: tickParticles.ttl
}
}
}]);
},

reachDestination: function (target, targetPos) {
if (this.obj.destroyed)
return;

let obj = this.obj;

obj.x = targetPos.x;
obj.y = targetPos.y;

let syncer = obj.syncer;
syncer.o.x = targetPos.x;
syncer.o.y = targetPos.y;

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

this.onCastTick(0.01);

this.obj.aggro.move();

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

isTileValid: function (physics, fromX, fromY, toX, toY) {
if (physics.isTileBlocking(toX, toY))
return false;
return physics.hasLos(fromX, fromY, toX, toY);
}
};

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

@@ -5,7 +5,7 @@ module.exports = {
manaCost: 10,
range: 9,

damage: 5,
damage: 1,
speed: 70,
isAttack: true,

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

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

if (this.castOnEnd)
this.obj.spellbook.spells[this.castOnEnd].cast();
},

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


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

@@ -7,6 +7,8 @@ module.exports = {
radius: 2,
pushback: 4,

damage: 1,

cast: function (action) {
let obj = this.obj;
let { x, y, instance: { physics, syncer } } = obj;
@@ -54,7 +56,8 @@ module.exports = {

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

let targetPos = {


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

@@ -35,6 +35,7 @@ module.exports = {
this.obj.effects.addEffect({
type: 'frenzy',
ttl: this.duration,
chance: this.chance,
newCd: 1
});
}


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

@@ -46,6 +46,39 @@ let cpnSmokePatch = {
}
};

const particles = {
scale: {
start: {
min: 16,
max: 30
},
end: {
min: 8,
max: 14
}
},
opacity: {
start: 0.02,
end: 0
},
lifetime: {
min: 1,
max: 3
},
speed: {
start: 12,
end: 2
},
color: {
start: ['fcfcfc', '80f643'],
end: ['c0c3cf', '2b4b3e']
},
chance: 0.03,
randomColor: true,
randomScale: true,
blendMode: 'screen'
};

module.exports = {
type: 'smokeBomb',

@@ -60,6 +93,8 @@ module.exports = {
targetGround: true,
targetPlayerPos: true,

particles: particles,

update: function () {
let selfCast = this.selfCast;



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

@@ -11,6 +11,8 @@ module.exports = {
castTimeMax: 0,

needLos: false,
//Should damage/heals caused by this spell cause events to be fired on objects?
noEvents: false,

currentAction: null,

@@ -78,8 +80,12 @@ module.exports = {
this.setCd();
this.currentAction = null;
}
} else
} else {
if (this.onCastTick)
this.onCastTick();
this.sendBump(null, 0, -1);
}

return;
}
@@ -280,6 +286,7 @@ module.exports = {
config.damage = target.stats.values.hpMax * this.damage;

let damage = combat.getDamage(config);
damage.noEvents = this.noEvents;

return damage;
},


+ 60
- 44
src/server/config/spells/spellWhirlwind.js View File

@@ -1,8 +1,39 @@
const coordinates = [
[[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]]
const coordinateDeltas = [
[[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]],
[[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 maxTicks = 8;
const applyDamage = (target, damage, threat, source) => {
target.stats.takeDamage(damage, threat, source);
};

const dealDamage = (spell, obj, coords) => {
const { delay } = spell;

const physics = obj.instance.physics;

coords.forEach(([x, y], i) => {
const cellDelay = i * delay;

const mobs = physics.getCell(x, y);
let mLen = mobs.length;
for (let k = 0; k < mLen; k++) {
const m = mobs[k];

if (!m) {
mLen--;
continue;
}

if (!m.aggro || !m.effects || !obj.aggro.canAttack(m))
continue;

const damage = spell.getDamage(m);

spell.queueCallback(applyDamage.bind(null, m, damage, 1, obj), cellDelay);
}
});
};

module.exports = {
type: 'whirlwind',
@@ -10,62 +41,47 @@ module.exports = {
cdMax: 5,
manaCost: 10,
range: 1,
//The delay is sent to the client and is how long (in ms) each tick takes to display
delay: 32,

damage: 0.0001,
isAttack: true,
row: 5,
col: 0,
frames: 3,

channelDuration: 100,
damage: 1,
isAttack: true,

isCasting: false,
ticker: 0,
targetGround: true,
targetPlayerPos: true,

cast: function (action) {
if (this.isCasting)
return;

//this.isCasting = true;
const { frames, row, col, delay, obj } = this;
const { id, instance, x: playerX, y: playerY } = obj;

const { x: playerX, y: playerY } = this.obj;

const coords = coordinates[this.range - 1].map(([x, y]) => [x + playerX, y + playerY]);
const coordinates = coordinateDeltas[this.range - 1].map(([x, y]) => [x + playerX, y + playerY]);

let blueprint = {
caster: this.obj.id,
caster: id,
components: [{
idSource: this.obj.id,
idSource: id,
type: 'whirlwind',
coordinates: coords,
row: 3,
col: 0
coordinates,
frames,
row,
col,
delay
}]
};

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

return true;
},

reachDestination: function (selfEffect) {
const { effects, destroyed } = this.obj;
if (destroyed)
return;
this.sendBump({
x: playerX,
y: playerY - 1
});

effects.removeEffect(selfEffect);
},
instance.syncer.queue('onGetObject', blueprint, -1);

spawnDamager: function (x, y) {
const { destroyed, instance } = this.obj;
if (destroyed)
return;
dealDamage(this, obj, coordinates);

instance.syncer.queue('onGetObject', {
x,
y,
components: [{
type: 'attackAnimation',
row: 3,
col: 0
}]
}, -1);
return true;
}
};

+ 26
- 1
src/server/config/spellsConfig.js View File

@@ -119,7 +119,20 @@ let spells = {
castTimeMax: 0,
manaCost: 12,
random: {
i_duration: [4, 9]
i_duration: [10, 20],
i_chance: [20, 50]
}
},
whirlwind: {
statType: 'str',
statMult: 1,
threatMult: 6,
cdMax: 12,
castTimeMax: 2,
manaCost: 7,
random: {
i_range: [1, 2.5],
damage: [4, 15]
}
},
smokebomb: {
@@ -135,6 +148,18 @@ let spells = {
i_duration: [7, 13]
}
},
ambush: {
statType: 'dex',
statMult: 1,
cdMax: 16,
castTimeMax: 7,
range: 10,
manaCost: 7,
random: {
damage: [8, 28],
i_stunDuration: [4, 6]
}
},
'crystal spikes': {
statType: ['dex', 'int'],
statMult: 1,


+ 8
- 0
src/server/db/io.js View File

@@ -0,0 +1,8 @@
let serverConfig = require('../config/serverConfig');

const mappings = {
sqlite: './ioSqlite',
rethink: './ioRethink'
};

module.exports = require(mappings[serverConfig.db]);

src/server/security/ioRethink.js → src/server/db/ioRethink.js View File

@@ -1,4 +1,5 @@
const serverConfig = require('../config/serverConfig');
const tableNames = require('./tableNames');

const r = require('rethinkdbdash')({
host: serverConfig.dbHost,
@@ -12,21 +13,6 @@ const dbConfig = {
db: 'live'
};

const tables = [
'character',
'characterList',
'stash',
'skins',
'login',
'leaderboard',
'customMap',
'mail',
'customChannels',
'error',
'modLog',
'accountInfo'
];

module.exports = {
staticCon: null,

@@ -44,12 +30,12 @@ module.exports = {
const con = await this.getConnection();

try {
await r.dbCreate(this.useDb).run();
await r.dbCreate(serverConfig.dbName).run();
} catch (e) {

}

for (const table of tables) {
for (const table of tableNames) {
try {
await r.tableCreate(table).run();
} catch (e) {

src/server/security/io.js → src/server/db/ioSqlite.js View File

@@ -1,10 +1,5 @@
let util = require('util');
let serverConfig = require('../config/serverConfig');

if (serverConfig.db === 'rethink') {
module.exports = require('./ioRethink');
return;
}
const tableNames = require('./tableNames');

module.exports = {
db: null,
@@ -13,20 +8,7 @@ module.exports = {
buffer: [],
processing: [],

tables: {
character: null,
characterList: null,
stash: null,
skins: null,
login: null,
leaderboard: null,
customMap: null,
mail: null,
customChannels: null,
error: null,
modLog: null,
accountInfo: null
},
tables: {},

init: async function (cbReady) {
let sqlite = require('sqlite3').verbose();
@@ -34,10 +16,10 @@ module.exports = {
},
onDbCreated: function (cbReady) {
let db = this.db;
let tables = this.tables;
let scope = this;

db.serialize(function () {
for (let t in tables) {
for (let t of tableNames) {
db.run(`
CREATE TABLE ${t} (key VARCHAR(50), value TEXT)
`, scope.onTableCreated.bind(scope, t));
@@ -46,6 +28,7 @@ module.exports = {
cbReady();
}, this);
},
onTableCreated: async function (table) {
},

+ 16
- 0
src/server/db/tableNames.js View File

@@ -0,0 +1,16 @@
module.exports = [
'character',
'characterList',
'stash',
'skins',
'login',
'leaderboard',
'customMap',
'mail',
'customChannels',
'error',
'modLog',
'accountInfo',
'mtxStash',
'recipes'
];

+ 1
- 1
src/server/globals.js View File

@@ -1,4 +1,4 @@
global.io = require('./security/io');
global.io = require('./db/io');
global.extend = require('./misc/clone');
global.cons = require('./security/connections');
global._ = require('./misc/helpers');


+ 2
- 1
src/server/index.js View File

@@ -23,7 +23,8 @@ let startup = {

onDbReady: async function () {
await fixes.fixDb();

process.on('unhandledRejection', this.onError.bind(this));
process.on('uncaughtException', this.onError.bind(this));

animations.init();


+ 15
- 1
src/server/items/enchanter.js View File

@@ -8,7 +8,9 @@ let configSlots = require('./config/slots');
let generator = require('./generator');

const reroll = (item, msg) => {
let enchantedStats = item.enchantedStats;
const enchantedStats = item.enchantedStats;
const implicitStats = item.implicitStats;

delete item.enchantedStats;
delete item.implicitStats;
delete msg.addStatMsgs;
@@ -45,6 +47,18 @@ const reroll = (item, msg) => {
}
}
item.enchantedStats = enchantedStats || null;

//Some items have special implicits (different stats than their types imply)
// We add the old one back in if this is the case. Ideally we'd like to reroll
// these but that'd be a pretty big hack. We'll solve this one day
if (
item.implicitStats &&
implicitStats &&
item.implicitStats[0] &&
implicitStats[0] &&
item.implicitStats[0].stat !== implicitStats[0].stat
)
item.implicitStats = implicitStats;
};

const relevel = item => {


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

@@ -10,15 +10,17 @@ let g9 = require('./generators/spellbook');
let g10 = require('./generators/currency');
let g11 = require('./generators/effects');
let g12 = require('./generators/attrRequire');
let g13 = require('./generators/recipeBook');

let generators = [g1, g2, g3, g4, g5, g6, g11, g12, g7];
let materialGenerators = [g6, g8];
let spellGenerators = [g1, g9, g7];
let currencyGenerators = [g10];
let recipeGenerators = [g6, g13];

module.exports = {
spellChance: 0.02,
currencyChance: 0.025,
spellChance: 0.035,
currencyChance: 0.035,

generate: function (blueprint, ownerLevel) {
let isSpell = false;
@@ -42,7 +44,7 @@ module.exports = {
if (blueprint.noCurrency)
currencyChance = 0;

if ((!blueprint.slot) && (!blueprint.noSpell)) {
if (!blueprint.slot && !blueprint.noSpell && !blueprint.material) {
isSpell = blueprint.spell;
isCurrency = blueprint.currency;
if ((!isCurrency) && (!isSpell) && ((!hadBlueprint) || ((!blueprint.type) && (!blueprint.slot) && (!blueprint.stats)))) {
@@ -69,7 +71,9 @@ module.exports = {
} else if (blueprint.type === 'mtx') {
item = extend({}, blueprint);
delete item.chance;
} else {
} else if (blueprint.type === 'recipe')
recipeGenerators.forEach(g => g.generate(item, blueprint));
else {
generators.forEach(g => g.generate(item, blueprint));
if (blueprint.spellName)
g9.generate(item, blueprint);


+ 5
- 0
src/server/items/generators/effects.js View File

@@ -11,6 +11,11 @@ module.exports = {
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;


+ 12
- 0
src/server/items/generators/recipeBook.js View File

@@ -0,0 +1,12 @@
module.exports = {
generate: function (item, { profession, teaches, sprite = [0, 5] }) {
item.sprite = sprite;
item.spritesheet = '../../../images/consumables.png';
item.type = 'recipe';

item.recipe = {
profession,
teaches
};
}
};

+ 37
- 2
src/server/items/generators/stats.js View File

@@ -9,6 +9,7 @@ module.exports = {
return (calcPerfection / max);
else if (!perfection)
return random.norm(1, max) * (blueprint.statMult.elementDmgPercent || 1);

return max * perfection * (blueprint.statMult.elementDmgPercent || 1);
},

@@ -23,6 +24,7 @@ module.exports = {
return (calcPerfection / max);
else if (!perfection)
return random.norm(1, max) * (blueprint.statMult.addCritMultiplier || 1);

return max * perfection * (blueprint.statMult.addCritMultiplier || 1);
},

@@ -37,6 +39,7 @@ module.exports = {
return (calcPerfection / max);
else if (!perfection)
return random.norm(1, max) * (blueprint.statMult.addCritChance || 1);

return max * perfection * (blueprint.statMult.addCritChance || 1);
},

@@ -51,6 +54,7 @@ module.exports = {
return (calcPerfection / max);
else if (!perfection)
return random.norm(1, max) * (blueprint.statMult.vit || 1);

return max * perfection * (blueprint.statMult.vit || 1);
},

@@ -66,6 +70,7 @@ module.exports = {
return ((calcPerfection - min) / (max - min));
else if (!perfection)
return random.norm(min, max) * (blueprint.statMult.mainStat || 1);

return (min + ((max - min) * perfection)) * (blueprint.statMult.mainStat || 1);
},
armor: function (item, level, blueprint, perfection, calcPerfection) {
@@ -76,6 +81,7 @@ module.exports = {
return ((calcPerfection - min) / (max - min));
else if (!perfection)
return random.norm(min, max) * blueprint.statMult.armor;

return (min + ((max - min) * perfection)) * (blueprint.statMult.armor || 1);
},
elementResist: function (item, level, blueprint, perfection, calcPerfection) {
@@ -87,6 +93,7 @@ module.exports = {
return (calcPerfection / (100 * div));
else if (!perfection)
return random.norm(1, 100) * (blueprint.statMult.elementResist || 1) * div;

return ~~((1 + (99 * perfection)) * (blueprint.statMult.elementResist || 1) * div);
},
regenHp: function (item, level, blueprint, perfection, calcPerfection) {
@@ -100,6 +107,7 @@ module.exports = {
return (calcPerfection / max);
else if (!perfection)
return random.norm(1, max) * (blueprint.statMult.regenHp || 1);

return max * perfection * (blueprint.statMult.regenHp || 1);
},
lvlRequire: function (item, level, blueprint, perfection, calcPerfection) {
@@ -109,7 +117,20 @@ module.exports = {
return (calcPerfection / max);
else if (!perfection)
return random.norm(1, max) * (blueprint.statMult.lvlRequire || 1);

return max * perfection * (blueprint.statMult.lvlRequire || 1);
},
lifeOnHit: function (item, level, blueprint, perfection, calcPerfection, statBlueprint) {
const { min, max } = statBlueprint;
const scale = level / consts.maxLevel;
const maxRoll = scale * (max - min);

if (calcPerfection)
return ((calcPerfection - min) / maxRoll);
else if (!perfection)
return (min + random.norm(1, maxRoll)) * (blueprint.statMult.lifeOnHit || 1);

return (min + (maxRoll * perfection)) * (blueprint.statMult.lifeOnHit || 1);
}
},

@@ -258,6 +279,13 @@ module.exports = {
ignore: true
},

lifeOnHit: {
min: 1,
max: 10,
ignore: true,
generator: 'lifeOnHit'
},

armor: {
generator: 'armor',
ignore: true
@@ -367,7 +395,10 @@ module.exports = {
},

offHand: {

lifeOnHit: {
min: 1,
max: 10
}
},

trinket: {
@@ -378,6 +409,10 @@ module.exports = {
castSpeed: {
min: 1,
max: 8.75
},
lifeOnHit: {
min: 1,
max: 10
}
},

@@ -556,7 +591,7 @@ module.exports = {
if (!value) {
if (statBlueprint.generator) {
let level = Math.min(20, item.originalLevel || item.level);
value = Math.ceil(this.generators[statBlueprint.generator](item, level, blueprint, blueprint.perfection));
value = Math.ceil(this.generators[statBlueprint.generator](item, level, blueprint, blueprint.perfection, null, statBlueprint));
} else if (!blueprint.perfection)
value = Math.ceil(random.norm(statBlueprint.min, statBlueprint.max));
else


+ 28
- 2
src/server/items/generators/types.js View File

@@ -3,7 +3,33 @@ let armorMaterials = require('../config/armorMaterials');

module.exports = {
generate: function (item, blueprint) {
let type = blueprint.type || _.randomKey(configTypes.types[item.slot]);
let type = blueprint.type;

if (!type) {
//Pick a material type first
const types = configTypes.types[item.slot];
const typeArray = Object.entries(types);
const materials = Object.values(types)
.map(t => {
return t.material;
})
.filter((m, i) => i === typeArray.findIndex(t => t[1].material === m));

const material = materials[~~(Math.random() * materials.length)];

const possibleTypes = {};

Object.entries(types)
.forEach(t => {
const [ typeName, typeConfig ] = t;

if (typeConfig.material === material)
possibleTypes[typeName] = typeConfig;
});

type = _.randomKey(possibleTypes);
}

let typeBlueprint = configTypes.types[item.slot][type] || {};

if (!typeBlueprint)
@@ -27,7 +53,7 @@ module.exports = {
blueprint.attrRequire = material.attrRequire;
}

if (typeBlueprint.implicitStat)
if (typeBlueprint.implicitStat && !blueprint.implicitStat)
blueprint.implicitStat = typeBlueprint.implicitStat;

if (typeBlueprint.attrRequire)


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

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

let time = this.getTime();

return Object.keys(time).every((t, i) => {
return ['minute', 'hour', 'day', 'month', 'weekday'].every((t, i) => {
let f = cron[i].split('-');
if (f[0] === '*')
return true;
@@ -100,6 +100,7 @@ module.exports = {
hour: time.getHours(),
day: time.getDate(),
month: time.getMonth() + 1,
year: time.getUTCFullYear(),
weekday: time.getDay()
};
},


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

Loading…
Cancel
Save