Compare commits

...

4 Commits

Author SHA1 Message Date
saucebing
f10dc1cf8d
fix healing ability of some characters to some extent (#1201)
* fix healing ability of some characters to some extent

* using SerialName to replace replaceAll in avatar ability files reading

* add class HealAbilityManager

* move codes in onAbilityInvoke of class AbilityManager to class HealAbilityManager
2022-06-14 04:36:53 -07:00
Theosapira
64d74387a2
Minor README update (#1246)
* Update README.md

Updated old jar link with new jar from actions

* Update README_zh-CN.md

Updated old jar link with new jar from actions
2022-06-14 01:15:03 -07:00
Tesutarin
f2f6993b3d KickCommand: Remove unnecessary subpermission 2022-06-14 01:14:33 -07:00
ImmuState
ded7ce1dcf
Implement Resin (#1257)
* Basic resin usage/refresh.

* Honor resin config, move some logic to logon.

* Add resin usage to DungeonChallenge

* Make fragile and transient resin usable.

* Get resin cost from dungeon excel.

* Add ability to unlock combine diagrams.

* Refactor CombineManager to use Inventory.payItems, enabling crafting of condensed resin.

* Refactor ForgingManager to use Inventory.payItems, to prepare for eventually forging Mystic Enhancement Ores using resin.

* Remove comment

* Check resin usage in addResin
2022-06-14 01:14:15 -07:00
22 changed files with 633 additions and 136 deletions

View File

@ -37,7 +37,7 @@ EN | [中文](README_zh-CN.md)
**Note:** If you updated from an older version, delete `config.json` to regenerate it.
1. Get `grasscutter.jar`
- Download from [actions](https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip)
- Download from [actions](https://github.com/Grasscutters/Grasscutter/suites/6895963598/artifacts/267483297)
- [Build by yourself](#Building)
2. Create a `resources` folder in the directory where grasscutter.jar is located and move your `BinOutput` and `ExcelBinOutput` folders there *(Check the [wiki](https://github.com/Grasscutters/Grasscutter/wiki) for more details how to get those.)*
3. Run Grasscutter with `java -jar grasscutter.jar`. **Make sure mongodb service is running as well.**

View File

@ -37,7 +37,7 @@
**注:** 如果您从旧版本升级到新版本,最好删除 `config.json` 并启动服务端jar来重新生成它
1. 获取 `grasscutter.jar`
- 从 [actions](https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip) 中下载
- 从 [actions](https://github.com/Grasscutters/Grasscutter/suites/6895963598/artifacts/267483297) 中下载
- [自行构建](#构建)
2. 在**grasscutter.jar** 所在目录中创建 `resources` 文件夹并将 `BinOutput``ExcelBinOutput` 放入其中 *(查看 [wiki](https://github.com/Grasscutters/Grasscutter/wiki) 了解更多)*
3. 通过命令 `java -jar grasscutter.jar` 来运行Grasscutter. **在此之前请确认MongoDB服务运行正常**

View File

@ -8,7 +8,7 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "kick", usage = "kick", permission = "server.kick", permissionTargeted = "server.kick.others", description = "commands.kick.description")
@Command(label = "kick", usage = "kick", permission = "server.kick", description = "commands.kick.description")
public final class KickCommand implements CommandHandler {
@Override

View File

@ -1,9 +1,16 @@
package emu.grasscutter.data.binout;
import java.util.Map;
import java.io.Serializable;
public class AbilityModifier {
import com.google.gson.annotations.SerializedName;
public class AbilityModifier implements Serializable {
private static final long serialVersionUID = -2001232313615923575L;
@SerializedName(value="onAdded", alternate={"KCICDEJLIJD"})
public AbilityModifierAction[] onAdded;
@SerializedName(value="onThinkInterval", alternate={"PBDDACFFPOE"})
public AbilityModifierAction[] onThinkInterval;
public AbilityModifierAction[] onRemoved;
@ -13,6 +20,7 @@ public class AbilityModifier {
public static class AbilityData {
public String abilityName;
@SerializedName(value="modifiers", alternate={"HNEIEGHMLKH"})
public Map<String, AbilityModifier> modifiers;
}

View File

@ -16,6 +16,9 @@ public class DungeonData extends GameResource {
private RewardPreviewData previewData;
private int statueCostID;
private int statueCostCount;
@Override
public int getId() {
return this.id;
@ -33,6 +36,14 @@ public class DungeonData extends GameResource {
return previewData;
}
public int getStatueCostID() {
return statueCostID;
}
public int getStatueCostCount() {
return statueCostCount;
}
@Override
public void onLoad() {
if (this.passRewardPreviewID > 0) {

View File

@ -1,6 +1,8 @@
package emu.grasscutter.game.ability;
import java.util.*;
import java.util.Optional;
import java.util.Map.Entry;
import com.google.protobuf.InvalidProtocolBufferException;
@ -28,12 +30,15 @@ import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalar
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
import emu.grasscutter.utils.Position;
import emu.grasscutter.utils.Utils;
import emu.grasscutter.game.props.FightProperty;
public class AbilityManager {
private Player player;
HealAbilityManager healAbilityManager;
public AbilityManager(Player player) {
this.player = player;
this.healAbilityManager = new HealAbilityManager(player);
}
public Player getPlayer() {
@ -41,7 +46,9 @@ public class AbilityManager {
}
public void onAbilityInvoke(AbilityInvokeEntry invoke) throws Exception {
// Grasscutter.getLogger().info(invoke.getArgumentType() + " (" + invoke.getArgumentTypeValue() + "): " + Utils.bytesToHex(invoke.toByteArray()));
healAbilityManager.healHandler(invoke);
//Grasscutter.getLogger().info(invoke.getArgumentType() + " (" + invoke.getArgumentTypeValue() + "): " + Utils.bytesToHex(invoke.toByteArray()));
switch (invoke.getArgumentType()) {
case ABILITY_INVOKE_ARGUMENT_META_OVERRIDE_PARAM:
handleOverrideParam(invoke);
@ -61,6 +68,7 @@ public class AbilityManager {
default:
break;
}
}
private void handleOverrideParam(AbilityInvokeEntry invoke) throws Exception {
@ -155,19 +163,6 @@ public class AbilityManager {
private void invokeAction(AbilityModifierAction action, GameEntity target, GameEntity sourceEntity) {
switch (action.type) {
case HealHP -> {
if (action.amount == null) {
return;
}
float healAmount = 0;
if (action.amount.isDynamic && action.amount.dynamicKey != null) {
healAmount = sourceEntity.getMetaOverrideMap().getOrDefault(action.amount.dynamicKey, 0f);
}
if (healAmount > 0) {
target.heal(healAmount);
}
}
case LoseHP -> {
if (action.amountByTargetCurrentHPRatio == null) {

View File

@ -0,0 +1,185 @@
package emu.grasscutter.game.ability;
import java.util.*;
import java.util.Optional;
import java.util.Map.Entry;
import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.AbilityModifierEntry;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.data.excels.AvatarSkillDepotData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.EntityClientGadget;
import emu.grasscutter.game.entity.EntityItem;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ElementType;
import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall;
import emu.grasscutter.net.proto.AbilityInvokeEntryHeadOuterClass.AbilityInvokeEntryHead;
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
import emu.grasscutter.net.proto.AbilityMetaModifierChangeOuterClass.AbilityMetaModifierChange;
import emu.grasscutter.net.proto.AbilityMetaReInitOverrideMapOuterClass.AbilityMetaReInitOverrideMap;
import emu.grasscutter.net.proto.AbilityMixinCostStaminaOuterClass.AbilityMixinCostStamina;
import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry;
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
import emu.grasscutter.utils.Position;
import emu.grasscutter.utils.Utils;
import emu.grasscutter.game.props.FightProperty;
public class HealAbilityManager {
private class HealData {
public boolean isString = true;
public String abilityType = ""; //"E" or "Q"
public String sRatio = "";
public String sBase = "";
public float fRatio = 0;
public float fBase = 0;
public boolean healAll = false;
public HealData(String _abilityType, String _sRatio, String _sBase, boolean _healAll) {
abilityType = _abilityType;
isString = true;
sRatio = _sRatio;
sBase = _sBase;
healAll = _healAll;
}
public HealData(String _abilityType, String _sRatio, float _fRatio, float _fBase, boolean _healAll) {
abilityType = _abilityType;
isString = false;
sRatio = _sRatio;
fRatio = _fRatio;
fBase = _fBase;
healAll = _healAll;
}
}
private class HealDataAvatar {
public static int MAX_NUM_HEAL_ABILITY = 4;
public String avatar = "";
public int fightPropertyType= 0; //0: maxHP, 1: curAttack, 2: curDefense
public ArrayList<HealData> healDataList;
public HealDataAvatar(String _avatar, int _fightPropertyType) {
avatar = _avatar;
fightPropertyType = _fightPropertyType;
healDataList = new ArrayList();
}
public HealDataAvatar addHealData(String abilityType, String sRatio, String sBase, boolean healAll) {
HealData healData = new HealData(abilityType, sRatio, sBase, healAll);
healDataList.add(healData);
return this;
}
public HealDataAvatar addHealData(String abilityType, String sRatio, float fRatio, float fBase, boolean healAll) {
HealData healData = new HealData(abilityType, sRatio, fRatio, fBase, healAll);
healDataList.add(healData);
return this;
}
}
ArrayList<HealDataAvatar> healDataAvatarList;
private Player player;
public HealAbilityManager (Player player) {
this.player = player;
healDataAvatarList = new ArrayList();
healDataAvatarList.add(new HealDataAvatar("Kokomi", 0).addHealData("E", "ElementalArt_Heal_MaxHP_Base_Percentage", "ElementalArt_Heal_Base_Amount", false));
healDataAvatarList.add(new HealDataAvatar("Qin", 1).addHealData("Q", "Heal", "BurstHealConst", true));
healDataAvatarList.add(new HealDataAvatar("Noel", 2).addHealData("E", "OnAttack_HealthRate", 0.452f, 282f, true));
healDataAvatarList.add(new HealDataAvatar("Bennett", 0).addHealData("Q", "HealMaxHpRatio", "HealConst", false));
healDataAvatarList.add(new HealDataAvatar("Diona", 0).addHealData("Q", "HealHPRatio", "HealHP_Const", false));
healDataAvatarList.add(new HealDataAvatar("Sayu", 1).addHealData("Q", "Constellation_6_Damage", "Heal_BaseAmount", true).addHealData("Q", "Heal_AttackRatio", "Constellation_6_Heal", true));
healDataAvatarList.add(new HealDataAvatar("Barbara", 0).addHealData("E", "HealHPOnAdded", "HealHPOnAdded_Const", true).addHealData("E", "HealHP_OnHittingOthers", "HealHP_Const_OnHittingOthers", true));
healDataAvatarList.add(new HealDataAvatar("Shinobu", 0).addHealData("E", "ElementalArt_Heal_MaxHP_Percentage", 0.064f, 795f, false));
healDataAvatarList.add(new HealDataAvatar("Qiqi", 1).addHealData("E", "HealHP_OnHittingOthers", "HealHP_Const_OnHittingOthers", true).addHealData("E", "ElementalArt_HealHp_Ratio", "ElementalArt_HealHp_Const", true));
}
public Player getPlayer() {
return this.player;
}
public void healHandler(AbilityInvokeEntry invoke) throws Exception {
AbilityMetaModifierChange data = AbilityMetaModifierChange.parseFrom(invoke.getAbilityData());
if (data == null) {
return;
}
GameEntity sourceEntity = player.getScene().getEntityById(data.getApplyEntityId());
String modifierString = "";
if(data.getParentAbilityName() != null)
modifierString = data.getParentAbilityName().getStr();
if(sourceEntity != null)
checkAndHeal(sourceEntity, modifierString);
}
public void checkAndHeal(GameEntity sourceEntity, String modifierString) {
int fightPropertyType = 0;
float healAmount = 0;
float ratio = 0, base = 0;
float maxHP, curAttack, curDefense;
Map<String, Float> map = sourceEntity.getMetaOverrideMap();
for(int i = 0 ; i < healDataAvatarList.size() ; i ++) {
HealDataAvatar healDataAvatar = healDataAvatarList.get(i);
if(modifierString.contains(healDataAvatar.avatar)) {
fightPropertyType = healDataAvatar.fightPropertyType;
ArrayList<HealData> healDataList = healDataAvatar.healDataList;
for(int j = 0 ; j < healDataList.size(); j++) {
HealData healData = healDataList.get(j);
if(map.containsKey(healData.sRatio)) {
if(healData.isString) {
ratio = map.get(healData.sRatio);
base = map.get(healData.sBase);
}
else {
ratio = healData.fRatio;
base = healData.fBase;
}
}
List<EntityAvatar> activeTeam = player.getTeamManager().getActiveTeam();
List<EntityAvatar> needHealAvatars = new ArrayList();
int currentIndex = player.getTeamManager().getCurrentCharacterIndex();
EntityAvatar currentAvatar = activeTeam.get(currentIndex);
if(healData.healAll) {
needHealAvatars = activeTeam;
}
else {
needHealAvatars.add(currentAvatar);
}
maxHP = currentAvatar.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
curAttack = currentAvatar.getFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK);
curDefense = currentAvatar.getFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE);
switch(fightPropertyType) {
case 0:
healAmount = ratio * maxHP + base;
break;
case 1:
healAmount = ratio * curAttack + base;
break;
case 2:
healAmount = ratio * curDefense + base;
break;
}
for(int k = 0 ; k < needHealAvatars.size() ; k ++) {
EntityAvatar avatar = needHealAvatars.get(k);
avatar.heal(healAmount);
}
}
break;
}
}
}
}

View File

@ -3,13 +3,17 @@ package emu.grasscutter.game.combine;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.excels.CombineData;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.net.proto.RetcodeOuterClass;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.PacketCombineFormulaDataNotify;
import emu.grasscutter.server.packet.send.PacketCombineRsp;
import it.unimi.dsi.fastutil.Pair;
import java.util.ArrayList;
import java.util.List;
public class CombineManger {
@ -23,6 +27,27 @@ public class CombineManger {
this.gameServer = gameServer;
}
public boolean unlockCombineDiagram(Player player, GameItem diagramItem) {
// Make sure this is actually a diagram.
if (!diagramItem.getItemData().getItemUse().get(0).getUseOp().equals("ITEM_USE_UNLOCK_COMBINE")) {
return false;
}
// Determine the combine item we should unlock.
int combineId = Integer.parseInt(diagramItem.getItemData().getItemUse().get(0).getUseParam().get(0));
// Remove the diagram from the player's inventory.
// We need to do this here, before sending CombineFormulaDataNotify, or the the combine UI won't correctly
// update when unlocking the diagram.
player.getInventory().removeItem(diagramItem, 1);
// Tell the client that this diagram is now unlocked and add the unlocked item to the player.
player.getUnlockedCombines().add(combineId);
player.sendPacket(new PacketCombineFormulaDataNotify(combineId));
return true;
}
public CombineResult combineItem(Player player, int cid, int count){
// check config exist
if(!GameData.getCombineDataMap().containsKey(cid)){
@ -35,36 +60,17 @@ public class CombineManger {
if(combineData.getPlayerLevel() > player.getLevel()){
return null;
}
// check enough
var enough = combineData.getMaterialItems().stream()
.filter(item -> player.getInventory()
.getInventoryTab(ItemType.ITEM_MATERIAL)
.getItemById(item.getId())
.getCount() < item.getCount() * count
)
.findAny()
.isEmpty();
// if not enough
if(!enough){
player.getWorld().getHost().sendPacket(
new PacketCombineRsp(RetcodeOuterClass.Retcode.RET_ITEM_COMBINE_COUNT_NOT_ENOUGH_VALUE)
);
return null;
// consume items
List<ItemParamData> material = new ArrayList<>(combineData.getMaterialItems());
material.add(new ItemParamData(202, combineData.getScoinCost()));
boolean success = player.getInventory().payItems(material.toArray(new ItemParamData[0]), count, ActionReason.Combine);
// abort if not enough material
if (!success) {
player.sendPacket(new PacketCombineRsp(RetcodeOuterClass.Retcode.RET_ITEM_COMBINE_COUNT_NOT_ENOUGH_VALUE));
}
if (player.getMora() >= combineData.getScoinCost()) {
player.setMora(player.getMora() - combineData.getScoinCost() * count);
} else {
return null;
}
// try to remove materials
combineData.getMaterialItems().stream()
.map(item -> Pair.of(player.getInventory()
.getInventoryTab(ItemType.ITEM_MATERIAL)
.getItemById(item.getId())
,item.getCount() * count)
)
.forEach(item -> player.getInventory().removeItem(item.first(), item.second()));
// make the result
player.getInventory().addItem(combineData.getResultItemId(),

View File

@ -4,9 +4,11 @@ import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.excels.DungeonData;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
import emu.grasscutter.scripts.constants.EventType;
import emu.grasscutter.scripts.data.SceneGroup;
import emu.grasscutter.scripts.data.ScriptArgs;
@ -153,8 +155,20 @@ public class DungeonChallenge {
}
}
public void getStatueDrops(Player player) {
private List<GameItem> rollRewards() {
List<GameItem> rewards = new ArrayList<>();
for (ItemParamData param : getScene().getDungeonData().getRewardPreview().getPreviewItems()) {
rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1)));
}
return rewards;
}
public void getStatueDrops(Player player, GadgetInteractReq request) {
DungeonData dungeonData = getScene().getDungeonData();
int resinCost = dungeonData.getStatueCostCount() != 0 ? dungeonData.getStatueCostCount() : 20;
if (!isSuccess() || dungeonData == null || dungeonData.getRewardPreview() == null || dungeonData.getRewardPreview().getPreviewItems().length == 0) {
return;
}
@ -164,11 +178,43 @@ public class DungeonChallenge {
return;
}
// Get rewards.
List<GameItem> rewards = new ArrayList<>();
for (ItemParamData param : getScene().getDungeonData().getRewardPreview().getPreviewItems()) {
rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1)));
if (request.getIsUseCondenseResin()) {
// Check if condensed resin is usable here.
// For this, we use the following logic for now:
// The normal resin cost of the dungeon has to be 20.
if (resinCost != 20) {
return;
}
// Make sure the player has condensed resin.
GameItem condensedResin = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(220007);
if (condensedResin == null || condensedResin.getCount() <= 0) {
return;
}
// Deduct.
player.getInventory().removeItem(condensedResin, 1);
// Roll rewards, twice (because condensed).
rewards.addAll(this.rollRewards());
rewards.addAll(this.rollRewards());
}
else {
// If the player used regular resin, try to deduct.
// Stop if insufficient resin.
boolean success = player.getResinManager().useResin(resinCost);
if (!success) {
return;
}
// Roll rewards.
rewards.addAll(this.rollRewards());
}
// Add rewards to player and send notification.
player.getInventory().addItems(rewards, ActionReason.DungeonStatueDrop);
player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(rewards));

View File

@ -17,6 +17,7 @@ import emu.grasscutter.game.avatar.AvatarStorage;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
import emu.grasscutter.server.packet.send.PacketAvatarEquipChangeNotify;
import emu.grasscutter.server.packet.send.PacketItemAddHintNotify;
@ -258,6 +259,8 @@ public class Inventory implements Iterable<GameItem> {
getPlayer().addExpDirectly(count);
case 105 -> // Companionship exp
getPlayer().getServer().getInventoryManager().upgradeAvatarFetterLevel(player, getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), count);
case 106 -> // Resin
getPlayer().getResinManager().addResin(count);
case 201 -> // Primogem
getPlayer().setPrimogems(player.getPrimogems() + count);
case 202 -> // Mora
@ -275,6 +278,8 @@ public class Inventory implements Iterable<GameItem> {
return player.getMora();
case 203: // Genesis Crystals
return player.getCrystals();
case 106: // Resin
return player.getProperty(PlayerProperty.PROP_PLAYER_RESIN);
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();
@ -313,6 +318,8 @@ public class Inventory implements Iterable<GameItem> {
player.setMora(player.getMora() - (cost.getCount() * quantity));
case 203 -> // Genesis Crystals
player.setCrystals(player.getCrystals() - (cost.getCount() * quantity));
case 106 -> // Resin
player.getResinManager().useResin(cost.getCount() * quantity);
default ->
removeItem(getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()), cost.getCount() * quantity);
}

View File

@ -15,6 +15,7 @@ import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.net.proto.ForgeStartReqOuterClass;
import emu.grasscutter.net.proto.ForgeQueueDataOuterClass.ForgeQueueData;
import emu.grasscutter.net.proto.ForgeQueueManipulateReqOuterClass.ForgeQueueManipulateReq;
@ -147,37 +148,15 @@ public class ForgingManager {
ForgeData forgeData = GameData.getForgeDataMap().get(req.getForgeId());
// Check if we have enough of each material.
for (var material : forgeData.getMaterialItems()) {
if (material.getItemId() == 0) {
continue;
}
// Check if we have enough of each material and consume.
List<ItemParamData> material = new ArrayList<>(forgeData.getMaterialItems());
material.add(new ItemParamData(202, forgeData.getScoinCost()));
int currentCount = this.player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(material.getItemId()).getCount();
boolean success = player.getInventory().payItems(material.toArray(new ItemParamData[0]), req.getForgeCount(), ActionReason.ForgeCost);
if (currentCount < material.getCount() * req.getForgeCount()) {
if (!success) {
this.player.sendPacket(new PacketForgeStartRsp(Retcode.RET_FORGE_POINT_NOT_ENOUGH)); //ToDo: Probably the wrong return code.
return;
}
}
// Check if we have enough Mora.
if (this.player.getMora() < forgeData.getScoinCost() * req.getForgeCount()) {
this.player.sendPacket(new PacketForgeStartRsp(Retcode.RET_FORGE_POINT_NOT_ENOUGH)); //ToDo: Probably the wrong return code.
return;
}
// Consume material and Mora.
for (var material : forgeData.getMaterialItems()) {
if (material.getItemId() == 0) {
continue;
}
GameItem item = this.player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(material.getItemId());
this.player.getInventory().removeItem(item, material.getCount() * req.getForgeCount());
}
this.player.setMora(this.player.getMora() - forgeData.getScoinCost() * req.getForgeCount());
// Create and add active forge.
ActiveForgeData activeForge = new ActiveForgeData();

View File

@ -856,6 +856,27 @@ public class InventoryManager {
// Unlock.
useSuccess = player.getForgingManager().unlockForgingBlueprint(useItem);
}
// Handle combine diagrams.
if (useItem.getItemData().getItemUse().get(0).getUseOp().equals("ITEM_USE_UNLOCK_COMBINE")) {
// Unlock.
useSuccess = player.getServer().getCombineManger().unlockCombineDiagram(player, useItem);
}
break;
case MATERIAL_CONSUME_BATCH_USE:
// Make sure we have usage data for this material.
if (useItem.getItemData().getItemUse() == null) {
break;
}
// Handle fragile/transient resin.
if (useItem.getItemId() == 107009 || useItem.getItemId() == 107012){
// Add resin to the inventory.
ItemData resinItemData = GameData.getItemDataMap().get(106);
player.getInventory().addItem(new GameItem(resinItemData, 60 * count), ActionReason.PlayerUseItem);
// Set used amount.
used = count;
}
break;
case MATERIAL_CHEST:
List<ShopChestTable> shopChestTableList = player.getServer().getShopManager().getShopChestData();

View File

@ -0,0 +1,143 @@
package emu.grasscutter.game.managers;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.server.packet.send.PacketPlayerPropNotify;
import emu.grasscutter.server.packet.send.PacketResinChangeNotify;
import emu.grasscutter.utils.Utils;
import static emu.grasscutter.Configuration.GAME_OPTIONS;
public class ResinManager {
private final Player player;
public ResinManager(Player player) {
this.player = player;
}
/********************
* Change resin.
********************/
public synchronized boolean useResin(int amount) {
// Check if resin enabled.
if (!GAME_OPTIONS.resinOptions.resinUsage) {
return true;
}
int currentResin = this.player.getProperty(PlayerProperty.PROP_PLAYER_RESIN);
// Check if the player has sufficient resin.
if (currentResin < amount) {
return false;
}
// Deduct the resin from the player.
int newResin = currentResin - amount;
this.player.setProperty(PlayerProperty.PROP_PLAYER_RESIN, newResin);
// Check if this has taken the player under the recharge cap,
// starting the recharging process.
if (this.player.getNextResinRefresh() == 0 && newResin < GAME_OPTIONS.resinOptions.cap) {
int currentTime = Utils.getCurrentSeconds();
this.player.setNextResinRefresh(currentTime + GAME_OPTIONS.resinOptions.rechargeTime);
}
// Send packets.
this.player.sendPacket(new PacketPlayerPropNotify(this.player, PlayerProperty.PROP_PLAYER_RESIN));
this.player.sendPacket(new PacketResinChangeNotify(this.player));
return true;
}
public synchronized void addResin(int amount) {
// Check if resin enabled.
if (!GAME_OPTIONS.resinOptions.resinUsage) {
return;
}
// Add resin.
int currentResin = this.player.getProperty(PlayerProperty.PROP_PLAYER_RESIN);
int newResin = currentResin + amount;
this.player.setProperty(PlayerProperty.PROP_PLAYER_RESIN, newResin);
// Stop recharging if player is now at or over the cap.
if (newResin >= GAME_OPTIONS.resinOptions.cap) {
this.player.setNextResinRefresh(0);
}
// Send packets.
this.player.sendPacket(new PacketPlayerPropNotify(this.player, PlayerProperty.PROP_PLAYER_RESIN));
this.player.sendPacket(new PacketResinChangeNotify(this.player));
}
/********************
* Recharge resin.
********************/
public synchronized void rechargeResin() {
// Check if resin enabled.
if (!GAME_OPTIONS.resinOptions.resinUsage) {
return;
}
int currentResin = this.player.getProperty(PlayerProperty.PROP_PLAYER_RESIN);
int currentTime = Utils.getCurrentSeconds();
// Make sure we are currently in "recharging mode".
// This is denoted by Player.nextResinRefresh being greater than 0.
if (this.player.getNextResinRefresh() <= 0) {
return;
}
// Determine if we actually need to recharge yet.
if (currentTime < this.player.getNextResinRefresh()) {
return;
}
// Calculate how much resin we need to refill and update player.
// Note that this can be more than one in case the player
// logged off with uncapped resin and is now logging in again.
int recharge = 1 + (int)((currentTime - this.player.getNextResinRefresh()) / GAME_OPTIONS.resinOptions.rechargeTime);
int newResin = Math.min(GAME_OPTIONS.resinOptions.cap, currentResin + recharge);
int resinChange = newResin - currentResin;
this.player.setProperty(PlayerProperty.PROP_PLAYER_RESIN, newResin);
// Calculate next recharge time.
// Set to zero to disable recharge (because on/over cap.)
if (newResin >= GAME_OPTIONS.resinOptions.cap) {
this.player.setNextResinRefresh(0);
}
else {
int nextRecharge = this.player.getNextResinRefresh() + resinChange * GAME_OPTIONS.resinOptions.rechargeTime;
this.player.setNextResinRefresh(nextRecharge);
}
// Send packets.
this.player.sendPacket(new PacketPlayerPropNotify(this.player, PlayerProperty.PROP_PLAYER_RESIN));
this.player.sendPacket(new PacketResinChangeNotify(this.player));
}
/********************
* Player login.
********************/
public synchronized void onPlayerLogin() {
// If resin usage is disabled, set resin to cap.
if (!GAME_OPTIONS.resinOptions.resinUsage) {
this.player.setProperty(PlayerProperty.PROP_PLAYER_RESIN, GAME_OPTIONS.resinOptions.cap);
this.player.setNextResinRefresh(0);
}
// In case server administrators change the resin cap while players are capped,
// we need to restart recharging here.
int currentResin = this.player.getProperty(PlayerProperty.PROP_PLAYER_RESIN);
int currentTime = Utils.getCurrentSeconds();
if (currentResin < GAME_OPTIONS.resinOptions.cap && this.player.getNextResinRefresh() == 0) {
this.player.setNextResinRefresh(currentTime + GAME_OPTIONS.resinOptions.rechargeTime);
}
// Send initial notifications on logon.
this.player.sendPacket(new PacketPlayerPropNotify(this.player, PlayerProperty.PROP_PLAYER_RESIN));
this.player.sendPacket(new PacketResinChangeNotify(this.player));
}
}

View File

@ -27,6 +27,7 @@ import emu.grasscutter.game.inventory.Inventory;
import emu.grasscutter.game.mail.Mail;
import emu.grasscutter.game.mail.MailHandler;
import emu.grasscutter.game.managers.InsectCaptureManager;
import emu.grasscutter.game.managers.ResinManager;
import emu.grasscutter.game.managers.StaminaManager.StaminaManager;
import emu.grasscutter.game.managers.SotSManager;
import emu.grasscutter.game.managers.EnergyManager.EnergyManager;
@ -48,6 +49,7 @@ import emu.grasscutter.net.proto.*;
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult;
import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry;
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType;
import emu.grasscutter.net.proto.MpSettingTypeOuterClass.MpSettingType;
import emu.grasscutter.net.proto.OnlinePlayerInfoOuterClass.OnlinePlayerInfo;
@ -94,6 +96,7 @@ public class Player {
private Set<Integer> flyCloakList;
private Set<Integer> costumeList;
private Set<Integer> unlockedForgingBlueprints;
private Set<Integer> unlockedCombines;
private List<ActiveForgeData> activeForges;
private Integer widgetId;
@ -158,11 +161,13 @@ public class Player {
@Transient private MapMarksManager mapMarksManager;
@Transient private StaminaManager staminaManager;
@Transient private EnergyManager energyManager;
@Transient private ResinManager resinManager;
@Transient private ForgingManager forgingManager;
@Transient private DeforestationManager deforestationManager;
private long springLastUsed;
private HashMap<String, MapMark> mapMarks;
private int nextResinRefresh;
@Deprecated
@SuppressWarnings({"rawtypes", "unchecked"}) // Morphia only!
@ -193,6 +198,7 @@ public class Player {
this.costumeList = new HashSet<>();
this.towerData = new TowerData();
this.unlockedForgingBlueprints = new HashSet<>();
this.unlockedCombines = new HashSet<>();
this.activeForges = new ArrayList<>();
this.setSceneId(3);
@ -217,6 +223,7 @@ public class Player {
this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this);
this.energyManager = new EnergyManager(this);
this.resinManager = new ResinManager(this);
this.forgingManager = new ForgingManager(this);
}
@ -248,6 +255,7 @@ public class Player {
this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this);
this.energyManager = new EnergyManager(this);
this.resinManager = new ResinManager(this);
this.deforestationManager = new DeforestationManager(this);
this.forgingManager = new ForgingManager(this);
}
@ -540,6 +548,10 @@ public class Player {
return this.unlockedForgingBlueprints;
}
public Set<Integer> getUnlockedCombines() {
return this.unlockedCombines;
}
public List<ActiveForgeData> getActiveForges() {
return this.activeForges;
}
@ -648,6 +660,14 @@ public class Player {
springLastUsed = val;
}
public int getNextResinRefresh() {
return nextResinRefresh;
}
public void setNextResinRefresh(int value) {
this.nextResinRefresh = value;
}
public SceneLoadState getSceneLoadState() {
return sceneState;
}
@ -940,7 +960,7 @@ public class Player {
return this.getMailHandler().replaceMailByIndex(index, message);
}
public void interactWith(int gadgetEntityId) {
public void interactWith(int gadgetEntityId, GadgetInteractReq request) {
GameEntity entity = getScene().getEntityById(gadgetEntityId);
if (entity == null) {
return;
@ -970,7 +990,7 @@ public class Player {
} else if (entity instanceof EntityGadget gadget) {
if (gadget.getGadgetData().getType() == EntityType.RewardStatue) {
if (scene.getChallenge() != null) {
scene.getChallenge().getStatueDrops(this);
scene.getChallenge().getStatueDrops(this, request);
}
this.sendPacket(new PacketGadgetInteractRsp(gadget, InteractType.INTERACT_TYPE_OPEN_STATUE));
}
@ -1142,6 +1162,10 @@ public class Player {
return this.energyManager;
}
public ResinManager getResinManager() {
return this.resinManager;
}
public ForgingManager getForgingManager() {
return this.forgingManager;
}
@ -1208,6 +1232,9 @@ public class Player {
// Send updated forge queue data, if necessary.
this.getForgingManager().sendPlayerForgingUpdate();
// Recharge resin.
this.getResinManager().rechargeResin();
}
@ -1291,7 +1318,9 @@ public class Player {
session.send(new PacketWidgetGadgetAllDataNotify());
session.send(new PacketPlayerHomeCompInfoNotify(this));
session.send(new PacketHomeComfortInfoNotify(this));
session.send(new PacketCombineDataNotify(this.unlockedCombines));
this.forgingManager.sendForgeDataNotify();
this.resinManager.onPlayerLogin();
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.

View File

@ -13,7 +13,7 @@ public class HandlerGadgetInteractReq extends PacketHandler {
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
GadgetInteractReq req = GadgetInteractReq.parseFrom(payload);
session.getPlayer().interactWith(req.getGadgetEntityId());
session.getPlayer().interactWith(req.getGadgetEntityId(), req);
}
}

View File

@ -0,0 +1,18 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.CombineDataNotifyOuterClass.CombineDataNotify;
public class PacketCombineDataNotify extends BasePacket {
public PacketCombineDataNotify(Iterable<Integer> unlockedCombines) {
super(PacketOpcodes.CombineDataNotify);
CombineDataNotify proto = CombineDataNotify.newBuilder()
.addAllCombineIdList(unlockedCombines)
.build();
this.setData(proto);
}
}

View File

@ -0,0 +1,19 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.CombineFormulaDataNotifyOuterClass.CombineFormulaDataNotify;
public class PacketCombineFormulaDataNotify extends BasePacket {
public PacketCombineFormulaDataNotify(int combineId) {
super(PacketOpcodes.CombineFormulaDataNotify);
CombineFormulaDataNotify proto = CombineFormulaDataNotify.newBuilder()
.setCombineId(combineId)
.setIsLocked(false)
.build();
this.setData(proto);
}
}

View File

@ -0,0 +1,23 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.ResinChangeNotifyOuterClass.ResinChangeNotify;
public class PacketResinChangeNotify extends BasePacket {
public PacketResinChangeNotify(Player player) {
super(PacketOpcodes.ResinChangeNotify);
ResinChangeNotify proto = ResinChangeNotify.newBuilder()
.setCurValue(player.getProperty(PlayerProperty.PROP_PLAYER_RESIN))
.setNextAddTimestamp(player.getNextResinRefresh())
.build();
// ToDo: Add ability to buy resin with primogems, has to be included here.
this.setData(proto);
}
}

View File

@ -177,6 +177,7 @@ public class ConfigContainer {
public boolean enableShopItems = true;
public boolean staminaUsage = true;
public boolean energyUsage = false;
public ResinOptions resinOptions = new ResinOptions();
public Rates rates = new Rates();
public static class InventoryLimits {
@ -197,6 +198,12 @@ public class ConfigContainer {
public float mora = 1.0f;
public float leyLines = 1.0f;
}
public static class ResinOptions {
public boolean resinUsage = false;
public int cap = 160;
public int rechargeTime = 480;
}
}
public static class JoinOptions {

View File

@ -140,6 +140,11 @@
"success": "Summoned %s to %s's world.",
"description": "Forces someone to join the world of others. If no one is targeted, it sends you into co-op mode anyway."
},
"drop": {
"command_usage": "Usage: drop <itemID|itemName> [amount]",
"success": "Dropped %s of %s.",
"description": "Drops an item near you"
},
"enter_dungeon": {
"usage": "Usage: enterdungeon <dungeonID>",
"changed": "Changed to dungeon %s.",
@ -181,19 +186,16 @@
"success": "Godmode is now %s for %s.",
"description": "Prevents you from taking damage. Defaults to toggle."
},
"nostamina": {
"success": "NoStamina is now %s for %s.",
"description": "Keep your endurance to the maximum."
},
"unlimitenergy": {
"success": "UnlimitEnergy is now %s for %s.",
"config_error": "Command is disabled, because energyUsage is false in config.json.",
"description": "Use the element does not waste energy"
},
"heal": {
"success": "All characters have been healed.",
"description": "Heal all characters in your current team."
},
"help": {
"usage": "Usage: ",
"aliases": "Aliases: ",
"available_commands": "Available commands: ",
"description": "Sends the help message or shows information about a specified command"
},
"kick": {
"player_kick_player": "Player [%s:%s] has kicked player [%s:%s]",
"server_kick_player": "Kicking player [%s:%s]...",
@ -220,6 +222,10 @@
"success": "There are %s player(s) online:",
"description": "List online players"
},
"nostamina": {
"success": "NoStamina is now %s for %s.",
"description": "Keep your endurance to the maximum."
},
"permission": {
"usage": "Usage: permission <add|remove> <username> <permission>",
"add": "Permission added.",
@ -256,6 +262,9 @@
"success": "Reset complete.",
"description": "Reset target player's shop refresh time"
},
"restart": {
"description": "Restarts the current session"
},
"sendMail": {
"usage": "Usage: sendmail <userID|all|help> [templateID]",
"user_not_exist": "The user with an ID of '%s' does not exist.",
@ -368,29 +377,20 @@
"success": "Teleported %s to %s, %s, %s in scene %s.",
"description": "Change the player's position"
},
"unlimitenergy": {
"success": "UnlimitEnergy is now %s for %s.",
"config_error": "Command is disabled, because energyUsage is false in config.json.",
"description": "Use the element does not waste energy"
},
"unlocktower": {
"success": "Abyss Corridor's Floors are all unlocked now.",
"description": "Unlock all levels of tower"
},
"weather": {
"usage": "Usage: weather <climate type(weatherId)> <weather type(climateId)>\nWeather types 0: None, 1: Sunny, 2: Cloudy, 3: Rain, 4: Thunderstorm, 5: Snow, 6: Mist",
"success": "Changed climate type to %s with weather type %s.",
"invalid_id": "Invalid ID.",
"description": "Changes the weather"
},
"drop": {
"command_usage": "Usage: drop <itemID|itemName> [amount]",
"success": "Dropped %s of %s.",
"description": "Drops an item near you"
},
"help": {
"usage": "Usage: ",
"aliases": "Aliases: ",
"available_commands": "Available commands: ",
"description": "Sends the help message or shows information about a specified command"
},
"restart": {
"description": "Restarts the current session"
},
"unlocktower": {
"success": "Unlock done.",
"description": "Unlock Abyss Corridor's Floors"
}
},
"gacha": {

View File

@ -140,6 +140,11 @@
"success": "已强制传送 %s 到 %s 的世界。",
"description": "强制传送指定玩家到他人的世界。如果没有指定玩家,则会使你进入多人游戏状态"
},
"drop": {
"command_usage": "用法drop <物品ID|物品名称> [数量]",
"success": "已丢下 %s 个 %s。",
"description": "在你附近丢下物品"
},
"enter_dungeon": {
"usage": "用法enterdungeon <秘境ID>",
"changed": "已进入秘境 %s。",
@ -181,19 +186,16 @@
"success": "Godmode 已设为 %s。[用户:%s]",
"description": "防止你受到伤害"
},
"nostamina": {
"success": "NoStamina 已设为 %s。[用户:%s]",
"description": "保持你的体力处于最高状态"
},
"unlimitenergy": {
"success": "UnlimitEnergy 已设为 %s。[用户:%s]",
"config_error": "命令不可用。因为 config.json 中 energyUsage 为 false。",
"description": "使用元素爆发而不消耗能量"
},
"heal": {
"success": "已治疗所有角色。",
"description": "治疗当前队伍的角色"
},
"help": {
"usage": "用法:",
"aliases": "别名:",
"available_commands": "可用命令:",
"description": "发送帮助信息或显示指定命令的信息"
},
"kick": {
"player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出。",
"server_kick_player": "正在踢出玩家 [%s:%s]...",
@ -220,6 +222,10 @@
"success": "目前在线人数:%s",
"description": "查看所有玩家"
},
"nostamina": {
"success": "NoStamina 已设为 %s。[用户:%s]",
"description": "保持你的体力处于最高状态"
},
"permission": {
"usage": "用法permission <add|remove> <用户名> <权限>",
"add": "权限已添加。",
@ -256,6 +262,9 @@
"success": "重置完成。",
"description": "重置指定玩家的商店刷新时间"
},
"restart": {
"description": "重新启动服务器"
},
"sendMail": {
"usage": "用法sendmail <用户ID|all|help> [模板ID]",
"user_not_exist": "用户 '%s' 不存在。",
@ -368,29 +377,20 @@
"success": "传送 %s 到坐标 %s, %s, %s场景为 %s。",
"description": "改变指定玩家的位置"
},
"unlimitenergy": {
"success": "UnlimitEnergy 已设为 %s。[用户:%s]",
"config_error": "命令不可用。因为 config.json 中 energyUsage 为 false。",
"description": "使用元素爆发而不消耗能量"
},
"unlocktower": {
"success": "现已解锁深境回廊(1-8层)。",
"description": "解锁深境螺旋"
},
"weather": {
"usage": "用法weather <气候类型(天气ID)> <天气类型(气候ID)>\n天气类型 0: 无, 1: 晴天, 2: 多云, 3: 雨, 4: 雷雨, 5: 雪, 6: 雾",
"success": "已更改气候类型为 %s天气类型为 %s。",
"invalid_id": "无效的ID。",
"description": "更改天气"
},
"drop": {
"command_usage": "用法drop <物品ID|物品名称> [数量]",
"success": "已丢下 %s 个 %s。",
"description": "在你附近丢下物品"
},
"help": {
"usage": "用法:",
"aliases": "别名:",
"available_commands": "可用命令:",
"description": "发送帮助信息或显示指定命令的信息"
},
"restart": {
"description": "重新启动服务器"
},
"unlocktower": {
"success": "解锁完成。",
"description": "解锁深境回廊(1-8层)"
}
},
"gacha": {

View File

@ -389,8 +389,8 @@
"description": "重新啟動伺服器。"
},
"unlocktower": {
"success": "解鎖完成。",
"description": "解鎖深境迴廊。"
"success": "現已解鎖深境迴廊。",
"description": "解鎖深境螺旋。"
}
},
"gacha": {