diff --git a/BOTS.md b/BOTS.md index e53b1f1..32a704d 100644 --- a/BOTS.md +++ b/BOTS.md @@ -12,13 +12,15 @@ **Осады** (`FakePlayerSieges`). По расписанию `SiegeSchedule.xml` бот-кланы с ≥3 членами 40+ онлайн регистрируются атакующими на замки бот-кланов (по 2 клана на замок, `FakePlayerSiegeAttackerClans`; на замки игроков — только при `FakePlayerSiegeAttackPlayerCastles = True`). Лидеры кланов подгружаются в мир заранее. В начале осады атакующие собираются за внешними воротами, защитники — у артефакта. Атакующие бьют врагов, ломают ворота, идут к артефакту, лидер клана кастует Печать Правителя (3 минуты, остальные прикрывают) — замок переходит настоящей механикой движка. Крики в крик-чат: начало, ворота, печать, победа/поражение. Форс: `do=siege&arg=Giran`, `do=siegeend`, `do=siegeinfo`. +**Рейды** (`FakePlayerRaids`). Раз в 30–90 минут собирается настоящая пати из 5–9 ботов уровня живого рейд-босса (20–80, гранд-боссы не трогаются), телепорт к боссу, боевой клич, бой до смерти босса или 25 минут (умершие встают в городе и возвращаются к боссу), потом все расходятся по спотам с событием «после рб» в контексте чата. `do=raid` — запустить сейчас, `do=raidinfo` — статус. + **Пати.** Соклановцы одного уровня стартуют на одном споте и собираются в настоящие пати (до 6), держатся лидера, ассистят его цель, хилеры лечат. С игроком: «го пати» → бот **приглашает сам** (обычное окно приглашения), инвайт игроку бот принимает. **Чат — один движок на всех.** Шёпот и общий чат идут через `FakePlayerUnderstanding`: интенты с опечатками, память (кто убивал/помогал/соклановец/враг по кв), позиция на 15 минут (второй раз «я ж сказал: …»), молчание на первое сообщение незнакомца, реакция на капс, зеркалирование стиля, идиолект (свой смех, паразит, сленг, мат у трети), факты контекста (план: фарм/торговля/осада/переезд, событие: ап, ганг, профа, конец осады) и слоты (`{zone} {level} {mob} {town} {crowd} {drop} {clan} {enemyclan} {castle}`). Боты **разговаривают друг с другом**, когда рядом есть живой игрок: здороваются при встрече, задают вопросы (`bot_talk`: «как фарм», «дроп есть?», «какой лвл», «го в пати», «пк ходят?», «клан есть?»…), отвечают через тот же разбор интентов (до 3 реплик в цепочке), подхватывают жалобы и вопросы, поздравляют с лвлом, сплетничают. Зов на помощь при проигрываемом пвп — соклановцы в радиусе 1500 подтягиваются. LLM (Ollama) по умолчанию только для фраз без интента (`FakePlayerLlmPriority = seams`), `llm` — наоборот. **Проверка, что они это делают, а не делают вид**: `do=stats` (сумма по населению), `do=stats&name=Ник` (один бот: убийства, опыт, лут, продано/куплено/адена, лавки, касты, отдыхи, реплики, пати, переезды, профы, осады, последнее действие), `/api/stats` (JSON), строка `FakePlayerTelemetry: last N min -> kills=+… exp=+… loot=+…` в логе раз в `FakePlayerStatsLogMinutes`, `FakePlayerChatLog = True` — каждая реплика в лог с каналом и швом. Оффлайн: `tools/run_harness.sh` (движок чата на реальных XML), `python tools/check_data.py` (пулы, слоты, швы, скиллы профилей). -Настройки — секция «SPP additions» в `game\config\Custom\FakePlayers.ini`: `FakePlayerLlmPriority`, `FakePlayerBotDialogues`, `FakePlayerEconomy`, `FakePlayerProfessionChange`, `FakePlayerSieges`, `FakePlayerSiegeAttackerClans`, `FakePlayerSiegeAttackPlayerCastles`, `FakePlayerStatsLogMinutes`, `FakePlayerChatLog`. +Настройки — секция «SPP additions» в `game\config\Custom\FakePlayers.ini`: `FakePlayerLlmPriority`, `FakePlayerBotDialogues`, `FakePlayerEconomy`, `FakePlayerProfessionChange`, `FakePlayerSieges`, `FakePlayerSiegeAttackerClans`, `FakePlayerSiegeAttackPlayerCastles`, `FakePlayerRaids`, `FakePlayerStatsLogMinutes`, `FakePlayerChatLog`. Ограничения: NPC-режим ботов (`FakePlayerProgression = True`) не поддерживается — шаблонов `fpc_spp_*` в сервере нет; смертность у магов и на спавне в центре стай выше, чем у живых (см. `AUDIT.md`). diff --git a/DEBUG.md b/DEBUG.md index 7ee3b47..c9258e4 100644 --- a/DEBUG.md +++ b/DEBUG.md @@ -19,6 +19,7 @@ | `do=talk&name=Бот&arg=ТвойНик` | два бота телепортируются к тебе и начинают диалог (вопрос → ответ через разбор интентов) | | `do=invite&arg=ТвойНик` | бот прыгает к тебе и **приглашает в пати** (окно приглашения у тебя) | | `do=siege&arg=Giran` | форс-осада: регистрирует бот-кланы атакующими и запускает осаду движка; `do=siegeend&arg=Giran` — закончить; `do=siegeinfo` — расписание, зарегистрированные кланы, сколько ботов в бою | +| `do=raid` | собрать рейд-пати (5–9 ботов уровня босса) и отправить на живого рейд-босса; `do=raidinfo` — что сейчас с рейдом | | `do=online&arg=charId` | подгрузить конкретного сид-персонажа в мир (так осадный менеджер подтягивает лидеров кланов) | `/api/stats` — то же, что `do=stats`, в JSON; `/api/sieges` — как `do=siegeinfo`. diff --git a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/dist/game/config/Custom/FakePlayers.ini b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/dist/game/config/Custom/FakePlayers.ini index 80c4f19..86f6661 100644 --- a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/dist/game/config/Custom/FakePlayers.ini +++ b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/dist/game/config/Custom/FakePlayers.ini @@ -126,6 +126,9 @@ FakePlayerSiegeAttackerClans = 2 # Also besiege castles owned by real players' clans. FakePlayerSiegeAttackPlayerCastles = False +# Raid boss hunts: every 30-90 minutes a real party of 5-9 bots goes for a live raid boss of its level. +FakePlayerRaids = True + # Print bot activity totals (kills, exp, loot, trades, chat...) to the log every N minutes. 0 = off. FakePlayerStatsLogMinutes = 10 diff --git a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/GameServer.java b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/GameServer.java index c0bde99..083c321 100644 --- a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/GameServer.java +++ b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/GameServer.java @@ -130,6 +130,7 @@ import org.l2jmobius.gameserver.managers.FakePlayerDashboard; import org.l2jmobius.gameserver.managers.FakePlayerHeadlessManager; import org.l2jmobius.gameserver.managers.FakePlayerIntentParser; import org.l2jmobius.gameserver.managers.FakePlayerRumorManager; +import org.l2jmobius.gameserver.managers.FakePlayerRaidManager; import org.l2jmobius.gameserver.managers.FakePlayerSiegeManager; import org.l2jmobius.gameserver.managers.FakePlayerTelemetry; import org.l2jmobius.gameserver.managers.FakePlayerProgressionManager; @@ -362,6 +363,7 @@ public class GameServer DayNightSpawnManager.getInstance().trim().notifyChangeMode(); DimensionalRiftManager.getInstance(); RaidBossSpawnManager.getInstance(); + FakePlayerRaidManager.getInstance(); printSection("Siege"); SiegeManager.getInstance().getSieges(); diff --git a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/config/custom/FakePlayersConfig.java b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/config/custom/FakePlayersConfig.java index 5117a8a..be142f1 100644 --- a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/config/custom/FakePlayersConfig.java +++ b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/config/custom/FakePlayersConfig.java @@ -69,6 +69,7 @@ public class FakePlayersConfig public static boolean FAKE_PLAYER_SIEGE_ATTACK_PLAYER_CASTLES; public static int FAKE_PLAYER_STATS_LOG_MINUTES; public static boolean FAKE_PLAYER_CHAT_LOG; + public static boolean FAKE_PLAYER_RAIDS; public static void load() { @@ -110,5 +111,6 @@ public class FakePlayersConfig FAKE_PLAYER_SIEGE_ATTACK_PLAYER_CASTLES = config.getBoolean("FakePlayerSiegeAttackPlayerCastles", false); FAKE_PLAYER_STATS_LOG_MINUTES = config.getInt("FakePlayerStatsLogMinutes", 10); FAKE_PLAYER_CHAT_LOG = config.getBoolean("FakePlayerChatLog", false); + FAKE_PLAYER_RAIDS = config.getBoolean("FakePlayerRaids", true); } } diff --git a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerBotState.java b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerBotState.java index 3c66cbd..f29edc2 100644 --- a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerBotState.java +++ b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerBotState.java @@ -84,6 +84,9 @@ public class FakePlayerBotState public int siegeCastleId = 0; public long siegeNextThink = 0; + // --- raid --- + public int raidBossId = 0; + // --- telemetry --- public final AtomicLong kills = new AtomicLong(); public final AtomicLong expGained = new AtomicLong(); @@ -99,6 +102,7 @@ public class FakePlayerBotState public final AtomicLong skillsCast = new AtomicLong(); public final AtomicLong rests = new AtomicLong(); public final AtomicLong siegesJoined = new AtomicLong(); + public final AtomicLong raidsWon = new AtomicLong(); public final AtomicLong migrations = new AtomicLong(); public final AtomicLong professions = new AtomicLong(); public final AtomicLong partiesJoined = new AtomicLong(); diff --git a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerHeadlessManager.java b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerHeadlessManager.java index 920f9f1..7418a51 100644 --- a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerHeadlessManager.java +++ b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerHeadlessManager.java @@ -522,6 +522,12 @@ public class FakePlayerHeadlessManager return; } + // Raid boss hunt: the party stays on the boss. + if ((state.raidBossId != 0) && FakePlayerRaidManager.getInstance().handle(bot, state, now)) + { + return; + } + // Perception and talk first: a farming bot still notices people and chats. if (FakePlayerSocial.getInstance().tick(bot, state, now)) { @@ -1770,6 +1776,14 @@ public class FakePlayerHeadlessManager { return FakePlayerSiegeManager.getInstance().info(); } + case "raid": + { + return FakePlayerRaidManager.getInstance().start(); + } + case "raidinfo": + { + return FakePlayerRaidManager.getInstance().info(); + } case "online": { try diff --git a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerRaidManager.java b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerRaidManager.java new file mode 100644 index 0000000..a276c0b --- /dev/null +++ b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerRaidManager.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2013 L2jMobius + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.l2jmobius.gameserver.managers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.l2jmobius.commons.threads.ThreadPool; +import org.l2jmobius.commons.util.Rnd; +import org.l2jmobius.gameserver.config.custom.FakePlayersConfig; +import org.l2jmobius.gameserver.entity.World; +import org.l2jmobius.gameserver.entity.WorldObject; +import org.l2jmobius.gameserver.entity.actor.Npc; +import org.l2jmobius.gameserver.entity.actor.Player; +import org.l2jmobius.gameserver.entity.actor.instance.RaidBoss; +import org.l2jmobius.gameserver.entity.groups.Party; + +/** + * Raid boss hunts by headless bots: every 30-90 minutes a real party of 5-9 + * bots close to a live raid boss' level teleports to it, calls out the raid + * and fights until the boss dies or 25 minutes pass, then everybody goes back + * to farming. The party is a real L2 party (shared exp, loot rules), the boss + * is a real raid (drop, raid points), the fight is the ordinary combat brain + * with the boss forced as target. Grand bosses are never touched. + * @author Mobius SPP + */ +public class FakePlayerRaidManager +{ + private static final Logger LOGGER = Logger.getLogger(FakePlayerRaidManager.class.getName()); + + private static final long TICK = 60000; + private static final long RAID_LENGTH = 25 * 60000L; + private static final int PARTY_MIN = 5; + private static final int PARTY_MAX = 9; + + private long _nextRaidAt = System.currentTimeMillis() + Rnd.get(1800000, 3600000); + private long _raidEndsAt = 0; + private int _bossObjectId = 0; + private String _bossName = ""; + private final List _party = new ArrayList<>(); + + protected FakePlayerRaidManager() + { + if (!FakePlayersConfig.FAKE_PLAYERS_ENABLED || !FakePlayersConfig.FAKE_PLAYER_RAIDS || (FakePlayersConfig.FAKE_PLAYER_HEADLESS_COUNT <= 0)) + { + LOGGER.info(getClass().getSimpleName() + ": Disabled."); + return; + } + ThreadPool.scheduleAtFixedRate(this::tick, TICK, TICK); + LOGGER.info(getClass().getSimpleName() + ": Raid parties every 30-90 minutes."); + } + + private void tick() + { + try + { + final long now = System.currentTimeMillis(); + if (_bossObjectId != 0) + { + final WorldObject boss = World.findObject(_bossObjectId); + final boolean gone = !(boss instanceof Npc bossNpc) || bossNpc.isDead(); + if (gone || (now >= _raidEndsAt)) + { + finish(gone); + } + return; + } + if (now >= _nextRaidAt) + { + start(); + } + } + catch (Exception e) + { + LOGGER.warning(getClass().getSimpleName() + ": tick failed: " + e); + } + } + + /** + * Picks a live raid boss and a party of bots of a matching level, sends them in. + * @return result text. + */ + public String start() + { + final long now = System.currentTimeMillis(); + final List bosses = new ArrayList<>(); + for (RaidBoss boss : RaidBossSpawnManager.getInstance().getBosses().values()) + { + if ((boss != null) && !boss.isDead() && boss.isSpawned() && (boss.getLevel() >= 20) && (boss.getLevel() <= 80)) + { + bosses.add(boss); + } + } + if (bosses.isEmpty()) + { + _nextRaidAt = now + 3600000; + return "no live raid boss"; + } + // Try a few bosses until one has enough bots of its level online and free. + for (int attempt = 0; attempt < 6; attempt++) + { + final RaidBoss boss = bosses.get(Rnd.get(bosses.size())); + final List party = new ArrayList<>(); + for (Player bot : FakePlayerHeadlessManager.getInstance().bots()) + { + final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot); + if ((state == null) || bot.isDead() || (state.siegeSide != 0) || (state.tradeUntil > 0) || ((bot.getParty() != null) && (FakePlayerSocial.humanPartyAnchor(bot) != null)) || (Math.abs(bot.getLevel() - boss.getLevel()) > 6) || (bot.getLevel() < (boss.getLevel() - 2))) + { + continue; + } + party.add(bot); + if (party.size() >= PARTY_MAX) + { + break; + } + } + if (party.size() < PARTY_MIN) + { + continue; + } + _bossObjectId = boss.getObjectId(); + _bossName = boss.getName(); + _raidEndsAt = now + RAID_LENGTH; + _party.clear(); + _party.addAll(party); + for (Player bot : party) + { + if (bot.getParty() != null) + { + bot.leaveParty(); // Farming squads dissolve into the raid party. + } + } + final Player leader = party.get(0); + final Party group = new Party(leader, leader.getPartyDistributionType()); + leader.setParty(group); + int index = 0; + for (Player bot : party) + { + final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot); + if (bot != leader) + { + bot.joinParty(group); + } + state.partiesJoined.incrementAndGet(); + state.raidBossId = boss.getObjectId(); + state.action("raid " + boss.getName()); + FakePlayerBotContext.setRaid(state.seedId, boss.getName(), RAID_LENGTH); + FakePlayerHeadlessManager.getInstance().closeStore(bot); + final int delay = 1000 + (index++ * 600); + ThreadPool.schedule(() -> + { + if (!bot.isDead()) + { + FakePlayerHeadlessManager.getInstance().teleport(bot, boss.getX() + Rnd.get(-250, 250), boss.getY() + Rnd.get(-250, 250), boss.getZ()); + } + }, delay); + } + final String cry = FakePlayerUnderstanding.eventLine(FakePlayerBotRef.ofHeadless(leader), "raid_start", null, Map.of("boss", boss.getName())); + if (cry != null) + { + ThreadPool.schedule(() -> FakePlayerHeadlessManager.getInstance().shout(leader, cry, "raid_start"), 6000); + } + LOGGER.info(getClass().getSimpleName() + ": Raid started - " + party.size() + " bots vs " + boss.getName() + " (lvl " + boss.getLevel() + ")."); + return "raid: " + party.size() + " bots vs " + boss.getName() + " lvl" + boss.getLevel(); + } + _nextRaidAt = now + 1800000; + return "no party of " + PARTY_MIN + "+ free bots for any live boss"; + } + + private void finish(boolean bossDead) + { + for (Player bot : _party) + { + final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot); + if (state == null) + { + continue; + } + state.raidBossId = 0; + if (bossDead) + { + state.raidsWon.incrementAndGet(); + } + FakePlayerBotContext.setPlan(state.seedId, "farm", 0); + FakePlayerBotContext.setEvent(state.seedId, "raid_finished"); + state.action(bossDead ? ("raid won " + _bossName) : "raid over"); + if (bot.getParty() != null) + { + bot.leaveParty(); + } + ThreadPool.schedule(() -> FakePlayerHeadlessManager.getInstance().returnToSpot(bot), 15000 + Rnd.get(45000)); + } + LOGGER.info(getClass().getSimpleName() + ": Raid finished (" + (bossDead ? "boss dead" : "timeout") + ") - " + _bossName + "."); + _party.clear(); + _bossObjectId = 0; + _bossName = ""; + _nextRaidAt = System.currentTimeMillis() + Rnd.get(1800000, 5400000); + } + + /** + * @param bot the bot. + * @param state its state. + * @return true when the bot is on a raid and the raid logic took the tick. + */ + public boolean handle(Player bot, FakePlayerBotState state, long now) + { + if (state.raidBossId == 0) + { + return false; + } + final WorldObject boss = World.findObject(state.raidBossId); + if (!(boss instanceof Npc bossNpc) || bossNpc.isDead() || (_bossObjectId != state.raidBossId)) + { + state.raidBossId = 0; + return false; + } + if (bot.calculateDistance2D(bossNpc) > 3000) + { + // Back from the village after dying: rejoin. + FakePlayerHeadlessManager.getInstance().teleport(bot, bossNpc.getX() + Rnd.get(-250, 250), bossNpc.getY() + Rnd.get(-250, 250), bossNpc.getZ()); + return true; + } + // Survival and healing first, then the boss (the brain handles a targeted creature in combat). + if ((bot.getTarget() != bossNpc) || (!bot.isAttackingNow() && !bot.isCastingNow())) + { + bot.setTarget(bossNpc); + bot.getAI().setIntentionAttack(bossNpc); + } + return FakePlayerBrain.act(bot, state, false) || true; + } + + public String info() + { + if (_bossObjectId != 0) + { + return "raid in progress: " + _party.size() + " bots vs " + _bossName + ", ends in " + Math.max(0, (_raidEndsAt - System.currentTimeMillis()) / 60000) + " min"; + } + return "next raid in " + Math.max(0, (_nextRaidAt - System.currentTimeMillis()) / 60000) + " min"; + } + + public static FakePlayerRaidManager getInstance() + { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder + { + protected static final FakePlayerRaidManager INSTANCE = new FakePlayerRaidManager(); + } +} diff --git a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerTelemetry.java b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerTelemetry.java index e5a57fb..bc3274e 100644 --- a/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerTelemetry.java +++ b/src_mobius/mobius/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/managers/FakePlayerTelemetry.java @@ -72,6 +72,7 @@ public class FakePlayerTelemetry long casts = 0; long rests = 0; long sieges = 0; + long raids = 0; long migrations = 0; long professions = 0; long parties = 0; @@ -103,6 +104,7 @@ public class FakePlayerTelemetry casts += state.skillsCast.get(); rests += state.rests.get(); sieges += state.siegesJoined.get(); + raids += state.raidsWon.get(); migrations += state.migrations.get(); professions += state.professions.get(); parties += state.partiesJoined.get(); @@ -158,6 +160,7 @@ public class FakePlayerTelemetry totals.put("migrations", migrations); totals.put("professions", professions); totals.put("sieges", sieges); + totals.put("raidsWon", raids); totals.put("uptimeMin", (System.currentTimeMillis() - _startedAt) / 60000); return totals; } @@ -235,7 +238,7 @@ public class FakePlayerTelemetry sb.append(" kills=").append(state.kills.get()).append(" exp=").append(state.expGained.get()).append(" pvp=").append(state.pvpKills.get()).append(" deaths=").append(state.deaths.get()).append('\n'); sb.append(" loot=").append(state.lootPicked.get()).append(" sold=").append(state.itemsSold.get()).append(" earned=").append(state.adenaEarned.get()).append(" spent=").append(state.adenaSpent.get()).append(" adena=").append(bot.getAdena()).append(" stores=").append(state.storesOpened.get()).append('\n'); sb.append(" casts=").append(state.skillsCast.get()).append(" rests=").append(state.rests.get()).append(" lines=").append(state.linesSaid.get()).append(" replies=").append(state.repliesGiven.get()).append(" parties=").append(state.partiesJoined.get()).append('\n'); - sb.append(" migrations=").append(state.migrations.get()).append(" professions=").append(state.professions.get()).append(" sieges=").append(state.siegesJoined.get()).append(" siegeSide=").append(state.siegeSide).append('\n'); + sb.append(" migrations=").append(state.migrations.get()).append(" professions=").append(state.professions.get()).append(" sieges=").append(state.siegesJoined.get()).append(" siegeSide=").append(state.siegeSide).append(" raidsWon=").append(state.raidsWon.get()).append(" raidBoss=").append(state.raidBossId).append('\n'); sb.append(" shots=").append(bot.getAutoSoulShot()).append(" inventory=").append(bot.getInventory().getSize()).append(" hp=").append((int) bot.getCurrentHp()).append('/').append(bot.getMaxHp()); return sb.toString(); }