@@ -1,4 +1,4 @@ | |||
image: node:10 | |||
image: node:12 | |||
stages: | |||
- test | |||
@@ -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"] |
@@ -55,4 +55,4 @@ | |||
@grayB: #c0c3cf; | |||
@grayC: #929398; | |||
@grayD: #69696e; | |||
@grayD: #69696e; |
@@ -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%); | |||
} | |||
} | |||
@@ -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); | |||
} | |||
} | |||
}; | |||
}); |
@@ -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; | |||
} | |||
@@ -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 | |||
}] | |||
}); | |||
}, | |||
@@ -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 | |||
@@ -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); | |||
@@ -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); | |||
@@ -28,6 +28,8 @@ define([ | |||
if (!list) | |||
list = pool[type] = []; | |||
delete sprite.isFake; | |||
list.push(sprite); | |||
} | |||
}; | |||
@@ -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; | |||
} | |||
@@ -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(); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}; | |||
}); |
@@ -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 + '%', | |||
@@ -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); | |||
@@ -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> |
@@ -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(); | |||
@@ -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 () { | |||
@@ -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]); | |||
@@ -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 { | |||
@@ -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 | |||
@@ -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); | |||
@@ -15,6 +15,7 @@ define([ | |||
centered: true, | |||
modal: true, | |||
hasClose: true, | |||
skin: null, | |||
wardrobeId: null, | |||
@@ -15,6 +15,7 @@ define([ | |||
centered: true, | |||
modal: true, | |||
hasClose: true, | |||
workbenchId: null, | |||
@@ -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 | |||
@@ -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]; | |||
@@ -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); | |||
}; |
@@ -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); | |||
} | |||
}; |
@@ -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) { | |||
@@ -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; | |||
}; |
@@ -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; | |||
}; |
@@ -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; | |||
}; |
@@ -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); | |||
} | |||
}; |
@@ -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('<') | |||
.split('>') | |||
.join('>'); | |||
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 () { | |||
@@ -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'); | |||
}; |
@@ -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('<') | |||
.split('>') | |||
.join('>'); | |||
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 | |||
}] | |||
} | |||
}); | |||
} | |||
}; |
@@ -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; | |||
} | |||
@@ -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); | |||
@@ -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); | |||
} | |||
} | |||
}; |
@@ -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,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; | |||
@@ -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 | |||
} | |||
@@ -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(); | |||
} | |||
} | |||
}; |
@@ -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, | |||
@@ -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', | |||
@@ -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', | |||
@@ -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: [{ | |||
@@ -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: { | |||
@@ -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] | |||
} | |||
} | |||
}; |
@@ -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 * * *' | |||
} | |||
} | |||
}; |
@@ -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 | |||
} | |||
} | |||
}; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}; |
@@ -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; | |||
@@ -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', | |||
@@ -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 | |||
}] | |||
}]; |
@@ -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,5 +1,5 @@ | |||
module.exports = { | |||
version: '0.3.3', | |||
version: '0.4.0', | |||
port: 4000, | |||
startupMessage: 'Server: ready', | |||
defaultZone: 'fjolarok', | |||
@@ -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] | |||
} | |||
}; | |||
@@ -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.', | |||
@@ -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); | |||
} | |||
}; |
@@ -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) { | |||
@@ -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 = { | |||
@@ -35,6 +35,7 @@ module.exports = { | |||
this.obj.effects.addEffect({ | |||
type: 'frenzy', | |||
ttl: this.duration, | |||
chance: this.chance, | |||
newCd: 1 | |||
}); | |||
} | |||
@@ -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; | |||
@@ -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; | |||
}, | |||
@@ -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; | |||
} | |||
}; |
@@ -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, | |||
@@ -0,0 +1,8 @@ | |||
let serverConfig = require('../config/serverConfig'); | |||
const mappings = { | |||
sqlite: './ioSqlite', | |||
rethink: './ioRethink' | |||
}; | |||
module.exports = require(mappings[serverConfig.db]); |
@@ -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) { |
@@ -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) { | |||
}, |
@@ -0,0 +1,16 @@ | |||
module.exports = [ | |||
'character', | |||
'characterList', | |||
'stash', | |||
'skins', | |||
'login', | |||
'leaderboard', | |||
'customMap', | |||
'mail', | |||
'customChannels', | |||
'error', | |||
'modLog', | |||
'accountInfo', | |||
'mtxStash', | |||
'recipes' | |||
]; |
@@ -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'); | |||
@@ -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(); | |||
@@ -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 => { | |||
@@ -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); | |||
@@ -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; | |||
@@ -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 | |||
}; | |||
} | |||
}; |
@@ -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 | |||
@@ -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) | |||
@@ -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() | |||
}; | |||
}, | |||