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()) {
// Set ended now because calling EVENT_DUNGEON_SETTLE
// during finishDungeon() may cause reentrance into
// this function, leading to double settles.
ended = true;
finishDungeon();
}
}

View File

@ -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));
}

View File

@ -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<Integer> paramList;
private int timeLimit;
private GameEntity guardEntity;
private final List<ChallengeTrigger> 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;

View File

@ -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)));

View File

@ -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));

View File

@ -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();

View File

@ -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 =

View File

@ -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) {

View File

@ -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));

View File

@ -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);

View File

@ -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(

View File

@ -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();
}

View File

@ -46,6 +46,7 @@ public class SceneScriptManager {
/** current triggers controlled by RefreshGroup */
private final Map<Integer, Set<SceneTrigger>> currentTriggers;
private final Set<SceneTrigger> ongoingTriggers;
private final Map<String, Set<SceneTrigger>> triggersByGroupScene;
private final Map<Integer, Set<Pair<String, Integer>>> activeGroupTimers;
private final Map<String, AtomicInteger> 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<SceneTrigger>(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 =

View File

@ -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.

View File

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

View File

@ -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