Skip to content

Commit

Permalink
Implement abyss defense objective (#2422)
Browse files Browse the repository at this point in the history
  • Loading branch information
longfruit authored Nov 3, 2023
1 parent 205b79d commit 24874e7
Show file tree
Hide file tree
Showing 16 changed files with 113 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public void triggerEvent(DungeonPassConditionType conditionType, int... params)
}

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();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.*;
Expand All @@ -25,16 +26,22 @@ public void onDungeonSettle(DungeonManager dungeonManager, DungeonEndReason endR
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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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;
Expand All @@ -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;
Expand Down Expand Up @@ -58,6 +60,7 @@ public WorldChallenge(
this.challengeTriggers = challengeTriggers;
this.goal = goal;
this.score = new AtomicInteger(0);
this.guardEntity = null;
}

public boolean inProgress() {
Expand Down Expand Up @@ -143,13 +146,29 @@ private void finish(boolean success) {
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));
}

public int increaseScore() {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public WorldChallenge build(
realGroup,
challengeId, // Id
challengeIndex, // Index
List.of(monstersToKill, 0),
List.of(monstersToKill, gadgetCFGId),
0, // Limit
monstersToKill, // Goal
List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ public GuardTrigger(int entityToProtectCFGId) {
}

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
public void onGadgetDamage(WorldChallenge challenge, EntityGadget gadget) {
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ public void onBegin(WorldChallenge challenge) {

@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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ public TowerResult(
@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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public void runLuaCallbacks(EntityDamageEvent event) {
.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) {
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/emu/grasscutter/game/entity/EntityGadget.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
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;
Expand Down Expand Up @@ -104,6 +106,20 @@ public EntityGadget(
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public int getOpenState(int openState) {
}

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);
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/emu/grasscutter/game/tower/TowerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public int getCurrentLevel() {

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();
Expand Down Expand Up @@ -153,8 +153,11 @@ public int getCurLevelStars() {
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(
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/emu/grasscutter/game/world/Scene.java
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ public void onTick() {
// 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();
}

Expand Down
31 changes: 30 additions & 1 deletion src/main/java/emu/grasscutter/scripts/SceneScriptManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<>();
Expand Down Expand Up @@ -264,6 +266,15 @@ public int refreshGroup(

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 -> {
Expand Down Expand Up @@ -925,6 +936,7 @@ private boolean evaluateTriggerCondition(SceneTrigger trigger, ScriptArgs params

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();
Expand Down Expand Up @@ -956,11 +968,16 @@ private void callTrigger(SceneTrigger trigger, ScriptArgs params) {
}

// 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) {
Expand Down Expand Up @@ -1104,6 +1121,18 @@ public RTree<SceneBlock, Geometry> getBlocksIndex() {
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 =
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/emu/grasscutter/scripts/ScriptLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public synchronized int ActiveChallenge(int challengeId, int challengeIndex, int
}

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public final class SceneTrigger {
private String tag;

public transient SceneGroup currentGroup;
private boolean preserved;

@Override
public boolean equals(Object obj) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ public void onNotify(EntityMonster sceneMonster) {

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
Expand Down

0 comments on commit 24874e7

Please sign in to comment.