mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-01-23 23:32:58 +08:00
Fix some revives; improve dungeon exit flow (#2409)
This commit is contained in:
parent
837e30e04b
commit
f86259a430
@ -7,6 +7,7 @@ import emu.grasscutter.data.binout.*;
|
|||||||
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
|
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
|
||||||
import emu.grasscutter.game.ability.actions.*;
|
import emu.grasscutter.game.ability.actions.*;
|
||||||
import emu.grasscutter.game.ability.mixins.*;
|
import emu.grasscutter.game.ability.mixins.*;
|
||||||
|
import emu.grasscutter.game.entity.EntityAvatar;
|
||||||
import emu.grasscutter.game.entity.GameEntity;
|
import emu.grasscutter.game.entity.GameEntity;
|
||||||
import emu.grasscutter.game.player.*;
|
import emu.grasscutter.game.player.*;
|
||||||
import emu.grasscutter.game.props.FightProperty;
|
import emu.grasscutter.game.props.FightProperty;
|
||||||
@ -562,6 +563,14 @@ public final class AbilityManager extends BasePlayerManager {
|
|||||||
if (killState.getKilled()) {
|
if (killState.getKilled()) {
|
||||||
scene.killEntity(entity);
|
scene.killEntity(entity);
|
||||||
} else if (!entity.isAlive()) {
|
} else if (!entity.isAlive()) {
|
||||||
|
if (entity instanceof EntityAvatar) {
|
||||||
|
// TODO Should EntityAvatar act on this invocation?
|
||||||
|
// It bugs revival due to resetting HP to max when
|
||||||
|
// the avatar should just stay dead.
|
||||||
|
Grasscutter.getLogger()
|
||||||
|
.trace("Entity of ID {} is EntityAvatar. Ignoring", invoke.getEntityId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
entity.setFightProperty(
|
entity.setFightProperty(
|
||||||
FightProperty.FIGHT_PROP_CUR_HP,
|
FightProperty.FIGHT_PROP_CUR_HP,
|
||||||
entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP));
|
entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP));
|
||||||
|
@ -168,13 +168,21 @@ public final class DungeonSystem extends BaseGameSystem {
|
|||||||
dungeonManager.unsetTrialTeam(player);
|
dungeonManager.unsetTrialTeam(player);
|
||||||
}
|
}
|
||||||
// clean temp team if it has
|
// clean temp team if it has
|
||||||
player.getTeamManager().cleanTemporaryTeam();
|
if (!player.getTeamManager().cleanTemporaryTeam())
|
||||||
|
{
|
||||||
|
// no temp team. Will use real current team, but check
|
||||||
|
// for any dead avatar to prevent switching into them.
|
||||||
|
player.getTeamManager().checkCurrentAvatarIsAlive(null);
|
||||||
|
}
|
||||||
player.getTowerManager().clearEntry();
|
player.getTowerManager().clearEntry();
|
||||||
dungeonManager.setTowerDungeon(false);
|
dungeonManager.setTowerDungeon(false);
|
||||||
|
|
||||||
// Transfer player back to world
|
// Transfer player back to world after a small delay.
|
||||||
player.getWorld().transferPlayerToScene(player, prevScene, prevPos);
|
// This wait is important for avoiding double teleports,
|
||||||
player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp));
|
// which specifically happen when player quits a dungeon
|
||||||
|
// by teleporting to map waypoints.
|
||||||
|
// From testing, 200ms seem reasonable.
|
||||||
|
player.getWorld().queueTransferPlayerToScene(player, prevScene, prevPos, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void restartDungeon(Player player) {
|
public void restartDungeon(Player player) {
|
||||||
|
@ -67,6 +67,11 @@ public class EntityAvatar extends GameEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.initAbilities();
|
this.initAbilities();
|
||||||
|
|
||||||
|
// New EntityAvatar instances are created on every scene transition.
|
||||||
|
// Ensure that isDead is properly carried over between scenes.
|
||||||
|
// Otherwise avatars could have 0 HP but not considered dead.
|
||||||
|
this.checkIfDead();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -174,13 +174,7 @@ public abstract class GameEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.lastAttackType = attackType;
|
this.lastAttackType = attackType;
|
||||||
|
this.checkIfDead();
|
||||||
// Check if dead
|
|
||||||
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
|
|
||||||
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
|
|
||||||
this.isDead = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.runLuaCallbacks(event);
|
this.runLuaCallbacks(event);
|
||||||
|
|
||||||
// Packets
|
// Packets
|
||||||
@ -194,6 +188,17 @@ public abstract class GameEntity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void checkIfDead() {
|
||||||
|
if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
|
||||||
|
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
|
||||||
|
this.isDead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the Lua callbacks for {@link EntityDamageEvent}.
|
* Runs the Lua callbacks for {@link EntityDamageEvent}.
|
||||||
*
|
*
|
||||||
@ -333,6 +338,8 @@ public abstract class GameEntity {
|
|||||||
if (entityController != null) {
|
if (entityController != null) {
|
||||||
entityController.onDie(this, getLastAttackType());
|
entityController.onDie(this, getLastAttackType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isDead = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Invoked when a global ability value is updated. */
|
/** Invoked when a global ability value is updated. */
|
||||||
|
@ -176,6 +176,7 @@ public class Player implements PlayerHook, FieldFetch {
|
|||||||
@Getter @Setter private Set<Date> moonCardGetTimes;
|
@Getter @Setter private Set<Date> moonCardGetTimes;
|
||||||
|
|
||||||
@Transient @Getter private boolean paused;
|
@Transient @Getter private boolean paused;
|
||||||
|
@Transient @Getter @Setter private Future<?> queuedTeleport;
|
||||||
@Transient @Getter @Setter private int enterSceneToken;
|
@Transient @Getter @Setter private int enterSceneToken;
|
||||||
@Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE;
|
@Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE;
|
||||||
@Transient private boolean hasSentLoginPackets;
|
@Transient private boolean hasSentLoginPackets;
|
||||||
|
@ -425,6 +425,30 @@ public final class TeamManager extends BasePlayerDataManager {
|
|||||||
this.getPlayer().sendPacket(responsePacket);
|
this.getPlayer().sendPacket(responsePacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure new selected character index is alive.
|
||||||
|
// If not, change to another alive one or revive.
|
||||||
|
checkCurrentAvatarIsAlive(currentEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkCurrentAvatarIsAlive(EntityAvatar currentEntity) {
|
||||||
|
if (currentEntity == null) {
|
||||||
|
currentEntity = this.getCurrentAvatarEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure currently selected character is still alive
|
||||||
|
if (!this.getActiveTeam().get(this.currentCharacterIndex).isAlive()) {
|
||||||
|
// Character died in a dungeon challenge...
|
||||||
|
int replaceIndex = getDeadAvatarReplacement();
|
||||||
|
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
|
||||||
|
this.currentCharacterIndex = replaceIndex;
|
||||||
|
} else {
|
||||||
|
// Team wiped in dungeon...
|
||||||
|
// Revive and change to first avatar.
|
||||||
|
this.currentCharacterIndex = 0;
|
||||||
|
this.reviveAvatar(this.getCurrentAvatarEntity().getAvatar());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if character changed
|
// Check if character changed
|
||||||
var newAvatarEntity = this.getCurrentAvatarEntity();
|
var newAvatarEntity = this.getCurrentAvatarEntity();
|
||||||
if (currentEntity != null && newAvatarEntity != null && currentEntity != newAvatarEntity) {
|
if (currentEntity != null && newAvatarEntity != null && currentEntity != newAvatarEntity) {
|
||||||
@ -700,15 +724,16 @@ public final class TeamManager extends BasePlayerDataManager {
|
|||||||
this.updateTeamEntities(null);
|
this.updateTeamEntities(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanTemporaryTeam() {
|
public boolean cleanTemporaryTeam() {
|
||||||
// check if using temporary team
|
// check if using temporary team
|
||||||
if (useTemporarilyTeamIndex < 0) {
|
if (useTemporarilyTeamIndex < 0) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.useTemporarilyTeamIndex = -1;
|
this.useTemporarilyTeamIndex = -1;
|
||||||
this.temporaryTeam = null;
|
this.temporaryTeam = null;
|
||||||
this.updateTeamEntities(null);
|
this.updateTeamEntities(null);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void setCurrentTeam(int teamId) {
|
public synchronized void setCurrentTeam(int teamId) {
|
||||||
@ -810,20 +835,13 @@ public final class TeamManager extends BasePlayerDataManager {
|
|||||||
// TODO: Perhaps find a way to get vanilla experience?
|
// TODO: Perhaps find a way to get vanilla experience?
|
||||||
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
|
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
|
||||||
} else {
|
} else {
|
||||||
// Replacement avatar
|
// Find replacement avatar
|
||||||
EntityAvatar replacement = null;
|
int replaceIndex = getDeadAvatarReplacement();
|
||||||
int replaceIndex = -1;
|
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
|
||||||
|
// Set index and spawn replacement member
|
||||||
for (int i = 0; i < this.getActiveTeam().size(); i++) {
|
this.setCurrentCharacterIndex(replaceIndex);
|
||||||
EntityAvatar entity = this.getActiveTeam().get(i);
|
this.getPlayer().getScene().addEntity(this.getActiveTeam().get(replaceIndex));
|
||||||
if (entity.isAlive()) {
|
} else {
|
||||||
replaceIndex = i;
|
|
||||||
replacement = entity;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replacement == null) {
|
|
||||||
// No more living team members...
|
// No more living team members...
|
||||||
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
|
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
|
||||||
// Invoke player team death event.
|
// Invoke player team death event.
|
||||||
@ -831,10 +849,6 @@ public final class TeamManager extends BasePlayerDataManager {
|
|||||||
new PlayerTeamDeathEvent(
|
new PlayerTeamDeathEvent(
|
||||||
this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex()));
|
this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex()));
|
||||||
event.call();
|
event.call();
|
||||||
} else {
|
|
||||||
// Set index and spawn replacement member
|
|
||||||
this.setCurrentCharacterIndex(replaceIndex);
|
|
||||||
this.getPlayer().getScene().addEntity(replacement);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -842,6 +856,20 @@ public final class TeamManager extends BasePlayerDataManager {
|
|||||||
this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0));
|
this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getDeadAvatarReplacement() {
|
||||||
|
int replaceIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < this.getActiveTeam().size(); i++) {
|
||||||
|
EntityAvatar entity = this.getActiveTeam().get(i);
|
||||||
|
if (entity.isAlive()) {
|
||||||
|
replaceIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return replaceIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean reviveAvatar(Avatar avatar) {
|
public boolean reviveAvatar(Avatar avatar) {
|
||||||
for (EntityAvatar entity : this.getActiveTeam()) {
|
for (EntityAvatar entity : this.getActiveTeam()) {
|
||||||
if (entity.getAvatar() == avatar) {
|
if (entity.getAvatar() == avatar) {
|
||||||
|
@ -2,6 +2,7 @@ package emu.grasscutter.game.world;
|
|||||||
|
|
||||||
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
|
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
|
||||||
|
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.data.GameData;
|
import emu.grasscutter.data.GameData;
|
||||||
import emu.grasscutter.data.excels.dungeon.DungeonData;
|
import emu.grasscutter.data.excels.dungeon.DungeonData;
|
||||||
import emu.grasscutter.game.entity.*;
|
import emu.grasscutter.game.entity.*;
|
||||||
@ -19,8 +20,10 @@ import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
|
|||||||
import emu.grasscutter.server.game.GameServer;
|
import emu.grasscutter.server.game.GameServer;
|
||||||
import emu.grasscutter.server.packet.send.*;
|
import emu.grasscutter.server.packet.send.*;
|
||||||
import emu.grasscutter.utils.ConversionUtils;
|
import emu.grasscutter.utils.ConversionUtils;
|
||||||
|
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||||
import it.unimi.dsi.fastutil.ints.*;
|
import it.unimi.dsi.fastutil.ints.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@ -43,6 +46,16 @@ public class World implements Iterable<Player> {
|
|||||||
@Getter private boolean isPaused = false;
|
@Getter private boolean isPaused = false;
|
||||||
@Getter private long currentWorldTime;
|
@Getter private long currentWorldTime;
|
||||||
|
|
||||||
|
private static final ExecutorService eventExecutor =
|
||||||
|
new ThreadPoolExecutor(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
60,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingDeque<>(1000),
|
||||||
|
FastThreadLocalThread::new,
|
||||||
|
new ThreadPoolExecutor.AbortPolicy());
|
||||||
|
|
||||||
public World(Player player) {
|
public World(Player player) {
|
||||||
this(player, false);
|
this(player, false);
|
||||||
}
|
}
|
||||||
@ -311,6 +324,17 @@ public class World implements Iterable<Player> {
|
|||||||
this.getScenes().values().forEach(Scene::saveGroups);
|
this.getScenes().values().forEach(Scene::saveGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void queueTransferPlayerToScene(Player player, int sceneId, Position pos, int delayMs) {
|
||||||
|
player.setQueuedTeleport(eventExecutor.submit(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(delayMs);
|
||||||
|
transferPlayerToScene(player, sceneId, pos);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Grasscutter.getLogger().trace("queueTransferPlayerToScene: teleport to scene {} is interrupted", sceneId);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean transferPlayerToScene(Player player, int sceneId, Position pos) {
|
public boolean transferPlayerToScene(Player player, int sceneId, Position pos) {
|
||||||
return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos);
|
return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos);
|
||||||
}
|
}
|
||||||
@ -381,6 +405,16 @@ public class World implements Iterable<Player> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean transferPlayerToScene(Player player, TeleportProperties teleportProperties) {
|
public boolean transferPlayerToScene(Player player, TeleportProperties teleportProperties) {
|
||||||
|
// If a queued teleport already exists, cancel it. This prevents the player from
|
||||||
|
// becoming stranded in a dungeon due to quitting it by teleporting to a map waypoint.
|
||||||
|
synchronized (player) {
|
||||||
|
var queuedTeleport = player.getQueuedTeleport();
|
||||||
|
if (queuedTeleport != null) {
|
||||||
|
player.setQueuedTeleport(null);
|
||||||
|
queuedTeleport.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the teleport properties are valid.
|
// Check if the teleport properties are valid.
|
||||||
if (teleportProperties.getTeleportTo() == null)
|
if (teleportProperties.getTeleportTo() == null)
|
||||||
teleportProperties.setTeleportTo(player.getPosition());
|
teleportProperties.setTeleportTo(player.getPosition());
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package emu.grasscutter.server.packet.recv;
|
||||||
|
|
||||||
|
import emu.grasscutter.net.packet.*;
|
||||||
|
import emu.grasscutter.net.proto.DungeonDieOptionReqOuterClass.DungeonDieOptionReq;
|
||||||
|
import emu.grasscutter.net.proto.PlayerDieOptionOuterClass.PlayerDieOption;
|
||||||
|
import emu.grasscutter.server.game.GameSession;
|
||||||
|
|
||||||
|
@Opcodes(PacketOpcodes.DungeonDieOptionReq)
|
||||||
|
public class HandlerDungeonDieOptionReq extends PacketHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||||
|
DungeonDieOptionReq req = DungeonDieOptionReq.parseFrom(payload);
|
||||||
|
var dieOption = req.getDieOption();
|
||||||
|
// TODO Handle other die options
|
||||||
|
if (req.getIsQuitImmediately()) {
|
||||||
|
session.getPlayer().getServer().getDungeonSystem().exitDungeon(session.getPlayer());
|
||||||
|
}
|
||||||
|
session.getPlayer().sendPacket(new BasePacket(PacketOpcodes.DungeonDieOptionRsp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user