mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2026-05-18 03:59:50 +08:00
bdc4b5af89
* add statue promo data * implement levelup city feature * fix get level city when enter game * format code * fix typo, remove some property in the player, add the field cityInfoData to player class
308 lines
12 KiB
Java
308 lines
12 KiB
Java
package emu.grasscutter.game.managers;
|
|
|
|
import ch.qos.logback.classic.Logger;
|
|
import emu.grasscutter.Grasscutter;
|
|
import emu.grasscutter.data.GameData;
|
|
import emu.grasscutter.data.excels.CityData;
|
|
import emu.grasscutter.data.excels.RewardData;
|
|
import emu.grasscutter.game.city.CityInfoData;
|
|
import emu.grasscutter.game.entity.EntityAvatar;
|
|
import emu.grasscutter.game.player.BasePlayerManager;
|
|
import emu.grasscutter.game.player.Player;
|
|
import emu.grasscutter.game.props.ActionReason;
|
|
import emu.grasscutter.game.props.FightProperty;
|
|
import emu.grasscutter.game.props.PlayerProperty;
|
|
import emu.grasscutter.game.quest.enums.QuestContent;
|
|
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason;
|
|
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
|
|
import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify;
|
|
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
|
|
import emu.grasscutter.server.packet.send.PacketLevelupCityRsp;
|
|
import emu.grasscutter.server.packet.send.PacketSceneForceUnlockNotify;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Timer;
|
|
import java.util.TimerTask;
|
|
|
|
// Statue of the Seven Manager
|
|
public class SotSManager extends BasePlayerManager {
|
|
|
|
// NOTE: Spring volume balance *1 = fight prop HP *100
|
|
|
|
public static final int GlobalMaximumSpringVolume =
|
|
PlayerProperty.PROP_MAX_SPRING_VOLUME.getMax();
|
|
private final Logger logger = Grasscutter.getLogger();
|
|
private final boolean enablePriorityHealing = false;
|
|
private Timer autoRecoverTimer;
|
|
|
|
public SotSManager(Player player) {
|
|
super(player);
|
|
}
|
|
|
|
public boolean getIsAutoRecoveryEnabled() {
|
|
return player.getProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE) == 1;
|
|
}
|
|
|
|
public void setIsAutoRecoveryEnabled(boolean enabled) {
|
|
player.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, enabled ? 1 : 0);
|
|
player.save();
|
|
}
|
|
|
|
public int getAutoRecoveryPercentage() {
|
|
return player.getProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT);
|
|
}
|
|
|
|
public void setAutoRecoveryPercentage(int percentage) {
|
|
player.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, percentage);
|
|
player.save();
|
|
}
|
|
|
|
public long getLastUsed() {
|
|
return player.getSpringLastUsed();
|
|
}
|
|
|
|
public void setLastUsed() {
|
|
player.setSpringLastUsed(System.currentTimeMillis() / 1000);
|
|
player.save();
|
|
}
|
|
|
|
public int getMaxVolume() {
|
|
return player.getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME);
|
|
}
|
|
|
|
public void setMaxVolume(int volume) {
|
|
player.setProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME, volume);
|
|
player.save();
|
|
}
|
|
|
|
public int getCurrentVolume() {
|
|
return player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME);
|
|
}
|
|
|
|
public void setCurrentVolume(int volume) {
|
|
player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, volume);
|
|
setLastUsed();
|
|
player.save();
|
|
}
|
|
|
|
public void handleEnterTransPointRegionNotify() {
|
|
logger.trace("Player entered statue region");
|
|
autoRevive();
|
|
if (autoRecoverTimer == null) {
|
|
autoRecoverTimer = new Timer();
|
|
autoRecoverTimer.schedule(new AutoRecoverTimerTick(), 2500, 15000);
|
|
}
|
|
}
|
|
|
|
public void handleExitTransPointRegionNotify() {
|
|
logger.trace("Player left statue region");
|
|
if (autoRecoverTimer != null) {
|
|
autoRecoverTimer.cancel();
|
|
autoRecoverTimer = null;
|
|
}
|
|
}
|
|
|
|
// autoRevive automatically revives all team members.
|
|
public void autoRevive() {
|
|
player
|
|
.getTeamManager()
|
|
.getActiveTeam()
|
|
.forEach(
|
|
entity -> {
|
|
boolean isAlive = entity.isAlive();
|
|
if (isAlive) {
|
|
return;
|
|
}
|
|
logger.trace("Reviving avatar " + entity.getAvatar().getAvatarData().getName());
|
|
player.getTeamManager().reviveAvatar(entity.getAvatar());
|
|
player.getTeamManager().healAvatar(entity.getAvatar(), 30, 0);
|
|
});
|
|
}
|
|
|
|
public void checkAndHealAvatar(EntityAvatar entity) {
|
|
int maxHP = (int) (entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * 100);
|
|
int currentHP = (int) (entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) * 100);
|
|
if (currentHP == maxHP) {
|
|
return;
|
|
}
|
|
int targetHP = maxHP * getAutoRecoveryPercentage() / 100;
|
|
|
|
if (targetHP > currentHP) {
|
|
int needHP = targetHP - currentHP;
|
|
int currentVolume = getCurrentVolume();
|
|
if (currentVolume >= needHP) {
|
|
// sufficient
|
|
setCurrentVolume(currentVolume - needHP);
|
|
} else {
|
|
// insufficient balance
|
|
needHP = currentVolume;
|
|
setCurrentVolume(0);
|
|
}
|
|
if (needHP > 0) {
|
|
logger.trace(
|
|
"Healing avatar " + entity.getAvatar().getAvatarData().getName() + " +" + needHP);
|
|
player.getTeamManager().healAvatar(entity.getAvatar(), 0, needHP);
|
|
player
|
|
.getSession()
|
|
.send(
|
|
new PacketEntityFightPropChangeReasonNotify(
|
|
entity,
|
|
FightProperty.FIGHT_PROP_CUR_HP,
|
|
((float) needHP / 100),
|
|
List.of(3),
|
|
PropChangeReason.PROP_CHANGE_REASON_STATUE_RECOVER,
|
|
ChangeHpReason.CHANGE_HP_REASON_ADD_STATUE));
|
|
player
|
|
.getSession()
|
|
.send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
|
|
}
|
|
}
|
|
}
|
|
|
|
public void refillSpringVolume() {
|
|
// Temporary: Max spring volume depends on level of the statues in Mondstadt and Liyue. Override
|
|
// until we have statue level.
|
|
// TODO: remove
|
|
// https://genshin-impact.fandom.com/wiki/Statue_of_The_Seven#:~:text=region%20of%20Inazuma.-,Statue%20Levels,-Upon%20first%20unlocking
|
|
setMaxVolume(8500000);
|
|
// Temporary: Auto enable 100% statue recovery until we can adjust statue settings in game
|
|
// TODO: remove
|
|
setAutoRecoveryPercentage(100);
|
|
setIsAutoRecoveryEnabled(true);
|
|
|
|
int maxVolume = getMaxVolume();
|
|
int currentVolume = getCurrentVolume();
|
|
if (currentVolume < maxVolume) {
|
|
long now = System.currentTimeMillis() / 1000;
|
|
int secondsSinceLastUsed = (int) (now - getLastUsed());
|
|
// 15s = 1% max volume
|
|
int volumeRefilled = secondsSinceLastUsed * maxVolume / 15 / 100;
|
|
logger.trace("Statue has refilled HP volume: " + volumeRefilled);
|
|
currentVolume = Math.min(currentVolume + volumeRefilled, maxVolume);
|
|
logger.trace("Statue remaining HP volume: " + currentVolume);
|
|
setCurrentVolume(currentVolume);
|
|
}
|
|
}
|
|
|
|
private class AutoRecoverTimerTick extends TimerTask {
|
|
// autoRecover checks player setting to see if auto recover is enabled, and refill HP to the
|
|
// predefined level.
|
|
public void run() {
|
|
refillSpringVolume();
|
|
|
|
logger.trace(
|
|
"isAutoRecoveryEnabled: "
|
|
+ getIsAutoRecoveryEnabled()
|
|
+ "\tautoRecoverPercentage: "
|
|
+ getAutoRecoveryPercentage());
|
|
|
|
if (getIsAutoRecoveryEnabled()) {
|
|
List<EntityAvatar> activeTeam = player.getTeamManager().getActiveTeam();
|
|
// When the statue does not have enough remaining volume:
|
|
// Enhanced experience: Enable priority healing
|
|
// The current active character will get healed first, then
|
|
// sequential.
|
|
// Vanilla experience: Disable priority healing
|
|
// Sequential healing based on character index.
|
|
int priorityIndex =
|
|
enablePriorityHealing ? player.getTeamManager().getCurrentCharacterIndex() : -1;
|
|
if (priorityIndex >= 0) {
|
|
checkAndHealAvatar(activeTeam.get(priorityIndex));
|
|
}
|
|
for (int i = 0; i < activeTeam.size(); i++) {
|
|
if (i != priorityIndex) {
|
|
checkAndHealAvatar(activeTeam.get(i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public CityData getCityByAreaId(int areaId) {
|
|
return GameData.getCityDataMap().values().stream()
|
|
.filter(city -> city.getAreaIdVec().contains(areaId))
|
|
.findFirst()
|
|
.orElse(null);
|
|
}
|
|
|
|
public CityInfoData getCityInfo(int cityId) {
|
|
if (player.getCityInfoData() == null) player.setCityInfoData(new HashMap<>());
|
|
var cityInfo = player.getCityInfoData().get(cityId);
|
|
if (cityInfo == null) {
|
|
cityInfo = new CityInfoData(cityId);
|
|
player.getCityInfoData().put(cityId, cityInfo);
|
|
}
|
|
return cityInfo;
|
|
}
|
|
|
|
public void addCityInfo(CityInfoData cityInfoData) {
|
|
if (player.getCityInfoData() == null) player.setCityInfoData(new HashMap<>());
|
|
|
|
player.getCityInfoData().put(cityInfoData.getCityId(), cityInfoData);
|
|
}
|
|
|
|
public void levelUpSotS(int areaId, int sceneId, int itemNum) {
|
|
if (itemNum <= 0) return;
|
|
|
|
// search city by areaId
|
|
var city = this.getCityByAreaId(areaId);
|
|
if (city == null) return;
|
|
var cityId = city.getCityId();
|
|
|
|
// check data level up
|
|
var cityInfo = this.getCityInfo(cityId);
|
|
var nextStatuePromoteData = GameData.getStatuePromoteData(cityId, cityInfo.getLevel() + 1);
|
|
if (nextStatuePromoteData == null) return;
|
|
var nextLevelCrystal = nextStatuePromoteData.getCostItems()[0].getCount();
|
|
|
|
// delete item from inventory
|
|
var itemNumrequired = Math.min(itemNum, nextLevelCrystal - cityInfo.getNumCrystal());
|
|
player
|
|
.getInventory()
|
|
.removeItemById(nextStatuePromoteData.getCostItems()[0].getId(), itemNumrequired);
|
|
|
|
// update number oculi
|
|
cityInfo.setNumCrystal(cityInfo.getNumCrystal() + itemNumrequired);
|
|
|
|
// hanble quest
|
|
if (itemNumrequired >= 1)
|
|
player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_CITY_LEVEL_UP, cityId, areaId);
|
|
|
|
// handle oculi overflow
|
|
if (cityInfo.getNumCrystal() >= nextLevelCrystal) {
|
|
cityInfo.setNumCrystal(cityInfo.getNumCrystal() - nextLevelCrystal);
|
|
cityInfo.setLevel(cityInfo.getLevel() + 1);
|
|
|
|
// update max stamina and notify client
|
|
player.setProperty(
|
|
PlayerProperty.PROP_MAX_STAMINA,
|
|
player.getProperty(PlayerProperty.PROP_MAX_STAMINA)
|
|
+ nextStatuePromoteData.getStamina() * 100,
|
|
true);
|
|
|
|
// Add items to inventory
|
|
if (nextStatuePromoteData.getRewardIdList() != null) {
|
|
for (var rewardId : nextStatuePromoteData.getRewardIdList()) {
|
|
RewardData rewardData = GameData.getRewardDataMap().get(rewardId);
|
|
if (rewardData == null) continue;
|
|
|
|
player
|
|
.getInventory()
|
|
.addItemParamDatas(rewardData.getRewardItemList(), ActionReason.CityLevelupReward);
|
|
}
|
|
}
|
|
|
|
// unlock forcescene
|
|
player.sendPacket(new PacketSceneForceUnlockNotify(1, true));
|
|
}
|
|
|
|
// update data
|
|
this.addCityInfo(cityInfo);
|
|
|
|
// Packets
|
|
player.sendPacket(
|
|
new PacketLevelupCityRsp(
|
|
sceneId, cityInfo.getLevel(), cityId, cityInfo.getNumCrystal(), areaId, 0));
|
|
}
|
|
}
|