From 24874e7fbab2031c17e61e8625bb86dda8376036 Mon Sep 17 00:00:00 2001 From: longfruit <147137915+longfruit@users.noreply.github.com> Date: Thu, 2 Nov 2023 19:00:05 -0700 Subject: [PATCH] Implement abyss defense objective (#2422) --- .../game/dungeons/DungeonManager.java | 4 +++ .../dungeons/TowerDungeonSettleListener.java | 19 ++++++++---- .../dungeons/challenge/WorldChallenge.java | 19 ++++++++++++ .../KillAndGuardChallengeFactoryHandler.java | 2 +- .../challenge/trigger/GuardTrigger.java | 8 ++--- .../challenge/trigger/InTimeTrigger.java | 6 ---- .../dungeons/dungeon_results/TowerResult.java | 11 ++++--- .../game/entity/EntityBaseGadget.java | 5 +++ .../grasscutter/game/entity/EntityGadget.java | 16 ++++++++++ .../game/player/PlayerProgressManager.java | 2 +- .../grasscutter/game/tower/TowerManager.java | 9 ++++-- .../emu/grasscutter/game/world/Scene.java | 2 +- .../scripts/SceneScriptManager.java | 31 ++++++++++++++++++- .../emu/grasscutter/scripts/ScriptLib.java | 2 +- .../scripts/data/SceneTrigger.java | 1 + .../service/ScriptMonsterTideService.java | 6 +++- 16 files changed, 113 insertions(+), 30 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java index 140dfe5ba..4c1698f81 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java @@ -68,6 +68,10 @@ public final class DungeonManager { } if (isFinishedSuccessfully()) { + // Set ended now because calling EVENT_DUNGEON_SETTLE + // during finishDungeon() may cause reentrance into + // this function, leading to double settles. + ended = true; finishDungeon(); } } diff --git a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java index a2f826f48..6e32363b6 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java +++ b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java @@ -1,5 +1,6 @@ package emu.grasscutter.game.dungeons; +import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult; import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult.DungeonEndReason; import emu.grasscutter.game.dungeons.dungeon_results.TowerResult; import emu.grasscutter.server.packet.send.*; @@ -25,16 +26,22 @@ public class TowerDungeonSettleListener implements DungeonSettleListener { var towerManager = scene.getPlayers().get(0).getTowerManager(); var stars = towerManager.getCurLevelStars(); - towerManager.notifyCurLevelRecordChangeWhenDone(stars); - scene.broadcastPacket( - new PacketTowerFloorRecordChangeNotify( - towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor())); + if (endReason == DungeonEndReason.COMPLETED) { + // Update star record only when challenge completes successfully. + towerManager.notifyCurLevelRecordChangeWhenDone(stars); + scene.broadcastPacket( + new PacketTowerFloorRecordChangeNotify( + towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor())); + } var challenge = scene.getChallenge(); + var finishedTime = challenge == null ? challenge.getFinishedTime() : 0; var dungeonStats = new DungeonEndStats( - scene.getKilledMonsterCount(), challenge.getFinishedTime(), 0, endReason); - var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars); + scene.getKilledMonsterCount(), finishedTime, 0, endReason); + var result = endReason == DungeonEndReason.COMPLETED ? + new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars) : + new BaseDungeonResult(dungeonData, dungeonStats); scene.broadcastPacket(new PacketDungeonSettleNotify(result)); } diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java index dcebee3a5..b20c2cb99 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java @@ -4,6 +4,7 @@ import emu.grasscutter.Grasscutter; import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.constants.EventType; @@ -22,6 +23,7 @@ public class WorldChallenge { private final int challengeIndex; private final List paramList; private int timeLimit; + private GameEntity guardEntity; private final List challengeTriggers; private final int goal; private final AtomicInteger score; @@ -58,6 +60,7 @@ public class WorldChallenge { this.challengeTriggers = challengeTriggers; this.goal = goal; this.score = new AtomicInteger(0); + this.guardEntity = null; } public boolean inProgress() { @@ -143,6 +146,10 @@ public class WorldChallenge { this.progress = false; this.success = success; this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt)); + + // Despawn all leftover mobs in this challenge's SceneGroup + getScene().getScriptManager().removeMonstersInGroup(group); + getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this)); } @@ -150,6 +157,18 @@ public class WorldChallenge { return score.incrementAndGet(); } + public int getGuardEntityHpPercent() { + if (guardEntity == null) { + Grasscutter.getLogger().warn("getGuardEntityHpPercent: Could not find guardEntity for this challenge = {}", this); + return 100; + } + + var curHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId()); + var maxHp = guardEntity.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId()); + int percent = (int) (curHp * 100 / maxHp); + return percent; + } + public void onMonsterDeath(EntityMonster monster) { if (!inProgress()) { return; diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java index b6de03f56..90d690de6 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java @@ -33,7 +33,7 @@ public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHand realGroup, challengeId, // Id challengeIndex, // Index - List.of(monstersToKill, 0), + List.of(monstersToKill, gadgetCFGId), 0, // Limit monstersToKill, // Goal List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId))); diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/GuardTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/GuardTrigger.java index 8cc43cb51..a515f3501 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/GuardTrigger.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/GuardTrigger.java @@ -14,7 +14,9 @@ public class GuardTrigger extends ChallengeTrigger { } public void onBegin(WorldChallenge challenge) { - challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, 100)); + challenge.setGuardEntity(challenge.getScene().getEntityByConfigId(entityToProtectCFGId, challenge.getGroup().id)); + lastSendPercent = challenge.getGuardEntityHpPercent(); + challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, lastSendPercent)); } @Override @@ -22,9 +24,7 @@ public class GuardTrigger extends ChallengeTrigger { if (gadget.getConfigId() != entityToProtectCFGId) { return; } - var curHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId()); - var maxHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId()); - int percent = (int) (curHp / maxHp); + var percent = challenge.getGuardEntityHpPercent(); if (percent != lastSendPercent) { challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, percent)); diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/InTimeTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/InTimeTrigger.java index 7842a5d80..9eca97ed4 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/InTimeTrigger.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/InTimeTrigger.java @@ -18,12 +18,6 @@ public class InTimeTrigger extends ChallengeTrigger { @Override public void onCheckTimeout(WorldChallenge challenge) { - // In Tower challenges, time can run out without - // causing the challenge to fail. (Player just - // gets 0 stars when they ultimately finish.) - var dungeonManager = challenge.getScene().getDungeonManager(); - if (dungeonManager != null && dungeonManager.isTowerDungeon()) return; - var current = challenge.getScene().getSceneTimeSeconds(); if (current - challenge.getStartedAt() > challenge.getTimeLimit()) { challenge.fail(); diff --git a/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TowerResult.java b/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TowerResult.java index 2b9956561..b42e041ec 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TowerResult.java +++ b/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TowerResult.java @@ -32,11 +32,12 @@ public class TowerResult extends BaseDungeonResult { @Override protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) { var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE; - if (challenge.isSuccess() && canJump) { - continueStatus = - hasNextLevel - ? ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE - : ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE; + if (challenge.isSuccess()) { + if (hasNextLevel) { + continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE; + } else if (canJump) { + continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE; + } } var towerLevelEndNotify = diff --git a/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java b/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java index 0afac929e..2163300ca 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java @@ -70,6 +70,11 @@ public abstract class EntityBaseGadget extends GameEntity { .setSourceEntityId(getId()) .setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) .setEventSource(getConfigId())); + + var challenge = getScene().getChallenge(); + if (challenge != null && this instanceof EntityGadget gadget) { + challenge.onGadgetDamage(gadget); + } } protected void fillFightProps(ConfigEntityGadget configGadget) { diff --git a/src/main/java/emu/grasscutter/game/entity/EntityGadget.java b/src/main/java/emu/grasscutter/game/entity/EntityGadget.java index 7e9017df8..6597f48c5 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityGadget.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityGadget.java @@ -4,7 +4,9 @@ import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.binout.config.ConfigEntityGadget; import emu.grasscutter.data.binout.config.fields.ConfigAbilityData; +import emu.grasscutter.data.common.PropGrowCurve; import emu.grasscutter.data.excels.GadgetData; +import emu.grasscutter.data.excels.monster.MonsterCurveData; import emu.grasscutter.game.entity.gadget.*; import emu.grasscutter.game.entity.gadget.platform.*; import emu.grasscutter.game.player.Player; @@ -104,6 +106,20 @@ public class EntityGadget extends EntityBaseGadget { this.bornRot = this.getRotation().clone(); this.fillFightProps(configGadget); + // Check if this gadget is the abyss defense objective's gadget. + // That doesn't have a level and defaults to having 5000 hp, so it dies in like 2 hits on 11-1. + // I'll forgive player skill issues and scale its hp up here. + // TODO: find out how its fight props are actually scaled + if (gadgetData.getJsonName().equals("SceneObj_Gear_Operator_Mamolu_Entity")) { + MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(11); + if (curve != null) { + FightProperty[] hpProps = {FightProperty.FIGHT_PROP_MAX_HP, FightProperty.FIGHT_PROP_BASE_HP, FightProperty.FIGHT_PROP_CUR_HP}; + for (var prop : hpProps) { + setFightProperty(prop, this.getFightProperty(prop) * curve.getMultByProp("GROW_CURVE_HP_ENVIRONMENT")); + } + } + } + if (GameData.getGadgetMappingMap().containsKey(gadgetId)) { var controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController(); this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName)); diff --git a/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java b/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java index 1352594a6..7dfa50d48 100644 --- a/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java +++ b/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java @@ -100,7 +100,7 @@ public final class PlayerProgressManager extends BasePlayerDataManager { } private void setOpenState(int openState, int value, boolean sendNotify) { - int previousValue = this.player.getOpenStates().getOrDefault(openState, 0); + int previousValue = this.player.getOpenStates().getOrDefault(openState, -1 /* non-existent */); if (value != previousValue) { this.player.getOpenStates().put(openState, value); diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index c0e4b70c4..2574aee0f 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -40,7 +40,7 @@ public class TowerManager extends BasePlayerManager { public void onTick() { var challenge = player.getScene().getChallenge(); - if (challenge == null || !challenge.inProgress()) return; + if (!inProgress || challenge == null || !challenge.inProgress()) return; // Check star conditions and notify client if any failed. int stars = getCurLevelStars(); @@ -153,8 +153,11 @@ public class TowerManager extends BasePlayerManager { break; } } else if (cond == TowerLevelData.TowerCondType.TOWER_COND_LEFT_HP_GREATER_THAN) { - // TODO: Check monolith health - break; + var params = levelData.getHpCond(star); + var hpPercent = challenge.getGuardEntityHpPercent(); + if (hpPercent >= params.getMinimumHpPercentage()) { + break; + } } else { Grasscutter.getLogger() .error( diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 9a6252c15..d1511114e 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -600,7 +600,7 @@ public class Scene { // Should be OK to check only player 0, // as no other players could enter Tower var towerManager = getPlayers().get(0).getTowerManager(); - if (towerManager != null) { + if (towerManager != null && towerManager.isInProgress()) { towerManager.onTick(); } diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index 79cf61239..ec033dec7 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -46,6 +46,7 @@ public class SceneScriptManager { /** current triggers controlled by RefreshGroup */ private final Map> currentTriggers; + private final Set ongoingTriggers; private final Map> triggersByGroupScene; private final Map>> activeGroupTimers; private final Map triggerInvocations; @@ -76,6 +77,7 @@ public class SceneScriptManager { public SceneScriptManager(Scene scene) { this.scene = scene; this.currentTriggers = new ConcurrentHashMap<>(); + this.ongoingTriggers = ConcurrentHashMap.newKeySet(); this.triggersByGroupScene = new ConcurrentHashMap<>(); this.activeGroupTimers = new ConcurrentHashMap<>(); this.triggerInvocations = new ConcurrentHashMap<>(); @@ -264,6 +266,15 @@ public class SceneScriptManager { this.addGroupSuite(groupInstance, suiteData, entitiesAdded); + // refreshGroup may be called by a trigger. + // If that trigger has been refreshed, ensure it does not get + // deregistered anyway when the trigger completes its invocation. + for (var triggerSet : currentTriggers.values()) { + var toSave = new HashSet(triggerSet); + toSave.retainAll(ongoingTriggers); + toSave.forEach(t -> t.setPreserved(true)); + } + // Refesh variables here group.variables.forEach( variable -> { @@ -925,6 +936,7 @@ public class SceneScriptManager { private void callTrigger(SceneTrigger trigger, ScriptArgs params) { // the SetGroupVariableValueByGroup in tower need the param to record the first stage time + ongoingTriggers.add(trigger); var ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params); var invocationsCounter = triggerInvocations.get(trigger.getName()); var invocations = invocationsCounter.incrementAndGet(); @@ -956,11 +968,16 @@ public class SceneScriptManager { } // always deregister on error, otherwise only if the count is reached - if (ret.isboolean() && !ret.checkboolean() + // or the trigger should be preserved after a RefreshGroup call + if (trigger.isPreserved()) { + trigger.setPreserved(false); + } + else if (ret.isboolean() && !ret.checkboolean() || ret.isint() && ret.checkint() != 0 || trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) { deregisterTrigger(trigger); } + ongoingTriggers.remove(trigger); } private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) { @@ -1104,6 +1121,18 @@ public class SceneScriptManager { return meta.sceneBlockIndex; } + public void removeMonstersInGroup(SceneGroup group) { + var configSet = group.monsters.values().stream().map(m -> m.config_id).collect(Collectors.toSet()); + var toRemove = + getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityMonster) + .filter(e -> e.getGroupId() == group.id) + .filter(e -> configSet.contains(e.getConfigId())) + .toList(); + + getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS); + } + public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) { var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet()); var toRemove = diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index 8ebff5a65..e5ce947df 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -201,7 +201,7 @@ public class ScriptLib { } var towerManager = scene.getPlayers().get(0).getTowerManager(); - if (towerManager.isInProgress()) { + if (towerManager.isInProgress() && towerManager.getCurrentTimeLimit() > 0) { // Tower scripts call ActiveChallenge twice in mirror stages. // The second call provides the time _taken_ in the first stage, // not the actual time limit for the challenge. diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java b/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java index f8720dec9..440601711 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java @@ -17,6 +17,7 @@ public final class SceneTrigger { private String tag; public transient SceneGroup currentGroup; + private boolean preserved; @Override public boolean equals(Object obj) { diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java index 1bb5b0594..57c4b6737 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java @@ -62,7 +62,11 @@ public final class ScriptMonsterTideService { public SceneMonster getNextMonster() { var nextId = this.monsterConfigOrders.poll(); - if (currentGroup.monsters.containsKey(nextId)) { + if (nextId == null) { + // AutoMonsterTide has been called with fewer monster config IDs than the total tide count. + // Get last config ID from the list, then. + return currentGroup.monsters.get(monsterConfigIds.get(monsterConfigIds.size() - 1)); + } else if (currentGroup.monsters.containsKey(nextId)) { return currentGroup.monsters.get(nextId); } // TODO some monster config_id do not exist in groups, so temporarily set it to the first