Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Freeze stats #1596

Merged
3 commits merged into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions manage_languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def ppprint(data):
class JsonHelpers:
@staticmethod
def load(filename: str) -> dict:
with open(filename, 'r') as file:
with open(filename, 'r', encoding='utf-8') as file:
return json.load(file)

@staticmethod
Expand Down Expand Up @@ -117,7 +117,7 @@ def find_all_used_keys(self, expected_keys=[]) -> set:
for file in files:
if file.rpartition('.')[-1] in SOURCE_EXTENSIONS:
filename = os.path.join(root, file)
with open(filename, 'r') as f:
with open(filename, 'r', encoding='utf-8') as f:
data = f.read() # Loads in entire file at once
for k in self.TRANSLATION_KEY.findall(data):
used.add(k)
Expand Down
130 changes: 99 additions & 31 deletions src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@

import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;

@Command(label = "setStats", aliases = {"stats", "stat"}, usage = {"<stat> <value>"}, permission = "player.setstats", permissionTargeted = "player.setstats.others")
@Command(
label = "setStats",
aliases = {"stats", "stat"},
usage = {
"[set] <stat> <value>",
"(lock|freeze) <stat> [<value>]", // Can lock to current value
"(unlock|unfreeze) <stat>"},
permission = "player.setstats",
permissionTargeted = "player.setstats.others")
public final class SetStatsCommand implements CommandHandler {
static class Stat {
private static class Stat {
String name;
FightProperty prop;

Expand All @@ -27,9 +36,21 @@ public Stat(String name, FightProperty prop) {
this.prop = prop;
}
}

Map<String, Stat> stats;


private static enum Action {
ACTION_SET("commands.generic.set_to", "commands.generic.set_for_to"),
ACTION_LOCK("commands.setStats.locked_to", "commands.setStats.locked_for_to"),
ACTION_UNLOCK("commands.setStats.unlocked", "commands.setStats.unlocked_for");
public final String messageKeySelf;
public final String messageKeyOther;
private Action(String messageKeySelf, String messageKeyOther) {
this.messageKeySelf = messageKeySelf;
this.messageKeyOther = messageKeyOther;
}
}

private Map<String, Stat> stats;

public SetStatsCommand() {
this.stats = new HashMap<>();
for (String key : FightProperty.getShortNames()) {
Expand Down Expand Up @@ -62,50 +83,97 @@ public SetStatsCommand() {
this.stats.put("ephys", this.stats.get("phys%"));
}

public static float parsePercent(String input) throws NumberFormatException {
if (input.endsWith("%")) {
return Float.parseFloat(input.substring(0, input.length()-1))/100f;
} else {
return Float.parseFloat(input);
}
}

@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
String statStr;
String statStr = null;
String valueStr;
float value = 0f;

if (args.size() == 2) {
statStr = args.get(0).toLowerCase();
valueStr = args.get(1);
} else {
if (args.size() < 2) {
sendUsageMessage(sender);
return;
}

// Get the action and stat
String arg0 = args.remove(0).toLowerCase();
Action action = switch (arg0) {
default -> {statStr = arg0; yield Action.ACTION_SET;} // Implicit set command
case "set" -> Action.ACTION_SET; // Explicit set command
case "lock", "freeze" -> Action.ACTION_LOCK;
case "unlock", "unfreeze" -> Action.ACTION_UNLOCK;
};
if (statStr == null) {
statStr = args.remove(0).toLowerCase();
}
if (!stats.containsKey(statStr)) {
sendUsageMessage(sender); // Invalid stat or action
return;
}
Stat stat = stats.get(statStr);
EntityAvatar entity = targetPlayer.getTeamManager().getCurrentAvatarEntity();
Avatar avatar = entity.getAvatar();

float value;
// Get the value if the action requires it
try {
if (valueStr.endsWith("%")) {
value = Float.parseFloat(valueStr.substring(0, valueStr.length()-1))/100f;
} else {
value = Float.parseFloat(valueStr);
switch (action) {
case ACTION_LOCK:
if (args.isEmpty()) { // Lock to current value
value = avatar.getFightProperty(stat.prop);
break;
} // Else fall-through and lock to supplied value
case ACTION_SET:
value = parsePercent(args.remove(0));
break;
case ACTION_UNLOCK:
break;
}
} catch (NumberFormatException ignored) {
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.statValue");
return;
} catch (IndexOutOfBoundsException ignored) {
sendUsageMessage(sender);
return;
}

if (stats.containsKey(statStr)) {
Stat stat = stats.get(statStr);
entity.setFightProperty(stat.prop, value);
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, stat.prop));
if (FightProperty.isPercentage(stat.prop)) {
valueStr = String.format("%.1f%%", value * 100f);
} else {
valueStr = String.format("%.0f", value);
}
if (targetPlayer == sender) {
CommandHandler.sendTranslatedMessage(sender, "commands.generic.set_to", stat.name, valueStr);
} else {
String uidStr = targetPlayer.getAccount().getId();
CommandHandler.sendTranslatedMessage(sender, "commands.generic.set_for_to", stat.name, uidStr, valueStr);
}
} else {
if (!args.isEmpty()) { // Leftover arguments!
sendUsageMessage(sender);
return;
}

switch (action) {
case ACTION_SET:
entity.setFightProperty(stat.prop, value);
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, stat.prop));
break;
case ACTION_LOCK:
avatar.getFightPropOverrides().put(stat.prop.getId(), value);
avatar.recalcStats();
break;
case ACTION_UNLOCK:
avatar.getFightPropOverrides().remove(stat.prop.getId());
avatar.recalcStats();
break;
}

// Report action
if (FightProperty.isPercentage(stat.prop)) {
valueStr = String.format("%.1f%%", value * 100f);
} else {
valueStr = String.format("%.0f", value);
}
if (targetPlayer == sender) {
CommandHandler.sendTranslatedMessage(sender, action.messageKeySelf, stat.name, valueStr);
} else {
String uidStr = targetPlayer.getAccount().getId();
CommandHandler.sendTranslatedMessage(sender, action.messageKeyOther, stat.name, uidStr, valueStr);
}
return;
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/emu/grasscutter/game/avatar/Avatar.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;

@Entity(value = "avatars", useDiscriminator = false)
public class Avatar {
Expand All @@ -85,6 +86,7 @@ public class Avatar {

@Transient private final Int2ObjectMap<GameItem> equips;
@Transient private final Int2FloatOpenHashMap fightProp;
@Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides;
@Transient private Set<String> extraAbilityEmbryos;

private List<Integer> fetters;
Expand All @@ -111,6 +113,7 @@ public class Avatar {
public Avatar() {
this.equips = new Int2ObjectOpenHashMap<>();
this.fightProp = new Int2FloatOpenHashMap();
this.fightPropOverrides = new Int2FloatOpenHashMap();
this.extraAbilityEmbryos = new HashSet<>();
this.proudSkillBonusMap = new HashMap<>();
this.fetters = new ArrayList<>(); // TODO Move to avatar
Expand Down Expand Up @@ -728,6 +731,9 @@ public void recalcStats(boolean forceSendAbilityChange) {
(getFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE) * (1f + getFightProperty(FightProperty.FIGHT_PROP_DEFENSE_PERCENT))) + getFightProperty(FightProperty.FIGHT_PROP_DEFENSE)
);

// Reapply all overrides
this.fightProp.putAll(this.fightPropOverrides);

// Set current hp
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);

Expand Down
6 changes: 5 additions & 1 deletion src/main/resources/languages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,11 @@
"description": "Sets accountwide properties. Things like godmode can be enabled this way, as well as changing things like unlocked abyss floor and battle pass progress.\n\tValues for <prop>: godmode | nostamina | unlimitedenergy | abyss | worldlevel | bplevel\n\t(cont.) see PlayerProperty enum for other possible values, of form PROP_MAX_SPRING_VOLUME -> max_spring_volume"
},
"setStats": {
"description": "Sets fight property for your current active character\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys"
"description": "Sets fight property for your current active character\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys",
"locked_to": "%s locked to %s.",
"locked_for_to": "%s for %s locked to %s.",
"unlocked": "%s unlocked.",
"unlocked_for": "%s for %s unlocked."
},
"spawn": {
"success": "Spawned %s of %s.",
Expand Down
20 changes: 14 additions & 6 deletions src/main/resources/languages/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,11 @@
"description": "Establece propiedades de la cuenta. Cosas como el modo Dios pueden ser establecidos con este comando, además de cambiar cosas como desbloquear pisos del abusmo o progreso del pase de batalla.\n\tValores para <prop>: godmode | nostamina | unlimitedenergy | abyss | worldlevel | bplevel\n\t(cont.) Observa PlayerProperty enum para ver otros posibles valores, de la forma PROP_MAX_SPRING_VOLUME -> max_spring_volume"
},
"setStats": {
"description": "Establece propiedades de combate para tu personaje actual\n\tValores para <estado>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus de daño elemental: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Resistencia elemental: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys"
"description": "Establece propiedades de combate para tu personaje actual\n\tValores para <estado>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus de daño elemental: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Resistencia elemental: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys",
"locked_to": "🇺🇸%s locked to %s.",
"locked_for_to": "🇺🇸%s for %s locked to %s.",
"unlocked": "🇺🇸%s unlocked.",
"unlocked_for": "🇺🇸%s for %s unlocked."
},
"spawn": {
"success": "Generados %s de %s.",
Expand Down Expand Up @@ -334,6 +338,10 @@
"invalid_time": "No se puede establecer la marca de tiempo.",
"description": "Beta a un jugador"
},
"unlockall": {
"success": "🇺🇸Unlocked all open states for %s.",
"description": "🇺🇸Unlocks all open states for a player."
},
"unban": {
"success": "Exitoso.",
"failure": "Error, jugador no encontrado.",
Expand All @@ -355,24 +363,24 @@
},
"documentation": {
"handbook": {
"title": "GM Handbook",
"title": "🇺🇸GM Handbook",
"title_commands": "Comandos",
"title_avatars": "Avatares",
"title_items": "Objetos",
"title_scenes": "Escenario",
"title_monsters": "Monstruos",
"header_id": "Id",
"header_id": "🇺🇸Id",
"header_command": "Comando",
"header_description": "Descripción",
"header_avatar": "Avatar",
"header_avatar": "🇺🇸Avatar",
"header_item": "Objeto",
"header_scene": "Escenario",
"header_monster": "Monstruo"
},
"index": {
"title": "Documentación",
"handbook": "GM Handbook",
"handbook": "🇺🇸GM Handbook",
"gacha_mapping": "JSON de mapeo del Gacha"
}
}
}
}
38 changes: 23 additions & 15 deletions src/main/resources/languages/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@
"send": "%s %s (niveau %s) ont été ajouté au message.\nContinuez d'ajouter plus d'objets ou utilisez '/sendmail finish' pour envoyer le message.",
"invalid_arguments_please_use": "Arguments invalides.\n Veuillez utiliser '/sendmail %s'",
"title": "<titre>",
"message": "<message>",
"message": "🇺🇸<message>",
"sender": "<expéditeur>",
"arguments": "<itemID|itemName|finish> [quantité] [niveau]",
"error": "ERREUR: Stade de construction invalide : %s. Vérifiez la console pour la pile d'appels.",
Expand All @@ -257,12 +257,12 @@
"description": "Envoie un message au joueur spécifié en tant que Serveur"
},
"setConst": {
"range_error": "Constellation level must be between 0 and 6.",
"level_error": "Invalid constellation level.",
"fail": "Failed to set constellation.",
"failed_success": "Constellations for %s have been set to %s. Please reload scene to see changes.",
"success": "Constellations for %s have been set to %s.",
"description": "Sets constellation level for your current active character"
"range_error": "🇺🇸Constellation level must be between 0 and 6.",
"level_error": "🇺🇸Invalid constellation level.",
"fail": "🇺🇸Failed to set constellation.",
"failed_success": "🇺🇸Constellations for %s have been set to %s. Please reload scene to see changes.",
"success": "🇺🇸Constellations for %s have been set to %s.",
"description": "🇺🇸Sets constellation level for your current active character"
},
"setFetterLevel": {
"range_error": "Le niveau d'affinité doit être compris entre 0 et 10.",
Expand All @@ -274,7 +274,11 @@
"description": "Définit des propriétes pour votre compte. Des choses comme le godemode peuvent être activés avec cette commande, et le déblocage de l'abysse ainsi que l'avancement du PB.\n\tValues for <prop>: godmode | nostamina | unlimitedenergy | abyss | worldlevel | bplevel\n\t(cont.) see PlayerProperty enum for other possible values, of form PROP_MAX_SPRING_VOLUME -> max_spring_volume"
},
"setStats": {
"description": "Définit les propriétés de combat de votre personnage actif\n\tValeurs pour <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus de dégât élémentaire: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Résistance élémentaire: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys"
"description": "Définit les propriétés de combat de votre personnage actif\n\tValeurs pour <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus de dégât élémentaire: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Résistance élémentaire: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys",
"locked_to": "🇺🇸%s locked to %s.",
"locked_for_to": "🇺🇸%s for %s locked to %s.",
"unlocked": "🇺🇸%s unlocked.",
"unlocked_for": "🇺🇸%s for %s unlocked."
},
"spawn": {
"success": " %s %s sont apparu.",
Expand Down Expand Up @@ -334,6 +338,10 @@
"invalid_time": "Impossible d'analyser le timestamp.",
"description": "Bannis un joueur"
},
"unlockall": {
"success": "🇺🇸Unlocked all open states for %s.",
"description": "🇺🇸Unlocks all open states for a player."
},
"unban": {
"success": "Succès.",
"failure": "Échec, joueur introuvable.",
Expand All @@ -349,30 +357,30 @@
},
"records": {
"title": "Historique de voeux",
"date": "Date",
"date": "🇺🇸Date",
"item": "Objet"
}
},
"documentation": {
"handbook": {
"title": "Manuel GM",
"title_commands": "Commandes",
"title_avatars": "Avatars",
"title_avatars": "🇺🇸Avatars",
"title_items": "Objets",
"title_scenes": "Scènes",
"title_monsters": "Monstres",
"header_id": "Id",
"header_id": "🇺🇸Id",
"header_command": "Commande",
"header_description": "Description",
"header_avatar": "Avatar",
"header_description": "🇺🇸Description",
"header_avatar": "🇺🇸Avatar",
"header_item": "Objet",
"header_scene": "Scène",
"header_monster": "Monstre"
},
"index": {
"title": "Documentation",
"title": "🇺🇸Documentation",
"handbook": "Manuel GM",
"gacha_mapping": "Gacha mapping JSON"
"gacha_mapping": "🇺🇸Gacha mapping JSON"
}
}
}
Loading