ggejs
Version:
A powerful JavaScript library for interacting with the server of Goodgame Empire: Four Kingdoms
708 lines (697 loc) • 40 kB
JavaScript
const {buildings, dailyactivities} = require('e4k-data').data;
const Horse = require('../../../structures/Horse');
const MovementManager = require('../../../managers/MovementManager');
const {HorseType, WorldMapArea, SpyType} = require("../../../utils/Constants");
const CombatConst = require('../../../utils/CombatConst');
module.exports.name = "dql";
/**
* @param {Socket} socket
* @param {number} errorCode
* @param {{PQL: number, RDQ:{QID:number, P:[number]}[], FDQ: number[], RS: [string,number|number[]][][]}} params
*/
module.exports.execute = async function (socket, errorCode, params) {
if (!params) return;
/** @type {Client} */
const client = socket.client;
try {
/** @type {Player} */
const thisPlayer = await client.players.getThisPlayer();
if (!thisPlayer) return await require('./dql').execute(socket, errorCode, params);
/** @type {CastleMapobject} */
const myMainCastle = thisPlayer.castles.find(x => x.areaType === WorldMapArea.MainCastle);
for (const quest of params.RDQ) {
for (const dailyActivity of dailyactivities) {
if (dailyActivity.dailyQuestID === quest?.QID) {
switch (quest.QID) {
case 1:
try {
socket["dailySpyAt"] = -1;
socket["dailySabotageAt"] = -1;
socket["dailyGoodsTravelTryCount"] = 0;
await client.__x__x__relogin();
} catch (e) {
if (socket.debug) console.error('[DQL]', quest.QID, e);
}
break; //login;
case 2:
try {
/** @type {Castle} */
const mainCastleInfo = await client.getCastleInfo(myMainCastle);
const dungeon = await getClosestDungeon(client, myMainCastle, false);
const horse = new Horse(client, mainCastleInfo, HorseType.Ruby_1);
client.movements.startSpyMovement(myMainCastle, dungeon, 1, SpyType.Military, 50, horse);
} catch (e) {
if (socket.debug) console.error('[DQL]', quest.QID, e);
}
break; //spendC2
case 3:
break; //collectTax
case 4:
try {
if (!socket["dailyGoodsTravelTryCount"]) socket["dailyGoodsTravelTryCount"] = 0;
/** @type {[string, number][]} */
const goods = [["W", 1], ["S", 1], ["F", 1]];
/** @type {WorldMapSector} */
const sector = await client.worldMaps.getSector(myMainCastle.kingdomId, myMainCastle.position.X, myMainCastle.position.Y);
const travelTargetCastles = sector.mapObjects.filter(o => o.ownerInfo !== undefined && o.ownerInfo.playerId !== thisPlayer.playerId && ((o.areaType === 1 && !o.ownerInfo.isRuin) || o.areaType === 4));
if (travelTargetCastles.length === 0) break;
const travelTargetCastle = travelTargetCastles.sort((a, b) => {
return MovementManager.getDistance(myMainCastle, a) - MovementManager.getDistance(myMainCastle, b)
})[socket["dailyGoodsTravelTryCount"]];
client.movements.startMarketMovement(myMainCastle, travelTargetCastle, goods);
socket["dailyGoodsTravelTryCount"] += 1;
} catch (e) {
if (socket.debug) console.error('[DQL]', quest.QID, e);
}
break; //resourceToPlayer
case 5:
try {
if (client.clientUserData.maxSpies <= 1) break;
if (socket["dailySpyAt"] === quest.P && socket["dailySpyTime"] + 1800000 >= Date.now()) break;
const dungeon = await getClosestDungeon(client, myMainCastle, false);
client.movements.startSpyMovement(myMainCastle, dungeon, Math.round(client.clientUserData.maxSpies / 4), SpyType.Military, 50);
socket["dailySpyAt"] = quest.P;
socket["dailySpyTime"] = Date.now();
} catch (e) {
if (socket.debug) console.error('[DQL]', quest.QID, e);
}
break; //spy
case 6:
try {
if (socket["inDQL_q6"]) return;
socket["inDQL_q6"] = true;
const closestRuinsOutpost = getClosestRuinsOutpost(client, await client.worldMaps.get(0), myMainCastle);
if (socket["dailySabotageAt"] !== quest.P || socket["dailySabotageTime"] + 1800000 < Date.now()) {
client.movements.startSpyMovement(myMainCastle, closestRuinsOutpost, Math.round(client.clientUserData.maxSpies / 4), SpyType.Sabotage, 10);
socket["dailySabotageAt"] = quest.P;
socket["dailySabotageTime"] = Date.now();
}
delete socket["inDQL_q6"];
} catch (e) {
delete socket["inDQL_q6"];
if (socket.debug) console.error('[DQL]', quest.QID, e);
}
break; //sabotageDamage
case 7:
case 8:
case 9:
case 10:
try {
const myCastle = dailyActivity.triggerKingdomID === 0 ? myMainCastle : thisPlayer.castles.find(x => x.kingdomId === dailyActivity.triggerKingdomID && x.areaType === WorldMapArea.KingdomCastle);
if (myCastle == null) break;
const lord = socket.client.equipments.getAvailableCommandants()[0];
if (lord == null) break;
await attackDungeon(client, thisPlayer, myCastle, lord);
await new Promise(resolve => setTimeout(resolve, 5000)); //Wait for the attack to be registered to avoid duplicate commander requests.
} catch (e) {
if (socket.debug && false) console.error('[DQL]', quest.QID, e);
}
break; //countDungeons
case 13:
break; //craftEquipment todo
case 14:
// TODO: client.sendChatMessage(" ");
break; //writeInAllianceChat
case 15:
break; //collectFromCitizen, handled by IRC
case 16:
break; //recruitUnits todo
case 17:
break; //produceTools todo
case 21:
break; //requestAllianceHelp todo
case 22:
break; //completeMercenaryMission, handled in 'connection'
case 24:
try {
const lord = socket.client.equipments.getAvailableCommandants()[0];
if (lord == null) break;
await attackDungeon(client, thisPlayer, myMainCastle, lord);
await new Promise(resolve => setTimeout(resolve, 5000)); //Wait for the attack to be registered to avoid duplicate commander requests.
} catch (e) {
if (socket.debug && false) console.error('[DQL]', quest.QID, e);
}
break; //countDungeons tempServer
case 25:
break; //countBattles 10 tempServer todo
case 26:
break; //countBattles 15 tempServer todo
default:
console.warn('[DQL]', "Unknown Daily Activity Quest!", quest);
}
break;
}
}
}
} catch (e) {
if (socket.debug) console.error('[DQL]', e);
}
}
/**
* @param {Client} client
* @param {WorldMap} ClassicMap
* @param {CastleMapobject} myMainCastle
* @returns {CastleMapobject}
*/
function getClosestRuinsOutpost(client, ClassicMap, myMainCastle) {
/** @type {CastleMapobject[]}*/
const ruinedPlayerOutposts = ClassicMap.mapObjects.filter(x => {
if (x.areaType !== WorldMapArea.Outpost) return false;
const owner = x.ownerInfo;
return owner.isRuin && !owner.isInAlliance;
})
ClassicMap = null;
ruinedPlayerOutposts.sort((a, b) => {
const distanceA = MovementManager.getDistance(myMainCastle, a);
const distanceB = MovementManager.getDistance(myMainCastle, b);
return distanceA - distanceB;
})
if (ruinedPlayerOutposts.length === 0) throw "No target found!";
return ruinedPlayerOutposts[0];
}
/**
* @param {Client} client
* @param {CastleMapobject} castle
* @param {boolean} attackable
* @returns {Promise<DungeonMapobject>}
*/
async function getClosestDungeon(client, castle, attackable = true) {
/** @type {DungeonMapobject[]} */
const dungeons = (await client.worldMaps.getSector(castle.kingdomId, castle.position.X, castle.position.Y)).mapObjects.filter(x => x.areaType === WorldMapArea.Dungeon && (!attackable || !x.attackCooldownEnd) && (x.attackCount >= 1 || MovementManager.getDistance(x, castle) <= 12.5));
if (dungeons.length === 0) throw "No target found!";
dungeons.sort((a, b) => {
const distanceA = MovementManager.getDistance(castle, a);
const distanceB = MovementManager.getDistance(castle, b);
return distanceA - distanceB;
})
if (attackable) {
/** @type {Movement[]} */
const movements = client.movements.get();
while (true) {
if (dungeons.length === 0) throw "No attackable dungeon found!";
const __mov = movements.find(x => x.targetArea?.kingdomId === dungeons[0].kingdomId && x.targetArea.position.toString() === dungeons[0].position.toString());
if (__mov == null) break; else dungeons.shift();
}
}
const targetDungeon = dungeons[0];
if (MovementManager.getDistance(castle, targetDungeon) > 50) throw "Target too far away";
return targetDungeon;
}
/**
* @param {DungeonMapobject} dungeon
* @returns {{middle: {moat: number, gate: number, wall: number}, left: {moat: number, wall: number}, right: {moat: number, wall: number}}}
*/
function getDungeonProtection(dungeon) {
/** @type {{left: {wall: number, moat: number}, middle: {wall: number, gate: number, moat: number}, right: {wall: number, moat: number}}} */
const protection = {
left: {
wall: 0, moat: 0,
}, middle: {
wall: 0, gate: 0, moat: 0,
}, right: {
wall: 0, moat: 0,
}
}
protection.middle.gate = buildings.find(b => b.wodID === dungeon.gateWodId).wallBonus;
const dungeonWallBaseProtection = buildings.find(b => b.wodID === dungeon.wallWodId).wallBonus;
for (const k in protection) {
protection[k].wall = dungeonWallBaseProtection;
}
for (const __effect of dungeon.lord.effects) {
if (__effect.name === "gateBonus") {
protection.middle.gate += __effect.power;
} else if (__effect.name === "wallBonus") {
protection.left.wall += __effect.power;
protection.middle.wall += __effect.power;
protection.right.wall += __effect.power;
}
}
for (const k in dungeon.defence.tools) {
for (const /** @type {InventoryItem<Tool>}*/t of dungeon.defence.tools[k]) {
const unitData = t.item.rawData;
if (unitData.typ === "Defence") {
if (unitData.wallBonus) protection[k].wall += unitData.wallBonus; else if (unitData.gateBonus) protection[k].gate += unitData.gateBonus; else if (unitData.moatBonus) protection[k].moat += unitData.moatBonus;
}
}
}
return protection;
}
/**
* @param {DungeonMapobject} dungeon
* @param {{left:{wall:number, moat:number}, middle:{wall:number, gate:number, moat:number}, right:{wall:number, moat:number}}} protection
* @param {{left:{wall:number, moat:number}, middle:{wall:number, gate:number, moat:number}, right:{wall:number, moat:number}}} attackLowerProtection
* @param {{ left: InventoryItem<Tool>[], middle: InventoryItem<Tool>[], right: InventoryItem<Tool>[] }} usedTools
* @returns {{middle: {range: number, melee: number}, left: {range: number, melee: number}, center: {range: number, melee: number}, right: {range: number, melee: number}}}
*/
function getDungeonDefenceStrength(dungeon, protection, attackLowerProtection, usedTools) {
/** @type {{left: {melee: number, range: number}, middle: {melee: number, range: number}, right: {melee: number, range: number}, center: {melee: number, range: number}}} */
const meleeDefenceStrength = {
left: {
melee: 0, range: 0,
}, middle: {
melee: 0, range: 0,
}, right: {
melee: 0, range: 0,
}, center: {
melee: 0, range: 0,
}
}
/** @type {{left: {melee: number, range: number}, middle: {melee: number, range: number}, right: {melee: number, range: number}, center: {melee: number, range: number}}} */
const rangeDefenceStrength = {
left: {
melee: 0, range: 0,
}, middle: {
melee: 0, range: 0,
}, right: {
melee: 0, range: 0,
}, center: {
melee: 0, range: 0,
}
}
for (const k in dungeon.defence.troops) {
for (const /** @type {InventoryItem<Unit>} */ u of dungeon.defence.troops[k]) {
/** @type {Unit} */
const unit = u.item;
/** @type {number} */
const count = u.count;
if (unit.rangeDefence > unit.meleeDefence) {
rangeDefenceStrength[k].range += unit.rangeDefence * count;
rangeDefenceStrength[k].melee += unit.meleeDefence * count;
continue;
}
meleeDefenceStrength[k].range += unit.rangeDefence * count;
meleeDefenceStrength[k].melee += unit.meleeDefence * count;
}
}
/** @type {{left: {melee: number, range: number}, middle: {melee: number, range: number}, right: {melee: number, range: number}, center: {melee: number, range: number}}} */
const defenceStrengthBonus = {
left: {
melee: 0, range: 0,
}, middle: {
melee: 0, range: 0,
}, right: {
melee: 0, range: 0,
}, center: {
melee: 0, range: 0,
}
}
for (const __effect of dungeon.lord.effects) {
if (__effect.name === "meleeBonus") {
defenceStrengthBonus.left.melee += __effect.power;
defenceStrengthBonus.middle.melee += __effect.power;
defenceStrengthBonus.right.melee += __effect.power;
defenceStrengthBonus.center.melee += __effect.power;
continue;
}
if (__effect.name === "rangeBonus") {
defenceStrengthBonus.left.range += __effect.power;
defenceStrengthBonus.middle.range += __effect.power;
defenceStrengthBonus.right.range += __effect.power;
defenceStrengthBonus.center.range += __effect.power;
}
}
for (const k in dungeon.defence.tools) {
for (/** @type {InventoryItem<Tool>}*/const t of dungeon.defence.tools[k]) {
/** @type {Unit} */
const unit = t.item;
if (unit.fightType === 1) {
if (unit.meleeBonus) defenceStrengthBonus[k].melee += unit.meleeBonus; else if (unit.rangeBonus) defenceStrengthBonus[k].range += unit.rangeBonus;
}
}
}
for (const k in usedTools) {
for (/** @type {InventoryItem<Tool>} */const toolAndCount of usedTools[k]) {
/** @type {Unit} */
const tool = toolAndCount.item;
if (tool.fightType === 0) {
if (tool.defRangeBonus) defenceStrengthBonus[k].range -= tool.defRangeBonus * toolAndCount.count;
}
}
}
for (const i in attackLowerProtection) {
for (const j in attackLowerProtection[i]) {
protection[i][j] -= attackLowerProtection[i][j];
}
}
/** @type {{left: number, middle: number, right: number}} */
const defenceBonusWGM = {
left: (1 + Math.max(0, protection.left.wall / 100)) * (1 + Math.max(0, protection.left.moat / 100)) - 1,
middle: (1 + Math.max(0, protection.middle.wall / 100)) * (1 + Math.max(0, protection.middle.gate / 100)) * (1 + Math.max(0, protection.middle.moat / 100)) - 1,
right: (1 + Math.max(0, protection.right.wall / 100)) * (1 + Math.max(0, protection.right.moat / 100)) - 1,
}
/** @type {{left: {melee: number, range: number}, middle: {melee: number, range: number}, right: {melee: number, range: number}, center: {melee: number, range: number}}} */
const totalDefenceBonus = {
left: {
melee: (1 + defenceBonusWGM.left) * (1 + defenceStrengthBonus.left.melee / 100),
range: (1 + defenceBonusWGM.left) * (1 + defenceStrengthBonus.left.range / 100),
}, middle: {
melee: (1 + defenceBonusWGM.middle) * (1 + defenceStrengthBonus.middle.melee / 100),
range: (1 + defenceBonusWGM.middle) * (1 + defenceStrengthBonus.middle.range / 100),
}, right: {
melee: (1 + defenceBonusWGM.right) * (1 + defenceStrengthBonus.right.melee / 100),
range: (1 + defenceBonusWGM.right) * (1 + defenceStrengthBonus.right.range / 100),
}, center: {
melee: 1 + defenceStrengthBonus.center.melee / 100, range: 1 + defenceStrengthBonus.center.range / 100,
}
}
/** @type {{left: {melee: number, range: number}, middle: {melee: number, range: number}, right: {melee: number, range: number}, center: {melee: number, range: number}}} */
const defenceStrengthTotal = {
left: {
melee: 0, range: 0,
}, middle: {
melee: 0, range: 0,
}, right: {
melee: 0, range: 0,
}, center: {
melee: 0, range: 0,
}
}
for (const k in defenceStrengthTotal) {
for (const l in defenceStrengthTotal[k]) {
defenceStrengthTotal[k][l] = meleeDefenceStrength[k][l] * totalDefenceBonus[k].melee + rangeDefenceStrength[k][l] * totalDefenceBonus[k].range;
}
}
return defenceStrengthTotal;
}
/**
* @param {DungeonMapobject} dungeon
* @param {Lord} general
* @param {{left:{wall:number, moat:number}, middle:{wall:number, gate:number, moat:number}, right:{wall:number, moat:number}}} dungeonProtection
* @param {InventoryItem<Tool>[]} availableTools
* @returns {{attackLowerProtection:{ left: { wall: number, moat: number }, middle: { wall: number, gate: number, moat: number }, right: { wall: number, moat: number } }, usedTools: { left: InventoryItem<Tool>[], middle: InventoryItem<Tool>[], right: InventoryItem<Tool>[] }}}
*/
function getAttackLowerProtectionDungeon(dungeon, general, dungeonProtection, availableTools) {
/** @type {{left: {wall: number, moat: number}, middle: {wall: number, gate: number, moat: number}, right: {wall: number, moat: number}}} */
const attackLowerProtection = {
left: {
wall: 0, moat: 0,
}, middle: {
wall: 0, gate: 0, moat: 0,
}, right: {
wall: 0, moat: 0,
}
}
for (const effect of general.effects) {
if (effect.name === "wallReduction" || effect.name === "wallReductionPVE" || effect.name === "relicWallReduction" || effect.name === "relicWallReductionPVE") {
attackLowerProtection.left.wall += effect.power;
attackLowerProtection.middle.wall += effect.power;
attackLowerProtection.right.wall += effect.power;
}
if (effect.name === "gateReduction" || effect.name === "gateReductionPVE" || effect.name === "relicGateReduction" || effect.name === "relicGateReductionPVE") {
attackLowerProtection.middle.gate += effect.power;
}
if (effect.name === "moatReduction" || effect.name === "moatReductionPVE" || effect.name === "relicMoatReduction" || effect.name === "relicMoatReductionPVE") {
attackLowerProtection.left.moat += effect.power;
attackLowerProtection.middle.moat += effect.power;
attackLowerProtection.right.moat += effect.power;
}
}
/** @type {{left: {wall: number, moat: number}, middle: {wall: number, gate: number, moat: number}, right: {wall: number, moat: number}}} */
const restProtection = {
left: {
wall: 0, moat: 0,
}, middle: {
wall: 0, gate: 0, moat: 0,
}, right: {
wall: 0, moat: 0,
}
}
for (const i in restProtection) {
for (const j in restProtection[i]) {
restProtection[i][j] = dungeonProtection[i][j] - attackLowerProtection[i][j];
}
}
/** @type {{middle: InventoryItem<Tool>[], left: InventoryItem<Tool>[], right: InventoryItem<Tool>[]}} */
const usedTools = {left: [], middle: [], right: []};
const availableToolBoxesFlank = CombatConst.getToolSlotCountFlank(dungeon.level);
const availableToolBoxesMiddle = CombatConst.getToolSlotCountFront(dungeon.level);
const maxToolAmountFlank = CombatConst.getTotalAmountToolsFlank(dungeon.level);
const maxToolAmountMiddle = CombatConst.getTotalAmountToolsMiddle(dungeon.level);
const lordRangeDefenceBonus = dungeon.lord.effects.filter(e => e.name === "rangeBonus").map(e => e.power).reduce((sum, a) => sum + a, 0);
for (const side in restProtection) {
const sideIsMiddle = side === "middle"
let toolsUsedOnSide = 0;
const maxToolCountOnSide = sideIsMiddle ? maxToolAmountMiddle : maxToolAmountFlank;
const availableToolBoxes = sideIsMiddle ? availableToolBoxesMiddle : availableToolBoxesFlank;
if (availableToolBoxes >= 2) {
for (const j in restProtection[side]) {
if (restProtection[side][j] > 0) {
for (const availableTool of availableTools) {
const _tool = availableTool.item;
const _toolData = _tool.rawData;
if (_toolData[`${j}Bonus`] == null || restProtection[side][j] / _toolData[`${j}Bonus`] > availableTool.count) continue;
const _toolUseCount = Math.min(Math.ceil(restProtection[side][j] / _toolData[`${j}Bonus`]), maxToolCountOnSide - toolsUsedOnSide);
const tool = usedTools[side].find(t => t.item.wodId === _tool.wodId);
if (tool !== undefined) {
tool.count += _toolUseCount;
toolsUsedOnSide += _toolUseCount;
} else {
usedTools[side].push({item: _tool, count: _toolUseCount});
toolsUsedOnSide += _toolUseCount;
}
availableTool.count -= _toolUseCount;
restProtection[side][j] -= _toolUseCount * _toolData[`${j}Bonus`];
attackLowerProtection[side][j] += _toolUseCount * _toolData[`${j}Bonus`];
break;
}
}
}
if (availableToolBoxes > usedTools[side].length) {
const _rangeDefender = dungeon.defence.troops[side].find(x => x.item.rangeAttack !== undefined && x.item.rangeAttack > 0);
if (_rangeDefender !== undefined) {
const availableShields = availableTools.filter(x => x.item.rawData.defRangeBonus != null);
if (availableShields.length > 0) {
const _tool = availableShields[0].item;
const _toolUseCount = Math.min(availableShields[0].count, Math.ceil((100 + lordRangeDefenceBonus) / _tool.rawData.defRangeBonus), maxToolCountOnSide - toolsUsedOnSide);
if (_toolUseCount > 0) {
usedTools[side].push({item: _tool, count: _toolUseCount});
availableShields[0].count -= _toolUseCount;
}
}
}
}
}
if (availableToolBoxes === 1) {
let highestProtectionType = "";
let highestProtectionValue = 0;
for (const k in restProtection[side]) {
if (restProtection[side][k] > highestProtectionValue) {
highestProtectionType = k;
highestProtectionValue = restProtection[side][k];
}
}
const j = highestProtectionType;
if (restProtection[side][j] > 0) {
for (const availableTool of availableTools) {
const _tool = availableTool.item;
const _toolData = _tool.rawData;
if (_toolData[`${j}Bonus`] == null || restProtection[side][j] / _toolData[`${j}Bonus`] > availableTool.count) continue;
const _toolUseCount = Math.min(Math.ceil(restProtection[side][j] / _toolData[`${j}Bonus`]), maxToolCountOnSide - toolsUsedOnSide);
const tool = usedTools[side].find(t => t.item.wodId === _tool.wodId);
if (tool !== undefined) {
tool.count += _toolUseCount;
toolsUsedOnSide += _toolUseCount;
} else {
usedTools[side].push({item: _tool, count: _toolUseCount});
toolsUsedOnSide += _toolUseCount;
}
availableTool.count -= _toolUseCount;
restProtection[side][j] -= _toolUseCount * _toolData[`${j}Bonus`];
attackLowerProtection[side][j] += _toolUseCount * _toolData[`${j}Bonus`];
break;
}
} else {
const _rangeDefender = dungeon.defence.troops[side].find(x => x.item.rangeAttack !== undefined && x.item.rangeAttack > 0);
if (_rangeDefender !== undefined) {
const availableShields = availableTools.filter(x => x.item.rawData.defRangeBonus != null);
if (availableShields.length > 0) {
const _tool = availableShields[0].item;
const _toolUseCount = Math.min(availableShields[0].count, Math.ceil((100 + lordRangeDefenceBonus) / _tool.rawData.defRangeBonus), maxToolCountOnSide - toolsUsedOnSide);
if (_toolUseCount > 0) {
usedTools[side].push({item: _tool, count: _toolUseCount});
availableShields[0].count -= _toolUseCount;
}
}
}
}
}
}
return {attackLowerProtection, usedTools};
}
/**
* @param {Player} player
* @param {DungeonMapobject} dungeon
* @param {{ left: { melee: number, range: number }, middle: { melee: number, range: number }, right: { melee: number, range: number }, center: { melee: number, range: number } }} defenceStrength
* @param {InventoryItem<Unit>[]} availableSoldiers
* @param {Lord} lord
* @param {{ left: InventoryItem<Tool>[], middle: InventoryItem<Tool>[], right: InventoryItem<Tool>[] }} usedTools
* @param {InventoryItem<Tool>[]} availableTools
* @returns {ArmyWave[]}
*/
function getBestArmyForDungeon(player, dungeon, defenceStrength, availableSoldiers, lord, usedTools, availableTools) {
let meleeStrength = 0;
let rangeStrength = 0;
for (const soldier of availableSoldiers) {
const unit = soldier.item;
const count = soldier.count;
if (unit.rangeAttack) rangeStrength += unit.rangeAttack * count;
if (unit.meleeAttack) meleeStrength += unit.meleeAttack * count;
}
const attackStrength = meleeStrength + rangeStrength;
let _defenceStrengthTotal = 0;
for (const side in defenceStrength) {
for (const type in defenceStrength[side]) {
_defenceStrengthTotal += defenceStrength[side][type];
}
}
if (_defenceStrengthTotal > attackStrength) return [];
let lordMeleeBonus = 0;
let lordRangeBonus = 0;
let frontAttackBonus = 0;
let flankAttackBonus = 0;
let frontUnitAmountBonus = 0;
let flankUnitAmountBonus = 0;
let additionalWaves = 0;
for (const effect of lord.effects) {
if (effect.name === "meleeBonus" || effect.name === "offensiveMeleeBonus" || effect.name === "offensiveMeleeBonusPVE" || effect.name === "relicOffensiveMeleeBonus" || effect.name === "relicOffensiveMeleeBonusPVE" || effect.name === "relicMeleeBonus" || effect.name === "relicMeleeBonusPvE") {
lordMeleeBonus += effect.power / 100;
} else if (effect.name === "offensiveMeleeMalusPVE") {
lordMeleeBonus -= Math.abs(effect.power) / 100;
} else if (effect.name === "rangeBonus" || effect.name === "offensiveRangeBonus" || effect.name === "offensiveRangeBonusPVE" || effect.name === "relicOffensiveRangeBonus" || effect.name === "relicOffensiveRangeBonusPVE" || effect.name === "relicRangeBonus" || effect.name === "relicRangeBonusPvE") {
lordRangeBonus += effect.power / 100;
} else if (effect.name === "offensiveRangeMalusPVE") {
lordRangeBonus -= Math.abs(effect.power) / 100;
} else if (effect.name === "AttackBoostFront" || effect.name === "AttackBoostFront2" || effect.name === "relicAttackBoostFront") {
frontAttackBonus += effect.power / 100;
} else if (effect.name === "AttackBoostFlank" || effect.name === "AttackBoostFlank2" || effect.name === "relicAttackBoostFlank") {
flankAttackBonus += effect.power / 100;
} else if (effect.name === "AttackUnitAmountFront" || effect.name === "attackUnitAmountFrontPVE" || effect.name === "relicAttackUnitAmountFront" || effect.name === "relicAttackUnitAmountFrontPVE") {
frontUnitAmountBonus += effect.power;
} else if (effect.name === "attackUnitAmountFrontMalusPVE") {
frontUnitAmountBonus -= Math.abs(effect.power);
} else if (effect.name === "attackUnitAmountFlank" || effect.name === "attackUnitAmountFlankPVE" || effect.name === "relicAttackUnitAmountFlank" || effect.name === "relicAttackUnitAmountFlankPVE") {
flankUnitAmountBonus += effect.power;
} else if (effect.name === "attackUnitAmountFlankMalusPVE") {
flankUnitAmountBonus -= Math.abs(effect.power);
} else if (effect.name === "additionalWaves" || effect.name === "relicAdditionalWaves") {
additionalWaves += effect.power;
}
}
const meleeSoldiersSorted = availableSoldiers.filter(x => x.item.meleeAttack !== undefined);
const rangeSoldiersSorted = availableSoldiers.filter(x => x.item.rangeAttack !== undefined);
/** @type {ArmyWave[]} */
const army = [];
const waveCount = CombatConst.getMaxWaveCountWithBonus(player.playerLevel, false, additionalWaves);
for (let w = 0; w < waveCount; w++) {
/** @type {ArmyWave} */
const wave = {
left: {units: [], tools: []}, middle: {units: [], tools: []}, right: {units: [], tools: []}
}
for (const side in defenceStrength) {
if (wave[side] == null) continue;
let fillRange = false;
let fillMelee = false;
const maxSoldiersOnSide = CombatConst.getAmountSoldiers(side === "middle" ? 1 : 0, dungeon.level, flankUnitAmountBonus, frontUnitAmountBonus);
const maxSoldierBoxesOnSide = side === "middle" ? CombatConst.getUnitSlotCountFront(dungeon.level) : CombatConst.getUnitSlotCountFlank(dungeon.level);
meleeSoldiersSorted.sort((a, b) => -(a.item.meleeAttack * Math.min(a.count, maxSoldiersOnSide) - b.item.meleeAttack * Math.min(b.count, maxSoldiersOnSide)));
rangeSoldiersSorted.sort((a, b) => -(a.item.rangeAttack * Math.min(a.count, maxSoldiersOnSide) - b.item.rangeAttack * Math.min(b.count, maxSoldiersOnSide)));
if (defenceStrength[side].melee < defenceStrength[side].range) {
if (meleeSoldiersSorted.length === 0) continue;
fillMelee = true;
} else if (defenceStrength[side].melee > defenceStrength[side].range) {
if (rangeSoldiersSorted.length === 0) continue;
fillRange = true;
} else if (defenceStrength.center.melee < defenceStrength.center.range) {
if (meleeSoldiersSorted.length === 0) continue;
fillMelee = true;
} else if (defenceStrength.center.melee > defenceStrength.center.range) {
if (rangeSoldiersSorted.length === 0) continue;
fillRange = true;
} else if (meleeSoldiersSorted.length === 0) {
if (rangeSoldiersSorted.length === 0) continue;
fillRange = true;
} else if (rangeSoldiersSorted.length === 0) {
if (meleeSoldiersSorted.length === 0) continue;
fillMelee = true;
} else if (rangeSoldiersSorted[0].count > meleeSoldiersSorted[0].count) {
if (rangeSoldiersSorted.length === 0) continue;
fillRange = true;
} else {
if (meleeSoldiersSorted.length === 0) continue;
fillMelee = true;
}
const sideAttackBonus = side === "middle" ? frontAttackBonus : flankAttackBonus;
if (fillRange) {
const unitCount = Math.min(maxSoldiersOnSide, rangeSoldiersSorted[0].count);
const _unitCount = !rangeSoldiersSorted[1] ? 0 : Math.max(0, Math.min(maxSoldiersOnSide - unitCount, rangeSoldiersSorted[1].count));
const bonus = 1 + lordRangeBonus + sideAttackBonus;
const unitStrength = (unitCount * rangeSoldiersSorted[0].item.rangeAttack * bonus) + (!rangeSoldiersSorted[1] ? 0 : (_unitCount * rangeSoldiersSorted[1].item.rangeAttack * bonus));
if (unitStrength > defenceStrength[side].range) {
wave[side].units.push({item: rangeSoldiersSorted[0].item, count: unitCount});
rangeSoldiersSorted[0].count -= unitCount;
if (maxSoldierBoxesOnSide > 1 && _unitCount > 0) {
wave[side].units.push({item: rangeSoldiersSorted[1].item, count: _unitCount});
rangeSoldiersSorted[1].count -= _unitCount;
}
}
}
if (fillMelee) {
const unitCount = Math.min(maxSoldiersOnSide, meleeSoldiersSorted[0].count);
const _unitCount = !meleeSoldiersSorted[1] ? 0 : Math.max(0, Math.min(maxSoldiersOnSide - unitCount, meleeSoldiersSorted[1].count));
const bonus = 1 + lordMeleeBonus + sideAttackBonus;
const unitStrength = (unitCount * meleeSoldiersSorted[0].item.meleeAttack * bonus) + (!meleeSoldiersSorted[1] ? 0 : (_unitCount * meleeSoldiersSorted[1].item.meleeAttack * bonus));
if (unitStrength > defenceStrength[side].melee) {
wave[side].units.push({item: meleeSoldiersSorted[0].item, count: unitCount});
meleeSoldiersSorted[0].count -= unitCount;
if (maxSoldierBoxesOnSide > 1 && _unitCount > 0) {
wave[side].units.push({item: meleeSoldiersSorted[1].item, count: _unitCount});
meleeSoldiersSorted[1].count -= _unitCount;
}
}
}
}
if (w === 0) {
for (const side in wave) {
if (wave[side].units.length > 0) {
for (const tool of usedTools[side]) {
wave[side].tools.push(tool);
}
}
}
}
for (const side in wave) {
if (wave[side].units.length > 0) {
army.push(wave);
break;
}
}
}
return army;
}
/**
* @param {Client} client
* @param {Player} thisPlayer
* @param {CastleMapobject} castle
* @param {Lord} lord
* @returns {Promise<void>}
*/
async function attackDungeon(client, thisPlayer, castle, lord) {
const dungeon = await getClosestDungeon(client, castle);
/** @type {Castle} */
const castleData = await client.getCastleInfo(castle);
const availableTroops = castleData.unitInventory?.units;
if (availableTroops == null) throw "No troops!";
const availableSoldiers = availableTroops.filter(t => t.item.isSoldier && t.item.fightType === 0);
/** @type {InventoryItem<Tool>[]} */
const availableDungeonAttackTools = availableTroops.filter(t => !t.item.isSoldier && t.item.fightType === 0 && t.item.canBeUsedToAttackNPC && t.item.name !== "Eventtool" && t.item.amountPerWave == null && (t.item.costC2 === undefined || t.item.costC2 === 0));
if (availableSoldiers.length === 0) throw 'No attacking soldiers available';
const dungeonProtection = getDungeonProtection(dungeon);
const {
attackLowerProtection, usedTools
} = getAttackLowerProtectionDungeon(dungeon, lord, dungeonProtection, availableDungeonAttackTools);
const defenceStrengthTotal = getDungeonDefenceStrength(dungeon, dungeonProtection, attackLowerProtection, usedTools);
const army = getBestArmyForDungeon(thisPlayer, dungeon, defenceStrengthTotal, availableSoldiers, lord, usedTools, availableDungeonAttackTools);
if (army.length === 0) throw 'Not enough attacking soldiers available';
const horse = new Horse(client, castleData, HorseType.Coin);
client.movements.startAttackMovement(castle, dungeon, army, lord, horse);
}