Implement abyss defense objective (#2422)

This commit is contained in:
longfruit 2023-11-02 19:00:05 -07:00 committed by GitHub
parent 205b79dc02
commit 24874e7fba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 113 additions and 30 deletions

View File

@ -68,6 +68,10 @@ public final class DungeonManager {
} }
if (isFinishedSuccessfully()) { 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(); finishDungeon();
} }
} }

View File

@ -1,5 +1,6 @@
package emu.grasscutter.game.dungeons; 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.BaseDungeonResult.DungeonEndReason;
import emu.grasscutter.game.dungeons.dungeon_results.TowerResult; import emu.grasscutter.game.dungeons.dungeon_results.TowerResult;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
@ -25,16 +26,22 @@ public class TowerDungeonSettleListener implements DungeonSettleListener {
var towerManager = scene.getPlayers().get(0).getTowerManager(); var towerManager = scene.getPlayers().get(0).getTowerManager();
var stars = towerManager.getCurLevelStars(); var stars = towerManager.getCurLevelStars();
towerManager.notifyCurLevelRecordChangeWhenDone(stars); if (endReason == DungeonEndReason.COMPLETED) {
scene.broadcastPacket( // Update star record only when challenge completes successfully.
new PacketTowerFloorRecordChangeNotify( towerManager.notifyCurLevelRecordChangeWhenDone(stars);
towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor())); scene.broadcastPacket(
new PacketTowerFloorRecordChangeNotify(
towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor()));
}
var challenge = scene.getChallenge(); var challenge = scene.getChallenge();
var finishedTime = challenge == null ? challenge.getFinishedTime() : 0;
var dungeonStats = var dungeonStats =
new DungeonEndStats( new DungeonEndStats(
scene.getKilledMonsterCount(), challenge.getFinishedTime(), 0, endReason); scene.getKilledMonsterCount(), finishedTime, 0, endReason);
var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars); var result = endReason == DungeonEndReason.COMPLETED ?
new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars) :
new BaseDungeonResult(dungeonData, dungeonStats);
scene.broadcastPacket(new PacketDungeonSettleNotify(result)); scene.broadcastPacket(new PacketDungeonSettleNotify(result));
} }

View File

@ -4,6 +4,7 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.EventType;
@ -22,6 +23,7 @@ public class WorldChallenge {
private final int challengeIndex; private final int challengeIndex;
private final List<Integer> paramList; private final List<Integer> paramList;
private int timeLimit; private int timeLimit;
private GameEntity guardEntity;
private final List<ChallengeTrigger> challengeTriggers; private final List<ChallengeTrigger> challengeTriggers;
private final int goal; private final int goal;
private final AtomicInteger score; private final AtomicInteger score;
@ -58,6 +60,7 @@ public class WorldChallenge {
this.challengeTriggers = challengeTriggers; this.challengeTriggers = challengeTriggers;
this.goal = goal; this.goal = goal;
this.score = new AtomicInteger(0); this.score = new AtomicInteger(0);
this.guardEntity = null;
} }
public boolean inProgress() { public boolean inProgress() {
@ -143,6 +146,10 @@ public class WorldChallenge {
this.progress = false; this.progress = false;
this.success = success; this.success = success;
this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt)); 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)); getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
} }
@ -150,6 +157,18 @@ public class WorldChallenge {
return score.incrementAndGet(); 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) { public void onMonsterDeath(EntityMonster monster) {
if (!inProgress()) { if (!inProgress()) {
return; return;

View File

@ -33,7 +33,7 @@ public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHand
realGroup, realGroup,
challengeId, // Id challengeId, // Id
challengeIndex, // Index challengeIndex, // Index
List.of(monstersToKill, 0), List.of(monstersToKill, gadgetCFGId),
0, // Limit 0, // Limit
monstersToKill, // Goal monstersToKill, // Goal
List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId))); List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId)));

View File

@ -14,7 +14,9 @@ public class GuardTrigger extends ChallengeTrigger {
} }
public void onBegin(WorldChallenge challenge) { 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 @Override
@ -22,9 +24,7 @@ public class GuardTrigger extends ChallengeTrigger {
if (gadget.getConfigId() != entityToProtectCFGId) { if (gadget.getConfigId() != entityToProtectCFGId) {
return; return;
} }
var curHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId()); var percent = challenge.getGuardEntityHpPercent();
var maxHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId());
int percent = (int) (curHp / maxHp);
if (percent != lastSendPercent) { if (percent != lastSendPercent) {
challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, percent)); challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, percent));

View File

@ -18,12 +18,6 @@ public class InTimeTrigger extends ChallengeTrigger {
@Override @Override
public void onCheckTimeout(WorldChallenge challenge) { 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(); var current = challenge.getScene().getSceneTimeSeconds();
if (current - challenge.getStartedAt() > challenge.getTimeLimit()) { if (current - challenge.getStartedAt() > challenge.getTimeLimit()) {
challenge.fail(); challenge.fail();

View File

@ -32,11 +32,12 @@ public class TowerResult extends BaseDungeonResult {
@Override @Override
protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) { protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) {
var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE; var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE;
if (challenge.isSuccess() && canJump) { if (challenge.isSuccess()) {
continueStatus = if (hasNextLevel) {
hasNextLevel continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE;
? ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE } else if (canJump) {
: ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE; continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE;
}
} }
var towerLevelEndNotify = var towerLevelEndNotify =

View File

@ -70,6 +70,11 @@ public abstract class EntityBaseGadget extends GameEntity {
.setSourceEntityId(getId()) .setSourceEntityId(getId())
.setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) .setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP))
.setEventSource(getConfigId())); .setEventSource(getConfigId()));
var challenge = getScene().getChallenge();
if (challenge != null && this instanceof EntityGadget gadget) {
challenge.onGadgetDamage(gadget);
}
} }
protected void fillFightProps(ConfigEntityGadget configGadget) { protected void fillFightProps(ConfigEntityGadget configGadget) {

View File

@ -4,7 +4,9 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.config.ConfigEntityGadget; import emu.grasscutter.data.binout.config.ConfigEntityGadget;
import emu.grasscutter.data.binout.config.fields.ConfigAbilityData; 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.GadgetData;
import emu.grasscutter.data.excels.monster.MonsterCurveData;
import emu.grasscutter.game.entity.gadget.*; import emu.grasscutter.game.entity.gadget.*;
import emu.grasscutter.game.entity.gadget.platform.*; import emu.grasscutter.game.entity.gadget.platform.*;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
@ -104,6 +106,20 @@ public class EntityGadget extends EntityBaseGadget {
this.bornRot = this.getRotation().clone(); this.bornRot = this.getRotation().clone();
this.fillFightProps(configGadget); 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)) { if (GameData.getGadgetMappingMap().containsKey(gadgetId)) {
var controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController(); var controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController();
this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName)); this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName));

View File

@ -100,7 +100,7 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
} }
private void setOpenState(int openState, int value, boolean sendNotify) { 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) { if (value != previousValue) {
this.player.getOpenStates().put(openState, value); this.player.getOpenStates().put(openState, value);

View File

@ -40,7 +40,7 @@ public class TowerManager extends BasePlayerManager {
public void onTick() { public void onTick() {
var challenge = player.getScene().getChallenge(); 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. // Check star conditions and notify client if any failed.
int stars = getCurLevelStars(); int stars = getCurLevelStars();
@ -153,8 +153,11 @@ public class TowerManager extends BasePlayerManager {
break; break;
} }
} else if (cond == TowerLevelData.TowerCondType.TOWER_COND_LEFT_HP_GREATER_THAN) { } else if (cond == TowerLevelData.TowerCondType.TOWER_COND_LEFT_HP_GREATER_THAN) {
// TODO: Check monolith health var params = levelData.getHpCond(star);
break; var hpPercent = challenge.getGuardEntityHpPercent();
if (hpPercent >= params.getMinimumHpPercentage()) {
break;
}
} else { } else {
Grasscutter.getLogger() Grasscutter.getLogger()
.error( .error(

View File

@ -600,7 +600,7 @@ public class Scene {
// Should be OK to check only player 0, // Should be OK to check only player 0,
// as no other players could enter Tower // as no other players could enter Tower
var towerManager = getPlayers().get(0).getTowerManager(); var towerManager = getPlayers().get(0).getTowerManager();
if (towerManager != null) { if (towerManager != null && towerManager.isInProgress()) {
towerManager.onTick(); towerManager.onTick();
} }

View File

@ -46,6 +46,7 @@ public class SceneScriptManager {
/** current triggers controlled by RefreshGroup */ /** current triggers controlled by RefreshGroup */
private final Map<Integer, Set<SceneTrigger>> currentTriggers; private final Map<Integer, Set<SceneTrigger>> currentTriggers;
private final Set<SceneTrigger> ongoingTriggers;
private final Map<String, Set<SceneTrigger>> triggersByGroupScene; private final Map<String, Set<SceneTrigger>> triggersByGroupScene;
private final Map<Integer, Set<Pair<String, Integer>>> activeGroupTimers; private final Map<Integer, Set<Pair<String, Integer>>> activeGroupTimers;
private final Map<String, AtomicInteger> triggerInvocations; private final Map<String, AtomicInteger> triggerInvocations;
@ -76,6 +77,7 @@ public class SceneScriptManager {
public SceneScriptManager(Scene scene) { public SceneScriptManager(Scene scene) {
this.scene = scene; this.scene = scene;
this.currentTriggers = new ConcurrentHashMap<>(); this.currentTriggers = new ConcurrentHashMap<>();
this.ongoingTriggers = ConcurrentHashMap.newKeySet();
this.triggersByGroupScene = new ConcurrentHashMap<>(); this.triggersByGroupScene = new ConcurrentHashMap<>();
this.activeGroupTimers = new ConcurrentHashMap<>(); this.activeGroupTimers = new ConcurrentHashMap<>();
this.triggerInvocations = new ConcurrentHashMap<>(); this.triggerInvocations = new ConcurrentHashMap<>();
@ -264,6 +266,15 @@ public class SceneScriptManager {
this.addGroupSuite(groupInstance, suiteData, entitiesAdded); 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<SceneTrigger>(triggerSet);
toSave.retainAll(ongoingTriggers);
toSave.forEach(t -> t.setPreserved(true));
}
// Refesh variables here // Refesh variables here
group.variables.forEach( group.variables.forEach(
variable -> { variable -> {
@ -925,6 +936,7 @@ public class SceneScriptManager {
private void callTrigger(SceneTrigger trigger, ScriptArgs params) { private void callTrigger(SceneTrigger trigger, ScriptArgs params) {
// the SetGroupVariableValueByGroup in tower need the param to record the first stage time // 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 ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params);
var invocationsCounter = triggerInvocations.get(trigger.getName()); var invocationsCounter = triggerInvocations.get(trigger.getName());
var invocations = invocationsCounter.incrementAndGet(); var invocations = invocationsCounter.incrementAndGet();
@ -956,11 +968,16 @@ public class SceneScriptManager {
} }
// always deregister on error, otherwise only if the count is reached // 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 || ret.isint() && ret.checkint() != 0
|| trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) { || trigger.getTrigger_count() > 0 && invocations >= trigger.getTrigger_count()) {
deregisterTrigger(trigger); deregisterTrigger(trigger);
} }
ongoingTriggers.remove(trigger);
} }
private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) { private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) {
@ -1104,6 +1121,18 @@ public class SceneScriptManager {
return meta.sceneBlockIndex; 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) { public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) {
var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet()); var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet());
var toRemove = var toRemove =

View File

@ -201,7 +201,7 @@ public class ScriptLib {
} }
var towerManager = scene.getPlayers().get(0).getTowerManager(); var towerManager = scene.getPlayers().get(0).getTowerManager();
if (towerManager.isInProgress()) { if (towerManager.isInProgress() && towerManager.getCurrentTimeLimit() > 0) {
// Tower scripts call ActiveChallenge twice in mirror stages. // Tower scripts call ActiveChallenge twice in mirror stages.
// The second call provides the time _taken_ in the first stage, // The second call provides the time _taken_ in the first stage,
// not the actual time limit for the challenge. // not the actual time limit for the challenge.

View File

@ -17,6 +17,7 @@ public final class SceneTrigger {
private String tag; private String tag;
public transient SceneGroup currentGroup; public transient SceneGroup currentGroup;
private boolean preserved;
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {

View File

@ -62,7 +62,11 @@ public final class ScriptMonsterTideService {
public SceneMonster getNextMonster() { public SceneMonster getNextMonster() {
var nextId = this.monsterConfigOrders.poll(); 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); return currentGroup.monsters.get(nextId);
} }
// TODO some monster config_id do not exist in groups, so temporarily set it to the first // TODO some monster config_id do not exist in groups, so temporarily set it to the first