mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-21 06:36:25 -04:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cdf91c97f | ||
|
|
3f5b659616 | ||
|
|
ee20f012e6 | ||
|
|
12dcc98cf9 | ||
|
|
8c92defc90 | ||
|
|
95509df022 | ||
|
|
bb20890b26 | ||
|
|
50d60674bb | ||
|
|
ff994f69f2 | ||
|
|
73448d4737 | ||
|
|
42099d3ae5 | ||
|
|
bc23ab8eb4 | ||
|
|
6c860d8e93 | ||
|
|
de6b713adf | ||
|
|
6f5c2575f3 | ||
|
|
b23afd17c2 | ||
|
|
74541c1bcb | ||
|
|
56c3ff55ca | ||
|
|
c5b82d1ad4 | ||
|
|
1237b51059 | ||
|
|
9529fe66ac | ||
|
|
7058ac21cb | ||
|
|
b5d0cddc82 | ||
|
|
2f918220e9 |
@@ -2,51 +2,22 @@
|
||||
## [1.7.2 (MONTH Xth, YEAR)](#1-7-2)
|
||||
|
||||
### Additions
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- Ride sounds can now be set as exclusive to passengers
|
||||
- Ride sounds no longer play when submerged unless specified with a new setting
|
||||
|
||||
### Particles
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ dependencies {
|
||||
// Flywheel has no common dep so just pick one and don't use any platform specific code in common
|
||||
// modCompileOnly(libs.flywheelFabric)
|
||||
|
||||
modCompileOnly(files("temp/vivecraft-1.21.1-1.3.4.jar")) // TODO: Switch this to a proper dependency when Vivecraft gets a maven repo
|
||||
|
||||
// Showdown
|
||||
modCompileOnly(libs.graal)
|
||||
|
||||
|
||||
@@ -11,15 +11,11 @@ package com.cobblemon.mod.common.mixin;
|
||||
import com.cobblemon.mod.common.Cobblemon;
|
||||
import com.cobblemon.mod.common.OrientationControllable;
|
||||
import com.cobblemon.mod.common.PlayerSpawnerAccessor;
|
||||
import com.cobblemon.mod.common.api.riding.RidingStyle;
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
|
||||
import com.cobblemon.mod.common.api.stats.CobblemonStats;
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@@ -93,32 +89,4 @@ public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
|
||||
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
|
||||
return original && !(entity instanceof PokemonEntity);
|
||||
}
|
||||
|
||||
@WrapOperation(method = "checkRidingStatistics", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;getVehicle()Lnet/minecraft/world/entity/Entity;"))
|
||||
public Entity cobblemon$checkRidingStatistics(ServerPlayer player, Operation<Entity> original, @Local int distance) {
|
||||
var entity = original.call(player);
|
||||
if (!(entity instanceof PokemonEntity pokemonEntity)) {
|
||||
return entity;
|
||||
}
|
||||
|
||||
var ridingController = pokemonEntity.getRidingController();
|
||||
if (ridingController == null) return entity;
|
||||
|
||||
var activeContext = ridingController.getContext();
|
||||
if (activeContext == null) return entity;
|
||||
|
||||
var ridingStyle = activeContext.getStyle();
|
||||
CobblemonStats.CobblemonStat stat = switch (ridingStyle) {
|
||||
case RidingStyle.LAND -> CobblemonStats.RIDING_LAND;
|
||||
case RidingStyle.AIR -> CobblemonStats.RIDING_AIR;
|
||||
case RidingStyle.LIQUID -> CobblemonStats.RIDING_LIQUID;
|
||||
default -> null;
|
||||
};
|
||||
|
||||
if (stat != null) {
|
||||
player.awardStat(CobblemonStats.getStat(stat), distance);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ 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.*
|
||||
import com.cobblemon.mod.common.integration.vivecraft.CobblemonVR
|
||||
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
|
||||
@@ -91,7 +92,7 @@ import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatu
|
||||
import com.cobblemon.mod.common.pokemon.feature.TagSeasonResolver
|
||||
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
|
||||
import com.cobblemon.mod.common.pokemon.properties.*
|
||||
import com.cobblemon.mod.common.pokemon.properties.LabelProperty
|
||||
import com.cobblemon.mod.common.pokemon.properties.tags.PokemonFlagProperty
|
||||
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
|
||||
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
|
||||
import com.cobblemon.mod.common.util.*
|
||||
@@ -130,10 +131,12 @@ object Cobblemon {
|
||||
const val MODID = CobblemonBuildDetails.MOD_ID
|
||||
const val VERSION = CobblemonBuildDetails.VERSION
|
||||
const val CONFIG_PATH = "config/$MODID/main.json"
|
||||
|
||||
@JvmField
|
||||
val LOGGER: Logger = LogManager.getLogger()
|
||||
|
||||
lateinit var implementation: CobblemonImplementation
|
||||
|
||||
@Deprecated("This field is now a config value", ReplaceWith("Cobblemon.config.captureCalculator"))
|
||||
var captureCalculator: CaptureCalculator
|
||||
get() = this.config.captureCalculator
|
||||
@@ -163,21 +166,49 @@ object Cobblemon {
|
||||
val serverPlayerStructs = mutableMapOf<UUID, ObjectValue<Player>>()
|
||||
|
||||
val statistics: CobblemonStats = CobblemonStats
|
||||
var cobblemonVR: CobblemonVR? = null
|
||||
|
||||
@JvmStatic
|
||||
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),
|
||||
CobblemonPack(id = "uniqueshinyforms", name = "Shinies for Magikarp Jump", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.NORMAL),
|
||||
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
|
||||
),
|
||||
CobblemonPack(
|
||||
id = "uniqueshinyforms",
|
||||
name = "Shinies for Magikarp Jump",
|
||||
packType = PackType.CLIENT_RESOURCES,
|
||||
activationBehaviour = ResourcePackActivationBehaviour.NORMAL
|
||||
),
|
||||
|
||||
CobblemonPack(id = "repurposedstructurescobblemon", name = "Repurposed Structures Cobblemon", packType = PackType.SERVER_DATA, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED, neededMods = setOf("repurposed_structures")),
|
||||
CobblemonPack(
|
||||
id = "repurposedstructurescobblemon",
|
||||
name = "Repurposed Structures Cobblemon",
|
||||
packType = PackType.SERVER_DATA,
|
||||
activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED,
|
||||
neededMods = setOf("repurposed_structures")
|
||||
),
|
||||
)
|
||||
|
||||
fun preInitialize(implementation: CobblemonImplementation) {
|
||||
this.implementation = implementation
|
||||
|
||||
this.LOGGER.info("Launching Cobblemon ${CobblemonBuildDetails.VERSION}${if(CobblemonBuildDetails.SNAPSHOT) "-SNAPSHOT" else ""} ")
|
||||
this.LOGGER.info("Launching Cobblemon ${CobblemonBuildDetails.VERSION}${if (CobblemonBuildDetails.SNAPSHOT) "-SNAPSHOT" else ""} ")
|
||||
if (CobblemonBuildDetails.SNAPSHOT) {
|
||||
this.LOGGER.info(" - Git Commit: ${smallCommitHash()} (https://gitlab.com/cable-mc/cobblemon/-/commit/${CobblemonBuildDetails.GIT_COMMIT})")
|
||||
this.LOGGER.info(" - Branch: ${CobblemonBuildDetails.BRANCH}")
|
||||
@@ -201,7 +232,6 @@ object Cobblemon {
|
||||
implementation.registerEntityDataSerializers()
|
||||
implementation.registerCriteria()
|
||||
implementation.registerEntitySubPredicates()
|
||||
implementation.registerMobEffects()
|
||||
DispenserBehaviorRegistry.registerDispenserBehaviors()
|
||||
|
||||
DropEntry.register("command", CommandDropEntry::class.java)
|
||||
@@ -242,7 +272,10 @@ object Cobblemon {
|
||||
}
|
||||
|
||||
PlatformEvents.RIGHT_CLICK_ENTITY.subscribe { event ->
|
||||
if (event.player.getItemInHand(event.hand).item is NameTagItem && event.entity.type.`is`(CobblemonEntityTypeTags.CANNOT_HAVE_NAME_TAG)) {
|
||||
if (event.player.getItemInHand(event.hand).item is NameTagItem && event.entity.type.`is`(
|
||||
CobblemonEntityTypeTags.CANNOT_HAVE_NAME_TAG
|
||||
)
|
||||
) {
|
||||
event.cancel()
|
||||
}
|
||||
}
|
||||
@@ -253,7 +286,10 @@ object Cobblemon {
|
||||
pokemon.lockedEvolutions
|
||||
.filterIsInstance<BlockClickEvolution>()
|
||||
.forEach { evolution ->
|
||||
evolution.attemptEvolution(pokemon, BlockClickEvolution.BlockInteractionContext(block, player.level()))
|
||||
evolution.attemptEvolution(
|
||||
pokemon,
|
||||
BlockClickEvolution.BlockInteractionContext(block, player.level())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,9 +349,14 @@ object Cobblemon {
|
||||
SlowpokeTailRegrowthSpeciesFeatureProvider
|
||||
)
|
||||
|
||||
if (implementation.isModInstalled("vivecraft")) { // VR integration
|
||||
cobblemonVR = CobblemonVR()
|
||||
cobblemonVR?.init()
|
||||
}
|
||||
|
||||
CustomPokemonProperty.register(UncatchableProperty)
|
||||
CustomPokemonProperty.register(BattleCloneProperty)
|
||||
CustomPokemonProperty.register(LabelProperty)
|
||||
CustomPokemonProperty.register(PokemonFlagProperty)
|
||||
CustomPokemonProperty.register(HiddenAbilityPropertyType)
|
||||
CustomPokemonProperty.register(AspectPropertyType)
|
||||
CustomPokemonProperty.register(UnaspectPropertyType)
|
||||
@@ -332,7 +373,7 @@ object Cobblemon {
|
||||
}
|
||||
|
||||
PlatformEvents.SERVER_TICK_POST.subscribe {
|
||||
ServerTaskTracker.update(1/20F)
|
||||
ServerTaskTracker.update(1 / 20F)
|
||||
ServerRealTimeTaskTracker.update()
|
||||
}
|
||||
PlatformEvents.SERVER_TICK_PRE.subscribe {
|
||||
@@ -376,10 +417,22 @@ object Cobblemon {
|
||||
.applyConnectionString(ConnectionString(config.mongoDBConnectionString))
|
||||
.build()
|
||||
mongoClient = MongoClients.create(mongoClientSettings)
|
||||
val generalMongoFactory = CachedPlayerDataStoreFactory(PlayerDataMongoBackend(mongoClient, config.mongoDBDatabaseName, "PlayerDataCollection"))
|
||||
val generalMongoFactory = CachedPlayerDataStoreFactory(
|
||||
PlayerDataMongoBackend(
|
||||
mongoClient,
|
||||
config.mongoDBDatabaseName,
|
||||
"PlayerDataCollection"
|
||||
)
|
||||
)
|
||||
generalMongoFactory.setup(server)
|
||||
|
||||
val pokedexMongoFactory = CachedPlayerDataStoreFactory(DexDataMongoBackend(mongoClient, config.mongoDBDatabaseName, "PokeDexCollection"))
|
||||
val pokedexMongoFactory = CachedPlayerDataStoreFactory(
|
||||
DexDataMongoBackend(
|
||||
mongoClient,
|
||||
config.mongoDBDatabaseName,
|
||||
"PokeDexCollection"
|
||||
)
|
||||
)
|
||||
pokedexMongoFactory.setup(server)
|
||||
|
||||
playerDataManager.setFactory(generalMongoFactory, PlayerInstancedDataStoreTypes.GENERAL)
|
||||
@@ -501,6 +554,7 @@ object Cobblemon {
|
||||
field.set(config, field.get(defaultConfig))
|
||||
}
|
||||
}
|
||||
|
||||
is IntConstraint -> {
|
||||
var value = field.get(config)
|
||||
if (value is Int) {
|
||||
@@ -553,20 +607,76 @@ object Cobblemon {
|
||||
}
|
||||
|
||||
private fun registerArgumentTypes() {
|
||||
this.implementation.registerCommandArgument(cobblemonResource("pokemon"), SpeciesArgumentType::class, SingletonArgumentInfo.contextFree(SpeciesArgumentType::species))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("pokemon_properties"), PokemonPropertiesArgumentType::class, SingletonArgumentInfo.contextFree(PokemonPropertiesArgumentType::properties))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("spawn_bucket"), SpawnBucketArgumentType::class, SingletonArgumentInfo.contextFree(SpawnBucketArgumentType::spawnBucket))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("move"), MoveArgumentType::class, SingletonArgumentInfo.contextFree(MoveArgumentType::move))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("party_slot"), PartySlotArgumentType::class, SingletonArgumentInfo.contextFree(PartySlotArgumentType::partySlot))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("pokemon_store"), PokemonStoreArgumentType::class, SingletonArgumentInfo.contextFree(PokemonStoreArgumentType::pokemonStore))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("dialogue"), DialogueArgumentType::class, SingletonArgumentInfo.contextFree(DialogueArgumentType::dialogue))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("form"), FormArgumentType::class, SingletonArgumentInfo.contextFree(FormArgumentType::form))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("dex"), DexArgumentType::class, SingletonArgumentInfo.contextFree (DexArgumentType::dex))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("npc_class"), NPCClassArgumentType::class, SingletonArgumentInfo.contextFree(NPCClassArgumentType::npcClass))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("wallpaper"), UnlockablePCBoxWallpaperArgumentType::class, SingletonArgumentInfo.contextFree(UnlockablePCBoxWallpaperArgumentType::wallpaper))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("mark"), MarkArgumentType::class, SingletonArgumentInfo.contextFree(MarkArgumentType::mark))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("transform_type"), TransformTypeArgumentType::class, SingletonArgumentInfo.contextFree(TransformTypeArgumentType::transformType))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("model_part"), ModelPartArgumentType::class, SingletonArgumentInfo.contextFree(ModelPartArgumentType::modelPart))
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("pokemon"),
|
||||
SpeciesArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(SpeciesArgumentType::species)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("pokemon_properties"),
|
||||
PokemonPropertiesArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(PokemonPropertiesArgumentType::properties)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("spawn_bucket"),
|
||||
SpawnBucketArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(SpawnBucketArgumentType::spawnBucket)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("move"),
|
||||
MoveArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(MoveArgumentType::move)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("party_slot"),
|
||||
PartySlotArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(PartySlotArgumentType::partySlot)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("pokemon_store"),
|
||||
PokemonStoreArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(PokemonStoreArgumentType::pokemonStore)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("dialogue"),
|
||||
DialogueArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(DialogueArgumentType::dialogue)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("form"),
|
||||
FormArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(FormArgumentType::form)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("dex"),
|
||||
DexArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(DexArgumentType::dex)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("npc_class"),
|
||||
NPCClassArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(NPCClassArgumentType::npcClass)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("wallpaper"),
|
||||
UnlockablePCBoxWallpaperArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(UnlockablePCBoxWallpaperArgumentType::wallpaper)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("mark"),
|
||||
MarkArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(MarkArgumentType::mark)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("transform_type"),
|
||||
TransformTypeArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(TransformTypeArgumentType::transformType)
|
||||
)
|
||||
this.implementation.registerCommandArgument(
|
||||
cobblemonResource("model_part"),
|
||||
ModelPartArgumentType::class,
|
||||
SingletonArgumentInfo.contextFree(ModelPartArgumentType::modelPart)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -117,11 +117,6 @@ interface CobblemonImplementation {
|
||||
|
||||
fun registerEntitySubPredicates()
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
fun registerMobEffects()
|
||||
|
||||
|
||||
/**
|
||||
* Add a feature to the current platform implementation.
|
||||
|
||||
@@ -1,33 +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.cooking.effects.CleanseAllEffect
|
||||
import com.cobblemon.mod.common.api.cooking.effects.CleanseNegativeEffect
|
||||
import com.cobblemon.mod.common.api.cooking.effects.MentalRestorationEffect
|
||||
import com.cobblemon.mod.common.platform.PlatformRegistry
|
||||
import net.minecraft.core.Registry
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.resources.ResourceKey
|
||||
import net.minecraft.world.effect.MobEffect
|
||||
|
||||
object CobblemonMobEffects : PlatformRegistry<Registry<MobEffect>, ResourceKey<Registry<MobEffect>>, MobEffect>() {
|
||||
override val registry: Registry<MobEffect> = BuiltInRegistries.MOB_EFFECT
|
||||
override val resourceKey: ResourceKey<Registry<MobEffect>> = Registries.MOB_EFFECT
|
||||
|
||||
@JvmField
|
||||
val CLEANSE_NEGATIVE = create("cleanse_negative", CleanseNegativeEffect())
|
||||
|
||||
@JvmField
|
||||
val MENTAL_RESTORATION = create("mental_restoration", MentalRestorationEffect())
|
||||
|
||||
@JvmField
|
||||
val CLEANSE_ALL = create("cleanse_all", CleanseAllEffect())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
-22
@@ -1,22 +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.cooking.effects
|
||||
|
||||
import net.minecraft.world.effect.MobEffect
|
||||
import net.minecraft.world.effect.MobEffectCategory
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
|
||||
class CleanseAllEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xFFFFFF) {
|
||||
override fun isInstantenous() = true
|
||||
override fun applyInstantenousEffect(source: Entity?, indirect: Entity?, target: LivingEntity, amplifier: Int, proximity: Double) {
|
||||
target.removeAllEffects()
|
||||
}
|
||||
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int) = false
|
||||
}
|
||||
-27
@@ -1,27 +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.cooking.effects
|
||||
|
||||
import net.minecraft.world.effect.MobEffect
|
||||
import net.minecraft.world.effect.MobEffectCategory
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
|
||||
class CleanseNegativeEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xFFFFFF) {
|
||||
override fun isInstantenous() = true
|
||||
override fun applyInstantenousEffect(source: Entity?, indirect: Entity?, target: LivingEntity, amplifier: Int, proximity: Double) {
|
||||
val snapshot = target.activeEffects.toList()
|
||||
for (ei in snapshot) {
|
||||
if (ei.effect.value().category == MobEffectCategory.HARMFUL) {
|
||||
target.removeEffect(ei.effect)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int) = false
|
||||
}
|
||||
-33
@@ -1,33 +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.cooking.effects
|
||||
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.stats.Stats
|
||||
import net.minecraft.world.effect.MobEffect
|
||||
import net.minecraft.world.effect.MobEffectCategory
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
|
||||
class MentalRestorationEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xAA66FF) {
|
||||
override fun applyEffectTick(entity: LivingEntity, amplifier: Int): Boolean {
|
||||
if (entity is ServerPlayer) {
|
||||
val stats = entity.stats
|
||||
val statType = Stats.CUSTOM.get(Stats.TIME_SINCE_REST)
|
||||
val current = stats.getValue(statType)
|
||||
val reduction = 31 * (amplifier + 1)// reduce by half a minute every second per level (30 ticks removed per tick + 1 to account for the tick passing)
|
||||
val newValue = Math.max(current - reduction, 0)
|
||||
stats.setValue(entity, statType, newValue)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
+2
-7
@@ -12,10 +12,10 @@ import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.OwnerQueryRequirement
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.net.messages.client.data.PokemonInteractionsSyncPacket
|
||||
import com.cobblemon.mod.common.pokemon.adapters.CobblemonRequirementAdapter
|
||||
import com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter
|
||||
import com.cobblemon.mod.common.pokemon.requirements.PokemonPropertiesRequirement
|
||||
@@ -51,12 +51,7 @@ object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
|
||||
val speciesInteractions = mutableListOf<PokemonInteractionSet>()
|
||||
val generalInteractions = mutableListOf<PokemonInteractionSet>()
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
PokemonInteractionsSyncPacket(
|
||||
speciesInteractions,
|
||||
generalInteractions,
|
||||
).sendToPlayer(player)
|
||||
}
|
||||
override fun sync(player: ServerPlayer) {}
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>) {
|
||||
speciesInteractions.clear()
|
||||
|
||||
+19
-88
@@ -15,10 +15,11 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
|
||||
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.cobblemon.mod.common.util.asExpressionLike
|
||||
import com.cobblemon.mod.common.util.giveOrDropItemStack
|
||||
import com.cobblemon.mod.common.util.withPlayerValue
|
||||
import com.cobblemon.mod.common.util.withQueryValue
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.network.FriendlyByteBuf
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.sounds.SoundSource
|
||||
@@ -27,83 +28,30 @@ import net.minecraft.world.entity.EquipmentSlot
|
||||
import net.minecraft.world.entity.item.ItemEntity
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
/**
|
||||
* A
|
||||
*/
|
||||
data class PokemonInteractionSet(
|
||||
val requirements: List<Requirement> = listOf(),
|
||||
val interactions: List<PokemonInteraction> = listOf(),
|
||||
) {
|
||||
fun encode(buffer: FriendlyByteBuf) {
|
||||
buffer.writeCollection(requirements) { it, requirement ->
|
||||
CobblemonRequirementAdapter.encode(requirement, it)
|
||||
}
|
||||
buffer.writeCollection(interactions) { it, interaction ->
|
||||
interaction.encode(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(buffer: FriendlyByteBuf): PokemonInteractionSet {
|
||||
val requirements = buffer.readList { it ->
|
||||
CobblemonRequirementAdapter.decode(it)
|
||||
}
|
||||
val interactions = buffer.readList { it ->
|
||||
PokemonInteraction.decode(it)
|
||||
}
|
||||
|
||||
return PokemonInteractionSet(
|
||||
requirements,
|
||||
interactions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val interactions: List<PokemonInteraction> = listOf()
|
||||
)
|
||||
data class PokemonInteraction(
|
||||
val grouping: ResourceLocation,
|
||||
val requirements: List<Requirement> = listOf(),
|
||||
val effects: List<InteractionEffect> = listOf(),
|
||||
val cooldown: ExpressionLike = "0".asExpressionLike()
|
||||
) {
|
||||
fun encode(buffer: FriendlyByteBuf) {
|
||||
buffer.writeResourceLocation(grouping)
|
||||
buffer.writeCollection(requirements) { it, requirement ->
|
||||
CobblemonRequirementAdapter.encode(requirement, it)
|
||||
}
|
||||
buffer.writeExpressionLike(cooldown)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(buffer: FriendlyByteBuf): PokemonInteraction {
|
||||
val grouping = buffer.readResourceLocation()
|
||||
val requirements = buffer.readList { it ->
|
||||
CobblemonRequirementAdapter.decode(it)
|
||||
}
|
||||
|
||||
// No need to sync these for now
|
||||
val effects = emptyList<InteractionEffect>()
|
||||
val cooldown = buffer.readExpressionLike()
|
||||
|
||||
return PokemonInteraction(
|
||||
grouping,
|
||||
requirements,
|
||||
effects,
|
||||
cooldown,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
interface InteractionEffect {
|
||||
fun applyEffect(pokemon: PokemonEntity, player: ServerPlayer)
|
||||
}
|
||||
|
||||
class DropItemEffect(val item: ResourceLocation, val amount: IntRange?) : InteractionEffect {
|
||||
class DropItemEffect(val item: ResourceLocation, val amount: IntRange?): InteractionEffect {
|
||||
override fun applyEffect(
|
||||
pokemon: PokemonEntity,
|
||||
player: ServerPlayer
|
||||
) {
|
||||
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException(
|
||||
"Cannot load item with id: $item"
|
||||
)
|
||||
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException("Cannot load item with id: $item")
|
||||
val stack = ItemStack(item)
|
||||
stack.count = amount?.randomOrNull() ?: 1
|
||||
if (stack.isEmpty)
|
||||
@@ -120,14 +68,12 @@ class DropItemEffect(val item: ResourceLocation, val amount: IntRange?) : Intera
|
||||
}
|
||||
}
|
||||
|
||||
class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?) : InteractionEffect {
|
||||
class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?): InteractionEffect {
|
||||
override fun applyEffect(
|
||||
pokemon: PokemonEntity,
|
||||
player: ServerPlayer
|
||||
) {
|
||||
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException(
|
||||
"Cannot load item with id: $item"
|
||||
)
|
||||
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException("Cannot load item with id: $item")
|
||||
val stack = ItemStack(item)
|
||||
stack.count = amount?.randomOrNull() ?: 1
|
||||
if (stack.isEmpty)
|
||||
@@ -140,29 +86,14 @@ class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?) : Intera
|
||||
}
|
||||
}
|
||||
|
||||
class PlaySoundEffect(
|
||||
val sound: ResourceLocation,
|
||||
val soundSource: SoundSource?,
|
||||
val playAround: Boolean = true,
|
||||
val distance: Double = 64.0,
|
||||
val volume: Float = 1.0F,
|
||||
val pitch: Float = 1.0F
|
||||
) : InteractionEffect {
|
||||
class PlaySoundEffect(val sound: ResourceLocation, val soundSource: SoundSource?, 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
|
||||
) {
|
||||
val packet = UnvalidatedPlaySoundS2CPacket(
|
||||
sound,
|
||||
soundSource ?: SoundSource.NEUTRAL,
|
||||
pokemon.x,
|
||||
pokemon.y,
|
||||
pokemon.z,
|
||||
volume,
|
||||
pitch
|
||||
)
|
||||
val packet = UnvalidatedPlaySoundS2CPacket(sound, soundSource ?: SoundSource.NEUTRAL, pokemon.x, pokemon.y, pokemon.z, volume, pitch)
|
||||
if (playAround) {
|
||||
packet.sendToPlayersAround(pokemon.x, pokemon.y, pokemon.z, distance, pokemon.level().dimension())
|
||||
packet.sendToPlayersAround( pokemon.x, pokemon.y, pokemon.z, distance, pokemon.level().dimension())
|
||||
} else {
|
||||
packet.sendToPlayer(player)
|
||||
}
|
||||
@@ -173,7 +104,7 @@ class PlaySoundEffect(
|
||||
}
|
||||
}
|
||||
|
||||
class ShrinkItemEffect(val amount: Int = 1) : InteractionEffect {
|
||||
class ShrinkItemEffect(val amount: Int = 1): InteractionEffect {
|
||||
override fun applyEffect(
|
||||
pokemon: PokemonEntity,
|
||||
player: ServerPlayer
|
||||
@@ -189,7 +120,7 @@ class ShrinkItemEffect(val amount: Int = 1) : InteractionEffect {
|
||||
}
|
||||
}
|
||||
|
||||
class ScriptEffect(val script: ExpressionLike) : InteractionEffect {
|
||||
class ScriptEffect(val script: ExpressionLike): InteractionEffect {
|
||||
override fun applyEffect(
|
||||
pokemon: PokemonEntity,
|
||||
player: ServerPlayer
|
||||
|
||||
@@ -311,7 +311,7 @@ object MoLangFunctions {
|
||||
runtime.environment.variable = params.environment.variable
|
||||
val args = params.params.subList(1, params.params.size)
|
||||
runtime.environment.context = ContextStruct(
|
||||
(params.environment.context?.map ?: emptyMap()) + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
|
||||
params.environment.context.map + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
|
||||
)
|
||||
val script = params.getString(0).asIdentifierDefaultingNamespace()
|
||||
// store the args in the
|
||||
@@ -779,20 +779,6 @@ object MoLangFunctions {
|
||||
battleStartResult.ifSuccessful { returnValue = it.struct }
|
||||
return@put returnValue
|
||||
}
|
||||
map.put("get_custom_stat") { params ->
|
||||
val statName = params.getString(0)
|
||||
|
||||
val resourceLocation = ResourceLocation.tryParse(statName) ?: return@put DoubleValue.ZERO
|
||||
val statResourceLocation = BuiltInRegistries.CUSTOM_STAT.get(resourceLocation) ?: return@put DoubleValue.ZERO
|
||||
|
||||
val exists = net.minecraft.stats.Stats.CUSTOM.contains(statResourceLocation)
|
||||
if (!exists) return@put DoubleValue.ZERO
|
||||
|
||||
val stat = net.minecraft.stats.Stats.CUSTOM.get(statResourceLocation)
|
||||
val value = player.stats.getValue(stat)
|
||||
|
||||
DoubleValue(value)
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
@@ -1753,35 +1739,22 @@ object MoLangFunctions {
|
||||
pokemon.removeCosmeticItem()
|
||||
}
|
||||
map.put("add_marks") { params ->
|
||||
var appliedMark = false
|
||||
for (param in params.params) {
|
||||
val identifier = param.asString().asIdentifierDefaultingNamespace()
|
||||
val mark = Marks.getByIdentifier(identifier)
|
||||
if (mark != null) {
|
||||
pokemon.exchangeMark(mark, true)
|
||||
appliedMark = true
|
||||
}
|
||||
if (mark != null) pokemon.exchangeMark(mark, true)
|
||||
}
|
||||
|
||||
DoubleValue(appliedMark)
|
||||
}
|
||||
map.put("add_marks_with_chance") { params ->
|
||||
var appliedMark = false
|
||||
for (param in params.params) {
|
||||
val identifier = param.asString().asIdentifierDefaultingNamespace()
|
||||
val mark = Marks.getByIdentifier(identifier)
|
||||
|
||||
mark?.let {
|
||||
val probability = it.chance.coerceIn(0F, 1F) * 100
|
||||
val randomValue = Random.nextDouble(0.0, 100.0)
|
||||
if (randomValue < probability) {
|
||||
pokemon.exchangeMark(it, true)
|
||||
appliedMark = true
|
||||
}
|
||||
if (randomValue < probability) pokemon.exchangeMark(it, true)
|
||||
}
|
||||
}
|
||||
|
||||
DoubleValue(appliedMark)
|
||||
}
|
||||
map.put("add_potential_marks") { params ->
|
||||
for (param in params.params) {
|
||||
|
||||
+8
-10
@@ -80,9 +80,7 @@ open class ChoiceSpeciesFeatureProvider(
|
||||
|
||||
internal constructor(): this(emptyList())
|
||||
|
||||
override fun get(pokemon: Pokemon): StringSpeciesFeature? {
|
||||
return pokemon.features.filterIsInstance<StringSpeciesFeature>().find { it.name in keys }
|
||||
}
|
||||
override fun get(pokemon: Pokemon) = pokemon.getFeature<StringSpeciesFeature>(keys.first())
|
||||
|
||||
override fun invoke(pokemon: Pokemon): StringSpeciesFeature? {
|
||||
val existing = get(pokemon)
|
||||
@@ -103,15 +101,15 @@ open class ChoiceSpeciesFeatureProvider(
|
||||
}
|
||||
|
||||
override fun invoke(nbt: CompoundTag): StringSpeciesFeature? {
|
||||
val key = keys.find { nbt.contains(it) }
|
||||
if (key == null) return null
|
||||
return StringSpeciesFeature(key, "").also { it.loadFromNBT(nbt) }
|
||||
return if (nbt.contains(keys.first())) {
|
||||
StringSpeciesFeature(keys.first(), "").also { it.loadFromNBT(nbt) }
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun invoke(json: JsonObject): StringSpeciesFeature? {
|
||||
val key = keys.find { json.has(it) }
|
||||
if (key == null) return null
|
||||
return StringSpeciesFeature(key, "").also { it.loadFromJSON(json) }
|
||||
return if (json.has(keys.first())) {
|
||||
StringSpeciesFeature(keys.first(), "").also { it.loadFromJSON(json) }
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun fromString(value: String?): StringSpeciesFeature? {
|
||||
@@ -133,7 +131,7 @@ open class ChoiceSpeciesFeatureProvider(
|
||||
|
||||
override fun provide(properties: PokemonProperties): Set<String> {
|
||||
return if (isAspect) {
|
||||
val feature = properties.customProperties.filterIsInstance<StringSpeciesFeature>().find { it.name in keys }
|
||||
val feature = properties.customProperties.filterIsInstance<StringSpeciesFeature>().find { it.name == keys.first() }
|
||||
if (feature != null) {
|
||||
setOf(getAspect(feature))
|
||||
} else {
|
||||
|
||||
+11
-23
@@ -132,9 +132,7 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
|
||||
|
||||
constructor(vararg keys: String) : this(keys.toList())
|
||||
|
||||
override fun get(pokemon: Pokemon): FlagSpeciesFeature? {
|
||||
return pokemon.features.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
|
||||
}
|
||||
override fun get(pokemon: Pokemon) = pokemon.getFeature<FlagSpeciesFeature>(keys.first())
|
||||
|
||||
override fun invoke(pokemon: Pokemon): FlagSpeciesFeature? {
|
||||
return get(pokemon)
|
||||
@@ -146,15 +144,15 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
|
||||
}
|
||||
|
||||
override fun invoke(nbt: CompoundTag): FlagSpeciesFeature? {
|
||||
val key = keys.find { nbt.contains(it) }
|
||||
if (key == null) return null
|
||||
return FlagSpeciesFeature(key, false).also { it.loadFromNBT(nbt) }
|
||||
return if (nbt.contains(keys.first())) {
|
||||
FlagSpeciesFeature(keys.first(), false).also { it.loadFromNBT(nbt) }
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun invoke(json: JsonObject): FlagSpeciesFeature? {
|
||||
val key = keys.find { json.has(it) }
|
||||
if (key == null) return null
|
||||
return FlagSpeciesFeature(key, false).also { it.loadFromJSON(json) }
|
||||
return if (json.has(keys.first())) {
|
||||
FlagSpeciesFeature(keys.first(), false).also { it.loadFromJSON(json) }
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun fromString(value: String?): FlagSpeciesFeature? {
|
||||
@@ -172,26 +170,16 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
|
||||
}
|
||||
|
||||
override fun provide(pokemon: Pokemon): Set<String> {
|
||||
return if (isAspect) {
|
||||
val feature = pokemon.features.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
|
||||
if (feature != null && feature.enabled) {
|
||||
setOf(keys.first())
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
return if (isAspect && pokemon.getFeature<FlagSpeciesFeature>(keys.first())?.enabled == true) {
|
||||
setOf(keys.first())
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
override fun provide(properties: PokemonProperties): Set<String> {
|
||||
return if (isAspect) {
|
||||
val feature = properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
|
||||
if (feature != null && feature.enabled) {
|
||||
setOf(keys.first())
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
return if (isAspect && properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name == keys.first() }?.enabled == true) {
|
||||
setOf(keys.first())
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
+10
-6
@@ -142,15 +142,19 @@ class IntSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<IntSpeciesF
|
||||
}
|
||||
|
||||
override fun invoke(nbt: CompoundTag): IntSpeciesFeature? {
|
||||
val key = keys.find { nbt.contains(it) }
|
||||
if (key == null) return null
|
||||
return IntSpeciesFeature(keys.first(), nbt.getInt(key));
|
||||
return if (nbt.contains(keys.first())) {
|
||||
IntSpeciesFeature(keys.first(), nbt.getInt(keys.first()))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun invoke(json: JsonObject): IntSpeciesFeature? {
|
||||
val key = keys.find { json.has(it) }
|
||||
if (key == null) return null
|
||||
return IntSpeciesFeature(keys.first(), json.get(key).asInt)
|
||||
return if (json.has(keys.first())) {
|
||||
IntSpeciesFeature(keys.first(), json.get(keys.first()).asInt)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(pokemon: Pokemon) = pokemon.features.filterIsInstance<IntSpeciesFeature>().find { it.name in keys }
|
||||
|
||||
+4
-5
@@ -12,7 +12,6 @@ import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.player.Player
|
||||
|
||||
/**
|
||||
* A [Requirement] that expects a [LivingEntity] to be attached to the [Pokemon].
|
||||
@@ -27,7 +26,7 @@ interface OwnerQueryRequirement : Requirement {
|
||||
override fun check(pokemon: Pokemon): Boolean {
|
||||
val owner = pokemon.getOwnerEntity() ?: return false
|
||||
return when (owner) {
|
||||
is Player -> {
|
||||
is ServerPlayer -> {
|
||||
this.checkPlayer(pokemon, owner)
|
||||
}
|
||||
is NPCEntity -> {
|
||||
@@ -38,13 +37,13 @@ interface OwnerQueryRequirement : Requirement {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given [Pokemon] & [Player] satisfies the requirement.
|
||||
* Checks if the given [Pokemon] & [ServerPlayer] satisfies the requirement.
|
||||
*
|
||||
* @param pokemon The [Pokemon] being queried.
|
||||
* @param owner The [Player] that owns this [Pokemon].
|
||||
* @param owner The [ServerPlayer] that owns this [Pokemon].
|
||||
* @return If the requirement was satisfied.
|
||||
*/
|
||||
fun checkPlayer(pokemon: Pokemon, owner: Player): Boolean
|
||||
fun checkPlayer(pokemon: Pokemon, owner: ServerPlayer): Boolean
|
||||
|
||||
/**
|
||||
* Checks if the given [Pokemon] & [NPCEntity] satisfies the requirement.
|
||||
|
||||
@@ -12,7 +12,6 @@ package com.cobblemon.mod.common.api.riding.sound
|
||||
import com.cobblemon.mod.common.api.riding.behaviour.types.composite.CompositeBehaviour
|
||||
import com.cobblemon.mod.common.api.riding.behaviour.types.composite.CompositeState
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.getIsSubmerged
|
||||
import com.cobblemon.mod.common.util.resolveDouble
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance
|
||||
@@ -66,21 +65,6 @@ class RideLoopSound(val ride: PokemonEntity, val soundSettings: RideSoundSetting
|
||||
return
|
||||
}
|
||||
|
||||
// Mute the sound if it's not meant to play for the passenger
|
||||
if (this.isPassenger && !this.soundSettings.playForPassengers) {
|
||||
this.volume = 0.0f
|
||||
return
|
||||
}
|
||||
|
||||
// Mute sounds when submerged unless they support underwater sounds, underwater sounds don't play above water
|
||||
if (this.ride.getIsSubmerged() && !this.soundSettings.submerged) {
|
||||
this.volume = 0.0f
|
||||
return
|
||||
} else if (!this.ride.getIsSubmerged() && this.soundSettings.submerged) {
|
||||
this.volume = 0.0f
|
||||
return
|
||||
}
|
||||
|
||||
// Perform data driven pitch and volume calculation
|
||||
this.volume = ((soundSettings.volumeExpr.let { ride.runtime.resolveDouble(it) })).toFloat()
|
||||
this.volume *= this.fade
|
||||
|
||||
-8
@@ -27,9 +27,7 @@ data class RideSoundSettings(
|
||||
val soundLocation: ResourceLocation,
|
||||
@SerializedName("volumeExpr") private val _volumeExpr: Expression? = "1.0".asExpression(),
|
||||
@SerializedName("pitchExpr") private val _pitchExpr: Expression? = "1.0".asExpression(),
|
||||
val playForPassengers: Boolean = true,
|
||||
val playForNonPassengers: Boolean = false,
|
||||
val submerged: Boolean = false,
|
||||
val muffleEnabled: Boolean = false,
|
||||
@SerializedName("attenuationModel") private val _attenuationModel: RideAttenuationModel? = RideAttenuationModel.NONE
|
||||
): Encodable {
|
||||
@@ -48,9 +46,7 @@ data class RideSoundSettings(
|
||||
buffer.writeResourceLocation(soundLocation)
|
||||
buffer.writeExpression(volumeExpr)
|
||||
buffer.writeExpression(pitchExpr)
|
||||
buffer.writeBoolean(playForPassengers)
|
||||
buffer.writeBoolean(playForNonPassengers)
|
||||
buffer.writeBoolean(submerged)
|
||||
buffer.writeBoolean(muffleEnabled)
|
||||
buffer.writeEnum(attenuationModel)
|
||||
}
|
||||
@@ -61,9 +57,7 @@ data class RideSoundSettings(
|
||||
val soundLocation = buffer.readResourceLocation()
|
||||
val volumeExpr = buffer.readExpression()
|
||||
val pitchExpr = buffer.readExpression()
|
||||
val playForPassengers = buffer.readBoolean()
|
||||
val playForNonPassengers = buffer.readBoolean()
|
||||
val submerged = buffer.readBoolean()
|
||||
val muffleEnabled = buffer.readBoolean()
|
||||
val attenuationModel = buffer.readEnum(RideAttenuationModel::class.java)
|
||||
|
||||
@@ -71,9 +65,7 @@ data class RideSoundSettings(
|
||||
soundLocation,
|
||||
volumeExpr,
|
||||
pitchExpr,
|
||||
playForPassengers,
|
||||
playForNonPassengers,
|
||||
submerged,
|
||||
muffleEnabled,
|
||||
attenuationModel
|
||||
)
|
||||
|
||||
@@ -8,58 +8,30 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.stats
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.stats.StatFormatter
|
||||
|
||||
object CobblemonStats {
|
||||
val stats = mutableMapOf<String, CobblemonStat>()
|
||||
val stats = mutableMapOf<String, ResourceLocation>()
|
||||
|
||||
@JvmField
|
||||
var CAPTURED: CobblemonStat = CobblemonStat("captured")
|
||||
@JvmField
|
||||
val SHINIES_CAPTURED: CobblemonStat = CobblemonStat("shinies_captured")
|
||||
@JvmField
|
||||
val RELEASED: CobblemonStat = CobblemonStat("released")
|
||||
@JvmField
|
||||
val EVOLVED: CobblemonStat = CobblemonStat("evolved")
|
||||
@JvmField
|
||||
val LEVEL_UP: CobblemonStat = CobblemonStat("level_up")
|
||||
@JvmField
|
||||
val BATTLES_WON: CobblemonStat = CobblemonStat("battles_won")
|
||||
@JvmField
|
||||
val BATTLES_LOST: CobblemonStat = CobblemonStat("battles_lost")
|
||||
@JvmField
|
||||
val BATTLES_FLED: CobblemonStat = CobblemonStat("battles_fled")
|
||||
@JvmField
|
||||
val BATTLES_TOTAL: CobblemonStat = CobblemonStat("battles_total")
|
||||
@JvmField
|
||||
val DEX_ENTRIES: CobblemonStat = CobblemonStat("dex_entries")
|
||||
@JvmField
|
||||
val EGGS_COLLECTED: CobblemonStat = CobblemonStat("eggs_collected")
|
||||
@JvmField
|
||||
val EGGS_HATCHED: CobblemonStat = CobblemonStat("eggs_hatched")
|
||||
@JvmField
|
||||
val TRADED: CobblemonStat = CobblemonStat("traded")
|
||||
@JvmField
|
||||
val FOSSILS_REVIVED: CobblemonStat = CobblemonStat("fossils_revived")
|
||||
@JvmField
|
||||
val TIMES_RIDDEN: CobblemonStat = CobblemonStat("times_ridden")
|
||||
@JvmField
|
||||
val ROD_CASTS: CobblemonStat = CobblemonStat("rod_casts")
|
||||
@JvmField
|
||||
val REEL_INS: CobblemonStat = CobblemonStat("reel_ins")
|
||||
@JvmField
|
||||
val POKEMON_INTERACTED_WITH: CobblemonStat = CobblemonStat("pokemon_interacted_with")
|
||||
@JvmField
|
||||
val RIDING_LAND: CobblemonStat = CobblemonStat("riding_land", StatFormatter.DISTANCE)
|
||||
@JvmField
|
||||
val RIDING_AIR: CobblemonStat = CobblemonStat("riding_air", StatFormatter.DISTANCE)
|
||||
@JvmField
|
||||
val RIDING_LIQUID: CobblemonStat = CobblemonStat("riding_liquid", StatFormatter.DISTANCE)
|
||||
|
||||
//TODO block, stats (interact, gimmi)
|
||||
var CAPTURED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "captured") //makeCustomStat("captured", StatFormatter.DEFAULT)
|
||||
val SHINIES_CAPTURED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "shinies_captured") //makeCustomStat("shinies_captured", StatFormatter.DEFAULT)
|
||||
val RELEASED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "released") //makeCustomStat("released", StatFormatter.DEFAULT)
|
||||
val EVOLVED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "evolved") //makeCustomStat("evolved", StatFormatter.DEFAULT)
|
||||
val LEVEL_UP: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "level_up") //makeCustomStat("level_up", StatFormatter.DEFAULT)
|
||||
val BATTLES_WON: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "battles_won") //makeCustomStat("battles_won", StatFormatter.DEFAULT)
|
||||
val BATTLES_LOST: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "battles_lost") //makeCustomStat("battles_lost", StatFormatter.DEFAULT)
|
||||
val BATTLES_FLED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "battles_fled") //makeCustomStat("battles_fled", StatFormatter.DEFAULT)
|
||||
val BATTLES_TOTAL: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "battles_total") //makeCustomStat("battles_total", StatFormatter.DEFAULT)
|
||||
val DEX_ENTRIES: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "dex_entries") //makeCustomStat("dex_entries", StatFormatter.DEFAULT)
|
||||
val EGGS_COLLECTED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "eggs_collected") //makeCustomStat("eggs_collected", StatFormatter.DEFAULT)
|
||||
val EGGS_HATCHED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "eggs_hatched") //makeCustomStat("eggs_hatched", StatFormatter.DEFAULT)
|
||||
val TRADED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "traded") //makeCustomStat("traded", StatFormatter.DEFAULT)
|
||||
val FOSSILS_REVIVED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "fossils_revived") //makeCustomStat("fossils_revived", StatFormatter.DEFAULT)
|
||||
val TIMES_RIDDEN: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "times_ridden")
|
||||
val ROD_CASTS: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "rod_casts")
|
||||
val REEL_INS: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "reel_ins")
|
||||
val POKEMON_INTERACTED_WITH: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "pokemon_interacted_with") //makeCustomStat("pokemon_interacted_with", StatFormatter.DEFAULT)
|
||||
//TODO block, stats (interact, gimmi), riding (styles?)
|
||||
|
||||
fun registerStats() {
|
||||
stats["captured"] = CAPTURED
|
||||
@@ -80,22 +52,5 @@ object CobblemonStats {
|
||||
stats["rod_casts"] = ROD_CASTS
|
||||
stats["reel_ins"] = REEL_INS
|
||||
stats["pokemon_interacted_with"] = POKEMON_INTERACTED_WITH
|
||||
stats["riding_land"] = RIDING_LAND
|
||||
stats["riding_air"] = RIDING_AIR
|
||||
stats["riding_liquid"] = RIDING_LIQUID
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getStat(cobblemonStat: CobblemonStat) : ResourceLocation {
|
||||
val resourceLocation = cobblemonStat.resourceLocation
|
||||
val stat = BuiltInRegistries.CUSTOM_STAT.get(resourceLocation)
|
||||
if (stat == null) {
|
||||
Cobblemon.LOGGER.error("Could not find stat with id {}", resourceLocation)
|
||||
}
|
||||
return stat ?: throw NullPointerException("Could not find stat with id $resourceLocation")
|
||||
}
|
||||
|
||||
data class CobblemonStat(val path: String, val formatter: StatFormatter = StatFormatter.DEFAULT) {
|
||||
val resourceLocation: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,671 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
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
|
||||
}
|
||||
-483
@@ -1,483 +0,0 @@
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
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"
|
||||
)
|
||||
}
|
||||
-737
@@ -1,737 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
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
|
||||
}
|
||||
-340
@@ -1,340 +0,0 @@
|
||||
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
|
||||
)
|
||||
-324
@@ -1,324 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
-403
@@ -1,403 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
+1
@@ -31,6 +31,7 @@ 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")
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
package com.cobblemon.mod.common.client
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.Cobblemon.LOGGER
|
||||
import com.cobblemon.mod.common.Cobblemon.cobblemonVR
|
||||
import com.cobblemon.mod.common.CobblemonBlockEntities
|
||||
import com.cobblemon.mod.common.CobblemonBlocks
|
||||
import com.cobblemon.mod.common.CobblemonClientImplementation
|
||||
@@ -27,6 +29,7 @@ import com.cobblemon.mod.common.client.gui.PartyOverlayDataControl
|
||||
import com.cobblemon.mod.common.client.gui.RideControlsOverlay
|
||||
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
|
||||
import com.cobblemon.mod.common.client.gui.cookingpot.CookingPotScreen
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.CobblemonVRClient
|
||||
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
|
||||
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
|
||||
import com.cobblemon.mod.common.client.render.DeferredRenderer
|
||||
@@ -72,6 +75,7 @@ import com.cobblemon.mod.common.client.tooltips.TooltipManager
|
||||
import com.cobblemon.mod.common.client.trade.ClientTrade
|
||||
import com.cobblemon.mod.common.entity.boat.CobblemonBoatType
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.integration.vivecraft.CobblemonVR
|
||||
import com.cobblemon.mod.common.platform.events.PlatformEvents
|
||||
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext
|
||||
import com.cobblemon.mod.common.util.isLookingAt
|
||||
@@ -152,6 +156,10 @@ object CobblemonClient {
|
||||
this.registerTooltipManagers()
|
||||
this.registerMenuScreens()
|
||||
|
||||
if (Cobblemon.implementation.isModInstalled("vivecraft")) { // VR integration
|
||||
CobblemonVRClient.init()
|
||||
}
|
||||
|
||||
LOGGER.info("Registering custom BuiltinItemRenderers")
|
||||
CobblemonBuiltinItemRendererRegistry.register(CobblemonItems.POKEMON_MODEL, PokemonItemRenderer())
|
||||
|
||||
@@ -187,7 +195,9 @@ object CobblemonClient {
|
||||
|
||||
nearbyPokemon?.forEach { entity ->
|
||||
if (entity is PokemonEntity && !entity.isSilent && !entity.passengers.contains(player)) {
|
||||
if (player.isLookingAt(entity) && !player.isSpectator && entity.pokemon.shiny) entity.delegate.spawnShinyParticle(player)
|
||||
if (player.isLookingAt(entity) && !player.isSpectator && entity.pokemon.shiny) entity.delegate.spawnShinyParticle(
|
||||
player
|
||||
)
|
||||
entity.delegate.spawnAspectParticle()
|
||||
}
|
||||
}
|
||||
@@ -369,7 +379,10 @@ object CobblemonClient {
|
||||
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.DISPLAY_CASE, ::DisplayCaseRenderer)
|
||||
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.LECTERN, ::LecternBlockEntityRenderer)
|
||||
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.CAMPFIRE, ::CampfireBlockEntityRenderer)
|
||||
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.POKE_SNACK, ::PokeSnackBlockEntityRenderer)
|
||||
this.implementation.registerBlockEntityRenderer(
|
||||
CobblemonBlockEntities.POKE_SNACK,
|
||||
::PokeSnackBlockEntityRenderer
|
||||
)
|
||||
}
|
||||
|
||||
private fun registerEntityRenderers() {
|
||||
@@ -398,7 +411,11 @@ object CobblemonClient {
|
||||
|
||||
private fun registerItemColors() {
|
||||
implementation.registerItemColors(AprijuiceItemColorProvider, *CobblemonItems.aprijuices.toTypedArray())
|
||||
implementation.registerItemColors(PokeSnackItemColorProvider, CobblemonItems.POKE_SNACK, CobblemonItems.POKE_CAKE)
|
||||
implementation.registerItemColors(
|
||||
PokeSnackItemColorProvider,
|
||||
CobblemonItems.POKE_SNACK,
|
||||
CobblemonItems.POKE_CAKE
|
||||
)
|
||||
implementation.registerItemColors(PokeBaitItemColorProvider, CobblemonItems.POKE_BAIT)
|
||||
implementation.registerItemColors(PonigiriItemColorProvider, CobblemonItems.PONIGIRI)
|
||||
implementation.registerItemColors(SinisterTeaItemColorProvider, CobblemonItems.SINISTER_TEA)
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft
|
||||
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.party.PartyOnArm
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.proximity.ProximityManager
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.proximity.ProximityRegistration
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.listener.proximity.PokeRodBaitListener
|
||||
import org.vivecraft.api.data.VRBodyPart
|
||||
|
||||
class CobblemonVRClient {
|
||||
|
||||
companion object {
|
||||
fun init() {
|
||||
PartyOnArm.init()
|
||||
ProximityManager.init()
|
||||
ProximityManager.register(
|
||||
ProximityRegistration(
|
||||
bodyA = VRBodyPart.MAIN_HAND,
|
||||
bodyB = VRBodyPart.OFF_HAND,
|
||||
distanceThreshold = 0.25f,
|
||||
angleThreshold = 120f,
|
||||
listener = PokeRodBaitListener("Main-OffHand")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft.listener.proximity
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonNetwork
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.proximity.ProximityListener
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.proximity.ProximityRegistration
|
||||
import com.cobblemon.mod.common.net.messages.server.pokerod.ApplyBaitPacket
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import org.vivecraft.api.data.VRBodyPart
|
||||
import org.vivecraft.api.data.VRPose
|
||||
|
||||
class PokeRodBaitListener(private val name: String) : ProximityListener {
|
||||
//TODO: Also add that if 1 is Rod the other needs to be seasoning
|
||||
override fun isActive(registration: ProximityRegistration): Boolean {
|
||||
val player = Minecraft.getInstance().player ?: return false
|
||||
val handA: InteractionHand = VRBodyPart.toInteractionHand(registration.bodyA) ?: return false
|
||||
val itemStackA: ItemStack = player.getItemInHand(handA)
|
||||
|
||||
val handB: InteractionHand = VRBodyPart.toInteractionHand(registration.bodyB) ?: return false
|
||||
val itemStackB: ItemStack = player.getItemInHand(handB)
|
||||
|
||||
return itemStackA.`is`(CobblemonItemTags.POKE_RODS) || itemStackB.`is`(CobblemonItemTags.POKE_RODS)
|
||||
}
|
||||
|
||||
override fun onEnter(bodyA: VRBodyPart, bodyB: VRBodyPart, pose: VRPose) {
|
||||
Cobblemon.LOGGER.info("[$name] Enter: $bodyA <-> $bodyB")
|
||||
}
|
||||
|
||||
override fun onExit(bodyA: VRBodyPart, bodyB: VRBodyPart, pose: VRPose) {
|
||||
Cobblemon.LOGGER.info("[$name] Exit: $bodyA <-> $bodyB")
|
||||
}
|
||||
|
||||
override fun onTick(bodyA: VRBodyPart, bodyB: VRBodyPart, pose: VRPose) {
|
||||
Cobblemon.LOGGER.info("[$name] Proximity: $bodyA <-> $bodyB")
|
||||
CobblemonNetwork.sendToServer(ApplyBaitPacket())
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft.listener.swing
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.vivecraft.api.client.VRClientAPI
|
||||
import org.vivecraft.api.client.tracker.swing.SwingContext
|
||||
import org.vivecraft.api.client.tracker.swing.SwingListener
|
||||
import org.vivecraft.api.data.VRBodyPart
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Listener that classifies directional swings (up, down, forward, backward, left, right)
|
||||
* using Vivecraft VR pose history and invokes handler methods for each direction.
|
||||
*
|
||||
* @param forwardDotThreshold Dot product threshold used to classify forward/back/left/right directions.
|
||||
* @param historyTicks Number of ticks used to compute velocity from historical poses.
|
||||
*/
|
||||
abstract class DirectionalSwingListener(
|
||||
private val forwardDotThreshold: Double = 0.3,
|
||||
private val historyTicks: Int = 10
|
||||
) : SwingListener {
|
||||
|
||||
protected val minecraft: Minecraft = Minecraft.getInstance()
|
||||
protected val clientData: VRClientAPI = VRClientAPI.instance()
|
||||
|
||||
/**
|
||||
* Compute the net movement vector for the specified body part over the given number of ticks.
|
||||
* Returns null if historical pose data is unavailable, insufficient, or movement is negligible.
|
||||
*
|
||||
* @param bodyPart The VR body part to compute movement for.
|
||||
* @param ticks The number of ticks of history to consider.
|
||||
* @return The movement vector or null if unavailable/insignificant.
|
||||
*/
|
||||
private fun changeForVelocity(bodyPart: VRBodyPart, ticks: Int = historyTicks): Vec3? {
|
||||
val history = clientData.historicalVRPoses ?: return null
|
||||
if (history.ticksOfHistory() < ticks) return null
|
||||
|
||||
val delta = history.netMovement(bodyPart, ticks)
|
||||
delta?.lengthSqr()?.let { if (it < 1e-6) return null }
|
||||
|
||||
return delta
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an upward swing is detected.
|
||||
*
|
||||
* @param useHand The hand used for the swing.
|
||||
* @param handVel The velocity vector of the hand during the swing.
|
||||
* @param player The player performing the swing.
|
||||
* @return True if the upward action was handled, false otherwise.
|
||||
*/
|
||||
protected open fun onUpwardSwing(
|
||||
useHand: InteractionHand,
|
||||
player: Player
|
||||
): Boolean {
|
||||
Cobblemon.LOGGER.info(
|
||||
"Ignoring swing: upward movement. player={}, hand={}",
|
||||
player, useHand
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a downward swing is detected.
|
||||
*
|
||||
* @param useHand The hand used for the swing.
|
||||
* @param handVel The velocity vector of the hand during the swing.
|
||||
* @param player The player performing the swing.
|
||||
* @return True if the downward action was handled, false otherwise.
|
||||
*/
|
||||
protected open fun onDownwardSwing(
|
||||
useHand: InteractionHand,
|
||||
handVel: Vec3,
|
||||
player: Player
|
||||
): Boolean {
|
||||
Cobblemon.LOGGER.info(
|
||||
"Ignoring swing: downward movement. handVel={}, player={}, hand={}",
|
||||
handVel, player, useHand
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a forward swing is detected.
|
||||
*
|
||||
* @param useHand The hand used for the swing.
|
||||
* @param handVel The velocity vector of the hand during the swing.
|
||||
* @param player The player performing the swing.
|
||||
* @return True if the forward action was handled, false otherwise.
|
||||
*/
|
||||
protected open fun onForwardSwing(
|
||||
useHand: InteractionHand,
|
||||
handVel: Vec3,
|
||||
player: Player
|
||||
): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a backward swing is detected.
|
||||
*
|
||||
* @param useHand The hand used for the swing.
|
||||
* @param handVel The velocity vector of the hand during the swing.
|
||||
* @param player The player performing the swing.
|
||||
* @return True if the backward action was handled, false otherwise.
|
||||
*/
|
||||
protected open fun onBackwardSwing(
|
||||
useHand: InteractionHand,
|
||||
handVel: Vec3,
|
||||
player: Player
|
||||
): Boolean {
|
||||
Cobblemon.LOGGER.info(
|
||||
"Ignoring swing: backward movement. handVel={}, player={}, hand={}",
|
||||
handVel, player, useHand
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a leftward swing is detected.
|
||||
*
|
||||
* @param useHand The hand used for the swing.
|
||||
* @param handVel The velocity vector of the hand during the swing.
|
||||
* @param player The player performing the swing.
|
||||
* @return True if the leftward action was handled, false otherwise.
|
||||
*/
|
||||
protected open fun onLeftSwing(
|
||||
useHand: InteractionHand,
|
||||
handVel: Vec3,
|
||||
player: Player
|
||||
): Boolean {
|
||||
Cobblemon.LOGGER.info(
|
||||
"Ignoring swing: leftward movement. handVel={}, player={}, hand={}",
|
||||
handVel, player, useHand
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a rightward swing is detected.
|
||||
*
|
||||
* @param useHand The hand used for the swing.
|
||||
* @param handVel The velocity vector of the hand during the swing.
|
||||
* @param player The player performing the swing.
|
||||
* @return True if the rightward action was handled, false otherwise.
|
||||
*/
|
||||
protected open fun onRightSwing(
|
||||
useHand: InteractionHand,
|
||||
handVel: Vec3,
|
||||
player: Player
|
||||
): Boolean {
|
||||
Cobblemon.LOGGER.info(
|
||||
"Ignoring swing: rightward movement. handVel={}, player={}, hand={}",
|
||||
handVel, player, useHand
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the given item stack is valid for processing by this swing listener.
|
||||
* Subclasses must implement this to specify which items they handle.
|
||||
* @param itemStack The item stack to validate.
|
||||
* @return True if the item stack is valid, false otherwise.
|
||||
*/
|
||||
protected abstract fun isValidItem(itemStack: ItemStack): Boolean
|
||||
|
||||
/**
|
||||
* Called when a swing ends. Validates the held item, computes hand velocity from historical poses,
|
||||
* handles vertical swings first (upward/downward) and then classifies horizontal direction:
|
||||
* forward/backward by dot with player look direction; left/right by projecting onto a right vector
|
||||
* computed from world up and forward. Ambiguous swings are ignored.
|
||||
*
|
||||
* @param context Vivecraft swing context containing body part information.
|
||||
*/
|
||||
override fun onSwingEnd(context: SwingContext?) {
|
||||
val player = minecraft.player ?: return
|
||||
val ctx = context ?: return
|
||||
val useHand: InteractionHand = VRBodyPart.toInteractionHand(ctx.bodyPart) ?: return
|
||||
val itemStack: ItemStack = player.getItemInHand(useHand)
|
||||
|
||||
if (!isValidItem(itemStack)) return
|
||||
|
||||
val handVel = changeForVelocity(ctx.bodyPart) ?: run {
|
||||
Cobblemon.LOGGER.info(
|
||||
"Ignoring swing: insufficient historical pose data for velocity. bodyPart={}, player={}",
|
||||
ctx.bodyPart, player
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val vertical = handVel.y
|
||||
if (vertical > 0.0) {
|
||||
if (onUpwardSwing(useHand, player)) return
|
||||
} else if (vertical < 0.0) {
|
||||
if (onDownwardSwing(useHand, handVel, player)) return
|
||||
}
|
||||
|
||||
val forward = player.lookAngle.normalize()
|
||||
val handDir = handVel.normalize()
|
||||
val forwardDot = handDir.dot(forward)
|
||||
|
||||
val worldUp = Vec3(0.0, 1.0, 0.0)
|
||||
val right = forward.cross(worldUp).let {
|
||||
if (it.lengthSqr() < 1e-6) Vec3(1.0, 0.0, 0.0) else it.normalize()
|
||||
}
|
||||
val rightDot = handDir.dot(right)
|
||||
|
||||
if (forwardDot > forwardDotThreshold) {
|
||||
if (onForwardSwing(useHand, handVel, player)) return
|
||||
return
|
||||
}
|
||||
|
||||
if (forwardDot < -forwardDotThreshold) {
|
||||
if (onBackwardSwing(useHand, handVel, player)) return
|
||||
return
|
||||
}
|
||||
|
||||
if (abs(rightDot) > forwardDotThreshold) {
|
||||
if (rightDot > 0.0) {
|
||||
if (onRightSwing(useHand, handVel, player)) return
|
||||
} else {
|
||||
if (onLeftSwing(useHand, handVel, player)) return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Cobblemon.LOGGER.info(
|
||||
"Ignoring swing: ambiguous direction. forwardDot={}, rightDot={}, handVel={}, player={}",
|
||||
forwardDot, rightDot, handVel, player
|
||||
)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft.listener.swing
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonNetwork
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeball.SendOutPokeBallPacket
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class PokeBallSwingListener : DirectionalSwingListener() {
|
||||
|
||||
override fun isValidItem(itemStack: ItemStack): Boolean =
|
||||
itemStack.`is`(CobblemonItemTags.POKE_BALLS)
|
||||
|
||||
override fun onForwardSwing(
|
||||
useHand: InteractionHand,
|
||||
handVel: Vec3,
|
||||
player: Player
|
||||
): Boolean {
|
||||
CobblemonNetwork.sendToServer(SendOutPokeBallPacket())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDownwardSwing(useHand: InteractionHand, handVel: Vec3, player: Player): Boolean {
|
||||
CobblemonNetwork.sendToServer(SendOutPokeBallPacket())
|
||||
return super.onDownwardSwing(useHand, handVel, player)
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft.listener.swing
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonNetwork
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.net.messages.server.pokerod.CastPokeRodPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokerod.ReelPokeRodPacket
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class PokeRodSwingListener : DirectionalSwingListener() {
|
||||
|
||||
override fun isValidItem(itemStack: ItemStack): Boolean =
|
||||
itemStack.`is`(CobblemonItemTags.POKE_RODS)
|
||||
|
||||
override fun onUpwardSwing(useHand: InteractionHand, player: Player): Boolean {
|
||||
CobblemonNetwork.sendToServer(ReelPokeRodPacket(useHand == InteractionHand.MAIN_HAND))
|
||||
return super.onUpwardSwing(useHand, player)
|
||||
}
|
||||
|
||||
override fun onForwardSwing(useHand: InteractionHand, handVel: Vec3, player: Player): Boolean {
|
||||
CobblemonNetwork.sendToServer(CastPokeRodPacket(useHand == InteractionHand.MAIN_HAND))
|
||||
return super.onForwardSwing(useHand, handVel, player)
|
||||
}
|
||||
|
||||
override fun onDownwardSwing(useHand: InteractionHand, handVel: Vec3, player: Player): Boolean {
|
||||
CobblemonNetwork.sendToServer(CastPokeRodPacket(useHand == InteractionHand.MAIN_HAND))
|
||||
return super.onDownwardSwing(useHand, handVel, player)
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft.party
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.platform.events.ClientTickEvent
|
||||
import com.cobblemon.mod.common.platform.events.PlatformEvents
|
||||
import com.cobblemon.mod.common.platform.events.RenderEvent
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.mojang.math.Axis
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.player.LocalPlayer
|
||||
import net.minecraft.client.renderer.LightTexture
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.joml.Vector3f
|
||||
import org.vivecraft.api.VRAPI
|
||||
import org.vivecraft.api.client.InteractModule
|
||||
import org.vivecraft.api.client.VRClientAPI
|
||||
import org.vivecraft.api.data.VRBodyPartData
|
||||
import java.util.function.Consumer
|
||||
|
||||
class PartyOnArm : InteractModule {
|
||||
|
||||
val fakePartyEntities: Array<Entity?> = arrayOfNulls(6)
|
||||
var selectedIndex = -1
|
||||
|
||||
private fun onTick(tickEvent: ClientTickEvent.Post) {
|
||||
val client = tickEvent.client
|
||||
client.player ?: return
|
||||
val playerData = VRClientAPI.instance().postTickWorldPose ?: return
|
||||
val offHandData = playerData.offHand
|
||||
|
||||
// Keep up-to-date fakePartyEntities, which is done so we always have a Pokemon or PokeBall entity available to
|
||||
// render and keep it ticking so we have proper animations.
|
||||
val party = CobblemonClient.storage.party
|
||||
|
||||
// Handle the selected index, placing either a Pokemon or null
|
||||
if (selectedIndex > -1) {
|
||||
val actual = party.get(selectedIndex)
|
||||
if (actual == null) {
|
||||
fakePartyEntities[selectedIndex] = null
|
||||
} else if (fakePartyEntities[selectedIndex] is PokemonEntity) {
|
||||
fakePartyEntities[selectedIndex]!!.tick()
|
||||
} else {
|
||||
fakePartyEntities[selectedIndex] = PokemonEntity(client.level!!, actual)
|
||||
}
|
||||
}
|
||||
// Handle the other indices, placing a pokeball or null
|
||||
for (i in 0..5) {
|
||||
if (i != selectedIndex) {
|
||||
val actual = party.get(i)
|
||||
if (actual == null) {
|
||||
fakePartyEntities[i] = null
|
||||
} else if (fakePartyEntities[i] !is EmptyPokeBallEntity) {
|
||||
val pokeBall = EmptyPokeBallEntity(actual.caughtBall, client.level!!)
|
||||
pokeBall.captureState = EmptyPokeBallEntity.CaptureState.FALL
|
||||
fakePartyEntities[i] = pokeBall
|
||||
} else {
|
||||
fakePartyEntities[i]!!.tick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check primary hand for overlap with any of the Pokemon
|
||||
val centerPos = getCenterPos(offHandData)
|
||||
selectedIndex = -1
|
||||
for (i in 0..5) {
|
||||
if (party.get(i) != null) {
|
||||
val pokemonPos = getPokemonPos(centerPos, playerData.offHand, i)
|
||||
if (playerData.mainHand.pos.distanceToSqr(pokemonPos) < DIST_SELECT_SQR) {
|
||||
selectedIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRender(renderEvent: RenderEvent) {
|
||||
if (renderEvent.stage != RenderEvent.Stage.AFTER_ENTITIES) return
|
||||
val mc = Minecraft.getInstance()
|
||||
val bufferSource = mc.renderBuffers().bufferSource()
|
||||
val guiGraphics = GuiGraphics(mc, mc.renderBuffers().bufferSource())
|
||||
val cameraPos = renderEvent.camera.getPosition()
|
||||
val poseStack = guiGraphics.pose()
|
||||
val partialTick = renderEvent.tickCounter.getGameTimeDeltaPartialTick(true)
|
||||
val offHandData = VRClientAPI.instance().getWorldRenderPose()?.offHand ?: return
|
||||
|
||||
val centerPos = getCenterPos(offHandData)
|
||||
|
||||
|
||||
for (i in 0..5) {
|
||||
val pokemon = fakePartyEntities[i]
|
||||
if (pokemon != null) {
|
||||
poseStack.pushPose()
|
||||
val pokemonPos = getPokemonPos(centerPos, offHandData, i)
|
||||
val renderPos = Vec3(
|
||||
-cameraPos.x + pokemonPos.x,
|
||||
-cameraPos.y + pokemonPos.y,
|
||||
-cameraPos.z + pokemonPos.z,
|
||||
)
|
||||
poseStack.translate(renderPos.x, renderPos.y, renderPos.z)
|
||||
poseStack.scale(0.05f, 0.05f, 0.05f)
|
||||
poseStack.mulPose(Axis.YN.rotation(offHandData.yaw.toFloat()))
|
||||
poseStack.mulPose(Axis.XN.rotation(offHandData.pitch.toFloat()))
|
||||
poseStack.mulPose(Axis.ZP.rotation(offHandData.roll.toFloat() + Math.PI.toFloat()))
|
||||
mc.entityRenderDispatcher.getRenderer(pokemon).render(pokemon, 0f, partialTick, poseStack, bufferSource,
|
||||
LightTexture.FULL_BRIGHT)
|
||||
poseStack.popPose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCenterPos(pose: VRBodyPartData): Vec3 {
|
||||
return pose.pos.subtract(pose.dir.scale(0.125))
|
||||
}
|
||||
|
||||
private fun getPokemonPos(centerPos: Vec3, controllerData: VRBodyPartData, index: Int): Vec3 {
|
||||
var pos = centerPos
|
||||
// Move to bottom of hand
|
||||
var downVecF = Vector3f(0f, -0.0625f, 0f)
|
||||
downVecF.rotate(Axis.ZP.rotation(controllerData.roll.toFloat()))
|
||||
downVecF.rotate(Axis.XN.rotation(controllerData.pitch.toFloat()))
|
||||
downVecF.rotate(Axis.YN.rotation(controllerData.yaw.toFloat()))
|
||||
pos = pos.add(Vec3(downVecF.x.toDouble(), downVecF.y.toDouble(), downVecF.z.toDouble()))
|
||||
// Move right or left based on index
|
||||
val rightF = Vector3f(0f, 0f, 1f) // Default forward vector of Minecraft
|
||||
rightF.rotate(Axis.YN.rotation(Math.PI.toFloat() / 2f))
|
||||
rightF.rotate(Axis.ZP.rotation(controllerData.roll.toFloat()))
|
||||
rightF.rotate(Axis.XN.rotation(controllerData.pitch.toFloat()))
|
||||
rightF.rotate(Axis.YN.rotation(controllerData.yaw.toFloat()))
|
||||
val right = Vec3(rightF.x.toDouble(), rightF.y.toDouble(), rightF.z.toDouble())
|
||||
if (index > 2) {
|
||||
pos = pos.add(right.scale(-0.0625))
|
||||
} else {
|
||||
pos = pos.add(right.scale(0.0625))
|
||||
}
|
||||
// Offset by -hand direction if on left, 0 if center, or +hand direction if right
|
||||
return pos.add(controllerData.dir.scale(DIST_BETWEEN_HORIZONTAL * (index % 3 - 1)))
|
||||
}
|
||||
|
||||
override fun getId(): ResourceLocation {
|
||||
return INTERACT_ID
|
||||
}
|
||||
|
||||
override fun isActive(
|
||||
player: LocalPlayer,
|
||||
hand: InteractionHand,
|
||||
handPosition: Vec3
|
||||
): Boolean {
|
||||
return hand == InteractionHand.MAIN_HAND && selectedIndex > -1
|
||||
}
|
||||
|
||||
override fun onPress(
|
||||
player: LocalPlayer,
|
||||
hand: InteractionHand
|
||||
): Boolean {
|
||||
// UUID lookup is done at press time since the Pokemon may have switched since initially hovered over, such
|
||||
// as via commands.
|
||||
val uuid = CobblemonClient.storage.party.get(selectedIndex)?.uuid ?: return false
|
||||
CobblemonClient.storage.switchToPokemon(uuid)
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DIST_BETWEEN_HORIZONTAL: Double = 0.125
|
||||
const val DIST_BETWEEN_VERTICAL: Float = 0.05f
|
||||
const val DIST_SELECT_SQR: Double = 0.1 * 0.1
|
||||
|
||||
val INTERACT_ID = ResourceLocation(Cobblemon.MODID, "party_on_arm_interact_module")
|
||||
|
||||
private val INSTANCE = PartyOnArm()
|
||||
|
||||
fun init() {
|
||||
PlatformEvents.RENDER.subscribe(Consumer { renderEvent: RenderEvent? -> INSTANCE.onRender(renderEvent!!) })
|
||||
PlatformEvents.CLIENT_TICK_POST.subscribe(Consumer { tickEvent: ClientTickEvent.Post? ->
|
||||
INSTANCE.onTick(
|
||||
tickEvent!!
|
||||
)
|
||||
})
|
||||
VRAPI.instance().register(INSTANCE)
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft.proximity
|
||||
|
||||
import org.vivecraft.api.data.VRBodyPart
|
||||
import org.vivecraft.api.data.VRPose
|
||||
|
||||
// TODO: Make an PR to Vivecraft that adds this feature directly to Vivecraft
|
||||
interface ProximityListener {
|
||||
fun isActive(registration: ProximityRegistration): Boolean = true
|
||||
fun onEnter(bodyA: VRBodyPart, bodyB: VRBodyPart, pose: VRPose)
|
||||
fun onExit(bodyA: VRBodyPart, bodyB: VRBodyPart, pose: VRPose)
|
||||
fun onTick(bodyA: VRBodyPart, bodyB: VRBodyPart, pose: VRPose)
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft.proximity
|
||||
|
||||
import com.cobblemon.mod.common.platform.events.ClientTickEvent
|
||||
import com.cobblemon.mod.common.platform.events.PlatformEvents
|
||||
import org.vivecraft.api.client.VRClientAPI
|
||||
import kotlin.math.acos
|
||||
|
||||
// TODO: Make an PR to Vivecraft that adds this feature directly to Vivecraft
|
||||
class ProximityManager {
|
||||
private val registrations = mutableListOf<ProximityRegistration>()
|
||||
private val previousInProximity = mutableMapOf<ProximityRegistration, Boolean>()
|
||||
private val api: VRClientAPI = VRClientAPI.instance()
|
||||
|
||||
fun register(registration: ProximityRegistration) {
|
||||
registrations += registration
|
||||
previousInProximity.putIfAbsent(registration, false)
|
||||
}
|
||||
|
||||
fun unregister(registration: ProximityRegistration) {
|
||||
registrations -= registration
|
||||
previousInProximity.remove(registration)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
registrations.clear()
|
||||
previousInProximity.clear()
|
||||
}
|
||||
|
||||
fun isActive(registration: ProximityRegistration): Boolean {
|
||||
return registration.listener.isActive(registration)
|
||||
}
|
||||
|
||||
fun onTick(tickEvent: ClientTickEvent.Post) {
|
||||
val vrPose = api.getPostTickWorldPose() ?: return
|
||||
|
||||
for (reg in registrations) {
|
||||
if (!isActive(reg)) continue
|
||||
val poseA = vrPose.getBodyPartData(reg.bodyA) ?: continue
|
||||
val poseB = vrPose.getBodyPartData(reg.bodyB) ?: continue
|
||||
|
||||
val distance = poseA.pos.distanceTo(poseB.pos)
|
||||
|
||||
val dot = poseA.dir.dot(poseB.dir).coerceIn(-1.0, 1.0)
|
||||
val angleDeg = Math.toDegrees(acos(dot)).toFloat()
|
||||
val angleOk = angleDeg <= reg.angleThreshold
|
||||
|
||||
val nowInProximity = distance <= reg.distanceThreshold && angleOk
|
||||
val wasInProximity = previousInProximity[reg] ?: false
|
||||
|
||||
if (!wasInProximity && nowInProximity) {
|
||||
reg.listener.onEnter(reg.bodyA, reg.bodyB, vrPose)
|
||||
} else if (wasInProximity && !nowInProximity) {
|
||||
reg.listener.onExit(reg.bodyA, reg.bodyB, vrPose)
|
||||
}
|
||||
|
||||
if (nowInProximity) {
|
||||
reg.listener.onTick(reg.bodyA, reg.bodyB, vrPose)
|
||||
}
|
||||
|
||||
previousInProximity[reg] = nowInProximity
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val INSTANCE = ProximityManager()
|
||||
|
||||
fun init() {
|
||||
PlatformEvents.CLIENT_TICK_POST.subscribe { tickEvent: ClientTickEvent.Post? ->
|
||||
INSTANCE.onTick(
|
||||
tickEvent!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun register(registration: ProximityRegistration) = INSTANCE.register(registration)
|
||||
fun unregister(registration: ProximityRegistration) = INSTANCE.unregister(registration)
|
||||
fun clearAll() = INSTANCE.clear()
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.cobblemon.mod.common.client.integration.vivecraft.proximity
|
||||
|
||||
import org.vivecraft.api.data.VRBodyPart
|
||||
|
||||
// TODO: Make an PR to Vivecraft that adds this feature directly to Vivecraft
|
||||
data class ProximityRegistration(
|
||||
val bodyA: VRBodyPart,
|
||||
val bodyB: VRBodyPart,
|
||||
val distanceThreshold: Float,
|
||||
val angleThreshold: Float = 180f,
|
||||
val listener: ProximityListener
|
||||
)
|
||||
+9
-9
@@ -143,18 +143,18 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
|
||||
}
|
||||
|
||||
private fun canAttemptDismount(player: LocalPlayer, selectedPartyPokemon: Pokemon?): Boolean {
|
||||
val vehicle = player.vehicle as? PokemonEntity ?: return false
|
||||
|
||||
if (player != vehicle.controllingPassenger) return true
|
||||
|
||||
if (player.vehicle !is PokemonEntity) return false
|
||||
val vehicle = player.vehicle as PokemonEntity
|
||||
if (player != vehicle.controllingPassenger) {
|
||||
return true
|
||||
}
|
||||
val isAirRide = vehicle.ridingController?.context?.style == RidingStyle.AIR
|
||||
val hasLandRide = vehicle.rideProp.behaviours?.get(RidingStyle.LAND) != null
|
||||
|
||||
if (!Cobblemon.config.enableInFlightDismounting && isAirRide && hasLandRide) {
|
||||
return false
|
||||
return if (isAirRide && hasLandRide) {
|
||||
false
|
||||
} else {
|
||||
vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
|
||||
}
|
||||
|
||||
return vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
|
||||
}
|
||||
|
||||
private fun isRidingPokemon(player: LocalPlayer, ignoreControlling: Boolean = false): Boolean {
|
||||
|
||||
-27
@@ -1,27 +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.client.net.data
|
||||
|
||||
import com.cobblemon.mod.common.api.interaction.PokemonInteractions
|
||||
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.net.messages.client.data.PokemonInteractionsSyncPacket
|
||||
import net.minecraft.client.Minecraft
|
||||
|
||||
object PokemonInteractionsSyncHandler : ClientNetworkPacketHandler<PokemonInteractionsSyncPacket> {
|
||||
|
||||
override fun handle(
|
||||
packet: PokemonInteractionsSyncPacket,
|
||||
client: Minecraft,
|
||||
) {
|
||||
PokemonInteractions.speciesInteractions.clear()
|
||||
PokemonInteractions.speciesInteractions.addAll(packet.speciesInteractions)
|
||||
PokemonInteractions.generalInteractions.clear()
|
||||
PokemonInteractions.generalInteractions.addAll(packet.generalInteractions)
|
||||
}
|
||||
}
|
||||
+5
-21
@@ -8,8 +8,6 @@
|
||||
|
||||
package com.cobblemon.mod.common.client.persisted
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.Cobblemon.LOGGER
|
||||
import com.cobblemon.mod.common.config.CobblemonConfig
|
||||
import java.io.BufferedReader
|
||||
import java.io.BufferedWriter
|
||||
@@ -33,14 +31,7 @@ object ClientPersistedData {
|
||||
fun readSnapshotsData() : SnapshotAcknowledgementData? {
|
||||
if (snapshotsPath.exists()) {
|
||||
BufferedReader(InputStreamReader(FileInputStream(snapshotsPath.toFile()))).use { reader ->
|
||||
try {
|
||||
return gson.fromJson(reader, SnapshotAcknowledgementData::class.java)
|
||||
} catch (exception: Exception) {
|
||||
LOGGER.error("Failed to load snapshots.json... Resetting File")
|
||||
val data = SnapshotAcknowledgementData(Cobblemon.VERSION, false)
|
||||
writeSnapshotsData(data)
|
||||
return data
|
||||
}
|
||||
return gson.fromJson(reader, SnapshotAcknowledgementData::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,23 +46,16 @@ object ClientPersistedData {
|
||||
fun readRidingPerspectivesData() : RidingPerspectiveData? {
|
||||
if (ridingPerspectivesPath.exists()) {
|
||||
BufferedReader(InputStreamReader(FileInputStream(ridingPerspectivesPath.toFile()))).use { reader ->
|
||||
try {
|
||||
return gson.fromJson(reader, RidingPerspectiveData::class.java)
|
||||
} catch (exception: Exception) {
|
||||
LOGGER.error("Failed to load riding_perspectives.json... Resetting File")
|
||||
val data = RidingPerspectiveData()
|
||||
saveRidingPerspectivesData(data)
|
||||
return data
|
||||
}
|
||||
return gson.fromJson(reader, RidingPerspectiveData::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun saveRidingPerspectivesData(data: RidingPerspectiveData) {
|
||||
fun saveRidingPerspectivesData() {
|
||||
ridingPerspectivesPath.toFile().parentFile.mkdirs()
|
||||
BufferedWriter(FileWriter(ridingPerspectivesPath.toFile())).use { writer -> gson.toJson(data, writer) }
|
||||
BufferedWriter(FileWriter(ridingPerspectivesPath.toFile())).use { writer -> gson.toJson(ridingPerspectives, writer) }
|
||||
}
|
||||
|
||||
data class SnapshotAcknowledgementData(val version: String, val dontShowAgain: Boolean)
|
||||
@@ -80,7 +64,7 @@ object ClientPersistedData {
|
||||
) {
|
||||
fun updatePerspective(key: ResourceLocation, cameraType: CameraType) {
|
||||
perspectives[key] = cameraType
|
||||
saveRidingPerspectivesData(this)
|
||||
saveRidingPerspectivesData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,9 +329,6 @@ class CobblemonConfig {
|
||||
@CobblemonConfigField(Category.Riding, lang = "remember_riding_camera", CLIENT)
|
||||
var rememberRidingCamera = false
|
||||
|
||||
@CobblemonConfigField(Category.Riding, lang = "enable_in_flight_dismounting", CLIENT)
|
||||
var enableInFlightDismounting = false
|
||||
|
||||
@CobblemonConfigField(Category.Debug, lang = "enable_debug_keys", CLIENT)
|
||||
var enableDebugKeys = false
|
||||
|
||||
|
||||
@@ -269,6 +269,9 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
|
||||
// If we can fly and we're airborne, return the current Y position
|
||||
return vec.y
|
||||
}
|
||||
if (world.getBlockState(blockPos).block == CobblemonBlocks.SACCHARINE_LEAVES && pather.canPathThroughSaccLeaves()) {
|
||||
return vec.y + 0.5
|
||||
}
|
||||
return if ((canFloat()) && blockGetter.getFluidState(blockPos).`is`(FluidTags.WATER)) vec.y + 0.5 else super.getGroundY(vec)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.api.storage.party.PartyStore
|
||||
import com.cobblemon.mod.common.battles.ai.CompetitiveBattleAI
|
||||
import com.cobblemon.mod.common.battles.ai.StrongBattleAI
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
|
||||
@@ -31,7 +30,7 @@ class NPCBattleActor(
|
||||
) : AIBattleActor(
|
||||
gameId = npc.uuid,
|
||||
pokemonList = pokemonList.let { if (npc.npc.randomizePartyOrder) it.shuffled() else it },
|
||||
battleAI = CompetitiveBattleAI(skill)
|
||||
battleAI = StrongBattleAI(skill)
|
||||
), EntityBackedBattleActor<NPCEntity> {
|
||||
override val entity = npc
|
||||
override val type = ActorType.NPC
|
||||
|
||||
@@ -1208,13 +1208,14 @@ open class PokemonEntity(
|
||||
}
|
||||
}
|
||||
|
||||
if (hand == InteractionHand.MAIN_HAND) {
|
||||
if (player.isShiftKeyDown && player is ServerPlayer) {
|
||||
if (hand == InteractionHand.MAIN_HAND && player is ServerPlayer) {
|
||||
if (player.isShiftKeyDown) {
|
||||
showInteractionWheel(player, itemStack)
|
||||
return InteractionResult.sidedSuccess(level().isClientSide)
|
||||
}
|
||||
else if (this.ownerUUID == player.uuid && this.attemptItemInteraction(player, player.getItemInHand(hand))) {
|
||||
return InteractionResult.sidedSuccess(level().isClientSide)
|
||||
else if (pokemon.getOwnerPlayer() == player) {
|
||||
// TODO #105
|
||||
if (this.attemptItemInteraction(player, player.getItemInHand(hand))) return InteractionResult.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1390,24 +1391,18 @@ open class PokemonEntity(
|
||||
return bagItemLike.handleInteraction(player, battlePokemon, stack)
|
||||
}
|
||||
|
||||
if (this.isBusy) {
|
||||
if (player !is ServerPlayer || this.isBusy) {
|
||||
return false
|
||||
}
|
||||
|
||||
val interaction = PokemonInteractions.findInteraction(this)
|
||||
|
||||
if (interaction != null && !pokemon.isOnInteractionCooldown(interaction.grouping)) {
|
||||
if (player is ServerPlayer) {
|
||||
interaction.effects.forEach { it.applyEffect(this, player) }
|
||||
pokemon.interactionCooldowns.put(interaction.grouping, runtime.resolveInt(interaction.cooldown))
|
||||
}
|
||||
|
||||
interaction.effects.forEach { it.applyEffect(this, player) }
|
||||
pokemon.interactionCooldowns.put(interaction.grouping, runtime.resolveInt(interaction.cooldown))
|
||||
return true
|
||||
}
|
||||
|
||||
if (player !is ServerPlayer) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Evolution item logic
|
||||
if (pokemon.getOwnerPlayer() == player) {
|
||||
val context = ItemInteractionEvolution.ItemInteractionContext(stack, player.level())
|
||||
|
||||
@@ -30,13 +30,21 @@ import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
|
||||
import com.cobblemon.mod.common.api.events.fishing.PokerodCastEvent
|
||||
import com.cobblemon.mod.common.api.events.fishing.PokerodReelEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.*
|
||||
import com.cobblemon.mod.common.api.events.pokemon.CollectEggEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.FossilRevivedEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.HatchEggEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.LevelUpEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.PokemonCapturedEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.PokemonGainedEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.RidePokemonEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.TradeEvent
|
||||
import com.cobblemon.mod.common.api.events.pokemon.evolution.EvolutionCompleteEvent
|
||||
import com.cobblemon.mod.common.api.events.storage.ReleasePokemonEvent
|
||||
import com.cobblemon.mod.common.api.stats.CobblemonStats.getStat
|
||||
import com.cobblemon.mod.common.util.getPlayer
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
object StatHandler : EventHandler {
|
||||
override fun registerListeners() {
|
||||
@@ -92,13 +100,13 @@ object StatHandler : EventHandler {
|
||||
|
||||
fun onBattleStart(event : BattleStartedEvent) {
|
||||
event.battle.players.forEach { player ->
|
||||
player.awardStat(getStat(Cobblemon.statistics.BATTLES_TOTAL))
|
||||
player.awardStat(this.getStat(getStat(Cobblemon.statistics.BATTLES_TOTAL)))
|
||||
}
|
||||
}
|
||||
|
||||
fun onDexEntryGain(event : PokemonGainedEvent) {
|
||||
val player = event.pokemon.getOwnerPlayer()
|
||||
player?.awardStat(getStat(Cobblemon.statistics.DEX_ENTRIES))
|
||||
player?.awardStat(getStat(getStat(Cobblemon.statistics.DEX_ENTRIES)))
|
||||
}
|
||||
|
||||
fun onCollectEgg(event : CollectEggEvent) {
|
||||
@@ -131,4 +139,12 @@ object StatHandler : EventHandler {
|
||||
fun onReelIn(event : PokerodReelEvent) {
|
||||
event.player.awardStat(getStat(Cobblemon.statistics.REEL_INS))
|
||||
}
|
||||
|
||||
fun getStat(resourceLocation: ResourceLocation) : ResourceLocation {
|
||||
val stat = BuiltInRegistries.CUSTOM_STAT.get(resourceLocation)
|
||||
if (stat == null) {
|
||||
Cobblemon.LOGGER.error("Could not find stat with id {}", resourceLocation)
|
||||
}
|
||||
return stat ?: throw NullPointerException("Could not find stat with id $resourceLocation")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cobblemon.mod.common.integration.vivecraft
|
||||
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.listener.swing.PokeBallSwingListener
|
||||
import com.cobblemon.mod.common.client.integration.vivecraft.listener.swing.PokeRodSwingListener
|
||||
import org.vivecraft.api.VRAPI
|
||||
|
||||
class CobblemonVR {
|
||||
|
||||
fun init() {
|
||||
VRAPI.instance()
|
||||
.register(PokeBallSwingListener())
|
||||
|
||||
VRAPI.instance()
|
||||
.register(PokeRodSwingListener())
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import net.minecraft.world.level.Level
|
||||
import kotlin.math.cos
|
||||
|
||||
class PokeBallItem(
|
||||
val pokeBall: PokeBall
|
||||
val pokeBall: PokeBall
|
||||
) : CobblemonItem(Properties().apply {
|
||||
when (pokeBall.name) {
|
||||
// Master balls are a netherite product and should be fire immune
|
||||
@@ -43,7 +43,7 @@ class PokeBallItem(
|
||||
return InteractionResultHolder.sidedSuccess(itemStack, world.isClientSide)
|
||||
}
|
||||
|
||||
private fun throwPokeBall(world: Level, player: ServerPlayer) {
|
||||
fun throwPokeBall(world: Level, player: ServerPlayer) {
|
||||
val pokeBallEntity = EmptyPokeBallEntity(pokeBall, player.level(), player).apply {
|
||||
val overhandFactor: Float = if (player.xRot < 0) {
|
||||
5f * cos(player.xRot.toRadians())
|
||||
@@ -57,4 +57,5 @@ class PokeBallItem(
|
||||
}
|
||||
world.addFreshEntity(pokeBallEntity)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,7 +25,9 @@ class MochiItem(stat: Stats): EVIncreaseItem(stat, 4) {
|
||||
}
|
||||
|
||||
pokemon.feedPokemon(1)
|
||||
|
||||
|
||||
stack.consume(1, player)
|
||||
|
||||
return super.applyToPokemon(player, stack, pokemon)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,8 @@ class PokerodItem(val pokeRodId: ResourceLocation, settings: Properties) : Fishi
|
||||
|
||||
if (baitCount > 1) {
|
||||
baitStack.count = baitCount - 1
|
||||
stack.set(CobblemonItemComponents.BAIT, RodBaitComponent(baitStack)
|
||||
stack.set(
|
||||
CobblemonItemComponents.BAIT, RodBaitComponent(baitStack)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -149,90 +150,163 @@ class PokerodItem(val pokeRodId: ResourceLocation, settings: Properties) : Fishi
|
||||
}
|
||||
|
||||
override fun use(world: Level, user: Player, hand: InteractionHand): InteractionResultHolder<ItemStack> {
|
||||
|
||||
val itemStack = user.getItemInHand(hand)
|
||||
val offHandItem = user.getItemInHand(InteractionHand.OFF_HAND)
|
||||
|
||||
// Check if offhand item is valid bait and the rod is not in use, if so then apply bait from offhand
|
||||
if (!world.isClientSide && user.fishing == null && SpawnBaitEffects.isFishingBait(offHandItem)) {
|
||||
CobblemonEvents.BAIT_SET_PRE.postThen(BaitSetEvent(itemStack, offHandItem), { event ->
|
||||
return InteractionResultHolder.fail(itemStack)
|
||||
}, {
|
||||
playAttachSound(user)
|
||||
attemptApplyOffhandBait(world, user, itemStack, offHandItem)?.let { return it }
|
||||
|
||||
// if there is bait on the rod already then drop it on the ground before applying offhand bait
|
||||
val baitStack = getBaitStackOnRod(itemStack)
|
||||
if (user.fishing != null) {
|
||||
attemptReel(world, user, hand, itemStack)?.let { return it }
|
||||
} else {
|
||||
attemptCast(world, user, hand, itemStack)?.let { return it }
|
||||
}
|
||||
|
||||
if (baitStack != ItemStack.EMPTY && offHandItem.item != baitStack.item) {
|
||||
user.drop(baitStack, true) // Drop the full stack
|
||||
return InteractionResultHolder.sidedSuccess(itemStack, world.isClientSide)
|
||||
}
|
||||
|
||||
// apply single bait item from offhand
|
||||
val singleBait = offHandItem.copy()
|
||||
singleBait.count = 1
|
||||
setBait(itemStack, singleBait)
|
||||
offHandItem.shrink(1)
|
||||
fun applyBait(
|
||||
rodStack: ItemStack,
|
||||
baitSource: ItemStack,
|
||||
player: Player?
|
||||
): Boolean {
|
||||
if (baitSource.isEmpty || !SpawnBaitEffects.isFishingBait(baitSource)) return false
|
||||
|
||||
val existingBait = getBaitStackOnRod(rodStack)
|
||||
if (!existingBait.isEmpty && existingBait.item == baitSource.item) return false
|
||||
|
||||
var applied = false
|
||||
|
||||
CobblemonEvents.BAIT_SET_PRE.postThen(
|
||||
BaitSetEvent(rodStack, baitSource),
|
||||
{ return false },
|
||||
{
|
||||
if (!existingBait.isEmpty && player != null) {
|
||||
player.drop(existingBait, true)
|
||||
}
|
||||
})
|
||||
|
||||
val singleBait = baitSource.copy()
|
||||
singleBait.count = 1
|
||||
setBait(rodStack, singleBait)
|
||||
|
||||
baitSource.shrink(1)
|
||||
applied = true
|
||||
}
|
||||
)
|
||||
|
||||
return applied
|
||||
}
|
||||
|
||||
fun attemptApplyOffhandBait(
|
||||
world: Level,
|
||||
user: Player,
|
||||
rodStack: ItemStack,
|
||||
offHandItem: ItemStack
|
||||
): InteractionResultHolder<ItemStack>? {
|
||||
if (world.isClientSide) return null
|
||||
if (user.fishing != null) return null
|
||||
|
||||
val applied = applyBait(
|
||||
rodStack = rodStack,
|
||||
baitSource = offHandItem,
|
||||
player = user
|
||||
)
|
||||
|
||||
if (applied) {
|
||||
playAttachSound(user)
|
||||
return InteractionResultHolder.sidedSuccess(rodStack, false)
|
||||
}
|
||||
|
||||
var i: Int
|
||||
if (user.fishing != null) { // if the bobber is out yet
|
||||
if (!world.isClientSide) {
|
||||
CobblemonEvents.POKEROD_REEL.postThen(
|
||||
PokerodReelEvent(user, itemStack),
|
||||
{ event -> return InteractionResultHolder.fail(itemStack) },
|
||||
{ event ->
|
||||
i = user.fishing!!.retrieve(itemStack)
|
||||
itemStack.hurtAndBreak(i, user, hand.toEquipmentSlot())
|
||||
world.playSoundServer(
|
||||
Vec3(user.x,
|
||||
user.y,
|
||||
user.z),
|
||||
CobblemonSounds.FISHING_ROD_REEL_IN,
|
||||
SoundSource.PLAYERS,
|
||||
1.0f,
|
||||
1.0f / (world.getRandom().nextFloat() * 0.4f + 0.8f)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
world.playSound(null as Player?, user.x, user.y, user.z, CobblemonSounds.FISHING_ROD_REEL_IN, SoundSource.PLAYERS, 1.0f, 1.0f)
|
||||
user.gameEvent(GameEvent.ITEM_INTERACT_FINISH)
|
||||
} else { // if the bobber is not out yet
|
||||
fun attemptReel(
|
||||
world: Level,
|
||||
user: Player,
|
||||
hand: InteractionHand,
|
||||
itemStack: ItemStack
|
||||
): InteractionResultHolder<ItemStack>? {
|
||||
if (user.fishing == null) return null
|
||||
|
||||
if (!world.isClientSide) {
|
||||
val lureLevel = world.enchantmentRegistry.getHolder(Enchantments.LURE).map { EnchantmentHelper.getItemEnchantmentLevel(it, itemStack) }.orElse(0)
|
||||
val luckLevel = world.enchantmentRegistry.getHolder(Enchantments.LUCK_OF_THE_SEA).map { EnchantmentHelper.getItemEnchantmentLevel(it, itemStack) }.orElse(0)
|
||||
|
||||
val bobberEntity = PokeRodFishingBobberEntity(
|
||||
user,
|
||||
pokeRodId,
|
||||
getBaitStackOnRod(itemStack),
|
||||
world,
|
||||
luckLevel,
|
||||
lureLevel,
|
||||
itemStack
|
||||
)
|
||||
CobblemonEvents.POKEROD_CAST_PRE.postThen(
|
||||
PokerodCastEvent.Pre(itemStack, bobberEntity, getBaitStackOnRod(itemStack)),
|
||||
{ event -> return InteractionResultHolder.fail(itemStack) },
|
||||
{ event ->
|
||||
world.addFreshEntity(bobberEntity)
|
||||
var baitId = getBaitStackOnRod(itemStack).takeUnless { it.isEmpty }?.itemHolder?.unwrapKey()?.orElse(null)?.location() ?: cobblemonResource("empty_bait")
|
||||
CobblemonCriteria.CAST_POKE_ROD.trigger(user as ServerPlayer, CastPokeRodContext(baitId))
|
||||
|
||||
CobblemonEvents.POKEROD_CAST_POST.post(
|
||||
PokerodCastEvent.Post(itemStack, bobberEntity, getBaitStackOnRod(itemStack))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
user.awardStat(Stats.ITEM_USED.get(this))
|
||||
user.gameEvent(GameEvent.ITEM_INTERACT_START)
|
||||
if (!world.isClientSide) {
|
||||
CobblemonEvents.POKEROD_REEL.postThen(
|
||||
PokerodReelEvent(user, itemStack),
|
||||
{ event -> return InteractionResultHolder.fail(itemStack) },
|
||||
{ event ->
|
||||
val i = user.fishing!!.retrieve(itemStack)
|
||||
itemStack.hurtAndBreak(i, user, hand.toEquipmentSlot())
|
||||
world.playSoundServer(
|
||||
Vec3(user.x, user.y, user.z),
|
||||
CobblemonSounds.FISHING_ROD_REEL_IN,
|
||||
SoundSource.PLAYERS,
|
||||
1.0f,
|
||||
1.0f / (world.getRandom().nextFloat() * 0.4f + 0.8f)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
world.playSound(
|
||||
null as Player?,
|
||||
user.x,
|
||||
user.y,
|
||||
user.z,
|
||||
CobblemonSounds.FISHING_ROD_REEL_IN,
|
||||
SoundSource.PLAYERS,
|
||||
1.0f,
|
||||
1.0f
|
||||
)
|
||||
user.gameEvent(GameEvent.ITEM_INTERACT_FINISH)
|
||||
|
||||
return InteractionResultHolder.sidedSuccess(itemStack, world.isClientSide)
|
||||
}
|
||||
|
||||
fun attemptCast(
|
||||
world: Level,
|
||||
user: Player,
|
||||
hand: InteractionHand,
|
||||
itemStack: ItemStack
|
||||
): InteractionResultHolder<ItemStack>? {
|
||||
if (user.fishing != null) return null
|
||||
|
||||
if (!world.isClientSide) {
|
||||
val lureLevel = world.enchantmentRegistry.getHolder(Enchantments.LURE)
|
||||
.map { EnchantmentHelper.getItemEnchantmentLevel(it, itemStack) }
|
||||
.orElse(0)
|
||||
val luckLevel = world.enchantmentRegistry.getHolder(Enchantments.LUCK_OF_THE_SEA)
|
||||
.map { EnchantmentHelper.getItemEnchantmentLevel(it, itemStack) }
|
||||
.orElse(0)
|
||||
|
||||
val bobberEntity = PokeRodFishingBobberEntity(
|
||||
user,
|
||||
pokeRodId,
|
||||
getBaitStackOnRod(itemStack),
|
||||
world,
|
||||
luckLevel,
|
||||
lureLevel,
|
||||
itemStack
|
||||
)
|
||||
|
||||
CobblemonEvents.POKEROD_CAST_PRE.postThen(
|
||||
PokerodCastEvent.Pre(itemStack, bobberEntity, getBaitStackOnRod(itemStack)),
|
||||
{ event -> return InteractionResultHolder.fail(itemStack) },
|
||||
{ event ->
|
||||
world.addFreshEntity(bobberEntity)
|
||||
val baitId = getBaitStackOnRod(itemStack).takeUnless { it.isEmpty }
|
||||
?.itemHolder?.unwrapKey()?.orElse(null)?.location()
|
||||
?: cobblemonResource("empty_bait")
|
||||
CobblemonCriteria.CAST_POKE_ROD.trigger(user as ServerPlayer, CastPokeRodContext(baitId))
|
||||
|
||||
CobblemonEvents.POKEROD_CAST_POST.post(
|
||||
PokerodCastEvent.Post(itemStack, bobberEntity, getBaitStackOnRod(itemStack))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
user.awardStat(Stats.ITEM_USED.get(this))
|
||||
user.gameEvent(GameEvent.ITEM_INTERACT_START)
|
||||
|
||||
return InteractionResultHolder.sidedSuccess(itemStack, world.isClientSide)
|
||||
}
|
||||
|
||||
|
||||
+3
-10
@@ -35,16 +35,9 @@ class SinisterTeaItem : Item(
|
||||
val effectsComponent = stack.get(CobblemonItemComponents.MOB_EFFECTS)
|
||||
|
||||
if (effectsComponent != null) {
|
||||
for (raw in effectsComponent.mobEffects) {
|
||||
val inst = MobEffectInstance(raw)
|
||||
val holder = inst.effect
|
||||
val effect = holder.value()
|
||||
|
||||
if (effect.isInstantenous) {
|
||||
effect.applyInstantenousEffect(user as? Player, null, user, inst.amplifier, 1.0)
|
||||
} else {
|
||||
user.addEffect(inst)
|
||||
}
|
||||
for (effect in effectsComponent.mobEffects) {
|
||||
val effectCopy = MobEffectInstance(effect)
|
||||
user.addEffect(effectCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-48
@@ -1,48 +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.net.messages.client.data
|
||||
|
||||
import com.cobblemon.mod.common.api.interaction.PokemonInteractionSet
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
class PokemonInteractionsSyncPacket(
|
||||
val speciesInteractions: List<PokemonInteractionSet>,
|
||||
val generalInteractions: List<PokemonInteractionSet>,
|
||||
) : NetworkPacket<PokemonInteractionsSyncPacket> {
|
||||
override val id = ID
|
||||
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeCollection(speciesInteractions) { it, speciesInteraction ->
|
||||
speciesInteraction.encode(it)
|
||||
}
|
||||
buffer.writeCollection(generalInteractions) { it, generalInteraction ->
|
||||
generalInteraction.encode(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("pokemon_interactions_sync")
|
||||
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): PokemonInteractionsSyncPacket {
|
||||
val speciesInteractions = buffer.readList { it ->
|
||||
PokemonInteractionSet.decode(it)
|
||||
}
|
||||
val generalInteractions = buffer.readList { it ->
|
||||
PokemonInteractionSet.decode(it)
|
||||
}
|
||||
|
||||
return PokemonInteractionsSyncPacket(
|
||||
speciesInteractions,
|
||||
generalInteractions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -21,9 +21,7 @@ class PokemonStateUpdatePacket(pokemon: () -> Pokemon?, value: PokemonState): Si
|
||||
value.writeToBuffer(buffer)
|
||||
}
|
||||
|
||||
override fun set(pokemon: Pokemon, value: PokemonState) {
|
||||
pokemon.state = value
|
||||
}
|
||||
override fun set(pokemon: Pokemon, value: PokemonState) { pokemon.state = value }
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("state_update")
|
||||
|
||||
+10
-24
@@ -13,24 +13,30 @@ import com.cobblemon.mod.common.api.mark.Marks
|
||||
import com.cobblemon.mod.common.api.pokeball.PokeBalls
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.riding.stats.RidingStat
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.entity.PlatformType
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.util.*
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.readEnumConstant
|
||||
import com.cobblemon.mod.common.util.readIdentifier
|
||||
import com.cobblemon.mod.common.util.readString
|
||||
import com.cobblemon.mod.common.util.readText
|
||||
import com.cobblemon.mod.common.util.writeEnumConstant
|
||||
import com.cobblemon.mod.common.util.writeIdentifier
|
||||
import com.cobblemon.mod.common.util.writeString
|
||||
import com.cobblemon.mod.common.util.writeText
|
||||
import java.util.UUID
|
||||
import net.minecraft.client.multiplayer.ClientLevel
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.entity.Entity
|
||||
import java.util.*
|
||||
|
||||
class SpawnPokemonPacket(
|
||||
var ownerId: UUID?,
|
||||
var pokemonUUID: UUID,
|
||||
var storageUUID: UUID?,
|
||||
var scaleModifier: Float,
|
||||
var speciesId: ResourceLocation,
|
||||
var gender: Gender,
|
||||
@@ -55,7 +61,6 @@ class SpawnPokemonPacket(
|
||||
var tickSpawned: Int,
|
||||
var rideBoosts: Map<RidingStat, Float>,
|
||||
var rideStamina: Float,
|
||||
var silent: Boolean,
|
||||
vanillaSpawnPacket: ClientboundAddEntityPacket,
|
||||
) : SpawnExtraDataEntityPacket<SpawnPokemonPacket, PokemonEntity>(vanillaSpawnPacket) {
|
||||
|
||||
@@ -64,7 +69,6 @@ class SpawnPokemonPacket(
|
||||
constructor(entity: PokemonEntity, vanillaSpawnPacket: ClientboundAddEntityPacket) : this(
|
||||
entity.ownerUUID,
|
||||
entity.pokemon.uuid,
|
||||
entity.pokemon.storeCoordinates.get()?.store?.uuid,
|
||||
entity.pokemon.scaleModifier,
|
||||
entity.exposedSpecies.resourceIdentifier,
|
||||
entity.pokemon.gender,
|
||||
@@ -89,14 +93,12 @@ class SpawnPokemonPacket(
|
||||
entity.tickCount,
|
||||
entity.entityData.get(PokemonEntity.RIDE_BOOSTS),
|
||||
entity.entityData.get(PokemonEntity.RIDE_STAMINA),
|
||||
entity.isSilent,
|
||||
vanillaSpawnPacket
|
||||
)
|
||||
|
||||
override fun encodeEntityData(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeNullable(ownerId) { _, v -> buffer.writeUUID(v) }
|
||||
buffer.writeUUID(this.pokemonUUID)
|
||||
buffer.writeNullable(this.storageUUID) { pb, value -> pb.writeUUID(value) }
|
||||
buffer.writeFloat(this.scaleModifier)
|
||||
buffer.writeIdentifier(this.speciesId)
|
||||
buffer.writeEnumConstant(this.gender)
|
||||
@@ -125,21 +127,10 @@ class SpawnPokemonPacket(
|
||||
{ _, value -> buffer.writeFloat(value) }
|
||||
)
|
||||
buffer.writeFloat(rideStamina)
|
||||
buffer.writeBoolean(silent)
|
||||
}
|
||||
|
||||
override fun applyData(entity: PokemonEntity, level: ClientLevel) {
|
||||
entity.ownerUUID = ownerId
|
||||
|
||||
// Reuse the stored Pokémon instance to avoid creating a duplicate,
|
||||
// preventing client/entity desync issues.
|
||||
this@SpawnPokemonPacket.storageUUID?.let {
|
||||
val storedPokemon = CobblemonClient.storage.locatePokemon(it, this@SpawnPokemonPacket.pokemonUUID)
|
||||
if (storedPokemon != null) {
|
||||
entity.pokemon = storedPokemon
|
||||
}
|
||||
}
|
||||
|
||||
entity.pokemon.apply {
|
||||
uuid = this@SpawnPokemonPacket.pokemonUUID
|
||||
scaleModifier = this@SpawnPokemonPacket.scaleModifier
|
||||
@@ -171,7 +162,6 @@ class SpawnPokemonPacket(
|
||||
entity.entityData.set(PokemonEntity.RIDE_BOOSTS, rideBoosts)
|
||||
entity.entityData.set(PokemonEntity.RIDE_STAMINA, rideStamina)
|
||||
entity.entityData.set(PokemonEntity.SCALE_MODIFIER, scaleModifier)
|
||||
entity.isSilent = silent
|
||||
|
||||
entity.ejectPassengers()
|
||||
passengers.forEach {
|
||||
@@ -189,7 +179,6 @@ class SpawnPokemonPacket(
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): SpawnPokemonPacket {
|
||||
val ownerId = buffer.readNullable { buffer.readUUID() }
|
||||
val pokemonUUID = buffer.readUUID()
|
||||
val storageUUID = buffer.readNullable { buffer.readUUID() }
|
||||
val scaleModifier = buffer.readFloat()
|
||||
val speciesId = buffer.readIdentifier()
|
||||
val gender = buffer.readEnumConstant(Gender::class.java)
|
||||
@@ -217,13 +206,11 @@ class SpawnPokemonPacket(
|
||||
{ buffer.readFloat() }
|
||||
)
|
||||
val rideStamina = buffer.readFloat()
|
||||
val silent = buffer.readBoolean()
|
||||
val vanillaPacket = decodeVanillaPacket(buffer)
|
||||
|
||||
return SpawnPokemonPacket(
|
||||
ownerId,
|
||||
pokemonUUID,
|
||||
storageUUID,
|
||||
scaleModifier,
|
||||
speciesId,
|
||||
gender,
|
||||
@@ -248,7 +235,6 @@ class SpawnPokemonPacket(
|
||||
tickSpawned,
|
||||
rideBoosts,
|
||||
rideStamina,
|
||||
silent,
|
||||
vanillaPacket
|
||||
)
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.cobblemon.mod.common.net.messages.server.pokeball
|
||||
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
class SendOutPokeBallPacket() : NetworkPacket<SendOutPokeBallPacket> {
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("send_out_pokeball")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf) = SendOutPokeBallPacket()
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.cobblemon.mod.common.net.messages.server.pokerod
|
||||
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
class ApplyBaitPacket() : NetworkPacket<ApplyBaitPacket> {
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("apply_bait")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf) = ApplyBaitPacket()
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.cobblemon.mod.common.net.messages.server.pokerod
|
||||
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
class CastPokeRodPacket(val mainHand: Boolean) : NetworkPacket<CastPokeRodPacket> {
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeBoolean(mainHand)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("cast_pokerod")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf) = CastPokeRodPacket(buffer.readBoolean())
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.cobblemon.mod.common.net.messages.server.pokerod
|
||||
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
class ReelPokeRodPacket(val mainHand: Boolean) : NetworkPacket<ReelPokeRodPacket> {
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeBoolean(mainHand)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("reel_pokerod")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf) = ReelPokeRodPacket(buffer.readBoolean())
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.net.serverhandling.pokeball
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.item.PokeBallItem
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeball.SendOutPokeBallPacket
|
||||
import com.cobblemon.mod.common.util.isServerSide
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
object SendOutPokeBallHandler : ServerNetworkPacketHandler<SendOutPokeBallPacket> {
|
||||
|
||||
override fun handle(packet: SendOutPokeBallPacket, server: MinecraftServer, player: ServerPlayer) {
|
||||
(player.mainHandItem.item as? PokeBallItem)?.let { pokeball ->
|
||||
if (player.level().isServerSide()) {
|
||||
pokeball.throwPokeBall(player.level(), player)
|
||||
}
|
||||
// TODO: player.mainHandItem.consume(1, player)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.cobblemon.mod.common.net.serverhandling.pokerod
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.item.interactive.PokerodItem
|
||||
import com.cobblemon.mod.common.net.messages.server.pokerod.ApplyBaitPacket
|
||||
import com.cobblemon.mod.common.util.isServerSide
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.InteractionHand
|
||||
|
||||
object ApplyBaitHandler : ServerNetworkPacketHandler<ApplyBaitPacket> {
|
||||
|
||||
override fun handle(packet: ApplyBaitPacket, server: MinecraftServer, player: ServerPlayer) {
|
||||
|
||||
val mainHandItem = player.getItemInHand(InteractionHand.MAIN_HAND)
|
||||
val offHandItem = player.getItemInHand(InteractionHand.OFF_HAND)
|
||||
|
||||
println("[ApplyBaitHandler] Main hand: ${mainHandItem.item}")
|
||||
println("[ApplyBaitHandler] Off hand: ${offHandItem.item}")
|
||||
|
||||
(mainHandItem.item as? PokerodItem)?.let { pokerod ->
|
||||
if (player.level().isServerSide()) {
|
||||
pokerod.applyBait(
|
||||
mainHandItem,
|
||||
offHandItem,
|
||||
player
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(offHandItem.item as? PokerodItem)?.let { pokerod ->
|
||||
if (player.level().isServerSide()) {
|
||||
pokerod.applyBait(
|
||||
offHandItem,
|
||||
mainHandItem,
|
||||
player
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.net.serverhandling.pokerod
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.item.PokeBallItem
|
||||
import com.cobblemon.mod.common.item.interactive.PokerodItem
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeball.SendOutPokeBallPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokerod.CastPokeRodPacket
|
||||
import com.cobblemon.mod.common.util.isServerSide
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.InteractionHand
|
||||
|
||||
object CastPokeRodHandler : ServerNetworkPacketHandler<CastPokeRodPacket> {
|
||||
|
||||
override fun handle(packet: CastPokeRodPacket, server: MinecraftServer, player: ServerPlayer) {
|
||||
val hand = if (packet.mainHand) InteractionHand.MAIN_HAND else InteractionHand.OFF_HAND
|
||||
(player.getItemInHand(hand).item as? PokerodItem)?.let { pokerod ->
|
||||
if (player.level().isServerSide()) pokerod.attemptCast(
|
||||
player.level(),
|
||||
player,
|
||||
hand,
|
||||
player.getItemInHand(hand)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.net.serverhandling.pokerod
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.item.PokeBallItem
|
||||
import com.cobblemon.mod.common.item.interactive.PokerodItem
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeball.SendOutPokeBallPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokerod.CastPokeRodPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokerod.ReelPokeRodPacket
|
||||
import com.cobblemon.mod.common.util.isServerSide
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.InteractionHand
|
||||
|
||||
object ReelPokeRodHandler : ServerNetworkPacketHandler<ReelPokeRodPacket> {
|
||||
|
||||
override fun handle(packet: ReelPokeRodPacket, server: MinecraftServer, player: ServerPlayer) {
|
||||
val hand = if (packet.mainHand) InteractionHand.MAIN_HAND else InteractionHand.OFF_HAND
|
||||
(player.getItemInHand(hand).item as? PokerodItem)?.let { pokerod ->
|
||||
if (player.level().isServerSide()) pokerod.attemptReel(
|
||||
player.level(),
|
||||
player,
|
||||
hand,
|
||||
player.getItemInHand(hand)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+2
-7
@@ -9,7 +9,6 @@
|
||||
package com.cobblemon.mod.common.net.serverhandling.riding
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.api.riding.RidingStyle
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.net.messages.server.riding.DismountPokemonPacket
|
||||
import net.minecraft.server.MinecraftServer
|
||||
@@ -26,16 +25,12 @@ object DismountPokemonPacketHandler : ServerNetworkPacketHandler<DismountPokemon
|
||||
|
||||
val pokemon = player.vehicle as PokemonEntity
|
||||
if (!canPlayerStopRidingPokemon(pokemon, player)) return
|
||||
|
||||
val isRidingInAir = pokemon.ridingController?.context?.style == RidingStyle.AIR
|
||||
|
||||
if (pokemon.controllingPassenger == player) {
|
||||
pokemon.passengers.forEach { it.stopRiding() }
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
player.stopRiding()
|
||||
}
|
||||
|
||||
if (isRidingInAir) pokemon.setFlying(true)
|
||||
}
|
||||
|
||||
private fun canPlayerStopRidingPokemon(pokemon: PokemonEntity, player: ServerPlayer): Boolean {
|
||||
|
||||
@@ -32,6 +32,7 @@ data class RenderEvent(
|
||||
|
||||
/** Represents when a layer is rendered by [LevelRenderer.renderLevel]. Ordinal corresponds with its render order. */
|
||||
enum class Stage {
|
||||
AFTER_ENTITIES,
|
||||
TRANSLUCENT
|
||||
}
|
||||
}
|
||||
@@ -1437,11 +1437,6 @@ open class Pokemon : ShowdownIdentifiable {
|
||||
}
|
||||
|
||||
fun getOwnerEntity(): LivingEntity? {
|
||||
if (isClient) {
|
||||
val ownerUUID = entity?.ownerUUID ?: return null
|
||||
return entity?.level()?.getPlayerByUUID(ownerUUID)
|
||||
}
|
||||
|
||||
return storeCoordinates.get()?.let {
|
||||
if (isPlayerOwned()) {
|
||||
server()?.playerList?.getPlayer(it.store.uuid)
|
||||
|
||||
+7
-55
@@ -8,22 +8,13 @@
|
||||
|
||||
package com.cobblemon.mod.common.pokemon.adapters
|
||||
|
||||
import com.cobblemon.mod.common.api.interaction.InteractionEffect
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.adapters.RequirementAdapter
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
|
||||
import com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter
|
||||
import com.cobblemon.mod.common.pokemon.requirements.*
|
||||
import com.cobblemon.mod.common.util.adapters.*
|
||||
import com.cobblemon.mod.common.util.readString
|
||||
import com.cobblemon.mod.common.util.writeString
|
||||
import com.google.common.collect.HashBiMap
|
||||
import com.google.gson.*
|
||||
import net.minecraft.advancements.critereon.ItemPredicate
|
||||
import net.minecraft.network.FriendlyByteBuf
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.util.LowerCaseEnumTypeAdapterFactory
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@@ -69,60 +60,21 @@ object CobblemonRequirementAdapter : RequirementAdapter {
|
||||
this.registerType(OwnerHoldsItemRequirement.ADAPTER_VARIANT, OwnerHoldsItemRequirement::class)
|
||||
}
|
||||
|
||||
val gson: Gson by lazy {
|
||||
GsonBuilder()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
|
||||
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
|
||||
.registerTypeAdapter(Requirement::class.java, CobblemonRequirementAdapter)
|
||||
.registerTypeAdapter(InteractionEffect::class.java, InteractionEffectAdapter)
|
||||
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
|
||||
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
|
||||
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory())
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun <T : Requirement> registerType(id: String, type: KClass<T>) {
|
||||
this.types[id.lowercase()] = type
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Requirement {
|
||||
val variant = json.asJsonObject.get(VARIANT).asString.lowercase()
|
||||
val type = getRequirementType(variant)
|
||||
val type = this.types[variant] ?: throw IllegalArgumentException("Cannot resolve evolution requirement type for variant $variant")
|
||||
return context.deserialize(json, type.java)
|
||||
}
|
||||
|
||||
override fun serialize(requirement: Requirement, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
||||
val json = context.serialize(requirement, requirement::class.java).asJsonObject
|
||||
val variant = getVariant(requirement)
|
||||
override fun serialize(src: Requirement, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
||||
val json = context.serialize(src, src::class.java).asJsonObject
|
||||
val variant = this.types.inverse()[src::class] ?: throw IllegalArgumentException("Cannot resolve evolution requirement for type ${src::class.qualifiedName}")
|
||||
json.addProperty(VARIANT, variant)
|
||||
return json
|
||||
}
|
||||
|
||||
fun encode(requirement: Requirement, buffer: FriendlyByteBuf) {
|
||||
buffer.writeString(gson.toJson(requirement, Requirement::class.java))
|
||||
}
|
||||
|
||||
fun decode(buffer: FriendlyByteBuf): Requirement {
|
||||
return gson.fromJson(buffer.readString(), Requirement::class.java)
|
||||
}
|
||||
|
||||
private fun getVariant(requirement: Requirement): String {
|
||||
val variant = this.types.inverse()[requirement::class]
|
||||
if (variant == null) {
|
||||
throw IllegalArgumentException("Cannot resolve evolution requirement for type ${requirement::class.qualifiedName}")
|
||||
}
|
||||
|
||||
return variant
|
||||
}
|
||||
|
||||
private fun getRequirementType(variant: String): KClass<out Requirement> {
|
||||
val requirementType = this.types[variant]
|
||||
if (requirementType == null) {
|
||||
throw IllegalArgumentException("Cannot resolve evolution requirement type for variant $variant")
|
||||
}
|
||||
|
||||
return requirementType
|
||||
}
|
||||
}
|
||||
@@ -562,8 +562,8 @@ class OmniPathNodeMaker : NodeEvaluator() {
|
||||
//// PathType.WALKABLE
|
||||
//// } else if (blockState.canPathfindThrough(world, pos, NavigationType.AIR) && !blockStateBelow.isSideSolid(world, below, Direction.UP, SideShapeType.FULL)) {
|
||||
//// PathType.OPEN
|
||||
if (canPathThroughLeaves() && blockState.block == CobblemonBlocks.SACCHARINE_LEAVES)
|
||||
return PathType.OPEN
|
||||
// } else if (blockState.`is`(BlockTags.LEAVES) && blockState.block == CobblemonBlocks.SACCHARINE_LEAVES && canPathThroughLeaves()) {
|
||||
// return PathType.OPEN
|
||||
// } else PathType.BLOCKED
|
||||
|
||||
// This is the 1.6.1 logic but with some optimizations
|
||||
@@ -633,6 +633,9 @@ class OmniPathNodeMaker : NodeEvaluator() {
|
||||
pfContext, x, y, z, sizeX, sizeY, sizeZ, canOpenDoors, canPassDoors, set, PathType.BLOCKED, BlockPos(x, y, z), returnedEarlyPathType
|
||||
)
|
||||
|
||||
if (PathType.LEAVES in set) {
|
||||
return PathType.OPEN
|
||||
}
|
||||
|
||||
if (PathType.FENCE in set) {
|
||||
return PathType.FENCE
|
||||
@@ -752,7 +755,7 @@ class OmniPathNodeMaker : NodeEvaluator() {
|
||||
} else if (type == PathType.RAIL && block !is BaseRailBlock && pfContext.getCachedBlockState(pos.below()).block !is BaseRailBlock) {
|
||||
PathType.UNPASSABLE_RAIL
|
||||
} else if (type == PathType.LEAVES) {
|
||||
if (canPathThroughLeaves() && blockState.block === CobblemonBlocks.SACCHARINE_LEAVES) {
|
||||
if (blockState.block === CobblemonBlocks.SACCHARINE_LEAVES && canPathThroughLeaves()) {
|
||||
PathType.OPEN
|
||||
} else {
|
||||
PathType.BLOCKED
|
||||
|
||||
+8
-4
@@ -6,15 +6,19 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.pokemon.properties
|
||||
package com.cobblemon.mod.common.pokemon.properties.tags
|
||||
|
||||
import com.cobblemon.mod.common.api.properties.CustomPokemonPropertyType
|
||||
import com.cobblemon.mod.common.pokemon.properties.StringProperty
|
||||
|
||||
object LabelProperty : CustomPokemonPropertyType<StringProperty> {
|
||||
override val keys = setOf("label", "tag")
|
||||
object PokemonFlagProperty : CustomPokemonPropertyType<StringProperty> {
|
||||
|
||||
private const val KEY = "tag"
|
||||
|
||||
override val keys = setOf(KEY)
|
||||
override val needsKey = true
|
||||
|
||||
override fun fromString(value: String?) = if (value == null) null else StringProperty(keys.first(), value, { _, _ -> }, { pokemon, underlyingValue -> pokemon.hasLabels(underlyingValue) })
|
||||
override fun fromString(value: String?) = if (value == null) null else StringProperty(KEY, value, { _, _ -> }, { pokemon, underlyingValue -> pokemon.hasLabels(underlyingValue) })
|
||||
|
||||
override fun examples() = emptySet<String>()
|
||||
}
|
||||
+4
-7
@@ -11,9 +11,9 @@ package com.cobblemon.mod.common.pokemon.requirements
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.OwnerQueryRequirement
|
||||
import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import kotlin.collections.iterator
|
||||
|
||||
/**
|
||||
* An [com.cobblemon.mod.common.api.pokemon.requirement.Requirement] that checks if the player has a certain [net.minecraft.advancements.Advancement]
|
||||
@@ -26,16 +26,13 @@ class AdvancementRequirement(val requiredAdvancement: ResourceLocation) : OwnerQ
|
||||
|
||||
override fun checkPlayer(
|
||||
pokemon: Pokemon,
|
||||
owner: Player
|
||||
owner: ServerPlayer
|
||||
): Boolean {
|
||||
val serverPlayer = owner as? ServerPlayer ?: return false
|
||||
|
||||
for (entry in serverPlayer.advancements.progress) {
|
||||
for (entry in owner.advancements.progress) {
|
||||
if (entry.key.id == requiredAdvancement && entry.value.isDone) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -12,9 +12,11 @@ import com.cobblemon.mod.common.api.pokemon.requirement.OwnerQueryRequirement
|
||||
import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.advancements.critereon.ItemPredicate
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import kotlin.collections.iterator
|
||||
|
||||
/**
|
||||
* An [OwnerQueryRequirement] that checks if the player/npc is holding a certain [ItemPredicate]
|
||||
@@ -28,7 +30,7 @@ class OwnerHoldsItemRequirement(val itemCondition: ItemPredicate)
|
||||
|
||||
override fun checkPlayer(
|
||||
pokemon: Pokemon,
|
||||
owner: Player
|
||||
owner: ServerPlayer
|
||||
): Boolean {
|
||||
return checkItem(owner)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.util
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.joml.Vector3f
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* For conversion from Vec3dto BlockPos, loses accuracy
|
||||
@@ -22,9 +23,17 @@ fun Vec3.toBlockPos(): BlockPos {
|
||||
fun Vec3.toVec3f(): Vector3f = Vector3f(x.toFloat(), y.toFloat(), z.toFloat())
|
||||
fun Vector3f.toVec3d(): Vec3 =
|
||||
Vec3(x.toDouble(), y.toDouble(), z.toDouble())
|
||||
|
||||
fun Vector3f.set(vec3d: Vec3): Vector3f {
|
||||
x = vec3d.x.toFloat()
|
||||
y = vec3d.y.toFloat()
|
||||
z = vec3d.z.toFloat()
|
||||
return this
|
||||
}
|
||||
|
||||
fun Vector3f.distance(other: Vector3f): Float {
|
||||
val dx = other.x - x
|
||||
val dy = other.y - y
|
||||
val dz = other.z - z
|
||||
return sqrt(dx * dx + dy * dy + dz * dz)
|
||||
}
|
||||
+8
-12
@@ -8,22 +8,22 @@
|
||||
|
||||
package com.cobblemon.mod.common.util.adapters
|
||||
|
||||
import com.google.gson.*
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import com.mojang.serialization.Codec
|
||||
import com.mojang.serialization.JsonOps
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class CodecBackedAdapter<T>(val codec: Codec<T>) : JsonDeserializer<T>, JsonSerializer<T> {
|
||||
|
||||
override fun deserialize(
|
||||
jElement: JsonElement,
|
||||
typeOfT: Type,
|
||||
context: JsonDeserializationContext
|
||||
): T {
|
||||
val ops = RegistryOpsProvider.getOpsWithDefaultFallback()
|
||||
val result = ops.withDecoder(codec).apply(jElement)
|
||||
return result.result().orElseThrow {
|
||||
IllegalStateException("Failed to deserialize $jElement: ${result.error().orElse(null)}")
|
||||
}.first
|
||||
return JsonOps.INSTANCE.withDecoder(codec).apply(jElement).result().get().first as T
|
||||
}
|
||||
|
||||
override fun serialize(
|
||||
@@ -31,10 +31,6 @@ class CodecBackedAdapter<T>(val codec: Codec<T>) : JsonDeserializer<T>, JsonSeri
|
||||
typeOfSrc: Type?,
|
||||
context: JsonSerializationContext?
|
||||
): JsonElement {
|
||||
val ops = RegistryOpsProvider.getOpsWithDefaultFallback()
|
||||
val result = ops.withEncoder(codec).apply(src)
|
||||
return result.result().orElseThrow {
|
||||
IllegalStateException("Failed to serialize $src: ${result.error().orElse(null)}")
|
||||
}
|
||||
return JsonOps.INSTANCE.withEncoder(codec).apply(src).result().get()
|
||||
}
|
||||
}
|
||||
@@ -1,41 +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.util.adapters
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.Environment
|
||||
import com.cobblemon.mod.common.util.server
|
||||
import com.google.gson.JsonElement
|
||||
import com.mojang.serialization.DynamicOps
|
||||
import com.mojang.serialization.JsonOps
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.resources.RegistryOps
|
||||
|
||||
/**
|
||||
* Utility for obtaining RegistryOps with the correct client/server registry access.
|
||||
* Used when manually encoding/decoding registry-backed values (e.g., items, predicates).
|
||||
*/
|
||||
object RegistryOpsProvider {
|
||||
fun getOpsWithDefaultFallback(): DynamicOps<JsonElement> {
|
||||
if (Cobblemon.implementation.environment() == Environment.CLIENT) {
|
||||
val minecraft = Minecraft.getInstance()
|
||||
if (minecraft != null && minecraft.level != null)
|
||||
return RegistryOps.create(JsonOps.INSTANCE, minecraft.level!!.registryAccess())
|
||||
}
|
||||
|
||||
|
||||
if (Cobblemon.implementation.environment() == Environment.SERVER) {
|
||||
val server = server()
|
||||
if (server != null)
|
||||
return RegistryOps.create(JsonOps.INSTANCE, server.registryAccess())
|
||||
}
|
||||
|
||||
return JsonOps.INSTANCE
|
||||
}
|
||||
}
|
||||
@@ -820,14 +820,6 @@
|
||||
"cobblemon.cooking.cooking_quality.medium": "Medium",
|
||||
"cobblemon.cooking.cooking_quality.high": "High",
|
||||
|
||||
"effect.cobblemon.cleanse_all": "Effect Cleanse",
|
||||
"effect.cobblemon.cleanse_negative": "Negative Effect Cleanse",
|
||||
"effect.cobblemon.mental_restoration": "Mental Restoration",
|
||||
|
||||
"effect.cobblemon.cleanse_all": "Effect Cleanse",
|
||||
"effect.cobblemon.cleanse_negative": "Negative Effect Cleanse",
|
||||
"effect.cobblemon.mental_restoration": "Mental Restoration",
|
||||
|
||||
"cobblemon.fossil.armor": "Armor",
|
||||
"cobblemon.fossil.bird": "Bird",
|
||||
"cobblemon.fossil.claw": "Claw",
|
||||
@@ -2629,8 +2621,6 @@
|
||||
"cobblemon.config.ui.infinite_ride_stamina.tooltip": "Whether rideable Pokémon have infinite stamina.",
|
||||
"cobblemon.config.ui.remember_riding_camera": "Remember Riding Camera",
|
||||
"cobblemon.config.ui.remember_riding_camera.tooltip": "Whether the camera mode will automatically switch to the last used camera mode when mounting a rideable Pokémon.",
|
||||
"cobblemon.config.ui.enable_in_flight_dismounting": "Enable In-Flight Dismounting",
|
||||
"cobblemon.config.ui.enable_in_flight_dismounting.tooltip": "Allows players to dismount while riding a Pokémon in the air. When disabled, dismounting is only possible while on the ground.",
|
||||
|
||||
"cobblemon.config.ui.allow_starter_on_join": "Allow Starter on Join",
|
||||
"cobblemon.config.ui.prompt_starter_once_only": "Prompt Starter Once Only",
|
||||
@@ -3525,7 +3515,6 @@
|
||||
"tag.item.cobblemon.plants": "Plants",
|
||||
"tag.item.cobblemon.seeds": "Seeds",
|
||||
"tag.item.cobblemon.boats": "Boats",
|
||||
"tag.item.cobblemon.chest_boats": "Chest Boats",
|
||||
"tag.item.cobblemon.hanging_signs": "Hanging Signs",
|
||||
"tag.item.cobblemon.signs": "Signs",
|
||||
"tag.item.cobblemon.dawn_stone_ores": "Dawn Stone Ores",
|
||||
@@ -9456,8 +9445,5 @@
|
||||
"stat.cobblemon.traded": "Pokémon Traded",
|
||||
"stat.cobblemon.times_ridden": "Pokémon Ridden",
|
||||
"stat.cobblemon.rod_casts": "PokéRod Casts",
|
||||
"stat.cobblemon.reel_ins": "PokéRod Catches",
|
||||
"stat.cobblemon.riding_land": "Distance by Pokémon on Land",
|
||||
"stat.cobblemon.riding_air": "Distance by Pokémon in Air",
|
||||
"stat.cobblemon.riding_liquid": "Distance by Pokémon in Liquid"
|
||||
"stat.cobblemon.reel_ins": "PokéRod Catches"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"parent": "minecraft:builtin/entity",
|
||||
"gui_light": "side",
|
||||
"textures": {
|
||||
"particle": "cobblemon:item/poke_balls/cherish_ball"
|
||||
}
|
||||
"gui_light": "side"
|
||||
}
|
||||
@@ -1517,139 +1517,76 @@
|
||||
{ "name": "cobblemon:animation/wings/steel_wing_flap_medium_8", "volume": 0.25 }
|
||||
]
|
||||
},
|
||||
"ride.loop.wind.mono": {
|
||||
"ride.loop.wind": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/wind_loop_mono",
|
||||
"name": "cobblemon:ride/loop/wind_loop",
|
||||
"stream": true,
|
||||
"attenuation_distance": 48.0,
|
||||
"volume": 0.15
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.wind.stereo": {
|
||||
"ride.loop.leather": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/wind_loop_stereo",
|
||||
"stream": true,
|
||||
"volume": 0.04
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.leather.mono": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/leather_flight_loop_mono",
|
||||
"name": "cobblemon:ride/loop/leather_flight_loop",
|
||||
"stream": true,
|
||||
"attenuation_distance": 48.0,
|
||||
"volume": 0.25
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.leather.stereo": {
|
||||
"ride.loop.plumage": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/leather_flight_loop_stereo",
|
||||
"stream": true,
|
||||
"volume": 0.05
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.plumage.mono": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/plumage_flight_loop_mono",
|
||||
"name": "cobblemon:ride/loop/plumage_flight_loop",
|
||||
"stream": true,
|
||||
"attenuation_distance": 48.0,
|
||||
"volume": 0.25
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.plumage.stereo": {
|
||||
"ride.loop.saucer": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/plumage_flight_loop_stereo",
|
||||
"stream": true,
|
||||
"volume": 0.05
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.saucer.mono": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/saucer_flight_loop_mono",
|
||||
"name": "cobblemon:ride/loop/saucer_flight_loop",
|
||||
"stream": true,
|
||||
"attenuation_distance": 48.0,
|
||||
"volume": 0.25
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.saucer.stereo": {
|
||||
"ride.loop.rocket": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/saucer_flight_loop_stereo",
|
||||
"stream": true,
|
||||
"volume": 0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.rocket.mono": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/rocket_flight_loop_mono",
|
||||
"name": "cobblemon:ride/loop/rocket_flight_loop",
|
||||
"stream": true,
|
||||
"attenuation_distance": 48.0,
|
||||
"volume": 0.4
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.rocket.stereo": {
|
||||
"ride.loop.water_surface": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/rocket_flight_loop_stereo",
|
||||
"stream": true,
|
||||
"volume": 0.3
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.water_surface.mono": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/water_surface_loop_mono",
|
||||
"name": "cobblemon:ride/loop/water_surface_loop",
|
||||
"stream": true,
|
||||
"attenuation_distance": 32.0,
|
||||
"volume": 0.4
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.water_surface.stereo": {
|
||||
"ride.loop.underwater": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/water_surface_loop_stereo",
|
||||
"stream": true,
|
||||
"volume": 0.13
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.underwater.mono": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/underwater_loop_mono",
|
||||
"name": "cobblemon:ride/loop/underwater_loop",
|
||||
"stream": true,
|
||||
"attenuation_distance": 16.0,
|
||||
"volume": 0.4
|
||||
}
|
||||
]
|
||||
},
|
||||
"ride.loop.underwater.stereo": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "cobblemon:ride/loop/underwater_loop_stereo",
|
||||
"stream": true,
|
||||
"volume": 0.2
|
||||
}
|
||||
]
|
||||
},
|
||||
"pokemon.bulbasaur.ambient": {
|
||||
"sounds": [ "cobblemon:pokemon/bulbasaur/bulbasaur_ambient" ]
|
||||
},
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user