Commit a2ff8c84 authored by KingRainbow44's avatar KingRainbow44
Browse files

Merge `development` into `plugin-auth`

parents 3adf0d44 a751e71d
package emu.grasscutter.data.custom;
import emu.grasscutter.game.quest.enums.LogicType;
import emu.grasscutter.game.quest.enums.QuestTrigger;
import emu.grasscutter.game.quest.enums.QuestType;
public class MainQuestData {
private int id;
private int series;
private QuestType type;
private long titleTextMapHash;
private int[] suggestTrackMainQuestList;
private int[] rewardIdList;
private SubQuestData[] subQuests;
public int getId() {
return id;
}
public int getSeries() {
return series;
}
public QuestType getType() {
return type;
}
public long getTitleTextMapHash() {
return titleTextMapHash;
}
public int[] getSuggestTrackMainQuestList() {
return suggestTrackMainQuestList;
}
public int[] getRewardIdList() {
return rewardIdList;
}
public SubQuestData[] getSubQuests() {
return subQuests;
}
public static class SubQuestData {
private int subId;
public int getSubId() {
return subId;
}
}
}
package emu.grasscutter.data.def;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.GameResource;
import emu.grasscutter.data.ResourceType;
@ResourceType(name = {"QuestCodexExcelConfigData.json"}, loadPriority = ResourceType.LoadPriority.HIGH)
public class CodexQuest extends GameResource {
private int Id;
private int ParentQuestId;
private int ChapterId;
private int SortOrder;
private boolean IsDisuse;
public int getParentQuestId() {
return ParentQuestId;
}
public int getId() {
return Id;
}
public int getChapterId() {
return ChapterId;
}
public int getSortOrder() {
return SortOrder;
}
public boolean getIsDisuse() {
return IsDisuse;
}
@Override
public void onLoad() {
if(!this.getIsDisuse()) {
GameData.getCodexQuestIdMap().put(this.getParentQuestId(), this);
}
}
}
package emu.grasscutter.data.def;
import java.util.Arrays;
import java.util.List;
import emu.grasscutter.data.GameResource;
import emu.grasscutter.data.ResourceType;
import emu.grasscutter.game.quest.enums.LogicType;
import emu.grasscutter.game.quest.enums.QuestTrigger;
@ResourceType(name = "QuestExcelConfigData.json")
public class QuestData extends GameResource {
private int SubId;
private int MainId;
private int Order;
private long DescTextMapHash;
private boolean FinishParent;
private boolean IsRewind;
private LogicType AcceptCondComb;
private QuestCondition[] acceptConditons;
private LogicType FinishCondComb;
private QuestCondition[] finishConditons;
private LogicType FailCondComb;
private QuestCondition[] failConditons;
private List<QuestParam> AcceptCond;
private List<QuestParam> FinishCond;
private List<QuestParam> FailCond;
private List<QuestExecParam> BeginExec;
private List<QuestExecParam> FinishExec;
private List<QuestExecParam> FailExec;
public int getId() {
return SubId;
}
public int getMainId() {
return MainId;
}
public int getOrder() {
return Order;
}
public long getDescTextMapHash() {
return DescTextMapHash;
}
public boolean finishParent() {
return FinishParent;
}
public boolean isRewind() {
return IsRewind;
}
public LogicType getAcceptCondComb() {
return AcceptCondComb;
}
public QuestCondition[] getAcceptCond() {
return acceptConditons;
}
public LogicType getFinishCondComb() {
return FinishCondComb;
}
public QuestCondition[] getFinishCond() {
return finishConditons;
}
public LogicType getFailCondComb() {
return FailCondComb;
}
public QuestCondition[] getFailCond() {
return failConditons;
}
public void onLoad() {
this.acceptConditons = AcceptCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new);
AcceptCond = null;
this.finishConditons = FinishCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new);
FinishCond = null;
this.failConditons = FailCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new);
FailCond = null;
}
public class QuestParam {
QuestTrigger Type;
int[] Param;
String count;
}
public class QuestExecParam {
QuestTrigger Type;
String[] Param;
String count;
}
public static class QuestCondition {
private QuestTrigger type;
private int[] param;
private String count;
public QuestCondition(QuestParam param) {
this.type = param.Type;
this.param = param.Param;
}
public QuestTrigger getType() {
return type;
}
public int[] getParam() {
return param;
}
public String getCount() {
return count;
}
}
}
...@@ -15,6 +15,7 @@ import emu.grasscutter.game.gacha.GachaRecord; ...@@ -15,6 +15,7 @@ import emu.grasscutter.game.gacha.GachaRecord;
import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.Mail;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.GameMainQuest;
import static com.mongodb.client.model.Filters.eq; import static com.mongodb.client.model.Filters.eq;
...@@ -111,6 +112,8 @@ public final class DatabaseHelper { ...@@ -111,6 +112,8 @@ public final class DatabaseHelper {
DatabaseManager.getGameDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid()));
// Delete GameItem.class data // Delete GameItem.class data
DatabaseManager.getGameDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid()));
// Delete GameMainQuest.class data
DatabaseManager.getGameDatabase().getCollection("quests").deleteMany(eq("ownerUid", target.getPlayerUid()));
// Delete friendships. // Delete friendships.
// Here, we need to make sure to not only delete the deleted account's friendships, // Here, we need to make sure to not only delete the deleted account's friendships,
...@@ -260,4 +263,16 @@ public final class DatabaseHelper { ...@@ -260,4 +263,16 @@ public final class DatabaseHelper {
DeleteResult result = DatabaseManager.getGameDatastore().delete(mail); DeleteResult result = DatabaseManager.getGameDatastore().delete(mail);
return result.wasAcknowledged(); return result.wasAcknowledged();
} }
public static List<GameMainQuest> getAllQuests(Player player) {
return DatabaseManager.getGameDatastore().find(GameMainQuest.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList();
}
public static void saveQuest(GameMainQuest quest) {
DatabaseManager.getGameDatastore().save(quest);
}
public static boolean deleteQuest(GameMainQuest quest) {
return DatabaseManager.getGameDatastore().delete(quest).wasAcknowledged();
}
} }
...@@ -19,6 +19,8 @@ import emu.grasscutter.game.gacha.GachaRecord; ...@@ -19,6 +19,8 @@ import emu.grasscutter.game.gacha.GachaRecord;
import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.Mail;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.GameMainQuest;
import emu.grasscutter.game.quest.GameQuest;
import static emu.grasscutter.Configuration.*; import static emu.grasscutter.Configuration.*;
...@@ -27,7 +29,8 @@ public final class DatabaseManager { ...@@ -27,7 +29,8 @@ public final class DatabaseManager {
private static Datastore dispatchDatastore; private static Datastore dispatchDatastore;
private static final Class<?>[] mappedClasses = new Class<?>[] { private static final Class<?>[] mappedClasses = new Class<?>[] {
DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, GachaRecord.class, Mail.class DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class,
GachaRecord.class, Mail.class, GameMainQuest.class
}; };
public static Datastore getGameDatastore() { public static Datastore getGameDatastore() {
......
...@@ -144,16 +144,17 @@ public class Account { ...@@ -144,16 +144,17 @@ public class Account {
} }
public boolean hasPermission(String permission) { public boolean hasPermission(String permission) {
if (this.permissions.contains(permission) || this.permissions.contains("*")) {
return true; if (this.permissions.contains(permission)) return true;
} if(this.permissions.contains("*") && this.permissions.size() == 1) return true;
String[] permissionParts = permission.split("\\."); String[] permissionParts = permission.split("\\.");
for (String p : this.permissions) { for (String p : this.permissions) {
if (permissionMatchesWildcard(p, permissionParts)) { if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) return false;
return true; if (permissionMatchesWildcard(p, permissionParts)) return true;
}
} }
return false;
return this.permissions.contains("*");
} }
public boolean removePermission(String permission) { public boolean removePermission(String permission) {
......
...@@ -7,6 +7,7 @@ import emu.grasscutter.data.custom.ScenePointEntry; ...@@ -7,6 +7,7 @@ import emu.grasscutter.data.custom.ScenePointEntry;
import emu.grasscutter.data.def.DungeonData; import emu.grasscutter.data.def.DungeonData;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.SceneType; import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.game.quest.enums.QuestTrigger;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
...@@ -51,8 +52,9 @@ public class DungeonManager { ...@@ -51,8 +52,9 @@ public class DungeonManager {
int sceneId = data.getSceneId(); int sceneId = data.getSceneId();
player.getScene().setPrevScene(sceneId); player.getScene().setPrevScene(sceneId);
if(player.getWorld().transferPlayerToScene(player, sceneId, data)){ if (player.getWorld().transferPlayerToScene(player, sceneId, data)) {
player.getScene().addDungeonSettleObserver(basicDungeonSettleObserver); player.getScene().addDungeonSettleObserver(basicDungeonSettleObserver);
player.getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_ENTER_DUNGEON, data.getId());
} }
player.getScene().setPrevScenePoint(pointId); player.getScene().setPrevScenePoint(pointId);
......
...@@ -2,27 +2,48 @@ package emu.grasscutter.game.gacha; ...@@ -2,27 +2,48 @@ package emu.grasscutter.game.gacha;
import emu.grasscutter.net.proto.GachaInfoOuterClass.GachaInfo; import emu.grasscutter.net.proto.GachaInfoOuterClass.GachaInfo;
import emu.grasscutter.net.proto.GachaUpInfoOuterClass.GachaUpInfo; import emu.grasscutter.net.proto.GachaUpInfoOuterClass.GachaUpInfo;
import emu.grasscutter.utils.Utils;
import static emu.grasscutter.Configuration.*; import static emu.grasscutter.Configuration.*;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.common.ItemParamData;
public class GachaBanner { public class GachaBanner {
private int gachaType; private int gachaType;
private int scheduleId; private int scheduleId;
private String prefabPath; private String prefabPath;
private String previewPrefabPath; private String previewPrefabPath;
private String titlePath; private String titlePath;
private int costItem; private int costItemId = 0;
private int costItemAmount = 1;
private int costItemId10 = 0;
private int costItemAmount10 = 10;
private int beginTime; private int beginTime;
private int endTime; private int endTime;
private int sortId; private int sortId;
private int[] rateUpItems1; private int[] rateUpItems4 = {};
private int[] rateUpItems2; private int[] rateUpItems5 = {};
private int baseYellowWeight = 60; // Max 10000 private int[] fallbackItems3 = {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304};
private int basePurpleWeight = 510; // Max 10000 private int[] fallbackItems4Pool1 = {1014, 1020, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064};
private int eventChance = 50; // Chance to win a featured event item private int[] fallbackItems4Pool2 = {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405};
private int softPity = 75; private int[] fallbackItems5Pool1 = {1003, 1016, 1042, 1035, 1041};
private int hardPity = 90; private int[] fallbackItems5Pool2 = {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
private boolean removeC6FromPool = false;
private boolean autoStripRateUpFromFallback = true;
private int[][] weights4 = {{1,510}, {8,510}, {10,10000}};
private int[][] weights5 = {{1,75}, {73,150}, {90,10000}};
private int[][] poolBalanceWeights4 = {{1,255}, {17,255}, {21,10455}};
private int[][] poolBalanceWeights5 = {{1,30}, {147,150}, {181,10230}};
private int eventChance4 = 50; // Chance to win a featured event item
private int eventChance5 = 50; // Chance to win a featured event item
private BannerType bannerType = BannerType.STANDARD; private BannerType bannerType = BannerType.STANDARD;
// Kinda wanna deprecate these but they're in people's configs
private int[] rateUpItems1 = {};
private int[] rateUpItems2 = {};
private int eventChance = -1;
private int costItem = 0;
public int getGachaType() { public int getGachaType() {
return gachaType; return gachaType;
...@@ -48,8 +69,15 @@ public class GachaBanner { ...@@ -48,8 +69,15 @@ public class GachaBanner {
return titlePath; return titlePath;
} }
public ItemParamData getCost(int numRolls) {
return switch (numRolls) {
case 10 -> new ItemParamData((costItemId10 > 0) ? costItemId10 : getCostItem(), costItemAmount10);
default -> new ItemParamData(getCostItem(), costItemAmount * numRolls);
};
}
public int getCostItem() { public int getCostItem() {
return costItem; return (costItem > 0) ? costItem : costItemId;
} }
public int getBeginTime() { public int getBeginTime() {
...@@ -64,32 +92,42 @@ public class GachaBanner { ...@@ -64,32 +92,42 @@ public class GachaBanner {
return sortId; return sortId;
} }
public int getBaseYellowWeight() { public int[] getRateUpItems4() {
return baseYellowWeight; return (rateUpItems2.length > 0) ? rateUpItems2 : rateUpItems4;
} }
public int[] getRateUpItems5() {
public int getBasePurpleWeight() { return (rateUpItems1.length > 0) ? rateUpItems1 : rateUpItems5;
return basePurpleWeight;
} }
public int[] getRateUpItems1() { public int[] getFallbackItems3() {return fallbackItems3;}
return rateUpItems1; public int[] getFallbackItems4Pool1() {return fallbackItems4Pool1;}
} public int[] getFallbackItems4Pool2() {return fallbackItems4Pool2;}
public int[] getFallbackItems5Pool1() {return fallbackItems5Pool1;}
public int[] getFallbackItems5Pool2() {return fallbackItems5Pool2;}
public int[] getRateUpItems2() { public boolean getRemoveC6FromPool() {return removeC6FromPool;}
return rateUpItems2; public boolean getAutoStripRateUpFromFallback() {return autoStripRateUpFromFallback;}
}
public int getSoftPity() { public int getWeight(int rarity, int pity) {
return softPity - 1; return switch(rarity) {
case 4 -> Utils.lerp(pity, weights4);
default -> Utils.lerp(pity, weights5);
};
} }
public int getHardPity() { public int getPoolBalanceWeight(int rarity, int pity) {
return hardPity - 1; return switch(rarity) {
case 4 -> Utils.lerp(pity, poolBalanceWeights4);
default -> Utils.lerp(pity, poolBalanceWeights5);
};
} }
public int getEventChance() { public int getEventChance(int rarity) {
return eventChance; return switch(rarity) {
case 4 -> eventChance4;
default -> (eventChance > -1) ? eventChance : eventChance5;
};
} }
@Deprecated @Deprecated
...@@ -102,34 +140,40 @@ public class GachaBanner { ...@@ -102,34 +140,40 @@ public class GachaBanner {
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort)
+ "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType;
String details = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://"
+ lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":"
+ lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort)
+ "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType;
// Grasscutter.getLogger().info("record = " + record); // Grasscutter.getLogger().info("record = " + record);
ItemParamData costItem1 = this.getCost(1);
ItemParamData costItem10 = this.getCost(10);
GachaInfo.Builder info = GachaInfo.newBuilder() GachaInfo.Builder info = GachaInfo.newBuilder()
.setGachaType(this.getGachaType()) .setGachaType(this.getGachaType())
.setScheduleId(this.getScheduleId()) .setScheduleId(this.getScheduleId())
.setBeginTime(this.getBeginTime()) .setBeginTime(this.getBeginTime())
.setEndTime(this.getEndTime()) .setEndTime(this.getEndTime())
.setCostItemId(this.getCostItem()) .setCostItemId(costItem1.getId())
.setCostItemNum(1) .setCostItemNum(costItem1.getCount())
.setTenCostItemId(costItem10.getId())
.setTenCostItemNum(costItem10.getCount())
.setGachaPrefabPath(this.getPrefabPath()) .setGachaPrefabPath(this.getPrefabPath())
.setGachaPreviewPrefabPath(this.getPreviewPrefabPath()) .setGachaPreviewPrefabPath(this.getPreviewPrefabPath())
.setGachaProbUrl(record) .setGachaProbUrl(details)
.setGachaProbUrlOversea(record) .setGachaProbUrlOversea(details)
.setGachaRecordUrl(record) .setGachaRecordUrl(record)
.setGachaRecordUrlOversea(record) .setGachaRecordUrlOversea(record)
.setTenCostItemId(this.getCostItem())
.setTenCostItemNum(10)
.setLeftGachaTimes(Integer.MAX_VALUE) .setLeftGachaTimes(Integer.MAX_VALUE)
.setGachaTimesLimit(Integer.MAX_VALUE) .setGachaTimesLimit(Integer.MAX_VALUE)
.setGachaSortId(this.getSortId()); .setGachaSortId(this.getSortId());
if (this.getTitlePath() != null) { if (this.getTitlePath() != null) {
info.setGachaTitlePath(this.getTitlePath()); info.setGachaTitlePath(this.getTitlePath());
} }
if (this.getRateUpItems1().length > 0) { if (this.getRateUpItems5().length > 0) {
GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(1); GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(1);
for (int id : getRateUpItems1()) { for (int id : getRateUpItems5()) {
upInfo.addItemIdList(id); upInfo.addItemIdList(id);
info.addMainNameId(id); info.addMainNameId(id);
} }
...@@ -137,10 +181,10 @@ public class GachaBanner { ...@@ -137,10 +181,10 @@ public class GachaBanner {
info.addGachaUpInfoList(upInfo); info.addGachaUpInfoList(upInfo);
} }
if (this.getRateUpItems2().length > 0) { if (this.getRateUpItems4().length > 0) {
GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(2); GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(2);
for (int id : getRateUpItems2()) { for (int id : getRateUpItems4()) {
upInfo.addItemIdList(id); upInfo.addItemIdList(id);
if (info.getSubNameIdCount() == 0) { if (info.getSubNameIdCount() == 0) {
info.addSubNameId(id); info.addSubNameId(id);
......
...@@ -4,6 +4,7 @@ import java.io.File; ...@@ -4,6 +4,7 @@ import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.nio.file.*; import java.nio.file.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
...@@ -13,11 +14,12 @@ import com.google.gson.reflect.TypeToken; ...@@ -13,11 +14,12 @@ import com.google.gson.reflect.TypeToken;
import com.sun.nio.file.SensitivityWatchEventModifier; import com.sun.nio.file.SensitivityWatchEventModifier;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.def.ItemData; import emu.grasscutter.data.def.ItemData;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.gacha.GachaBanner.BannerType;
import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.Inventory;
import emu.grasscutter.game.inventory.ItemType; import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.inventory.MaterialType; import emu.grasscutter.game.inventory.MaterialType;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
...@@ -28,6 +30,7 @@ import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; ...@@ -28,6 +30,7 @@ import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.game.GameServerTickEvent; import emu.grasscutter.server.game.GameServerTickEvent;
import emu.grasscutter.server.packet.send.PacketDoGachaRsp; import emu.grasscutter.server.packet.send.PacketDoGachaRsp;
import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntArrayList;
...@@ -41,15 +44,11 @@ public class GachaManager { ...@@ -41,15 +44,11 @@ public class GachaManager {
private final Int2ObjectMap<GachaBanner> gachaBanners; private final Int2ObjectMap<GachaBanner> gachaBanners;
private GetGachaInfoRsp cachedProto; private GetGachaInfoRsp cachedProto;
WatchService watchService; WatchService watchService;
private final int[] yellowAvatars = new int[] {1003, 1016, 1042, 1035, 1041};
private final int[] yellowWeapons = new int[] {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
private final int[] purpleAvatars = new int[] {1006, 1014, 1015, 1020, 1021, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064};
private final int[] purpleWeapons = new int[] {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405};
private final int[] blueWeapons = new int[] {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304};
private static final int starglitterId = 221; private static final int starglitterId = 221;
private static final int stardustId = 222; private static final int stardustId = 222;
private int[] fallbackItems4Pool2Default = {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405};
private int[] fallbackItems5Pool2Default = {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
public GachaManager(GameServer server) { public GachaManager(GameServer server) {
this.server = server; this.server = server;
...@@ -66,7 +65,7 @@ public class GachaManager { ...@@ -66,7 +65,7 @@ public class GachaManager {
return gachaBanners; return gachaBanners;
} }
public int randomRange(int min, int max) { public int randomRange(int min, int max) { // Both are inclusive
return ThreadLocalRandom.current().nextInt(max - min + 1) + min; return ThreadLocalRandom.current().nextInt(max - min + 1) + min;
} }
...@@ -83,6 +82,8 @@ public class GachaManager { ...@@ -83,6 +82,8 @@ public class GachaManager {
getGachaBanners().put(banner.getGachaType(), banner); getGachaBanners().put(banner.getGachaType(), banner);
} }
Grasscutter.getLogger().info("Banners successfully loaded."); Grasscutter.getLogger().info("Banners successfully loaded.");
this.cachedProto = createProto(); this.cachedProto = createProto();
} else { } else {
Grasscutter.getLogger().error("Unable to load banners. Banners size is 0."); Grasscutter.getLogger().error("Unable to load banners. Banners size is 0.");
...@@ -92,13 +93,153 @@ public class GachaManager { ...@@ -92,13 +93,153 @@ public class GachaManager {
e.printStackTrace(); e.printStackTrace();
} }
} }
private class BannerPools {
public int[] rateUpItems4;
public int[] rateUpItems5;
public int[] fallbackItems4Pool1;
public int[] fallbackItems4Pool2;
public int[] fallbackItems5Pool1;
public int[] fallbackItems5Pool2;
public BannerPools(GachaBanner banner) {
rateUpItems4 = banner.getRateUpItems4();
rateUpItems5 = banner.getRateUpItems5();
fallbackItems4Pool1 = banner.getFallbackItems4Pool1();
fallbackItems4Pool2 = banner.getFallbackItems4Pool2();
fallbackItems5Pool1 = banner.getFallbackItems5Pool1();
fallbackItems5Pool2 = banner.getFallbackItems5Pool2();
if (banner.getAutoStripRateUpFromFallback()) {
fallbackItems4Pool1 = Utils.setSubtract(fallbackItems4Pool1, rateUpItems4);
fallbackItems4Pool2 = Utils.setSubtract(fallbackItems4Pool2, rateUpItems4);
fallbackItems5Pool1 = Utils.setSubtract(fallbackItems5Pool1, rateUpItems5);
fallbackItems5Pool2 = Utils.setSubtract(fallbackItems5Pool2, rateUpItems5);
}
}
public void removeFromAllPools(int[] itemIds) {
rateUpItems4 = Utils.setSubtract(rateUpItems4, itemIds);
rateUpItems5 = Utils.setSubtract(rateUpItems5, itemIds);
fallbackItems4Pool1 = Utils.setSubtract(fallbackItems4Pool1, itemIds);
fallbackItems4Pool2 = Utils.setSubtract(fallbackItems4Pool2, itemIds);
fallbackItems5Pool1 = Utils.setSubtract(fallbackItems5Pool1, itemIds);
fallbackItems5Pool2 = Utils.setSubtract(fallbackItems5Pool2, itemIds);
}
}
private synchronized int checkPlayerAvatarConstellationLevel(Player player, int itemId) { // Maybe this would be useful in the Player class?
ItemData itemData = GameData.getItemDataMap().get(itemId);
if ((itemData == null) || (itemData.getMaterialType() != MaterialType.MATERIAL_AVATAR)){
return -2; // Not an Avatar
}
Avatar avatar = player.getAvatars().getAvatarById((itemId % 1000) + 10000000);
if (avatar == null) {
return -1; // Doesn't have
}
// Constellation
int constLevel = avatar.getCoreProudSkillLevel();
GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId + 100);
constLevel += (constItem == null)? 0 : constItem.getCount();
return constLevel;
}
private synchronized int[] removeC6FromPool(int[] itemPool, Player player) {
IntList temp = new IntArrayList();
for (int itemId : itemPool) {
if (checkPlayerAvatarConstellationLevel(player, itemId) < 6) {
temp.add(itemId);
}
}
return temp.toIntArray();
}
private synchronized int drawRoulette(int[] weights, int cutoff) {
// This follows the logic laid out in issue #183
// Simple weighted selection with an upper bound for the roll that cuts off trailing entries
// All weights must be >= 0
int total = 0;
for (int weight : weights) {
if (weight < 0) {
throw new IllegalArgumentException("Weights must be non-negative!");
}
total += weight;
}
int roll = ThreadLocalRandom.current().nextInt((total < cutoff)? total : cutoff);
int subTotal = 0;
for (int i=0; i<weights.length; i++) {
subTotal += weights[i];
if (roll < subTotal) {
return i;
}
}
// throw new IllegalStateException();
return 0; // This should only be reachable if total==0
}
private synchronized int doRarePull(int[] featured, int[] fallback1, int[] fallback2, int rarity, GachaBanner banner, PlayerGachaBannerInfo gachaInfo) {
int itemId = 0;
boolean pullFeatured = (gachaInfo.getFailedFeaturedItemPulls(rarity) >= 1) // Lost previous coinflip
|| (this.randomRange(1, 100) <= banner.getEventChance(rarity)); // Won this coinflip
if (pullFeatured && (featured.length > 0)) {
itemId = getRandom(featured);
gachaInfo.setFailedFeaturedItemPulls(rarity, 0);
} else {
gachaInfo.addFailedFeaturedItemPulls(rarity, 1);
if (fallback1.length < 1) {
if (fallback2.length < 1) {
itemId = getRandom((rarity==5)? fallbackItems5Pool2Default : fallbackItems4Pool2Default);
} else {
itemId = getRandom(fallback2);
}
} else if (fallback2.length < 1) {
itemId = getRandom(fallback1);
} else { // Both pools are possible, use the pool balancer
int pityPool1 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 1));
int pityPool2 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 2));
int chosenPool = switch ((pityPool1 >= pityPool2)? 1 : 0) { // Larger weight must come first for the hard cutoff to function correctly
case 1 -> 1 + drawRoulette(new int[] {pityPool1, pityPool2}, 10000);
default -> 2 - drawRoulette(new int[] {pityPool2, pityPool1}, 10000);
};
itemId = switch (chosenPool) {
case 1:
gachaInfo.setPityPool(rarity, 1, 0);
yield getRandom(fallback1);
default:
gachaInfo.setPityPool(rarity, 2, 0);
yield getRandom(fallback2);
};
}
}
return itemId;
}
private synchronized int doPull(GachaBanner banner, PlayerGachaBannerInfo gachaInfo, BannerPools pools) {
// Pre-increment all pity pools (yes this makes all calculations assume 1-indexed pity)
gachaInfo.incPityAll();
int[] weights = {banner.getWeight(5, gachaInfo.getPity5()), banner.getWeight(4, gachaInfo.getPity4()), 10000};
int levelWon = 5 - drawRoulette(weights, 10000);
return switch (levelWon) {
case 5:
gachaInfo.setPity5(0);
yield doRarePull(pools.rateUpItems5, pools.fallbackItems5Pool1, pools.fallbackItems5Pool2, 5, banner, gachaInfo);
case 4:
gachaInfo.setPity4(0);
yield doRarePull(pools.rateUpItems4, pools.fallbackItems4Pool1, pools.fallbackItems4Pool2, 4, banner, gachaInfo);
default:
yield getRandom(banner.getFallbackItems3());
};
}
public synchronized void doPulls(Player player, int gachaType, int times) { public synchronized void doPulls(Player player, int gachaType, int times) {
// Sanity check // Sanity check
if (times != 10 && times != 1) { if (times != 10 && times != 1) {
return; return;
} }
if (player.getInventory().getInventoryTab(ItemType.ITEM_WEAPON).getSize() + times > player.getInventory().getInventoryTab(ItemType.ITEM_WEAPON).getMaxCapacity()) { Inventory inventory = player.getInventory();
if (inventory.getInventoryTab(ItemType.ITEM_WEAPON).getSize() + times > inventory.getInventoryTab(ItemType.ITEM_WEAPON).getMaxCapacity()) {
player.sendPacket(new PacketDoGachaRsp()); player.sendPacket(new PacketDoGachaRsp());
return; return;
} }
...@@ -111,93 +252,33 @@ public class GachaManager { ...@@ -111,93 +252,33 @@ public class GachaManager {
} }
// Spend currency // Spend currency
if (banner.getCostItem() > 0) { ItemParamData cost = banner.getCost(times);
GameItem costItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(banner.getCostItem()); if (cost.getCount() > 0 && !inventory.payItem(cost)) {
if (costItem == null || costItem.getCount() < times) { player.sendPacket(new PacketDoGachaRsp());
return; return;
}
player.getInventory().removeItem(costItem, times);
}
// Roll
PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner);
IntList wonItems = new IntArrayList(times);
for (int i = 0; i < times; i++) {
int random = this.randomRange(1, 10000);
int itemId = 0;
int bonusYellowChance = gachaInfo.getPity5() >= banner.getSoftPity() ? 100 * (gachaInfo.getPity5() - banner.getSoftPity() - 1): 0;
int yellowChance = banner.getBaseYellowWeight() + (int) Math.floor(100f * (gachaInfo.getPity5() / (banner.getSoftPity() - 1D))) + bonusYellowChance;
int purpleChance = 10000 - (banner.getBasePurpleWeight() + (int) Math.floor(790f * (gachaInfo.getPity4() / 8f)));
if (random <= yellowChance || gachaInfo.getPity5() >= banner.getHardPity()) {
if (banner.getRateUpItems1().length > 0) {
int eventChance = this.randomRange(1, 100);
if (eventChance <= banner.getEventChance() || gachaInfo.getFailedFeaturedItemPulls() >= 1) {
itemId = getRandom(banner.getRateUpItems1());
gachaInfo.setFailedFeaturedItemPulls(0);
} else {
// Lost the 50/50... rip
gachaInfo.addFailedFeaturedItemPulls(1);
}
}
if (itemId == 0) {
int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2);
if (typeChance == 1) {
itemId = getRandom(this.yellowAvatars);
} else {
itemId = getRandom(this.yellowWeapons);
}
}
// Pity
gachaInfo.addPity4(1);
gachaInfo.setPity5(0);
} else if (random >= purpleChance || gachaInfo.getPity4() >= 9) {
if (banner.getRateUpItems2().length > 0) {
int eventChance = this.randomRange(1, 100);
if (eventChance >= 50) {
itemId = getRandom(banner.getRateUpItems2());
}
}
if (itemId == 0) {
int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2);
if (typeChance == 1) {
itemId = getRandom(this.purpleAvatars);
} else {
itemId = getRandom(this.purpleWeapons);
}
}
// Pity
gachaInfo.addPity5(1);
gachaInfo.setPity4(0);
} else {
itemId = getRandom(this.blueWeapons);
// Pity
gachaInfo.addPity4(1);
gachaInfo.addPity5(1);
}
// Add winning item
wonItems.add(itemId);
} }
// Add to character // Add to character
PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner);
BannerPools pools = new BannerPools(banner);
List<GachaItem> list = new ArrayList<>(); List<GachaItem> list = new ArrayList<>();
int stardust = 0, starglitter = 0; int stardust = 0, starglitter = 0;
if (banner.getRemoveC6FromPool()) { // The ultimate form of pity (non-vanilla)
pools.rateUpItems4 = removeC6FromPool(pools.rateUpItems4, player);
pools.rateUpItems5 = removeC6FromPool(pools.rateUpItems5, player);
pools.fallbackItems4Pool1 = removeC6FromPool(pools.fallbackItems4Pool1, player);
pools.fallbackItems4Pool2 = removeC6FromPool(pools.fallbackItems4Pool2, player);
pools.fallbackItems5Pool1 = removeC6FromPool(pools.fallbackItems5Pool1, player);
pools.fallbackItems5Pool2 = removeC6FromPool(pools.fallbackItems5Pool2, player);
}
for (int itemId : wonItems) { for (int i = 0; i < times; i++) {
// Roll
int itemId = doPull(banner, gachaInfo, pools);
ItemData itemData = GameData.getItemDataMap().get(itemId); ItemData itemData = GameData.getItemDataMap().get(itemId);
if (itemData == null) { if (itemData == null) {
continue; continue; // Maybe we should bail out if an item fails instead of rolling the rest?
} }
// Write gacha record // Write gacha record
...@@ -210,57 +291,47 @@ public class GachaManager { ...@@ -210,57 +291,47 @@ public class GachaManager {
boolean isTransferItem = false; boolean isTransferItem = false;
// Const check // Const check
if (itemData.getMaterialType() == MaterialType.MATERIAL_AVATAR) { int constellation = checkPlayerAvatarConstellationLevel(player, itemId);
int avatarId = (itemData.getId() % 1000) + 10000000; switch (constellation) {
Avatar avatar = player.getAvatars().getAvatarById(avatarId); case -2: // Is weapon
if (avatar != null) { switch (itemData.getRankLevel()) {
int constLevel = avatar.getCoreProudSkillLevel(); case 5 -> addStarglitter = 10;
int constItemId = itemData.getId() + 100; case 4 -> addStarglitter = 2;
GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId); default -> addStardust = 15;
if (constItem != null) {
constLevel += constItem.getCount();
} }
break;
if (constLevel < 6) { case -1: // New character
// Not max const gachaItem.setIsGachaItemNew(true);
addStarglitter = 2; break;
// Add 1 const default:
if (constellation >= 6) { // C6, give consolation starglitter
addStarglitter = (itemData.getRankLevel()==5)? 25 : 5;
} else { // C0-C5, give constellation item
if (banner.getRemoveC6FromPool() && constellation == 5) { // New C6, remove it from the pools so we don't get C7 in a 10pull
pools.removeFromAllPools(new int[] {itemId});
}
addStarglitter = (itemData.getRankLevel()==5)? 10 : 2;
int constItemId = itemId + 100;
GameItem constItem = inventory.getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId);
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null)); gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null));
player.getInventory().addItem(constItemId, 1); inventory.addItem(constItemId, 1);
} else {
// Is max const
addStarglitter = 5;
}
if (itemData.getRankLevel() == 5) {
addStarglitter *= 5;
} }
isTransferItem = true; isTransferItem = true;
} else { break;
// New
gachaItem.setIsGachaItemNew(true);
}
} else {
// Is weapon
switch (itemData.getRankLevel()) {
case 5 -> addStarglitter = 10;
case 4 -> addStarglitter = 2;
case 3 -> addStardust = 15;
}
} }
// Create item // Create item
GameItem item = new GameItem(itemData); GameItem item = new GameItem(itemData);
gachaItem.setGachaItem(item.toItemParam()); gachaItem.setGachaItem(item.toItemParam());
player.getInventory().addItem(item); inventory.addItem(item);
stardust += addStardust; stardust += addStardust;
starglitter += addStarglitter; starglitter += addStarglitter;
if (addStardust > 0) { if (addStardust > 0) {
gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(stardustId).setCount(addStardust)); gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(stardustId).setCount(addStardust));
} if (addStarglitter > 0) { }
if (addStarglitter > 0) {
ItemParam starglitterParam = ItemParam.newBuilder().setItemId(starglitterId).setCount(addStarglitter).build(); ItemParam starglitterParam = ItemParam.newBuilder().setItemId(starglitterId).setCount(addStarglitter).build();
if (isTransferItem) { if (isTransferItem) {
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(starglitterParam)); gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(starglitterParam));
...@@ -273,9 +344,10 @@ public class GachaManager { ...@@ -273,9 +344,10 @@ public class GachaManager {
// Add stardust/starglitter // Add stardust/starglitter
if (stardust > 0) { if (stardust > 0) {
player.getInventory().addItem(stardustId, stardust); inventory.addItem(stardustId, stardust);
} if (starglitter > 0) { }
player.getInventory().addItem(starglitterId, starglitter); if (starglitter > 0) {
inventory.addItem(starglitterId, starglitter);
} }
// Packets // Packets
......
...@@ -7,6 +7,11 @@ public class PlayerGachaBannerInfo { ...@@ -7,6 +7,11 @@ public class PlayerGachaBannerInfo {
private int pity5 = 0; private int pity5 = 0;
private int pity4 = 0; private int pity4 = 0;
private int failedFeaturedItemPulls = 0; private int failedFeaturedItemPulls = 0;
private int failedFeatured4ItemPulls = 0;
private int pity5Pool1 = 0;
private int pity5Pool2 = 0;
private int pity4Pool1 = 0;
private int pity4Pool2 = 0;
public int getPity5() { public int getPity5() {
return pity5; return pity5;
...@@ -32,15 +37,82 @@ public class PlayerGachaBannerInfo { ...@@ -32,15 +37,82 @@ public class PlayerGachaBannerInfo {
this.pity4 += amount; this.pity4 += amount;
} }
public int getFailedFeaturedItemPulls() { public int getFailedFeaturedItemPulls(int rarity) {
return failedFeaturedItemPulls; return switch (rarity) {
case 4 -> failedFeatured4ItemPulls;
default -> failedFeaturedItemPulls; // 5
};
} }
public void setFailedFeaturedItemPulls(int failedEventCharacterPulls) { public void setFailedFeaturedItemPulls(int rarity, int amount) {
this.failedFeaturedItemPulls = failedEventCharacterPulls; switch (rarity) {
case 4 -> failedFeatured4ItemPulls = amount;
default -> failedFeaturedItemPulls = amount; // 5
};
} }
public void addFailedFeaturedItemPulls(int amount) { public void addFailedFeaturedItemPulls(int rarity, int amount) {
failedFeaturedItemPulls += amount; switch (rarity) {
case 4 -> failedFeatured4ItemPulls += amount;
default -> failedFeaturedItemPulls += amount; // 5
};
}
public int getPityPool(int rarity, int pool) {
return switch (rarity) {
case 4 -> switch (pool) {
case 1 -> pity4Pool1;
default -> pity4Pool2;
};
default -> switch (pool) {
case 1 -> pity5Pool1;
default -> pity5Pool2;
};
};
}
public void setPityPool(int rarity, int pool, int amount) {
switch (rarity) {
case 4:
switch (pool) {
case 1 -> pity4Pool1 = amount;
default -> pity4Pool2 = amount;
};
break;
case 5:
default:
switch (pool) {
case 1 -> pity5Pool1 = amount;
default -> pity5Pool2 = amount;
};
break;
};
}
public void addPityPool(int rarity, int pool, int amount) {
switch (rarity) {
case 4:
switch (pool) {
case 1 -> pity4Pool1 += amount;
default -> pity4Pool2 += amount;
};
break;
case 5:
default:
switch (pool) {
case 1 -> pity5Pool1 += amount;
default -> pity5Pool2 += amount;
};
break;
};
}
public void incPityAll() {
pity4++;
pity5++;
pity4Pool1++;
pity4Pool2++;
pity5Pool1++;
pity5Pool2++;
} }
} }
...@@ -7,6 +7,7 @@ import java.util.List; ...@@ -7,6 +7,7 @@ import java.util.List;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.def.AvatarCostumeData; import emu.grasscutter.data.def.AvatarCostumeData;
import emu.grasscutter.data.def.AvatarData; import emu.grasscutter.data.def.AvatarData;
import emu.grasscutter.data.def.AvatarFlycloakData; import emu.grasscutter.data.def.AvatarFlycloakData;
...@@ -256,6 +257,64 @@ public class Inventory implements Iterable<GameItem> { ...@@ -256,6 +257,64 @@ public class Inventory implements Iterable<GameItem> {
getPlayer().setCrystals(player.getCrystals() + count); getPlayer().setCrystals(player.getCrystals() + count);
} }
} }
private int getVirtualItemCount(int itemId) {
switch (itemId) {
case 201: // Primogem
return player.getPrimogems();
case 202: // Mora
return player.getMora();
case 203: // Genesis Crystals
return player.getCrystals();
default:
GameItem item = getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId); // What if we ever want to operate on weapons/relics/furniture? :S
return (item == null) ? 0 : item.getCount();
}
}
public boolean payItem(int id, int count) {
return payItem(new ItemParamData(id, count));
}
public boolean payItem(ItemParamData costItem) {
return payItems(new ItemParamData[] {costItem}, 1, null);
}
public boolean payItems(ItemParamData[] costItems) {
return payItems(costItems, 1, null);
}
public boolean payItems(ItemParamData[] costItems, int quantity) {
return payItems(costItems, quantity, null);
}
public synchronized boolean payItems(ItemParamData[] costItems, int quantity, ActionReason reason) {
// Make sure player has requisite items
for (ItemParamData cost : costItems) {
if (getVirtualItemCount(cost.getId()) < (cost.getCount() * quantity)) {
return false;
}
}
// All costs are satisfied, now remove them all
for (ItemParamData cost : costItems) {
switch (cost.getId()) {
case 201 -> // Primogem
player.setPrimogems(player.getPrimogems() - (cost.getCount() * quantity));
case 202 -> // Mora
player.setMora(player.getMora() - (cost.getCount() * quantity));
case 203 -> // Genesis Crystals
player.setCrystals(player.getCrystals() - (cost.getCount() * quantity));
default ->
removeItem(getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()), cost.getCount() * quantity);
}
}
if (reason != null) { // Do we need these?
// getPlayer().sendPacket(new PacketItemAddHintNotify(changedItems, reason));
}
// getPlayer().sendPacket(new PacketStoreItemChangeNotify(changedItems));
return true;
}
public void removeItems(List<GameItem> items) { public void removeItems(List<GameItem> items) {
// TODO Bulk delete // TODO Bulk delete
......
package emu.grasscutter.game.managers; package emu.grasscutter.game.managers;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -38,6 +39,8 @@ public class InventoryManager { ...@@ -38,6 +39,8 @@ public class InventoryManager {
private final static int RELIC_MATERIAL_1 = 105002; // Sanctifying Unction private final static int RELIC_MATERIAL_1 = 105002; // Sanctifying Unction
private final static int RELIC_MATERIAL_2 = 105003; // Sanctifying Essence private final static int RELIC_MATERIAL_2 = 105003; // Sanctifying Essence
private final static int RELIC_MATERIAL_EXP_1 = 2500; // Sanctifying Unction
private final static int RELIC_MATERIAL_EXP_2 = 10000; // Sanctifying Essence
private final static int WEAPON_ORE_1 = 104011; // Enhancement Ore private final static int WEAPON_ORE_1 = 104011; // Enhancement Ore
private final static int WEAPON_ORE_2 = 104012; // Fine Enhancement Ore private final static int WEAPON_ORE_2 = 104012; // Fine Enhancement Ore
...@@ -85,6 +88,7 @@ public class InventoryManager { ...@@ -85,6 +88,7 @@ public class InventoryManager {
int moraCost = 0; int moraCost = 0;
int expGain = 0; int expGain = 0;
List<GameItem> foodRelics = new ArrayList<GameItem>();
for (long guid : foodRelicList) { for (long guid : foodRelicList) {
// Add to delete queue // Add to delete queue
GameItem food = player.getInventory().getItemByGuid(guid); GameItem food = player.getInventory().getItemByGuid(guid);
...@@ -96,23 +100,21 @@ public class InventoryManager { ...@@ -96,23 +100,21 @@ public class InventoryManager {
expGain += food.getItemData().getBaseConvExp(); expGain += food.getItemData().getBaseConvExp();
// Feeding artifact with exp already // Feeding artifact with exp already
if (food.getTotalExp() > 0) { if (food.getTotalExp() > 0) {
expGain += (int) Math.floor(food.getTotalExp() * .8f); expGain += (food.getTotalExp() * 4) / 5;
} }
foodRelics.add(food);
} }
List<ItemParamData> payList = new ArrayList<ItemParamData>();
for (ItemParam itemParam : list) { for (ItemParam itemParam : list) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemParam.getItemId()); int amount = itemParam.getCount(); // Previously this capped to inventory amount, but rejecting the payment makes more sense for an invalid order
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_RELIQUARY_MATERIAL) { int gain = amount * switch(itemParam.getItemId()) {
continue; case RELIC_MATERIAL_1 -> RELIC_MATERIAL_EXP_1;
} case RELIC_MATERIAL_2 -> RELIC_MATERIAL_EXP_2;
int amount = Math.min(food.getCount(), itemParam.getCount()); default -> 0;
int gain = 0; };
if (food.getItemId() == RELIC_MATERIAL_2) {
gain = 10000 * amount;
} else if (food.getItemId() == RELIC_MATERIAL_1) {
gain = 2500 * amount;
}
expGain += gain; expGain += gain;
moraCost += gain; moraCost += gain;
payList.add(new ItemParamData(itemParam.getItemId(), itemParam.getCount()));
} }
// Make sure exp gain is valid // Make sure exp gain is valid
...@@ -120,28 +122,14 @@ public class InventoryManager { ...@@ -120,28 +122,14 @@ public class InventoryManager {
return; return;
} }
// Check mora // Confirm payment of materials and mora (assume food relics are payable afterwards)
if (player.getMora() < moraCost) { payList.add(new ItemParamData(202, moraCost));
if (!player.getInventory().payItems(payList.toArray(new ItemParamData[0]))) {
return; return;
} }
player.setMora(player.getMora() - moraCost);
// Consume food items // Consume food relics
for (long guid : foodRelicList) { player.getInventory().removeItems(foodRelics);
GameItem food = player.getInventory().getItemByGuid(guid);
if (food == null || !food.isDestroyable()) {
continue;
}
player.getInventory().removeItem(food);
}
for (ItemParam itemParam : list) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemParam.getItemId());
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_RELIQUARY_MATERIAL) {
continue;
}
int amount = Math.min(food.getCount(), itemParam.getCount());
player.getInventory().removeItem(food, amount);
}
// Implement random rate boost // Implement random rate boost
int rate = 1; int rate = 1;
...@@ -231,22 +219,16 @@ public class InventoryManager { ...@@ -231,22 +219,16 @@ public class InventoryManager {
} }
expGain += food.getItemData().getWeaponBaseExp(); expGain += food.getItemData().getWeaponBaseExp();
if (food.getTotalExp() > 0) { if (food.getTotalExp() > 0) {
expGain += (int) Math.floor(food.getTotalExp() * .8f); expGain += (food.getTotalExp() * 4) / 5;
} }
} }
for (ItemParam param : itemParamList) { for (ItemParam param : itemParamList) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId()); expGain += param.getCount() * switch(param.getItemId()) {
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) { case WEAPON_ORE_1 -> WEAPON_ORE_EXP_1;
continue; case WEAPON_ORE_2 -> WEAPON_ORE_EXP_2;
} case WEAPON_ORE_3 -> WEAPON_ORE_EXP_3;
int amount = Math.min(param.getCount(), food.getCount()); default -> 0;
if (food.getItemId() == WEAPON_ORE_3) { };
expGain += 10000 * amount;
} else if (food.getItemId() == WEAPON_ORE_2) {
expGain += 2000 * amount;
} else if (food.getItemId() == WEAPON_ORE_1) {
expGain += 400 * amount;
}
} }
// Try // Try
...@@ -288,65 +270,45 @@ public class InventoryManager { ...@@ -288,65 +270,45 @@ public class InventoryManager {
} }
// Get exp gain // Get exp gain
int expGain = 0, moraCost = 0; int expGain = 0, expGainFree = 0;
List<GameItem> foodWeapons = new ArrayList<GameItem>();
for (long guid : foodWeaponGuidList) { for (long guid : foodWeaponGuidList) {
GameItem food = player.getInventory().getItemByGuid(guid); GameItem food = player.getInventory().getItemByGuid(guid);
if (food == null || !food.isDestroyable()) { if (food == null || !food.isDestroyable()) {
continue; continue;
} }
expGain += food.getItemData().getWeaponBaseExp(); expGain += food.getItemData().getWeaponBaseExp();
moraCost += (int) Math.floor(food.getItemData().getWeaponBaseExp() * .1f);
if (food.getTotalExp() > 0) { if (food.getTotalExp() > 0) {
expGain += (int) Math.floor(food.getTotalExp() * .8f); expGainFree += (food.getTotalExp() * 4) / 5; // No tax :D
} }
foodWeapons.add(food);
} }
List<ItemParamData> payList = new ArrayList<ItemParamData>();
for (ItemParam param : itemParamList) { for (ItemParam param : itemParamList) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId()); int amount = param.getCount(); // Previously this capped to inventory amount, but rejecting the payment makes more sense for an invalid order
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) { int gain = amount * switch(param.getItemId()) {
continue; case WEAPON_ORE_1 -> WEAPON_ORE_EXP_1;
} case WEAPON_ORE_2 -> WEAPON_ORE_EXP_2;
int amount = Math.min(param.getCount(), food.getCount()); case WEAPON_ORE_3 -> WEAPON_ORE_EXP_3;
int gain = 0; default -> 0;
if (food.getItemId() == WEAPON_ORE_3) { };
gain = 10000 * amount;
} else if (food.getItemId() == WEAPON_ORE_2) {
gain = 2000 * amount;
} else if (food.getItemId() == WEAPON_ORE_1) {
gain = 400 * amount;
}
expGain += gain; expGain += gain;
moraCost += (int) Math.floor(gain * .1f); payList.add(new ItemParamData(param.getItemId(), amount));
} }
// Make sure exp gain is valid // Make sure exp gain is valid
int moraCost = expGain / 10;
expGain += expGainFree;
if (expGain <= 0) { if (expGain <= 0) {
return; return;
} }
// Mora check // Confirm payment of materials and mora (assume food weapons are payable afterwards)
if (player.getMora() >= moraCost) { payList.add(new ItemParamData(202, moraCost));
player.setMora(player.getMora() - moraCost); if (!player.getInventory().payItems(payList.toArray(new ItemParamData[0]))) {
} else {
return; return;
} }
player.getInventory().removeItems(foodWeapons);
// Consume weapon/items used to feed
for (long guid : foodWeaponGuidList) {
GameItem food = player.getInventory().getItemByGuid(guid);
if (food == null || !food.isDestroyable()) {
continue;
}
player.getInventory().removeItem(food);
}
for (ItemParam param : itemParamList) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId());
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) {
continue;
}
int amount = Math.min(param.getCount(), food.getCount());
player.getInventory().removeItem(food, amount);
}
// Level up // Level up
int maxLevel = promoteData.getUnlockMaxLevel(); int maxLevel = promoteData.getUnlockMaxLevel();
...@@ -393,7 +355,7 @@ public class InventoryManager { ...@@ -393,7 +355,7 @@ public class InventoryManager {
player.sendPacket(new PacketWeaponUpgradeRsp(weapon, oldLevel, leftovers)); player.sendPacket(new PacketWeaponUpgradeRsp(weapon, oldLevel, leftovers));
} }
private List<ItemParam> getLeftoverOres(float leftover) { private List<ItemParam> getLeftoverOres(int leftover) {
List<ItemParam> leftoverOreList = new ArrayList<>(3); List<ItemParam> leftoverOreList = new ArrayList<>(3);
if (leftover < WEAPON_ORE_EXP_1) { if (leftover < WEAPON_ORE_EXP_1) {
...@@ -401,11 +363,11 @@ public class InventoryManager { ...@@ -401,11 +363,11 @@ public class InventoryManager {
} }
// Get leftovers // Get leftovers
int ore3 = (int) Math.floor(leftover / WEAPON_ORE_EXP_3); int ore3 = leftover / WEAPON_ORE_EXP_3;
leftover = leftover % WEAPON_ORE_EXP_3; leftover = leftover % WEAPON_ORE_EXP_3;
int ore2 = (int) Math.floor(leftover / WEAPON_ORE_EXP_2); int ore2 = leftover / WEAPON_ORE_EXP_2;
leftover = leftover % WEAPON_ORE_EXP_2; leftover = leftover % WEAPON_ORE_EXP_2;
int ore1 = (int) Math.floor(leftover / WEAPON_ORE_EXP_1); int ore1 = leftover / WEAPON_ORE_EXP_1;
if (ore3 > 0) { if (ore3 > 0) {
leftoverOreList.add(ItemParam.newBuilder().setItemId(WEAPON_ORE_3).setCount(ore3).build()); leftoverOreList.add(ItemParam.newBuilder().setItemId(WEAPON_ORE_3).setCount(ore3).build());
...@@ -496,27 +458,16 @@ public class InventoryManager { ...@@ -496,27 +458,16 @@ public class InventoryManager {
return; return;
} }
// Make sure player has promote items // Pay materials and mora if possible
for (ItemParamData cost : nextPromoteData.getCostItems()) { ItemParamData[] costs = nextPromoteData.getCostItems(); // Can this be null?
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()); if (nextPromoteData.getCoinCost() > 0) {
if (feedItem == null || feedItem.getCount() < cost.getCount()) { costs = Arrays.copyOf(costs, costs.length + 1);
return; costs[costs.length-1] = new ItemParamData(202, nextPromoteData.getCoinCost());
}
} }
if (!player.getInventory().payItems(costs)) {
// Mora check
if (player.getMora() >= nextPromoteData.getCoinCost()) {
player.setMora(player.getMora() - nextPromoteData.getCoinCost());
} else {
return; return;
} }
// Consume promote filler items
for (ItemParamData cost : nextPromoteData.getCostItems()) {
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
player.getInventory().removeItem(feedItem, cost.getCount());
}
int oldPromoteLevel = weapon.getPromoteLevel(); int oldPromoteLevel = weapon.getPromoteLevel();
weapon.setPromoteLevel(nextPromoteLevel); weapon.setPromoteLevel(nextPromoteLevel);
weapon.save(); weapon.save();
...@@ -552,27 +503,16 @@ public class InventoryManager { ...@@ -552,27 +503,16 @@ public class InventoryManager {
return; return;
} }
// Make sure player has cost items // Pay materials and mora if possible
for (ItemParamData cost : nextPromoteData.getCostItems()) { ItemParamData[] costs = nextPromoteData.getCostItems(); // Can this be null?
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()); if (nextPromoteData.getCoinCost() > 0) {
if (feedItem == null || feedItem.getCount() < cost.getCount()) { costs = Arrays.copyOf(costs, costs.length + 1);
return; costs[costs.length-1] = new ItemParamData(202, nextPromoteData.getCoinCost());
}
} }
if (!player.getInventory().payItems(costs)) {
// Mora check
if (player.getMora() >= nextPromoteData.getCoinCost()) {
player.setMora(player.getMora() - nextPromoteData.getCoinCost());
} else {
return; return;
} }
// Consume promote filler items
for (ItemParamData cost : nextPromoteData.getCostItems()) {
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
player.getInventory().removeItem(feedItem, cost.getCount());
}
// Update promote level // Update promote level
avatar.setPromoteLevel(nextPromoteLevel); avatar.setPromoteLevel(nextPromoteLevel);
...@@ -616,35 +556,26 @@ public class InventoryManager { ...@@ -616,35 +556,26 @@ public class InventoryManager {
return; return;
} }
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId);
if (feedItem == null || feedItem.getItemData().getMaterialType() != MaterialType.MATERIAL_EXP_FRUIT || feedItem.getCount() < count) {
return;
}
// Calc exp // Calc exp
int expGain = 0, moraCost = 0; int expGain = switch(itemId) {
case AVATAR_BOOK_1 -> AVATAR_BOOK_EXP_1 * count;
// TODO clean up case AVATAR_BOOK_2 -> AVATAR_BOOK_EXP_2 * count;
if (itemId == AVATAR_BOOK_3) { case AVATAR_BOOK_3 -> AVATAR_BOOK_EXP_3 * count;
expGain = AVATAR_BOOK_EXP_3 * count; default -> 0;
} else if (itemId == AVATAR_BOOK_2) { };
expGain = AVATAR_BOOK_EXP_2 * count;
} else if (itemId == AVATAR_BOOK_1) { // Sanity check
expGain = AVATAR_BOOK_EXP_1 * count; if (expGain <= 0) {
return;
} }
moraCost = (int) Math.floor(expGain * .2f);
// Payment check
// Mora check int moraCost = expGain / 5;
if (player.getMora() >= moraCost) { ItemParamData[] costItems = new ItemParamData[] {new ItemParamData(itemId, count), new ItemParamData(202, moraCost)};
player.setMora(player.getMora() - moraCost); if (!player.getInventory().payItems(costItems)) {
} else {
return; return;
} }
// Consume items
player.getInventory().removeItem(feedItem, count);
// Level up // Level up
upgradeAvatar(player, avatar, promoteData, expGain); upgradeAvatar(player, avatar, promoteData, expGain);
} }
...@@ -764,33 +695,15 @@ public class InventoryManager { ...@@ -764,33 +695,15 @@ public class InventoryManager {
return; return;
} }
// Make sure player has cost items // Pay materials and mora if possible
for (ItemParamData cost : proudSkill.getCostItems()) { List<ItemParamData> costs = new ArrayList<ItemParamData>(proudSkill.getCostItems()); // Can this be null?
if (cost.getId() == 0) { if (proudSkill.getCoinCost() > 0) {
continue; costs.add(new ItemParamData(202, proudSkill.getCoinCost()));
}
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
if (feedItem == null || feedItem.getCount() < cost.getCount()) {
return;
}
} }
if (!player.getInventory().payItems(costs.toArray(new ItemParamData[0]))) {
// Mora check
if (player.getMora() >= proudSkill.getCoinCost()) {
player.setMora(player.getMora() - proudSkill.getCoinCost());
} else {
return; return;
} }
// Consume promote filler items
for (ItemParamData cost : proudSkill.getCostItems()) {
if (cost.getId() == 0) {
continue;
}
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
player.getInventory().removeItem(feedItem, cost.getCount());
}
// Upgrade skill // Upgrade skill
avatar.getSkillLevelMap().put(skillId, nextLevel); avatar.getSkillLevelMap().put(skillId, nextLevel);
avatar.save(); avatar.save();
...@@ -822,14 +735,11 @@ public class InventoryManager { ...@@ -822,14 +735,11 @@ public class InventoryManager {
return; return;
} }
GameItem costItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(talentData.getMainCostItemId()); // Pay constellation item if possible
if (costItem == null || costItem.getCount() < talentData.getMainCostItemCount()) { if (!player.getInventory().payItem(talentData.getMainCostItemId(), 1)) {
return; return;
} }
// Consume item
player.getInventory().removeItem(costItem, talentData.getMainCostItemCount());
// Apply + recalc // Apply + recalc
avatar.getTalentIdList().add(talentData.getId()); avatar.getTalentIdList().add(talentData.getId());
avatar.setCoreProudSkillLevel(currentTalentLevel + 1); avatar.setCoreProudSkillLevel(currentTalentLevel + 1);
......
package emu.grasscutter.game.managers.MapMarkManager; package emu.grasscutter.game.managers.MapMarkManager;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import emu.grasscutter.net.proto.MapMarkFromTypeOuterClass; import emu.grasscutter.net.proto.MapMarkFromTypeOuterClass.MapMarkFromType;
import emu.grasscutter.net.proto.MapMarkPointOuterClass; import emu.grasscutter.net.proto.MapMarkPointOuterClass.MapMarkPoint;
import emu.grasscutter.net.proto.MapMarkPointTypeOuterClass; import emu.grasscutter.net.proto.MapMarkPointTypeOuterClass.MapMarkPointType;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
@Entity @Entity
public class MapMark { public class MapMark {
private int sceneId; private final int sceneId;
private String name; private final String name;
private Position position; private final Position position;
private MapMarkPointTypeOuterClass.MapMarkPointType pointType; private final MapMarkPointType pointType;
private int monsterId = 0; private final int monsterId;
private MapMarkFromTypeOuterClass.MapMarkFromType fromType; private final MapMarkFromType fromType;
private int questId = 7; private final int questId;
public MapMark(Position position, MapMarkPointTypeOuterClass.MapMarkPointType type) { public MapMark(MapMarkPoint mapMarkPoint) {
this.position = position;
}
public MapMark(MapMarkPointOuterClass.MapMarkPoint mapMarkPoint) {
this.sceneId = mapMarkPoint.getSceneId(); this.sceneId = mapMarkPoint.getSceneId();
this.name = mapMarkPoint.getName(); this.name = mapMarkPoint.getName();
this.position = new Position(mapMarkPoint.getPos().getX(), mapMarkPoint.getPos().getY(), mapMarkPoint.getPos().getZ()); this.position = new Position(
mapMarkPoint.getPos().getX(),
mapMarkPoint.getPos().getY(),
mapMarkPoint.getPos().getZ()
);
this.pointType = mapMarkPoint.getPointType(); this.pointType = mapMarkPoint.getPointType();
this.monsterId = mapMarkPoint.getMonsterId(); this.monsterId = mapMarkPoint.getMonsterId();
this.fromType = mapMarkPoint.getFromType(); this.fromType = mapMarkPoint.getFromType();
...@@ -33,41 +33,22 @@ public class MapMark { ...@@ -33,41 +33,22 @@ public class MapMark {
public int getSceneId() { public int getSceneId() {
return this.sceneId; return this.sceneId;
} }
public String getName() { public String getName() {
return this.name; return this.name;
} }
public Position getPosition() { public Position getPosition() {
return this.position; return this.position;
} }
public MapMarkPointType getMapMarkPointType() {
public MapMarkPointTypeOuterClass.MapMarkPointType getMapMarkPointType() {
return this.pointType; return this.pointType;
} }
public void setMapMarkPointType(MapMarkPointTypeOuterClass.MapMarkPointType pointType) {
this.pointType = pointType;
}
public int getMonsterId() { public int getMonsterId() {
return this.monsterId; return this.monsterId;
} }
public MapMarkFromType getMapMarkFromType() {
public void setMonsterId(int monsterId) {
this.monsterId = monsterId;
}
public MapMarkFromTypeOuterClass.MapMarkFromType getMapMarkFromType() {
return this.fromType; return this.fromType;
} }
public int getQuestId() { public int getQuestId() {
return this.questId; return this.questId;
} }
}
public void setQuestId(int questId) { \ No newline at end of file
this.questId = questId;
}
}
package emu.grasscutter.game.managers.MapMarkManager; package emu.grasscutter.game.managers.MapMarkManager;
import dev.morphia.annotations.Entity; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.MapMarkPointTypeOuterClass.MapMarkPointType;
import emu.grasscutter.net.proto.MarkMapReqOuterClass.MarkMapReq;
import emu.grasscutter.net.proto.MarkMapReqOuterClass.MarkMapReq.Operation;
import emu.grasscutter.server.packet.send.PacketMarkMapRsp;
import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import java.util.HashMap; import java.util.HashMap;
@Entity
public class MapMarksManager { public class MapMarksManager {
public static final int mapMarkMaxCount = 150;
static final int mapMarkMaxCount = 150;
private HashMap<String, MapMark> mapMarks; private HashMap<String, MapMark> mapMarks;
private final Player player;
public MapMarksManager() { public MapMarksManager(Player player) {
mapMarks = new HashMap<String, MapMark>(); this.player = player;
this.mapMarks = player.getMapMarks();
if (this.mapMarks == null) { this.mapMarks = new HashMap<>(); }
} }
public MapMarksManager(HashMap<String, MapMark> mapMarks) { public void handleMapMarkReq(MarkMapReq req) {
this.mapMarks = mapMarks; Operation op = req.getOp();
switch (op) {
case ADD -> {
MapMark createMark = new MapMark(req.getMark());
// keep teleporting functionality on fishhook mark.
if (createMark.getMapMarkPointType() == MapMarkPointType.MAP_MARK_POINT_TYPE_FISH_POOL) {
teleport(player, createMark);
return;
}
addMapMark(createMark);
}
case MOD -> {
MapMark oldMark = new MapMark(req.getOld());
removeMapMark(oldMark.getPosition());
MapMark newMark = new MapMark(req.getMark());
addMapMark(newMark);
}
case DEL -> {
MapMark deleteMark = new MapMark(req.getMark());
removeMapMark(deleteMark.getPosition());
}
}
if (op != Operation.GET) {
saveMapMarks();
}
player.getSession().send(new PacketMarkMapRsp(getMapMarks()));
} }
public HashMap<String, MapMark> getAllMapMarks() { public HashMap<String, MapMark> getMapMarks() {
return mapMarks; return mapMarks;
} }
public MapMark getMapMark(Position position) {
String key = getMapMarkKey(position);
if (mapMarks.containsKey(key)) {
return mapMarks.get(key);
} else {
return null;
}
}
public String getMapMarkKey(Position position) { public String getMapMarkKey(Position position) {
return "x" + (int)position.getX()+ "z" + (int)position.getZ(); return "x" + (int)position.getX()+ "z" + (int)position.getZ();
} }
public boolean removeMapMark(Position position) { public void removeMapMark(Position position) {
String key = getMapMarkKey(position); mapMarks.remove(getMapMarkKey(position));
if (mapMarks.containsKey(key)) {
mapMarks.remove(key);
return true;
}
return false;
} }
public boolean addMapMark(MapMark mapMark) { public void addMapMark(MapMark mapMark) {
if (mapMarks.size() < mapMarkMaxCount) { if (mapMarks.size() < mapMarkMaxCount) {
if (!mapMarks.containsKey(getMapMarkKey(mapMark.getPosition()))) { mapMarks.put(getMapMarkKey(mapMark.getPosition()), mapMark);
mapMarks.put(getMapMarkKey(mapMark.getPosition()), mapMark);
return true;
}
} }
return false;
} }
public void setMapMarks(HashMap<String, MapMark> mapMarks) { private void saveMapMarks() {
this.mapMarks = mapMarks; player.setMapMarks(mapMarks);
player.save();
} }
private void teleport(Player player, MapMark mapMark) {
float y;
try {
y = (float)Integer.parseInt(mapMark.getName());
} catch (Exception e) {
y = 300;
}
Position pos = mapMark.getPosition();
player.getPos().set(pos.getX(), y, pos.getZ());
if (mapMark.getSceneId() != player.getSceneId()) {
player.getWorld().transferPlayerToScene(player, mapMark.getSceneId(), player.getPos());
}
player.getScene().broadcastPacket(new PacketSceneEntityAppearNotify(player));
}
} }
package emu.grasscutter.game.managers; package emu.grasscutter.game.managers;
import ch.qos.logback.classic.Logger;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass; import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason;
import emu.grasscutter.net.proto.PropChangeReasonOuterClass; import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify;
import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify;
import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
...@@ -24,7 +22,9 @@ public class SotSManager { ...@@ -24,7 +22,9 @@ public class SotSManager {
// NOTE: Spring volume balance *1 = fight prop HP *100 // NOTE: Spring volume balance *1 = fight prop HP *100
private final Player player; private final Player player;
private final Logger logger = Grasscutter.getLogger();
private Timer autoRecoverTimer; private Timer autoRecoverTimer;
private final boolean enablePriorityHealing = false;
public final static int GlobalMaximumSpringVolume = 8500000; public final static int GlobalMaximumSpringVolume = 8500000;
...@@ -38,6 +38,7 @@ public class SotSManager { ...@@ -38,6 +38,7 @@ public class SotSManager {
public void setIsAutoRecoveryEnabled(boolean enabled) { public void setIsAutoRecoveryEnabled(boolean enabled) {
player.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, enabled ? 1 : 0); player.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, enabled ? 1 : 0);
player.save();
} }
public int getAutoRecoveryPercentage() { public int getAutoRecoveryPercentage() {
...@@ -46,49 +47,122 @@ public class SotSManager { ...@@ -46,49 +47,122 @@ public class SotSManager {
public void setAutoRecoveryPercentage(int percentage) { public void setAutoRecoveryPercentage(int percentage) {
player.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, percentage); player.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, percentage);
player.save();
} }
// autoRevive automatically revives all team members. public long getLastUsed() {
public void autoRevive(GameSession session) { return player.getSpringLastUsed();
player.getTeamManager().getActiveTeam().forEach(entity -> {
boolean isAlive = entity.isAlive();
float currentHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
float maxHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
// Grasscutter.getLogger().debug("" + entity.getAvatar().getAvatarData().getName() + "\t" + currentHP + "/" + maxHP + "\t" + (isAlive ? "ALIVE":"DEAD"));
float newHP = (float)(maxHP * 0.3);
if (currentHP < newHP) {
updateAvatarCurHP(session, entity, newHP);
}
if (!isAlive) {
entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar()));
}
});
} }
public void scheduleAutoRecover(GameSession session) { 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) { if (autoRecoverTimer == null) {
autoRecoverTimer = new Timer(); autoRecoverTimer = new Timer();
autoRecoverTimer.schedule(new AutoRecoverTimerTick(session), 2500); autoRecoverTimer.schedule(new AutoRecoverTimerTick(), 2500, 15000);
} }
} }
public void cancelAutoRecover() { public void handleExitTransPointRegionNotify() {
logger.trace("Player left statue region");
if (autoRecoverTimer != null) { if (autoRecoverTimer != null) {
autoRecoverTimer.cancel(); autoRecoverTimer.cancel();
autoRecoverTimer = null; autoRecoverTimer = null;
} }
} }
private class AutoRecoverTimerTick extends TimerTask // autoRevive automatically revives all team members.
{ public void autoRevive() {
private GameSession session; 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 AutoRecoverTimerTick(GameSession session) { private class AutoRecoverTimerTick extends TimerTask {
this.session = session; // autoRecover checks player setting to see if auto recover is enabled, and refill HP to the predefined level.
}
public void run() { public void run() {
autoRecover(session); refillSpringVolume();
cancelAutoRecover();
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 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_STATUE_RECOVER,
ChangeHpReason.ChangeHpAddStatue));
player.getSession().send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
}
} }
} }
...@@ -96,84 +170,23 @@ public class SotSManager { ...@@ -96,84 +170,23 @@ public class SotSManager {
// Temporary: Max spring volume depends on level of the statues in Mondstadt and Liyue. Override until we have statue level. // Temporary: Max spring volume depends on level of the statues in Mondstadt and Liyue. Override until we have statue level.
// TODO: remove // TODO: remove
// https://genshin-impact.fandom.com/wiki/Statue_of_The_Seven#:~:text=region%20of%20Inazuma.-,Statue%20Levels,-Upon%20first%20unlocking // https://genshin-impact.fandom.com/wiki/Statue_of_The_Seven#:~:text=region%20of%20Inazuma.-,Statue%20Levels,-Upon%20first%20unlocking
player.setProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME, 8500000); setMaxVolume(8500000);
// Temporary: Auto enable 100% statue recovery until we can adjust statue settings in game // Temporary: Auto enable 100% statue recovery until we can adjust statue settings in game
// TODO: remove // TODO: remove
player.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, 100); setAutoRecoveryPercentage(100);
player.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1); setIsAutoRecoveryEnabled(true);
long now = System.currentTimeMillis() / 1000;
long secondsSinceLastUsed = now - player.getSpringLastUsed();
float percentageRefilled = (float)secondsSinceLastUsed / 15 / 100; // 15s = 1% max volume
int maxVolume = player.getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME);
int currentVolume = player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME);
if (currentVolume < maxVolume) {
int volumeRefilled = (int)(percentageRefilled * maxVolume);
int newVolume = currentVolume + volumeRefilled;
if (currentVolume + volumeRefilled > maxVolume) {
newVolume = maxVolume;
}
player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, newVolume);
}
player.setSpringLastUsed(now);
player.save();
}
// autoRecover checks player setting to see if auto recover is enabled, and refill HP to the predefined level.
public void autoRecover(GameSession session) {
// TODO: In MP, respect SotS settings from the HOST.
boolean isAutoRecoveryEnabled = getIsAutoRecoveryEnabled();
int autoRecoverPercentage = getAutoRecoveryPercentage();
Grasscutter.getLogger().debug("isAutoRecoveryEnabled: " + isAutoRecoveryEnabled + "\tautoRecoverPercentage: " + autoRecoverPercentage);
if (isAutoRecoveryEnabled) { int maxVolume = getMaxVolume();
player.getTeamManager().getActiveTeam().forEach(entity -> { int currentVolume = getCurrentVolume();
float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); if (currentVolume < maxVolume) {
float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); long now = System.currentTimeMillis() / 1000;
if (currentHP == maxHP) { int secondsSinceLastUsed = (int) (now - getLastUsed());
return; // 15s = 1% max volume
} int volumeRefilled = secondsSinceLastUsed * maxVolume / 15 / 100;
float targetHP = maxHP * autoRecoverPercentage / 100; logger.trace("Statue has refilled HP volume: " + volumeRefilled);
currentVolume = Math.min(currentVolume + volumeRefilled, maxVolume);
if (targetHP > currentHP) { logger.trace("Statue remaining HP volume: " + currentVolume);
float needHP = targetHP - currentHP; setCurrentVolume(currentVolume);
float needSV = needHP * 100; // convert HP needed to Spring Volume needed
int sotsSVBalance = player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME);
if (sotsSVBalance >= needSV) {
// sufficient
sotsSVBalance -= needSV;
} else {
// insufficient balance
needSV = sotsSVBalance;
sotsSVBalance = 0;
}
player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, sotsSVBalance);
player.setSpringLastUsed(System.currentTimeMillis() / 1000);
float newHP = currentHP + needSV / 100; // convert SV to HP
updateAvatarCurHP(session, entity, newHP);
}
});
} }
} }
private void updateAvatarCurHP(GameSession session, EntityAvatar entity, float newHP) {
// TODO: Figure out why client shows current HP instead of added HP.
// Say an avatar had 12000 and now has 14000, it should show "2000".
// The client always show "+14000" which is incorrect.
entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP);
session.send(new PacketEntityFightPropChangeReasonNotify(entity, FightProperty.FIGHT_PROP_CUR_HP,
newHP, List.of(3), PropChangeReasonOuterClass.PropChangeReason.PROP_CHANGE_STATUE_RECOVER,
ChangeHpReasonOuterClass.ChangeHpReason.ChangeHpAddStatue));
session.send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
Avatar avatar = entity.getAvatar();
avatar.setCurrentHp(newHP);
session.send(new PacketAvatarFightPropUpdateNotify(avatar, FightProperty.FIGHT_PROP_CUR_HP));
player.save();
}
} }
...@@ -8,5 +8,5 @@ public interface AfterUpdateStaminaListener { ...@@ -8,5 +8,5 @@ public interface AfterUpdateStaminaListener {
* @param reason Why updating stamina. * @param reason Why updating stamina.
* @param newStamina New Stamina value. * @param newStamina New Stamina value.
*/ */
void onAfterUpdateStamina(String reason, int newStamina); void onAfterUpdateStamina(String reason, int newStamina, boolean isCharacterStamina);
} }
...@@ -8,7 +8,7 @@ public interface BeforeUpdateStaminaListener { ...@@ -8,7 +8,7 @@ public interface BeforeUpdateStaminaListener {
* @param newStamina New ABSOLUTE stamina value. * @param newStamina New ABSOLUTE stamina value.
* @return true if you want to cancel this update, otherwise false. * @return true if you want to cancel this update, otherwise false.
*/ */
int onBeforeUpdateStamina(String reason, int newStamina); int onBeforeUpdateStamina(String reason, int newStamina, boolean isCharacterStamina);
/** /**
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina. * onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
* This gives listeners a chance to intercept this update. * This gives listeners a chance to intercept this update.
...@@ -16,5 +16,5 @@ public interface BeforeUpdateStaminaListener { ...@@ -16,5 +16,5 @@ public interface BeforeUpdateStaminaListener {
* @param consumption ConsumptionType and RELATIVE stamina change amount. * @param consumption ConsumptionType and RELATIVE stamina change amount.
* @return true if you want to cancel this update, otherwise false. * @return true if you want to cancel this update, otherwise false.
*/ */
Consumption onBeforeUpdateStamina(String reason, Consumption consumption); Consumption onBeforeUpdateStamina(String reason, Consumption consumption, boolean isCharacterStamina);
} }
\ No newline at end of file
...@@ -13,18 +13,19 @@ public enum ConsumptionType { ...@@ -13,18 +13,19 @@ public enum ConsumptionType {
// Slow swimming is handled per movement, not per second. // Slow swimming is handled per movement, not per second.
// Arm movement frequency depends on gender/age/height. // Arm movement frequency depends on gender/age/height.
// TODO: Instead of cost -80 per tick, find a proper way to calculate cost. // TODO: Instead of cost -80 per tick, find a proper way to calculate cost.
SKIFF(-300), // TODO: Get real value SKIFF_DASH(-204),
SPRINT(-1800), SPRINT(-1800),
SWIM_DASH_START(-20), SWIM_DASH_START(-2000),
SWIM_DASH(-204), // -10.2 per second, 5Hz = -204 each tick SWIM_DASH(-204), // -10.2 per second, 5Hz = -204 each tick
SWIMMING(-80), SWIMMING(-80),
TALENT_DASH(-300), // -1500 per second, 5Hz = -300 each tick TALENT_DASH(-300), // -1500 per second, 5Hz = -300 each tick
TALENT_DASH_START(-1000), TALENT_DASH_START(-1000),
// restore // restore
POWERED_FLY(500), // TODO: Get real value POWERED_FLY(500),
POWERED_SKIFF(2000), // TODO: Get real value POWERED_SKIFF(500),
RUN(500), RUN(500),
SKIFF(500),
STANDBY(500), STANDBY(500),
WALK(500); WALK(500);
......
...@@ -2,6 +2,7 @@ package emu.grasscutter.game.managers.StaminaManager; ...@@ -2,6 +2,7 @@ package emu.grasscutter.game.managers.StaminaManager;
import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.Logger;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
...@@ -13,21 +14,21 @@ import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; ...@@ -13,21 +14,21 @@ import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
import emu.grasscutter.net.proto.VectorOuterClass.Vector; import emu.grasscutter.net.proto.VectorOuterClass.Vector;
import emu.grasscutter.net.proto.VehicleInteractTypeOuterClass.VehicleInteractType;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.Math;
import java.util.*; import java.util.*;
import static emu.grasscutter.Configuration.*; import static emu.grasscutter.Configuration.GAME_OPTIONS;
public class StaminaManager { public class StaminaManager {
// TODO: Skiff state detection? // TODO: Skiff state detection?
private final Player player; private final Player player;
private final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{ private static final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{
put("CLIMB", new HashSet<>(List.of( put("CLIMB", new HashSet<>(List.of(
MotionState.MOTION_CLIMB, // sustained, when not moving no cost no recover MotionState.MOTION_CLIMB, // sustained, when not moving no cost no recover
MotionState.MOTION_STANDBY_TO_CLIMB // NOT OBSERVED, see MOTION_JUMP_UP_WALL_FOR_STANDBY MotionState.MOTION_STANDBY_TO_CLIMB // NOT OBSERVED, see MOTION_JUMP_UP_WALL_FOR_STANDBY
...@@ -48,7 +49,7 @@ public class StaminaManager { ...@@ -48,7 +49,7 @@ public class StaminaManager {
))); )));
put("SKIFF", new HashSet<>(List.of( put("SKIFF", new HashSet<>(List.of(
MotionState.MOTION_SKIFF_BOARDING, // NOT OBSERVED even when boarding MotionState.MOTION_SKIFF_BOARDING, // NOT OBSERVED even when boarding
MotionState.MOTION_SKIFF_DASH, // NOT OBSERVED even when dashing MotionState.MOTION_SKIFF_DASH, // sustained, observed with waverider entity ID.
MotionState.MOTION_SKIFF_NORMAL, // sustained, OBSERVED when both normal and dashing MotionState.MOTION_SKIFF_NORMAL, // sustained, OBSERVED when both normal and dashing
MotionState.MOTION_SKIFF_POWERED_DASH // sustained, recover MotionState.MOTION_SKIFF_POWERED_DASH // sustained, recover
))); )));
...@@ -108,7 +109,8 @@ public class StaminaManager { ...@@ -108,7 +109,8 @@ public class StaminaManager {
}}; }};
private final Logger logger = Grasscutter.getLogger(); private final Logger logger = Grasscutter.getLogger();
public final static int GlobalMaximumStamina = 24000; public final static int GlobalCharacterMaximumStamina = 24000;
public final static int GlobalVehicleMaxStamina = 24000;
private Position currentCoordinates = new Position(0, 0, 0); private Position currentCoordinates = new Position(0, 0, 0);
private Position previousCoordinates = new Position(0, 0, 0); private Position previousCoordinates = new Position(0, 0, 0);
private MotionState currentState = MotionState.MOTION_STANDBY; private MotionState currentState = MotionState.MOTION_STANDBY;
...@@ -122,74 +124,58 @@ public class StaminaManager { ...@@ -122,74 +124,58 @@ public class StaminaManager {
private int lastSkillId = 0; private int lastSkillId = 0;
private int lastSkillCasterId = 0; private int lastSkillCasterId = 0;
private boolean lastSkillFirstTick = true; private boolean lastSkillFirstTick = true;
public static final HashSet<Integer> TalentMovements = new HashSet<>(List.of( private int vehicleId = -1;
10013, // Kamisato Ayaka private int vehicleStamina = GlobalVehicleMaxStamina;
10413 // Mona private static final HashSet<Integer> TalentMovements = new HashSet<>(List.of(
)); 10013, 10413
// TODO: Get from somewhere else, instead of hard-coded here?
public static final HashSet<Integer> ClaymoreSkills = new HashSet<>(List.of(
10160, // Diluc, /=2
10201, // Razor
10241, // Beidou
10341, // Noelle
10401, // Chongyun
10441, // Xinyan
10511, // Eula
10531, // Sayu
10571 // Arataki Itto, = 0
));
public static final HashSet<Integer> CatalystSkills = new HashSet<>(List.of(
10060, // Lisa
10070, // Barbara
10271, // Ningguang
10291, // Klee
10411, // Mona
10431, // Sucrose
10481, // Yanfei
10541, // Sangonomoiya Kokomi
10581 // Yae Miko
));
public static final HashSet<Integer> PolearmSkills = new HashSet<>(List.of(
10231, // Xiangling
10261, // Xiao
10301, // Zhongli
10451, // Rosaria
10461, // Hu Tao
10501, // Thoma
10521, // Raiden Shogun
10631, // Shenhe
10641 // Yunjin
));
public static final HashSet<Integer> SwordSkills = new HashSet<>(List.of(
10024, // Kamisato Ayaka
10031, // Jean
10073, // Kaeya
10321, // Bennett
10337, // Tartaglia, melee stance (10332 switch to melee, 10336 switch to ranged stance)
10351, // Qiqi
10381, // Xingqiu
10386, // Albedo
10421, // Keqing, =-2500
10471, // Kaedehara Kazuha
10661, // Kamisato Ayato
100553, // Lumine
100540 // Aether
));
public static final HashSet<Integer> BowSkills = new HashSet<>(List.of(
10041, 10043, // Amber
10221, 10223,// Venti
10311, 10315, // Fischl
10331, 10335, // Tartaglia, ranged stance
10371, // Ganyu
10391, 10394, // Diona
10491, // Yoimiya
10551, 10554, // Gorou
10561, 10564, // Kojou Sara
10621, // Aloy
99998, 99999 // Yelan // TODO: get real values
)); ));
private static final HashMap<Integer, Float> ClimbFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> DashFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> FlyFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> SwimFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> ClimbTalentReductionMap = new HashMap<>() {{
put(262301, 0.8f);
}};
private static final HashMap<Integer, Float> FlyTalentReductionMap = new HashMap<>() {{
put(212301, 0.8f);
put(222301, 0.8f);
}};
private static final HashMap<Integer, Float> SwimTalentReductionMap = new HashMap<>() {{
put(242301, 0.8f);
put(542301, 0.8f);
}};
public static final HashSet<Integer> BowAvatars = new HashSet<>();
public static final HashSet<Integer> CatalystAvatars = new HashSet<>();
public static final HashSet<Integer> ClaymoreAvatars = new HashSet<>();
public static final HashSet<Integer> PolearmAvatars = new HashSet<>();
public static final HashSet<Integer> SwordAvatars = new HashSet<>();
public static void initialize() {
// Initialize skill categories
GameData.getAvatarDataMap().forEach((avatarId, avatarData) -> {
switch (avatarData.getWeaponType()) {
case "WEAPON_BOW" -> BowAvatars.add(avatarId);
case "WEAPON_CLAYMORE" -> ClaymoreAvatars.add(avatarId);
case "WEAPON_CATALYST" -> CatalystAvatars.add(avatarId);
case "WEAPON_POLE" -> PolearmAvatars.add(avatarId);
case "WEAPON_SWORD_ONE_HAND" -> SwordAvatars.add(avatarId);
}
});
// TODO: Initialize foods etc.
}
public StaminaManager(Player player) { public StaminaManager(Player player) {
this.player = player; this.player = player;
...@@ -203,6 +189,22 @@ public class StaminaManager { ...@@ -203,6 +189,22 @@ public class StaminaManager {
lastSkillCasterId = skillCasterId; lastSkillCasterId = skillCasterId;
} }
public int getMaxCharacterStamina() {
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
}
public int getCurrentCharacterStamina() {
return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
}
public int getMaxVehicleStamina() {
return GlobalVehicleMaxStamina;
}
public int getCurrentVehicleStamina() {
return vehicleStamina;
}
public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) { public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
if (beforeUpdateStaminaListeners.containsKey(listenerName)) { if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
return false; return false;
...@@ -244,67 +246,71 @@ public class StaminaManager { ...@@ -244,67 +246,71 @@ public class StaminaManager {
return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3; return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
} }
public int updateStaminaRelative(GameSession session, Consumption consumption) { public int updateStaminaRelative(GameSession session, Consumption consumption, boolean isCharacterStamina) {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentStamina = isCharacterStamina ? getCurrentCharacterStamina() : getCurrentVehicleStamina();
if (consumption.amount == 0) { if (consumption.amount == 0) {
return currentStamina; return currentStamina;
} }
// notify will update // notify will update
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption); Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption, isCharacterStamina);
if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) { if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) {
logger.debug("[StaminaManager] Stamina update relative(" + logger.debug("Stamina update relative(" +
consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" + consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" +
consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey()); consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey());
return currentStamina; return currentStamina;
} }
} }
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int maxStamina = isCharacterStamina ? getMaxCharacterStamina() : getMaxVehicleStamina();
logger.trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + logger.trace((isCharacterStamina ? "C " : "V ") + currentStamina + "/" + maxStamina + "\t" + currentState + "\t" +
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," + (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," +
consumption.amount + ")"); consumption.amount + ")");
int newStamina = currentStamina + consumption.amount; int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) { if (newStamina < 0) {
newStamina = 0; newStamina = 0;
} else if (newStamina > playerMaxStamina) { } else if (newStamina > maxStamina) {
newStamina = playerMaxStamina; newStamina = maxStamina;
} }
return setStamina(session, consumption.type.toString(), newStamina); return setStamina(session, consumption.type.toString(), newStamina, isCharacterStamina);
} }
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) { public int updateStaminaAbsolute(GameSession session, String reason, int newStamina, boolean isCharacterStamina) {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentStamina = isCharacterStamina ? getCurrentCharacterStamina() : getCurrentVehicleStamina();
// notify will update // notify will update
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina); int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina, isCharacterStamina);
if (overriddenNewStamina != newStamina) { if (overriddenNewStamina != newStamina) {
logger.debug("[StaminaManager] Stamina update absolute(" + logger.debug("Stamina update absolute(" +
reason + ", " + newStamina + ") overridden to absolute(" + reason + ", " + newStamina + ") overridden to absolute(" +
reason + ", " + newStamina + ") by: " + listener.getKey()); reason + ", " + newStamina + ") by: " + listener.getKey());
return currentStamina; return currentStamina;
} }
} }
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int maxStamina = isCharacterStamina ? getMaxCharacterStamina() : getMaxVehicleStamina();
if (newStamina < 0) { if (newStamina < 0) {
newStamina = 0; newStamina = 0;
} else if (newStamina > playerMaxStamina) { } else if (newStamina > maxStamina) {
newStamina = playerMaxStamina; newStamina = maxStamina;
} }
return setStamina(session, reason, newStamina); return setStamina(session, reason, newStamina, isCharacterStamina);
} }
// Returns new stamina and sends PlayerPropNotify // Returns new stamina and sends PlayerPropNotify or VehicleStaminaNotify
public int setStamina(GameSession session, String reason, int newStamina) { public int setStamina(GameSession session, String reason, int newStamina, boolean isCharacterStamina) {
if (!GAME_OPTIONS.staminaUsage) { if (!GAME_OPTIONS.staminaUsage) {
newStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); newStamina = getMaxCharacterStamina();
}
// set stamina if is character stamina
if (isCharacterStamina) {
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
} else {
vehicleStamina = newStamina;
session.send(new PacketVehicleStaminaNotify(vehicleId, ((float) newStamina) / 100));
} }
// set stamina
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
// notify updated // notify updated
for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) { for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) {
listener.getValue().onAfterUpdateStamina(reason, newStamina); listener.getValue().onAfterUpdateStamina(reason, newStamina, isCharacterStamina);
} }
return newStamina; return newStamina;
} }
...@@ -343,22 +349,23 @@ public class StaminaManager { ...@@ -343,22 +349,23 @@ public class StaminaManager {
// External trigger handler // External trigger handler
public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) { public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) {
// Ignore if skill not cast by not current active // Ignore if skill not cast by not current active avatar
if (casterId != player.getTeamManager().getCurrentAvatarEntity().getId()) { if (casterId != player.getTeamManager().getCurrentAvatarEntity().getId()) {
return; return;
} }
setSkillCast(skillId, casterId); setSkillCast(skillId, casterId);
// Handle immediate stamina cost // Handle immediate stamina cost
if (ClaymoreSkills.contains(skillId)) { int currentAvatarId = player.getTeamManager().getCurrentAvatarEntity().getAvatar().getAvatarId();
if (ClaymoreAvatars.contains(currentAvatarId)) {
// Exclude claymore as their stamina cost starts when MixinStaminaCost gets in // Exclude claymore as their stamina cost starts when MixinStaminaCost gets in
return; return;
} }
// TODO: Differentiate normal attacks from charged attacks and exclude // TODO: Differentiate normal attacks from charged attacks and exclude
// TODO: Temporary: Exclude non-claymore attacks for now // TODO: Temporary: Exclude non-claymore attacks for now
if (BowSkills.contains(skillId) if (BowAvatars.contains(currentAvatarId)
|| SwordSkills.contains(skillId) || SwordAvatars.contains(currentAvatarId)
|| PolearmSkills.contains(skillId) || PolearmAvatars.contains(currentAvatarId)
|| CatalystSkills.contains(skillId) || CatalystAvatars.contains(currentAvatarId)
) { ) {
return; return;
} }
...@@ -367,7 +374,7 @@ public class StaminaManager { ...@@ -367,7 +374,7 @@ public class StaminaManager {
public void handleMixinCostStamina(boolean isSwim) { public void handleMixinCostStamina(boolean isSwim) {
// Talent moving and claymore avatar charged attack duration // Talent moving and claymore avatar charged attack duration
// logger.trace("abilityMixinCostStamina: isSwim: " + isSwim); // logger.trace("abilityMixinCostStamina: isSwim: " + isSwim + "\tlastSkill: " + lastSkillId);
if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) { if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) {
handleImmediateStamina(cachedSession, lastSkillId); handleImmediateStamina(cachedSession, lastSkillId);
} }
...@@ -381,11 +388,11 @@ public class StaminaManager { ...@@ -381,11 +388,11 @@ public class StaminaManager {
MotionState motionState = motionInfo.getState(); MotionState motionState = motionInfo.getState();
int notifyEntityId = entity.getId(); int notifyEntityId = entity.getId();
int currentAvatarEntityId = session.getPlayer().getTeamManager().getCurrentAvatarEntity().getId(); int currentAvatarEntityId = session.getPlayer().getTeamManager().getCurrentAvatarEntity().getId();
if (notifyEntityId != currentAvatarEntityId) { if (notifyEntityId != currentAvatarEntityId && notifyEntityId != vehicleId) {
return; return;
} }
currentState = motionState; currentState = motionState;
// logger.trace("" + currentState); // logger.trace(currentState + "\t" + (notifyEntityId == currentAvatarEntityId ? "character" : "vehicle"));
Vector posVector = motionInfo.getPos(); Vector posVector = motionInfo.getPos();
Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ());
if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
...@@ -395,28 +402,40 @@ public class StaminaManager { ...@@ -395,28 +402,40 @@ public class StaminaManager {
handleImmediateStamina(session, motionState); handleImmediateStamina(session, motionState);
} }
public void handleVehicleInteractReq(GameSession session, int vehicleId, VehicleInteractType vehicleInteractType) {
if (vehicleInteractType == VehicleInteractType.VEHICLE_INTERACT_IN) {
this.vehicleId = vehicleId;
// Reset character stamina here to prevent falling into water immediately on ejection if char stamina is
// close to empty when boarding.
updateStaminaAbsolute(session, "board vehicle", getMaxCharacterStamina(), true);
updateStaminaAbsolute(session, "board vehicle", getMaxVehicleStamina(), false);
} else {
this.vehicleId = -1;
}
}
// Internal handler // Internal handler
private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) { private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) {
switch (motionState) { switch (motionState) {
case MOTION_CLIMB: case MOTION_CLIMB:
if (currentState != MotionState.MOTION_CLIMB) { if (currentState != MotionState.MOTION_CLIMB) {
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START)); updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START), true);
} }
break; break;
case MOTION_DASH_BEFORE_SHAKE: case MOTION_DASH_BEFORE_SHAKE:
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) { if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT)); updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT), true);
} }
break; break;
case MOTION_CLIMB_JUMP: case MOTION_CLIMB_JUMP:
if (previousState != MotionState.MOTION_CLIMB_JUMP) { if (previousState != MotionState.MOTION_CLIMB_JUMP) {
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP)); updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP), true);
} }
break; break;
case MOTION_SWIM_DASH: case MOTION_SWIM_DASH:
if (previousState != MotionState.MOTION_SWIM_DASH) { if (previousState != MotionState.MOTION_SWIM_DASH) {
updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START)); updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START), true);
} }
break; break;
} }
...@@ -424,18 +443,20 @@ public class StaminaManager { ...@@ -424,18 +443,20 @@ public class StaminaManager {
private void handleImmediateStamina(GameSession session, int skillId) { private void handleImmediateStamina(GameSession session, int skillId) {
Consumption consumption = getFightConsumption(skillId); Consumption consumption = getFightConsumption(skillId);
updateStaminaRelative(session, consumption); updateStaminaRelative(session, consumption, true);
} }
private class SustainedStaminaHandler extends TimerTask { private class SustainedStaminaHandler extends TimerTask {
public void run() { public void run() {
boolean moving = isPlayerMoving(); boolean moving = isPlayerMoving();
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentCharacterStamina = getCurrentCharacterStamina();
int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int maxCharacterStamina = getMaxCharacterStamina();
if (moving || (currentStamina < maxStamina)) { int currentVehicleStamina = getCurrentVehicleStamina();
int maxVehicleStamina = getMaxVehicleStamina();
if (moving || (currentCharacterStamina < maxCharacterStamina) || (currentVehicleStamina < maxVehicleStamina)) {
logger.trace("Player moving: " + moving + ", stamina full: " + logger.trace("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina"); (currentCharacterStamina >= maxCharacterStamina) + ", recalculate stamina");
boolean isCharacterStamina = true;
Consumption consumption; Consumption consumption;
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbConsumption(); consumption = getClimbConsumption();
...@@ -447,43 +468,44 @@ public class StaminaManager { ...@@ -447,43 +468,44 @@ public class StaminaManager {
consumption = new Consumption(ConsumptionType.RUN); consumption = new Consumption(ConsumptionType.RUN);
} else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) { } else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) {
consumption = getSkiffConsumption(); consumption = getSkiffConsumption();
isCharacterStamina = false;
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = new Consumption(ConsumptionType.STANDBY); consumption = new Consumption(ConsumptionType.STANDBY);
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { } else if (MotionStatesCategorized.get("SWIM").contains(currentState)) {
consumption = getSwimConsumptions(); consumption = getSwimConsumptions();
} else if (MotionStatesCategorized.get("WALK").contains((currentState))) { } else if (MotionStatesCategorized.get("WALK").contains(currentState)) {
consumption = new Consumption(ConsumptionType.WALK); consumption = new Consumption(ConsumptionType.WALK);
} else if (MotionStatesCategorized.get("OTHER").contains((currentState))) { } else if (MotionStatesCategorized.get("NOCOST_NORECOVER").contains(currentState)) {
consumption = new Consumption();
} else if (MotionStatesCategorized.get("OTHER").contains(currentState)) {
consumption = getOtherConsumptions(); consumption = getOtherConsumptions();
} else { } else { // ignore
// ignore
return; return;
} }
if (consumption.amount < 0) {
/* Do not apply reduction factor when recovering stamina if (consumption.amount < 0 && isCharacterStamina) {
TODO: Reductions that apply to all motion types: // Do not apply reduction factor when recovering stamina
Elemental Resonance if (player.getTeamManager().getTeamResonances().contains(10301)) {
Wind: -15% consumption.amount *= 0.85f;
Skills }
Diona E: -10% while shield lasts - applies to SP+MP
Barbara E: -12% while lasts - applies to SP+MP
*/
} }
// Delay 2 seconds before starts recovering stamina // Delay 1 seconds before starts recovering stamina
if (cachedSession != null) { if (consumption.amount != 0 && cachedSession != null) {
if (consumption.amount < 0) { if (consumption.amount < 0) {
staminaRecoverDelay = 0; staminaRecoverDelay = 0;
} }
if (consumption.amount > 0 && consumption.type != ConsumptionType.POWERED_FLY) { if (consumption.amount > 0
// For POWERED_FLY recover immediately - things like Amber's gliding exam may require this. && consumption.type != ConsumptionType.POWERED_FLY
if (staminaRecoverDelay < 10) { && consumption.type != ConsumptionType.POWERED_SKIFF) {
// For others recover after 2 seconds (10 ticks) - as official server does. // For POWERED_* recover immediately - things like Amber's gliding exam and skiff challenges may require this.
if (staminaRecoverDelay < 5) {
// For others recover after 1 seconds (5 ticks) - as official server does.
staminaRecoverDelay++; staminaRecoverDelay++;
consumption.amount = 0; consumption.amount = 0;
logger.trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay); logger.trace("Delaying recovery: " + staminaRecoverDelay);
} }
} }
updateStaminaRelative(cachedSession, consumption); updateStaminaRelative(cachedSession, consumption, isCharacterStamina);
} }
} }
previousState = currentState; previousState = currentState;
...@@ -496,10 +518,11 @@ public class StaminaManager { ...@@ -496,10 +518,11 @@ public class StaminaManager {
} }
private void handleDrowning() { private void handleDrowning() {
int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); // TODO: fix drowning waverider entity
int stamina = getCurrentCharacterStamina();
if (stamina < 10) { if (stamina < 10) {
logger.trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + logger.trace(getCurrentCharacterStamina() + "/" +
player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState); getMaxCharacterStamina() + "\t" + currentState);
if (currentState != MotionState.MOTION_SWIM_IDLE) { if (currentState != MotionState.MOTION_SWIM_IDLE) {
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN); killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
} }
...@@ -517,24 +540,25 @@ public class StaminaManager { ...@@ -517,24 +540,25 @@ public class StaminaManager {
return getTalentMovingSustainedCost(skillCasting); return getTalentMovingSustainedCost(skillCasting);
} }
// Bow avatar charged attack // Bow avatar charged attack
if (BowSkills.contains(skillCasting)) { int currentAvatarId = player.getTeamManager().getCurrentAvatarEntity().getAvatar().getAvatarId();
if (BowAvatars.contains(currentAvatarId)) {
return getBowSustainedCost(skillCasting); return getBowSustainedCost(skillCasting);
} }
// Claymore avatar charged attack // Claymore avatar charged attack
if (ClaymoreSkills.contains(skillCasting)) { if (ClaymoreAvatars.contains(currentAvatarId)) {
return getClaymoreSustainedCost(skillCasting); return getClaymoreSustainedCost(skillCasting);
} }
// Catalyst avatar charged attack // Catalyst avatar charged attack
if (CatalystSkills.contains(skillCasting)) { if (CatalystAvatars.contains(currentAvatarId)) {
return getCatalystSustainedCost(skillCasting); return getCatalystCost(skillCasting);
} }
// Polearm avatar charged attack // Polearm avatar charged attack
if (PolearmSkills.contains(skillCasting)) { if (PolearmAvatars.contains(currentAvatarId)) {
return getPolearmSustainedCost(skillCasting); return getPolearmCost(skillCasting);
} }
// Sword avatar charged attack // Sword avatar charged attack
if (SwordSkills.contains(skillCasting)) { if (SwordAvatars.contains(skillCasting)) {
return getSwordSustainedCost(skillCasting); return getSwordCost(skillCasting);
} }
return new Consumption(); return new Consumption();
} }
...@@ -546,18 +570,8 @@ public class StaminaManager { ...@@ -546,18 +570,8 @@ public class StaminaManager {
consumption.amount = ConsumptionType.CLIMBING.amount; consumption.amount = ConsumptionType.CLIMBING.amount;
} }
// Climbing specific reductions // Climbing specific reductions
// TODO: create a food cost reduction map consumption.amount *= getFoodCostReductionFactor(ClimbFoodReductionMap);
HashMap<Integer, Float> foodReductionMap = new HashMap<>() {{ consumption.amount *= getTalentCostReductionFactor(ClimbTalentReductionMap);
// TODO: get real talent id
put(0, 0.8f); // Sample food
}};
consumption.amount *= getFoodCostReductionFactor(foodReductionMap);
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{
// TODO: get real talent id
put(0, 0.8f); // Xiao
}};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
return consumption; return consumption;
} }
...@@ -572,13 +586,9 @@ public class StaminaManager { ...@@ -572,13 +586,9 @@ public class StaminaManager {
consumption.type = ConsumptionType.SWIM_DASH; consumption.type = ConsumptionType.SWIM_DASH;
consumption.amount = ConsumptionType.SWIM_DASH.amount; consumption.amount = ConsumptionType.SWIM_DASH.amount;
} }
// Reductions // Swimming specific reductions
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{ consumption.amount *= getFoodCostReductionFactor(SwimFoodReductionMap);
// TODO: get real talent id consumption.amount *= getTalentCostReductionFactor(SwimTalentReductionMap);
put(0, 0.8f); // Beidou
put(1, 0.8f); // Sangonomiya Kokomi
}};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
return consumption; return consumption;
} }
...@@ -587,8 +597,8 @@ public class StaminaManager { ...@@ -587,8 +597,8 @@ public class StaminaManager {
if (currentState == MotionState.MOTION_DASH) { if (currentState == MotionState.MOTION_DASH) {
consumption.type = ConsumptionType.DASH; consumption.type = ConsumptionType.DASH;
consumption.amount = ConsumptionType.DASH.amount; consumption.amount = ConsumptionType.DASH.amount;
// TODO: Dashing specific reductions // Dashing specific reductions
// Foods: consumption.amount *= getFoodCostReductionFactor(DashFoodReductionMap);
} }
return consumption; return consumption;
} }
...@@ -599,32 +609,34 @@ public class StaminaManager { ...@@ -599,32 +609,34 @@ public class StaminaManager {
return new Consumption(ConsumptionType.POWERED_FLY); return new Consumption(ConsumptionType.POWERED_FLY);
} }
Consumption consumption = new Consumption(ConsumptionType.FLY); Consumption consumption = new Consumption(ConsumptionType.FLY);
// Passive Talents // Flying specific reductions
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{ consumption.amount *= getFoodCostReductionFactor(FlyFoodReductionMap);
put(212301, 0.8f); // Amber consumption.amount *= getTalentCostReductionFactor(FlyTalentReductionMap);
put(222301, 0.8f); // Venti
}};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
// TODO: Foods
return consumption; return consumption;
} }
private Consumption getSkiffConsumption() { private Consumption getSkiffConsumption() {
// POWERED_SKIFF, e.g. wind tunnel
if (currentState == MotionState.MOTION_SKIFF_POWERED_DASH) {
return new Consumption(ConsumptionType.POWERED_SKIFF);
}
// No known reduction for skiffing. // No known reduction for skiffing.
return new Consumption(ConsumptionType.SKIFF); return switch (currentState) {
case MOTION_SKIFF_DASH -> new Consumption(ConsumptionType.SKIFF_DASH);
case MOTION_SKIFF_POWERED_DASH -> new Consumption(ConsumptionType.POWERED_SKIFF);
case MOTION_SKIFF_NORMAL -> new Consumption(ConsumptionType.SKIFF);
default -> new Consumption();
};
} }
private Consumption getOtherConsumptions() { private Consumption getOtherConsumptions() {
if (currentState == MotionState.MOTION_NOTIFY) { switch (currentState) {
if (BowSkills.contains(lastSkillId)) { case MOTION_NOTIFY:
// if (BowSkills.contains(lastSkillId)) {
// return new Consumption(ConsumptionType.FIGHT, 500);
// }
break;
case MOTION_FIGHT:
// TODO: what if charged attack
return new Consumption(ConsumptionType.FIGHT, 500); return new Consumption(ConsumptionType.FIGHT, 500);
}
} }
// TODO: Add other logic
return new Consumption(); return new Consumption();
} }
...@@ -671,11 +683,11 @@ public class StaminaManager { ...@@ -671,11 +683,11 @@ public class StaminaManager {
return new Consumption(ConsumptionType.FIGHT, +500); return new Consumption(ConsumptionType.FIGHT, +500);
} }
private Consumption getCatalystSustainedCost(int skillId) { private Consumption getCatalystCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -5000); Consumption consumption = new Consumption(ConsumptionType.FIGHT, -5000);
// Character specific handling // Character specific handling
switch (skillId) { switch (skillId) {
// TODO: Yanfei // TODO:
} }
return consumption; return consumption;
} }
...@@ -684,18 +696,20 @@ public class StaminaManager { ...@@ -684,18 +696,20 @@ public class StaminaManager {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -1333); // 4000 / 3 = 1333 Consumption consumption = new Consumption(ConsumptionType.FIGHT, -1333); // 4000 / 3 = 1333
// Character specific handling // Character specific handling
switch (skillId) { switch (skillId) {
case 10571: // Arataki Itto, does not consume stamina at all. case 10571:
case 10532:
consumption.amount = 0; consumption.amount = 0;
break; break;
case 10160: // Diluc, with talent "Relentless" stamina cost is decreased by 50% case 10160:
// TODO: How to get talent status? if (player.getTeamManager().getCurrentAvatarEntity().getAvatar().getProudSkillList().contains(162101)) {
consumption.amount /= 2; consumption.amount /= 2;
}
break; break;
} }
return consumption; return consumption;
} }
private Consumption getPolearmSustainedCost(int skillId) { private Consumption getPolearmCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2500); Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2500);
// Character specific handling // Character specific handling
switch (skillId) { switch (skillId) {
...@@ -704,11 +718,11 @@ public class StaminaManager { ...@@ -704,11 +718,11 @@ public class StaminaManager {
return consumption; return consumption;
} }
private Consumption getSwordSustainedCost(int skillId) { private Consumption getSwordCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000); Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000);
// Character specific handling // Character specific handling
switch (skillId) { switch (skillId) {
case 10421: // Keqing, -2500 case 10421:
consumption.amount = -2500; consumption.amount = -2500;
break; break;
} }
......
...@@ -29,6 +29,9 @@ import emu.grasscutter.game.props.ActionReason; ...@@ -29,6 +29,9 @@ import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.EntityType;
import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.game.props.SceneType; import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.game.quest.GameMainQuest;
import emu.grasscutter.game.quest.GameQuest;
import emu.grasscutter.game.quest.QuestManager;
import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.shop.ShopLimit;
import emu.grasscutter.game.managers.MapMarkManager.*; import emu.grasscutter.game.managers.MapMarkManager.*;
import emu.grasscutter.game.tower.TowerManager; import emu.grasscutter.game.tower.TowerManager;
...@@ -82,6 +85,11 @@ public class Player { ...@@ -82,6 +85,11 @@ public class Player {
private Set<Integer> flyCloakList; private Set<Integer> flyCloakList;
private Set<Integer> costumeList; private Set<Integer> costumeList;
private Integer widgetId;
private Set<Integer> realmList;
private Integer currentRealmId;
@Transient private long nextGuid = 0; @Transient private long nextGuid = 0;
@Transient private int peerId; @Transient private int peerId;
@Transient private World world; @Transient private World world;
...@@ -93,6 +101,7 @@ public class Player { ...@@ -93,6 +101,7 @@ public class Player {
@Transient private MailHandler mailHandler; @Transient private MailHandler mailHandler;
@Transient private MessageHandler messageHandler; @Transient private MessageHandler messageHandler;
@Transient private AbilityManager abilityManager; @Transient private AbilityManager abilityManager;
@Transient private QuestManager questManager;
@Transient private SotSManager sotsManager; @Transient private SotSManager sotsManager;
...@@ -132,10 +141,11 @@ public class Player { ...@@ -132,10 +141,11 @@ public class Player {
@Transient private final InvokeHandler<AbilityInvokeEntry> abilityInvokeHandler; @Transient private final InvokeHandler<AbilityInvokeEntry> abilityInvokeHandler;
@Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler; @Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler;
private MapMarksManager mapMarksManager; @Transient private MapMarksManager mapMarksManager;
@Transient private StaminaManager staminaManager; @Transient private StaminaManager staminaManager;
private long springLastUsed; private long springLastUsed;
private HashMap<String, MapMark> mapMarks;
@Deprecated @Deprecated
...@@ -147,6 +157,7 @@ public class Player { ...@@ -147,6 +157,7 @@ public class Player {
this.mailHandler = new MailHandler(this); this.mailHandler = new MailHandler(this);
this.towerManager = new TowerManager(this); this.towerManager = new TowerManager(this);
this.abilityManager = new AbilityManager(this); this.abilityManager = new AbilityManager(this);
this.setQuestManager(new QuestManager(this));
this.pos = new Position(); this.pos = new Position();
this.rotation = new Position(); this.rotation = new Position();
this.properties = new HashMap<>(); this.properties = new HashMap<>();
...@@ -179,7 +190,7 @@ public class Player { ...@@ -179,7 +190,7 @@ public class Player {
this.shopLimit = new ArrayList<>(); this.shopLimit = new ArrayList<>();
this.expeditionInfo = new HashMap<>(); this.expeditionInfo = new HashMap<>();
this.messageHandler = null; this.messageHandler = null;
this.mapMarksManager = new MapMarksManager(); this.mapMarksManager = new MapMarksManager(this);
this.staminaManager = new StaminaManager(this); this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this); this.sotsManager = new SotSManager(this);
} }
...@@ -207,7 +218,7 @@ public class Player { ...@@ -207,7 +218,7 @@ public class Player {
this.getPos().set(GameConstants.START_POSITION); this.getPos().set(GameConstants.START_POSITION);
this.getRotation().set(0, 307, 0); this.getRotation().set(0, 307, 0);
this.messageHandler = null; this.messageHandler = null;
this.mapMarksManager = new MapMarksManager(); this.mapMarksManager = new MapMarksManager(this);
this.staminaManager = new StaminaManager(this); this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this); this.sotsManager = new SotSManager(this);
} }
...@@ -297,6 +308,39 @@ public class Player { ...@@ -297,6 +308,39 @@ public class Player {
this.updateProfile(); this.updateProfile();
} }
public Integer getWidgetId() {
return widgetId;
}
public void setWidgetId(Integer widgetId) {
this.widgetId = widgetId;
}
public Set<Integer> getRealmList() {
return realmList;
}
public void setRealmList(Set<Integer> realmList) {
this.realmList = realmList;
}
public void addRealmList(int realmId) {
if (this.realmList == null) {
this.realmList = new HashSet<>();
} else if (this.realmList.contains(realmId)) {
return;
}
this.realmList.add(realmId);
}
public Integer getCurrentRealmId() {
return currentRealmId;
}
public void setCurrentRealmId(Integer currentRealmId) {
this.currentRealmId = currentRealmId;
}
public Position getPos() { public Position getPos() {
return pos; return pos;
} }
...@@ -411,6 +455,14 @@ public class Player { ...@@ -411,6 +455,14 @@ public class Player {
return towerManager; return towerManager;
} }
public QuestManager getQuestManager() {
return questManager;
}
public void setQuestManager(QuestManager questManager) {
this.questManager = questManager;
}
public PlayerGachaInfo getGachaInfo() { public PlayerGachaInfo getGachaInfo() {
return gachaInfo; return gachaInfo;
} }
...@@ -885,9 +937,7 @@ public class Player { ...@@ -885,9 +937,7 @@ public class Player {
} }
public void sendPacket(BasePacket packet) { public void sendPacket(BasePacket packet) {
if (this.hasSentAvatarDataNotify) { this.getSession().send(packet);
this.getSession().send(packet);
}
} }
public OnlinePlayerInfo getOnlinePlayerInfo() { public OnlinePlayerInfo getOnlinePlayerInfo() {
...@@ -1034,6 +1084,10 @@ public class Player { ...@@ -1034,6 +1084,10 @@ public class Player {
return abilityManager; return abilityManager;
} }
public HashMap<String, MapMark> getMapMarks() { return mapMarks; }
public void setMapMarks(HashMap<String, MapMark> newMarks) { mapMarks = newMarks; }
public synchronized void onTick() { public synchronized void onTick() {
// Check ping // Check ping
if (this.getLastPingTime() > System.currentTimeMillis() + 60000) { if (this.getLastPingTime() > System.currentTimeMillis() + 60000) {
...@@ -1120,7 +1174,23 @@ public class Player { ...@@ -1120,7 +1174,23 @@ public class Player {
this.getFriendsList().loadFromDatabase(); this.getFriendsList().loadFromDatabase();
this.getMailHandler().loadFromDatabase(); this.getMailHandler().loadFromDatabase();
this.getQuestManager().loadFromDatabase();
// Quest - Commented out because a problem is caused if you log out while this quest is active
/*
if (getQuestManager().getMainQuestById(351) == null) {
GameQuest quest = getQuestManager().addQuest(35104);
if (quest != null) {
quest.finish();
}
getQuestManager().addQuest(35101);
this.setSceneId(3);
this.getPos().set(GameConstants.START_POSITION);
}
*/
// Create world // Create world
World world = new World(this); World world = new World(this);
world.addPlayer(this); world.addPlayer(this);
...@@ -1140,6 +1210,14 @@ public class Player { ...@@ -1140,6 +1210,14 @@ public class Player {
session.send(new PacketStoreWeightLimitNotify()); session.send(new PacketStoreWeightLimitNotify());
session.send(new PacketPlayerStoreNotify(this)); session.send(new PacketPlayerStoreNotify(this));
session.send(new PacketAvatarDataNotify(this)); session.send(new PacketAvatarDataNotify(this));
session.send(new PacketFinishedParentQuestNotify(this));
session.send(new PacketQuestListNotify(this));
session.send(new PacketCodexDataFullNotify(this));
session.send(new PacketServerCondMeetQuestListUpdateNotify(this));
session.send(new PacketAllWidgetDataNotify(this));
session.send(new PacketWidgetGadgetAllDataNotify());
session.send(new PacketPlayerHomeCompInfoNotify(this));
session.send(new PacketHomeComfortInfoNotify(this));
getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward. getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward.
...@@ -1237,7 +1315,7 @@ public class Player { ...@@ -1237,7 +1315,7 @@ public class Player {
} else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009 } else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009
if (!(0 <= value && value <= 1)) { return false; } if (!(0 <= value && value <= 1)) { return false; }
} else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010 } else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010
if (!(value >= 0 && value <= StaminaManager.GlobalMaximumStamina)) { return false; } if (!(value >= 0 && value <= StaminaManager.GlobalCharacterMaximumStamina)) { return false; }
} else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011 } else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011
int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA); int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (!(value >= 0 && value <= playerMaximumStamina)) { return false; } if (!(value >= 0 && value <= playerMaximumStamina)) { return false; }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment