diff --git a/src/server/components/aggro.js b/src/server/components/aggro.js index df67e17b..568fa0d7 100644 --- a/src/server/components/aggro.js +++ b/src/server/components/aggro.js @@ -1,7 +1,6 @@ const configThreatCeiling = { regular: 1, - rare: 0.5, - champion: 0.2 + rare: 0.5 }; module.exports = { diff --git a/src/server/components/equipment.js b/src/server/components/equipment.js index 9a36fb3d..0b1e19b0 100644 --- a/src/server/components/equipment.js +++ b/src/server/components/equipment.js @@ -4,7 +4,6 @@ module.exports = { type: 'equipment', eq: {}, - doAutoEq: true, quickSlots: {}, @@ -32,24 +31,6 @@ module.exports = { return !this.eq.has(slot); }, - autoEquip: function (itemId) { - if (!this.doAutoEq) - return; - - let item = this.obj.inventory.findItem(itemId); - if (!item) - return; - else if ((!item.slot) || (item.material) || (item.quest) || (item.ability) || (!this.obj.inventory.canEquipItem(item))) { - item.eq = false; - return; - } - - if (!this.eq.has(item.slot)) { - this.equip({ itemId }); - return true; - } - }, - equip: function (itemId) { let slot = null; if (typeof (itemId) === 'object') { @@ -122,7 +103,8 @@ module.exports = { this.eq[slot] = itemId; item.equipSlot = slot; - obj.spellbook.calcDps(); + if (obj.spellbook) + obj.spellbook.calcDps(); if ((!obj.mob) || (item.ability)) { if (item.spell) diff --git a/src/server/components/spellbook.js b/src/server/components/spellbook.js index e555ef6e..a7602b3d 100644 --- a/src/server/components/spellbook.js +++ b/src/server/components/spellbook.js @@ -27,8 +27,6 @@ module.exports = { this.objects = this.obj.instance.objects; this.physics = this.obj.instance.physics; - this.dmgMult = blueprint.dmgMult; - (blueprint.spells || []).forEach(s => this.addSpell(s, -1)); if (blueprint.rotation) { diff --git a/src/server/components/stats.js b/src/server/components/stats.js index c5d589c7..6c7fa06a 100644 --- a/src/server/components/stats.js +++ b/src/server/components/stats.js @@ -348,8 +348,6 @@ module.exports = { let mobDiffMult = 1; if (target.isRare) mobDiffMult = 2; - else if (target.isChampion) - mobDiffMult = 5; //Who should get xp? let aggroList = target.aggro.list; diff --git a/src/server/config/consts.js b/src/server/config/consts.js index 197919ec..6b8c2ac9 100644 --- a/src/server/config/consts.js +++ b/src/server/config/consts.js @@ -5,6 +5,12 @@ module.exports = { //The maximum level a player can reach maxLevel: 20, + //Rune damage is multiplied by nth entry from this array where n = level - 1 + dmgMults: [0.25, 0.4, 0.575, 0.8, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2, 2.1, 2.2, 2.3, 2.4, 2.5], + + //Mob HP is multiplied by nth entry from this array where n = level - 1 + hpMults: [0.1, 0.2, 0.4, 0.7, 0.78, 0.91, 1.16, 1.19, 1.65, 2.36, 3.07, 3.55, 4.1, 4.85, 5.6, 5.9, 6.5, 7.1, 7.9, 12], + //How far a player can see objects horizontally viewDistanceX: 25, diff --git a/src/server/config/effects/effectChampion.js b/src/server/config/effects/effectChampion.js deleted file mode 100644 index 49e518ce..00000000 --- a/src/server/config/effects/effectChampion.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - type: 'champion' -}; diff --git a/src/server/world/atlas.js b/src/server/world/atlas.js index 869353b4..1ad943cd 100644 --- a/src/server/world/atlas.js +++ b/src/server/world/atlas.js @@ -196,6 +196,7 @@ module.exports = { try { global[message.module][message.method](message); } catch (e) { + /* eslint-disable-next-line no-console */ console.log('No global method found', message.module, message.method); process.exit(); } diff --git a/src/server/world/mobBuilder.js b/src/server/world/mobBuilder.js index 816c6e2e..90dc2bed 100644 --- a/src/server/world/mobBuilder.js +++ b/src/server/world/mobBuilder.js @@ -1,193 +1,201 @@ -let animations = require('../config/animations'); -let itemGenerator = require('../items/generator'); - -//const track = {}; +//Balance +const { hpMults, dmgMults } = require('../config/consts'); + +//Imports +const animations = require('../config/animations'); +const itemGenerator = require('../items/generator'); + +//Mobs will be given random items to equip for these slots +const generateSlots = [ + 'head', + 'chest', + 'neck', + 'hands', + 'waist', + 'legs', + 'feet', + 'finger', + 'trinket', + 'twoHanded' +]; + +//Mobs will pick one of these stats to be force rolles onto their items +const statSelector = ['str', 'dex', 'int']; + +//These stat values are synced to players +const syncStats = ['hp', 'hpMax', 'mana', 'manaMax', 'level']; + +//Component generators +const buildCpnMob = (mob, blueprint, typeDefinition) => { + const { walkDistance, grantRep, deathRep, patrol, needLos } = blueprint; + + const cpnMob = mob.addComponent('mob'); + extend(cpnMob, { + walkDistance, + grantRep, + deathRep, + needLos + }); + + if (patrol !== undefined) + cpnMob.patrol = blueprint.patrol; + + if (cpnMob.patrol) + cpnMob.walkDistance = 1; +}; -module.exports = { - build: function (mob, blueprint, type, zoneName) { - mob.instance.eventEmitter.emit('onBeforeBuildMob', zoneName, mob.name.toLowerCase(), blueprint); +const buildCpnStats = (mob, blueprint, typeDefinition) => { + const { + level, + hpMult: baseHpMult = typeDefinition.hpMult + } = blueprint; - let typeDefinition = blueprint[type] || blueprint; + const hpMax = ~~(level * 40 * hpMults[level - 1] * baseHpMult); - if (blueprint.nonSelectable) - mob.nonSelectable = true; + const cpnStats = mob.addComponent('stats', { + values: { + level, + hpMax, + hp: hpMax + } + }); - mob.addComponent('effects'); - if (type && type !== 'regular') { - mob.effects.addEffect({ - type: type - }); + //Hack to disallow low level mobs from having any lifeOnHit + // since that makes it very difficult (and confusing) for low level players + if (level <= 3) + cpnStats.values.lifeOnHit = 0; +}; - mob['is' + type[0].toUpperCase() + type.substr(1)] = true; +const buildCpnInventory = (mob, blueprint, { drops }, preferStat) => { + const { level } = blueprint; - mob.baseName = mob.name; - mob.name = typeDefinition.name || mob.baseName; - } + const cpnInventory = mob.addComponent('inventory', drops); - if (typeDefinition.sheetName) - mob.sheetName = typeDefinition.sheetName; + cpnInventory.inventorySize = -1; + cpnInventory.dailyDrops = blueprint.dailyDrops; - if (typeDefinition.has('cell')) - mob.cell = typeDefinition.cell; + if (!drops?.blueprints || drops?.alsoRandom) { + generateSlots.forEach(slot => { + const item = itemGenerator.generate({ + noSpell: true, + level, + slot, + quality: 4, + forceStats: [preferStat] + }); + delete item.spell; + item.eq = true; - mob.addComponent('stats', { - values: { - level: blueprint.level - } + cpnInventory.getItem(item); }); + } else { + //TODO: Don't give mobs these items: they'll drop them anyway + drops.blueprints.forEach(d => { + if (d.type === 'key') + return; + + const drop = extend({}, d); + drop.level = level; - let cpnMob = mob.addComponent('mob'); - extend(cpnMob, { - walkDistance: blueprint.walkDistance, - hpMult: blueprint.hpMult || typeDefinition.hpMult, - dmgMult: blueprint.dmgMult || typeDefinition.dmgMult, - grantRep: blueprint.grantRep, - deathRep: blueprint.deathRep + cpnInventory.getItem(itemGenerator.generate(drop)); }); - if (blueprint.patrol) - cpnMob.patrol = blueprint.patrol; + } +}; - if (cpnMob.patrol) - cpnMob.walkDistance = 1; +const buildCpnSpells = (mob, blueprint, typeDefinition, preferStat) => { + const dmgMult = 4.5 * typeDefinition.dmgMult * dmgMults[blueprint.level - 1]; - cpnMob.needLos = blueprint.needLos; + const spells = extend([], blueprint.spells); - let spells = extend([], blueprint.spells); - spells.forEach(s => { - if (!s.animation && mob.sheetName === 'mobs' && animations.mobs[mob.cell]) - s.animation = 'basic'; - }); + mob.addComponent('spellbook', { spells }); - mob.addComponent('spellbook', { - spells: spells, - dmgMult: typeDefinition.dmgMult - }); + let spellCount = 0; + if (mob.isRare) + spellCount = 1; - if (!blueprint.has('attackable') || blueprint.attackable === true) { - mob.addComponent('aggro', { - faction: blueprint.faction - }); + for (let i = 0; i < spellCount; i++) { + const rune = itemGenerator.generate({ spell: true }); + rune.eq = true; - mob.aggro.calcThreatCeiling(type); - } + mob.inventory.getItem(rune); + } - mob.addComponent('equipment'); - mob.addComponent('inventory', typeDefinition.drops); - mob.inventory.inventorySize = -1; - mob.inventory.dailyDrops = blueprint.dailyDrops; - - if (this.zoneConfig) { - let chats = this.zoneConfig.chats; - if (chats && chats[mob.name.toLowerCase()]) { - mob.addComponent('chatter', { - chats: chats[mob.name.toLowerCase()] - }); - } - - let dialogues = this.zoneConfig.dialogues; - if (dialogues && dialogues[mob.name.toLowerCase()]) { - mob.addComponent('dialogue', { - config: dialogues[mob.name.toLowerCase()] - }); - } - } + mob.spellbook.spells.forEach(s => { + s.dmgMult = s.name ? dmgMult / 3 : dmgMult; + s.statType = preferStat; + s.manaCost = 0; - if (blueprint.properties && blueprint.properties.cpnTrade) - mob.addComponent('trade', blueprint.properties.cpnTrade); + if (!s.animation && mob.sheetName === 'mobs' && animations.mobs[mob.cell]) + s.animation = 'basic'; + }); +}; - this.scale(mob, blueprint.level); +const fnComponentGenerators = [ + buildCpnMob, buildCpnStats, buildCpnInventory, buildCpnSpells +]; - mob.instance.eventEmitter.emit('onAfterBuildMob', { - zoneName, - mob - }); - }, - - scale: function (mob, level) { - let drops = mob.inventory.blueprint || {}; - - let statValues = mob.stats.values; - - let preferStat = ['str', 'dex', 'int'][~~(Math.random() * 3)]; - - mob.equipment.unequipAll(); - mob.inventory.clear(); - - let hp = level * 40; - statValues.hpMax = hp; - - statValues.level = level; - - if ((!drops.blueprints) || (drops.alsoRandom)) { - [ - 'head', - 'chest', - 'neck', - 'hands', - 'waist', - 'legs', - 'feet', - 'finger', - 'trinket', - 'twoHanded' - ].forEach(slot => { - let item = itemGenerator.generate({ - noSpell: true, - level: level, - slot: slot, - quality: 4, - forceStats: [preferStat] - }); - - delete item.spell; - mob.inventory.getItem(item); - mob.equipment.autoEquip(item.id); - }); - } else { - //TODO: Don't give the mob these items: he'll drop them anyway - drops.blueprints.forEach(d => { - if (d.type === 'key') - return; +//Main Generator +/* + mob = the mob object + blueprint = mob blueprint (normally from the zoneFile) + type = regular,rare + zoneName = the name of the zone +*/ +const build = (mob, blueprint, type, zoneName) => { + mob.instance.eventEmitter.emit('onBeforeBuildMob', zoneName, mob.name.toLowerCase(), blueprint); - let drop = extend({}, d); - drop.level = level; + const typeDefinition = blueprint[type] || blueprint; - mob.inventory.getItem(itemGenerator.generate(drop)); - }); - } + if (blueprint.nonSelectable) + mob.nonSelectable = true; - let spellCount = (mob.isRare ? 1 : 0) + (mob.isChampion ? 2 : 0); + mob.addComponent('effects'); + if (type === 'rare') { + mob.effects.addEffect({ type: 'rare' }); + mob.isRare = true; - for (let i = 0; i < spellCount; i++) { - let rune = itemGenerator.generate({ - spell: true - }); - rune.eq = true; - mob.inventory.getItem(rune); - } + mob.baseName = mob.name; + mob.name = typeDefinition.name ?? mob.name; + } - let dmgMult = 4.5 * mob.mob.dmgMult; - let hpMult = 1 * mob.mob.hpMult; + if (typeDefinition.sheetName) + mob.sheetName = typeDefinition.sheetName; - dmgMult *= [0.25, 0.4, 0.575, 0.8, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2, 2.1, 2.2, 2.3, 2.4, 2.5][level - 1]; + if (typeDefinition.has('cell')) + mob.cell = typeDefinition.cell; - statValues.hpMax = ~~(statValues.hpMax * [0.1, 0.2, 0.4, 0.7, 0.78, 0.91, 1.16, 1.19, 1.65, 2.36, 3.07, 3.55, 4.1, 4.85, 5.6, 5.9, 6.5, 7.1, 7.9, 12][level - 1]); + mob.addComponent('equipment'); - statValues.hpMax *= hpMult; - statValues.hp = statValues.hpMax; - statValues.mana = statValues.manaMax; + const preferStat = statSelector[~~(Math.random() * 3)]; - mob.spellbook.spells.forEach(s => { - s.dmgMult = s.name ? dmgMult / 3 : dmgMult; - s.statType = preferStat; - s.manaCost = 0; - }); + fnComponentGenerators.forEach(fn => fn(mob, blueprint, typeDefinition, preferStat)); - //Hack to disallow low level mobs from having any lifeOnHit - // since that makes it very difficult (and confusing) for low level players - if (level <= 3) - mob.stats.values.lifeOnHit = 0; + if (blueprint.attackable !== false) { + mob.addComponent('aggro', { faction: blueprint.faction }); - ['hp', 'hpMax', 'mana', 'manaMax', 'level'].forEach(s => mob.syncer.setObject(false, 'stats', 'values', s, statValues[s])); + mob.aggro.calcThreatCeiling(type); } + + const zoneConfig = instancer.instances[0].map.zoneConfig; + + const chats = zoneConfig?.chats?.[mob.name.toLowerCase()]; + if (chats) + mob.addComponent('chatter', { chats }); + + const dialogues = zoneConfig?.dialogues?.[mob.name.toLowerCase()]; + if (dialogues) + mob.addComponent('dialogue', { config: dialogues }); + + if (blueprint?.properties?.cpnTrade) + mob.addComponent('trade', blueprint.properties.cpnTrade); + + mob.instance.eventEmitter.emit('onAfterBuildMob', { + zoneName, + mob + }); + + const statValues = mob.stats.values; + syncStats.forEach(s => mob.syncer.setObject(false, 'stats', 'values', s, statValues[s])); }; + +module.exports = { build }; diff --git a/src/server/world/spawners.js b/src/server/world/spawners.js index ea611bef..a9c27d05 100644 --- a/src/server/world/spawners.js +++ b/src/server/world/spawners.js @@ -10,9 +10,6 @@ module.exports = { this.objects = msg.objects; this.syncer = msg.syncer; this.zoneConfig = msg.zoneConfig; - this.mobBuilder = extend({ - zoneConfig: this.zoneConfig - }, mobBuilder); }, reset: function () { @@ -176,9 +173,7 @@ module.exports = { setupMob: function (mob, blueprint) { let type = 'regular'; - if (blueprint.isChampion) - type = 'champion'; - else if (blueprint.rare.count > 0) { + if (blueprint.rare.count > 0) { const rareCount = this.list.filter(l => ( (l.mob) && (!l.mob.destroyed) && @@ -194,7 +189,7 @@ module.exports = { this.setupObj(mob, blueprint); - this.mobBuilder.build(mob, blueprint, type, this.zoneConfig.name); + mobBuilder.build(mob, blueprint, type, this.zoneConfig.name); }, setupObj: function (obj, blueprint) {