Compare commits

..
1364 changed files with 12996 additions and 64218 deletions
-20
View File
@@ -1,20 +0,0 @@
# Changelog
## [1.8.0 (MONTH Xth, 2026)](#1-8-0)
### Additions
- Added a new `cobblemon:structure`, which is a feature without any restrictions allowing the Builders to place certain structures in much more specific manner.
### Changes
### Fixes
- Fixed rare Pokémon being far too likely to spawn multiple times in a single spawn cycle. The dev told me that the exact bug is too complicated to explain.
### Developer
- Tweaked the spawning selector interface to take a bucket function rather than a bucket directly.
### Molang & Datapacks
### Particles
### Localization
- Updated translations for:
-16
View File
@@ -1,16 +0,0 @@
### Additions
### Changes
- Wild Pokémon now reliably have at least one offensive move, if they can have offensive moves at that level.
### Fixes
### Developer
### Molang & Datapacks
- Added `moveset_builders` datapack folder for different styles of movesets.
- Added `party_pools` datapack folder for defining groups of Pokémon for the purposes of complex NPC party definitions.
- Added `party_compositions` datapack folder for arranging parties on NPCs that rely on party pools.
- Added `composed_pool` NPC party provider type which combines a party pool with a party composition to create more complex NPC parties.
### Particles
+41 -38
View File
@@ -1,51 +1,54 @@
# Changelog
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
## [1.7.2 (MONTH Xth, YEAR)](#1-7-2)
### Additions
- Party Overlay Exp Gained Animation will now display the amount of EXP the Pokémon gained
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys??!
- Added new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
- Added missing crops to Botany Pots integration
### New rideable Pokémon
### Pokémon Added
### Model updates for the following Pokémon
### Animation updates for the following Pokémon
- Added riding statistics: `Distance by Pokémon on Land`, `Distance by Pokémon in Air` and `Distance by Pokémon in Liquid`.
- Added the Partner Mark, which party Pokémon have a very small chance to earn as the player walks, provided they have enough friendship.
- Added trades for Saccharine Saplings, Hearty Grains, Chipped Pot, and Masterpiece Teacup to the Wandering Trader.
- Added brewing recipe for Throat Spray.
- Added new effects
- Mental Restoration: Reduces "Time Since Last Rest" statistic, which controls Insomnia, by half a minute per second per effect level
- Negative Effect Cleanse: Removes all negative effects
- Effect Cleanse: Removes all effects
- Added more seasonings for sinister tea
- Mental Herb - Mental Restoration I for 10 seconds
- White Herb - Negative Effect Cleanse
- Milk - Effect Cleanse
- Moomoo Milk - Effect Cleanse
- Added `label` as an alternate key to the `tag` property.
### Changes
- Restructured Botany Pots integration folder structure
- Decrease default pastured mon wander radius to 32.
- Factor in the weight of pokemon when calculating pushing forces between hitboxes.
- Remade the Starter Selection Screen with new assets
- Refactored berry trees to be less impactful on TPS (more performant)
- Ride sounds have been separated into stereo for passengers and mono for other players. Riding will now sound more spacious.
- Added a new config option, `Enable In-Flight Dismounting` (default: off), which lets you dismount while riding a Pokémon in the air.
### Fixes
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
- Fix occasional crash on retrieving revived fossils
- Fixed a compatibility issue with World Game Rules which caused some log spam in specific cases.
- Fixed improper validation of Pokémon when performing a trade.
- Fixed issue where particle like beam can disappear when you don't see the beginning of the beam or you look away
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
- Fix occasional crash on retrieving revived fossils
- Fix brushing not working on Neoforge
- [Various Submarine Ride Fixes](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2157)
- Fix Pastured mons despawning and running away.
- Fix Apricorn trees crashing when other mods bonemeal it
- [Various UFO Ride Improvements](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2156)
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within it's menu
- Fixed NPCs switching pokemon very often during battles
- Fixed an issue in the OmniPathNodeMaker/Navigation that prevented Combees from pathing into and out of Saccharine leaves.
- Fixed orientation not getting reset after crashing while flying and getting back on the Pokémon.
- Fixed Furfrou not being trimmable on NeoForge.
- Fixed shiny Pokémon not respecting the silent flag on spawn.
- Fixed honey from Saccharine Leaves being harvestable at less than the max age.
- Items given from interacting with Saccharine Leaves are placed in the active hotbar slot if possible.
- Fix all Pokémon being saved to chunks and never despawning.
- Fixed PokeSnacks crashing if there was no available spawn.
- Fixed Pokémon spawn influences not being specific to regional forms
- Fixed crashing when viewing another player in spectator mode
- Fixed missing apricorn textures for Adorn blocks
- Fixed crashing when riding due to malformed json file, instead resetting it
- Fixed mochi items consuming two items at a time
- Fix item interaction sometimes not working properly when playing on servers.
- Fixed Soothe Bell not being properly tagged as a held item.
- Fixed Mint Leaves not being usable for filling the Resurrection Machine.
- Added Saccharine Boats and Saccharine Boat with Chests to relevant boat tags.
- Fixed the `run_script` Molang function to not fail if the environment's context is null.
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
- Changed the `owner` parameter in the `OwnerQueryRequirement` interface from `ServerPlayer` to `Player`. This method is now also called on the client to verify whether a Pokémon interaction succeeded, so make sure to update your implementations to handle both server and client contexts.
### Molang & Datapacks
- q.item.is_enchanted()
- q.item.has_enchantment(minecraft:sharpness, 3)
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
- Ride sounds can now be set as exclusive to passengers
- Ride sounds no longer play when submerged unless specified with a new setting
### Particles
### Localization
- Updated translations for:
- Updated translations for:
-137
View File
@@ -1,137 +0,0 @@
# Changelog
## [1.7.2 (January 25th, 2026)](#1-7-2)
### Additions
- Added Saccharine wood furniture for Adorn.
- Added the Partner Mark, which party Pokémon have a very small chance to earn as the player walks, provided they have enough friendship.
- Added decorative block models for existing items: Potion, Super Potion, Hyper Potion, Max Potion, Full Restore, Antidote, Awakening, Burn Heal, Ice Heal, Paralyze Heal, Full Heal, Ether, Elixir, Max Ether, Max Elixir, X Accuracy, X Attack, X Defence, X Special Attack, X Special Defence, X Speed, Dire Hit, Guard Spec, Weakness Policy, Blunder Policy, Cleanse Tag, and Spell Tag.
- Added decorative sign blocks called Plaques.
- Added ride style labels to ride stats in Pokédex, as well as ride style icons in the summary GUI.
- Added more seasonings for sinister tea:
- Mental Herb - Mental Restoration I for 10 seconds, which staves off Insomnia.
- White Herb - Removes negative effects.
- Milk, Moomoo Milk - Removes effects.
- Added dispenser support for:
- Applying a Honey Bottle to Saccharine Logs.
- Washing honey off of Saccharine Slathered Logs.
- Applying and removing honey on Saccharine Leaves.
- Added riding statistics: `Distance by Pokémon on Land`, `Distance by Pokémon in Air` and `Distance by Pokémon in Liquid`.
### New rideable Pokémon
- Honchkrow
- Mantine
### Pokémon Added
#### Gen 5
- Throh
- Sawk
#### Gen 6
- Litleo
- Pyroar
- Spritzee
- Aromatisse
- Swirlix
- Slurpuff
### Model updates for the following Pokémon
- Falinks
- Cottonee
- Whimsicott
- Ferroseed
- Ferrothorn
- Chingling
- Chimecho
### Animation updates for the following Pokémon
- Mantine (ride animations)
- Flygon (ride jump animation)
- Honchkrow (ride animations)
- Croagunk
- Toxicroak
### Changes
- Added a new config option, `Enable In-Flight Dismounting` (default: off), which lets you dismount while riding a Pokémon in the air. This is the textbook definition of caveat emptor.
- Ride sounds have been separated into stereo for passengers and mono for other players. Riding will now sound more spacious.
- Battle AI now uses a smarter threshold for switching, reducing unnecessary switches.
- AI will always use the most damaging move when at low HP and when it is not switching, improving endgame decision-making.
- Items given from interacting with Saccharine Leaves are placed in the active hotbar slot if possible.
- Added `label` as an alternate key to the `tag` property.
### Fixes
- Fixed crashing when viewing another player in spectator mode. Some said this was reason to release this update a month ago. They were probably right.
- Fixed crashing when sending a Pokémon out after editing its form.
- Fixed all Pokémon being saved to chunks and never despawning even when they should be.
- Fixed Poké Snacks crashing if there was no available spawn.
- Fixed Poké Snack / Bait effects so that EV Yield, Type, and Egg Group filters now all apply together when weighting spawns. And you thought you were going crazy.
- Fixed Furfrou not being trimmable on NeoForge specifically.
- Fixed honey from Saccharine Leaves being harvestable at less than the max age.
- Fixed possible error on world generation when Combee are placed in naturally generated bee nests.
- Fixed Galarian Weezing crashing the world if it decides to blink the wrong way. The same could happen to you!
- Fixed Galarian Mr. Mime incorrectly being rideable. Some would argue that regular Mr. Mime shouldn't be rideable, but that would make them wrong.
- Fixed missing apricorn textures for Adorn blocks.
- Fixed Apricorn block model rotation for counters/sinks/cupboards/drawers to match that of most recent version of Adorn.
- Fixed item interaction sometimes not working properly when playing on servers.
- Fixed Mint Leaves not being usable for filling the Resurrection Machine.
- Fixed Super Potion recipes incorrectly using Hondew Berry instead of Aguav Berry. They look similar, to be fair.
- Fixed Wishiwashi not schooling properly.
- Fixed battle item duplication issue. How did it work? Wouldn't you like to know, weather boy.
- Fixed the ordering of Aprijuice's riding stat boosts so they always have the same order.
- Fixed the movesets of certain Pokémon being incorrectly sanitized upon reload, causing some forms to keep losing moves.
- Fixed selected leading Pokémon in battle not being used when levels are set.
- Fixed the bottom half of pasture blocks having an incorrect hit-box when facing north or south.
- Fixed gimmicks not being usable in double and triple battles.
- Fixed gimmick buttons showing when already used in the same turn in double and triple battles.
- Fixed mochi items consuming two items at a time.
- Fixed Fresh Start Mochi's EV reset not syncing health changes properly.
- Fixed Pokémon spawn influences not being specific to regional forms.
- Fixed memory leak from battles not being cleaned up after ending.
- Fixed orientation not getting reset after crashing while flying and getting back on the Pokémon.
- Fixed shiny Pokémon not respecting the `silent` flag on spawn.
- Fixed 'Plain' prefix not being applied to all low-quality Aprijuice.
- Fixed Pokémon interactions not playing sound effects when the volume was not specified.
- Fixed Pokémon interaction cooldowns taking longer than intended when the Pokémon was in the player’s party.
- Fixed Pokémon interactions occasionally crashing the game when spamming an interaction with a 0 cooldown.
- Fixed Pokémon fullness decreasing more than intended when the Pokémon was pastured.
- Fixed Scraggy line, Milcery line, and Morpeko spawning on unnatural blocks.
- Fixed invulnerability-bypassed damage being resisted by invulnerable NPC entities.
- Fixed Fabric network packets not being handled immediately, fixing incompatibilities with Supplementaries, Prometheus, Sleep Tight, and others.
- Fixed killer not being set early enough on a wild Pokémon entity from `WinInstruction`.
- Fixed requirements such as `biomeCondition` crashing the game when used to define Pokémon interactions.
- Fixed an issue that prevented registering custom dispenser behaviours (no longer overrides vanilla behaviour).
- Fixed an issue navigation that prevented Combee from pathing into and out of Saccharine Leaves.
- Added Saccharine Boats and Saccharine Boat with Chests to relevant boat tags.
- Fixed Soothe Bell not being properly tagged as a held item.
- Fixed `/calculateseatpositions` expecting a locator format that even we don't use. It needs an underscore after "seat".
- Fixed the `run_script` Molang function to not fail if the environment's context is null.
- Fixed the Molang functions `date_local_time`, `date_of`, `date_is_after` to use the correct date format.
### Developer
- Changed the `owner` parameter in the `OwnerQueryRequirement` interface from `ServerPlayer` to `Player`. This method is now also called on the client to verify whether a Pokémon interaction succeeded, so make sure to update your implementations to handle both server and client contexts.
- Added the `pnx` to the `BattleFaintedEvent` and `FormeChangeEvent`.
- Added register methods for custom instructions to `ShowdownInterpreter`.
- Changed callback operations in `BattlePokemon` to allow multiple callbacks and be mutable.
- Added `display_name`, `description`, and `max_pp` functions to the `MoveTemplate` struct.
- Changed the implementations of `AIBattleActor` to be open classes to allow extension.
- Changed the resource location of Poké Ball icons in the GUI to use the balls namespace instead of Cobblemon's, allowing mods to properly use their own namespace for these textures.
### Molang & Datapacks
- Ride sounds can now be set as exclusive to passengers.
- Ride sounds no longer play when submerged unless specified with a new setting.
- Exception handling has been added to `run_molang`, resolving some crashes caused by malformed MoLang expressions.
- Added the `create_itemstack`, `has_inventory_space`, `set_inventory_slot`, and `give_item` MoLang functions for item and inventory utility.
- Added the `get_move_from_id` MoLang function for general move info queries without requiring a Moveset object to access.
- Added support for optional message variants in battle activate instructions for more context-specific battle text.
- Example: `this.add('-activate', pokemon, 'ability: example', '[msg]message1');` will parse to lang key `cobblemon.battle.activate.example.message1`.
### Localization
- Updated translations for:
- French
- Canadian French
- Simplified Chinese
- Ukrainian
- Brazilian Portuguese
- Spanish
- Korean
- Japanese
-22
View File
@@ -1,22 +0,0 @@
# Changelog
## [1.7.3 (January 31st, 2026)](#1-7-3)
### Fixes
- Fixed mounted Pokémon not playing sounds.
- Fixed players failing to join servers randomly.
- Fixed some Pokémon having no stamina for flying.
- Fixed `/pctake` not respecting the actual size of the PC.
### Developer
- Poké Ball GUI textures now respect the ball's namespace, enabling proper texture loading for custom balls from other mods.
### Molang & Datapacks
- Added a `chance` requirement type for Pokémon interactions.
### Localization
- Updated translations for:
- French
- Canadian French
- Korean
- Portuguese
- Simplified Chinese
- Spanish
@@ -11,9 +11,7 @@ package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.CobblemonMemories;
import com.cobblemon.mod.common.api.ai.config.task.PathToBeeHiveTaskConfig;
import com.cobblemon.mod.common.api.pokemon.PokemonProperties;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.world.feature.SaccharineTreeFeature;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.tags.BlockTags;
@@ -44,21 +42,7 @@ public abstract class BeeOccupantMixin {
if (cir.getReturnValue() == null) {
final BeehiveBlockEntity.Occupant occupant = (BeehiveBlockEntity.Occupant) (Object) this;
CompoundTag compoundTag = occupant.entityData().copyTag();
String id = compoundTag.getString("id");
Entity entity = null;
if (id.equals(SaccharineTreeFeature.Companion.getPOKEMON_PLACEHOLD_ID())) {
// Special case for Pokemon stuffed into a beenest upon world gen.
// For technical reasons we cannot create them as full entities during world gen.
// So we look for this special id tag and complete the entity when it attempts to exit a hive.
if (compoundTag.contains("pokemonProperties")) {
String properties = compoundTag.getString("pokemonProperties");
PokemonProperties pokemon = PokemonProperties.Companion.parse(properties);
entity = pokemon.createEntity(level);
}
} else {
entity = EntityType.loadEntityRecursive(compoundTag, level, (entityx) -> entityx);
}
Entity entity = EntityType.loadEntityRecursive(compoundTag, level, (entityx) -> entityx);
if (entity instanceof PokemonEntity pokemonEntity) {
var state = level.getBlockState(pos);
var newPos = Vec3.ZERO;
@@ -15,8 +15,6 @@ import com.cobblemon.mod.common.api.events.CobblemonEvents;
import com.cobblemon.mod.common.api.events.item.LeftoversCreatedEvent;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies;
import com.cobblemon.mod.common.api.riding.RidingStyle;
import com.cobblemon.mod.common.api.riding.behaviour.types.air.HoverBehaviour;
import com.cobblemon.mod.common.api.storage.party.PartyStore;
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore;
import com.cobblemon.mod.common.api.tags.CobblemonItemTags;
@@ -61,7 +59,6 @@ import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@@ -172,32 +169,6 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
}
}
@Redirect(
method = "getDestroySpeed",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;onGround()Z"
)
)
private boolean cobblemon$considerHoverOnGround(Player player) {
// If riding a Hover Mount, return true to bypass the 5x slowdown
if (player.getVehicle() instanceof PokemonEntity vehicle) {
var ridingController = vehicle.getRidingController();
if (ridingController == null) player.onGround();
var activeContext = ridingController.getContext();
if (activeContext == null) player.onGround();
if (activeContext.getStyle() == RidingStyle.AIR &&
activeContext.getBehaviour().equals(HoverBehaviour.Companion.getKEY())) {
return true;
}
}
// Otherwise, return the actual value
return player.onGround();
}
@Inject(
method = "eat",
at = @At(
@@ -1,26 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.world.item.Item;
import net.minecraft.core.dispenser.DispenseItemBehavior;
import net.minecraft.world.level.block.DispenserBlock;
import java.util.Map;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(DispenserBlock.class)
public interface DispenserBlockAccessor {
@Accessor("DISPENSER_REGISTRY")
static Map<Item, DispenseItemBehavior> getItemBehaviors() {
throw new AssertionError();
}
}
@@ -35,7 +35,6 @@ import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts;
import static com.google.common.primitives.Floats.min;
import static net.minecraft.util.Mth.lerp;
@@ -86,13 +85,11 @@ public abstract class MouseHandlerMixin {
CobblemonClient.INSTANCE.getStorage().shiftSelected(false);
ci.cancel();
PartySendBinding.INSTANCE.actioned();
PartyTutorialToasts.INSTANCE.onHoldKeyScrollSwitchedSlot();
} else if (i < 0) {
accumulatedScrollY -= i;
CobblemonClient.INSTANCE.getStorage().shiftSelected(true);
ci.cancel();
PartySendBinding.INSTANCE.actioned();
PartyTutorialToasts.INSTANCE.onHoldKeyScrollSwitchedSlot();
}
}
}
@@ -313,7 +310,7 @@ public abstract class MouseHandlerMixin {
var playerRotater = (RidePassenger)player;
playerRotater.cobblemon$setRideXRot(playerRotater.cobblemon$getRideXRot() + f);
playerRotater.cobblemon$setRideYRot(playerRotater.cobblemon$getRideYRot() + g);
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -89.0F, 89.0F));
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -90.0F, 90.0F));
playerRotater.cobblemon$setRideYRot(Mth.clamp(playerRotater.cobblemon$getRideYRot(), -105.0F, 105.0F));
if (player.getVehicle() != null) {
player.getVehicle().onPassengerTurned(player);
@@ -14,14 +14,9 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokemon.Pokemon;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
@@ -94,61 +89,4 @@ public abstract class EntityMixin {
cir.setReturnValue(customEyePos);
}
@WrapOperation(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 0)
)
public void cobblemon$customPusherLogic(Entity pusher, double x, double y, double z,
Operation<Void> original,
@Local(argsOnly = true) Entity pushee,
@Local(name = "e") double e, @Local(name = "d") double d,
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
pusherIsPlayerOrPokemon.set(pusher instanceof PokemonEntity || pusher instanceof Player);
pusheeIsPlayerOrPokemon.set(pushee instanceof PokemonEntity || pushee instanceof Player);
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
original.call(pusher, x, y, z);
return;
}
/**********************************************
* Custom portion of the code:
* Handle collisions with weight in mind. Use
* the weight value of pokemon to determine
* which side gets what portion of the total
* force.
*********************************************/
// Weights in hectograms
// Check to see if the pusher or pushee is a player. 200ish pounds. Steve's like a 6'6 miner
var pusherWeight = pusher instanceof PokemonEntity pusherPokemon ? pusherPokemon.getPokemon().getSpecies().getWeight() : 900F;
var pusheeWeight = pushee instanceof PokemonEntity pusheePokemon ? pusheePokemon.getPokemon().getSpecies().getWeight() : 900F;
// Calculate the portion of force received for both entities. Bigger receives less from the
// collision than the smaller one.
var totalWeight = pusherWeight + pusheeWeight;
var pusherReceivedForce = pusheeWeight / totalWeight * 2F;
pusheeReceivedForce.set((pusherWeight / totalWeight) * 2F);
original.call(pusher, -d * pusherReceivedForce, 0.0, -e * pusherReceivedForce);
}
@WrapOperation(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 1)
)
public void cobblemon$customPusheeLogic(Entity pusher, double x, double y, double z, Operation<Void> original,
@Local(name = "e") double e, @Local(name = "d") double d,
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
original.call(pusher, x, y, z);
return;
}
original.call(pusher, d * pusheeReceivedForce.get(), 0.0, e * pusheeReceivedForce.get());
}
}
@@ -65,14 +65,10 @@ import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataMongoBacken
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
import com.cobblemon.mod.common.api.tags.CobblemonEntityTypeTags
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.ShowdownThread
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.block.dispenser.DispenserBehaviorRegistry
import com.cobblemon.mod.common.block.DispenserBehaviorRegistry
import com.cobblemon.mod.common.client.CobblemonPack
import com.cobblemon.mod.common.command.argument.*
import com.cobblemon.mod.common.config.CobblemonConfig
@@ -80,12 +76,7 @@ import com.cobblemon.mod.common.config.LastChangedVersion
import com.cobblemon.mod.common.config.constraint.IntConstraint
import com.cobblemon.mod.common.config.starter.StarterConfig
import com.cobblemon.mod.common.data.CobblemonDataProvider
import com.cobblemon.mod.common.events.AdvancementHandler
import com.cobblemon.mod.common.events.CallbackHandler
import com.cobblemon.mod.common.events.EntityCallbackHandler
import com.cobblemon.mod.common.events.PokedexHandler
import com.cobblemon.mod.common.events.ServerTickHandler
import com.cobblemon.mod.common.events.StatHandler
import com.cobblemon.mod.common.events.*
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
@@ -99,23 +90,11 @@ import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatu
import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatureProvider
import com.cobblemon.mod.common.pokemon.feature.TagSeasonResolver
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.BattleCloneProperty
import com.cobblemon.mod.common.pokemon.properties.FreezeFrameProperty
import com.cobblemon.mod.common.pokemon.properties.HiddenAbilityPropertyType
import com.cobblemon.mod.common.pokemon.properties.*
import com.cobblemon.mod.common.pokemon.properties.LabelProperty
import com.cobblemon.mod.common.pokemon.properties.NoAIProperty
import com.cobblemon.mod.common.pokemon.properties.UnaspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.ifDedicatedServer
import com.cobblemon.mod.common.util.isLaterVersion
import com.cobblemon.mod.common.util.party
import com.cobblemon.mod.common.util.requestWallpapers
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.world.feature.CobblemonPlacedFeatures
import com.cobblemon.mod.common.world.feature.ore.CobblemonOrePlacedFeatures
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
@@ -123,17 +102,6 @@ import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import net.minecraft.client.Minecraft
import net.minecraft.commands.synchronization.SingletonArgumentInfo
import net.minecraft.resources.ResourceKey
@@ -146,6 +114,17 @@ import net.minecraft.world.level.Level
import net.minecraft.world.level.storage.LevelResource
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
object Cobblemon {
const val MODID = CobblemonBuildDetails.MOD_ID
@@ -186,7 +165,7 @@ object Cobblemon {
val statistics: CobblemonStats = CobblemonStats
@JvmStatic
val builtinPacks = listOf(
val builtinPacks = listOf<CobblemonPack>(
CobblemonPack(id = "adorncompatibility", name = "Adorn Compatibility", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.ALWAYS_ENABLED, neededMods = setOf("adorn")),
CobblemonPack(id = "gyaradosjump", name = "Gyarados Jump Patterns", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED),
CobblemonPack(id = "regionbiasforms", name = "Region Bias Forms", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED),
@@ -472,7 +451,7 @@ object Cobblemon {
server()?.getLevel(dimension)
} else {
val mc = Minecraft.getInstance()
mc.singleplayerServer?.getLevel(dimension) ?: mc.level
return mc.singleplayerServer?.getLevel(dimension) ?: mc.level
}
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.apricorn.Apricorn
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.block.*
import com.cobblemon.mod.common.block.campfirepot.CampfireBlock
import com.cobblemon.mod.common.block.LecternBlock
@@ -42,11 +41,11 @@ import net.minecraft.world.level.block.state.BlockBehaviour
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.properties.BlockSetType
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.level.block.state.properties.BlockStateProperties.LIT
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument
import net.minecraft.world.level.block.state.properties.WoodType
import net.minecraft.world.level.material.MapColor
import net.minecraft.world.level.material.PushReaction
import net.minecraft.world.phys.shapes.Shapes
@Suppress("SameParameterValue", "HasPlatformType", "MemberVisibilityCanBePrivate", "unused")
object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<Block>>, Block>() {
@@ -244,9 +243,9 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
val REVIVAL_HERB = this.create("revival_herb", RevivalHerbBlock(BlockBehaviour.Properties.of().pushReaction(PushReaction.DESTROY).mapColor(MapColor.PLANT).ignitedByLava().noCollission().instabreak().sound(CobblemonSounds.REVIVAL_HERB_SOUNDS)))
@JvmField
val POKE_CAKE = this.create("poke_cake", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(BlockStateProperties.LIT)) 3 else 0 }, false))
val POKE_CAKE = this.create("poke_cake", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(LIT)) 3 else 0 }, false))
@JvmField
val POKE_SNACK = this.create("poke_snack", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(BlockStateProperties.LIT)) 3 else 0 }, true))
val POKE_SNACK = this.create("poke_snack", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(LIT)) 3 else 0 }, true))
@JvmField
val TUMBLESTONE_CLUSTER = tumblestoneBlock("tumblestone_cluster", GrowableStoneBlock.STAGE_3, 7, 3, null)
@@ -478,99 +477,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val GIMMIGHOUL_CHEST = create("gimmighoul_chest", GildedChestBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.CHEST).noOcclusion().sound(CobblemonSounds.GILDED_CHEST_SOUNDS), GildedChestBlock.Type.FAKE))
@JvmField
val WHITE_PLAQUE = create("white_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val LIGHT_GRAY_PLAQUE = create("light_gray_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val GRAY_PLAQUE = create("gray_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val BLACK_PLAQUE = create("black_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val BROWN_PLAQUE = create("brown_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val RED_PLAQUE = create("red_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val ORANGE_PLAQUE = create("orange_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val YELLOW_PLAQUE = create("yellow_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val LIME_PLAQUE = create("lime_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val GREEN_PLAQUE = create("green_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val CYAN_PLAQUE = create("cyan_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val LIGHT_BLUE_PLAQUE = create("light_blue_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val BLUE_PLAQUE = create("blue_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val PURPLE_PLAQUE = create("purple_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val MAGENTA_PLAQUE = create("magenta_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val PINK_PLAQUE = create("pink_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val CLEANSE_TAG = create("cleanse_tag", WallAttachedStackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion().noCollission(), WallAttachedStackableItemBlock.WallStackableItemBlockType.TAG))
@JvmField
val SPELL_TAG = create("spell_tag", WallAttachedStackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion().noCollission(), WallAttachedStackableItemBlock.WallStackableItemBlockType.TAG))
@JvmField
val BLUNDER_POLICY = create("blunder_policy", WallAttachedDirectionalShapeBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion().noCollission(), 14, 12, 1))
@JvmField
val WEAKNESS_POLICY = create("weakness_policy", WallAttachedDirectionalShapeBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion().noCollission(), 14, 12, 1))
@JvmField
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ANTIDOTE = create("antidote", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val AWAKENING = create("awakening", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val BURN_HEAL = create("burn_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ICE_HEAL = create("ice_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val PARALYZE_HEAL = create("paralyze_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ETHER = create("ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val MAX_ETHER = create("max_ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val ELIXIR = create("elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val MAX_ELIXIR = create("max_elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val POTION = create("potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val SUPER_POTION = create("super_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val HYPER_POTION = create("hyper_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val MAX_POTION = create("max_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
@JvmField
val FULL_RESTORE = create("full_restore", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
@JvmField
val DIRE_HIT = create("dire_hit", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val GUARD_SPEC = create("guard_spec", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_ACCURACY = create("x_${Stats.ACCURACY.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_ATTACK = create("x_${Stats.ATTACK.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_DEFENSE = create("x_${Stats.DEFENCE.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_SP_ATK = create("x_${Stats.SPECIAL_ATTACK.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_SP_DEF = create("x_${Stats.SPECIAL_DEFENCE.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_SPEED = create("x_${Stats.SPEED.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val MONITOR = create("monitor", MonitorBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).requiresCorrectToolForDrops().strength(5.0F, 6.0F).lightLevel { if (it.getValue(MonitorBlock.SCREEN) != MonitorBlock.MonitorScreen.OFF) 13 else 0 }))
@JvmField
@@ -74,8 +74,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val NPC_EDITOR = create("npc_editor", CobblemonItem(Item.Properties().stacksTo(1)))
@JvmField
val POKEEDIT_WAND = create("pokeedit_wand", PokeEditWandItem())
@JvmField
val pokeBalls = mutableListOf<PokeBallItem>()
@@ -509,23 +507,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val SCROLL_OF_DARKNESS = noSettingsItem("scroll_of_darkness")
@JvmField val SCROLL_OF_WATERS = noSettingsItem("scroll_of_waters")
@JvmField val WHITE_PLAQUE = blockItem("white_plaque", CobblemonBlocks.WHITE_PLAQUE)
@JvmField val LIGHT_GRAY_PLAQUE = blockItem("light_gray_plaque", CobblemonBlocks.LIGHT_GRAY_PLAQUE)
@JvmField val GRAY_PLAQUE = blockItem("gray_plaque", CobblemonBlocks.GRAY_PLAQUE)
@JvmField val BLACK_PLAQUE = blockItem("black_plaque", CobblemonBlocks.BLACK_PLAQUE)
@JvmField val BROWN_PLAQUE = blockItem("brown_plaque", CobblemonBlocks.BROWN_PLAQUE)
@JvmField val RED_PLAQUE = blockItem("red_plaque", CobblemonBlocks.RED_PLAQUE)
@JvmField val ORANGE_PLAQUE = blockItem("orange_plaque", CobblemonBlocks.ORANGE_PLAQUE)
@JvmField val YELLOW_PLAQUE = blockItem("yellow_plaque", CobblemonBlocks.YELLOW_PLAQUE)
@JvmField val LIME_PLAQUE = blockItem("lime_plaque", CobblemonBlocks.LIME_PLAQUE)
@JvmField val GREEN_PLAQUE = blockItem("green_plaque", CobblemonBlocks.GREEN_PLAQUE)
@JvmField val CYAN_PLAQUE = blockItem("cyan_plaque", CobblemonBlocks.CYAN_PLAQUE)
@JvmField val LIGHT_BLUE_PLAQUE = blockItem("light_blue_plaque", CobblemonBlocks.LIGHT_BLUE_PLAQUE)
@JvmField val BLUE_PLAQUE = blockItem("blue_plaque", CobblemonBlocks.BLUE_PLAQUE)
@JvmField val PURPLE_PLAQUE = blockItem("purple_plaque", CobblemonBlocks.PURPLE_PLAQUE)
@JvmField val MAGENTA_PLAQUE = blockItem("magenta_plaque", CobblemonBlocks.MAGENTA_PLAQUE)
@JvmField val PINK_PLAQUE = blockItem("pink_plaque", CobblemonBlocks.PINK_PLAQUE)
private val berries = mutableMapOf<ResourceLocation, BerryItem>()
// Plants
@JvmField val CHERI_BERRY = berryItem("cheri", StatusCuringBerryItem(CobblemonBlocks.CHERI_BERRY, Statuses.PARALYSIS))
@@ -718,15 +699,15 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
@JvmField
val POTION = create("potion", PotionItem(PotionType.POTION, CobblemonBlocks.POTION))
val POTION = create("potion", PotionItem(PotionType.POTION))
@JvmField
val SUPER_POTION = create("super_potion", PotionItem(PotionType.SUPER_POTION, CobblemonBlocks.SUPER_POTION))
val SUPER_POTION = create("super_potion", PotionItem(PotionType.SUPER_POTION))
@JvmField
val HYPER_POTION = create("hyper_potion", PotionItem(PotionType.HYPER_POTION, CobblemonBlocks.HYPER_POTION))
val HYPER_POTION = create("hyper_potion", PotionItem(PotionType.HYPER_POTION))
@JvmField
val MAX_POTION = create("max_potion", PotionItem(PotionType.MAX_POTION, CobblemonBlocks.MAX_POTION))
val MAX_POTION = create("max_potion", PotionItem(PotionType.MAX_POTION))
@JvmField
val FULL_RESTORE = create("full_restore", PotionItem(PotionType.FULL_RESTORE, CobblemonBlocks.FULL_RESTORE))
val FULL_RESTORE = create("full_restore", PotionItem(PotionType.FULL_RESTORE))
@JvmField
val HEAL_POWDER = create("heal_powder", HealPowderItem())
@@ -811,27 +792,28 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SERIOUS_MINT = mintItem("serious_mint", MintItem(Natures.SERIOUS))
@JvmField val X_ACCURACY = create("x_${Stats.ACCURACY.identifier.path}", XStatItem(Stats.ACCURACY, block = CobblemonBlocks.X_ACCURACY))
@JvmField val X_ATTACK = create("x_${Stats.ATTACK.identifier.path}", XStatItem(Stats.ATTACK, block = CobblemonBlocks.X_ATTACK))
@JvmField val X_DEFENSE = create("x_${Stats.DEFENCE.identifier.path}", XStatItem(Stats.DEFENCE, block = CobblemonBlocks.X_DEFENSE))
@JvmField val X_SP_ATK = create("x_${Stats.SPECIAL_ATTACK.identifier.path}", XStatItem(Stats.SPECIAL_ATTACK, block = CobblemonBlocks.X_SP_ATK))
@JvmField val X_SP_DEF = create("x_${Stats.SPECIAL_DEFENCE.identifier.path}", XStatItem(Stats.SPECIAL_DEFENCE, block = CobblemonBlocks.X_SP_DEF))
@JvmField val X_SPEED = create("x_${Stats.SPEED.identifier.path}", XStatItem(Stats.SPEED, block = CobblemonBlocks.X_SPEED))
@JvmField val X_ACCURACY = create("x_${Stats.ACCURACY.identifier.path}", XStatItem(Stats.ACCURACY))
@JvmField val X_ATTACK = create("x_${Stats.ATTACK.identifier.path}", XStatItem(Stats.ATTACK))
@JvmField val X_DEFENSE = create("x_${Stats.DEFENCE.identifier.path}", XStatItem(Stats.DEFENCE))
@JvmField val X_SP_ATK = create("x_${Stats.SPECIAL_ATTACK.identifier.path}", XStatItem(Stats.SPECIAL_ATTACK))
@JvmField val X_SP_DEF = create("x_${Stats.SPECIAL_DEFENCE.identifier.path}", XStatItem(Stats.SPECIAL_DEFENCE))
@JvmField val X_SPEED = create("x_${Stats.SPEED.identifier.path}", XStatItem(Stats.SPEED))
@JvmField val DIRE_HIT = create("dire_hit", DireHitItem(CobblemonBlocks.DIRE_HIT))
@JvmField val GUARD_SPEC = create("guard_spec", GuardSpecItem(CobblemonBlocks.GUARD_SPEC))
@JvmField val DIRE_HIT = create("dire_hit", DireHitItem())
@JvmField val GUARD_SPEC = create("guard_spec", GuardSpecItem())
@JvmField val FULL_HEAL = create("full_heal", StatusCureItem("item.cobblemon.full_heal", block = CobblemonBlocks.FULL_HEAL))
@JvmField val ANTIDOTE = create("antidote", StatusCureItem("item.cobblemon.antidote", Statuses.POISON, Statuses.POISON_BADLY, block = CobblemonBlocks.ANTIDOTE))
@JvmField val AWAKENING = create("awakening", StatusCureItem("item.cobblemon.awakening", Statuses.SLEEP, block = CobblemonBlocks.AWAKENING))
@JvmField val BURN_HEAL = create("burn_heal", StatusCureItem("item.cobblemon.burn_heal", Statuses.BURN, block = CobblemonBlocks.BURN_HEAL))
@JvmField val ICE_HEAL = create("ice_heal", StatusCureItem("item.cobblemon.ice_heal", Statuses.FROZEN, block = CobblemonBlocks.ICE_HEAL))
@JvmField val PARALYZE_HEAL = create("paralyze_heal", StatusCureItem("item.cobblemon.paralyze_heal", Statuses.PARALYSIS, block = CobblemonBlocks.PARALYZE_HEAL))
@JvmField val BURN_HEAL = create("burn_heal", StatusCureItem("item.cobblemon.burn_heal", Statuses.BURN))
@JvmField val PARALYZE_HEAL = create("paralyze_heal", StatusCureItem("item.cobblemon.paralyze_heal", Statuses.PARALYSIS))
@JvmField val ICE_HEAL = create("ice_heal", StatusCureItem("item.cobblemon.ice_heal", Statuses.FROZEN))
@JvmField val ANTIDOTE = create("antidote", StatusCureItem("item.cobblemon.antidote", Statuses.POISON, Statuses.POISON_BADLY))
@JvmField val AWAKENING = create("awakening", StatusCureItem("item.cobblemon.awakening", Statuses.SLEEP))
@JvmField val ETHER = create("ether", EtherItem(max = false, CobblemonBlocks.ETHER))
@JvmField val MAX_ETHER = create("max_ether", EtherItem(max = true, CobblemonBlocks.MAX_ETHER))
@JvmField val ELIXIR = create("elixir", ElixirItem(max = false, CobblemonBlocks.ELIXIR))
@JvmField val MAX_ELIXIR = create("max_elixir", ElixirItem(max = true, CobblemonBlocks.MAX_ELIXIR))
@JvmField val FULL_HEAL = create("full_heal", StatusCureItem("item.cobblemon.full_heal"))
@JvmField val ETHER = create("ether", EtherItem(max = false))
@JvmField val MAX_ETHER = create("max_ether", EtherItem(max = true))
@JvmField val ELIXIR = create("elixir", ElixirItem(max = false))
@JvmField val MAX_ELIXIR = create("max_elixir", ElixirItem(max = true))
@JvmField
val ABILITY_CAPSULE = this.create("ability_capsule", AbilityChangeItem(AbilityChanger.COMMON_ABILITY))
@@ -940,7 +922,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val BLACK_SLUDGE = heldItem("black_sludge")
@JvmField
val BLUNDER_POLICY = itemNameBlockItem("blunder_policy", CobblemonBlocks.BLUNDER_POLICY)
val BLUNDER_POLICY = heldItem("blunder_policy")
@JvmField
val CELL_BATTERY = heldItem("cell_battery")
@JvmField
@@ -948,7 +930,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val CHOICE_SCARF = heldItem("choice_scarf")
@JvmField
val CLEANSE_TAG = itemNameBlockItem("cleanse_tag", CobblemonBlocks.CLEANSE_TAG)
val CLEANSE_TAG = heldItem("cleanse_tag")
@JvmField
val CLEAR_AMULET = heldItem("clear_amulet")
@JvmField
@@ -1056,7 +1038,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SOFT_SAND = heldItem("soft_sand")
@JvmField
val SPELL_TAG = itemNameBlockItem("spell_tag", CobblemonBlocks.SPELL_TAG)
val SPELL_TAG = heldItem("spell_tag")
@JvmField
val SMOKE_BALL = heldItem("smoke_ball")
@JvmField
@@ -1074,7 +1056,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val UTILITY_UMBRELLA = heldItem("utility_umbrella")
@JvmField
val WEAKNESS_POLICY = itemNameBlockItem("weakness_policy", CobblemonBlocks.WEAKNESS_POLICY)
val WEAKNESS_POLICY = heldItem("weakness_policy")
@JvmField
val WIDE_LENS = heldItem("wide_lens")
@JvmField
@@ -1571,7 +1553,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
}
}
)
private fun heldItem(name: String, item: Item, remappedName: String? = null) = create(
name = name,
entry = item.also {
@@ -1,55 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.moves.MoveSelector
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderAdapter
import com.cobblemon.mod.common.util.adapters.StringToObjectAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonMovesetBuilders : JsonDataRegistry<MovesetBuilder> {
override val gson: Gson = GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderAdapter)
.registerTypeAdapter(MoveSelector::class.java, StringToObjectAdapter(MoveSelector.selectors))
.create()
override val typeToken = TypeToken.get(MovesetBuilder::class.java)
override val resourcePath = "moveset_builders"
override val id: ResourceLocation = cobblemonResource("moveset_builders")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<CobblemonMovesetBuilders>()
val movesetBuilders = mutableMapOf<ResourceLocation, MovesetBuilder>()
fun getOrThrow(id: ResourceLocation): MovesetBuilder {
return movesetBuilders[id]
?: throw IllegalArgumentException("Unknown MovesetBuilder id: $id")
}
override fun sync(player: ServerPlayer) { /* These don't sync. But what if they did? Nah probably shouldn't sync. */ }
override fun reload(data: Map<ResourceLocation, MovesetBuilder>) {
movesetBuilders.clear()
data.forEach { (id, value) -> value.id = id }
movesetBuilders.putAll(data)
observable.emit(this)
}
}
@@ -21,15 +21,12 @@ import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallb
import com.cobblemon.mod.common.client.net.cooking.ToggleCookingPotLidHandler
import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.PokemonInteractionsSyncHandler
import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
import com.cobblemon.mod.common.client.net.effect.SaccharineLogBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormEntityParticleHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
import com.cobblemon.mod.common.client.net.effect.*
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
@@ -37,11 +34,7 @@ import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonUnpasturedHandler
import com.cobblemon.mod.common.client.net.pasture.UpdatePastureConflictFlagHandler
import com.cobblemon.mod.common.client.net.pasture.*
import com.cobblemon.mod.common.client.net.pokedex.ServerConfirmedRegisterHandler
import com.cobblemon.mod.common.client.net.pokemon.update.ClientboundUpdateRidingStateHandler
import com.cobblemon.mod.common.client.net.pokemon.update.PokemonUpdatePacketHandler
@@ -59,14 +52,7 @@ import com.cobblemon.mod.common.client.net.storage.party.SetPartyPokemonHandler
import com.cobblemon.mod.common.client.net.storage.party.SetPartyReferenceHandler
import com.cobblemon.mod.common.client.net.storage.pc.*
import com.cobblemon.mod.common.client.net.toast.ToastPacketHandler
import com.cobblemon.mod.common.client.net.trade.TradeAcceptanceChangedHandler
import com.cobblemon.mod.common.client.net.trade.TradeCancelledHandler
import com.cobblemon.mod.common.client.net.trade.TradeCompletedHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferExpiredHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeProcessStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.client.net.trade.*
import com.cobblemon.mod.common.net.PacketRegisterInfo
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.net.messages.client.OpenBehaviourEditorPacket
@@ -83,20 +69,12 @@ import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
import com.cobblemon.mod.common.net.messages.client.effect.SaccharineLogBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormParticlePacket
import com.cobblemon.mod.common.net.messages.client.effect.*
import com.cobblemon.mod.common.net.messages.client.npc.CloseNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.npc.OpenNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateDriverInputPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateOrientationPacket
import com.cobblemon.mod.common.net.messages.client.pasture.ClosePasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.OpenPasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonPasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonUnpasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureConflictFlagPacket
import com.cobblemon.mod.common.net.messages.client.pasture.*
import com.cobblemon.mod.common.net.messages.client.pokedex.ServerConfirmedRegisterPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.*
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.AddEvolutionPacket
@@ -116,37 +94,19 @@ import com.cobblemon.mod.common.net.messages.client.storage.party.InitializePart
import com.cobblemon.mod.common.net.messages.client.storage.party.MoveClientPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyReferencePacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.ClosePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.OpenPCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.RenamePCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.*
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.ChangePCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.SetPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeAcceptanceChangedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCancelledPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCompletedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferExpiredPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferNotificationPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
import com.cobblemon.mod.common.net.messages.client.trade.*
import com.cobblemon.mod.common.net.messages.client.ui.ExpGainedDataPacket
import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
import com.cobblemon.mod.common.net.messages.server.*
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamResponsePacket
import com.cobblemon.mod.common.net.messages.server.battle.RemoveSpectatorPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.battle.*
import com.cobblemon.mod.common.net.messages.server.behaviour.DamageOnCollisionPacket
import com.cobblemon.mod.common.net.messages.server.behaviour.SetEntityBehaviourPacket
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
@@ -170,13 +130,8 @@ import com.cobblemon.mod.common.net.messages.server.pasture.UnpastureAllPokemonP
import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.StartScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetActiveMarkPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetItemHiddenPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetMarkingsPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetNicknamePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.*
import com.cobblemon.mod.common.net.messages.server.pokemon.update.evolution.AcceptEvolutionPacket
import com.cobblemon.mod.common.net.messages.server.riding.DismountPokemonPacket
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateDriverInputPacket
@@ -184,25 +139,14 @@ import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateRide
import com.cobblemon.mod.common.net.messages.server.starter.RequestStarterScreenPacket
import com.cobblemon.mod.common.net.messages.server.storage.SwapPCPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.PartyTutorialPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.ReleasePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.SwapPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.*
import com.cobblemon.mod.common.net.messages.server.trade.AcceptTradeRequestPacket
import com.cobblemon.mod.common.net.messages.server.trade.CancelTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.ChangeTradeAcceptancePacket
import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.PerformTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.messages.server.trade.*
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.ChallengeResponseHandler
import com.cobblemon.mod.common.net.serverhandling.RequestInteractionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.RemoveSpectatorHandler
import com.cobblemon.mod.common.net.serverhandling.battle.SpectateBattleHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamLeaveHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHandler
import com.cobblemon.mod.common.net.serverhandling.battle.*
import com.cobblemon.mod.common.net.serverhandling.behaviour.DamageOnCollisionPacketHandler
import com.cobblemon.mod.common.net.serverhandling.behaviour.SetEntityBehaviourHandler
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
@@ -221,20 +165,14 @@ import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandl
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.PartyTutorialPacketHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokeedit.PokeEditActionHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetItemHiddenHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetMarkingsHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetNicknameHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.*
import com.cobblemon.mod.common.net.serverhandling.riding.DismountPokemonPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DriverInputPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.ServerboundUpdateRiderRotationHandler
@@ -248,12 +186,7 @@ import com.cobblemon.mod.common.net.serverhandling.storage.party.MovePartyPokemo
import com.cobblemon.mod.common.net.serverhandling.storage.party.ReleasePCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.SwapPartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.*
import com.cobblemon.mod.common.net.serverhandling.trade.AcceptTradeRequestHandler
import com.cobblemon.mod.common.net.serverhandling.trade.CancelTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.ChangeTradeAcceptanceHandler
import com.cobblemon.mod.common.net.serverhandling.trade.OfferTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.PerformTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.UpdateTradeOfferHandler
import com.cobblemon.mod.common.net.serverhandling.trade.*
import com.cobblemon.mod.common.util.server
import net.minecraft.server.level.ServerPlayer
@@ -399,7 +332,7 @@ object CobblemonNetwork {
// Data registries
list.add(PacketRegisterInfo(AbilityRegistrySyncPacket.ID, AbilityRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(CobblemonMechanicsSyncPacket.ID, CobblemonMechanicsSyncPacket::decode, CobblemonMechanicsSyncHandler))
list.add(PacketRegisterInfo(PokemonInteractionsSyncPacket.ID, PokemonInteractionsSyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(PokemonInteractionsSyncPacket.ID, PokemonInteractionsSyncPacket::decode, PokemonInteractionsSyncHandler))
list.add(PacketRegisterInfo(MovesRegistrySyncPacket.ID, MovesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(BerryRegistrySyncPacket.ID, BerryRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(SpeciesRegistrySyncPacket.ID, SpeciesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
@@ -527,7 +460,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(ReleasePCPokemonPacket.ID, ReleasePCPokemonPacket::decode, ReleasePCPokemonHandler))
list.add(PacketRegisterInfo(UnlinkPlayerFromPCPacket.ID, UnlinkPlayerFromPCPacket::decode, UnlinkPlayerFromPCHandler))
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.ID, RequestRenamePCBoxPacket::decode, RequestRenamePCBoxHandler))
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.Companion.ID, RequestRenamePCBoxPacket.Companion::decode, RequestRenamePCBoxHandler))
list.add(PacketRegisterInfo(PCBoxWallpapersPacket.ID, PCBoxWallpapersPacket::decode, PCBoxWallpapersHandler))
list.add(PacketRegisterInfo(RequestChangePCBoxWallpaperPacket.ID, RequestChangePCBoxWallpaperPacket::decode, RequestChangePCBoxWallpaperHandler))
list.add(PacketRegisterInfo(MarkPCBoxWallpapersSeenPacket.ID, MarkPCBoxWallpapersSeenPacket::decode, MarkPCBoxWallpapersSeenHandler))
@@ -563,9 +496,6 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(StartScanningPacket.ID, StartScanningPacket::decode, StartScanningHandler))
list.add(PacketRegisterInfo(FinishScanningPacket.ID, FinishScanningPacket::decode, FinishScanningHandler))
// PokeEdit actions
list.add(PacketRegisterInfo(PokeEditActionPacket.ID, PokeEditActionPacket::decode, PokeEditActionHandler))
// Pasture
list.add(PacketRegisterInfo(PasturePokemonPacket.ID, PasturePokemonPacket::decode, PasturePokemonHandler))
list.add(PacketRegisterInfo(UnpasturePokemonPacket.ID, UnpasturePokemonPacket::decode, UnpasturePokemonHandler))
@@ -588,9 +518,6 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(PartyPokemonMoveSelectedPacket.ID, PartyPokemonMoveSelectedPacket::decode, PartyPokemonMoveSelectedHandler))
list.add(PacketRegisterInfo(PartyMoveSelectCancelledPacket.ID, PartyMoveSelectCancelledPacket::decode, PartyMoveSelectCancelledHandler))
//Party move tutorial toasts packets
list.add(PacketRegisterInfo(PartyTutorialPacket.ID, PartyTutorialPacket::decode, PartyTutorialPacketHandler))
// Dialogue packets
list.add(PacketRegisterInfo(EscapeDialoguePacket.ID, EscapeDialoguePacket::decode, EscapeDialogueHandler))
list.add(PacketRegisterInfo(InputToDialoguePacket.ID, InputToDialoguePacket::decode, InputToDialogueHandler))
@@ -624,4 +551,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -1,47 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.npc.PartyComposition
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonPartyCompositions : JsonDataRegistry<PartyComposition> {
override val id: ResourceLocation = cobblemonResource("party_compositions")
override val type = PackType.SERVER_DATA
override val typeToken: TypeToken<PartyComposition> = TypeToken.get(PartyComposition::class.java)
override val resourcePath: String = "party_compositions"
override val observable = SimpleObservable<CobblemonPartyCompositions>()
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.setPrettyPrinting()
.create()
val partyCompositions = mutableMapOf<ResourceLocation, PartyComposition>()
override fun sync(player: ServerPlayer) {
// probably worth syncing which exist, yeah. the deeper details probably not. I'll get to this eventually.
}
override fun reload(data: Map<ResourceLocation, PartyComposition>) {
data.entries.forEach { it.value.id = it.key }
partyCompositions.clear()
partyCompositions.putAll(data)
observable.emit(this)
Cobblemon.LOGGER.info("Loaded ${data.size} party compositions.")
}
}
@@ -1,60 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderReferenceAdapter
import com.cobblemon.mod.common.util.adapters.PokemonPropertiesAdapter
import com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonPartyPools : JsonDataRegistry<PartyPool> {
override val id: ResourceLocation = cobblemonResource("party_pools")
override val type = PackType.SERVER_DATA
override val typeToken: TypeToken<PartyPool> = TypeToken.get(PartyPool::class.java)
override val resourcePath: String = "party_pools"
override val observable = SimpleObservable<CobblemonPartyPools>()
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, PokemonPropertiesAdapter(saveLong = false))
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(ScriptableIntRange::class.java, ScriptableIntRangeAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.setPrettyPrinting()
.create()
val partyPools = mutableMapOf<ResourceLocation, PartyPool>()
override fun sync(player: ServerPlayer) {
// probably worth syncing. Knowing the details might help exploit the server? lot of effort for little though.
}
override fun reload(data: Map<ResourceLocation, PartyPool>) {
data.entries.forEach { it.value.id = it.key }
partyPools.clear()
partyPools.putAll(data)
observable.emit(this)
}
}
@@ -17,7 +17,6 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangV
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.ai.CobblemonRandomSurfacePos
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
@@ -185,12 +184,6 @@ class WanderTaskConfig : SingleTaskConfig {
if (targetVec == null) continue
// Ignore pathing targets outside of the pasture zone if the pokemon is pastured.
val tethering = (entity as? PokemonEntity)?.tethering
if (tethering?.canRoamTo(BlockPos.containing(targetVec)) == false) {
continue
}
pos = applyHeightConstraints(
pos = BlockPos.containing(targetVec),
minimumHeight = minimumHeight,
@@ -304,8 +304,7 @@ open class PokemonBattle(
.forEach{it.pokemon.heal()}
actors.forEach { actor ->
actor.pokemonList.forEach { battlePokemon ->
battlePokemon.postBattlePokemonOperations.forEach { it(battlePokemon) }
battlePokemon.entity?.let { entity -> battlePokemon.postBattleEntityOperations.forEach { it(entity) } }
battlePokemon.entity?.let { entity -> battlePokemon.postBattleEntityOperation(entity) }
if (battlePokemon.effectedPokemon.entity != null
&& battlePokemon.effectedPokemon.entity?.beamMode == 0
&& battlePokemon.effectedPokemon.entity?.platform != PlatformType.NONE) {
@@ -499,8 +498,8 @@ open class PokemonBattle(
}
fun stop() {
writeShowdownAction(">forcetie") // This will terminate the Showdown connection
end()
writeShowdownAction(">forcetie") // This will terminate the Showdown connection
}
fun checkForInputDispatch() {
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.api.events.battles
import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
@@ -26,8 +25,7 @@ import com.cobblemon.mod.common.util.asArrayValue
data class BattleFaintedEvent(
override val battle: PokemonBattle,
val killed: BattlePokemon,
val context: BattleContext,
val pnx: String
val context: BattleContext
) : BattleEvent {
val structContext = mutableMapOf<String, MoValue>(
"battle" to battle.struct,
@@ -35,7 +33,6 @@ data class BattleFaintedEvent(
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct },
"pokemon" to killed.struct,
"context" to context.getStruct(),
"pnx" to StringValue(pnx)
"context" to context.getStruct()
)
}
}
@@ -25,8 +25,7 @@ import com.cobblemon.mod.common.util.asArrayValue
data class FormeChangeEvent(
override val battle: PokemonBattle,
val pokemon: BattlePokemon,
val formeName: String,
val pnx: String
val formeName: String
) : BattleEvent {
val context = mutableMapOf(
"battle" to battle.struct,
@@ -34,7 +33,6 @@ data class FormeChangeEvent(
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct },
"pokemon" to pokemon.struct,
"forme" to StringValue(formeName),
"pnx" to StringValue(pnx)
"forme" to StringValue(formeName)
)
}
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.api.interaction
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
@@ -30,11 +29,6 @@ import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.util.LowerCaseEnumTypeAdapterFactory
import net.minecraft.world.item.Item
import net.minecraft.world.level.biome.Biome
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.levelgen.structure.Structure
import net.minecraft.world.level.material.Fluid
object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
override val id = cobblemonResource("pokemon_interactions")
@@ -49,42 +43,45 @@ object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
.registerTypeAdapter(Requirement::class.java, CobblemonRequirementAdapter)
.registerTypeAdapter(InteractionEffect::class.java, InteractionEffectAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Biome::class.java).type, BiomeLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Block::class.java).type, BlockLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Item::class.java).type, ItemLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Structure::class.java).type, StructureLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Fluid::class.java).type, FluidLikeConditionAdapter)
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory())
.setPrettyPrinting()
.create()
val interactions = mutableListOf<PokemonInteractionSet>()
val speciesInteractions = mutableListOf<PokemonInteractionSet>()
val generalInteractions = mutableListOf<PokemonInteractionSet>()
override fun sync(player: ServerPlayer) {
PokemonInteractionsSyncPacket(interactions).sendToPlayer(player)
PokemonInteractionsSyncPacket(
speciesInteractions,
generalInteractions,
).sendToPlayer(player)
}
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>) {
interactions.clear()
val split = data.entries
.partition {
it.value.requirements.any { requirement -> requirement is PokemonPropertiesRequirement && requirement.target.species != null }
}
// Put the species specific interactions first as they take priority
interactions.addAll(split.first.map {it.value})
interactions.addAll(split.second.map {it.value})
Cobblemon.LOGGER.info("Loaded {} Pokémon interaction sets", interactions.size)
speciesInteractions.clear()
generalInteractions.clear()
val split = data.entries.partition { it.value.requirements.any { requirement -> requirement is PokemonPropertiesRequirement && requirement.target.species != null } }
speciesInteractions.addAll(split.first.map {it.value})
generalInteractions.addAll(split.second.map {it.value})
Cobblemon.LOGGER.info("Loaded {} Pokémon interaction sets", data.size)
}
fun findInteraction(pokemon: PokemonEntity): PokemonInteraction? {
val setCheck: (PokemonInteractionSet) -> Boolean = { it.requirements.all { req -> req.check(pokemon.pokemon) }}
val interactionCheck: (PokemonInteraction) -> Boolean = { it.requirements.all { req -> req.check(pokemon.pokemon) } && !pokemon.pokemon.isOnInteractionCooldown(it.grouping) }
return interactions
// species-specific interactions take priority
val validInteractions = speciesInteractions
.filter(setCheck)
.flatMap { it.interactions }
.firstOrNull(interactionCheck)
.filter(interactionCheck)
.toMutableList()
if (validInteractions.isEmpty()) { // if all species-specific interactions are absent/on cooldown, fallback to more generic ones
validInteractions.addAll(generalInteractions.filter(setCheck).flatMap { it.interactions }
.filter(interactionCheck)
)
}
return validInteractions.randomOrNull()
}
}
@@ -17,7 +17,6 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.sound.UnvalidatedPlaySoundS2CPacket
import com.cobblemon.mod.common.pokemon.adapters.CobblemonRequirementAdapter
import com.cobblemon.mod.common.util.*
import com.google.gson.annotations.SerializedName
import net.minecraft.core.registries.Registries
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.resources.ResourceLocation
@@ -43,9 +42,17 @@ data class PokemonInteractionSet(
companion object {
fun decode(buffer: FriendlyByteBuf): PokemonInteractionSet {
val requirements = buffer.readList(CobblemonRequirementAdapter::decode)
val interactions = buffer.readList(PokemonInteraction::decode)
return PokemonInteractionSet(requirements, interactions)
val requirements = buffer.readList { it ->
CobblemonRequirementAdapter.decode(it)
}
val interactions = buffer.readList { it ->
PokemonInteraction.decode(it)
}
return PokemonInteractionSet(
requirements,
interactions,
)
}
}
}
@@ -136,12 +143,11 @@ class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?) : Intera
class PlaySoundEffect(
val sound: ResourceLocation,
val soundSource: SoundSource?,
@SerializedName("playAround") private val _playAround: Boolean?,
@SerializedName("distance") private val _distance: Double?,
@SerializedName("volume") private val _volume: Float?,
@SerializedName("pitch") private val _pitch: Float?,
val playAround: Boolean = true,
val distance: Double = 64.0,
val volume: Float = 1.0F,
val pitch: Float = 1.0F
) : InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
@@ -155,7 +161,6 @@ class PlaySoundEffect(
volume,
pitch
)
if (playAround) {
packet.sendToPlayersAround(pokemon.x, pokemon.y, pokemon.z, distance, pokemon.level().dimension())
} else {
@@ -163,18 +168,6 @@ class PlaySoundEffect(
}
}
val playAround: Boolean
get() = _playAround != false
val distance: Double
get() = _distance ?: 64.0
val volume: Float
get() = _volume ?: 1.0F
val pitch: Float
get() = _pitch ?: 1.0F
companion object {
val ID = "play_sound"
}
@@ -145,7 +145,6 @@ import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.monster.Monster
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.enchantment.EnchantmentHelper
import net.minecraft.world.level.ClipContext
import net.minecraft.world.level.GameType
import net.minecraft.world.level.Level
@@ -325,24 +324,13 @@ object MoLangFunctions {
runtime.environment.context = params.environment.context
val expression = params.getString(0).asExpressionLike()
val delayInSeconds = params.getDoubleOrNull(1)?.toFloat() ?: 0.0f
fun evaluate(): MoValue {
return try {
runtime.resolve(expression)
} catch (ex: Exception) {
Cobblemon.LOGGER.warn("Failed to evaluate MoLang expression${if (delayInSeconds > 0.0f) " (delayed)" else ""}: $expression", ex)
DoubleValue.ZERO
}
}
if (delayInSeconds > 0.0f) {
val tracker = if (Cobblemon.implementation.environment() == Environment.SERVER) ServerTaskTracker else ClientTaskTracker
tracker.after(delayInSeconds) {
evaluate()
runtime.resolve(expression)
}
DoubleValue.ONE
} else {
evaluate()
runtime.resolve(expression)
}
},
"system_time_millis" to java.util.function.Function { _ ->
@@ -352,28 +340,23 @@ object MoLangFunctions {
"date_local_time" to java.util.function.Function { _ ->
val time = System.currentTimeMillis()
val date = java.util.Date(time)
val formatted = java.text.SimpleDateFormat("dd/MM/yyyy").format(date)
val formatted = java.text.SimpleDateFormat("DD/MM/YYYY").format(date)
StringValue(formatted)
},
"date_of" to java.util.function.Function { params ->
val time = params.getDouble(0).toLong()
val date = java.util.Date(time)
val formatted = java.text.SimpleDateFormat("dd/MM/yyyy").format(date)
val formatted = java.text.SimpleDateFormat("DD/MM/YYYY").format(date)
StringValue(formatted)
},
"date_is_after" to java.util.function.Function { params ->
val dateA = params.getString(0)
val dateB = params.getString(1)
val format = java.text.SimpleDateFormat("dd/MM/yyyy")
val format = java.text.SimpleDateFormat("DD/MM/YYYY")
val a = format.parse(dateA)
val b = format.parse(dateB)
DoubleValue(a.after(b))
},
"get_move_from_id" to java.util.function.Function { params ->
val moveId = params.getString(0)
val moveTemplate = Moves.getByName(moveId)
return@Function moveTemplate?.struct ?: DoubleValue.ZERO
},
"create_simple_party_provider" to java.util.function.Function { params ->
val partyProvider = SimplePartyProvider()
return@Function partyProvider.struct
@@ -384,13 +367,6 @@ object MoLangFunctions {
val pickupItem = ObtainableItem(item = item, pickupPriority = pickupPriority)
return@Function pickupItem.struct
},
"create_itemstack" to java.util.function.Function { params ->
val itemId = params.getString(0).asIdentifierDefaultingNamespace()
val item = BuiltInRegistries.ITEM.get(itemId) ?: return@Function DoubleValue.ZERO
val count = params.getIntOrNull(1) ?: 1
val itemStack = ItemStack(item, count)
return@Function ObjectValue(itemStack)
},
"file" to java.util.function.Function { MoLangLoadedFilesCache.struct }
)
val biomeFunctions = mutableListOf<(Holder<Biome>) -> HashMap<String, java.util.function.Function<MoParams, Any>>>()
@@ -596,62 +572,6 @@ object MoLangFunctions {
}
return@put items
}
map.put("has_inventory_space") { _ ->
val inventory = player.inventory
return@put if (inventory.getFreeSlot() != -1) DoubleValue.ONE else DoubleValue.ZERO
}
map.put("set_inventory_slot") { params ->
val slot = params.getInt(0)
val inventory = player.inventory
if (slot !in 0 until inventory.containerSize) return@put DoubleValue.ZERO
val value = params.getOrNull<MoValue>(1) ?: return@put DoubleValue.ZERO
val stack: ItemStack? = when (value) {
is ObjectValue<*> -> value.obj as? ItemStack
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(id) ?: return@put DoubleValue.ZERO
ItemStack(item)
}
else -> null
}
if (stack == null) return@put DoubleValue.ZERO
inventory.setItem(slot, stack)
return@put DoubleValue.ONE
}
map.put("give_item") { params ->
val inventory = player.inventory
val freeSlot = inventory.getFreeSlot()
val value = params.getOrNull<MoValue>(0) ?: return@put DoubleValue.ZERO
val dropIfFull = params.getBooleanOrNull(1) ?: false
val stack: ItemStack? = when (value) {
is ObjectValue<*> -> value.obj as? ItemStack
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(id) ?: return@put DoubleValue.ZERO
ItemStack(item)
}
else -> null
}
if (stack == null) return@put DoubleValue.ZERO
if (freeSlot == -1) {
if (dropIfFull) {
player.drop(stack, false)
return@put DoubleValue.ONE
} else {
return@put DoubleValue.ZERO
}
}
inventory.setItem(freeSlot, stack)
return@put DoubleValue.ONE
}
map.put("face") { params -> ObjectValue(PlayerDialogueFaceProvider(player.uuid, params.getBooleanOrNull(0) != false)) }
map.put("swing_hand") { _ -> player.swing(player.usedItemHand) }
map.put("food_level") { _ -> DoubleValue(player.foodData.foodLevel) }
@@ -695,7 +615,8 @@ object MoLangFunctions {
}
map.put("active_dialogue") { _ ->
if (player.isInDialogue) {
return@put player.activeDialogue?.toMoLangStruct() ?: DoubleValue.ZERO
player.activeDialogue?.dialogueId.toString()
return@put DoubleValue.ONE
} else {
DoubleValue.ZERO
}
@@ -872,8 +793,6 @@ object MoLangFunctions {
DoubleValue(value)
}
map.put("get_starter_uuid") { _ -> Cobblemon.playerDataManager.getGenericData(player).starterUUID?.let{ StringValue(it.toString()) } ?: DoubleValue.ZERO }
map.put("has_chosen_starter") { _ -> DoubleValue(Cobblemon.playerDataManager.getGenericData(player).starterSelected) }
}
map
}
@@ -894,15 +813,6 @@ object MoLangFunctions {
map.put("is_of") { params -> DoubleValue(holder.`is`(params.getString(0).asIdentifierDefaultingNamespace())) }
map.put("is_in") { params -> DoubleValue(holder.`is`(TagKey.create(Registries.ITEM, params.getString(0).replace("#", "").asIdentifierDefaultingNamespace()))) }
map.put("is_food") { params -> DoubleValue(stack.has(DataComponents.FOOD)) }
map.put("is_enchanted") { params -> DoubleValue(stack.hasFoil()) }
map.put("has_enchantment") { params ->
val enchantmentId = params.getString(0).asIdentifierDefaultingNamespace()
val minLevel = params.getIntOrNull(1) ?: 1
val enchantmentRegistry = registryAccess.registryOrThrow(Registries.ENCHANTMENT)
val holder = enchantmentRegistry.getHolder(enchantmentId).orElse(null) ?: return@put DoubleValue.ZERO
val level = EnchantmentHelper.getItemEnchantmentLevel(holder, stack)
return@put DoubleValue(level >= minLevel)
}
return@mutableListOf map
}
)
@@ -1941,7 +1851,8 @@ object MoLangFunctions {
return@put DoubleValue(if (pokemon.addRideBoost(stat, value)) 1.0 else 0.0)
}
map.put("initialize_moveset") { params ->
pokemon.initializeMovesetFromDefault()
val preferLatest = params.getBooleanOrNull(0) ?: true
pokemon.initializeMoveset(preferLatest)
return@put DoubleValue.ONE
}
map.put("validate_moveset") { params ->
@@ -2826,8 +2737,8 @@ object MoLangFunctions {
}
variable
}
else -> VariableStruct()
}
else -> null
} ?: throw IllegalArgumentException("Invalid NBT element type: ${nbt.type}")
}
}
@@ -1,57 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
/**
* This is all you need tbh. A moveset builder that picks moves from up to 4 slots of move selectors.
*
* @author Hiroku
* @since December 5th, 2025
*/
class DefaultMovesetBuilder : MovesetBuilder {
companion object {
val TYPE = cobblemonResource("default")
}
override val type: ResourceLocation = TYPE
override var id: ResourceLocation = cobblemonResource("temp") // replaced on load.
val slot1: List<MoveSelector> = listOf()
val slot2: List<MoveSelector> = listOf()
val slot3: List<MoveSelector> = listOf()
val slot4: List<MoveSelector> = listOf()
override fun build(form: FormData, level: Int): MoveSet {
val chosenMoves = mutableSetOf<MoveTemplate>()
tryAddMoveFromSlot(form, level, slot1, chosenMoves)
tryAddMoveFromSlot(form, level, slot2, chosenMoves)
tryAddMoveFromSlot(form, level, slot3, chosenMoves)
tryAddMoveFromSlot(form, level, slot4, chosenMoves)
if (chosenMoves.isEmpty()) {
chosenMoves.add(Moves.getExceptional())
}
val moveset = MoveSet()
chosenMoves.map(MoveTemplate::create).forEach(moveset::add)
return moveset
}
fun tryAddMoveFromSlot(form: FormData, level: Int, selectors: List<MoveSelector>, chosenMoves: MutableSet<MoveTemplate>) {
for (selector in selectors) {
val move = selector(form, form.moves, level, chosenMoves)
if (move != null) {
chosenMoves.add(move)
return
}
}
}
}
@@ -1,148 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.pokemon.moves.Learnset
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.weightedSelection
/**
* A function that selects a move from a given [Learnset] for a particular [FormData] at a certain level,
* taking into account any moves that have already been chosen for the moveset.
*
* @author Hiroku
* @since December 5th, 2025
*/
fun interface MoveSelector {
operator fun invoke(form: FormData, learnset: Learnset, level: Int, chosenMoves: Set<MoveTemplate>): MoveTemplate?
companion object {
val NONE = MoveSelector { _, _, _, _ -> null }
val LAST_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it] ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory != DamageCategories.STATUS } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_SUITABLE_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
val suitableCategories = if (form.baseStats[Stats.ATTACK]!! > form.baseStats[Stats.SPECIAL_ATTACK]!!) {
setOf(DamageCategories.PHYSICAL)
} else if (form.baseStats[Stats.SPECIAL_ATTACK]!! > form.baseStats[Stats.ATTACK]!!) {
setOf(DamageCategories.SPECIAL)
} else {
setOf(DamageCategories.PHYSICAL, DamageCategories.SPECIAL)
}
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory in suitableCategories } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_STATUS = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory == DamageCategories.STATUS } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val STAB = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory != DamageCategories.STATUS }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.PHYSICAL }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.SPECIAL }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val physicalMoves = moves.filter { it.damageCategory == DamageCategories.PHYSICAL }
return@MoveSelector physicalMoves.weightedSelection { it.getSelectionWeight(form) }
}
val SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val specialMoves = moves.filter { it.damageCategory == DamageCategories.SPECIAL }
return@MoveSelector specialMoves.weightedSelection { it.getSelectionWeight(form) }
}
val OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val offensiveMoves = moves.filter { it.damageCategory != DamageCategories.STATUS }
return@MoveSelector offensiveMoves.weightedSelection { it.getSelectionWeight(form) }
}
val RANDOM_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val STATUS = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val statusMoves = moves.filter { it.damageCategory == DamageCategories.STATUS }
return@MoveSelector statusMoves.weightedSelection { it.getSelectionWeight(form) }
}
val TM = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_TM = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
val stabMoves = moves.filter { it.elementalType in form.types }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val EGG = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.eggMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val selectors = mutableMapOf(
"none" to NONE,
"last_levelup" to LAST_LEVELUP,
"last_offensive" to LAST_OFFENSIVE,
"last_suitable_offensive" to LAST_SUITABLE_OFFENSIVE,
"last_status" to LAST_STATUS,
"levelup" to RANDOM_LEVELUP,
"stab" to STAB,
"stab_physical" to STAB_PHYSICAL,
"stab_special" to STAB_SPECIAL,
"physical" to PHYSICAL,
"special" to SPECIAL,
"offensive" to OFFENSIVE,
"status" to STATUS,
"tm" to TM,
"stab_tm" to STAB_TM,
"egg" to EGG
)
}
}
@@ -12,12 +12,12 @@ import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.moves.categories.DamageCategory
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.codec.CodecUtils
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.lang
@@ -53,26 +53,22 @@ open class MoveTemplate(
val pp: Int,
val priority: Int,
val critRatio: Double,
val effectChances: Array<Double>,
val weight: Float
val effectChances: Array<Double>
) {
val struct: MoStruct by lazy {
QueryStruct(hashMapOf())
.addFunction("name") { StringValue(name) }
.addFunction("display_name") { StringValue(displayName.string) }
.addFunction("description") { StringValue(description.string) }
.addFunction("type") { StringValue(elementalType.showdownId) }
.addFunction("damage_category") { StringValue(damageCategory.name) }
.addFunction("power") { DoubleValue(power) }
.addFunction("target") { StringValue(target.name) }
.addFunction("accuracy") { DoubleValue(accuracy) }
.addFunction("pp") { DoubleValue(pp) }
.addFunction("max_pp") { DoubleValue(maxPp) }
.addFunction("priority") { DoubleValue(priority) }
.addFunction("crit_ratio") { DoubleValue(critRatio) }
.addFunction("weight") { DoubleValue(weight.toDouble()) }
}
val displayName: MutableComponent
get() = lang("move.$name")
val description: MutableComponent
@@ -90,8 +86,7 @@ open class MoveTemplate(
pp = 5,
priority = 0,
critRatio = 0.0,
effectChances = emptyArray(),
weight = 0F,
effectChances = emptyArray()
)
companion object {
@@ -162,12 +157,4 @@ open class MoveTemplate(
}
return this.elementalType
}
fun getSelectionWeight(form: FormData): Float {
return if (this in form.signatureMoves) {
weight * MovesetBuilder.signatureMoveWeightMultiplier
} else {
weight
}
}
}
@@ -10,15 +10,15 @@ package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.data.MovesRegistrySyncPacket
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.fromJson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import net.minecraft.resources.ResourceLocation
@@ -46,14 +46,6 @@ object Moves : DataRegistry {
this.idMapping.clear()
this.moveScripts.clear()
val weights = mutableMapOf<String, Number>()
manager.getResourceStack(cobblemonResource("move_weights.json")).forEach {
it.open().use {
val map = GsonBuilder().create().fromJson<Map<String, Number>>(it.reader())
weights.putAll(map)
}
}
ShowdownService.service.resetRegistryData("move")
manager.listResources("moves") { it.path.endsWith(".js") }.forEach { (identifier, resource) ->
resource.open().use { stream ->
@@ -83,7 +75,6 @@ object Moves : DataRegistry {
val priority = jsMove.get("priority").asInt
val critRatio = jsMove.get("critRatio")?.asDouble ?: 1.0
val effectChances = arrayListOf<Double>()
val weight = weights[id]?.toFloat() ?: 50F
val secondariesMember = jsMove.get("secondaries")
val secondaryMember = jsMove.get("secondary")
if (secondariesMember != null && secondariesMember is JsonArray) {
@@ -101,20 +92,7 @@ object Moves : DataRegistry {
effectChances += secondaryMember.get("chance").asDouble
}
}
val move = MoveTemplate(
name = id,
num = num,
elementalType = elementalType,
damageCategory = damageCategory,
power = power,
target = target,
accuracy = accuracy,
pp = pp,
priority = priority,
critRatio = critRatio,
effectChances = effectChances.toTypedArray(),
weight = weight
)
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
this.register(move)
} catch (e: Exception) {
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
@@ -135,7 +113,7 @@ object Moves : DataRegistry {
@JvmStatic
fun getByNameOrDummy(name: String) = allMoves[name.lowercase()] ?: MoveTemplate.dummy(name.lowercase())
@JvmStatic
fun getExceptional() = getByName("tackle") ?: allMoves.values.first()
fun getExceptional() = getByName("tackle") ?: allMoves.values.random()
@JvmStatic
fun count() = allMoves.size
@JvmStatic
@@ -151,4 +129,5 @@ object Moves : DataRegistry {
this.allMoves[move.name] = move
this.idMapping[move.num] = move
}
}
@@ -1,38 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
/**
* Builders a moveset for a particular [FormData] and level. Probably only really need [DefaultMovesetBuilder] though.
*
* @author Hiroku
* @since December 5th, 2025
*/
interface MovesetBuilder {
/** The ID of this builder. It's autofilled from the registry loader. */
var id: ResourceLocation
/** The subtype designation for this moveset builder. Generally unused once read from JSON. */
val type: ResourceLocation
/** Builds a brand new moveset for the [form] and [level]. */
fun build(form: FormData, level: Int): MoveSet
companion object {
/** Weight multiplier for moves that are in the form's signatureMoves list. Makes them way more likely to be chosen. */
var signatureMoveWeightMultiplier = 5F
/** Subtypes of MovesetBuilder. I personally doubt you'll need any other subtypes, but hey. */
val movesetBuilderTypes = mutableMapOf<ResourceLocation, Class<out MovesetBuilder>>(
cobblemonResource("default") to DefaultMovesetBuilder::class.java
)
}
}
@@ -8,14 +8,12 @@
package com.cobblemon.mod.common.api.npc
import com.cobblemon.mod.common.api.npc.partyproviders.ComposedPoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.PoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.ScriptPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.SimplePartyProvider
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.google.gson.JsonElement
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
/**
@@ -27,15 +25,14 @@ import net.minecraft.server.level.ServerPlayer
*/
interface NPCPartyProvider {
companion object {
val types = mutableMapOf(
SimplePartyProvider.TYPE to ::SimplePartyProvider,
PoolPartyProvider.TYPE to ::PoolPartyProvider,
ScriptPartyProvider.TYPE to ::ScriptPartyProvider,
ComposedPoolPartyProvider.TYPE to ::ComposedPoolPartyProvider
val types = mutableMapOf<String, (String) -> NPCPartyProvider>(
SimplePartyProvider.TYPE to { SimplePartyProvider() },
PoolPartyProvider.TYPE to { PoolPartyProvider() },
ScriptPartyProvider.TYPE to { ScriptPartyProvider() }
)
}
val type: ResourceLocation
val type: String
val isStatic: Boolean
fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer> = emptyList()): NPCPartyStore
// Why did I opt for manual JSON loading??? I must have had a reason but I can't remember. Maybe for S2C? Use a codec doofus
@@ -1,68 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import kotlin.random.Random
/**
* A kind of definition for how to compose a party of Pokémon given a [PartyPool].
*
* @author Hiroku
* @since December 7th, 2025
*/
class PartyComposition {
var id = cobblemonResource("dummy_composition")
// val displayName = "cobblemon.party_composition.$id" // dunno if i will use this yet
val scrambleOrder: Boolean = false
val slot1 = listOf<String>()
val slot2 = listOf<String>()
val slot3 = listOf<String>()
val slot4 = listOf<String>()
val slot5 = listOf<String>()
val slot6 = listOf<String>()
fun compose(pool: PartyPool, level: Int, aspects: Set<String>, desiredPokemonCount: Int, runtime: MoLangRuntime, random: Random = Random.Default): List<Pokemon> {
val chosenEntries = mutableListOf<PartyPool.PoolEntry>()
val availableEntries = pool.entries.filter { level in it.npcLevels.resolve(runtime) && (it.npcAspects.isEmpty() || it.npcAspects.all(aspects::contains)) }
for (labelSet in listOf(slot1, slot2, slot3, slot4, slot5, slot6)) {
pool.tryChoosingEntry(labelSet, runtime, random, availableEntries, chosenEntries)
if (chosenEntries.size >= desiredPokemonCount) {
break
}
}
val finalEntries = if (scrambleOrder) {
chosenEntries.shuffled(random)
} else {
chosenEntries
}
val pokemon = finalEntries.map { entry ->
val levelVariation = entry.levelVariation.resolve(runtime)
val lvlVariation = if (levelVariation.first == levelVariation.last) {
levelVariation.first
} else {
levelVariation.random(random)
}
val finalLevel = level + lvlVariation
val properties = entry.pokemon.copy()
properties.level = properties.level ?: finalLevel
val pokemon = properties.create()
entry.movesetBuilders.randomOrNull()?.let(pokemon::initializeMovesetFrom)
pokemon
}
return pokemon
}
}
@@ -1,77 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.toProperties
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
/**
* A pool of Pokémon to choose from, typically for generating a party for an NPC. The entries
* of the pool work on a weight and label system which is used as part of a [PartyComposition].
*
* @author Hiroku
* @since December 7th, 2025
*/
class PartyPool {
var id = cobblemonResource("dummy")
val displayName = "cobblemon.party_pool.$id"
val entries = mutableListOf<PoolEntry>()
class PoolEntry {
val pokemon: PokemonProperties = "pikachu".toProperties()
val labels: List<String> = emptyList()
val npcAspects: List<String> = emptyList()
val required: List<String> = emptyList()
val excluded: List<String> = emptyList()
val maxSelectableTimes: ExpressionLike = "6".asExpressionLike()
val levelVariation: ScriptableIntRange = ScriptableIntRange(0, 0)
val npcLevels: ScriptableIntRange = ScriptableIntRange(1, 100)
val movesetBuilders: List<MovesetBuilder> = emptyList()
val weight: ExpressionLike = "50".asExpressionLike()
}
/**
* Goes through each of the labels in order, trying to find a valid choice. Once it finds one, it skips the later
* labels and adds the value to [chosenEntries] and returns. If none of the labels could find a valid entry, nothing is added.
*/
fun tryChoosingEntry(
labels: List<String>,
runtime: MoLangRuntime,
random: Random,
availableEntries: List<PoolEntry>,
chosenEntries: MutableList<PoolEntry>,
) {
for (label in labels) {
val entries = availableEntries
.filter { label in it.labels || label.lowercase() == "any" }
.filter { entry ->
// Check required labels
entry.required.all { it in labels } &&
// Check excluded labels
entry.excluded.none { it in labels } &&
// Check max selectable times
chosenEntries.count { it == entry } < entry.maxSelectableTimes.resolveFloat(runtime)
}
if (entries.isNotEmpty()) {
val selectedEntry = entries.weightedSelection(random) { it.weight.resolveFloat(runtime) } ?: return
chosenEntries.add(selectedEntry)
return
}
}
}
}
@@ -1,94 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc.partyproviders
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.CobblemonPartyCompositions
import com.cobblemon.mod.common.CobblemonPartyPools
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.api.npc.PartyComposition
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import kotlin.random.Random
import kotlin.ranges.random
import net.minecraft.server.level.ServerPlayer
/**
* An [NPCPartyProvider] which uses a [PartyComposition] and [PartyPool] to generate a party.
*
* There are controls for minimum and maximum Pokémon count, as well as whether to use a fixed random seed for the NPC.
*
* @author Hiroku
* @since December 8th, 2025
*/
class ComposedPoolPartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("composed_pool")
}
override val type = TYPE
override var isStatic: Boolean = true
var useFixedRandom: Boolean = false
var minPokemon: Expression = "1".asExpression()
var maxPokemon: Expression = "6".asExpression()
lateinit var pool: PartyPool
lateinit var composition: PartyComposition
override fun loadFromJSON(json: JsonElement) {
json as JsonObject
isStatic = json.getAsJsonPrimitive("isStatic")?.asBoolean ?: true
useFixedRandom = json.getAsJsonPrimitive("useFixedRandom")?.asBoolean ?: false
pool = CobblemonPartyPools.partyPools[json.get("pool").asString.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown PartyPool id: ${json.get("pool").asString}")
composition = CobblemonPartyCompositions.partyCompositions[json.get("composition").asString.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown PartyComposition id: ${json.get("composition").asString}")
minPokemon = json.getAsJsonPrimitive("minPokemon").asString?.asExpression() ?: "1".asExpression()
maxPokemon = json.getAsJsonPrimitive("maxPokemon").asString?.asExpression() ?: "6".asExpression()
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
val runtime = MoLangRuntime().setup().withQueryValue("npc", npc.struct)
val random = if (useFixedRandom) Random(npc.uuid.hashCode()) else Random.Default
runtime.withQueryValue("level", DoubleValue(level))
runtime.withQueryValue("players", players.asArrayValue { it.asMoLangValue() })
if (players.size == 1) {
// This is for the convenience, most cases will be pvn one player
runtime.withQueryValue("player", players.first().asMoLangValue())
}
val minPokemon = runtime.resolveInt(this.minPokemon)
val maxPokemon = runtime.resolveInt(this.maxPokemon)
val desiredPokemonCount = (minPokemon..maxPokemon).random(random)
val pokemonList = composition.compose(
pool = pool,
level = level,
aspects = npc.aspects,
desiredPokemonCount = desiredPokemonCount,
runtime = runtime,
random = random
)
val partyStore = NPCPartyStore(npc)
pokemonList.forEach(partyStore::add)
return partyStore
}
}
@@ -48,7 +48,7 @@ import kotlin.random.Random
*/
class PoolPartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("pool")
const val TYPE = "pool"
}
@Transient
@@ -150,10 +150,10 @@ class PoolPartyProvider : NPCPartyProvider {
val instance = selected.pokemon.copy().also { it.level = it.level ?: dictatedLevel ?: randomLevel }.create()
party.add(instance)
}
if (party.none()) {
LOGGER.error("$npc had no Pokémon produced by their party provider")
LOGGER.error("${npc} has no Pokemon on Party")
}
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
@@ -28,7 +28,7 @@ import net.minecraft.server.level.ServerPlayer
class ScriptPartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("script")
const val TYPE = "script"
}
override val type = TYPE
@@ -16,7 +16,6 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getBooleanOrNull
import com.cobblemon.mod.common.util.toProperties
import com.google.gson.JsonElement
@@ -30,7 +29,7 @@ import net.minecraft.server.level.ServerPlayer
*/
class SimplePartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("simple")
const val TYPE = "simple"
}
@Transient
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.pokemon
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.abilities.Ability
import com.cobblemon.mod.common.api.events.CobblemonEvents
@@ -149,13 +148,6 @@ open class PokemonProperties {
props.originalTrainerType = OriginalTrainerType.entries.toList().parsePropertyOfCollection(keyPairs, listOf("originaltrainertype", "ottype"), labelsOptional = true) { it.name.lowercase() }
props.originalTrainer = parsePlayerProperty(keyPairs, listOf("originaltrainer", "ot"))
props.moves = parseString(keyPairs, listOf("moves"))?.split(",")
props.movesetBuilders = parseString(keyPairs, listOf("moveset_builders", "movesetbuilders", "moveset_builder", "movesetbuilder"))?.split(",")?.mapNotNull {
try {
it.asIdentifierDefaultingNamespace()
} catch (_: ResourceLocationException) {
null
}
}
props.heldItem = parseString(keyPairs, listOf("helditem", "held_item"))
props.minPerfectIVs = parseIntProperty(keyPairs, listOf("min_perfect_ivs"))?.coerceIn(0, Stats.PERMANENT.size)
props.scaleModifier = parseFloatProperty(keyPairs, listOf("scale_modifier"))
@@ -364,7 +356,6 @@ open class PokemonProperties {
var originalTrainerType: OriginalTrainerType? = null
var originalTrainer: String? = null // Original Trainer by Username or UUID
var moves: List<String>? = null
var movesetBuilders: List<ResourceLocation>? = null
var heldItem: String? = null
var cosmeticItem: String? = null
var scaleModifier: Float? = null
@@ -471,7 +462,7 @@ open class PokemonProperties {
}
moves?.let { moves ->
if (pokemon.moveSet.getMoves().isEmpty()) {
pokemon.initializeMovesetFromDefault()
pokemon.initializeMoveset(preferLatest = true)
}
val moveTemplates = moves.mapNotNull(Moves::getByName).shuffled()
val replaceableIndices = (0..3).filterNot { pokemon.moveSet[it]?.template?.let(moveTemplates::contains) == true }.shuffled()
@@ -484,13 +475,6 @@ open class PokemonProperties {
pokemon.moveSet.update()
}
}
movesetBuilders?.let { movesetBuilders ->
val builders = movesetBuilders.mapNotNull { CobblemonMovesetBuilders.movesetBuilders[it] }
if (builders.isNotEmpty()) {
val movesetBuilder = builders.random()
pokemon.initializeMovesetFrom(movesetBuilder)
}
}
heldItem?.let { itemKey ->
val server = server() ?: return@let
val parser = ItemParser(server.registryAccess())
@@ -694,7 +678,6 @@ open class PokemonProperties {
originalTrainerType?.let { nbt.putInt(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.ordinal) }
originalTrainer?.let { nbt.putString(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
moves?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
movesetBuilders?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
heldItem?.let {nbt.putString(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
scaleModifier?.let { nbt.putFloat(DataKeys.POKEMON_SCALE_MODIFIER, it) }
val custom = ListTag()
@@ -729,7 +712,6 @@ open class PokemonProperties {
originalTrainerType = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) OriginalTrainerType.valueOf(tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) else null
originalTrainer = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER)) tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER) else null
moves = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVES)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVES).split(",") else null
movesetBuilders = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS).split(",").map { it.asIdentifierDefaultingNamespace() } else null
heldItem = if (tag.contains(DataKeys.POKEMON_PROPERTIES_HELDITEM)) tag.getString(DataKeys.POKEMON_PROPERTIES_HELDITEM) else null
scaleModifier = if (tag.contains(DataKeys.POKEMON_SCALE_MODIFIER)) tag.getFloat(DataKeys.POKEMON_SCALE_MODIFIER) else null
val custom = tag.getList(DataKeys.POKEMON_PROPERTIES_CUSTOM, Tag.TAG_STRING.toInt())
@@ -765,7 +747,6 @@ open class PokemonProperties {
originalTrainerType?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.name) }
originalTrainer?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
moves?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
movesetBuilders?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
heldItem?.let {json.addProperty(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
scaleModifier?.let { json.addProperty(DataKeys.POKEMON_SCALE_MODIFIER, it) }
val custom = JsonArray()
@@ -800,7 +781,6 @@ open class PokemonProperties {
originalTrainerType = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)?.asString?.let { OriginalTrainerType.valueOf(it) }
originalTrainer = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER)?.asString
moves = json.get(DataKeys.POKEMON_PROPERTIES_MOVES)?.asString?.split(",")
movesetBuilders = json.get(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)?.asString?.split(",")?.map { it.asIdentifierDefaultingNamespace() }
heldItem = json.get(DataKeys.POKEMON_PROPERTIES_HELDITEM)?.asString
scaleModifier = json.get(DataKeys.POKEMON_SCALE_MODIFIER)?.asFloat
val custom = json.get(DataKeys.POKEMON_PROPERTIES_CUSTOM)?.asJsonArray
@@ -840,7 +820,6 @@ open class PokemonProperties {
originalTrainer?.let { pieces.add("originaltrainer=$it") }
customProperties.forEach { pieces.add(it.asString()) }
moves?.let { pieces.add("moves=${it.joinToString(separator = ",")}") }
movesetBuilders?.let { pieces.add("movesetbuilders=${it.joinToString(separator = ",") { source -> source.toString() }}") }
heldItem?.let {pieces.add("helditem=$it")}
scaleModifier?.let { pieces.add("scale_modifier=$it") }
return pieces.joinToString(separator)
@@ -23,7 +23,6 @@ import com.cobblemon.mod.common.api.drop.ItemDropMethod
import com.cobblemon.mod.common.api.entity.EntityDimensionsAdapter
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies.getByIdentifier
@@ -132,7 +131,6 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(RidingBehaviourSettings::class.java, RidingBehaviourSettingsAdapter)
.registerTypeAdapter(RideSoundSettingsList::class.java, RideSoundSettingsListAdapter)
.registerTypeAdapter(ObtainableItemCondition::class.java, ObtainableItemConditionAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.disableHtmlEscaping()
.enableComplexMapKeySerialization()
.create()
@@ -102,12 +102,6 @@ object SpeciesFeatures : JsonDataRegistry<SpeciesFeatureProvider<*>> {
@JvmStatic
fun register(name: String, provider: SpeciesFeatureProvider<*>) = register(name, provider, isCoded = true)
@JvmStatic
fun registerType(key: String, clazz: Class<out SpeciesFeatureProvider<*>>) {
types[key] = clazz
}
private fun registerFromAssets(identifier: ResourceLocation, provider: SpeciesFeatureProvider<*>) = register(identifier.path, provider, isCoded = false)
@JvmStatic
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.api.reactive
import com.cobblemon.mod.common.api.events.Cancelable
open class EventObservable<T> : SimpleObservable<T>() {
@JvmOverloads
inline fun post(vararg events: T, then: (T) -> Unit = {}) {
// Issuing generic-typed pass-throughs to varargs doesn't work
// properly unless the source is a vararg as well, complicated story.
@@ -119,10 +119,7 @@ class RidingController(
private fun canTransitionToLand(): Boolean {
if ((entity.isUnderWater)) return false
if ((entity.isEyeInFluid(FluidTags.WATER) || entity.isEyeInFluid(FluidTags.LAVA))) return false
// If the currently in the air but has no land style then don't transition and render the ride useless.
// Hover is a good example for this.
val shouldStayInAir = entity.pokemon.riding.behaviours?.get(RidingStyle.LAND) == null && context?.style == RidingStyle.AIR
return entity.onGround() && !shouldStayInAir
return entity.onGround()
}
private fun canTransitionToLiquid(): Boolean {
@@ -213,23 +213,10 @@ class HoverBehaviour : RidingBehaviour<HoverSettings, HoverState> {
val accel = topSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalHover.accelerationExpr!!) * 20.0)
var newVelocity = state.rideVelocity.get()
val unrotateVel = vehicle.deltaMovement.yRot(vehicle.yRot)
val vehicleFrontVec = Vec3(0.0, 0.0, 1.0)
val faceVel = Vec3(vehicle.deltaMovement.x, 0.0, vehicle.deltaMovement.z)
val d = vehicle.deltaMovement.horizontalDistance()
val v2 = Vec3(faceVel.dot(Vec3(1.0, 0.0, 0.0)) * d, newVelocity.y, faceVel.dot(Vec3(0.0, 0.0, 1.0)) * d)
// Check for collisions and if found, update the speed to reflect it
if (vehicle.horizontalCollision) {
val smackingIntoWallSpeed = max( vehicle.deltaMovement.horizontalDistance(), 0.2)
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z).normalize().scale(smackingIntoWallSpeed).add(0.0, newVelocity.y, 0.0)
}
if (vehicle.verticalCollision) {
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z)
if (vehicle.verticalCollision || vehicle.horizontalCollision) {
state.speed.set(vehicle.deltaMovement.length())
}
// align the velocity vector to be in local vehicle space
@@ -41,6 +41,10 @@ interface CompositeRidingStrategy<T : CompositeSettings> {
toState: RidingBehaviourState,
toSettings: RidingBehaviourSettings
) {
if (vehicle.level().isClientSide) {
val fromSettings = if (settings.defaultBehaviour == toSettings) settings.alternateBehaviour else settings.defaultBehaviour
}
toState.stamina.set(fromState.stamina.get())
toState.rideVelocity.set(fromState.rideVelocity.get())
fromState.reset()
@@ -187,16 +187,17 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
return
}
var newStam = stam
val stamDrainRate = (1.0f / vehicle.runtime.resolveDouble(settings.staminaExpr ?: globalHorse.staminaExpr!!)).toFloat() / 20.0f
val newStam = if (state.sprinting.get()) {
max(0.0f,stam - stamDrainRate)
if (state.sprinting.get()) {
newStam = max(0.0f,stam - stamDrainRate)
} else {
min(1.0f,stam + stamDrainRate * 4)
newStam = min(1.0f,stam + stamDrainRate * 4)
}
state.stamina.set(newStam.coerceIn(0F, 1F))
state.stamina.set(newStam)
}
override fun updatePassengerRotation(
@@ -100,7 +100,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
driver: Player,
input: Vec3
) {
if (vehicle.level().isClientSide) {
if(vehicle.level().isClientSide) {
handleBoosting(state, vehicle)
tickStamina(settings, state, vehicle)
}
@@ -147,7 +147,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
val stamDrainRate = (1.0f / boostTime).toFloat()
val newStam = if (state.boosting.get()) max(0.0f,stam - stamDrainRate)
else (stam + stamDrainRate).coerceIn(0F, 1F)
else min(1.0f,stam + stamDrainRate)
state.stamina.set(newStam)
}
@@ -60,7 +60,7 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
val poseProvider = PoseProvider<SubmarineSettings, SubmarineState>(PoseType.FLOAT)
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && !state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z <= 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
override fun isActive(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return vehicle.isInWater || !vehicle.onGround()
@@ -191,17 +191,11 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
* Calculate Forward Velocity
*********************************************/
val currZVel = state.rideVelocity.get().z
var newZVel = when {
val newZVel = when {
driver.zza > 0.0 -> min(topSpeed, currZVel + accel)
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel)
else -> lerp(currZVel, 0.0, 0.02)
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel) //TODO: Might need to be applied in addition to the normal braking force?
else -> lerp(currZVel, 0.0, 0.05)
}
// If trying to brake then add the friction to the braking force
if (driver.zza != 0.0f && driver.zza.sign.toDouble() != newZVel.sign) {
newZVel = lerp(newZVel, 0.0, 0.02)
}
currVel = Vec3(currVel.x, currVel.y, newZVel)
/*********************************************
@@ -212,20 +206,14 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
val gravity = (9.8 / ( 20.0)) * 0.2 * 0.4
val terminalYVel = 2.0
val driverYInput = if(driver.jumping) 1.0 else if (driver.isShiftKeyDown) -1.0 else 0.0
var newYVel = when {
val newYVel = when {
(!vehicle.isInWater) -> max(-terminalYVel, currYVel - gravity)
//(vehicle.isInWater && !vehicle.isUnderWater && currYVel > 0.001) -> lerp(currYVel, 0.0, 0.1) // Make sure that yVel is above a threshold before doing this otherwise it'll block submerging.
//driverYInput != 0.0 && currYVel > 0.001 && driverYInput.sign != currYVel.sign -> lerp(currYVel, 0.0, 0.05)
driverYInput > 0.0 -> min(vertTopSpeed, currYVel + strafeAccel)
driverYInput < 0.0 -> max(-vertTopSpeed, currYVel - strafeAccel)
else -> lerp(currYVel, 0.0, 0.02)
else -> lerp(currYVel, 0.0, 0.05)
}
// If trying to brake then add the friction to the braking force
if (driverYInput != 0.0 && driverYInput != newYVel.sign) {
newYVel = lerp(newYVel, 0.0, 0.02)
}
currVel = Vec3(currVel.x, newYVel, currVel.z)
/*********************************************
@@ -233,17 +221,11 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
*********************************************/
val currXVel = state.rideVelocity.get().x
val horzTopSpeed = strafeSpeed
var newXVel = when {
val newXVel = when {
driver.xxa > 0.0 -> min(horzTopSpeed, currXVel + strafeAccel)
driver.xxa < 0.0 -> max( -horzTopSpeed, currXVel - strafeAccel)
else -> lerp(currXVel, 0.0, 0.02)
else -> lerp(currXVel, 0.0, 0.01)
}
// If trying to brake then add the friction to the braking force
if (driver.xxa != 0.0f && driver.xxa.sign.toDouble() != newZVel.sign) {
newXVel = lerp(newXVel, 0.0, 0.02)
}
currVel = Vec3(newXVel, currVel.y, currVel.z)
// Set the new velocity
@@ -323,6 +305,81 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
}
private fun correctOrientation(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
controller: OrientationController,
driver: Player,
deltaTime: Double
) {
// TODO: remove if not needed
// // Calculate correcting roll force.
// // If the ride is moving then have the correction rate be an equal proportion of:
// // - how rolled are you
// // - how long has it been since last mouse input (higher is stronger correction)
// // Also the correction is proportional as a whole to how NOT pitched are you so you don't spin endlessly when looking up or down
// val howRolledAmI = sqrt(abs((cos(controller.roll.toRadians()) - 1.0f) * 0.5f)) // sqrt so it trends towards one pretty quickly
// val maxRollCorrectionRate =
// if (state.rideVelocity.get().length() < 0.2) 10.0f * abs(cos(controller.pitch.toRadians()))
// else 10.0f * cos(controller.pitch.toRadians()).pow(2) * (
// howRolledAmI*1.0f +
// (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
// )
// val maxRollForce = maxRollCorrectionRate / 40.0f
// val rollArrivalDeg = 60.0f
// val desiredRoll = 0.0f
//
// // Calculate the signed error (how far and in what direction)
// val rollError = Mth.wrapDegrees(desiredRoll.angleDifference(controller.roll))
//
// // Use the absolute error to calculate the magnitude of the influence
// val influenceMagnitude = (min(rollArrivalDeg, abs(rollError)) / rollArrivalDeg)
// val desiredRollForce = maxRollCorrectionRate * influenceMagnitude * -rollError.sign
// var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
//
// // Ensure the 'steering' correction force doesn't exceed our maxForce
// if (abs(steeredRollForce) > maxRollForce) {
// steeredRollForce = steeredRollForce.sign * maxRollForce
// }
//
// // Apply the 'steered' force (its really not a steering force I need a better name)
// state.currRollCorrectionForce.set(
// state.currRollCorrectionForce.get() + steeredRollForce
// )
//Calculate correcting pitch force.
// val rollCorrectionTimer = vehicle.runtime.resolveDouble(settings.timeToRollCorrect ?: globalSubmarine.timeToRollCorrect!!)
// val maxPitchCorrectionRate = 5.0f * (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
// val maxPitchForce = maxPitchCorrectionRate / 40.0f
// val pitchArrivalDeg = 45.0f
// val desiredPitch = 0.0f
//
// if(state.rideVelocity.get().length() < 0.1 && state.noInputTime.get() > rollCorrectionTimer) {
// val pitchErrror = Mth.wrapDegrees(desiredPitch.angleDifference(controller.pitch))
// val arrivalInfluence = (min(pitchArrivalDeg,abs(pitchErrror)) / pitchArrivalDeg)
// val desiredPitchForce = maxPitchCorrectionRate * arrivalInfluence * -pitchErrror.sign
// var steeredPitchForce = desiredPitchForce - state.currPitchCorrectionForce.get()
//
// // Ensure the 'steering' correction force doesn't exceed our maxForce
// if (abs(steeredPitchForce) > maxPitchForce) {
// steeredPitchForce = steeredPitchForce.sign * maxPitchForce
// }
//
// // Apply the 'steered' force (its really not a steering force I need a better name)
// state.currPitchCorrectionForce.set(
// state.currPitchCorrectionForce.get() + steeredPitchForce
// )
// } else {
// state.currPitchCorrectionForce.set(
// //reduce rollCorrection gradually
// lerp(state.currPitchCorrectionForce.get(), 0.0, deltaTime * 0.98)
// )
// }
}
override fun rotationOnMouseXY(
settings: SubmarineSettings,
state: SubmarineState,
@@ -44,7 +44,6 @@ import com.cobblemon.mod.common.api.spawning.preset.BestSpawnerConfig
import com.cobblemon.mod.common.api.spawning.preset.PokemonSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
import com.cobblemon.mod.common.api.spawning.spawner.FishingSpawnerFactory
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
@@ -140,6 +139,19 @@ object BestSpawner {
fun onServerStarted(server: MinecraftServer) {
CobblemonSpawnPools.onServerLoad(server)
fishingSpawner = FishingSpawnerFactory.createSharedSpawner()
fishingSpawner = BasicSpawner(
name = "fishing",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
).also {
it.influences.add(
BucketMultiplyingInfluence(
multipliers = mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
)
}
}
}
@@ -26,7 +26,7 @@ import net.minecraft.world.entity.Entity
/**
* A [SpawningInfluence] that applies some number of SpawnBait effects.
*
* @author Hiroku, Plastered_Crab, ppVon
* @author Hiroku, Plastered_Crab
* @since March 18th, 2025
*/
open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (time: Int, entity: PokemonEntity?) -> Unit = { _, _ -> }) : SpawningInfluence {
@@ -50,84 +50,85 @@ open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (
}
}
// EV, Type, and Egg Group related bait effects
// EV related bait effects
override fun affectWeight(detail: SpawnDetail, spawnablePosition: SpawnablePosition, weight: Float): Float {
val merged = SpawnBaitUtils.mergeEffects(effects)
val hasRelevantEffects =
merged.any { it.type == Effects.EV } ||
merged.any { it.type == Effects.TYPING } ||
merged.any { it.type == Effects.EGG_GROUP }
if (!hasRelevantEffects) {
return super.affectWeight(detail, spawnablePosition, weight)
}
val pokemonDetail = detail as? PokemonSpawnDetail
?: return super.affectWeight(detail, spawnablePosition, weight)
val detailSpecies = pokemonDetail.pokemon.species
?.let { PokemonSpecies.getByName(it) }
?: return super.affectWeight(detail, spawnablePosition, weight)
val formData = detailSpecies.getForm(pokemonDetail.pokemon.aspects)
var newWeight = weight
var used = false
// if bait exists and any effects are related to EV yields
if (merged.any { it.type == Effects.EV }) {
val evEffect = effects.firstOrNull { it.type == Effects.EV }
val baitEVStat = evEffect?.subcategory?.path?.let { Stats.getStat(it) }
if (merged.any { it.type == Effects.EV }){
if (detail is PokemonSpawnDetail) {
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
val baitEVStat = effects.firstOrNull { it.type == Effects.EV }?.subcategory?.path?.let { Stats.getStat(it) }
if (baitEVStat != null) {
val evYieldValue = formData.evYield[baitEVStat]?.toFloat() ?: 0f
used = true
if (evYieldValue <= 0f) {
newWeight = 0f
} else {
LOGGER.debug("Boosted weight value");
if (detailSpecies != null && baitEVStat != null) {
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
val evYieldValue = formData.evYield[baitEVStat]?.toFloat() ?: 0f
return when {
evYieldValue > 0 -> {
markUsed()
super.affectWeight(detail, spawnablePosition, weight)
}
else -> {
markUsed()
super.affectWeight(detail, spawnablePosition, 0f)
}
}
}
}
}
// if bait exists and any effects are related to Typing
if (newWeight > 0f && merged.any { it.type == Effects.TYPING }) {
val baitEffect = effects.firstOrNull { it.type == Effects.TYPING }
val baitTypingEffect = baitEffect?.subcategory?.path?.let { ElementalTypes.get(it) }
if (merged.any { it.type == Effects.TYPING }){
if (detail is PokemonSpawnDetail) {
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
val baitEffect = effects.firstOrNull { it.type == Effects.TYPING }
val baitTypingEffect = baitEffect?.subcategory?.path?.let { ElementalTypes.get(it) }
if (baitEffect != null && baitTypingEffect != null) {
val isMatchingType = formData.types.contains(baitTypingEffect)
used = true
if (isMatchingType) {
newWeight *= baitEffect.value.toFloat()
if (detailSpecies != null && baitTypingEffect != null) {
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
val isMatchingType = formData.types.contains(baitTypingEffect)
return when {
isMatchingType -> {
markUsed()
super.affectWeight(detail, spawnablePosition, weight * baitEffect.value.toFloat())
}
else -> super.affectWeight(detail, spawnablePosition, weight)
}
}
}
}
// if bait exists and any effects are related to Egg Groups
if (newWeight > 0f && merged.any { it.type == Effects.EGG_GROUP }) {
val eggGroupEffects = effects.filter { it.type == Effects.EGG_GROUP }
if (merged.any { it.type == Effects.EGG_GROUP }) {
if (detail is PokemonSpawnDetail) {
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
val matchingEffect = eggGroupEffects.firstOrNull { effect ->
val key = effect.subcategory?.path ?: return@firstOrNull false
val eggGroup = EggGroup.fromIdentifier(key)
if (eggGroup == null) {
LOGGER.warn("Unknown egg group identifier: $key")
return@firstOrNull false
if (detailSpecies != null) {
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
// Collect all the egg group effects
val eggGroupEffects = effects.filter { it.type == Effects.EGG_GROUP }
// Check if any of the egg group effects match the species' egg groups
val matchingEffect = eggGroupEffects.firstOrNull { effect ->
val effectEggGroupKey = effect.subcategory?.path ?: return@firstOrNull false
val eggGroup = EggGroup.fromIdentifier(effectEggGroupKey)
if (eggGroup == null) {
LOGGER.warn("Unknown egg group identifier: $effectEggGroupKey")
return@firstOrNull false
}
formData.eggGroups.contains(eggGroup)
}
if (matchingEffect != null) {
markUsed()
val multiplier = matchingEffect.value
return super.affectWeight(detail, spawnablePosition, (weight * multiplier).toFloat())
}
}
formData.eggGroups.contains(eggGroup)
}
if (matchingEffect != null) {
used = true
newWeight *= matchingEffect.value.toFloat()
}
}
if (used) markUsed()
return super.affectWeight(detail, spawnablePosition, newWeight)
return super.affectWeight(detail, spawnablePosition, weight)
}
}
@@ -44,7 +44,7 @@ import kotlin.random.Random
* @since July 10th, 2022
*/
open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelectionData> {
class SelectingSpawnInformation(val spawnDetail: SpawnDetail) {
class SelectingSpawnInformation {
val spawnablePositions = mutableMapOf<SpawnablePosition, Float>()
var highestWeight = 0F
@@ -58,81 +58,61 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
fun chooseSpawnablePosition() = spawnablePositions.entries.weightedSelection { it.value }!!.key
}
class SpawnablePositionBucketSelectionData(
val selectingSpawnInformation: MutableList<SelectingSpawnInformation>
) {
var percentSum: Float = 0F
val seenSpawns = mutableListOf<SpawnDetail>()
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every spawnable position the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
seenSpawns.add(spawnDetail)
percentSum += spawnDetail.percentage
}
val spawnInformation = selectingSpawnInformation.find { it.spawnDetail == spawnDetail }
?: SelectingSpawnInformation(spawnDetail).also { selectingSpawnInformation.add(it) }
spawnInformation.add(spawnablePosition, spawnablePosition.getWeight(spawnDetail))
}
fun remove(spawnInformation: SelectingSpawnInformation) {
selectingSpawnInformation -= spawnInformation
if (spawnInformation.spawnDetail.percentage > 0) {
percentSum -= spawnInformation.spawnDetail.percentage
}
}
}
class SpawnablePositionSelectionData(
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>,
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData>,
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
var percentSum: Float
): SpawnSelectionData {
val size: Int
get() = spawnToSpawnablePosition.size
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
fun getDataForBucket(bucket: SpawnBucket): SpawnablePositionBucketSelectionData {
val existing = selectingSpawnInformation[bucket]
if (existing != null) {
return existing
}
val bucketSpawnInformation = SpawnablePositionBucketSelectionData(mutableListOf())
spawnablePositions.forEach { spawnablePosition ->
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
bucketSpawnInformation.add(it, spawnablePosition)
}
}
selectingSpawnInformation[bucket] = bucketSpawnInformation
return bucketSpawnInformation
}
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation.filter { shouldRemove(it.spawnDetail) } }
.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
}
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
val toRemove = selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation }.filter { positionData ->
positionData.spawnablePositions.removeIf { shouldRemove(positionData.spawnDetail, it.key) }
val toRemove = spawnToSpawnablePosition.entries.filter { (spawnDetail, positionData) ->
positionData.spawnablePositions.removeIf { shouldRemove(spawnDetail, it.key) }
positionData.highestWeight = positionData.spawnablePositions.maxOfOrNull { it.value } ?: 0F
positionData.spawnablePositions.isEmpty()
}
toRemove.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
}
}
override fun getSelectionData(
spawner: Spawner,
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): SpawnablePositionSelectionData {
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData> = mutableMapOf()
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation> = mutableMapOf()
var percentSum = 0F
return SpawnablePositionSelectionData(spawner, spawnablePositions, selectingSpawnInformation)
spawnablePositions.forEach { spawnablePosition ->
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every spawnable position the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnToSpawnablePosition.containsKey(it)) {
percentSum += it.percentage
}
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(
it,
::SelectingSpawnInformation
)
selectingSpawnInformation.add(spawnablePosition, spawnablePosition.getWeight(it))
}
}
return SpawnablePositionSelectionData(spawnToSpawnablePosition, percentSum)
}
override fun selectSpawnAction(
@@ -140,8 +120,8 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
bucket: SpawnBucket,
selectionData: SpawnablePositionSelectionData,
): SpawnAction<*>? {
val selectingSpawnInformation = selectionData.getDataForBucket(bucket) ?: return null
var percentSum = selectingSpawnInformation.percentSum
val spawnToSpawnablePosition = selectionData.spawnToSpawnablePosition
var percentSum = selectionData.percentSum
// First pass is doing percentage checks.
if (percentSum > 0) {
@@ -149,7 +129,7 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
if (percentSum > 100) {
Cobblemon.LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
This means you don't understand how this option works.
""".trimIndent()
)
@@ -160,13 +140,13 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
* It's [0, 1) and I want (0, 1]
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
*/
val selectedPercentage = 100 - Random.nextFloat() * 100
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
percentSum = 0F
for (info in selectingSpawnInformation.selectingSpawnInformation) {
if (info.spawnDetail.percentage > 0) {
percentSum += info.spawnDetail.percentage
for ((spawnDetail, info) in spawnToSpawnablePosition) {
if (spawnDetail.percentage > 0) {
percentSum += spawnDetail.percentage
if (percentSum >= selectedPercentage) {
return info.spawnDetail.choose(
return spawnDetail.choose(
spawnablePosition = info.chooseSpawnablePosition(),
bucket = bucket,
selectionData = selectionData
@@ -176,9 +156,9 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
}
}
val selectedSpawn = selectingSpawnInformation.selectingSpawnInformation.toList().weightedSelection { it.highestWeight }!!
return selectedSpawn.spawnDetail.choose(
spawnablePosition = selectedSpawn.chooseSpawnablePosition(),
val selectedSpawn = spawnToSpawnablePosition.entries.toList().weightedSelection { it.value.highestWeight }!!
return selectedSpawn.key.choose(
spawnablePosition = selectedSpawn.value.chooseSpawnablePosition(),
bucket = bucket,
selectionData = selectionData
)
@@ -189,16 +169,16 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
if (selectionData.selectingSpawnInformation.isEmpty()) {
if (selectionData.size == 0) {
return emptyMap()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
for (info in selectionData.selectingSpawnInformation) {
totalWeights[info.spawnDetail] = info.highestWeight
for ((spawnDetail, info) in selectionData.spawnToSpawnablePosition.entries) {
totalWeights[spawnDetail] = info.highestWeight
}
return totalWeights
@@ -18,6 +18,7 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.util.removeIf
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
import kotlin.reflect.full.createInstance
/**
* A spawning selector that compiles a distinct list of all spawn details that
@@ -32,9 +33,9 @@ import kotlin.random.Random
* of an entire spawnable position type to favour whichever are the more populous.
*
* The weight for a spawn when doing primary selection is whichever spawnable position-influenced
* weight is highest (as weight multipliers exist per spawnable position). After the spawn has been
* chosen, the selection of which spawnable position to spawn it at is selected via another weighted
* selection using spawnable position-adjusted weights.
* weight is highest (as weight multipliers exist per spawnable position) and then the
* selection of which spawnable position to spawn the primary selected spawn uses the spawnable
* position-adjusted weight.
*
* At a glance:
* - Spawn detail selection is flat across spawnable position quantity
@@ -44,6 +45,7 @@ import kotlin.random.Random
* @since July 10th, 2022
*/
open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnablePositionWeightedSelector.SeparatedSelectionData> {
open fun getWeight(spawnablePositionType: SpawnablePositionType<*>) = spawnablePositionType.getWeight()
class SelectingSpawnInformation {
val spawnablePositionWeights = mutableMapOf<SpawnablePosition, Float>()
@@ -60,25 +62,13 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
fun chooseSpawnablePosition() = spawnablePositionWeights.keys.toList().weightedSelection { spawnablePositionWeights[it]!! }!!
}
class SpawnablePositionSelectionData {
val spawnToSpawnablePosition = mutableMapOf<SpawnDetail, SelectingSpawnInformation>()
var percentSum: Float = 0F
val seenSpawns = mutableListOf<SpawnDetail>()
class SpawnablePositionSelectionData(
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
var percentSum: Float
) {
val size: Int
get() = spawnToSpawnablePosition.size
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
// position type, otherwise the percentage will get amplified for every spawnable position the thing
// was possible, completely ruining the point of this pre-selection percentage.
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
percentSum += spawnDetail.percentage
seenSpawns.add(spawnDetail)
}
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(spawnDetail, ::SelectingSpawnInformation)
selectingSpawnInformation.add(spawnDetail, spawnablePosition, 1F)
}
fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
@@ -99,68 +89,67 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
}
class SeparatedSelectionData(
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>
) : SpawnSelectionData {
val spawnablePositionTypeToSpawns: MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>
): SpawnSelectionData {
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
val bucketData = mutableMapOf<SpawnBucket, MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>>()
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
bucketData.values.forEach {
it.values.forEach { it.removeSpawnDetails(shouldRemove) }
it.removeIf { it.value.size == 0 }
}
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnDetails(shouldRemove) }
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
}
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
bucketData.values.forEach {
it.values.forEach { it.removeSpawnablePositions(shouldRemove) }
it.removeIf { it.value.size == 0 }
}
}
fun getDataForBucket(bucket: SpawnBucket): MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData> {
bucketData[bucket]?.let { return it }
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
spawnablePositions.forEach { spawnablePosition ->
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
if (possible.isNotEmpty()) {
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns
.getOrPut(spawnablePositionType, ::SpawnablePositionSelectionData)
possible.forEach { spawnablePositionSelectionData.add(it, spawnablePosition) }
}
}
bucketData[bucket] = spawnablePositionTypeToSpawns
return spawnablePositionTypeToSpawns
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnablePositions(shouldRemove) }
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
}
}
override fun getSelectionData(
spawner: Spawner,
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
) = SeparatedSelectionData(spawner, spawnablePositions)
): SeparatedSelectionData {
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
spawnablePositions.forEach { spawnablePosition ->
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
if (possible.isNotEmpty()) {
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns.getOrPut(spawnablePositionType) { SpawnablePositionSelectionData(mutableMapOf(), 0F) }
possible.forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
// position type, otherwise the percentage will get amplified for every spawnable position the thing
// was possible, completely ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnablePositionSelectionData.spawnToSpawnablePosition.containsKey(it)) {
spawnablePositionSelectionData.percentSum += it.percentage
}
val selectingSpawnInformation = spawnablePositionSelectionData.spawnToSpawnablePosition.getOrPut(
it,
SelectingSpawnInformation::class::createInstance
)
selectingSpawnInformation.add(it, spawnablePosition, getWeight(spawnablePositionType))
}
}
}
return SeparatedSelectionData(spawnablePositionTypeToSpawns)
}
override fun selectSpawnAction(
spawner: Spawner,
bucket: SpawnBucket,
selectionData: SeparatedSelectionData
): SpawnAction<*>? {
val bucketData = selectionData.getDataForBucket(bucket)
if (bucketData.isEmpty()) {
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
return null
}
// Which spawnable position type should we use?
val spawnablePositionSelectionData = bucketData.entries.toList()
.weightedSelection { it.key.getWeight() * it.value.size }
val spawnablePositionSelectionData = selectionData.spawnablePositionTypeToSpawns.entries.toList()
.weightedSelection { getWeight(it.key) * it.value.size }
?.value
?: return null
@@ -172,7 +161,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
if (percentSum > 100) {
LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
This means you don't understand how this option works.
""".trimIndent()
)
@@ -184,7 +173,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
* It's [0, 1) and I want (0, 1]
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
*/
val selectedPercentage = 100 - Random.nextFloat() * 100
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
percentSum = 0F
for ((spawnDetail, info) in spawnToSpawnablePosition) {
if (spawnDetail.percentage > 0) {
@@ -235,8 +224,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
val percentageWeight = (rescaledTotalWeight - totalWeight) / percentSum
for ((spawnDetail, info) in spawnToSpawnablePosition.entries) {
totalWeights[spawnDetail] =
info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
totalWeights[spawnDetail] = info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
}
return totalWeights
@@ -247,18 +235,18 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
if (selectionData.isEmpty()) {
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
return mapOf()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
val totalSpawnablePositionWeight = selectionData.keys.sumOf { it.getWeight().toDouble() }.toFloat()
val totalSpawnablePositionWeight = selectionData.spawnablePositionTypeToSpawns.keys.sumOf { getWeight(it).toDouble() }.toFloat()
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData) {
val spawnPositionWeightCorrection = spawnablePositionType.getWeight() / totalSpawnablePositionWeight
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData.spawnablePositionTypeToSpawns) {
val spawnPositionWeightCorrection = getWeight(spawnablePositionType) / totalSpawnablePositionWeight
val spawnPositionProbabilities = getProbabilitiesFromSpawnablePositionType(spawner, spawnPositionSelectionData)
spawnPositionProbabilities.entries.forEach {
@@ -29,7 +29,7 @@ interface SpawningSelector<T : SpawnSelectionData> {
val DEFAULT = FlatSpawnablePositionWeightedSelector()
}
fun getSelectionData(spawner: Spawner, spawnablePositions: List<SpawnablePosition>): T
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
fun selectSpawnAction(
spawner: Spawner,
@@ -37,18 +37,12 @@ interface SpawningSelector<T : SpawnSelectionData> {
selectionData: T
): SpawnAction<*>?
fun select(
spawner: Spawner,
bucketSelector: () -> SpawnBucket,
spawnablePositions: List<SpawnablePosition>,
maxSpawns: Int
): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, spawnablePositions)
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val spawnActions = selectionData.spawnActions
while (spawnActions.size < maxSpawns) {
val bucket = bucketSelector()
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -1,86 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.util.mutableLazy
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.item.ItemStack
import com.cobblemon.mod.common.api.spawning.BestSpawner;
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
/**
* This isn't really a factory, one FishingSpawner is created as a field on [BestSpawner] on
* server start. However, this emulates the systems that PlayerSpawner uses so extenders can
* add influences that affect all fishing encounters.
*
* Responsible for creating [BasicSpawner]s for Fishing with whatever appropriate settings.
* You can swap over the spawn pool and the influences here, and it will apply to all newly-generated
* [BasicSpawner]s for Fishing. Has a global every-cast influence list, as well as a context-sensitive
* influence list.
*
* @author Andy
* @since January 23rd, 2026
*/
object FishingSpawnerFactory {
var spawnPool: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
data class Context(
val player: ServerPlayer,
val rodStack: ItemStack,
val lureLevel: Int,
val luckOfTheSeaLevel: Int,
val world: ServerLevel,
val pos: BlockPos,
val spawner: BasicSpawner
)
/**
* List of influences for a new cast of the fishing rod. Context is provided at
* build time in [buildPositionInfluences]
*/
var positionInfluenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
{ ctx -> PlayerLevelRangeInfluence(ctx.player, TYPICAL_VARIATION) },
{ ctx -> BucketNormalizingInfluence(tier = ctx.lureLevel + ctx.luckOfTheSeaLevel) }
)
fun createSharedSpawner(): BasicSpawner {
return BasicSpawner(
name = "fishing",
spawnPool = spawnPool
).also { spawner ->
spawner.influences += BucketMultiplyingInfluence(
multipliers = mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
}
}
/**
* Builds the per-cast influences for [FishingSpawnablePosition]
*/
fun buildPositionInfluences(ctx: Context): MutableList<SpawningInfluence> {
return positionInfluenceBuilders.mapNotNullTo(mutableListOf()) { it(ctx) }
}
}
@@ -1,101 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.rules.SpawnRule
import com.cobblemon.mod.common.util.mutableLazy
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
/**
* Responsible for creating [FixedAreaSpawner]s for PokeSnacks with whatever appropriate settings.
* You can swap over the spawn pool and the influences here, and it will apply to all newly-generated
* [FixedAreaSpawner]s for PokeSnacks.
*
* @author Andy
* @since January 23rd, 2026
*/
object PokeSnackSpawnerFactory {
var spawnPool: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
data class Context(
val world: ServerLevel,
val pos: BlockPos,
val horizontalRadius: Int,
val verticalRadius: Int,
val maxPokemonPerChunk: Float,
val name: String,
val baitEffects: List<SpawnBait.Effect>
)
/**
* The list of influences for a new [FixedAreaSpawner] for a PokeSnack.
* The influences will be added to the spawner in the order they are added to this list.
*/
var influenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
{ ctx ->
val stackedLureTier = ctx.baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
if (stackedLureTier <= 0) null else BucketNormalizingInfluence(
tier = stackedLureTier,
gradient = 0.2F,
firstTier = 1.2F
)
},
{ _ ->
BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
},
{ ctx ->
SpawnBaitInfluence(effects = ctx.baitEffects)
},
)
fun create(ctx: Context): FixedAreaSpawner {
val influences = influenceBuilders.mapNotNull { it(ctx) }
return FixedAreaSpawner(
name = ctx.name,
spawnPool = spawnPool,
world = ctx.world,
position = ctx.pos,
horizontalRadius = ctx.horizontalRadius,
verticalRadius = ctx.verticalRadius,
maxPokemonPerChunk = ctx.maxPokemonPerChunk
).also { spawner ->
spawner.influences.addAll(influences)
// TODO(Andy): PokeSnacks currently bypass pokemon spawn rules, should that be true?
// Adding this rule will change that
spawner.influences.addAll(
CobblemonSpawnRules.rules.values
.filter(SpawnRule::enabled)
.flatMap(SpawnRule::components)
)
}
}
}
@@ -29,6 +29,13 @@ import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.Any
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.plus
import kotlin.math.max
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
@@ -88,9 +95,10 @@ interface Spawner {
): SpawnAction<*>? {
influences.removeIf { it.isExpired() }
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(
spawner = this,
bucketSelector = { chooseBucket(cause, spawnablePosition.influences) },
bucket = bucket,
spawnablePositions = listOf(spawnablePosition),
maxSpawns = 1
).firstOrNull()
@@ -108,9 +116,9 @@ interface Spawner {
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
ENTITY_LIMIT_CHUNK_RANGE * 16.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
1000.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
@@ -132,10 +140,11 @@ interface Spawner {
val zone = generator.generate(this, constrainedArea)
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
val influences = influences + zone.unconditionalInfluences
val bucket = chooseBucket(zoneInput.cause, influences)
return selector.select(
spawner = this,
bucketSelector = { chooseBucket(zoneInput.cause, influences) },
bucket = bucket,
spawnablePositions = spawnablePositions,
maxSpawns = maxSpawns
)
@@ -258,22 +258,17 @@ open class PartyStore(override val uuid: UUID) : PokemonStore<PartyPosition>() {
@JvmOverloads
fun toBattleTeam(clone: Boolean = false, healPokemon: Boolean = false, leadingPokemon: UUID? = null) : List<BattlePokemon> {
var leadingPokemonId = leadingPokemon
val result = this.mapNotNull {
return@mapNotNull if (clone) {
BattlePokemon.safeCopyOf(it).also { copy ->
if (leadingPokemon != null && it.uuid == leadingPokemonId) {
leadingPokemonId = copy.uuid
}
}
BattlePokemon.safeCopyOf(it)
} else {
BattlePokemon.playerOwned(it)
}.also { if (healPokemon) it.effectedPokemon.heal() }
}.toMutableList()
// reposition lead to front of the party
if (leadingPokemonId != null && result.first().uuid != leadingPokemonId) {
result.find { it.uuid == leadingPokemonId }?.let { lead ->
if (leadingPokemon != null && result.first().uuid != leadingPokemon) {
result.find { it.uuid == leadingPokemon }?.let { lead ->
result.remove(lead)
result.add(0, lead)
}
@@ -154,12 +154,12 @@ open class PlayerPartyStore(
// Metabolism for Fullness
if (pokemon.currentFullness > 0) {
pokemon.tickMetabolism(20)
pokemon.tickMetabolism()
}
// Interaction Cooldown
if (pokemon.interactionCooldowns.any()) {
pokemon.tickInteractionCooldown(20)
pokemon.tickInteractionCooldown()
}
pokemon.features.filterIsInstance<TickingSpeciesFeature>().forEach { it.onSecondPassed(player.serverLevel(), pokemon, null) }
@@ -28,7 +28,6 @@ data class GeneralPlayerData(
var starterUUID: UUID?,
var keyItems: MutableSet<ResourceLocation>,
var battleTheme: ResourceLocation?,
var partySelectTutorialDone: Boolean = false,
val extraData: MutableMap<String, PlayerDataExtension>,
) : InstancedPlayerData {
var advancementData: PlayerAdvancementData = PlayerAdvancementData()
@@ -54,8 +53,7 @@ data class GeneralPlayerData(
starterSelected,
starterUUID,
advancementData.totalBattleVictoryCount == 0,
battleTheme,
partySelectTutorialDone
battleTheme
)
}
@@ -36,8 +36,7 @@ class PlayerDataJsonBackend: JsonBackedPlayerDataStoreBackend<GeneralPlayerData>
starterUUID = null,
keyItems = Cobblemon.config.defaultKeyItems,
extraData = mutableMapOf(),
battleTheme = CobblemonSounds.PVP_BATTLE.location,
partySelectTutorialDone = false
battleTheme = CobblemonSounds.PVP_BATTLE.location
)}
override val gson = GsonBuilder()
@@ -37,8 +37,7 @@ class PlayerDataMongoBackend(mongoClient: MongoClient, databaseName: String, col
starterUUID = null,
keyItems = Cobblemon.config.defaultKeyItems,
extraData = mutableMapOf(),
battleTheme = CobblemonSounds.PVP_BATTLE.location,
partySelectTutorialDone = false
battleTheme = CobblemonSounds.PVP_BATTLE.location
)}
override val gson = GsonBuilder()
@@ -34,8 +34,7 @@ data class ClientGeneralPlayerData(
var starterSelected: Boolean = false,
var starterUUID: UUID? = null,
var showChallengeLabel: Boolean = true,
val battleTheme: ResourceLocation? = null,
var partySelectTutorialDone: Boolean = false
val battleTheme: ResourceLocation? = null
) : ClientInstancedPlayerData {
override fun encode(buf: RegistryFriendlyByteBuf) {
@@ -47,7 +46,6 @@ data class ClientGeneralPlayerData(
buf.writeNullable(starterUUID) { pb, value -> pb.writeString(value.toString()) }
buf.writeNullable(resetStarters) { pb, value -> pb.writeBoolean(value) }
buf.writeNullable(battleTheme) {pb, value -> pb.writeIdentifier(value)}
buf.writeBoolean(partySelectTutorialDone)
}
companion object {
fun decode(buffer: RegistryFriendlyByteBuf): SetClientPlayerDataPacket {
@@ -59,7 +57,6 @@ data class ClientGeneralPlayerData(
val starterUUID = buffer.readNullable { it.readString() }?.let { UUID.fromString(it) }
val resetStarterPrompt = buffer.readNullable { it.readBoolean() }
val battleTheme = buffer.readNullable { it.readIdentifier() }
val partySelectTutorialDone = buffer.readBoolean()
val data = ClientGeneralPlayerData(
resetStarterPrompt,
promptStarter,
@@ -67,8 +64,7 @@ data class ClientGeneralPlayerData(
starterSelected,
starterUUID,
showChallengeLabel,
battleTheme,
partySelectTutorialDone
battleTheme
)
//Weird to do this, but since the flag doesn't get passed to the decoded obj, do it here
//Should be fine, as long as decode doesn't get run on the server for some reason
@@ -42,7 +42,6 @@ object CobblemonItemTags {
@JvmField val DAWN_STONE_ORES = create("dawn_stone_ores")
@JvmField val POTTERY_SHERDS = create("decorated_pot_sherds")
@JvmField val DEEP_SEAS = create("deep_seas")
@JvmField val DISPLAY_CASE_ITEMS = create("display_case_items")
@JvmField val DUSK_STONE_ORES = create("dusk_stone_ores")
@JvmField val ETHERS = create("ethers")
@JvmField val EVOLUTION_ITEMS = create("evolution_items")
@@ -58,7 +57,6 @@ object CobblemonItemTags {
@JvmField val HANGING_SIGNS = create("hanging_signs")
@JvmField val HERBS = create("herbs")
@JvmField val ICE_STONE_ORES = create("ice_stone_ores")
@JvmField val ITEM_BLOCKS = create("item_blocks")
@JvmField val LEAF_STONE_ORES = create("leaf_stone_ores")
@JvmField val MACHINES = create("machines")
@JvmField val MINT_LEAF = create("mint_leaf")
@@ -67,7 +65,6 @@ object CobblemonItemTags {
@JvmField val MOCHIS = create("mochis")
@JvmField val MOON_STONE_ORES = create("moon_stone_ores")
@JvmField val MUTATED_BERRIES = create("mutated_berries")
@JvmField val PLAQUES = create("plaques")
@JvmField val PLANTS = create("plants")
@JvmField val POKE_BALLS = create("poke_balls")
@JvmField val POKE_RODS = create("poke_rods")
@@ -63,8 +63,8 @@ object BattleBuilder {
): BattleStartResult {
val adjustLevel = battleFormat.adjustLevel
val team1 = partyAccessor(player1).toBattleTeam(clone = cloneParties || adjustLevel > 0, healPokemon = healFirst, leadingPokemonPlayer1)
val team2 = partyAccessor(player2).toBattleTeam(clone = cloneParties || adjustLevel > 0, healPokemon = healFirst, leadingPokemonPlayer2)
val team1 = partyAccessor(player1).toBattleTeam(clone = cloneParties || adjustLevel > 0, healPokemon = healFirst, leadingPokemonPlayer1).sortedBy { it.health <= 0 }
val team2 = partyAccessor(player2).toBattleTeam(clone = cloneParties || adjustLevel > 0, healPokemon = healFirst, leadingPokemonPlayer2).sortedBy { it.health <= 0 }
val battlePartyStores = mutableListOf<PlayerPartyStore>()
@@ -105,18 +105,12 @@ data class BattleFormat(
}
fun toFormatJSON(): String {
/*
Bag clause does not exist in showdown as a rule because showdown simply has no concept of a 'bag'.
If we try sending it, the battle will error. So this simply omits that from ever getting sent.
If we ever find a case where we are doing this for more than just bag clause, we should make a proper abstraction.
*/
val rulesToSendToShowdown = ruleSet.filter { it != BattleRules.BAG_CLAUSE }
return """
{
"mod": "$mod",
"gameType": "${battleType.name}",
"gen": $gen,
"ruleset": [${rulesToSendToShowdown.joinToString { "\"$it\"" }}],
"ruleset": [${ruleSet.joinToString { "\"$it\"" }}],
"effectType": "Format"
}
""".trimIndent().replace("\n", "")
@@ -221,7 +221,6 @@ object BattleRegistry {
fun closeBattle(battle: PokemonBattle) {
battle.onEndHandlers.forEach { it(battle) }
battleMap.remove(battle.battleId)
ShowdownService.service.endBattle(battle)
}
@JvmStatic
@@ -12,7 +12,6 @@ object BattleRules {
const val OBTAINABLE = "Obtainable"
const val PAST = "+Past"
const val UNOBTAINABLE = "+Unobtainable"
const val BAG_CLAUSE = "Bag Clause"
const val TEAM_PREVIEW = "Team Preview"
const val ENDLESS_BATTLE_CLAUSE = "Endless Battle Clause"
const val CANCEL_MOD = "Cancel Mod"
@@ -289,8 +289,6 @@ class ShowdownMoveset {
var canDynamax = false
var maxMoves: List<InBattleGimmickMove?>? = null
var canTerastallize: String? = null
/** This stores a list of gimmicks that have already been used this turn by a previous Pokemon, but the full turn has not been processed (ie. double battles). */
val pendingGimmickUsedThisTurn = mutableListOf<Gimmick>()
fun saveToBuffer(buffer: RegistryFriendlyByteBuf) {
buffer.writeSizedInt(IntSize.U_BYTE, moves.size)
@@ -300,35 +300,4 @@ object ShowdownInterpreter {
} ?:
MissingContext()
}
/**
* Registers a new update instruction parser.
* @param id The ID of the instruction to register.
* @param callback The callback function to create the instruction.
*/
@JvmStatic
fun registerUpdateInstructionParser(id: String, callback: (PokemonBattle, InstructionSet, BattleMessage, Iterator<BattleMessage>) -> InterpreterInstruction) {
updateInstructionParser[id] = callback
}
/**
* Registers a new split instruction parser.
* @param id The ID of the instruction to register.
* @param callback The callback function to create the instruction.
*/
@JvmStatic
fun registerSplitInstructionParser(id: String, callback: (PokemonBattle, BattleActor, InstructionSet, BattleMessage, BattleMessage, Iterator<BattleMessage>) -> InterpreterInstruction) {
splitInstructionParser[id] = callback
}
/**
* Registers a new side instruction parser.
* @param id The ID of the instruction to register.
* @param callback The callback function to create the instruction.
*/
@JvmStatic
fun registerSideInstructionParser(id: String, callback: (PokemonBattle, BattleActor, InstructionSet, BattleMessage) -> InterpreterInstruction) {
sideInstructionParser[id] = callback
}
}
}
@@ -18,7 +18,7 @@ import java.util.UUID
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.Component
open class MultiPokemonBattleActor(
class MultiPokemonBattleActor(
pokemonList: List<BattlePokemon>,
artificialDecider: BattleAI = RandomBattleAI(),
uuid: UUID = UUID.randomUUID()
@@ -24,14 +24,17 @@ import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.Component
import net.minecraft.world.phys.Vec3
open class PokemonBattleActor(
class PokemonBattleActor(
uuid: UUID,
val pokemon: BattlePokemon,
override val fleeDistance: Float,
artificialDecider: BattleAI = RandomBattleAI(),
artificialDecider: BattleAI = RandomBattleAI()
) : AIBattleActor(uuid, listOf(pokemon), artificialDecider), EntityBackedBattleActor<PokemonEntity>, FleeableBattleActor {
override val initialPos: Vec3? = pokemon.entity?.position()
override val initialPos: Vec3?
init {
initialPos = entity?.position()
}
override fun getName() = pokemon.effectedPokemon.species.translatedName
override fun nameOwned(name: String): MutableComponent = Component.literal(name)
override fun getWorldAndPosition(): Pair<ServerLevel, Vec3>? {
@@ -17,7 +17,7 @@ import com.cobblemon.mod.common.util.battleLang
import java.util.UUID
import net.minecraft.network.chat.MutableComponent
open class TrainerBattleActor(
class TrainerBattleActor(
val trainerName: String,
uuid: UUID,
pokemonList: List<BattlePokemon>,
@@ -0,0 +1,671 @@
package com.cobblemon.mod.common.battles.ai
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.ai.BattleAI
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.ai.competitive.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import kotlin.random.Random
class CompetitiveBattleAI(private val skill: Int) : BattleAI {
companion object {
private const val DEBUG_TOP_MOVES = 4
}
private val skillLevel = skill.coerceIn(0, 5)
private val aiDebugEnabled = isAiDebugEnabled()
private val battleTracker = EnhancedActiveTracker()
private val opponentPredictor = OpponentPredictor()
private val strategyAnalyzer = StrategyAnalyzer()
private lateinit var damageCalculator: DamageCalculator
private lateinit var moveEvaluator: MoveEvaluator
private lateinit var switchDecisionMaker: SwitchDecisionMaker
private lateinit var doublesDecisionMaker: DoublesDecisionMaker
private var currentBattle: PokemonBattle? = null
private var currentGamePlan: GamePlan? = null
private var battleState: BattleState? = null
private var lastMoveEvaluation: MoveEvaluator.MoveEvaluation? = null
private var isDoublesMode: Boolean = false
private var currentSlot: Int = 0
private var currentTurn: Int = -1
private val turnDecisions: MutableMap<Int, DoublesDecisionMaker.MoveTargetPair> = mutableMapOf()
private var firstSlotDecided: Boolean = false
override fun choose(
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle,
aiSide: BattleSide,
moveset: ShowdownMoveset?,
forceSwitch: Boolean
): ShowdownActionResponse {
try {
initializeComponents(battle)
updateBattleState(activeBattlePokemon, battle)
if (currentGamePlan == null) {
val npcTeam = battle.side2.actors.first().pokemonList.map { it.effectedPokemon }
currentGamePlan = strategyAnalyzer.analyzeTeamComposition(npcTeam)
}
if (battle.turn != currentTurn) {
currentTurn = battle.turn
turnDecisions.clear()
firstSlotDecided = false
}
if (forceSwitch || activeBattlePokemon.isGone()) {
return handleForcedSwitch(activeBattlePokemon, battle)
}
if (!passesSkillCheck()) {
debugLog("Skill check failed; selecting random move.")
return makeRandomMove(moveset, activeBattlePokemon)
}
updateOpponentModel(battle)
if (isDoublesMode) {
return chooseForDoubles(activeBattlePokemon, battle, moveset)
}
if (shouldAttemptSwitch() && shouldSwitch(activeBattlePokemon, battle)) {
val switchTarget = findBestSwitch(activeBattlePokemon, battle)
if (switchTarget != null) {
switchTarget.willBeSwitchedIn = true
logSwitchDecision("voluntary_switch", battle, listOf(switchTarget), switchTarget)
return SwitchActionResponse(switchTarget.uuid)
}
}
val bestMove = chooseBestMove(moveset, activeBattlePokemon, battle)
logMoveDecision("move_select", bestMove, activeBattlePokemon)
return executeMove(bestMove, activeBattlePokemon)
} catch (e: Exception) {
e.printStackTrace()
return PassActionResponse
}
}
private fun chooseForDoubles(
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle,
moveset: ShowdownMoveset?
): ShowdownActionResponse {
val state = battleState ?: return PassActionResponse
val plan = currentGamePlan ?: return PassActionResponse
val myPokemon = battleTracker.p2Active.getActiveAt(currentSlot) ?: return PassActionResponse
val partnerSlot = 1 - currentSlot
val partner = battleTracker.p2Active.getActiveAt(partnerSlot)
val opponents = battleTracker.p1Active.getAllActive()
val decision = doublesDecisionMaker.findBestMoveAndTarget(
moveset,
activeBattlePokemon,
myPokemon,
opponents,
partner,
state,
plan
)
if (decision == null) {
debugLog("Doubles: no evaluated option; selecting random move.")
return makeRandomMove(moveset, activeBattlePokemon)
}
turnDecisions[currentSlot] = decision
firstSlotDecided = true
debugLog(
"Doubles: chose move=${decision.move.id} target=${decision.targetPNX ?: "auto"} " +
"score=${"%.2f".format(decision.evaluation.totalScore)} " +
"expectedDamage=${"%.1f".format(decision.evaluation.expectedDamage)} " +
"strategic=${"%.1f".format(decision.evaluation.strategicValue)} " +
"risk=${"%.2f".format(decision.evaluation.riskFactor)}"
)
return executeDoublesMove(decision, activeBattlePokemon)
}
private fun executeDoublesMove(
decision: DoublesDecisionMaker.MoveTargetPair,
activeBattlePokemon: ActiveBattlePokemon
): ShowdownActionResponse {
return if (decision.targetPNX != null) {
MoveActionResponse(decision.move.id, decision.targetPNX)
} else {
MoveActionResponse(decision.move.id)
}
}
private fun initializeComponents(battle: PokemonBattle) {
if (currentBattle != battle) {
currentBattle = battle
isDoublesMode = battle.format.battleType.pokemonPerSide > 1
damageCalculator = DamageCalculator(battle)
moveEvaluator = MoveEvaluator(damageCalculator, opponentPredictor, isDoublesMode)
switchDecisionMaker = SwitchDecisionMaker(damageCalculator, opponentPredictor, strategyAnalyzer)
doublesDecisionMaker = DoublesDecisionMaker(moveEvaluator, damageCalculator)
}
}
private fun updateBattleState(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle) {
val request = activeBattlePokemon.actor.request ?: return
// Determine current slot from PNX (e.g., "p2a" = slot 0, "p2b" = slot 1)
val pnx = activeBattlePokemon.getPNX()
currentSlot = if (pnx.length >= 3) pnx[2] - 'a' else 0
if (isDoublesMode) {
battle.side1.activePokemon.forEachIndexed { index, active ->
active.battlePokemon?.let { pokemon ->
battleTracker.updatePokemonSlot(battleTracker.p1Active, index, pokemon)
}
}
battle.side2.activePokemon.forEachIndexed { index, active ->
active.battlePokemon?.let { pokemon ->
battleTracker.updatePokemonSlot(battleTracker.p2Active, index, pokemon)
}
}
battleTracker.p1Active.activePokemon =
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
battleTracker.p2Active.activePokemon =
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
} else {
battle.side1.activePokemon.firstOrNull()?.battlePokemon?.let { playerPokemon ->
battleTracker.updatePokemon(battleTracker.p1Active.activePokemon, playerPokemon)
battleTracker.updateParty(battleTracker.p1Active, battleTracker.p1Active.activePokemon)
}
activeBattlePokemon.battlePokemon?.let { aiPokemon ->
battleTracker.updatePokemon(battleTracker.p2Active.activePokemon, aiPokemon)
battleTracker.updateParty(battleTracker.p2Active, battleTracker.p2Active.activePokemon)
}
}
updateRevealedMoves(battle)
battleState = createBattleState(battle, activeBattlePokemon)
}
private fun createBattleState(battle: PokemonBattle, activeBattlePokemon: ActiveBattlePokemon): BattleState? {
val p1Actor = battle.side1.actors.firstOrNull() ?: return null
val p2Actor = battle.side2.actors.firstOrNull() ?: return null
val playerActiveTracker = if (isDoublesMode) {
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
} else {
battleTracker.p1Active.activePokemon
}
val npcActiveTracker = if (isDoublesMode) {
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
} else {
battleTracker.p2Active.activePokemon
}
val playerActive = playerActiveTracker.pokemon ?: return null
val npcActive = npcActiveTracker.pokemon ?: return null
val playerActiveSlots = battleTracker.p1Active.getAllActive()
.mapNotNull { it.pokemon }
.ifEmpty { listOf(playerActive) }
val npcActiveSlots = battleTracker.p2Active.getAllActive()
.mapNotNull { it.pokemon }
.ifEmpty { listOf(npcActive) }
val partnerSlot = if (isDoublesMode) {
val mySlot = currentSlot
1 - mySlot
} else {
null
}
return BattleState(
turn = battle.turn,
playerTeam = p1Actor.pokemonList.map { it.effectedPokemon },
npcTeam = p2Actor.pokemonList.map { it.effectedPokemon },
playerActive = playerActive,
npcActive = npcActive,
weather = getWeather(battle),
terrain = getTerrain(battle),
playerHazards = getHazards(battle.side1),
npcHazards = getHazards(battle.side2),
playerBoosts = battleTracker.p1Active.activePokemon.boosts,
npcBoosts = battleTracker.p2Active.activePokemon.boosts,
battleContext = battle.contextManager,
isDoubles = isDoublesMode,
playerActiveSlots = playerActiveSlots,
npcActiveSlots = npcActiveSlots,
mySlotIndex = currentSlot,
partnerSlotIndex = partnerSlot
)
}
private fun handleForcedSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): ShowdownActionResponse {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
if (availableSwitches.isEmpty()) {
return ForfeitActionResponse()
}
val state = battleState
val plan = currentGamePlan
val bestSwitch = if (state != null && plan != null) {
switchDecisionMaker.findBestSwitch(
availableSwitches,
battleTracker.p1Active.activePokemon,
state,
plan
)
} else {
null
}
val switchTarget = bestSwitch ?: availableSwitches.first()
switchTarget.willBeSwitchedIn = true
logSwitchDecision("forced_switch", battle, availableSwitches, switchTarget)
return SwitchActionResponse(switchTarget.uuid)
}
private fun shouldSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): Boolean {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
if (availableSwitches.isEmpty()) return false
val state = battleState ?: return false
val plan = currentGamePlan ?: return false
return switchDecisionMaker.shouldVoluntarilySwitch(
battleTracker.p2Active.activePokemon,
battleTracker.p1Active.activePokemon,
state,
plan,
availableSwitches
)
}
private fun findBestSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): BattlePokemon? {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
val state = battleState ?: return null
val plan = currentGamePlan ?: return null
return switchDecisionMaker.findBestSwitch(
availableSwitches,
battleTracker.p1Active.activePokemon,
state,
plan
)
}
private fun chooseBestMove(
moveset: ShowdownMoveset?,
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle
): InBattleMove? {
if (moveset == null) return null
val usableMoves = moveset.moves.filter { it.canBeUsed() }
if (usableMoves.isEmpty()) return null
val state = battleState ?: return null
val plan = currentGamePlan ?: return null
val evaluations = usableMoves.map { move ->
moveEvaluator.evaluateMove(
move,
battleTracker.p2Active.activePokemon,
battleTracker.p1Active.activePokemon,
state,
plan
)
}
val bestEvaluation = evaluations.maxByOrNull { it.totalScore }
logMoveRanking(evaluations)
lastMoveEvaluation = bestEvaluation
return bestEvaluation?.move
}
private fun executeMove(move: InBattleMove?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
if (move == null) {
return makeRandomMove(null, activeBattlePokemon)
}
val target = if (move.mustBeUsed()) {
null
} else {
move.target.targetList(activeBattlePokemon)
}
return if (target == null) {
MoveActionResponse(move.id)
} else {
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull()
?: target.random()
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
}
}
private fun makeRandomMove(moveset: ShowdownMoveset?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
if (moveset == null) return PassActionResponse
val usableMoves = moveset.moves.filter { it.canBeUsed() }
val move = usableMoves.randomOrNull() ?: return PassActionResponse
val target = if (move.mustBeUsed()) null else move.target.targetList(activeBattlePokemon)
return if (target == null) {
MoveActionResponse(move.id)
} else {
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull()
?: target.random()
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
}
}
private fun updateOpponentModel(battle: PokemonBattle) {
val situation = categorizeSituation(battleState ?: return)
val opponentActives = if (isDoublesMode) {
battleTracker.p1Active.getAllActive()
} else {
listOf(battleTracker.p1Active.activePokemon)
}
for (opponentActive in opponentActives) {
for (move in opponentActive.revealedMoves) {
opponentPredictor.updateMoveHistory(
opponentActive.species ?: "unknown",
move,
situation.name.lowercase()
)
}
}
}
override fun onHealthChange(packet: BattleHealthChangePacket) {
try {
val battle = currentBattle ?: return
val pnx = packet.pnx
val activePokemon = battle.activePokemon.find { it.getPNX() == pnx } ?: return
val newHealth = packet.newHealth.toInt()
val isPlayerPokemon = activePokemon.actor == battle.side1.actors.firstOrNull()
val teamTracker = if (isPlayerPokemon) {
battleTracker.p1Active
} else {
battleTracker.p2Active
}
val tracker = battleTracker.getTrackedPokemonByPNX(pnx, teamTracker) ?: teamTracker.activePokemon
val oldHealth = tracker.currentHp
val hpChange = newHealth - oldHealth
tracker.previousHp = oldHealth
tracker.currentHp = newHealth
tracker.lastHpChange = hpChange
tracker.currentHpPercent = if (tracker.maxHp > 0) newHealth.toDouble() / tracker.maxHp else 0.0
updateMomentum(hpChange, isPlayerPokemon)
detectCriticalThresholds(tracker, oldHealth, newHealth, isPlayerPokemon)
} catch (e: Exception) {
}
}
private fun updateMomentum(hpChange: Int, isPlayerPokemon: Boolean) {
val momentumChange = when {
hpChange > 0 -> -5.0 // Opponent healing shifts momentum toward them
hpChange < 0 -> kotlin.math.abs(hpChange) / 10.0 // Damage shifts momentum
else -> 0.0
}
if (isPlayerPokemon) {
// Player took damage = AI momentum increases
battleTracker.p2Active.momentum += momentumChange
battleTracker.p1Active.momentum -= momentumChange
} else {
// AI took damage = Player momentum increases
battleTracker.p2Active.momentum -= momentumChange
battleTracker.p1Active.momentum += momentumChange
}
// Clamp momentum to -100 to 100
battleTracker.p1Active.momentum = battleTracker.p1Active.momentum.coerceIn(-100.0, 100.0)
battleTracker.p2Active.momentum = battleTracker.p2Active.momentum.coerceIn(-100.0, 100.0)
}
private fun detectCriticalThresholds(
tracker: TrackedPokemon,
oldHealth: Int,
newHealth: Int,
isPlayerPokemon: Boolean
) {
if (newHealth <= 0 && oldHealth > 0) {
if (isPlayerPokemon) {
battleTracker.p2Active.momentum += 20.0
} else {
battleTracker.p1Active.momentum += 20.0
}
}
}
private fun categorizeSituation(battleState: BattleState): BattleSituation {
val myHP = battleState.npcActive.currentHealth.toDouble() / battleState.npcActive.maxHealth
val oppHP = battleState.playerActive.currentHealth.toDouble() / battleState.playerActive.maxHealth
return when {
myHP < 0.25 -> BattleSituation.DESPERATE
oppHP < 0.25 -> BattleSituation.WINNING
myHP > 0.8 && oppHP > 0.8 -> BattleSituation.EARLY_GAME
battleState.npcTeam.count { it.currentHealth > 0 } <= 2 -> BattleSituation.ENDGAME
battleState.playerBoosts.values.any { it >= 2 } -> BattleSituation.OPPONENT_SETUP
battleState.npcBoosts.values.any { it >= 2 } -> BattleSituation.MY_SETUP
battleState.weather != null -> BattleSituation.WEATHER_CONTROL
else -> BattleSituation.NEUTRAL
}
}
private fun getAvailableSwitches(activeBattlePokemon: ActiveBattlePokemon): List<BattlePokemon> {
val currentUuid = activeBattlePokemon.battlePokemon?.uuid
return activeBattlePokemon.actor.pokemonList.filter {
it.health > 0 &&
it.uuid != currentUuid &&
try { it.canBeSentOut() } catch (e: Exception) { true }
}
}
private fun passesSkillCheck(): Boolean {
if (skillLevel == 5) return true
if (skillLevel == 0) return false
val baseChance = skillLevel * 20
val turnBonus = if ((battleState?.turn ?: 0) > 10) 10 else 0
return Random.nextInt(100) < (baseChance + turnBonus)
}
private fun shouldAttemptSwitch(): Boolean {
return when (skillLevel) {
in 0..2 -> false
3 -> Random.nextDouble() < 0.25
4 -> Random.nextDouble() < 0.70
5 -> true
else -> false
}
}
private fun getWeather(battle: PokemonBattle): String? {
return try {
battle.contextManager.get(BattleContext.Type.WEATHER)
?.firstOrNull()?.id
} catch (e: Exception) {
null
}
}
private fun getTerrain(battle: PokemonBattle): String? {
return try {
battle.contextManager.get(BattleContext.Type.TERRAIN)
?.firstOrNull()?.id
} catch (e: Exception) {
null
}
}
private fun getHazards(side: BattleSide): List<String> {
return try {
side.contextManager.get(BattleContext.Type.HAZARD)
?.map { it.id } ?: emptyList()
} catch (e: Exception) {
emptyList()
}
}
private fun updateRevealedMoves(battle: PokemonBattle) {
updateTeamRevealedMoves(battle, battleTracker.p1Active)
updateTeamRevealedMoves(battle, battleTracker.p2Active)
}
private fun updateTeamRevealedMoves(battle: PokemonBattle, team: TeamTracker) {
val active = team.getAllActive().ifEmpty { listOf(team.activePokemon) }
for (tracked in active) {
val pokemon = tracked.pokemon ?: continue
val majorMessage = battle.majorBattleActions[pokemon.uuid]
val minorMessage = battle.minorBattleActions[pokemon.uuid]
val candidates = listOfNotNull(majorMessage, minorMessage)
for (message in candidates) {
val id = message.id.lowercase()
if (id != "move" && id != "-singlemove" && id != "singlemove") continue
val moveId = message.effectAt(1)?.id?.lowercase() ?: continue
tracked.revealedMoves.add(moveId)
}
}
}
private fun logMoveDecision(
reason: String,
move: InBattleMove?,
activeBattlePokemon: ActiveBattlePokemon
) {
if (!aiDebugEnabled || move == null) return
val eval = lastMoveEvaluation
val self = activeBattlePokemon.battlePokemon?.effectedPokemon?.species?.name ?: "unknown"
val opp = battleTracker.p1Active.activePokemon.species ?: "unknown"
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
if (eval == null) {
debugLog("Decision=$reason self=$self opp=$opp move=${move.id} plan=$plan")
return
}
debugLog(
"Decision=$reason self=$self opp=$opp move=${move.id} plan=$plan " +
"score=${"%.2f".format(eval.totalScore)} " +
"expectedDamage=${"%.1f".format(eval.expectedDamage)} " +
"strategic=${"%.1f".format(eval.strategicValue)} " +
"risk=${"%.2f".format(eval.riskFactor)}"
)
}
private fun logMoveRanking(evaluations: List<MoveEvaluator.MoveEvaluation>) {
if (!aiDebugEnabled || evaluations.isEmpty()) return
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
val situation = battleState?.let { categorizeSituation(it).name } ?: "unknown"
val top = evaluations.sortedByDescending { it.totalScore }.take(DEBUG_TOP_MOVES)
val state = battleState
val planData = currentGamePlan
val attacker = battleTracker.p2Active.activePokemon
val defender = battleTracker.p1Active.activePokemon
val details = top.joinToString(", ") { eval ->
val debug = if (state != null && planData != null) {
moveEvaluator.buildDebugDetails(eval.move, attacker, defender, state, planData)
} else {
null
}
val flags = if (debug != null) {
listOf(
"stab=${debug.hasStab}",
"eff=${"%.2f".format(debug.effectiveness)}",
"acc=${"%.2f".format(debug.accuracy)}",
"prio=${debug.priority}",
"status=${debug.isStatusMove}",
"setup=${debug.isSetup}",
"hazardSet=${debug.setsHazards}",
"hazardClear=${debug.removesHazards}",
"weather=${debug.setsWeather}",
"recovery=${debug.isRecovery}",
"pivot=${debug.isPivot}",
"risky=${debug.isRisky}"
).joinToString(" ")
} else {
"n/a"
}
"${eval.move.id}:" +
"score=${"%.2f".format(eval.totalScore)} " +
"raw=${"%.1f".format(eval.rawDamage)} " +
"dmg=${"%.1f".format(eval.expectedDamage)} " +
"strat=${"%.1f".format(eval.strategicValue)} " +
"align=${"%.1f".format(eval.gamePlanAlignment)} " +
"turn=${"%.1f".format(eval.turnAdvantage)} " +
"risk=${"%.2f".format(eval.riskFactor)} " +
flags
}
debugLog("MoveRank plan=$plan situation=$situation top=[$details]")
}
private fun logSwitchDecision(
reason: String,
battle: PokemonBattle,
availableSwitches: List<BattlePokemon>,
chosen: BattlePokemon?
) {
if (!aiDebugEnabled) return
val state = battleState
val plan = currentGamePlan
val opponent = battleTracker.p1Active.activePokemon
val ranked = if (state != null && plan != null) {
availableSwitches.map { candidate ->
val score = switchDecisionMaker.evaluateSwitchOption(
candidate.effectedPokemon,
opponent,
state,
plan
)
candidate to score
}.sortedByDescending { it.second }
.take(3)
.joinToString(", ") { (candidate, score) ->
"${candidate.effectedPokemon.species.name}:${"%.1f".format(score)}"
}
} else {
"n/a"
}
val chosenName = chosen?.effectedPokemon?.species?.name ?: "none"
debugLog("Decision=$reason chosen=$chosenName options=[$ranked]")
}
private fun debugLog(message: String) {
if (aiDebugEnabled) {
Cobblemon.LOGGER.info("[CompetitiveBattleAI] $message")
}
}
private fun isAiDebugEnabled(): Boolean {
return true
}
}
@@ -48,7 +48,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
private val hpWeightConsideration = 0.25 // how much HP difference is a consideration for switchins
private val hpFractionCoefficient = 0.4 // how much HP differences should be taken into account for switch ins
private val boostWeightCoefficient = 1 // the amount of boosts considered a baseline to be removed
private val switchOutMatchupThreshold = -2 // todo change this to get it feeling just right (-7 never switches)
private val switchOutMatchupThreshold = 0 // todo change this to get it feeling just right (-7 never switches)
private val selfKoMoveMatchupThreshold = 0.3
private val trickRoomThreshold = 85
private val recoveryMoveThreshold = 0.50
@@ -218,14 +218,19 @@ class StrongBattleAI(skill: Int) : BattleAI {
.filter { it.second.canBeSentOut() }
if (forceSwitch || activeBattlePokemon.isGone()) {
// When forced to switch, pick the Pokemon with the best matchup estimation
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
?: return PassActionResponse
val bestSwitch = availableSwitches.firstOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) == bestSwitchScore }
?: return PassActionResponse
if (battle.turn == 1) {
val switchTo = activeBattlePokemon.actor.pokemonList.filter { it.canBeSentOut() }.randomOrNull()
?: return DefaultActionResponse()
switchTo.willBeSwitchedIn = true
return SwitchActionResponse(switchTo.uuid)
}
else {
val bestEstimation = availableSwitches.maxByOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
?: return PassActionResponse
bestSwitch.second.willBeSwitchedIn = true
return SwitchActionResponse(bestSwitch.second.uuid)
bestEstimation.second.willBeSwitchedIn = true
return SwitchActionResponse(bestEstimation.second.uuid)
}
}
if (moveset == null) {
return PassActionResponse
@@ -253,9 +258,9 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (!checkSkillLevel()){
val move = availableMoves
.filter { it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
.filter { it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
return chooseMove(move.first, activeBattlePokemon)
}
@@ -264,11 +269,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (checkSwitchOutSkill() && shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
considerSwitching(activeBattlePokemon, activeTrackerPokemon, opponents, availableMoves, availableSwitches, battle, aiSide)
}
// If HP is below 30% and not switching out, always use the most damaging move
if (activeTrackerPokemon.currentHpPercent < 0.3 && !shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
findAndUseMostDamagingMove(activeBattlePokemon, activeTrackerPokemon, opponents, availableMoves, moveset, battle, aiSide)?.let { return it }
}
activeTrackerPokemon.firstTurn = false
// Decision-making based on move availability and switch-out condition
if (!shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
@@ -289,6 +290,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
return chooseMove(fakeOut.first, activeBattlePokemon, targets.firstOrNull { validTarget != null && validTarget.id == it.battlePokemon!!.uuid})
}
activeTrackerPokemon.firstTurn = false
// Explosion/Self destruct
availableMoves.firstOrNull {
@@ -306,7 +309,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Deal with non-weather related field changing effects
for (move in availableMoves) {
// Tailwind
if (move.first.id == "tailwind" && move.first.id != activeTracker.alliedSide.tailwindCondition &&
availableSwitches.size > 2) {
@@ -315,21 +318,21 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Trick room
if (move.first.id == "trickroom" && move.first.id != activeTracker.currentRoom
&& availableSwitches.count { statEstimationActive(it.first, Stats.SPEED) <= trickRoomThreshold } >= 2) {
&& availableSwitches.count { statEstimationActive(it.first, Stats.SPEED) <= trickRoomThreshold } >= 2) {
return chooseMove(move.first, activeBattlePokemon)
}
// todo find a way to get list of active screens
// Aurora veil
if (move.first.id == "auroraveil" && move.first.id != activeTracker.alliedSide.screenCondition
&& activeTracker.currentWeather in listOf("hail", "snow")) {
&& activeTracker.currentWeather in listOf("hail", "snow")) {
return chooseMove(move.first, activeBattlePokemon)
}
// todo find a way to get list of active screens
// Light Screen
if (move.first.id == "lightscreen" && move.first.id != activeTracker.alliedSide.screenCondition
&& opponents.any { (it.species?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: 0) > (it.species?.baseStats?.get(Stats.ATTACK) ?: 0) }
&& opponents.any { (it.species?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: 0) > (it.species?.baseStats?.get(Stats.ATTACK) ?: 0) }
&& availableSwitches.size > 1) {
return chooseMove(move.first, activeBattlePokemon)
}
@@ -347,13 +350,13 @@ class StrongBattleAI(skill: Int) : BattleAI {
for (move in availableMoves) {
// Setup
if (nOppRemainingMons >= 3 && move.first.id in AIUtility.entryHazards
&& !activeTracker.opponentSide.sideHazards.contains(move.first.id)) {
&& !activeTracker.opponentSide.sideHazards.contains(move.first.id)) {
return chooseMove(move.first, activeBattlePokemon)
}
// Removal
if (nRemainingMons >= 2 && move.first.id in AIUtility.antiHazardsMoves
&& activeTracker.alliedSide.sideHazards.isNotEmpty()) {
&& activeTracker.alliedSide.sideHazards.isNotEmpty()) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -363,10 +366,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Court Change
for (move in availableMoves) {
if (move.first.id == "courtchange"
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -374,7 +377,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Strength Sap
for (move in availableMoves) {
if (move.first.id == "strengthsap" && activeTrackerPokemon.currentHpPercent < 0.5
&& activeTrackerPokemon.species!!.baseStats.getOrDefault(Stats.ATTACK, 0) > 80) {
&& activeTrackerPokemon.species!!.baseStats.getOrDefault(Stats.ATTACK, 0) > 80) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -382,10 +385,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Belly Drum
for (move in availableMoves) {
if (move.first.id == "bellydrum"
&& (activeTrackerPokemon.currentHpPercent > 0.6
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|| activeTrackerPokemon.currentHpPercent > 0.8)
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
&& (activeTrackerPokemon.currentHpPercent > 0.6
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|| activeTrackerPokemon.currentHpPercent > 0.8)
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -395,8 +398,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
for (move in availableMoves) {
AIUtility.weatherSetupMoves[move.first.id]?.let { requiredWeather ->
if (activeTracker.currentWeather != requiredWeather.lowercase() &&
!(activeTracker.currentWeather == "primordialsea" && requiredWeather == "raindance") &&
!(activeTracker.currentWeather == "desolateland" && requiredWeather == "sunnyday")) {
!(activeTracker.currentWeather == "primordialsea" && requiredWeather == "raindance") &&
!(activeTracker.currentWeather == "desolateland" && requiredWeather == "sunnyday")) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -422,8 +425,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Stall out side conditions
if ((activeTracker.opponentSide.screenCondition != null || activeTracker.opponentSide.tailwindCondition != null) &&
(activeTracker.alliedSide.screenCondition == null || activeTracker.alliedSide.tailwindCondition == null) ||
opponents.any { it.currentStatus == null } &&
activeTrackerPokemon.protectCount == 0 && opponents.none { it.currentAbility?.name == "unseenfist"}) {
opponents.any { it.currentStatus == null } &&
activeTrackerPokemon.protectCount == 0 && opponents.none { it.currentAbility?.name == "unseenfist"}) {
activeTrackerPokemon.protectCount = 3
return chooseMove(move.first, activeBattlePokemon)
}
@@ -451,10 +454,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
activeTrackerPokemon.firstTurn = false
val move = availableMoves
.filter { it.first.canBeUsed() }
.filter { it.first.mustBeUsed() || it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
.filter { it.first.canBeUsed() }
.filter { it.first.mustBeUsed() || it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
return chooseMove(move.first, activeBattlePokemon)
}
@@ -511,10 +514,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
return false
val activeTrackerPokemon = activeTracker.alliedSide.activePokemon.first { it.id == activeBattlePokemon.battlePokemon!!.uuid }
// Don't switch out if we just switched in
if (activeTrackerPokemon.firstTurn)
return false
val actorTracker = activeTracker.alliedSide.actors.first { activeTrackerPokemon in it.activePokemon }
val availableSwitches = actorTracker.party.filter { it.currentHp!! > 0 }
val currentAbility = activeTrackerPokemon.pokemon!!.ability
@@ -522,19 +521,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
val opponentActiveTracker = activeTracker.opponentSide.activePokemon
val opponentSpeedEstimations = opponentActiveTracker.map { statEstimationActive(it, Stats.SPEED) }
// Only switch if a switch-in is significantly better than the current Pokémon
val currentScore = estimateMatchup(activeBattlePokemon, side, battle)
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, side, battle, it) } ?: currentScore
val improvementThreshold = currentScore.absoluteValue * 0.5 + 3
if (bestSwitchScore <= currentScore + improvementThreshold) {
return false
}
if(bestSwitchScore < 1) {
return false
}
// todo add some way to keep track of the player's boosting to see if it needs to switch out to something that can stop it
// if slower speed stat than the opposing pokemon and HP is less than 20% don't switch out
@@ -562,8 +548,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (availableSwitches.isEmpty() || availableSwitches.none { estimateMatchup(activeBattlePokemon, side, battle, it) > 0 })
return false
// ...and a 'good' reason to switch out
if (opponentActiveTracker.any { it.boosts.getOrDefault(Stats.ACCURACY, 0) <= accuracySwitchThreshold } ||
opponentActiveTracker.any { it.boosts.getOrDefault(Stats.DEFENCE, 0) <= -3 } ||
@@ -990,4 +974,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
return MoveActionResponse(move.id, chosenTarget.getPNX())
}
}
}
}
@@ -0,0 +1,20 @@
package com.cobblemon.mod.common.battles.ai.competitive
object AiBattleUtils {
fun normalizeWeatherId(weather: String?): String? {
val id = weather?.lowercase() ?: return null
return when (id) {
"sun", "sunny", "sunnyday", "harshsunlight" -> "sunnyday"
"rain", "raindance", "heavyrain" -> "raindance"
"sand", "sandstorm" -> "sandstorm"
"hail" -> "hail"
"snow", "snowscape" -> "snow"
else -> id
}
}
fun hasStab(moveTypeName: String, pokemonTypes: List<String>): Boolean {
val moveType = moveTypeName.lowercase()
return pokemonTypes.any { it.lowercase() == moveType }
}
}
@@ -0,0 +1,236 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.interpreter.ContextManager
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.Pokemon
data class BattleState(
val turn: Int,
val playerTeam: List<Pokemon>,
val npcTeam: List<Pokemon>,
val playerActive: Pokemon,
val npcActive: Pokemon,
val weather: String?,
val terrain: String?,
val playerHazards: List<String>,
val npcHazards: List<String>,
val playerBoosts: Map<Stat, Int>,
val npcBoosts: Map<Stat, Int>,
val battleContext: ContextManager,
val gamePhase: GamePhase = determineGamePhase(turn, npcTeam, playerTeam),
val isDoubles: Boolean = false,
val playerActiveSlots: List<Pokemon> = listOf(playerActive),
val npcActiveSlots: List<Pokemon> = listOf(npcActive),
val mySlotIndex: Int = 0,
val partnerSlotIndex: Int? = null
) {
companion object {
private fun determineGamePhase(turn: Int, npcTeam: List<Pokemon>, playerTeam: List<Pokemon>): GamePhase {
val npcAlive = npcTeam.count { it.currentHealth > 0 }
val playerAlive = playerTeam.count { it.currentHealth > 0 }
return when {
turn <= 3 -> GamePhase.EARLY_GAME
npcAlive <= 2 || playerAlive <= 2 -> GamePhase.ENDGAME
turn >= 15 -> GamePhase.LATE_GAME
else -> GamePhase.MID_GAME
}
}
}
}
enum class GamePhase {
EARLY_GAME,
MID_GAME,
LATE_GAME,
ENDGAME
}
data class TrackedPokemon(
var pokemon: Pokemon? = null,
var originalPokemon: Pokemon? = null,
var species: String? = null,
var currentHp: Int = 0,
var currentHpPercent: Double = 0.0,
var maxHp: Int = 0,
var previousHp: Int = 0,
var lastHpChange: Int = 0,
var boosts: Map<Stat, Int> = mapOf(),
var atkBoost: Int = 0,
var defBoost: Int = 0,
var spaBoost: Int = 0,
var spdBoost: Int = 0,
var speBoost: Int = 0,
var currentAbility: String? = null,
var currentTypes: MutableList<String>? = null,
var stats: Map<Stat, Int> = mapOf(),
var moves: List<Move> = listOf(),
var revealedMoves: MutableSet<String> = mutableSetOf(),
var cachedAttack: Double = 0.0,
var cachedDefense: Double = 0.0,
var cachedSpAttack: Double = 0.0,
var cachedSpDefense: Double = 0.0,
var cachedSpeed: Double = 0.0,
var sideConditions: Map<String, Any> = mapOf(),
var firstTurn: Int = 0,
var protectCount: Int = 0,
var lastMoveDamage: Int = 0,
var statusTurns: Int = 0,
var switchInTurn: Int = 0,
var predictedRole: PokemonRole = PokemonRole.UNKNOWN,
var threatLevel: Int = 0
)
enum class PokemonRole {
UNKNOWN,
SWEEPER,
SETUP_SWEEPER,
WALLBREAKER,
REVENGE_KILLER,
TANK,
SUPPORT,
PIVOT,
BALANCED
}
data class TeamTracker(
var party: MutableList<TrackedPokemon> = mutableListOf(),
var uuid: String? = null,
var activePokemon: TrackedPokemon = TrackedPokemon(),
var activeSlots: MutableList<TrackedPokemon> = mutableListOf(),
var nRemainingMons: Int = 0,
var hazardDamage: Int = 0,
var teamSynergy: Double = 0.0,
var momentum: Double = 0.0
) {
fun getActiveAt(slotIndex: Int): TrackedPokemon? = activeSlots.getOrNull(slotIndex)
fun getAllActive(): List<TrackedPokemon> = activeSlots.filter { it.currentHp > 0 }
fun syncLegacyActivePokemon() {
if (activeSlots.isNotEmpty()) {
activePokemon = activeSlots[0]
}
}
}
class EnhancedActiveTracker {
var p1Active: TeamTracker = TeamTracker()
var p2Active: TeamTracker = TeamTracker()
private var lastStateUpdateTurn: Int = -1
fun updatePokemonSlot(team: TeamTracker, slotIndex: Int, battlePokemon: BattlePokemon) {
while (team.activeSlots.size <= slotIndex) {
team.activeSlots.add(TrackedPokemon())
}
updatePokemon(team.activeSlots[slotIndex], battlePokemon)
team.syncLegacyActivePokemon()
}
fun getTrackedPokemonByPNX(pnx: String, team: TeamTracker): TrackedPokemon? {
if (pnx.length < 3) return null
val slotIndex = pnx[2] - 'a'
return team.getActiveAt(slotIndex)
}
fun updatePokemon(tracker: TrackedPokemon, battlePokemon: BattlePokemon) {
val pokemon = battlePokemon.effectedPokemon
tracker.pokemon = pokemon
tracker.originalPokemon = battlePokemon.originalPokemon
tracker.species = pokemon.species.name
tracker.currentHp = pokemon.currentHealth
tracker.maxHp = pokemon.maxHealth
tracker.currentHpPercent = pokemon.currentHealth.toDouble() / pokemon.maxHealth
tracker.currentTypes = pokemon.types.map { it.name.lowercase() }.toMutableList()
tracker.currentAbility = pokemon.ability.name
tracker.moves = pokemon.moveSet.getMoves()
tracker.boosts = battlePokemon.statChanges
tracker.atkBoost = battlePokemon.statChanges[Stats.ATTACK] ?: 0
tracker.defBoost = battlePokemon.statChanges[Stats.DEFENCE] ?: 0
tracker.spaBoost = battlePokemon.statChanges[Stats.SPECIAL_ATTACK] ?: 0
tracker.spdBoost = battlePokemon.statChanges[Stats.SPECIAL_DEFENCE] ?: 0
tracker.speBoost = battlePokemon.statChanges[Stats.SPEED] ?: 0
cacheStats(tracker)
}
private fun cacheStats(tracker: TrackedPokemon) {
val pokemon = tracker.pokemon ?: return
tracker.cachedAttack = calculateStat(pokemon, Stats.ATTACK)
tracker.cachedDefense = calculateStat(pokemon, Stats.DEFENCE)
tracker.cachedSpAttack = calculateStat(pokemon, Stats.SPECIAL_ATTACK)
tracker.cachedSpDefense = calculateStat(pokemon, Stats.SPECIAL_DEFENCE)
tracker.cachedSpeed = calculateStat(pokemon, Stats.SPEED)
}
private fun calculateStat(pokemon: Pokemon, stat: Stat): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return ((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
fun updateParty(team: TeamTracker, pokemon: TrackedPokemon) {
val existingIndex = team.party.indexOfFirst { it.pokemon?.uuid == pokemon.pokemon?.uuid }
if (existingIndex == -1) {
team.party.add(pokemon)
} else {
team.party[existingIndex] = pokemon
}
}
}
data class FearThreat(
val pokemon: Pokemon,
val hasEndeavor: Boolean,
val hasPriorityMove: Boolean,
val hasFocusItem: Boolean,
val threatLevel: Int
)
enum class BattleSituation {
DESPERATE,
WINNING,
EARLY_GAME,
ENDGAME,
OPPONENT_SETUP,
MY_SETUP,
NEUTRAL,
HAZARD_ADVANTAGE,
WEATHER_CONTROL
}
data class MatchupEvaluation(
val offensiveAdvantage: Double,
val defensiveAdvantage: Double,
val speedAdvantage: Double,
val overallScore: Double,
val recommendation: MatchupRecommendation
)
enum class MatchupRecommendation {
STAY_IN_AND_ATTACK,
STAY_IN_AND_SETUP,
STAY_IN_AND_STATUS,
CONSIDER_SWITCHING,
SWITCH_OUT,
PIVOT_OUT
}
@@ -0,0 +1,483 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.core.registries.BuiltInRegistries
import java.util.*
import kotlin.math.max
class DamageCalculator(
private val battle: PokemonBattle?,
private val consumedItems: MutableMap<UUID, MutableList<String>> = mutableMapOf()
) {
fun calculateDamage(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
weather: String? = null,
criticalHit: Boolean = false,
isSpreadMove: Boolean = false,
targetCount: Int = 1
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var power = moveData.power.toDouble()
if (power <= 0) return 0.0
if (isSpreadMove && targetCount > 1) {
power *= 0.75
}
val immunityMultiplier = checkAllImmunities(moveData, attacker, defender)
if (immunityMultiplier == 0.0) return 0.0
val level = attacker.pokemon?.level ?: 50
val attackStat = getAttackStat(moveData, attacker)
val defenseStat = getDefenseStat(moveData, defender)
var damage = ((2.0 * level / 5.0 + 2.0) * power * (attackStat / defenseStat)) / 50.0 + 2.0
damage *= calculateCriticalMultiplier(criticalHit)
damage *= calculateSTABMultiplier(attacker, moveData)
damage *= TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, defender.currentTypes ?: listOf())
damage *= calculateWeatherMultiplier(moveData, AiBattleUtils.normalizeWeatherId(weather))
damage *= calculateAbilityMultiplier(attacker, defender, moveData)
damage *= calculateItemMultiplier(attacker, defender, moveData)
damage *= calculateBurnMultiplier(attacker, moveData)
damage *= calculateScreenMultiplier(attacker, defender, moveData)
damage *= calculateRandomFactor()
damage = calculateMultiHitDamage(moveData, damage)
val finalDamage = max(1.0, damage)
if (finalDamage > 0) {
trackConsumedItems(defender, moveData)
}
return finalDamage
}
private fun getAttackStat(moveData: MoveTemplate, attacker: TrackedPokemon): Double {
val pokemon = attacker.pokemon ?: return 100.0
val baseStat = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
getEffectiveStat(pokemon, Stats.ATTACK)
} else {
getEffectiveStat(pokemon, Stats.SPECIAL_ATTACK)
}
val boost = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
attacker.atkBoost
} else {
attacker.spaBoost
}
return applyStatBoosts(baseStat, boost)
}
private fun getDefenseStat(moveData: MoveTemplate, defender: TrackedPokemon): Double {
val pokemon = defender.pokemon ?: return 100.0
val baseStat = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
getEffectiveStat(pokemon, Stats.DEFENCE)
} else {
getEffectiveStat(pokemon, Stats.SPECIAL_DEFENCE)
}
val boost = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
defender.defBoost
} else {
defender.spdBoost
}
return applyStatBoosts(baseStat, boost)
}
private fun applyStatBoosts(baseStat: Double, boost: Int): Double {
val clampedBoost = boost.coerceIn(-6, 6)
return if (clampedBoost >= 0) {
baseStat * (2.0 + clampedBoost) / 2.0
} else {
baseStat * 2.0 / (2.0 - clampedBoost)
}
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stat): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
private fun checkAllImmunities(
moveData: MoveTemplate,
attacker: TrackedPokemon,
defender: TrackedPokemon
): Double {
if (checkTypeImmunity(moveData, defender)) return 0.0
if (checkAbilityImmunity(moveData, attacker, defender)) return 0.0
if (checkItemImmunity(moveData, defender)) return 0.0
if (checkMoveSpecificImmunity(moveData, defender)) return 0.0
return 1.0
}
private fun checkTypeImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
.map { it.lowercase() }
return when (moveData.elementalType) {
ElementalTypes.GROUND -> "flying" in defenderTypes
ElementalTypes.ELECTRIC -> "ground" in defenderTypes
ElementalTypes.NORMAL, ElementalTypes.FIGHTING -> "ghost" in defenderTypes
ElementalTypes.PSYCHIC -> "dark" in defenderTypes
ElementalTypes.POISON -> "steel" in defenderTypes
ElementalTypes.DRAGON -> "fairy" in defenderTypes
ElementalTypes.GHOST -> "normal" in defenderTypes
else -> false
}
}
private fun checkAbilityImmunity(
moveData: MoveTemplate,
attacker: TrackedPokemon,
defender: TrackedPokemon
): Boolean {
val defenderAbility = defender.currentAbility?.lowercase()
?: defender.pokemon?.ability?.name?.lowercase()
?: return false
val attackerAbility = attacker.currentAbility?.lowercase()
?: attacker.pokemon?.ability?.name?.lowercase()
if (attackerAbility in listOf("moldbreaker", "turboblaze", "teravolt")) {
if (defenderAbility != "wonderguard") return false
}
return when (defenderAbility) {
"levitate" -> moveData.elementalType == ElementalTypes.GROUND
"voltabsorb", "lightningrod", "motordrive" -> moveData.elementalType == ElementalTypes.ELECTRIC
"flashfire", "wellbakedbody" -> moveData.elementalType == ElementalTypes.FIRE
"waterabsorb", "stormdrain", "dryskin" -> moveData.elementalType == ElementalTypes.WATER
"sapsipper" -> moveData.elementalType == ElementalTypes.GRASS
"eartheater" -> moveData.elementalType == ElementalTypes.GROUND
"soundproof" -> isSoundMove(moveData)
"bulletproof" -> isBallOrBombMove(moveData)
"queenlymajesty", "dazzling" -> moveData.priority > 0
"wonderguard" -> {
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
effectiveness <= 1.0
}
"goodasgold" -> moveData.power.toInt() == 0
else -> false
}
}
private fun checkItemImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderItem = defender.pokemon?.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
} ?: return false
return when (defenderItem) {
"cobblemon:air_balloon" -> {
val notConsumed = consumedItems[defender.pokemon?.uuid]?.contains("airballoon") != true
notConsumed && moveData.elementalType == ElementalTypes.GROUND
}
"cobblemon:safety_goggles" -> isPowderMove(moveData) || isWeatherDamageMove(moveData)
else -> false
}
}
private fun checkMoveSpecificImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
.map { it.lowercase() }
val defenderAbility = defender.currentAbility?.lowercase()
?: defender.pokemon?.ability?.name?.lowercase()
return when (moveData.name.lowercase()) {
"thunderwave" -> {
"electric" in defenderTypes || "ground" in defenderTypes ||
defenderAbility in listOf("limber", "volt absorb", "lightningrod", "motordrive")
}
"willowisp" -> {
"fire" in defenderTypes ||
defenderAbility in listOf("waterveil", "waterbubble", "flashfire")
}
"toxic", "poisonpowder", "poisongas" -> {
"poison" in defenderTypes || "steel" in defenderTypes ||
defenderAbility in listOf("immunity", "poisonheal", "pastelveil")
}
"sleeppowder", "spore", "hypnosis", "sing" -> {
defenderAbility in listOf("insomnia", "vitalspirit", "comatose", "sweetveil")
}
else -> false
}
}
private fun calculateCriticalMultiplier(criticalHit: Boolean): Double {
return if (criticalHit) 1.5 else 1.0
}
private fun calculateSTABMultiplier(attacker: TrackedPokemon, moveData: MoveTemplate): Double {
val pokemonTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name } ?: return 1.0
return if (AiBattleUtils.hasStab(moveData.elementalType.name, pokemonTypes)) {
if (attacker.currentAbility?.lowercase() == "adaptability") 2.0 else 1.5
} else 1.0
}
private fun calculateWeatherMultiplier(moveData: MoveTemplate, weather: String?): Double {
return when (weather) {
"sunnyday" -> when {
moveData.elementalType == ElementalTypes.FIRE -> 1.5
moveData.elementalType == ElementalTypes.WATER && moveData.name != "hydrosteam" -> 0.5
moveData.name == "hydrosteam" -> 1.5
else -> 1.0
}
"raindance" -> when {
moveData.elementalType == ElementalTypes.WATER -> 1.5
moveData.elementalType == ElementalTypes.FIRE -> 0.5
else -> 1.0
}
"sandstorm", "hail", "snow" -> if (moveData.name == "weatherball") 2.0 else 1.0
else -> 1.0
}
}
private fun calculateAbilityMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
var multiplier = 1.0
when (attacker.currentAbility?.lowercase()) {
"hugepower", "purepower" -> {
if (moveData.damageCategory == DamageCategories.PHYSICAL) multiplier *= 2.0
}
"guts" -> {
if (attacker.pokemon?.status != null && moveData.damageCategory == DamageCategories.PHYSICAL) {
multiplier *= 1.5
}
}
"overgrow" -> {
if (moveData.elementalType == ElementalTypes.GRASS && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"blaze" -> {
if (moveData.elementalType == ElementalTypes.FIRE && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"torrent" -> {
if (moveData.elementalType == ElementalTypes.WATER && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"swarm" -> {
if (moveData.elementalType == ElementalTypes.BUG && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
}
when (defender.currentAbility?.lowercase()) {
"thickfat" -> {
if (moveData.elementalType == ElementalTypes.FIRE || moveData.elementalType == ElementalTypes.ICE) {
multiplier *= 0.5
}
}
"filter", "solidrock" -> {
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
if (effectiveness > 1.0) multiplier *= 0.75
}
}
return multiplier
}
private fun calculateItemMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
var multiplier = 1.0
val attackerItem = attacker.pokemon?.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
}
when (attackerItem) {
"cobblemon:choice_band" -> {
if (moveData.damageCategory == DamageCategories.PHYSICAL) multiplier *= 1.5
}
"cobblemon:choice_specs" -> {
if (moveData.damageCategory == DamageCategories.SPECIAL) multiplier *= 1.5
}
"cobblemon:life_orb" -> multiplier *= 1.3
}
return multiplier
}
private fun calculateBurnMultiplier(attacker: TrackedPokemon, moveData: MoveTemplate): Double {
return if (attacker.pokemon?.status?.status?.showdownName == "burn" &&
moveData.damageCategory == DamageCategories.PHYSICAL &&
attacker.currentAbility != "guts") {
0.5
} else {
1.0
}
}
private fun calculateScreenMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
if (attackerAbility == "infiltrator") {
return 1.0
}
val sideConditions = defender.sideConditions.keys.map { it.lowercase() }
val hasReflect = "reflect" in sideConditions
val hasLightScreen = "lightscreen" in sideConditions || "light screen" in sideConditions
val hasAuroraVeil = "auroraveil" in sideConditions || "aurora veil" in sideConditions
val multiplier = when (moveData.damageCategory) {
DamageCategories.PHYSICAL -> {
when {
hasAuroraVeil -> 0.5
hasReflect -> 0.5
else -> 1.0
}
}
DamageCategories.SPECIAL -> {
when {
hasAuroraVeil -> 0.5
hasLightScreen -> 0.5
else -> 1.0
}
}
else -> 1.0
}
return multiplier
}
private fun calculateRandomFactor(): Double = 0.925
private fun calculateMultiHitDamage(moveData: MoveTemplate, baseDamage: Double): Double {
val moveName = moveData.name.lowercase()
return when {
moveName == "populationbomb" -> {
val accuracy = moveData.accuracy / 100.0
var expectedDamage = 0.0
var probability = 1.0
for (hit in 1..10) {
expectedDamage += baseDamage * probability
probability *= accuracy
}
expectedDamage
}
moveName in listOf("triplekick", "tripleaxel") -> {
val accuracy = moveData.accuracy / 100.0
baseDamage * 1.0 +
baseDamage * 2.0 * accuracy +
baseDamage * 3.0 * accuracy * accuracy
}
moveName in MoveData.multiHitMoves.keys -> {
val hitRange = MoveData.multiHitMoves[moveName]!!
val minHits = hitRange.keys.minOrNull() ?: 1
val maxHits = hitRange.values.maxOrNull() ?: 1
when {
minHits == 2 && maxHits == 5 -> baseDamage * 3.0
minHits == 2 && maxHits == 2 -> baseDamage * 2.0
minHits == 3 && maxHits == 3 -> baseDamage * 3.0
else -> baseDamage * ((minHits + maxHits) / 2.0)
}
}
else -> baseDamage
}
}
private fun isSoundMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"boomburst", "bugbuzz", "chatter", "clangoroussoul", "disarmingvoice",
"echoedvoice", "grasswhistle", "growl", "hypervoice", "metalsound",
"nobleroar", "overdrive", "perishsong", "relicsong", "roar", "round",
"screech", "sing", "snore", "sparklingaria", "supersonic", "uproar"
)
}
private fun isBallOrBombMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"acidspray", "aurasphere", "barrage", "bulletseed", "eggbomb",
"electroball", "energyball", "focusblast", "gyroball", "iceball",
"magnetbomb", "mistball", "mudbomb", "octazooka", "pollenpuff",
"rockblast", "searingshot", "seedbomb", "shadowball", "sludgebomb",
"weatherball", "zapcannon"
)
}
private fun isPowderMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"sleeppowder", "stunspore", "poisonpowder", "cottonspore",
"ragepowder", "spore"
)
}
private fun isWeatherDamageMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf("sandstorm", "hail")
}
private fun trackConsumedItems(defender: TrackedPokemon, moveData: MoveTemplate) {
val pokemon = defender.pokemon ?: return
val defenderItem = pokemon.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
} ?: return
if (defenderItem == "cobblemon:air_balloon") {
val alreadyConsumed = consumedItems[pokemon.uuid]?.contains("airballoon") == true
if (!alreadyConsumed) {
addConsumedItem(pokemon.uuid, "airballoon")
}
}
}
private fun addConsumedItem(pokemonUuid: UUID, item: String) {
val items = consumedItems.getOrPut(pokemonUuid) { mutableListOf() }
items.add(item)
}
}
@@ -0,0 +1,135 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.battles.ShowdownMoveset
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
class DoublesDecisionMaker(
private val moveEvaluator: MoveEvaluator,
private val damageCalculator: DamageCalculator
) {
data class MoveTargetPair(
val move: InBattleMove,
val targetPNX: String?,
val evaluation: MoveEvaluator.MoveEvaluation
)
fun findBestMoveAndTarget(
moveset: ShowdownMoveset?,
activeBattlePokemon: ActiveBattlePokemon,
myPokemon: TrackedPokemon,
opponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveTargetPair? {
if (moveset == null) return null
val usableMoves = moveset.moves.filter { it.canBeUsed() }
if (usableMoves.isEmpty()) return null
val allOptions = mutableListOf<MoveTargetPair>()
for (move in usableMoves) {
val moveData = Moves.getByName(move.id) ?: continue
val targetList = move.target.targetList(activeBattlePokemon)
if (targetList == null) {
val evaluation = moveEvaluator.evaluateMoveForDoubles(
move, myPokemon, opponents.firstOrNull() ?: TrackedPokemon(),
opponents, partner, battleState, gamePlan
)
allOptions.add(MoveTargetPair(move, null, evaluation))
} else {
val opponentTargets = targetList.filterIsInstance<ActiveBattlePokemon>()
.filter { !it.isAllied(activeBattlePokemon) }
for (target in opponentTargets) {
val targetPokemon = opponents.find {
it.pokemon?.uuid == target.battlePokemon?.uuid
} ?: continue
val evaluation = moveEvaluator.evaluateMoveForDoubles(
move, myPokemon, targetPokemon,
opponents, partner, battleState, gamePlan
)
allOptions.add(MoveTargetPair(move, target.getPNX(), evaluation))
}
}
}
return allOptions.maxByOrNull { it.evaluation.totalScore }
}
fun scoreTarget(
target: TrackedPokemon,
myPokemon: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
battleState: BattleState
): Double {
var score = 0.0
score += target.threatLevel * 20.0
val mySpeed = myPokemon.cachedSpeed
val targetSpeed = target.cachedSpeed
if (targetSpeed > mySpeed && target.currentHpPercent < 0.5) {
score += 100.0
}
if (target.boosts.values.any { it >= 2 }) {
score += 120.0
}
if (target.cachedAttack > 100 || target.cachedSpAttack > 100) {
score += 40.0
}
if (target.currentHpPercent < 0.3) {
score += 80.0
}
return score
}
fun selectBestTarget(
move: InBattleMove,
activeBattlePokemon: ActiveBattlePokemon,
myPokemon: TrackedPokemon,
opponents: List<TrackedPokemon>,
battleState: BattleState
): String? {
val targetList = move.target.targetList(activeBattlePokemon) ?: return null
val opponentTargets = targetList.filterIsInstance<ActiveBattlePokemon>()
.filter { !it.isAllied(activeBattlePokemon) }
if (opponentTargets.isEmpty()) return null
val targetScores = opponentTargets.mapNotNull { target ->
val targetPokemon = opponents.find {
it.pokemon?.uuid == target.battlePokemon?.uuid
}
if (targetPokemon != null) {
val damage = damageCalculator.calculateDamage(
move, myPokemon, targetPokemon, battleState.weather
)
val targetScore = scoreTarget(targetPokemon, myPokemon, opponents, battleState)
val totalScore = damage + targetScore
target to totalScore
} else {
null
}
}
return targetScores.maxByOrNull { it.second }?.first?.getPNX()
}
}
@@ -0,0 +1,178 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
object MoveData {
val multiHitMoves: Map<String, Map<Int, Int>> = mapOf(
"armthrust" to mapOf(2 to 5),
"barrage" to mapOf(2 to 5),
"bonerush" to mapOf(2 to 5),
"bulletseed" to mapOf(2 to 5),
"cometpunch" to mapOf(2 to 5),
"doubleslap" to mapOf(2 to 5),
"furyattack" to mapOf(2 to 5),
"furyswipes" to mapOf(2 to 5),
"iciclespear" to mapOf(2 to 5),
"pinmissile" to mapOf(2 to 5),
"rockblast" to mapOf(2 to 5),
"scaleshot" to mapOf(2 to 5),
"spikecannon" to mapOf(2 to 5),
"tailslap" to mapOf(2 to 5),
"watershuriken" to mapOf(2 to 5),
"bonemerang" to mapOf(2 to 2),
"doublehit" to mapOf(2 to 2),
"doubleironbash" to mapOf(2 to 2),
"doublekick" to mapOf(2 to 2),
"dragondarts" to mapOf(2 to 2),
"dualchop" to mapOf(2 to 2),
"dualwingbeat" to mapOf(2 to 2),
"geargrind" to mapOf(2 to 2),
"twinbeam" to mapOf(2 to 2),
"twineedle" to mapOf(2 to 2),
"surgingstrikes" to mapOf(3 to 3),
"tripledive" to mapOf(3 to 3),
"tripleaxel" to mapOf(1 to 3),
"triplekick" to mapOf(1 to 3),
"populationbomb" to mapOf(1 to 10)
)
val statusMoves: Map<MoveTemplate?, String> = mapOf(
Moves.getByName("willowisp") to Statuses.BURN.showdownName,
Moves.getByName("scald") to Statuses.BURN.showdownName,
Moves.getByName("scorchingsands") to Statuses.BURN.showdownName,
Moves.getByName("flamethrower") to Statuses.BURN.showdownName,
Moves.getByName("sacredfire") to Statuses.BURN.showdownName,
Moves.getByName("glare") to Statuses.PARALYSIS.showdownName,
Moves.getByName("nuzzle") to Statuses.PARALYSIS.showdownName,
Moves.getByName("stunspore") to Statuses.PARALYSIS.showdownName,
Moves.getByName("thunderwave") to Statuses.PARALYSIS.showdownName,
Moves.getByName("bodyslam") to Statuses.PARALYSIS.showdownName,
Moves.getByName("discharge") to Statuses.PARALYSIS.showdownName,
Moves.getByName("spark") to Statuses.PARALYSIS.showdownName,
Moves.getByName("darkvoid") to Statuses.SLEEP.showdownName,
Moves.getByName("hypnosis") to Statuses.SLEEP.showdownName,
Moves.getByName("lovelykiss") to Statuses.SLEEP.showdownName,
Moves.getByName("relicsong") to Statuses.SLEEP.showdownName,
Moves.getByName("sing") to Statuses.SLEEP.showdownName,
Moves.getByName("sleeppowder") to Statuses.SLEEP.showdownName,
Moves.getByName("spore") to Statuses.SLEEP.showdownName,
Moves.getByName("yawn") to Statuses.SLEEP.showdownName,
Moves.getByName("chatter") to "confusion",
Moves.getByName("confuseray") to "confusion",
Moves.getByName("dynamicpunch") to "confusion",
Moves.getByName("flatter") to "confusion",
Moves.getByName("supersonic") to "confusion",
Moves.getByName("swagger") to "confusion",
Moves.getByName("sweetkiss") to "confusion",
Moves.getByName("teeterdance") to "confusion",
Moves.getByName("poisongas") to Statuses.POISON.showdownName,
Moves.getByName("poisonpowder") to Statuses.POISON.showdownName,
Moves.getByName("poisonjab") to Statuses.POISON.showdownName,
Moves.getByName("sludgebomb") to Statuses.POISON.showdownName,
Moves.getByName("crosspoison") to Statuses.POISON.showdownName,
Moves.getByName("toxic") to Statuses.POISON_BADLY.showdownName,
Moves.getByName("toxicthread") to Statuses.POISON.showdownName,
Moves.getByName("curse") to "cursed",
Moves.getByName("leechseed") to "leech"
)
val boostFromMoves: Map<String, Map<Stat, Int>> = mapOf(
"bellydrum" to mapOf(Stats.ATTACK to 6),
"swordsdance" to mapOf(Stats.ATTACK to 2),
"honeclaws" to mapOf(Stats.ATTACK to 1, Stats.ACCURACY to 1),
"bulkup" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1),
"curse" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPEED to -1),
"howl" to mapOf(Stats.ATTACK to 1),
"meditate" to mapOf(Stats.ATTACK to 1),
"sharpen" to mapOf(Stats.ATTACK to 1),
"workup" to mapOf(Stats.ATTACK to 1, Stats.SPECIAL_ATTACK to 1),
"acidarmor" to mapOf(Stats.DEFENCE to 2),
"barrier" to mapOf(Stats.DEFENCE to 2),
"cottonguard" to mapOf(Stats.DEFENCE to 3),
"defensecurl" to mapOf(Stats.DEFENCE to 1),
"irondefense" to mapOf(Stats.DEFENCE to 2),
"stockpile" to mapOf(Stats.DEFENCE to 1, Stats.SPECIAL_DEFENCE to 1),
"stuffcheeks" to mapOf(Stats.DEFENCE to 2),
"shelter" to mapOf(Stats.DEFENCE to 2, Stats.EVASION to 1),
"withdraw" to mapOf(Stats.DEFENCE to 1),
"harden" to mapOf(Stats.DEFENCE to 1),
"nastyplot" to mapOf(Stats.SPECIAL_ATTACK to 2),
"calmmind" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1),
"quiverdance" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"tailglow" to mapOf(Stats.SPECIAL_ATTACK to 3),
"takeheart" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1),
"geomancy" to mapOf(Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to 2, Stats.SPEED to 2),
"growth" to mapOf(Stats.SPECIAL_ATTACK to 1),
"amnesia" to mapOf(Stats.SPECIAL_DEFENCE to 2),
"agility" to mapOf(Stats.SPEED to 2),
"autotomize" to mapOf(Stats.SPEED to 2),
"rockpolish" to mapOf(Stats.SPEED to 2),
"dragondance" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 1),
"shiftgear" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 2),
"clangoroussoul" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"extremeevoboost" to mapOf(Stats.ATTACK to 2, Stats.DEFENCE to 2, Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to 2, Stats.SPEED to 2),
"clangoroussoulblaze" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"filletaway" to mapOf(Stats.ATTACK to 2, Stats.SPECIAL_ATTACK to 2, Stats.SPEED to 2),
"noretreat" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"shellsmash" to mapOf(Stats.ATTACK to 2, Stats.DEFENCE to -1, Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to -1, Stats.SPEED to 2),
"tidyup" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 1),
"victorydance" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPEED to 1),
"coil" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.ACCURACY to 1),
"minimize" to mapOf(Stats.EVASION to 2),
"doubleteam" to mapOf(Stats.EVASION to 1)
)
val weatherMoves: Map<String, String> = mapOf(
"raindance" to "rain",
"sunnyday" to "sun",
"sandstorm" to "sand",
"hail" to "hail",
"snowscape" to "snow"
)
val weatherDependentMoves: Map<String, String> = mapOf(
"thunder" to "rain",
"hurricane" to "rain",
"blizzard" to "hail",
"solarbeam" to "sun",
"hydrosteam" to "sun",
"weatherball" to "any"
)
val weatherAbilities: Map<String, String> = mapOf(
"swiftswim" to "rain",
"raindish" to "rain",
"dryskin" to "rain",
"chlorophyll" to "sun",
"leafguard" to "sun",
"flowergift" to "sun",
"solarpower" to "sun",
"sandrush" to "sand",
"sandveil" to "sand",
"sandforce" to "sand",
"icebody" to "hail",
"snowcloak" to "hail"
)
val entryHazards = listOf("spikes", "stealthrock", "stickyweb", "toxicspikes")
val antiHazardsMoves = listOf("rapidspin", "defog", "tidyup")
val antiBoostMoves = listOf("clearsmog", "haze", "roar", "whirlwind")
val pivotMoves = listOf("uturn", "flipturn", "partingshot", "batonpass", "voltswitch", "teleport")
val setupMoves = setOf("tailwind", "trickroom", "auroraveil", "lightscreen", "reflect")
val selfRecoveryMoves = listOf("healorder", "milkdrink", "recover", "rest", "roost", "slackoff", "softboiled")
val priorityMoves = listOf(
"quickattack", "extremespeed", "bulletpunch", "machpunch",
"aquajet", "shadowsneak", "accelerock", "iceshard",
"suckerpunch", "firstimpression", "fakeout"
)
val recoilMoves = listOf(
"doubleedge", "bravebird", "closecombat", "superpower",
"overheat", "dracometeor", "leafstorm", "flareblitz"
)
}
@@ -0,0 +1,737 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.abs
import kotlin.math.max
class MoveEvaluator(
private val damageCalculator: DamageCalculator,
private val opponentPredictor: OpponentPredictor,
private val isDoublesMode: Boolean = false
) {
data class MoveEvaluation(
val move: InBattleMove,
val rawDamage: Double,
val expectedDamage: Double,
val strategicValue: Double,
val riskFactor: Double,
val priority: Int,
val turnAdvantage: Double,
val gamePlanAlignment: Double
) {
val totalScore: Double = (expectedDamage + strategicValue + gamePlanAlignment + turnAdvantage) / riskFactor
}
data class MoveDebugDetails(
val moveId: String,
val moveType: String,
val accuracy: Double,
val rawDamage: Double,
val expectedDamage: Double,
val effectiveness: Double,
val hasStab: Boolean,
val priority: Int,
val isStatusMove: Boolean,
val setsWeather: Boolean,
val setsHazards: Boolean,
val removesHazards: Boolean,
val isRecovery: Boolean,
val isPivot: Boolean,
val isSetup: Boolean,
val isRisky: Boolean
)
fun buildDebugDetails(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): MoveDebugDetails? {
val moveData = Moves.getByName(move.id) ?: return null
val rawDamage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
val expectedDamage = rawDamage * accuracy
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
val attackerTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name } ?: listOf()
val hasStab = AiBattleUtils.hasStab(moveData.elementalType.name, attackerTypes)
val moveId = move.id.lowercase()
val isStatusMove = MoveData.statusMoves.keys.any { it?.name?.lowercase() == moveId }
val setsWeather = moveId in MoveData.weatherMoves.keys
val setsHazards = moveId in MoveData.entryHazards
val removesHazards = moveId in MoveData.antiHazardsMoves
val isRecovery = moveId in MoveData.selfRecoveryMoves
val isPivot = moveId in MoveData.pivotMoves
val isSetup = moveId in MoveData.boostFromMoves.keys
val isRisky = moveId in gamePlan.riskyMoves
return MoveDebugDetails(
moveId = move.id,
moveType = moveData.elementalType.name,
accuracy = accuracy,
rawDamage = rawDamage,
expectedDamage = expectedDamage,
effectiveness = effectiveness,
hasStab = hasStab,
priority = moveData.priority,
isStatusMove = isStatusMove,
setsWeather = setsWeather,
setsHazards = setsHazards,
removesHazards = removesHazards,
isRecovery = isRecovery,
isPivot = isPivot,
isSetup = isSetup,
isRisky = isRisky
)
}
fun evaluateMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
val rawDamage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
val expectedDamage = rawDamage * accuracy
val strategicValue = calculateStrategicValue(move, attacker, defender, battleState, gamePlan)
val riskFactor = calculateRiskFactor(move, attacker, defender, battleState, gamePlan)
val turnAdvantage = calculateTurnAdvantage(move, attacker, defender, battleState)
val gamePlanAlignment = calculateGamePlanAlignment(move, attacker, defender, battleState, gamePlan)
return MoveEvaluation(
move = move,
rawDamage = rawDamage,
expectedDamage = expectedDamage,
strategicValue = strategicValue,
riskFactor = riskFactor,
priority = moveData.priority,
turnAdvantage = turnAdvantage,
gamePlanAlignment = gamePlanAlignment
)
}
private fun getEffectiveAccuracy(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
weather: String?
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var accuracy = if (moveData.accuracy <= 0) 1.0 else moveData.accuracy / 100.0
val normalizedWeather = AiBattleUtils.normalizeWeatherId(weather)
when (normalizedWeather) {
"raindance" -> {
if (move.id.lowercase() in listOf("thunder", "hurricane")) accuracy = 1.0
}
"hail", "snow" -> {
if (move.id.lowercase() == "blizzard") accuracy = 1.0
}
"sunnyday" -> {
if (move.id.lowercase() in listOf("thunder", "hurricane")) accuracy *= 0.5
}
}
when (attacker.currentAbility?.lowercase()) {
"compoundeyes" -> accuracy *= 1.3
"noguard" -> accuracy = 1.0
}
when (defender.currentAbility?.lowercase()) {
"noguard" -> accuracy = 1.0
"sandveil" -> if (normalizedWeather == "sandstorm") accuracy *= 0.8
"snowcloak" -> if (normalizedWeather in listOf("hail", "snow")) accuracy *= 0.8
}
val accuracyBoost = attacker.boosts.getOrDefault(Stats.ACCURACY, 0)
val evasionBoost = defender.boosts.getOrDefault(Stats.EVASION, 0)
val netBoost = accuracyBoost - evasionBoost
if (netBoost != 0) {
accuracy *= if (netBoost > 0) {
(3.0 + netBoost) / 3.0
} else {
3.0 / (3.0 - netBoost)
}
}
return accuracy.coerceIn(0.0, 1.0)
}
private fun calculateStrategicValue(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
var value = 0.0
if (move.id.lowercase() in MoveData.weatherMoves.keys) {
value += evaluateWeatherSetting(move, gamePlan, battleState)
}
if (move.id.lowercase() in MoveData.boostFromMoves.keys) {
value += evaluateSetupMove(move, attacker, defender, battleState, gamePlan)
}
val moveId = move.id.lowercase()
val isStatusMove = MoveData.statusMoves.keys.any { it?.name?.lowercase() == moveId }
if (isStatusMove) {
value += evaluateStatusMove(move, attacker, defender, battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.entryHazards) {
value += evaluateHazardSetting(battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.antiHazardsMoves) {
value += evaluateHazardRemoval(battleState, gamePlan)
}
val moveData = Moves.getByName(move.id)
if (moveData != null && moveData.priority > 0) {
value += evaluatePriorityMove(move, attacker, defender, battleState)
}
if (move.id.lowercase() in MoveData.selfRecoveryMoves) {
value += evaluateRecoveryMove(attacker, battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.pivotMoves) {
value += evaluatePivotMove(move, attacker, defender, battleState)
}
if (battleState.gamePhase == GamePhase.ENDGAME) {
value += evaluateEndgameMove(move, battleState)
}
return value
}
private fun calculateGamePlanAlignment(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var alignment = 0.0
val normalizedWeather = AiBattleUtils.normalizeWeatherId(battleState.weather)
if (move.id.lowercase() in gamePlan.riskyMoves) {
alignment -= 100.0
}
alignment += when (gamePlan.winCondition) {
WinCondition.RAIN_OFFENSE -> {
var score = 0.0
if (move.id.lowercase() == "raindance") score += 200.0
if (normalizedWeather == "raindance") {
if (moveData.elementalType.name.lowercase() == "water") score += 50.0
if (move.id.lowercase() in listOf("thunder", "hurricane")) score += 100.0
}
score
}
WinCondition.SUN_OFFENSE -> {
var score = 0.0
if (move.id.lowercase() == "sunnyday") score += 200.0
if (normalizedWeather == "sunnyday") {
if (moveData.elementalType.name.lowercase() == "fire") score += 50.0
if (move.id.lowercase() == "solarbeam") score += 100.0
}
score
}
WinCondition.SETUP_SWEEP -> {
var score = 0.0
if (move.id.lowercase() in MoveData.boostFromMoves.keys &&
attacker.species in gamePlan.keyPokemon
) {
score += 150.0
}
if (attacker.boosts.values.any { it >= 2 } && moveData.power > 80) {
score += 100.0
}
score
}
WinCondition.AGGRESSIVE_SWEEP -> {
var score = 0.0
if (moveData.power >= 90) score += 60.0
if (moveData.priority > 0 && defender.currentHpPercent < 0.5) score += 80.0
score
}
WinCondition.DEFENSIVE_STALL -> {
var score = 0.0
if (MoveData.statusMoves.keys.any { it?.name?.lowercase() == move.id.lowercase() }) score += 70.0
if (move.id.lowercase() in MoveData.selfRecoveryMoves) score += 60.0
if (moveData.power > 80) score -= 30.0
score
}
WinCondition.HAZARD_CONTROL -> {
var score = 0.0
if (move.id.lowercase() == "stealthrock" && "stealthrock" !in battleState.playerHazards) {
score += 250.0
}
if (move.id.lowercase() in MoveData.entryHazards) score += 100.0
score
}
WinCondition.REVENGE_KILLING -> {
var score = 0.0
if (moveData.priority > 0) score += 120.0
if (defender.currentHp > 50 && moveData.power >= 40 && moveData.power <= 80) score += 40.0
score
}
else -> 0.0
}
if (moveData.power > 0) {
alignment += moveData.power * gamePlan.aggressionLevel * 0.3
} else {
alignment += (1.0 - gamePlan.aggressionLevel) * 30.0
}
return alignment
}
private fun calculateRiskFactor(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val moveData = Moves.getByName(move.id) ?: return 1.0
var risk = 1.0
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
risk += (1.0 - accuracy) * 0.5
if (move.id.lowercase() in MoveData.recoilMoves) {
risk += if (attacker.currentHpPercent < 0.5) 0.5 else 0.2
}
if (move.id.lowercase() in listOf("explosion", "selfdestruct", "memento", "healingwish")) {
if (attacker.species in gamePlan.keyPokemon) {
risk += 2.0
} else if (attacker.currentHpPercent < 0.3) {
risk += 0.2
} else {
risk += 0.8
}
}
if (move.pp <= 1) {
risk += 0.3
}
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (abs(mySpeed - oppSpeed) < 5) {
risk += 0.1
}
val teamAdvantage = calculateTeamAdvantage(battleState)
if (teamAdvantage > 0) {
risk *= 0.8
} else if (teamAdvantage < -1) {
risk *= 1.2
}
return max(0.1, risk)
}
private fun calculateTurnAdvantage(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var advantage = 0.0
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (mySpeed > oppSpeed) advantage += 15.0
if (moveData.priority > 0) advantage += moveData.priority * 25.0
if (battleState.gamePhase == GamePhase.ENDGAME && moveData.power > 90) {
advantage += 20.0
}
return advantage
}
private fun evaluateWeatherSetting(move: InBattleMove, gamePlan: GamePlan, battleState: BattleState): Double {
val moveName = move.id.lowercase()
val weatherType = MoveData.weatherMoves[moveName] ?: return 0.0
return when (gamePlan.winCondition) {
WinCondition.RAIN_OFFENSE -> if (weatherType == "rain") 300.0 else 0.0
WinCondition.SUN_OFFENSE -> if (weatherType == "sun") 300.0 else 0.0
WinCondition.SAND_OFFENSE -> if (weatherType == "sand") 300.0 else 0.0
WinCondition.WEATHER_CONTROL -> 150.0
else -> 30.0
}
}
private fun evaluateSetupMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val boosts = MoveData.boostFromMoves[move.id.lowercase()] ?: return 0.0
var value = 0.0
val totalBoosts = boosts.values.sum()
value += totalBoosts * 40.0 * gamePlan.setupPriority
val relevantBoosts = boosts.keys.mapNotNull { stat -> attacker.boosts[stat] }
if (relevantBoosts.any { it >= 3 }) value *= 0.2
val predictedMoves = opponentPredictor.predictMoveHeuristically(defender, attacker)
val dangerLevel = predictedMoves.count { (moveName, _) ->
(Moves.getByName(moveName)?.power ?: 0.0) > 80
}
if (dangerLevel > 0 && attacker.currentHpPercent < 0.7) {
value *= 0.4
}
return value
}
private fun evaluateStatusMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
if (defender.pokemon?.status != null) return 0.0
return when (move.id.lowercase()) {
"thunderwave", "stunspore", "glare" -> {
if (defender.predictedRole == PokemonRole.SWEEPER) 90.0 else 50.0
}
"willowisp" -> {
if (defender.predictedRole in listOf(PokemonRole.WALLBREAKER, PokemonRole.SWEEPER)) {
80.0
} else 40.0
}
"toxic" -> {
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) 100.0 else 60.0
}
"spore", "sleeppowder" -> 120.0
else -> 30.0
}
}
private fun evaluateHazardSetting(battleState: BattleState, gamePlan: GamePlan): Double {
val remainingOpponents = battleState.playerTeam.count { it.currentHealth > 0 }
if (remainingOpponents < 2) return 15.0
var value = remainingOpponents * 20.0
if (gamePlan.winCondition == WinCondition.HAZARD_CONTROL) value *= 1.8
return value
}
private fun evaluateHazardRemoval(battleState: BattleState, gamePlan: GamePlan): Double {
val myHazards = battleState.npcHazards.size
if (myHazards == 0) return 5.0
var value = myHazards * 40.0
val remainingTeam = battleState.npcTeam.count { it.currentHealth > 0 }
if (remainingTeam >= 3) value *= 1.3
return value
}
private fun evaluatePriorityMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var value = moveData.priority * 20.0
if (defender.currentHpPercent < 0.5) {
val damage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
if (damage >= defender.currentHp) value += 150.0
}
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (abs(mySpeed - oppSpeed) < 10) value += 50.0
return value
}
private fun evaluateRecoveryMove(
attacker: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
if (attacker.currentHpPercent > 0.8) return 5.0
var value = (1.0 - attacker.currentHpPercent) * 140.0
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) value *= 1.5
return value
}
private fun evaluatePivotMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
var value = 40.0
val damage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
value += damage * 0.3
return value
}
private fun evaluateEndgameMove(move: InBattleMove, battleState: BattleState): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var bonus = 0.0
if (moveData.power > 0 && moveData.accuracy >= 90) {
bonus += 35.0
}
if (moveData.accuracy < 80) {
bonus -= 30.0
}
if (moveData.priority > 0) {
bonus += 50.0
}
return bonus
}
private fun calculateTeamAdvantage(battleState: BattleState): Int {
val npcAlive = battleState.npcTeam.count { it.currentHealth > 0 }
val playerAlive = battleState.playerTeam.count { it.currentHealth > 0 }
return npcAlive - playerAlive
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stats): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
fun evaluateMoveForDoubles(
move: InBattleMove,
attacker: TrackedPokemon,
primaryTarget: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
if (isSpreadMove(moveData.target)) {
return evaluateSpreadMove(move, attacker, allOpponents, partner, battleState, gamePlan)
}
val baseEvaluation = evaluateMove(move, attacker, primaryTarget, battleState, gamePlan)
var targetingBonus = 0.0
if (primaryTarget.threatLevel > 5) {
targetingBonus += 20.0
}
return MoveEvaluation(
move = baseEvaluation.move,
rawDamage = baseEvaluation.rawDamage,
expectedDamage = baseEvaluation.expectedDamage,
strategicValue = baseEvaluation.strategicValue + targetingBonus,
riskFactor = baseEvaluation.riskFactor,
priority = baseEvaluation.priority,
turnAdvantage = baseEvaluation.turnAdvantage,
gamePlanAlignment = baseEvaluation.gamePlanAlignment
)
}
private fun evaluateSpreadMove(
move: InBattleMove,
attacker: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
var totalDamage = 0.0
var totalStrategicValue = 0.0
val activeOpponents = allOpponents.filter { it.currentHp > 0 }
val targetCount = activeOpponents.size + (if (hitsPartner(moveData.target) && partner != null) 1 else 0)
for (opponent in activeOpponents) {
val damage = damageCalculator.calculateDamage(
move, attacker, opponent, battleState.weather,
isSpreadMove = true,
targetCount = targetCount
)
totalDamage += damage
if (damage >= opponent.currentHp) {
totalStrategicValue += 150.0
}
}
val multiTargetBonus = activeOpponents.size * 40.0
totalStrategicValue += multiTargetBonus
var partnerPenalty = 0.0
if (hitsPartner(moveData.target) && partner != null && partner.currentHp > 0) {
val partnerDamage = damageCalculator.calculateDamage(
move, attacker, partner, battleState.weather,
isSpreadMove = true,
targetCount = targetCount
)
partnerPenalty = when {
partnerDamage >= partner.currentHp -> -999999.0
partner.species in gamePlan.keyPokemon -> -partnerDamage * 5.0
partner.currentHpPercent < 0.3 -> -partnerDamage * 4.0
else -> -partnerDamage * 2.0
}
totalStrategicValue += partnerPenalty
}
val accuracy =
getEffectiveAccuracy(move, attacker, activeOpponents.firstOrNull() ?: TrackedPokemon(), battleState.weather)
val expectedDamage = totalDamage * accuracy
val riskFactor = if (partnerPenalty < -1000) {
999.0
} else if (partnerPenalty < 0) {
max(1.5, 1.0 + abs(partnerPenalty) / 100.0)
} else {
1.0
}
val gamePlanAlignment = if (activeOpponents.isNotEmpty()) {
calculateGamePlanAlignment(move, attacker, activeOpponents.first(), battleState, gamePlan)
} else {
0.0
}
return MoveEvaluation(
move = move,
rawDamage = totalDamage,
expectedDamage = expectedDamage,
strategicValue = totalStrategicValue,
riskFactor = riskFactor,
priority = moveData.priority,
turnAdvantage = 0.0,
gamePlanAlignment = gamePlanAlignment
)
}
private fun isSpreadMove(target: MoveTarget): Boolean {
return target in listOf(
MoveTarget.allAdjacent,
MoveTarget.allAdjacentFoes,
MoveTarget.all
)
}
private fun hitsPartner(target: MoveTarget): Boolean {
return target == MoveTarget.allAdjacent
}
}
@@ -0,0 +1,157 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.min
class OpponentPredictor {
private val moveHistory = mutableMapOf<String, MutableList<String>>()
private val switchHistory = mutableMapOf<String, MutableList<String>>()
private val situationResponses = mutableMapOf<String, MutableMap<String, Int>>()
private val aggressionLevel = mutableMapOf<String, Double>()
private val setupTendency = mutableMapOf<String, Double>()
private val switchFrequency = mutableMapOf<String, Double>()
fun updateMoveHistory(pokemon: String, move: String, situation: String) {
moveHistory.getOrPut(pokemon) { mutableListOf() }.add(move)
situationResponses.getOrPut(situation) { mutableMapOf() }.merge(move, 1, Int::plus)
val moveData = Moves.getByName(move)
if (moveData != null) {
val currentAggression = aggressionLevel[pokemon] ?: 0.5
val newAggression = if (moveData.power > 0) {
currentAggression * 0.9 + 0.8 * 0.1
} else {
currentAggression * 0.9 + 0.2 * 0.1
}
aggressionLevel[pokemon] = newAggression
if (move in MoveData.boostFromMoves.keys) {
val currentSetup = setupTendency[pokemon] ?: 0.3
setupTendency[pokemon] = min(1.0, currentSetup + 0.15)
}
}
}
fun updateSwitchPattern(fromPokemon: String, toPokemon: String, reason: String) {
switchHistory.getOrPut(fromPokemon) { mutableListOf() }.add(toPokemon)
situationResponses.getOrPut("switch_$reason") { mutableMapOf() }.merge(toPokemon, 1, Int::plus)
val totalActions = (moveHistory[fromPokemon]?.size ?: 0) + (switchHistory[fromPokemon]?.size ?: 0)
if (totalActions > 0) {
switchFrequency[fromPokemon] = (switchHistory[fromPokemon]?.size ?: 0).toDouble() / totalActions
}
}
fun predictNextMove(pokemon: String, situation: String): Map<String, Double> {
val responses = situationResponses[situation] ?: return emptyMap()
val total = responses.values.sum().toDouble()
return if (total > 0) {
responses.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun getSwitchProbability(pokemon: String, situation: String): Double {
val switches = switchHistory[pokemon]?.size ?: 0
val totalActions = (moveHistory[pokemon]?.size ?: 0) + switches
if (totalActions < 5) return 0.2
return min(0.8, switches.toDouble() / totalActions + 0.1)
}
private fun getAggressionLevel(pokemon: String): Double = aggressionLevel[pokemon] ?: 0.5
private fun getSetupTendency(pokemon: String): Double = setupTendency[pokemon] ?: 0.3
private fun getSwitchFrequency(pokemon: String): Double = switchFrequency[pokemon] ?: 0.2
fun predictMoveHeuristically(
opponent: TrackedPokemon,
myPokemon: TrackedPokemon
): Map<String, Double> {
val predictions = mutableMapOf<String, Double>()
val aggression = getAggressionLevel(opponent.species ?: "unknown")
for (moveName in opponent.revealedMoves) {
val moveData = Moves.getByName(moveName) ?: continue
var probability = 0.25
probability *= if (moveData.power > 0) {
aggression + 0.3
} else {
(1.0 - aggression) + 0.3
}
val myTypes = myPokemon.currentTypes ?: listOf()
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, myTypes)
probability *= (0.5 + effectiveness)
predictions[moveName] = probability
}
val total = predictions.values.sum()
return if (total > 0) {
predictions.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun predictSwitchTarget(
fromPokemon: String,
myActivePokemon: TrackedPokemon
): Map<String, Double> {
val pastSwitches = switchHistory[fromPokemon] ?: return emptyMap()
val switchCounts = mutableMapOf<String, Int>()
for (target in pastSwitches) {
switchCounts.merge(target, 1, Int::plus)
}
val total = switchCounts.values.sum().toDouble()
return if (total > 0) {
switchCounts.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun categorizePlaystyle(pokemon: String): OpponentPlaystyle {
val aggression = getAggressionLevel(pokemon)
val setupTend = getSetupTendency(pokemon)
val switchFreq = getSwitchFrequency(pokemon)
return when {
setupTend > 0.5 -> OpponentPlaystyle.SETUP_ORIENTED
aggression > 0.7 && switchFreq < 0.3 -> OpponentPlaystyle.HYPER_OFFENSIVE
switchFreq > 0.5 -> OpponentPlaystyle.MOMENTUM_BASED
aggression < 0.4 -> OpponentPlaystyle.DEFENSIVE_STALL
else -> OpponentPlaystyle.BALANCED
}
}
fun reset() {
moveHistory.clear()
switchHistory.clear()
situationResponses.clear()
aggressionLevel.clear()
setupTendency.clear()
switchFrequency.clear()
}
}
enum class OpponentPlaystyle {
HYPER_OFFENSIVE,
SETUP_ORIENTED,
MOMENTUM_BASED,
DEFENSIVE_STALL,
BALANCED
}
@@ -0,0 +1,340 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.max
class StrategyAnalyzer {
fun analyzeTeamComposition(team: List<Pokemon>): GamePlan {
val roles = team.map { identifyPokemonRole(it) }
val sweepers = roles.count { it == PokemonRole.SWEEPER }
val setupSweepers = roles.count { it == PokemonRole.SETUP_SWEEPER }
val wallbreakers = roles.count { it == PokemonRole.WALLBREAKER }
val supports = roles.count { it in listOf(PokemonRole.SUPPORT, PokemonRole.TANK) }
val revengeKillers = roles.count { it == PokemonRole.REVENGE_KILLER }
val weatherStrategy = identifyWeatherStrategy(team)
if (weatherStrategy != null) {
return createWeatherGamePlan(weatherStrategy, team)
}
val hazardStrategy = identifyHazardStrategy(team)
if (hazardStrategy != null) {
return createHazardGamePlan(team)
}
val winCondition = when {
setupSweepers >= 2 && supports >= 2 -> WinCondition.SETUP_SWEEP
sweepers + wallbreakers >= 4 -> WinCondition.AGGRESSIVE_SWEEP
supports >= 3 -> WinCondition.DEFENSIVE_STALL
revengeKillers >= 2 -> WinCondition.REVENGE_KILLING
else -> WinCondition.BALANCED
}
return createGamePlan(winCondition, team, roles)
}
private fun identifyWeatherStrategy(team: List<Pokemon>): WinCondition? {
val rainSetters = team.count { hasWeatherSettingMove(it, "rain") || hasWeatherSettingAbility(it, "rain") }
val rainAbusers = team.count { hasWeatherAbility(it, "rain") || hasWeatherMoves(it, "rain") }
val sunSetters = team.count { hasWeatherSettingMove(it, "sun") || hasWeatherSettingAbility(it, "sun") }
val sunAbusers = team.count { hasWeatherAbility(it, "sun") || hasWeatherMoves(it, "sun") }
val sandSetters = team.count { hasWeatherSettingMove(it, "sand") || hasWeatherSettingAbility(it, "sand") }
val sandAbusers = team.count { hasWeatherAbility(it, "sand") }
return when {
rainSetters >= 1 && rainAbusers >= 2 -> WinCondition.RAIN_OFFENSE
sunSetters >= 1 && sunAbusers >= 2 -> WinCondition.SUN_OFFENSE
sandSetters >= 1 && sandAbusers >= 2 -> WinCondition.SAND_OFFENSE
rainSetters >= 1 || sunSetters >= 1 || sandSetters >= 1 -> WinCondition.WEATHER_CONTROL
else -> null
}
}
private fun identifyHazardStrategy(team: List<Pokemon>): Boolean {
val hazardSetters = team.count { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.entryHazards }
}
val hazardRemovers = team.count { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.antiHazardsMoves }
}
return hazardSetters >= 2 && hazardRemovers >= 1
}
private fun createWeatherGamePlan(winCondition: WinCondition, team: List<Pokemon>): GamePlan {
val weatherType = when (winCondition) {
WinCondition.RAIN_OFFENSE -> "rain"
WinCondition.SUN_OFFENSE -> "sun"
WinCondition.SAND_OFFENSE -> "sand"
else -> "any"
}
val setters =
team.filter { hasWeatherSettingMove(it, weatherType) || hasWeatherSettingAbility(it, weatherType) }
val abusers = team.filter { hasWeatherAbility(it, weatherType) || hasWeatherMoves(it, weatherType) }
val keyPokemon = (setters + abusers).distinct().map { it.species.name }
val priorities = when (winCondition) {
WinCondition.RAIN_OFFENSE -> listOf(
"set_rain_immediately",
"preserve_rain_setter",
"sweep_with_rain_abusers",
"use_perfect_accuracy_moves",
"prevent_weather_changes"
)
WinCondition.SUN_OFFENSE -> listOf(
"set_sun_immediately",
"preserve_sun_setter",
"sweep_with_chlorophyll",
"use_solar_beam",
"prevent_weather_changes"
)
WinCondition.SAND_OFFENSE -> listOf(
"set_sandstorm",
"abuse_sand_rush",
"chip_damage_from_sand",
"prevent_weather_changes"
)
else -> listOf("maintain_weather", "use_weather_abusers")
}
val riskyMoves = listOf(
"explosion",
"selfdestruct",
"memento"
)
return GamePlan(
winCondition = winCondition,
keyPokemon = keyPokemon,
priorities = priorities,
riskyMoves = riskyMoves,
aggressionLevel = 0.8,
setupPriority = 0.9
)
}
private fun createHazardGamePlan(team: List<Pokemon>): GamePlan {
val hazardSetters = team.filter { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.entryHazards }
}.map { it.species.name }
return GamePlan(
winCondition = WinCondition.HAZARD_CONTROL,
keyPokemon = hazardSetters,
priorities = listOf(
"set_stealth_rock_early",
"stack_hazards_if_safe",
"remove_opponent_hazards",
"force_switches",
"preserve_hazard_setters"
),
riskyMoves = listOf("explosion"),
aggressionLevel = 0.6,
setupPriority = 0.7
)
}
private fun createGamePlan(winCondition: WinCondition, team: List<Pokemon>, roles: List<PokemonRole>): GamePlan {
val keyPokemon = identifyKeyPokemon(winCondition, team, roles)
val priorities = when (winCondition) {
WinCondition.SETUP_SWEEP -> listOf(
"create_setup_opportunity",
"execute_setup_moves",
"sweep_after_setup",
"preserve_setup_sweeper",
"remove_priority_threats"
)
WinCondition.AGGRESSIVE_SWEEP -> listOf(
"maintain_offensive_pressure",
"use_coverage_moves",
"preserve_sweepers",
"use_priority_to_finish",
"momentum_over_setup"
)
WinCondition.DEFENSIVE_STALL -> listOf(
"spread_status_conditions",
"use_recovery_moves",
"set_up_hazards",
"stall_out_threats",
"remove_opponent_hazards"
)
WinCondition.REVENGE_KILLING -> listOf(
"preserve_revenge_killer",
"use_priority_strategically",
"chip_into_priority_range",
"scout_with_protect",
"maintain_momentum"
)
WinCondition.HAZARD_CONTROL -> listOf(
"set_stealth_rock_early",
"stack_hazards_safely",
"remove_opponent_hazards",
"force_switches"
)
else -> listOf(
"maintain_balance",
"adapt_to_opponent",
"resource_management",
"take_calculated_risks"
)
}
val riskyMoves = when (winCondition) {
WinCondition.SETUP_SWEEP -> listOf("explosion", "memento", "healingwish", "lunardance")
WinCondition.REVENGE_KILLING -> listOf("doubleedge", "bravebird", "flareblitz")
WinCondition.DEFENSIVE_STALL -> MoveData.recoilMoves
else -> listOf("explosion", "memento")
}
val aggressionLevel = when (winCondition) {
WinCondition.AGGRESSIVE_SWEEP -> 0.9
WinCondition.SETUP_SWEEP -> 0.7
WinCondition.REVENGE_KILLING -> 0.75
WinCondition.DEFENSIVE_STALL -> 0.3
else -> 0.6
}
val setupPriority = when (winCondition) {
WinCondition.SETUP_SWEEP -> 0.95
WinCondition.DEFENSIVE_STALL -> 0.6
else -> 0.5
}
return GamePlan(
winCondition = winCondition,
keyPokemon = keyPokemon,
priorities = priorities,
riskyMoves = riskyMoves,
aggressionLevel = aggressionLevel,
setupPriority = setupPriority
)
}
private fun identifyKeyPokemon(
winCondition: WinCondition,
team: List<Pokemon>,
roles: List<PokemonRole>
): List<String> {
return when (winCondition) {
WinCondition.SETUP_SWEEP -> {
team.filterIndexed { index, _ -> roles[index] == PokemonRole.SETUP_SWEEPER }
.map { it.species.name }
}
WinCondition.AGGRESSIVE_SWEEP -> {
team.filterIndexed { index, _ -> roles[index] in listOf(PokemonRole.SWEEPER, PokemonRole.WALLBREAKER) }
.map { it.species.name }
}
WinCondition.REVENGE_KILLING -> {
team.filterIndexed { index, _ -> roles[index] == PokemonRole.REVENGE_KILLER }
.map { it.species.name }
}
WinCondition.DEFENSIVE_STALL -> {
team.filterIndexed { index, _ -> roles[index] in listOf(PokemonRole.TANK, PokemonRole.SUPPORT) }
.map { it.species.name }
}
else -> team.take(2).map { it.species.name }
}
}
fun identifyPokemonRole(pokemon: Pokemon): PokemonRole {
val attack = pokemon.species.baseStats[Stats.ATTACK] ?: 0
val spAttack = pokemon.species.baseStats[Stats.SPECIAL_ATTACK] ?: 0
val defense = pokemon.species.baseStats[Stats.DEFENCE] ?: 0
val spDefense = pokemon.species.baseStats[Stats.SPECIAL_DEFENCE] ?: 0
val speed = pokemon.species.baseStats[Stats.SPEED] ?: 0
val hp = pokemon.species.baseStats[Stats.HP] ?: 0
val maxAttack = max(attack, spAttack)
val avgDefense = (defense + spDefense) / 2.0
val bulk = hp + avgDefense
val hasSetupMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.boostFromMoves.keys }
val hasPriorityMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.priorityMoves }
val hasPivotMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.pivotMoves }
val hasRecoveryMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.selfRecoveryMoves }
return when {
hasPivotMoves -> PokemonRole.PIVOT
hasSetupMoves && speed >= 90 && maxAttack >= 90 -> PokemonRole.SETUP_SWEEPER
speed >= 110 && hasPriorityMoves -> PokemonRole.REVENGE_KILLER
speed >= 100 && maxAttack >= 100 -> PokemonRole.SWEEPER
maxAttack >= 120 -> PokemonRole.WALLBREAKER
bulk >= 180 && maxAttack < 80 -> PokemonRole.TANK
hasRecoveryMoves || hasSetupMoves -> PokemonRole.SUPPORT
else -> PokemonRole.BALANCED
}
}
private fun hasWeatherSettingMove(pokemon: Pokemon, weather: String): Boolean {
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
return when (weather) {
"rain" -> "raindance" in moves
"sun" -> "sunnyday" in moves
"sand" -> "sandstorm" in moves
else -> moves.any { it in MoveData.weatherMoves.keys }
}
}
private fun hasWeatherSettingAbility(pokemon: Pokemon, weather: String): Boolean {
val ability = pokemon.ability.name.lowercase()
return when (weather) {
"rain" -> ability == "drizzle"
"sun" -> ability == "drought"
"sand" -> ability == "sandstream"
else -> false
}
}
private fun hasWeatherAbility(pokemon: Pokemon, weather: String): Boolean {
val ability = pokemon.ability.name.lowercase()
return when (weather) {
"rain" -> ability in listOf("swiftswim", "raindish", "dryskin")
"sun" -> ability in listOf("chlorophyll", "leafguard", "flowergift", "solarpower")
"sand" -> ability in listOf("sandrush", "sandveil", "sandforce")
else -> MoveData.weatherAbilities[ability] == weather
}
}
private fun hasWeatherMoves(pokemon: Pokemon, weather: String): Boolean {
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
return when (weather) {
"rain" -> moves.any { it in listOf("thunder", "hurricane") }
"sun" -> moves.any { it in listOf("solarbeam", "hydrosteam") }
else -> false
}
}
}
enum class WinCondition {
AGGRESSIVE_SWEEP,
SETUP_SWEEP,
DEFENSIVE_STALL,
HAZARD_CONTROL,
REVENGE_KILLING,
BALANCED,
RAIN_OFFENSE,
SUN_OFFENSE,
SAND_OFFENSE,
WEATHER_CONTROL
}
data class GamePlan(
val winCondition: WinCondition,
val keyPokemon: List<String>,
val priorities: List<String>,
val riskyMoves: List<String>,
val aggressionLevel: Double = 0.6,
val setupPriority: Double = 0.5
)
@@ -0,0 +1,324 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.max
class SwitchDecisionMaker(
private val damageCalculator: DamageCalculator,
private val opponentPredictor: OpponentPredictor,
private val strategyAnalyzer: StrategyAnalyzer
) {
fun shouldVoluntarilySwitch(
myPokemon: TrackedPokemon,
opponentPokemon: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan,
availableSwitches: List<BattlePokemon>
): Boolean {
if (availableSwitches.isEmpty()) return false
val currentMatchup = evaluateMatchup(myPokemon, opponentPokemon, battleState)
val bestSwitchScore = availableSwitches.maxOfOrNull { switchOption ->
evaluateSwitchOption(
switchOption.effectedPokemon,
opponentPokemon,
battleState,
gamePlan
)
} ?: 0.0
val switchThreshold = 40.0
if (bestSwitchScore > currentMatchup.overallScore + switchThreshold) {
return true
}
val shouldSwitch = checkSwitchConditions(
myPokemon,
opponentPokemon,
battleState,
gamePlan,
currentMatchup
)
return shouldSwitch
}
fun findBestSwitch(
availableSwitches: List<BattlePokemon>,
opponentPokemon: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): BattlePokemon? {
if (availableSwitches.isEmpty()) return null
return availableSwitches.maxByOrNull { switchOption ->
evaluateSwitchOption(
switchOption.effectedPokemon,
opponentPokemon,
battleState,
gamePlan
)
}
}
fun evaluateSwitchOption(
candidate: Pokemon,
opponent: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
var score = 0.0
val candidateTracker = createTrackerPokemon(candidate)
val matchup = evaluateMatchup(candidateTracker, opponent, battleState)
score += matchup.overallScore
val hpFactor = candidateTracker.currentHpPercent
if (hpFactor < 0.5) {
score -= (1.0 - hpFactor) * 40.0
}
if (gamePlan.keyPokemon.contains(candidate.species.name)) {
score += 20.0
}
val mySpeed = getEffectiveStat(candidate, Stats.SPEED)
val oppSpeed = opponent.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (mySpeed > oppSpeed) score += 25.0
val hazardDamage = calculateEntryHazardDamage(candidate, battleState.npcHazards)
score -= hazardDamage * 0.5
val role = strategyAnalyzer.identifyPokemonRole(candidate)
score += evaluateRoleSynergy(role, gamePlan, battleState)
return score
}
private fun evaluateMatchup(
myPokemon: TrackedPokemon,
opponent: TrackedPokemon,
battleState: BattleState
): MatchupEvaluation {
val ourBestEffectiveness = calculateBestMoveEffectiveness(myPokemon, opponent)
val offensiveAdvantage = (ourBestEffectiveness - 1.0) * 40.0
val theirBestEffectiveness = calculateBestMoveEffectiveness(opponent, myPokemon)
val defensiveAdvantage = -(theirBestEffectiveness - 1.0) * 35.0
val mySpeed = myPokemon.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = opponent.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val speedAdvantage = if (mySpeed > oppSpeed) 20.0 else -15.0
val overallScore = offensiveAdvantage + defensiveAdvantage + speedAdvantage
val recommendation = when {
overallScore > 60 && myPokemon.currentHpPercent > 0.7 -> MatchupRecommendation.STAY_IN_AND_SETUP
overallScore > 30 -> MatchupRecommendation.STAY_IN_AND_ATTACK
overallScore > 0 -> MatchupRecommendation.CONSIDER_SWITCHING
overallScore > -30 -> MatchupRecommendation.PIVOT_OUT
else -> MatchupRecommendation.SWITCH_OUT
}
return MatchupEvaluation(
offensiveAdvantage = offensiveAdvantage,
defensiveAdvantage = defensiveAdvantage,
speedAdvantage = speedAdvantage,
overallScore = overallScore,
recommendation = recommendation
)
}
private fun checkSwitchConditions(
myPokemon: TrackedPokemon,
opponent: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan,
matchup: MatchupEvaluation
): Boolean {
val theirBestEffectiveness = calculateBestMoveEffectiveness(opponent, myPokemon)
if (theirBestEffectiveness >= 2.0 && myPokemon.currentHpPercent > 0.3) {
return true
}
if (myPokemon.currentHpPercent < 0.3 && matchup.overallScore < 30) {
return true
}
if (opponent.boosts.values.any { it >= 2 } && matchup.overallScore < 20) {
return true
}
if (myPokemon.species in gamePlan.keyPokemon &&
myPokemon.currentHpPercent < 0.5 &&
theirBestEffectiveness > 1.0
) {
return true
}
return false
}
private fun calculateBestMoveEffectiveness(
attacker: TrackedPokemon,
defender: TrackedPokemon
): Double {
val moves = attacker.pokemon?.moveSet?.getMoves() ?: return 1.0
return moves.maxOfOrNull { move ->
TypeEffectiveness.calculateTypeEffectiveness(
move.type,
defender.currentTypes ?: listOf()
)
} ?: 1.0
}
private fun calculateEntryHazardDamage(pokemon: Pokemon, hazards: List<String>): Double {
var damage = 0.0
val maxHP = getEffectiveStat(pokemon, Stats.HP)
for (hazard in hazards) {
when (hazard.lowercase()) {
"stealthrock" -> {
val rockEffectiveness = pokemon.types.fold(1.0) { acc, type ->
acc * TypeEffectiveness.getDamageMultiplier(
com.cobblemon.mod.common.api.types.ElementalTypes.ROCK,
type
)
}
damage += maxHP * rockEffectiveness * 0.125
}
"spikes" -> damage += maxHP * 0.125
"toxicspikes" -> {
if (!pokemon.types.any { it.name.lowercase() in listOf("poison", "steel") }) {
damage += maxHP * 0.15
}
}
"stickyweb" -> damage += 10.0
}
}
return damage
}
private fun evaluateRoleSynergy(
role: PokemonRole,
gamePlan: GamePlan,
battleState: BattleState
): Double {
return when (gamePlan.winCondition) {
WinCondition.SETUP_SWEEP -> {
when (role) {
PokemonRole.SETUP_SWEEPER -> 50.0
PokemonRole.SUPPORT -> if (battleState.gamePhase == GamePhase.EARLY_GAME) 30.0 else 10.0
else -> 0.0
}
}
WinCondition.AGGRESSIVE_SWEEP -> {
when (role) {
PokemonRole.SWEEPER, PokemonRole.WALLBREAKER -> 40.0
PokemonRole.REVENGE_KILLER -> 30.0
else -> 0.0
}
}
WinCondition.DEFENSIVE_STALL -> {
when (role) {
PokemonRole.TANK, PokemonRole.SUPPORT -> 45.0
else -> -10.0
}
}
WinCondition.REVENGE_KILLING -> {
when (role) {
PokemonRole.REVENGE_KILLER -> 60.0
PokemonRole.PIVOT -> 25.0
else -> 0.0
}
}
WinCondition.RAIN_OFFENSE, WinCondition.SUN_OFFENSE, WinCondition.SAND_OFFENSE -> {
when (role) {
PokemonRole.SWEEPER -> 35.0
else -> 0.0
}
}
else -> 0.0
}
}
private fun createTrackerPokemon(pokemon: Pokemon): TrackedPokemon {
val tracker = TrackedPokemon(
pokemon = pokemon,
originalPokemon = pokemon,
species = pokemon.species.name,
currentHp = pokemon.currentHealth,
maxHp = pokemon.maxHealth,
currentHpPercent = pokemon.currentHealth.toDouble() / pokemon.maxHealth,
currentTypes = pokemon.types.map { it.name.lowercase() }.toMutableList(),
currentAbility = pokemon.ability.name,
moves = pokemon.moveSet.getMoves(),
predictedRole = strategyAnalyzer.identifyPokemonRole(pokemon),
cachedAttack = getEffectiveStat(pokemon, Stats.ATTACK),
cachedDefense = getEffectiveStat(pokemon, Stats.DEFENCE),
cachedSpAttack = getEffectiveStat(pokemon, Stats.SPECIAL_ATTACK),
cachedSpDefense = getEffectiveStat(pokemon, Stats.SPECIAL_DEFENCE),
cachedSpeed = getEffectiveStat(pokemon, Stats.SPEED)
)
return tracker
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stats): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
}
@@ -0,0 +1,403 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
object TypeEffectiveness {
val typeEffectivenessMap: Map<ElementalType, Map<ElementalType, Double>> = mapOf(
ElementalTypes.NORMAL to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 0.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FIRE to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.WATER to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.ELECTRIC to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 2.0,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 0.0,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.GRASS to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 2.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.ICE to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 0.5,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FIGHTING to mapOf(
ElementalTypes.NORMAL to 2.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 0.5,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 0.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.POISON to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 0.5,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 0.5,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.0,
ElementalTypes.FAIRY to 2.0
),
ElementalTypes.GROUND to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 2.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 2.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FLYING to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.PSYCHIC to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 2.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 0.5,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.BUG to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 0.5,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.ROCK to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 0.5,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.GHOST to mapOf(
ElementalTypes.NORMAL to 0.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 2.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.5,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.DRAGON to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 0.0
),
ElementalTypes.DARK to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 2.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.5,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.STEEL to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 2.0
),
ElementalTypes.FAIRY to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
)
)
fun getDamageMultiplier(attackerType: ElementalType, defenderType: ElementalType): Double {
return typeEffectivenessMap[attackerType]?.get(defenderType) ?: 1.0
}
fun calculateTypeEffectiveness(moveType: ElementalType, defenderTypes: List<String>): Double {
var multiplier = 1.0
for (typeString in defenderTypes) {
val defenderType = ElementalTypes.get(typeString.lowercase())
if (defenderType != null) {
multiplier *= getDamageMultiplier(moveType, defenderType)
}
}
return multiplier
}
fun getEffectivenessCategory(effectiveness: Double): EffectivenessCategory {
return when {
effectiveness == 0.0 -> EffectivenessCategory.IMMUNE
effectiveness < 1.0 -> EffectivenessCategory.NOT_VERY_EFFECTIVE
effectiveness == 1.0 -> EffectivenessCategory.NEUTRAL
else -> EffectivenessCategory.SUPER_EFFECTIVE
}
}
enum class EffectivenessCategory {
IMMUNE, NOT_VERY_EFFECTIVE, NEUTRAL, SUPER_EFFECTIVE
}
}
@@ -132,7 +132,6 @@ class TrackerActor(val uuid: UUID) {
actor.activePokemon.filter { newActive -> activePokemon.none { it.id == newActive.battlePokemon?.uuid } }.forEach {
val active = party.firstOrNull { trackMon -> trackMon.id == it.battlePokemon?.uuid } ?: return@forEach
party.remove(active)
active.firstTurn = true // Reset firstTurn when switching in
activePokemon.add(active)
}
val swappedOut = activePokemon.filter { trackMon -> actor.activePokemon.none { it.battlePokemon?.uuid == trackMon.id } }
@@ -146,7 +145,6 @@ class TrackerActor(val uuid: UUID) {
actor.activePokemon.filter { newActive -> activePokemon.none { it.id == newActive.battlePokemon?.uuid } }.forEach {
val active = party.firstOrNull { trackMon -> trackMon.id == it.battlePokemon?.uuid } ?: return@forEach
party.remove(active)
active.firstTurn = true // Reset firstTurn when switching in
activePokemon.add(active)
}
val swappedOut = activePokemon.filter { trackMon -> actor.activePokemon.none { it.battlePokemon?.uuid == trackMon.id } }
@@ -114,14 +114,7 @@ class ActivateInstruction(val instructionSet: InstructionSet, val message: Battl
}
"maxguard", "protect" -> battleLang("activate.protect", pokemonName)
"shadowforce", "hyperspacefury", "hyperspacehole" -> battleLang("activate.phantomforce", pokemonName)
else -> {
val msgVariant = message.optionalArgument("msg")
if (msgVariant != null) {
battleLang("activate.${effect.id}.$msgVariant", pokemonName, sourceName, extraEffect)
} else {
battleLang("activate.${effect.id}", pokemonName, sourceName, extraEffect)
}
}
else -> battleLang("activate.${effect.id}", pokemonName, sourceName, extraEffect)
}
battle.broadcastChatMessage(lang)
//We check holds here so the chat msg + particle effect happen more concurrently, instead of sequentially
@@ -52,7 +52,7 @@ class FaintInstruction(battle: PokemonBattle, val message: BattleMessage) : Inte
preamble.thenAccept {
faintingPokemon.effectedPokemon.currentHealth = 0
faintingPokemon.sendUpdate()
CobblemonEvents.BATTLE_FAINTED.post(BattleFaintedEvent(battle, pokemon, context, pnx))
CobblemonEvents.BATTLE_FAINTED.post(BattleFaintedEvent(battle, pokemon, context))
battle.getActorAndActiveSlotFromPNX(pnx).second.battlePokemon = null
faintingPokemon.contextManager.add(context)
faintingPokemon.contextManager.clear(BattleContext.Type.STATUS, BattleContext.Type.VOLATILE, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
@@ -26,7 +26,6 @@ import com.cobblemon.mod.common.util.battleLang
class FormeChangeInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val (pnx, _) = message.pnxAndUuid(0) ?: return
val battlePokemon = message.battlePokemon(0, battle) ?: return
val details = message.argumentAt(1)?.split(',')?.get(0)?.lowercase() ?: return
val speciesName = details.substringBefore('-')
@@ -38,7 +37,7 @@ class FormeChangeInstruction(val message: BattleMessage): InterpreterInstruction
battle.dispatchWaiting {
battle.minorBattleActions[battlePokemon.uuid] = message
CobblemonEvents.FORME_CHANGE.post(FormeChangeEvent(battle, battlePokemon, formName, pnx))
CobblemonEvents.FORME_CHANGE.post(FormeChangeEvent(battle, battlePokemon, formName))
val pokemonName = battlePokemon.getName()
val lang = when(formName) {
"busted", "hero", "complete" -> return@dispatchWaiting
@@ -166,11 +166,6 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
// If we can't find the entity for some reason then we're going to skip the recall animation
val sendOutFuture = CompletableFuture<Unit>()
(pokemonEntity?.recallWithAnimation() ?: CompletableFuture.completedFuture(Unit)).thenApply {
if (battle.ended) {
sendOutFuture.complete(Unit)
return@thenApply
}
// Queue actual swap and send-in after the animation has ended
actor.pokemonList.swap(actor.activePokemon.indexOf(activePokemon), actor.pokemonList.indexOf(newPokemon))
activePokemon.battlePokemon = newPokemon
@@ -31,7 +31,6 @@ class WeatherInstruction(val message: BattleMessage): InterpreterInstruction {
val weather = message.effectAt(0)?.id ?: return
val source = message.battlePokemonFromOptional(battle)
source?.let { ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), source) }
battle.dispatchWaiting(1.5F) {
val lang = when {
message.hasOptionalArgument("upkeep") -> battleLang("weather.$weather.upkeep")
@@ -43,17 +43,17 @@ class WinInstruction(val message: BattleMessage): InterpreterInstruction {
val losersText = losers.map { it.getName() }.reduce { acc, next -> acc + " & " + next }
val wasCaught = battle.showdownMessages.any { "capture" in it }
// If the battle was a PvW battle, we need to set the killer of the wild Pokémon to the player
if (battle.isPvW) {
val nonPlayerActor = battle.actors.first { it.type == ActorType.WILD }
val wildPokemon: BattlePokemon = nonPlayerActor.pokemonList.first()
if (!wasCaught && losers.any { it.uuid == wildPokemon.uuid }) {
wildPokemon.effectedPokemon.entity?.killer = (battle.actors.firstOrNull { it.type == ActorType.PLAYER } as? PlayerBattleActor)?.entity
}
}
battle.dispatch {
// If the battle was a PvW battle, we need to set the killer of the wild Pokémon to the player
if (battle.isPvW) {
val nonPlayerActor = battle.actors.first { it.type == ActorType.WILD }
val wildPokemon: BattlePokemon = nonPlayerActor.pokemonList.first()
if (!wasCaught && losers.any { it.uuid == wildPokemon.uuid }) {
wildPokemon.effectedPokemon.entity?.killer = (battle.actors.firstOrNull { it.type == ActorType.PLAYER } as? PlayerBattleActor)?.entity
}
}
// broadcast victory / defeat
if (!wasCaught) {
val blackedOut = battle.isPvW && losers.any { it is PlayerBattleActor }
@@ -36,18 +36,10 @@ import java.util.function.Function
open class BattlePokemon(
val originalPokemon: Pokemon,
val effectedPokemon: Pokemon = originalPokemon,
var postBattlePokemonOperations: MutableList<(BattlePokemon) -> Unit> = mutableListOf(),
var postBattleEntityOperations: MutableList<(PokemonEntity) -> Unit> = mutableListOf()
val postBattleEntityOperation: (PokemonEntity) -> Unit = {}
) {
lateinit var actor: BattleActor
constructor(originalPokemon: Pokemon, effectedPokemon: Pokemon, postBattleEntityOperation: (PokemonEntity) -> Unit) : this(
originalPokemon = originalPokemon,
effectedPokemon = effectedPokemon,
postBattlePokemonOperations = mutableListOf<(BattlePokemon) -> Unit>(),
postBattleEntityOperations = mutableListOf(postBattleEntityOperation)
)
companion object {
fun safeCopyOf(pokemon: Pokemon): BattlePokemon {
//TOOD figure out a closer registry access (might have to break some method signatures for this (1.7?)
@@ -27,7 +27,6 @@ interface ShowdownService {
fun openConnection()
fun closeConnection()
fun startBattle(battle: PokemonBattle, messages: Array<String>)
fun endBattle(battle: PokemonBattle)
fun send(battleId: UUID, messages: Array<String>)
fun getRegistryData(type: String): JsonArray
fun sendRegistryData(data: Map<String, String>, type: String)
@@ -129,11 +129,6 @@ class GraalShowdownService : ShowdownService {
startBattleFunction.execute(this, battle.battleId.toString(), messages)
}
override fun endBattle(battle: PokemonBattle) {
val endBattleFunction = context.getBindings("js").getMember("endBattle")
endBattleFunction.execute(battle.battleId.toString())
}
override fun send(battleId: UUID, messages: Array<String>) {
sendToShowdown(battleId, messages)
}
@@ -62,11 +62,6 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
send(battle.battleId, messages)
}
override fun endBattle(battle: PokemonBattle) {
writer.write(">endbattle ${battle.battleId}\n")
acknowledge { Cobblemon.LOGGER.error("Failed to end battle ${battle.battleId}!") }
}
override fun send(battleId: UUID, messages: Array<String>) {
for (message in messages) {
writer.write("$battleId~$message\n")
@@ -133,10 +133,7 @@ class ApricornBlock(settings: Properties, val apricorn: Apricorn) : HorizontalDi
override fun isBonemealSuccess(world: Level, random: RandomSource, pos: BlockPos, state: BlockState) = true
override fun performBonemeal(world: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
val curAge = state.getValue(AGE)
val newAge = curAge + 1
if (newAge > MAX_AGE) return
world.setBlock(pos, state.setValue(AGE, newAge), UPDATE_CLIENTS)
world.setBlock(pos, state.setValue(AGE, state.getValue(AGE) + 1), 2)
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
@@ -189,17 +186,17 @@ class ApricornBlock(settings: Properties, val apricorn: Apricorn) : HorizontalDi
* This uses [Block.dropResources] to handle the drops.
* It will also reset the [BlockState] of this block at the given location to the start of growth.
*
* @param world The [Level] the apricorn is in.
* @param world The [World] the apricorn is in.
* @param state The [BlockState] of the apricorn.
* @param pos The [BlockPos] of the apricorn.
* @return The [BlockState] after harvest.
*/
fun harvest(world: Level, state: BlockState, pos: BlockPos): BlockState {
// Uses loot tables, to change the drops use 'data/cobblemon/loot_tables/blocks/<color>_apricorn.json'
dropResources(state, world, pos)
Block.dropResources(state, world, pos)
// Don't use default as we want to keep the facing
val resetState = state.setValue(AGE, MIN_AGE)
world.setBlock(pos, resetState, UPDATE_CLIENTS)
world.setBlock(pos, resetState, Block.UPDATE_CLIENTS)
return resetState
}
@@ -81,32 +81,7 @@ class BerryBlock(private val berryIdentifier: ResourceLocation, settings: Proper
override fun isBonemealSuccess(world: Level, random: RandomSource, pos: BlockPos, state: BlockState) = !this.isMaxAge(state)
// Use Random Tick instead of BlockEntityTicker for performance optimization
override fun <T : BlockEntity> getTicker(world: Level, blockState: BlockState, blockWithEntityType: BlockEntityType<T>): BlockEntityTicker<T>? = null
// Enable Random Tick: only immature and non-rooted berries need to tick
override fun isRandomlyTicking(state: BlockState): Boolean {
return state.getValue(AGE) < FRUIT_AGE && !state.getValue(IS_ROOTED)
}
// Random Tick handler: uses time compensation to ensure correct growth speed
@Deprecated("Deprecated in Java")
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
val blockEntity = world.getBlockEntity(pos) as? BerryBlockEntity ?: return
// processTick calculates elapsed time and updates timer, returns whether growth is needed
if (blockEntity.processTick(world, pos, state) && !world.getBlockTicks().hasScheduledTick(pos, this)) {
// Schedule growth for next tick to avoid tick scheduler conflicts during randomTick
world.scheduleTick(pos, this, 1)
}
}
// Scheduled tick handler: performs the actual growth
@Deprecated("Deprecated in Java")
override fun tick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
if (state.getValue(IS_ROOTED)) return
growHelper(world, random, pos, state)
}
override fun <T : BlockEntity> getTicker(world: Level, blockState: BlockState, blockWithEntityType: BlockEntityType<T>): BlockEntityTicker<T>? = createTickerHelper(blockWithEntityType, CobblemonBlockEntities.BERRY, BerryBlockEntity.TICKER)
init {
registerDefaultState(stateDefinition.any()
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import net.minecraft.world.item.Item
import net.minecraft.core.dispenser.DispenseItemBehavior
import net.minecraft.world.item.Items
import net.minecraft.world.item.alchemy.PotionContents
import net.minecraft.world.item.alchemy.Potions
import net.minecraft.world.level.block.DispenserBlock
object DispenserBehaviorRegistry {
private val behaviors = mutableMapOf<Item, DispenseItemBehavior>()
fun register(items: List<Item>, behaviorProvider: (Item) -> DispenseItemBehavior) {
for (item in items) {
behaviors[item] = behaviorProvider(item)
}
}
fun applyBehaviors() {
behaviors.forEach { (item, behavior) ->
DispenserBlock.registerBehavior(item, behavior)
}
}
// TODO: Seems like there can only 1 behavior per item, so registering the leaves with the honey bottle overrode the honey bottle behavior for the log.
// Registering a new behavior with `DispenserBlock.registerBehavior` will override any existing ones for that item, possibly even ones from Minecraft and other mods
// Will probably need to use an alternate way of implementing this to avoid conflicts.
fun registerDispenserBehaviors() {
// register(listOf(PotionContents.createItemStack(Items.POTION, Potions.WATER).item)) { SaccharineLogSlatheredBlock.createBehavior() }
// register(listOf(Items.HONEY_BOTTLE)) { SaccharineLogBlock.createBehavior() }
// register(listOf(Items.HONEY_BOTTLE, Items.GLASS_BOTTLE)) { item -> SaccharineLeafBlock.createBehavior(item)}
applyBehaviors()
}
}
@@ -19,7 +19,6 @@ import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.BlockItem
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.block.BaseEntityBlock
@@ -111,8 +110,6 @@ class DisplayCaseBlock(settings: Properties) : BaseEntityBlock(settings) {
override fun getRenderShape(state: BlockState) = RenderShape.MODEL
override fun getShadeBrightness(state: BlockState, level: BlockGetter, pos: BlockPos): Float = 0.75F
override fun getAnalogOutputSignal(state: BlockState, world: Level, pos: BlockPos): Int {
val stack = (world.getBlockEntity(pos) as DisplayCaseBlockEntity).getStack()
@@ -29,7 +29,6 @@ import net.minecraft.world.level.block.SoundType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.level.block.state.properties.EnumProperty
import net.minecraft.world.level.block.state.properties.IntegerProperty
@@ -44,6 +43,7 @@ class HeartyGrainsBlock(settings: Properties) : CropBlock(settings), SimpleWater
val CODEC = simpleCodec(::HeartyGrainsBlock)
const val MATURE_AGE = 6
const val AGE_AFTER_HARVEST = 3 // Last single block stage
val WATERLOGGED = BlockStateProperties.WATERLOGGED
val AGE: IntegerProperty = IntegerProperty.create("age", 0, MATURE_AGE)
val HALF: EnumProperty<DoubleBlockHalf> = BlockStateProperties.DOUBLE_BLOCK_HALF
@@ -21,7 +21,6 @@ import com.cobblemon.mod.common.util.isInBattle
import com.cobblemon.mod.common.util.playSoundServer
import com.cobblemon.mod.common.util.toVec3d
import com.cobblemon.mod.common.util.voxelShape
import com.cobblemon.mod.common.util.rotateShape
import com.mojang.serialization.MapCodec
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
@@ -72,10 +71,10 @@ class PastureBlock(settings: Properties): BaseEntityBlock(settings), SimpleWater
private val WEST_AABB_TOP = buildCollider(top = true, Direction.WEST)
private val EAST_AABB_TOP = buildCollider(top = true, Direction.EAST)
private val SOUTH_AABB_BOTTOM = buildCollider(top = false, Direction.SOUTH)
private val NORTH_AABB_BOTTOM = buildCollider(top = false, Direction.NORTH)
private val WEST_AABB_BOTTOM = buildCollider(top = false, Direction.WEST)
private val EAST_AABB_BOTTOM = buildCollider(top = false, Direction.EAST)
private val SOUTH_AABB_BOTTOM = rotateShape(Direction.EAST, Direction.SOUTH, EAST_AABB_BOTTOM)
private val NORTH_AABB_BOTTOM = rotateShape(Direction.EAST, Direction.NORTH, EAST_AABB_BOTTOM)
private val WEST_AABB_BOTTOM = rotateShape(Direction.EAST, Direction.WEST, EAST_AABB_BOTTOM)
private fun buildCollider(top: Boolean, direction: Direction): VoxelShape {
if (top) {
@@ -1,128 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.util.rotateShape
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.FaceAttachedHorizontalDirectionalBlock
import net.minecraft.world.level.block.SimpleWaterloggedBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.AttachFace
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class PlaqueBlock(properties : Properties) : FaceAttachedHorizontalDirectionalBlock(properties), SimpleWaterloggedBlock {
companion object {
val CODEC: MapCodec<PlaqueBlock> = RecordCodecBuilder.mapCodec {
it.group(propertiesCodec()).apply(it, ::PlaqueBlock)
}
val FLOOR_NORTH = Shapes.or(
Shapes.box(0.0625, 0.25, 0.4375, 0.9375, 1.0, 0.5625),
Shapes.box(0.125, 0.0, 0.4375, 0.25, 0.25, 0.5625),
Shapes.box(0.75, 0.0, 0.4375, 0.875, 0.25, 0.5625),
)
val WALL_NORTH = Shapes.box(0.0625, 0.125, 0.9375, 0.9375, 0.875, 1.0)
val CEILING_NORTH = Shapes.box(0.0625, 0.0, 0.4375, 0.9375, 0.75, 0.5625)
}
init {
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(FACE, AttachFace.WALL)
.setValue(WATERLOGGED, false)
)
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(FACING, FACE, WATERLOGGED)
}
override fun getFluidState(blockState: BlockState): FluidState? {
return if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState);
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
val fluidState = context.level.getFluidState(context.clickedPos)
for (direction in context.nearestLookingDirections) {
val blockState: BlockState
if (direction.getAxis() === Direction.Axis.Y) {
blockState = (defaultBlockState()
.setValue(FACE, if (direction == Direction.UP) AttachFace.CEILING else AttachFace.FLOOR) as BlockState)
.setValue(FACING, context.horizontalDirection.opposite)
.setValue(WATERLOGGED, fluidState.type === Fluids.WATER
) as BlockState
} else {
blockState = (defaultBlockState()
.setValue(FACE, AttachFace.WALL) as BlockState)
.setValue(FACING, direction.opposite)
.setValue(WATERLOGGED, fluidState.type === Fluids.WATER
) as BlockState
}
if (blockState.canSurvive(context.getLevel(), context.getClickedPos())) {
return blockState
}
}
return null
}
override fun codec(): MapCodec<PlaqueBlock> = CODEC
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
val direction = state.getValue(FACING)
return when (state.getValue(FACE)) {
AttachFace.FLOOR ->
if (direction.axis === Direction.Axis.X) {
rotateShape(Direction.NORTH, Direction.WEST, FLOOR_NORTH)
} else FLOOR_NORTH
AttachFace.WALL ->
when (direction) {
Direction.NORTH, Direction.UP, Direction.DOWN -> WALL_NORTH
else -> rotateShape(Direction.NORTH, direction, WALL_NORTH)
}
AttachFace.CEILING ->
if (direction.axis === Direction.Axis.X) {
rotateShape(Direction.NORTH, Direction.WEST, CEILING_NORTH)
} else CEILING_NORTH
}
}
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
if (state.getValue(WATERLOGGED)) {
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world))
}
return if (getConnectedDirection(state).opposite == direction && !state.canSurvive(world, pos)) Blocks.AIR.defaultBlockState()
else super.updateShape(state, direction, neighborState, world, pos, neighborPos)
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
return if (state.getValue(FACE) == AttachFace.CEILING) {
val direction = getConnectedDirection(state)
canSupportCenter(level, pos.relative(direction.opposite), direction)
} else {
super.canSurvive(state, level, pos)
}
}
}

Some files were not shown because too many files have changed in this diff Show More