Compare commits

...
Author SHA1 Message Date
terrainwax 767fc05387 feat: add test on weather and ensure weather is good 2026-02-25 22:26:43 +01:00
terrainwax 1941636113 feat: add a CompetitiveBattleAI WIP need more testing on 2v2 battle 2026-01-01 18:24:09 +01:00
Hiroku 1a3b655d3f Merge branch 'the-tiniest-of-tiny-changes' into 'main'
Edit pokemon_model.json

See merge request cable-mc/cobblemon!2091
2025-12-20 12:41:36 +00:00
dhi_holo 1f5f278970 Edit pokemon_model.json 2025-12-20 21:25:12 +10:00
Hiroku 0b4eb5239a Merge branch 'in-flight-dismounting-config' into 'main'
Add enable in-flight dismounting config

See merge request cable-mc/cobblemon!2079
2025-12-20 06:01:12 +00:00
lionandHiroku 118e8f6205 Add enable in-flight dismounting config 2025-12-20 06:01:12 +00:00
Hiroku cf126c5906 Merge branch 'molang-is-love-molang-is-life' into 'main'
Fixed the `run_script` Molang function to not fail if the environment's context is null.

See merge request cable-mc/cobblemon!2090
2025-12-20 01:59:22 +00:00
Spicy 645cf61911 Fixed the run_script Molang function to not fail if the environment's context is null. 2025-12-19 19:48:27 -06:00
Hiroku 87d5ede3dd Merge branch 'RestoreCombeeLeavesPathing' into 'main'
RestoreCombeeLeavesPathing

See merge request cable-mc/cobblemon!2046
2025-12-19 07:36:50 +00:00
Hiroku 203e5d89cd Merge branch 'label-consistency' into 'main'
Who are we if not consistent?

See merge request cable-mc/cobblemon!1985
2025-12-19 07:36:21 +00:00
JazzMcNade f6ad7ff865 Fix changelog 2025-12-17 21:08:46 -06:00
JazzMcNade 54e8763470 Fixed a bug in which Combees could become stuck inside of leaves 2025-12-13 21:17:59 -06:00
JazzMcNade db34687df0 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-13 21:11:57 -06:00
JazzMcNade 9dac604a93 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-10 00:20:49 -06:00
Neovitalism 25ce784a60 Hopefully fixes changelog
Idk why gitlab didn't detect that when merging main
2025-12-03 10:17:48 -06:00
Neovitalism 5358cccb62 Merge branch 'main' into label-consistency 2025-12-03 10:09:41 -06:00
JazzMcNade 698fa9dff9 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-01 00:46:50 -06:00
JazzMcNade a8949cf1f9 changelog 2025-11-25 21:49:38 -06:00
JazzMcNade 61db6334f9 Restores Combee pathing through leaves 2025-11-25 21:37:57 -06:00
Neovitalism 46d278ca55 key \:) 2025-11-17 03:18:02 -06:00
Neovitalism 339038956d Support for deserialization from any key
...I think.
2025-11-17 03:09:21 -06:00
Neovitalism 116a93d8cc Update CHANGELOG.md 2025-11-16 04:19:32 -06:00
Neovitalism acebc35d3f Who are we if not consistent?
Before, this property was called `PokemonFlagProperty`, had the key `tag`, and checked pokémon's labels. It was also in a package by itself.

Now, it's moved down into the properties package, renamed to LabelProperty, and the key "label" was added, keeping "tag" as a secondary for those that are using it currently.
2025-11-16 04:10:45 -06:00
28 changed files with 3791 additions and 63 deletions
+4
View File
@@ -15,11 +15,14 @@
- 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.
@@ -36,6 +39,7 @@
- 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.
@@ -91,7 +91,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.tags.PokemonFlagProperty
import com.cobblemon.mod.common.pokemon.properties.LabelProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.util.*
@@ -315,7 +315,7 @@ object Cobblemon {
CustomPokemonProperty.register(UncatchableProperty)
CustomPokemonProperty.register(BattleCloneProperty)
CustomPokemonProperty.register(PokemonFlagProperty)
CustomPokemonProperty.register(LabelProperty)
CustomPokemonProperty.register(HiddenAbilityPropertyType)
CustomPokemonProperty.register(AspectPropertyType)
CustomPokemonProperty.register(UnaspectPropertyType)
@@ -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 + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
(params.environment.context?.map ?: emptyMap()) + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
)
val script = params.getString(0).asIdentifierDefaultingNamespace()
// store the args in the
@@ -80,7 +80,9 @@ open class ChoiceSpeciesFeatureProvider(
internal constructor(): this(emptyList())
override fun get(pokemon: Pokemon) = pokemon.getFeature<StringSpeciesFeature>(keys.first())
override fun get(pokemon: Pokemon): StringSpeciesFeature? {
return pokemon.features.filterIsInstance<StringSpeciesFeature>().find { it.name in keys }
}
override fun invoke(pokemon: Pokemon): StringSpeciesFeature? {
val existing = get(pokemon)
@@ -101,15 +103,15 @@ open class ChoiceSpeciesFeatureProvider(
}
override fun invoke(nbt: CompoundTag): StringSpeciesFeature? {
return if (nbt.contains(keys.first())) {
StringSpeciesFeature(keys.first(), "").also { it.loadFromNBT(nbt) }
} else null
val key = keys.find { nbt.contains(it) }
if (key == null) return null
return StringSpeciesFeature(key, "").also { it.loadFromNBT(nbt) }
}
override fun invoke(json: JsonObject): StringSpeciesFeature? {
return if (json.has(keys.first())) {
StringSpeciesFeature(keys.first(), "").also { it.loadFromJSON(json) }
} else null
val key = keys.find { json.has(it) }
if (key == null) return null
return StringSpeciesFeature(key, "").also { it.loadFromJSON(json) }
}
override fun fromString(value: String?): StringSpeciesFeature? {
@@ -131,7 +133,7 @@ open class ChoiceSpeciesFeatureProvider(
override fun provide(properties: PokemonProperties): Set<String> {
return if (isAspect) {
val feature = properties.customProperties.filterIsInstance<StringSpeciesFeature>().find { it.name == keys.first() }
val feature = properties.customProperties.filterIsInstance<StringSpeciesFeature>().find { it.name in keys }
if (feature != null) {
setOf(getAspect(feature))
} else {
@@ -132,7 +132,9 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
constructor(vararg keys: String) : this(keys.toList())
override fun get(pokemon: Pokemon) = pokemon.getFeature<FlagSpeciesFeature>(keys.first())
override fun get(pokemon: Pokemon): FlagSpeciesFeature? {
return pokemon.features.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
}
override fun invoke(pokemon: Pokemon): FlagSpeciesFeature? {
return get(pokemon)
@@ -144,15 +146,15 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
}
override fun invoke(nbt: CompoundTag): FlagSpeciesFeature? {
return if (nbt.contains(keys.first())) {
FlagSpeciesFeature(keys.first(), false).also { it.loadFromNBT(nbt) }
} else null
val key = keys.find { nbt.contains(it) }
if (key == null) return null
return FlagSpeciesFeature(key, false).also { it.loadFromNBT(nbt) }
}
override fun invoke(json: JsonObject): FlagSpeciesFeature? {
return if (json.has(keys.first())) {
FlagSpeciesFeature(keys.first(), false).also { it.loadFromJSON(json) }
} else null
val key = keys.find { json.has(it) }
if (key == null) return null
return FlagSpeciesFeature(key, false).also { it.loadFromJSON(json) }
}
override fun fromString(value: String?): FlagSpeciesFeature? {
@@ -170,16 +172,26 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
}
override fun provide(pokemon: Pokemon): Set<String> {
return if (isAspect && pokemon.getFeature<FlagSpeciesFeature>(keys.first())?.enabled == true) {
setOf(keys.first())
return if (isAspect) {
val feature = pokemon.features.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
if (feature != null && feature.enabled) {
setOf(keys.first())
} else {
emptySet()
}
} else {
emptySet()
}
}
override fun provide(properties: PokemonProperties): Set<String> {
return if (isAspect && properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name == keys.first() }?.enabled == true) {
setOf(keys.first())
return if (isAspect) {
val feature = properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
if (feature != null && feature.enabled) {
setOf(keys.first())
} else {
emptySet()
}
} else {
emptySet()
}
@@ -142,19 +142,15 @@ class IntSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<IntSpeciesF
}
override fun invoke(nbt: CompoundTag): IntSpeciesFeature? {
return if (nbt.contains(keys.first())) {
IntSpeciesFeature(keys.first(), nbt.getInt(keys.first()))
} else {
null
}
val key = keys.find { nbt.contains(it) }
if (key == null) return null
return IntSpeciesFeature(keys.first(), nbt.getInt(key));
}
override fun invoke(json: JsonObject): IntSpeciesFeature? {
return if (json.has(keys.first())) {
IntSpeciesFeature(keys.first(), json.get(keys.first()).asInt)
} else {
null
}
val key = keys.find { json.has(it) }
if (key == null) return null
return IntSpeciesFeature(keys.first(), json.get(key).asInt)
}
override fun get(pokemon: Pokemon) = pokemon.features.filterIsInstance<IntSpeciesFeature>().find { it.name in keys }
@@ -0,0 +1,671 @@
package com.cobblemon.mod.common.battles.ai
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.ai.BattleAI
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.ai.competitive.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import kotlin.random.Random
class CompetitiveBattleAI(private val skill: Int) : BattleAI {
companion object {
private const val DEBUG_TOP_MOVES = 4
}
private val skillLevel = skill.coerceIn(0, 5)
private val aiDebugEnabled = isAiDebugEnabled()
private val battleTracker = EnhancedActiveTracker()
private val opponentPredictor = OpponentPredictor()
private val strategyAnalyzer = StrategyAnalyzer()
private lateinit var damageCalculator: DamageCalculator
private lateinit var moveEvaluator: MoveEvaluator
private lateinit var switchDecisionMaker: SwitchDecisionMaker
private lateinit var doublesDecisionMaker: DoublesDecisionMaker
private var currentBattle: PokemonBattle? = null
private var currentGamePlan: GamePlan? = null
private var battleState: BattleState? = null
private var lastMoveEvaluation: MoveEvaluator.MoveEvaluation? = null
private var isDoublesMode: Boolean = false
private var currentSlot: Int = 0
private var currentTurn: Int = -1
private val turnDecisions: MutableMap<Int, DoublesDecisionMaker.MoveTargetPair> = mutableMapOf()
private var firstSlotDecided: Boolean = false
override fun choose(
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle,
aiSide: BattleSide,
moveset: ShowdownMoveset?,
forceSwitch: Boolean
): ShowdownActionResponse {
try {
initializeComponents(battle)
updateBattleState(activeBattlePokemon, battle)
if (currentGamePlan == null) {
val npcTeam = battle.side2.actors.first().pokemonList.map { it.effectedPokemon }
currentGamePlan = strategyAnalyzer.analyzeTeamComposition(npcTeam)
}
if (battle.turn != currentTurn) {
currentTurn = battle.turn
turnDecisions.clear()
firstSlotDecided = false
}
if (forceSwitch || activeBattlePokemon.isGone()) {
return handleForcedSwitch(activeBattlePokemon, battle)
}
if (!passesSkillCheck()) {
debugLog("Skill check failed; selecting random move.")
return makeRandomMove(moveset, activeBattlePokemon)
}
updateOpponentModel(battle)
if (isDoublesMode) {
return chooseForDoubles(activeBattlePokemon, battle, moveset)
}
if (shouldAttemptSwitch() && shouldSwitch(activeBattlePokemon, battle)) {
val switchTarget = findBestSwitch(activeBattlePokemon, battle)
if (switchTarget != null) {
switchTarget.willBeSwitchedIn = true
logSwitchDecision("voluntary_switch", battle, listOf(switchTarget), switchTarget)
return SwitchActionResponse(switchTarget.uuid)
}
}
val bestMove = chooseBestMove(moveset, activeBattlePokemon, battle)
logMoveDecision("move_select", bestMove, activeBattlePokemon)
return executeMove(bestMove, activeBattlePokemon)
} catch (e: Exception) {
e.printStackTrace()
return PassActionResponse
}
}
private fun chooseForDoubles(
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle,
moveset: ShowdownMoveset?
): ShowdownActionResponse {
val state = battleState ?: return PassActionResponse
val plan = currentGamePlan ?: return PassActionResponse
val myPokemon = battleTracker.p2Active.getActiveAt(currentSlot) ?: return PassActionResponse
val partnerSlot = 1 - currentSlot
val partner = battleTracker.p2Active.getActiveAt(partnerSlot)
val opponents = battleTracker.p1Active.getAllActive()
val decision = doublesDecisionMaker.findBestMoveAndTarget(
moveset,
activeBattlePokemon,
myPokemon,
opponents,
partner,
state,
plan
)
if (decision == null) {
debugLog("Doubles: no evaluated option; selecting random move.")
return makeRandomMove(moveset, activeBattlePokemon)
}
turnDecisions[currentSlot] = decision
firstSlotDecided = true
debugLog(
"Doubles: chose move=${decision.move.id} target=${decision.targetPNX ?: "auto"} " +
"score=${"%.2f".format(decision.evaluation.totalScore)} " +
"expectedDamage=${"%.1f".format(decision.evaluation.expectedDamage)} " +
"strategic=${"%.1f".format(decision.evaluation.strategicValue)} " +
"risk=${"%.2f".format(decision.evaluation.riskFactor)}"
)
return executeDoublesMove(decision, activeBattlePokemon)
}
private fun executeDoublesMove(
decision: DoublesDecisionMaker.MoveTargetPair,
activeBattlePokemon: ActiveBattlePokemon
): ShowdownActionResponse {
return if (decision.targetPNX != null) {
MoveActionResponse(decision.move.id, decision.targetPNX)
} else {
MoveActionResponse(decision.move.id)
}
}
private fun initializeComponents(battle: PokemonBattle) {
if (currentBattle != battle) {
currentBattle = battle
isDoublesMode = battle.format.battleType.pokemonPerSide > 1
damageCalculator = DamageCalculator(battle)
moveEvaluator = MoveEvaluator(damageCalculator, opponentPredictor, isDoublesMode)
switchDecisionMaker = SwitchDecisionMaker(damageCalculator, opponentPredictor, strategyAnalyzer)
doublesDecisionMaker = DoublesDecisionMaker(moveEvaluator, damageCalculator)
}
}
private fun updateBattleState(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle) {
val request = activeBattlePokemon.actor.request ?: return
// Determine current slot from PNX (e.g., "p2a" = slot 0, "p2b" = slot 1)
val pnx = activeBattlePokemon.getPNX()
currentSlot = if (pnx.length >= 3) pnx[2] - 'a' else 0
if (isDoublesMode) {
battle.side1.activePokemon.forEachIndexed { index, active ->
active.battlePokemon?.let { pokemon ->
battleTracker.updatePokemonSlot(battleTracker.p1Active, index, pokemon)
}
}
battle.side2.activePokemon.forEachIndexed { index, active ->
active.battlePokemon?.let { pokemon ->
battleTracker.updatePokemonSlot(battleTracker.p2Active, index, pokemon)
}
}
battleTracker.p1Active.activePokemon =
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
battleTracker.p2Active.activePokemon =
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
} else {
battle.side1.activePokemon.firstOrNull()?.battlePokemon?.let { playerPokemon ->
battleTracker.updatePokemon(battleTracker.p1Active.activePokemon, playerPokemon)
battleTracker.updateParty(battleTracker.p1Active, battleTracker.p1Active.activePokemon)
}
activeBattlePokemon.battlePokemon?.let { aiPokemon ->
battleTracker.updatePokemon(battleTracker.p2Active.activePokemon, aiPokemon)
battleTracker.updateParty(battleTracker.p2Active, battleTracker.p2Active.activePokemon)
}
}
updateRevealedMoves(battle)
battleState = createBattleState(battle, activeBattlePokemon)
}
private fun createBattleState(battle: PokemonBattle, activeBattlePokemon: ActiveBattlePokemon): BattleState? {
val p1Actor = battle.side1.actors.firstOrNull() ?: return null
val p2Actor = battle.side2.actors.firstOrNull() ?: return null
val playerActiveTracker = if (isDoublesMode) {
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
} else {
battleTracker.p1Active.activePokemon
}
val npcActiveTracker = if (isDoublesMode) {
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
} else {
battleTracker.p2Active.activePokemon
}
val playerActive = playerActiveTracker.pokemon ?: return null
val npcActive = npcActiveTracker.pokemon ?: return null
val playerActiveSlots = battleTracker.p1Active.getAllActive()
.mapNotNull { it.pokemon }
.ifEmpty { listOf(playerActive) }
val npcActiveSlots = battleTracker.p2Active.getAllActive()
.mapNotNull { it.pokemon }
.ifEmpty { listOf(npcActive) }
val partnerSlot = if (isDoublesMode) {
val mySlot = currentSlot
1 - mySlot
} else {
null
}
return BattleState(
turn = battle.turn,
playerTeam = p1Actor.pokemonList.map { it.effectedPokemon },
npcTeam = p2Actor.pokemonList.map { it.effectedPokemon },
playerActive = playerActive,
npcActive = npcActive,
weather = getWeather(battle),
terrain = getTerrain(battle),
playerHazards = getHazards(battle.side1),
npcHazards = getHazards(battle.side2),
playerBoosts = battleTracker.p1Active.activePokemon.boosts,
npcBoosts = battleTracker.p2Active.activePokemon.boosts,
battleContext = battle.contextManager,
isDoubles = isDoublesMode,
playerActiveSlots = playerActiveSlots,
npcActiveSlots = npcActiveSlots,
mySlotIndex = currentSlot,
partnerSlotIndex = partnerSlot
)
}
private fun handleForcedSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): ShowdownActionResponse {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
if (availableSwitches.isEmpty()) {
return ForfeitActionResponse()
}
val state = battleState
val plan = currentGamePlan
val bestSwitch = if (state != null && plan != null) {
switchDecisionMaker.findBestSwitch(
availableSwitches,
battleTracker.p1Active.activePokemon,
state,
plan
)
} else {
null
}
val switchTarget = bestSwitch ?: availableSwitches.first()
switchTarget.willBeSwitchedIn = true
logSwitchDecision("forced_switch", battle, availableSwitches, switchTarget)
return SwitchActionResponse(switchTarget.uuid)
}
private fun shouldSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): Boolean {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
if (availableSwitches.isEmpty()) return false
val state = battleState ?: return false
val plan = currentGamePlan ?: return false
return switchDecisionMaker.shouldVoluntarilySwitch(
battleTracker.p2Active.activePokemon,
battleTracker.p1Active.activePokemon,
state,
plan,
availableSwitches
)
}
private fun findBestSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): BattlePokemon? {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
val state = battleState ?: return null
val plan = currentGamePlan ?: return null
return switchDecisionMaker.findBestSwitch(
availableSwitches,
battleTracker.p1Active.activePokemon,
state,
plan
)
}
private fun chooseBestMove(
moveset: ShowdownMoveset?,
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle
): InBattleMove? {
if (moveset == null) return null
val usableMoves = moveset.moves.filter { it.canBeUsed() }
if (usableMoves.isEmpty()) return null
val state = battleState ?: return null
val plan = currentGamePlan ?: return null
val evaluations = usableMoves.map { move ->
moveEvaluator.evaluateMove(
move,
battleTracker.p2Active.activePokemon,
battleTracker.p1Active.activePokemon,
state,
plan
)
}
val bestEvaluation = evaluations.maxByOrNull { it.totalScore }
logMoveRanking(evaluations)
lastMoveEvaluation = bestEvaluation
return bestEvaluation?.move
}
private fun executeMove(move: InBattleMove?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
if (move == null) {
return makeRandomMove(null, activeBattlePokemon)
}
val target = if (move.mustBeUsed()) {
null
} else {
move.target.targetList(activeBattlePokemon)
}
return if (target == null) {
MoveActionResponse(move.id)
} else {
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull()
?: target.random()
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
}
}
private fun makeRandomMove(moveset: ShowdownMoveset?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
if (moveset == null) return PassActionResponse
val usableMoves = moveset.moves.filter { it.canBeUsed() }
val move = usableMoves.randomOrNull() ?: return PassActionResponse
val target = if (move.mustBeUsed()) null else move.target.targetList(activeBattlePokemon)
return if (target == null) {
MoveActionResponse(move.id)
} else {
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull()
?: target.random()
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
}
}
private fun updateOpponentModel(battle: PokemonBattle) {
val situation = categorizeSituation(battleState ?: return)
val opponentActives = if (isDoublesMode) {
battleTracker.p1Active.getAllActive()
} else {
listOf(battleTracker.p1Active.activePokemon)
}
for (opponentActive in opponentActives) {
for (move in opponentActive.revealedMoves) {
opponentPredictor.updateMoveHistory(
opponentActive.species ?: "unknown",
move,
situation.name.lowercase()
)
}
}
}
override fun onHealthChange(packet: BattleHealthChangePacket) {
try {
val battle = currentBattle ?: return
val pnx = packet.pnx
val activePokemon = battle.activePokemon.find { it.getPNX() == pnx } ?: return
val newHealth = packet.newHealth.toInt()
val isPlayerPokemon = activePokemon.actor == battle.side1.actors.firstOrNull()
val teamTracker = if (isPlayerPokemon) {
battleTracker.p1Active
} else {
battleTracker.p2Active
}
val tracker = battleTracker.getTrackedPokemonByPNX(pnx, teamTracker) ?: teamTracker.activePokemon
val oldHealth = tracker.currentHp
val hpChange = newHealth - oldHealth
tracker.previousHp = oldHealth
tracker.currentHp = newHealth
tracker.lastHpChange = hpChange
tracker.currentHpPercent = if (tracker.maxHp > 0) newHealth.toDouble() / tracker.maxHp else 0.0
updateMomentum(hpChange, isPlayerPokemon)
detectCriticalThresholds(tracker, oldHealth, newHealth, isPlayerPokemon)
} catch (e: Exception) {
}
}
private fun updateMomentum(hpChange: Int, isPlayerPokemon: Boolean) {
val momentumChange = when {
hpChange > 0 -> -5.0 // Opponent healing shifts momentum toward them
hpChange < 0 -> kotlin.math.abs(hpChange) / 10.0 // Damage shifts momentum
else -> 0.0
}
if (isPlayerPokemon) {
// Player took damage = AI momentum increases
battleTracker.p2Active.momentum += momentumChange
battleTracker.p1Active.momentum -= momentumChange
} else {
// AI took damage = Player momentum increases
battleTracker.p2Active.momentum -= momentumChange
battleTracker.p1Active.momentum += momentumChange
}
// Clamp momentum to -100 to 100
battleTracker.p1Active.momentum = battleTracker.p1Active.momentum.coerceIn(-100.0, 100.0)
battleTracker.p2Active.momentum = battleTracker.p2Active.momentum.coerceIn(-100.0, 100.0)
}
private fun detectCriticalThresholds(
tracker: TrackedPokemon,
oldHealth: Int,
newHealth: Int,
isPlayerPokemon: Boolean
) {
if (newHealth <= 0 && oldHealth > 0) {
if (isPlayerPokemon) {
battleTracker.p2Active.momentum += 20.0
} else {
battleTracker.p1Active.momentum += 20.0
}
}
}
private fun categorizeSituation(battleState: BattleState): BattleSituation {
val myHP = battleState.npcActive.currentHealth.toDouble() / battleState.npcActive.maxHealth
val oppHP = battleState.playerActive.currentHealth.toDouble() / battleState.playerActive.maxHealth
return when {
myHP < 0.25 -> BattleSituation.DESPERATE
oppHP < 0.25 -> BattleSituation.WINNING
myHP > 0.8 && oppHP > 0.8 -> BattleSituation.EARLY_GAME
battleState.npcTeam.count { it.currentHealth > 0 } <= 2 -> BattleSituation.ENDGAME
battleState.playerBoosts.values.any { it >= 2 } -> BattleSituation.OPPONENT_SETUP
battleState.npcBoosts.values.any { it >= 2 } -> BattleSituation.MY_SETUP
battleState.weather != null -> BattleSituation.WEATHER_CONTROL
else -> BattleSituation.NEUTRAL
}
}
private fun getAvailableSwitches(activeBattlePokemon: ActiveBattlePokemon): List<BattlePokemon> {
val currentUuid = activeBattlePokemon.battlePokemon?.uuid
return activeBattlePokemon.actor.pokemonList.filter {
it.health > 0 &&
it.uuid != currentUuid &&
try { it.canBeSentOut() } catch (e: Exception) { true }
}
}
private fun passesSkillCheck(): Boolean {
if (skillLevel == 5) return true
if (skillLevel == 0) return false
val baseChance = skillLevel * 20
val turnBonus = if ((battleState?.turn ?: 0) > 10) 10 else 0
return Random.nextInt(100) < (baseChance + turnBonus)
}
private fun shouldAttemptSwitch(): Boolean {
return when (skillLevel) {
in 0..2 -> false
3 -> Random.nextDouble() < 0.25
4 -> Random.nextDouble() < 0.70
5 -> true
else -> false
}
}
private fun getWeather(battle: PokemonBattle): String? {
return try {
battle.contextManager.get(BattleContext.Type.WEATHER)
?.firstOrNull()?.id
} catch (e: Exception) {
null
}
}
private fun getTerrain(battle: PokemonBattle): String? {
return try {
battle.contextManager.get(BattleContext.Type.TERRAIN)
?.firstOrNull()?.id
} catch (e: Exception) {
null
}
}
private fun getHazards(side: BattleSide): List<String> {
return try {
side.contextManager.get(BattleContext.Type.HAZARD)
?.map { it.id } ?: emptyList()
} catch (e: Exception) {
emptyList()
}
}
private fun updateRevealedMoves(battle: PokemonBattle) {
updateTeamRevealedMoves(battle, battleTracker.p1Active)
updateTeamRevealedMoves(battle, battleTracker.p2Active)
}
private fun updateTeamRevealedMoves(battle: PokemonBattle, team: TeamTracker) {
val active = team.getAllActive().ifEmpty { listOf(team.activePokemon) }
for (tracked in active) {
val pokemon = tracked.pokemon ?: continue
val majorMessage = battle.majorBattleActions[pokemon.uuid]
val minorMessage = battle.minorBattleActions[pokemon.uuid]
val candidates = listOfNotNull(majorMessage, minorMessage)
for (message in candidates) {
val id = message.id.lowercase()
if (id != "move" && id != "-singlemove" && id != "singlemove") continue
val moveId = message.effectAt(1)?.id?.lowercase() ?: continue
tracked.revealedMoves.add(moveId)
}
}
}
private fun logMoveDecision(
reason: String,
move: InBattleMove?,
activeBattlePokemon: ActiveBattlePokemon
) {
if (!aiDebugEnabled || move == null) return
val eval = lastMoveEvaluation
val self = activeBattlePokemon.battlePokemon?.effectedPokemon?.species?.name ?: "unknown"
val opp = battleTracker.p1Active.activePokemon.species ?: "unknown"
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
if (eval == null) {
debugLog("Decision=$reason self=$self opp=$opp move=${move.id} plan=$plan")
return
}
debugLog(
"Decision=$reason self=$self opp=$opp move=${move.id} plan=$plan " +
"score=${"%.2f".format(eval.totalScore)} " +
"expectedDamage=${"%.1f".format(eval.expectedDamage)} " +
"strategic=${"%.1f".format(eval.strategicValue)} " +
"risk=${"%.2f".format(eval.riskFactor)}"
)
}
private fun logMoveRanking(evaluations: List<MoveEvaluator.MoveEvaluation>) {
if (!aiDebugEnabled || evaluations.isEmpty()) return
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
val situation = battleState?.let { categorizeSituation(it).name } ?: "unknown"
val top = evaluations.sortedByDescending { it.totalScore }.take(DEBUG_TOP_MOVES)
val state = battleState
val planData = currentGamePlan
val attacker = battleTracker.p2Active.activePokemon
val defender = battleTracker.p1Active.activePokemon
val details = top.joinToString(", ") { eval ->
val debug = if (state != null && planData != null) {
moveEvaluator.buildDebugDetails(eval.move, attacker, defender, state, planData)
} else {
null
}
val flags = if (debug != null) {
listOf(
"stab=${debug.hasStab}",
"eff=${"%.2f".format(debug.effectiveness)}",
"acc=${"%.2f".format(debug.accuracy)}",
"prio=${debug.priority}",
"status=${debug.isStatusMove}",
"setup=${debug.isSetup}",
"hazardSet=${debug.setsHazards}",
"hazardClear=${debug.removesHazards}",
"weather=${debug.setsWeather}",
"recovery=${debug.isRecovery}",
"pivot=${debug.isPivot}",
"risky=${debug.isRisky}"
).joinToString(" ")
} else {
"n/a"
}
"${eval.move.id}:" +
"score=${"%.2f".format(eval.totalScore)} " +
"raw=${"%.1f".format(eval.rawDamage)} " +
"dmg=${"%.1f".format(eval.expectedDamage)} " +
"strat=${"%.1f".format(eval.strategicValue)} " +
"align=${"%.1f".format(eval.gamePlanAlignment)} " +
"turn=${"%.1f".format(eval.turnAdvantage)} " +
"risk=${"%.2f".format(eval.riskFactor)} " +
flags
}
debugLog("MoveRank plan=$plan situation=$situation top=[$details]")
}
private fun logSwitchDecision(
reason: String,
battle: PokemonBattle,
availableSwitches: List<BattlePokemon>,
chosen: BattlePokemon?
) {
if (!aiDebugEnabled) return
val state = battleState
val plan = currentGamePlan
val opponent = battleTracker.p1Active.activePokemon
val ranked = if (state != null && plan != null) {
availableSwitches.map { candidate ->
val score = switchDecisionMaker.evaluateSwitchOption(
candidate.effectedPokemon,
opponent,
state,
plan
)
candidate to score
}.sortedByDescending { it.second }
.take(3)
.joinToString(", ") { (candidate, score) ->
"${candidate.effectedPokemon.species.name}:${"%.1f".format(score)}"
}
} else {
"n/a"
}
val chosenName = chosen?.effectedPokemon?.species?.name ?: "none"
debugLog("Decision=$reason chosen=$chosenName options=[$ranked]")
}
private fun debugLog(message: String) {
if (aiDebugEnabled) {
Cobblemon.LOGGER.info("[CompetitiveBattleAI] $message")
}
}
private fun isAiDebugEnabled(): Boolean {
return true
}
}
@@ -0,0 +1,20 @@
package com.cobblemon.mod.common.battles.ai.competitive
object AiBattleUtils {
fun normalizeWeatherId(weather: String?): String? {
val id = weather?.lowercase() ?: return null
return when (id) {
"sun", "sunny", "sunnyday", "harshsunlight" -> "sunnyday"
"rain", "raindance", "heavyrain" -> "raindance"
"sand", "sandstorm" -> "sandstorm"
"hail" -> "hail"
"snow", "snowscape" -> "snow"
else -> id
}
}
fun hasStab(moveTypeName: String, pokemonTypes: List<String>): Boolean {
val moveType = moveTypeName.lowercase()
return pokemonTypes.any { it.lowercase() == moveType }
}
}
@@ -0,0 +1,236 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.interpreter.ContextManager
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.Pokemon
data class BattleState(
val turn: Int,
val playerTeam: List<Pokemon>,
val npcTeam: List<Pokemon>,
val playerActive: Pokemon,
val npcActive: Pokemon,
val weather: String?,
val terrain: String?,
val playerHazards: List<String>,
val npcHazards: List<String>,
val playerBoosts: Map<Stat, Int>,
val npcBoosts: Map<Stat, Int>,
val battleContext: ContextManager,
val gamePhase: GamePhase = determineGamePhase(turn, npcTeam, playerTeam),
val isDoubles: Boolean = false,
val playerActiveSlots: List<Pokemon> = listOf(playerActive),
val npcActiveSlots: List<Pokemon> = listOf(npcActive),
val mySlotIndex: Int = 0,
val partnerSlotIndex: Int? = null
) {
companion object {
private fun determineGamePhase(turn: Int, npcTeam: List<Pokemon>, playerTeam: List<Pokemon>): GamePhase {
val npcAlive = npcTeam.count { it.currentHealth > 0 }
val playerAlive = playerTeam.count { it.currentHealth > 0 }
return when {
turn <= 3 -> GamePhase.EARLY_GAME
npcAlive <= 2 || playerAlive <= 2 -> GamePhase.ENDGAME
turn >= 15 -> GamePhase.LATE_GAME
else -> GamePhase.MID_GAME
}
}
}
}
enum class GamePhase {
EARLY_GAME,
MID_GAME,
LATE_GAME,
ENDGAME
}
data class TrackedPokemon(
var pokemon: Pokemon? = null,
var originalPokemon: Pokemon? = null,
var species: String? = null,
var currentHp: Int = 0,
var currentHpPercent: Double = 0.0,
var maxHp: Int = 0,
var previousHp: Int = 0,
var lastHpChange: Int = 0,
var boosts: Map<Stat, Int> = mapOf(),
var atkBoost: Int = 0,
var defBoost: Int = 0,
var spaBoost: Int = 0,
var spdBoost: Int = 0,
var speBoost: Int = 0,
var currentAbility: String? = null,
var currentTypes: MutableList<String>? = null,
var stats: Map<Stat, Int> = mapOf(),
var moves: List<Move> = listOf(),
var revealedMoves: MutableSet<String> = mutableSetOf(),
var cachedAttack: Double = 0.0,
var cachedDefense: Double = 0.0,
var cachedSpAttack: Double = 0.0,
var cachedSpDefense: Double = 0.0,
var cachedSpeed: Double = 0.0,
var sideConditions: Map<String, Any> = mapOf(),
var firstTurn: Int = 0,
var protectCount: Int = 0,
var lastMoveDamage: Int = 0,
var statusTurns: Int = 0,
var switchInTurn: Int = 0,
var predictedRole: PokemonRole = PokemonRole.UNKNOWN,
var threatLevel: Int = 0
)
enum class PokemonRole {
UNKNOWN,
SWEEPER,
SETUP_SWEEPER,
WALLBREAKER,
REVENGE_KILLER,
TANK,
SUPPORT,
PIVOT,
BALANCED
}
data class TeamTracker(
var party: MutableList<TrackedPokemon> = mutableListOf(),
var uuid: String? = null,
var activePokemon: TrackedPokemon = TrackedPokemon(),
var activeSlots: MutableList<TrackedPokemon> = mutableListOf(),
var nRemainingMons: Int = 0,
var hazardDamage: Int = 0,
var teamSynergy: Double = 0.0,
var momentum: Double = 0.0
) {
fun getActiveAt(slotIndex: Int): TrackedPokemon? = activeSlots.getOrNull(slotIndex)
fun getAllActive(): List<TrackedPokemon> = activeSlots.filter { it.currentHp > 0 }
fun syncLegacyActivePokemon() {
if (activeSlots.isNotEmpty()) {
activePokemon = activeSlots[0]
}
}
}
class EnhancedActiveTracker {
var p1Active: TeamTracker = TeamTracker()
var p2Active: TeamTracker = TeamTracker()
private var lastStateUpdateTurn: Int = -1
fun updatePokemonSlot(team: TeamTracker, slotIndex: Int, battlePokemon: BattlePokemon) {
while (team.activeSlots.size <= slotIndex) {
team.activeSlots.add(TrackedPokemon())
}
updatePokemon(team.activeSlots[slotIndex], battlePokemon)
team.syncLegacyActivePokemon()
}
fun getTrackedPokemonByPNX(pnx: String, team: TeamTracker): TrackedPokemon? {
if (pnx.length < 3) return null
val slotIndex = pnx[2] - 'a'
return team.getActiveAt(slotIndex)
}
fun updatePokemon(tracker: TrackedPokemon, battlePokemon: BattlePokemon) {
val pokemon = battlePokemon.effectedPokemon
tracker.pokemon = pokemon
tracker.originalPokemon = battlePokemon.originalPokemon
tracker.species = pokemon.species.name
tracker.currentHp = pokemon.currentHealth
tracker.maxHp = pokemon.maxHealth
tracker.currentHpPercent = pokemon.currentHealth.toDouble() / pokemon.maxHealth
tracker.currentTypes = pokemon.types.map { it.name.lowercase() }.toMutableList()
tracker.currentAbility = pokemon.ability.name
tracker.moves = pokemon.moveSet.getMoves()
tracker.boosts = battlePokemon.statChanges
tracker.atkBoost = battlePokemon.statChanges[Stats.ATTACK] ?: 0
tracker.defBoost = battlePokemon.statChanges[Stats.DEFENCE] ?: 0
tracker.spaBoost = battlePokemon.statChanges[Stats.SPECIAL_ATTACK] ?: 0
tracker.spdBoost = battlePokemon.statChanges[Stats.SPECIAL_DEFENCE] ?: 0
tracker.speBoost = battlePokemon.statChanges[Stats.SPEED] ?: 0
cacheStats(tracker)
}
private fun cacheStats(tracker: TrackedPokemon) {
val pokemon = tracker.pokemon ?: return
tracker.cachedAttack = calculateStat(pokemon, Stats.ATTACK)
tracker.cachedDefense = calculateStat(pokemon, Stats.DEFENCE)
tracker.cachedSpAttack = calculateStat(pokemon, Stats.SPECIAL_ATTACK)
tracker.cachedSpDefense = calculateStat(pokemon, Stats.SPECIAL_DEFENCE)
tracker.cachedSpeed = calculateStat(pokemon, Stats.SPEED)
}
private fun calculateStat(pokemon: Pokemon, stat: Stat): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return ((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
fun updateParty(team: TeamTracker, pokemon: TrackedPokemon) {
val existingIndex = team.party.indexOfFirst { it.pokemon?.uuid == pokemon.pokemon?.uuid }
if (existingIndex == -1) {
team.party.add(pokemon)
} else {
team.party[existingIndex] = pokemon
}
}
}
data class FearThreat(
val pokemon: Pokemon,
val hasEndeavor: Boolean,
val hasPriorityMove: Boolean,
val hasFocusItem: Boolean,
val threatLevel: Int
)
enum class BattleSituation {
DESPERATE,
WINNING,
EARLY_GAME,
ENDGAME,
OPPONENT_SETUP,
MY_SETUP,
NEUTRAL,
HAZARD_ADVANTAGE,
WEATHER_CONTROL
}
data class MatchupEvaluation(
val offensiveAdvantage: Double,
val defensiveAdvantage: Double,
val speedAdvantage: Double,
val overallScore: Double,
val recommendation: MatchupRecommendation
)
enum class MatchupRecommendation {
STAY_IN_AND_ATTACK,
STAY_IN_AND_SETUP,
STAY_IN_AND_STATUS,
CONSIDER_SWITCHING,
SWITCH_OUT,
PIVOT_OUT
}
@@ -0,0 +1,483 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.core.registries.BuiltInRegistries
import java.util.*
import kotlin.math.max
class DamageCalculator(
private val battle: PokemonBattle?,
private val consumedItems: MutableMap<UUID, MutableList<String>> = mutableMapOf()
) {
fun calculateDamage(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
weather: String? = null,
criticalHit: Boolean = false,
isSpreadMove: Boolean = false,
targetCount: Int = 1
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var power = moveData.power.toDouble()
if (power <= 0) return 0.0
if (isSpreadMove && targetCount > 1) {
power *= 0.75
}
val immunityMultiplier = checkAllImmunities(moveData, attacker, defender)
if (immunityMultiplier == 0.0) return 0.0
val level = attacker.pokemon?.level ?: 50
val attackStat = getAttackStat(moveData, attacker)
val defenseStat = getDefenseStat(moveData, defender)
var damage = ((2.0 * level / 5.0 + 2.0) * power * (attackStat / defenseStat)) / 50.0 + 2.0
damage *= calculateCriticalMultiplier(criticalHit)
damage *= calculateSTABMultiplier(attacker, moveData)
damage *= TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, defender.currentTypes ?: listOf())
damage *= calculateWeatherMultiplier(moveData, AiBattleUtils.normalizeWeatherId(weather))
damage *= calculateAbilityMultiplier(attacker, defender, moveData)
damage *= calculateItemMultiplier(attacker, defender, moveData)
damage *= calculateBurnMultiplier(attacker, moveData)
damage *= calculateScreenMultiplier(attacker, defender, moveData)
damage *= calculateRandomFactor()
damage = calculateMultiHitDamage(moveData, damage)
val finalDamage = max(1.0, damage)
if (finalDamage > 0) {
trackConsumedItems(defender, moveData)
}
return finalDamage
}
private fun getAttackStat(moveData: MoveTemplate, attacker: TrackedPokemon): Double {
val pokemon = attacker.pokemon ?: return 100.0
val baseStat = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
getEffectiveStat(pokemon, Stats.ATTACK)
} else {
getEffectiveStat(pokemon, Stats.SPECIAL_ATTACK)
}
val boost = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
attacker.atkBoost
} else {
attacker.spaBoost
}
return applyStatBoosts(baseStat, boost)
}
private fun getDefenseStat(moveData: MoveTemplate, defender: TrackedPokemon): Double {
val pokemon = defender.pokemon ?: return 100.0
val baseStat = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
getEffectiveStat(pokemon, Stats.DEFENCE)
} else {
getEffectiveStat(pokemon, Stats.SPECIAL_DEFENCE)
}
val boost = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
defender.defBoost
} else {
defender.spdBoost
}
return applyStatBoosts(baseStat, boost)
}
private fun applyStatBoosts(baseStat: Double, boost: Int): Double {
val clampedBoost = boost.coerceIn(-6, 6)
return if (clampedBoost >= 0) {
baseStat * (2.0 + clampedBoost) / 2.0
} else {
baseStat * 2.0 / (2.0 - clampedBoost)
}
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stat): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
private fun checkAllImmunities(
moveData: MoveTemplate,
attacker: TrackedPokemon,
defender: TrackedPokemon
): Double {
if (checkTypeImmunity(moveData, defender)) return 0.0
if (checkAbilityImmunity(moveData, attacker, defender)) return 0.0
if (checkItemImmunity(moveData, defender)) return 0.0
if (checkMoveSpecificImmunity(moveData, defender)) return 0.0
return 1.0
}
private fun checkTypeImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
.map { it.lowercase() }
return when (moveData.elementalType) {
ElementalTypes.GROUND -> "flying" in defenderTypes
ElementalTypes.ELECTRIC -> "ground" in defenderTypes
ElementalTypes.NORMAL, ElementalTypes.FIGHTING -> "ghost" in defenderTypes
ElementalTypes.PSYCHIC -> "dark" in defenderTypes
ElementalTypes.POISON -> "steel" in defenderTypes
ElementalTypes.DRAGON -> "fairy" in defenderTypes
ElementalTypes.GHOST -> "normal" in defenderTypes
else -> false
}
}
private fun checkAbilityImmunity(
moveData: MoveTemplate,
attacker: TrackedPokemon,
defender: TrackedPokemon
): Boolean {
val defenderAbility = defender.currentAbility?.lowercase()
?: defender.pokemon?.ability?.name?.lowercase()
?: return false
val attackerAbility = attacker.currentAbility?.lowercase()
?: attacker.pokemon?.ability?.name?.lowercase()
if (attackerAbility in listOf("moldbreaker", "turboblaze", "teravolt")) {
if (defenderAbility != "wonderguard") return false
}
return when (defenderAbility) {
"levitate" -> moveData.elementalType == ElementalTypes.GROUND
"voltabsorb", "lightningrod", "motordrive" -> moveData.elementalType == ElementalTypes.ELECTRIC
"flashfire", "wellbakedbody" -> moveData.elementalType == ElementalTypes.FIRE
"waterabsorb", "stormdrain", "dryskin" -> moveData.elementalType == ElementalTypes.WATER
"sapsipper" -> moveData.elementalType == ElementalTypes.GRASS
"eartheater" -> moveData.elementalType == ElementalTypes.GROUND
"soundproof" -> isSoundMove(moveData)
"bulletproof" -> isBallOrBombMove(moveData)
"queenlymajesty", "dazzling" -> moveData.priority > 0
"wonderguard" -> {
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
effectiveness <= 1.0
}
"goodasgold" -> moveData.power.toInt() == 0
else -> false
}
}
private fun checkItemImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderItem = defender.pokemon?.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
} ?: return false
return when (defenderItem) {
"cobblemon:air_balloon" -> {
val notConsumed = consumedItems[defender.pokemon?.uuid]?.contains("airballoon") != true
notConsumed && moveData.elementalType == ElementalTypes.GROUND
}
"cobblemon:safety_goggles" -> isPowderMove(moveData) || isWeatherDamageMove(moveData)
else -> false
}
}
private fun checkMoveSpecificImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
.map { it.lowercase() }
val defenderAbility = defender.currentAbility?.lowercase()
?: defender.pokemon?.ability?.name?.lowercase()
return when (moveData.name.lowercase()) {
"thunderwave" -> {
"electric" in defenderTypes || "ground" in defenderTypes ||
defenderAbility in listOf("limber", "volt absorb", "lightningrod", "motordrive")
}
"willowisp" -> {
"fire" in defenderTypes ||
defenderAbility in listOf("waterveil", "waterbubble", "flashfire")
}
"toxic", "poisonpowder", "poisongas" -> {
"poison" in defenderTypes || "steel" in defenderTypes ||
defenderAbility in listOf("immunity", "poisonheal", "pastelveil")
}
"sleeppowder", "spore", "hypnosis", "sing" -> {
defenderAbility in listOf("insomnia", "vitalspirit", "comatose", "sweetveil")
}
else -> false
}
}
private fun calculateCriticalMultiplier(criticalHit: Boolean): Double {
return if (criticalHit) 1.5 else 1.0
}
private fun calculateSTABMultiplier(attacker: TrackedPokemon, moveData: MoveTemplate): Double {
val pokemonTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name } ?: return 1.0
return if (AiBattleUtils.hasStab(moveData.elementalType.name, pokemonTypes)) {
if (attacker.currentAbility?.lowercase() == "adaptability") 2.0 else 1.5
} else 1.0
}
private fun calculateWeatherMultiplier(moveData: MoveTemplate, weather: String?): Double {
return when (weather) {
"sunnyday" -> when {
moveData.elementalType == ElementalTypes.FIRE -> 1.5
moveData.elementalType == ElementalTypes.WATER && moveData.name != "hydrosteam" -> 0.5
moveData.name == "hydrosteam" -> 1.5
else -> 1.0
}
"raindance" -> when {
moveData.elementalType == ElementalTypes.WATER -> 1.5
moveData.elementalType == ElementalTypes.FIRE -> 0.5
else -> 1.0
}
"sandstorm", "hail", "snow" -> if (moveData.name == "weatherball") 2.0 else 1.0
else -> 1.0
}
}
private fun calculateAbilityMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
var multiplier = 1.0
when (attacker.currentAbility?.lowercase()) {
"hugepower", "purepower" -> {
if (moveData.damageCategory == DamageCategories.PHYSICAL) multiplier *= 2.0
}
"guts" -> {
if (attacker.pokemon?.status != null && moveData.damageCategory == DamageCategories.PHYSICAL) {
multiplier *= 1.5
}
}
"overgrow" -> {
if (moveData.elementalType == ElementalTypes.GRASS && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"blaze" -> {
if (moveData.elementalType == ElementalTypes.FIRE && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"torrent" -> {
if (moveData.elementalType == ElementalTypes.WATER && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"swarm" -> {
if (moveData.elementalType == ElementalTypes.BUG && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
}
when (defender.currentAbility?.lowercase()) {
"thickfat" -> {
if (moveData.elementalType == ElementalTypes.FIRE || moveData.elementalType == ElementalTypes.ICE) {
multiplier *= 0.5
}
}
"filter", "solidrock" -> {
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
if (effectiveness > 1.0) multiplier *= 0.75
}
}
return multiplier
}
private fun calculateItemMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
var multiplier = 1.0
val attackerItem = attacker.pokemon?.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
}
when (attackerItem) {
"cobblemon:choice_band" -> {
if (moveData.damageCategory == DamageCategories.PHYSICAL) multiplier *= 1.5
}
"cobblemon:choice_specs" -> {
if (moveData.damageCategory == DamageCategories.SPECIAL) multiplier *= 1.5
}
"cobblemon:life_orb" -> multiplier *= 1.3
}
return multiplier
}
private fun calculateBurnMultiplier(attacker: TrackedPokemon, moveData: MoveTemplate): Double {
return if (attacker.pokemon?.status?.status?.showdownName == "burn" &&
moveData.damageCategory == DamageCategories.PHYSICAL &&
attacker.currentAbility != "guts") {
0.5
} else {
1.0
}
}
private fun calculateScreenMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
if (attackerAbility == "infiltrator") {
return 1.0
}
val sideConditions = defender.sideConditions.keys.map { it.lowercase() }
val hasReflect = "reflect" in sideConditions
val hasLightScreen = "lightscreen" in sideConditions || "light screen" in sideConditions
val hasAuroraVeil = "auroraveil" in sideConditions || "aurora veil" in sideConditions
val multiplier = when (moveData.damageCategory) {
DamageCategories.PHYSICAL -> {
when {
hasAuroraVeil -> 0.5
hasReflect -> 0.5
else -> 1.0
}
}
DamageCategories.SPECIAL -> {
when {
hasAuroraVeil -> 0.5
hasLightScreen -> 0.5
else -> 1.0
}
}
else -> 1.0
}
return multiplier
}
private fun calculateRandomFactor(): Double = 0.925
private fun calculateMultiHitDamage(moveData: MoveTemplate, baseDamage: Double): Double {
val moveName = moveData.name.lowercase()
return when {
moveName == "populationbomb" -> {
val accuracy = moveData.accuracy / 100.0
var expectedDamage = 0.0
var probability = 1.0
for (hit in 1..10) {
expectedDamage += baseDamage * probability
probability *= accuracy
}
expectedDamage
}
moveName in listOf("triplekick", "tripleaxel") -> {
val accuracy = moveData.accuracy / 100.0
baseDamage * 1.0 +
baseDamage * 2.0 * accuracy +
baseDamage * 3.0 * accuracy * accuracy
}
moveName in MoveData.multiHitMoves.keys -> {
val hitRange = MoveData.multiHitMoves[moveName]!!
val minHits = hitRange.keys.minOrNull() ?: 1
val maxHits = hitRange.values.maxOrNull() ?: 1
when {
minHits == 2 && maxHits == 5 -> baseDamage * 3.0
minHits == 2 && maxHits == 2 -> baseDamage * 2.0
minHits == 3 && maxHits == 3 -> baseDamage * 3.0
else -> baseDamage * ((minHits + maxHits) / 2.0)
}
}
else -> baseDamage
}
}
private fun isSoundMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"boomburst", "bugbuzz", "chatter", "clangoroussoul", "disarmingvoice",
"echoedvoice", "grasswhistle", "growl", "hypervoice", "metalsound",
"nobleroar", "overdrive", "perishsong", "relicsong", "roar", "round",
"screech", "sing", "snore", "sparklingaria", "supersonic", "uproar"
)
}
private fun isBallOrBombMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"acidspray", "aurasphere", "barrage", "bulletseed", "eggbomb",
"electroball", "energyball", "focusblast", "gyroball", "iceball",
"magnetbomb", "mistball", "mudbomb", "octazooka", "pollenpuff",
"rockblast", "searingshot", "seedbomb", "shadowball", "sludgebomb",
"weatherball", "zapcannon"
)
}
private fun isPowderMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"sleeppowder", "stunspore", "poisonpowder", "cottonspore",
"ragepowder", "spore"
)
}
private fun isWeatherDamageMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf("sandstorm", "hail")
}
private fun trackConsumedItems(defender: TrackedPokemon, moveData: MoveTemplate) {
val pokemon = defender.pokemon ?: return
val defenderItem = pokemon.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
} ?: return
if (defenderItem == "cobblemon:air_balloon") {
val alreadyConsumed = consumedItems[pokemon.uuid]?.contains("airballoon") == true
if (!alreadyConsumed) {
addConsumedItem(pokemon.uuid, "airballoon")
}
}
}
private fun addConsumedItem(pokemonUuid: UUID, item: String) {
val items = consumedItems.getOrPut(pokemonUuid) { mutableListOf() }
items.add(item)
}
}
@@ -0,0 +1,135 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.battles.ShowdownMoveset
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
class DoublesDecisionMaker(
private val moveEvaluator: MoveEvaluator,
private val damageCalculator: DamageCalculator
) {
data class MoveTargetPair(
val move: InBattleMove,
val targetPNX: String?,
val evaluation: MoveEvaluator.MoveEvaluation
)
fun findBestMoveAndTarget(
moveset: ShowdownMoveset?,
activeBattlePokemon: ActiveBattlePokemon,
myPokemon: TrackedPokemon,
opponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveTargetPair? {
if (moveset == null) return null
val usableMoves = moveset.moves.filter { it.canBeUsed() }
if (usableMoves.isEmpty()) return null
val allOptions = mutableListOf<MoveTargetPair>()
for (move in usableMoves) {
val moveData = Moves.getByName(move.id) ?: continue
val targetList = move.target.targetList(activeBattlePokemon)
if (targetList == null) {
val evaluation = moveEvaluator.evaluateMoveForDoubles(
move, myPokemon, opponents.firstOrNull() ?: TrackedPokemon(),
opponents, partner, battleState, gamePlan
)
allOptions.add(MoveTargetPair(move, null, evaluation))
} else {
val opponentTargets = targetList.filterIsInstance<ActiveBattlePokemon>()
.filter { !it.isAllied(activeBattlePokemon) }
for (target in opponentTargets) {
val targetPokemon = opponents.find {
it.pokemon?.uuid == target.battlePokemon?.uuid
} ?: continue
val evaluation = moveEvaluator.evaluateMoveForDoubles(
move, myPokemon, targetPokemon,
opponents, partner, battleState, gamePlan
)
allOptions.add(MoveTargetPair(move, target.getPNX(), evaluation))
}
}
}
return allOptions.maxByOrNull { it.evaluation.totalScore }
}
fun scoreTarget(
target: TrackedPokemon,
myPokemon: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
battleState: BattleState
): Double {
var score = 0.0
score += target.threatLevel * 20.0
val mySpeed = myPokemon.cachedSpeed
val targetSpeed = target.cachedSpeed
if (targetSpeed > mySpeed && target.currentHpPercent < 0.5) {
score += 100.0
}
if (target.boosts.values.any { it >= 2 }) {
score += 120.0
}
if (target.cachedAttack > 100 || target.cachedSpAttack > 100) {
score += 40.0
}
if (target.currentHpPercent < 0.3) {
score += 80.0
}
return score
}
fun selectBestTarget(
move: InBattleMove,
activeBattlePokemon: ActiveBattlePokemon,
myPokemon: TrackedPokemon,
opponents: List<TrackedPokemon>,
battleState: BattleState
): String? {
val targetList = move.target.targetList(activeBattlePokemon) ?: return null
val opponentTargets = targetList.filterIsInstance<ActiveBattlePokemon>()
.filter { !it.isAllied(activeBattlePokemon) }
if (opponentTargets.isEmpty()) return null
val targetScores = opponentTargets.mapNotNull { target ->
val targetPokemon = opponents.find {
it.pokemon?.uuid == target.battlePokemon?.uuid
}
if (targetPokemon != null) {
val damage = damageCalculator.calculateDamage(
move, myPokemon, targetPokemon, battleState.weather
)
val targetScore = scoreTarget(targetPokemon, myPokemon, opponents, battleState)
val totalScore = damage + targetScore
target to totalScore
} else {
null
}
}
return targetScores.maxByOrNull { it.second }?.first?.getPNX()
}
}
@@ -0,0 +1,178 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
object MoveData {
val multiHitMoves: Map<String, Map<Int, Int>> = mapOf(
"armthrust" to mapOf(2 to 5),
"barrage" to mapOf(2 to 5),
"bonerush" to mapOf(2 to 5),
"bulletseed" to mapOf(2 to 5),
"cometpunch" to mapOf(2 to 5),
"doubleslap" to mapOf(2 to 5),
"furyattack" to mapOf(2 to 5),
"furyswipes" to mapOf(2 to 5),
"iciclespear" to mapOf(2 to 5),
"pinmissile" to mapOf(2 to 5),
"rockblast" to mapOf(2 to 5),
"scaleshot" to mapOf(2 to 5),
"spikecannon" to mapOf(2 to 5),
"tailslap" to mapOf(2 to 5),
"watershuriken" to mapOf(2 to 5),
"bonemerang" to mapOf(2 to 2),
"doublehit" to mapOf(2 to 2),
"doubleironbash" to mapOf(2 to 2),
"doublekick" to mapOf(2 to 2),
"dragondarts" to mapOf(2 to 2),
"dualchop" to mapOf(2 to 2),
"dualwingbeat" to mapOf(2 to 2),
"geargrind" to mapOf(2 to 2),
"twinbeam" to mapOf(2 to 2),
"twineedle" to mapOf(2 to 2),
"surgingstrikes" to mapOf(3 to 3),
"tripledive" to mapOf(3 to 3),
"tripleaxel" to mapOf(1 to 3),
"triplekick" to mapOf(1 to 3),
"populationbomb" to mapOf(1 to 10)
)
val statusMoves: Map<MoveTemplate?, String> = mapOf(
Moves.getByName("willowisp") to Statuses.BURN.showdownName,
Moves.getByName("scald") to Statuses.BURN.showdownName,
Moves.getByName("scorchingsands") to Statuses.BURN.showdownName,
Moves.getByName("flamethrower") to Statuses.BURN.showdownName,
Moves.getByName("sacredfire") to Statuses.BURN.showdownName,
Moves.getByName("glare") to Statuses.PARALYSIS.showdownName,
Moves.getByName("nuzzle") to Statuses.PARALYSIS.showdownName,
Moves.getByName("stunspore") to Statuses.PARALYSIS.showdownName,
Moves.getByName("thunderwave") to Statuses.PARALYSIS.showdownName,
Moves.getByName("bodyslam") to Statuses.PARALYSIS.showdownName,
Moves.getByName("discharge") to Statuses.PARALYSIS.showdownName,
Moves.getByName("spark") to Statuses.PARALYSIS.showdownName,
Moves.getByName("darkvoid") to Statuses.SLEEP.showdownName,
Moves.getByName("hypnosis") to Statuses.SLEEP.showdownName,
Moves.getByName("lovelykiss") to Statuses.SLEEP.showdownName,
Moves.getByName("relicsong") to Statuses.SLEEP.showdownName,
Moves.getByName("sing") to Statuses.SLEEP.showdownName,
Moves.getByName("sleeppowder") to Statuses.SLEEP.showdownName,
Moves.getByName("spore") to Statuses.SLEEP.showdownName,
Moves.getByName("yawn") to Statuses.SLEEP.showdownName,
Moves.getByName("chatter") to "confusion",
Moves.getByName("confuseray") to "confusion",
Moves.getByName("dynamicpunch") to "confusion",
Moves.getByName("flatter") to "confusion",
Moves.getByName("supersonic") to "confusion",
Moves.getByName("swagger") to "confusion",
Moves.getByName("sweetkiss") to "confusion",
Moves.getByName("teeterdance") to "confusion",
Moves.getByName("poisongas") to Statuses.POISON.showdownName,
Moves.getByName("poisonpowder") to Statuses.POISON.showdownName,
Moves.getByName("poisonjab") to Statuses.POISON.showdownName,
Moves.getByName("sludgebomb") to Statuses.POISON.showdownName,
Moves.getByName("crosspoison") to Statuses.POISON.showdownName,
Moves.getByName("toxic") to Statuses.POISON_BADLY.showdownName,
Moves.getByName("toxicthread") to Statuses.POISON.showdownName,
Moves.getByName("curse") to "cursed",
Moves.getByName("leechseed") to "leech"
)
val boostFromMoves: Map<String, Map<Stat, Int>> = mapOf(
"bellydrum" to mapOf(Stats.ATTACK to 6),
"swordsdance" to mapOf(Stats.ATTACK to 2),
"honeclaws" to mapOf(Stats.ATTACK to 1, Stats.ACCURACY to 1),
"bulkup" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1),
"curse" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPEED to -1),
"howl" to mapOf(Stats.ATTACK to 1),
"meditate" to mapOf(Stats.ATTACK to 1),
"sharpen" to mapOf(Stats.ATTACK to 1),
"workup" to mapOf(Stats.ATTACK to 1, Stats.SPECIAL_ATTACK to 1),
"acidarmor" to mapOf(Stats.DEFENCE to 2),
"barrier" to mapOf(Stats.DEFENCE to 2),
"cottonguard" to mapOf(Stats.DEFENCE to 3),
"defensecurl" to mapOf(Stats.DEFENCE to 1),
"irondefense" to mapOf(Stats.DEFENCE to 2),
"stockpile" to mapOf(Stats.DEFENCE to 1, Stats.SPECIAL_DEFENCE to 1),
"stuffcheeks" to mapOf(Stats.DEFENCE to 2),
"shelter" to mapOf(Stats.DEFENCE to 2, Stats.EVASION to 1),
"withdraw" to mapOf(Stats.DEFENCE to 1),
"harden" to mapOf(Stats.DEFENCE to 1),
"nastyplot" to mapOf(Stats.SPECIAL_ATTACK to 2),
"calmmind" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1),
"quiverdance" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"tailglow" to mapOf(Stats.SPECIAL_ATTACK to 3),
"takeheart" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1),
"geomancy" to mapOf(Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to 2, Stats.SPEED to 2),
"growth" to mapOf(Stats.SPECIAL_ATTACK to 1),
"amnesia" to mapOf(Stats.SPECIAL_DEFENCE to 2),
"agility" to mapOf(Stats.SPEED to 2),
"autotomize" to mapOf(Stats.SPEED to 2),
"rockpolish" to mapOf(Stats.SPEED to 2),
"dragondance" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 1),
"shiftgear" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 2),
"clangoroussoul" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"extremeevoboost" to mapOf(Stats.ATTACK to 2, Stats.DEFENCE to 2, Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to 2, Stats.SPEED to 2),
"clangoroussoulblaze" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"filletaway" to mapOf(Stats.ATTACK to 2, Stats.SPECIAL_ATTACK to 2, Stats.SPEED to 2),
"noretreat" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"shellsmash" to mapOf(Stats.ATTACK to 2, Stats.DEFENCE to -1, Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to -1, Stats.SPEED to 2),
"tidyup" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 1),
"victorydance" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPEED to 1),
"coil" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.ACCURACY to 1),
"minimize" to mapOf(Stats.EVASION to 2),
"doubleteam" to mapOf(Stats.EVASION to 1)
)
val weatherMoves: Map<String, String> = mapOf(
"raindance" to "rain",
"sunnyday" to "sun",
"sandstorm" to "sand",
"hail" to "hail",
"snowscape" to "snow"
)
val weatherDependentMoves: Map<String, String> = mapOf(
"thunder" to "rain",
"hurricane" to "rain",
"blizzard" to "hail",
"solarbeam" to "sun",
"hydrosteam" to "sun",
"weatherball" to "any"
)
val weatherAbilities: Map<String, String> = mapOf(
"swiftswim" to "rain",
"raindish" to "rain",
"dryskin" to "rain",
"chlorophyll" to "sun",
"leafguard" to "sun",
"flowergift" to "sun",
"solarpower" to "sun",
"sandrush" to "sand",
"sandveil" to "sand",
"sandforce" to "sand",
"icebody" to "hail",
"snowcloak" to "hail"
)
val entryHazards = listOf("spikes", "stealthrock", "stickyweb", "toxicspikes")
val antiHazardsMoves = listOf("rapidspin", "defog", "tidyup")
val antiBoostMoves = listOf("clearsmog", "haze", "roar", "whirlwind")
val pivotMoves = listOf("uturn", "flipturn", "partingshot", "batonpass", "voltswitch", "teleport")
val setupMoves = setOf("tailwind", "trickroom", "auroraveil", "lightscreen", "reflect")
val selfRecoveryMoves = listOf("healorder", "milkdrink", "recover", "rest", "roost", "slackoff", "softboiled")
val priorityMoves = listOf(
"quickattack", "extremespeed", "bulletpunch", "machpunch",
"aquajet", "shadowsneak", "accelerock", "iceshard",
"suckerpunch", "firstimpression", "fakeout"
)
val recoilMoves = listOf(
"doubleedge", "bravebird", "closecombat", "superpower",
"overheat", "dracometeor", "leafstorm", "flareblitz"
)
}
@@ -0,0 +1,737 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.abs
import kotlin.math.max
class MoveEvaluator(
private val damageCalculator: DamageCalculator,
private val opponentPredictor: OpponentPredictor,
private val isDoublesMode: Boolean = false
) {
data class MoveEvaluation(
val move: InBattleMove,
val rawDamage: Double,
val expectedDamage: Double,
val strategicValue: Double,
val riskFactor: Double,
val priority: Int,
val turnAdvantage: Double,
val gamePlanAlignment: Double
) {
val totalScore: Double = (expectedDamage + strategicValue + gamePlanAlignment + turnAdvantage) / riskFactor
}
data class MoveDebugDetails(
val moveId: String,
val moveType: String,
val accuracy: Double,
val rawDamage: Double,
val expectedDamage: Double,
val effectiveness: Double,
val hasStab: Boolean,
val priority: Int,
val isStatusMove: Boolean,
val setsWeather: Boolean,
val setsHazards: Boolean,
val removesHazards: Boolean,
val isRecovery: Boolean,
val isPivot: Boolean,
val isSetup: Boolean,
val isRisky: Boolean
)
fun buildDebugDetails(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): MoveDebugDetails? {
val moveData = Moves.getByName(move.id) ?: return null
val rawDamage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
val expectedDamage = rawDamage * accuracy
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
val attackerTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name } ?: listOf()
val hasStab = AiBattleUtils.hasStab(moveData.elementalType.name, attackerTypes)
val moveId = move.id.lowercase()
val isStatusMove = MoveData.statusMoves.keys.any { it?.name?.lowercase() == moveId }
val setsWeather = moveId in MoveData.weatherMoves.keys
val setsHazards = moveId in MoveData.entryHazards
val removesHazards = moveId in MoveData.antiHazardsMoves
val isRecovery = moveId in MoveData.selfRecoveryMoves
val isPivot = moveId in MoveData.pivotMoves
val isSetup = moveId in MoveData.boostFromMoves.keys
val isRisky = moveId in gamePlan.riskyMoves
return MoveDebugDetails(
moveId = move.id,
moveType = moveData.elementalType.name,
accuracy = accuracy,
rawDamage = rawDamage,
expectedDamage = expectedDamage,
effectiveness = effectiveness,
hasStab = hasStab,
priority = moveData.priority,
isStatusMove = isStatusMove,
setsWeather = setsWeather,
setsHazards = setsHazards,
removesHazards = removesHazards,
isRecovery = isRecovery,
isPivot = isPivot,
isSetup = isSetup,
isRisky = isRisky
)
}
fun evaluateMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
val rawDamage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
val expectedDamage = rawDamage * accuracy
val strategicValue = calculateStrategicValue(move, attacker, defender, battleState, gamePlan)
val riskFactor = calculateRiskFactor(move, attacker, defender, battleState, gamePlan)
val turnAdvantage = calculateTurnAdvantage(move, attacker, defender, battleState)
val gamePlanAlignment = calculateGamePlanAlignment(move, attacker, defender, battleState, gamePlan)
return MoveEvaluation(
move = move,
rawDamage = rawDamage,
expectedDamage = expectedDamage,
strategicValue = strategicValue,
riskFactor = riskFactor,
priority = moveData.priority,
turnAdvantage = turnAdvantage,
gamePlanAlignment = gamePlanAlignment
)
}
private fun getEffectiveAccuracy(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
weather: String?
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var accuracy = if (moveData.accuracy <= 0) 1.0 else moveData.accuracy / 100.0
val normalizedWeather = AiBattleUtils.normalizeWeatherId(weather)
when (normalizedWeather) {
"raindance" -> {
if (move.id.lowercase() in listOf("thunder", "hurricane")) accuracy = 1.0
}
"hail", "snow" -> {
if (move.id.lowercase() == "blizzard") accuracy = 1.0
}
"sunnyday" -> {
if (move.id.lowercase() in listOf("thunder", "hurricane")) accuracy *= 0.5
}
}
when (attacker.currentAbility?.lowercase()) {
"compoundeyes" -> accuracy *= 1.3
"noguard" -> accuracy = 1.0
}
when (defender.currentAbility?.lowercase()) {
"noguard" -> accuracy = 1.0
"sandveil" -> if (normalizedWeather == "sandstorm") accuracy *= 0.8
"snowcloak" -> if (normalizedWeather in listOf("hail", "snow")) accuracy *= 0.8
}
val accuracyBoost = attacker.boosts.getOrDefault(Stats.ACCURACY, 0)
val evasionBoost = defender.boosts.getOrDefault(Stats.EVASION, 0)
val netBoost = accuracyBoost - evasionBoost
if (netBoost != 0) {
accuracy *= if (netBoost > 0) {
(3.0 + netBoost) / 3.0
} else {
3.0 / (3.0 - netBoost)
}
}
return accuracy.coerceIn(0.0, 1.0)
}
private fun calculateStrategicValue(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
var value = 0.0
if (move.id.lowercase() in MoveData.weatherMoves.keys) {
value += evaluateWeatherSetting(move, gamePlan, battleState)
}
if (move.id.lowercase() in MoveData.boostFromMoves.keys) {
value += evaluateSetupMove(move, attacker, defender, battleState, gamePlan)
}
val moveId = move.id.lowercase()
val isStatusMove = MoveData.statusMoves.keys.any { it?.name?.lowercase() == moveId }
if (isStatusMove) {
value += evaluateStatusMove(move, attacker, defender, battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.entryHazards) {
value += evaluateHazardSetting(battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.antiHazardsMoves) {
value += evaluateHazardRemoval(battleState, gamePlan)
}
val moveData = Moves.getByName(move.id)
if (moveData != null && moveData.priority > 0) {
value += evaluatePriorityMove(move, attacker, defender, battleState)
}
if (move.id.lowercase() in MoveData.selfRecoveryMoves) {
value += evaluateRecoveryMove(attacker, battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.pivotMoves) {
value += evaluatePivotMove(move, attacker, defender, battleState)
}
if (battleState.gamePhase == GamePhase.ENDGAME) {
value += evaluateEndgameMove(move, battleState)
}
return value
}
private fun calculateGamePlanAlignment(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var alignment = 0.0
val normalizedWeather = AiBattleUtils.normalizeWeatherId(battleState.weather)
if (move.id.lowercase() in gamePlan.riskyMoves) {
alignment -= 100.0
}
alignment += when (gamePlan.winCondition) {
WinCondition.RAIN_OFFENSE -> {
var score = 0.0
if (move.id.lowercase() == "raindance") score += 200.0
if (normalizedWeather == "raindance") {
if (moveData.elementalType.name.lowercase() == "water") score += 50.0
if (move.id.lowercase() in listOf("thunder", "hurricane")) score += 100.0
}
score
}
WinCondition.SUN_OFFENSE -> {
var score = 0.0
if (move.id.lowercase() == "sunnyday") score += 200.0
if (normalizedWeather == "sunnyday") {
if (moveData.elementalType.name.lowercase() == "fire") score += 50.0
if (move.id.lowercase() == "solarbeam") score += 100.0
}
score
}
WinCondition.SETUP_SWEEP -> {
var score = 0.0
if (move.id.lowercase() in MoveData.boostFromMoves.keys &&
attacker.species in gamePlan.keyPokemon
) {
score += 150.0
}
if (attacker.boosts.values.any { it >= 2 } && moveData.power > 80) {
score += 100.0
}
score
}
WinCondition.AGGRESSIVE_SWEEP -> {
var score = 0.0
if (moveData.power >= 90) score += 60.0
if (moveData.priority > 0 && defender.currentHpPercent < 0.5) score += 80.0
score
}
WinCondition.DEFENSIVE_STALL -> {
var score = 0.0
if (MoveData.statusMoves.keys.any { it?.name?.lowercase() == move.id.lowercase() }) score += 70.0
if (move.id.lowercase() in MoveData.selfRecoveryMoves) score += 60.0
if (moveData.power > 80) score -= 30.0
score
}
WinCondition.HAZARD_CONTROL -> {
var score = 0.0
if (move.id.lowercase() == "stealthrock" && "stealthrock" !in battleState.playerHazards) {
score += 250.0
}
if (move.id.lowercase() in MoveData.entryHazards) score += 100.0
score
}
WinCondition.REVENGE_KILLING -> {
var score = 0.0
if (moveData.priority > 0) score += 120.0
if (defender.currentHp > 50 && moveData.power >= 40 && moveData.power <= 80) score += 40.0
score
}
else -> 0.0
}
if (moveData.power > 0) {
alignment += moveData.power * gamePlan.aggressionLevel * 0.3
} else {
alignment += (1.0 - gamePlan.aggressionLevel) * 30.0
}
return alignment
}
private fun calculateRiskFactor(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val moveData = Moves.getByName(move.id) ?: return 1.0
var risk = 1.0
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
risk += (1.0 - accuracy) * 0.5
if (move.id.lowercase() in MoveData.recoilMoves) {
risk += if (attacker.currentHpPercent < 0.5) 0.5 else 0.2
}
if (move.id.lowercase() in listOf("explosion", "selfdestruct", "memento", "healingwish")) {
if (attacker.species in gamePlan.keyPokemon) {
risk += 2.0
} else if (attacker.currentHpPercent < 0.3) {
risk += 0.2
} else {
risk += 0.8
}
}
if (move.pp <= 1) {
risk += 0.3
}
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (abs(mySpeed - oppSpeed) < 5) {
risk += 0.1
}
val teamAdvantage = calculateTeamAdvantage(battleState)
if (teamAdvantage > 0) {
risk *= 0.8
} else if (teamAdvantage < -1) {
risk *= 1.2
}
return max(0.1, risk)
}
private fun calculateTurnAdvantage(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var advantage = 0.0
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (mySpeed > oppSpeed) advantage += 15.0
if (moveData.priority > 0) advantage += moveData.priority * 25.0
if (battleState.gamePhase == GamePhase.ENDGAME && moveData.power > 90) {
advantage += 20.0
}
return advantage
}
private fun evaluateWeatherSetting(move: InBattleMove, gamePlan: GamePlan, battleState: BattleState): Double {
val moveName = move.id.lowercase()
val weatherType = MoveData.weatherMoves[moveName] ?: return 0.0
return when (gamePlan.winCondition) {
WinCondition.RAIN_OFFENSE -> if (weatherType == "rain") 300.0 else 0.0
WinCondition.SUN_OFFENSE -> if (weatherType == "sun") 300.0 else 0.0
WinCondition.SAND_OFFENSE -> if (weatherType == "sand") 300.0 else 0.0
WinCondition.WEATHER_CONTROL -> 150.0
else -> 30.0
}
}
private fun evaluateSetupMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val boosts = MoveData.boostFromMoves[move.id.lowercase()] ?: return 0.0
var value = 0.0
val totalBoosts = boosts.values.sum()
value += totalBoosts * 40.0 * gamePlan.setupPriority
val relevantBoosts = boosts.keys.mapNotNull { stat -> attacker.boosts[stat] }
if (relevantBoosts.any { it >= 3 }) value *= 0.2
val predictedMoves = opponentPredictor.predictMoveHeuristically(defender, attacker)
val dangerLevel = predictedMoves.count { (moveName, _) ->
(Moves.getByName(moveName)?.power ?: 0.0) > 80
}
if (dangerLevel > 0 && attacker.currentHpPercent < 0.7) {
value *= 0.4
}
return value
}
private fun evaluateStatusMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
if (defender.pokemon?.status != null) return 0.0
return when (move.id.lowercase()) {
"thunderwave", "stunspore", "glare" -> {
if (defender.predictedRole == PokemonRole.SWEEPER) 90.0 else 50.0
}
"willowisp" -> {
if (defender.predictedRole in listOf(PokemonRole.WALLBREAKER, PokemonRole.SWEEPER)) {
80.0
} else 40.0
}
"toxic" -> {
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) 100.0 else 60.0
}
"spore", "sleeppowder" -> 120.0
else -> 30.0
}
}
private fun evaluateHazardSetting(battleState: BattleState, gamePlan: GamePlan): Double {
val remainingOpponents = battleState.playerTeam.count { it.currentHealth > 0 }
if (remainingOpponents < 2) return 15.0
var value = remainingOpponents * 20.0
if (gamePlan.winCondition == WinCondition.HAZARD_CONTROL) value *= 1.8
return value
}
private fun evaluateHazardRemoval(battleState: BattleState, gamePlan: GamePlan): Double {
val myHazards = battleState.npcHazards.size
if (myHazards == 0) return 5.0
var value = myHazards * 40.0
val remainingTeam = battleState.npcTeam.count { it.currentHealth > 0 }
if (remainingTeam >= 3) value *= 1.3
return value
}
private fun evaluatePriorityMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var value = moveData.priority * 20.0
if (defender.currentHpPercent < 0.5) {
val damage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
if (damage >= defender.currentHp) value += 150.0
}
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (abs(mySpeed - oppSpeed) < 10) value += 50.0
return value
}
private fun evaluateRecoveryMove(
attacker: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
if (attacker.currentHpPercent > 0.8) return 5.0
var value = (1.0 - attacker.currentHpPercent) * 140.0
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) value *= 1.5
return value
}
private fun evaluatePivotMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
var value = 40.0
val damage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
value += damage * 0.3
return value
}
private fun evaluateEndgameMove(move: InBattleMove, battleState: BattleState): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var bonus = 0.0
if (moveData.power > 0 && moveData.accuracy >= 90) {
bonus += 35.0
}
if (moveData.accuracy < 80) {
bonus -= 30.0
}
if (moveData.priority > 0) {
bonus += 50.0
}
return bonus
}
private fun calculateTeamAdvantage(battleState: BattleState): Int {
val npcAlive = battleState.npcTeam.count { it.currentHealth > 0 }
val playerAlive = battleState.playerTeam.count { it.currentHealth > 0 }
return npcAlive - playerAlive
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stats): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
fun evaluateMoveForDoubles(
move: InBattleMove,
attacker: TrackedPokemon,
primaryTarget: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
if (isSpreadMove(moveData.target)) {
return evaluateSpreadMove(move, attacker, allOpponents, partner, battleState, gamePlan)
}
val baseEvaluation = evaluateMove(move, attacker, primaryTarget, battleState, gamePlan)
var targetingBonus = 0.0
if (primaryTarget.threatLevel > 5) {
targetingBonus += 20.0
}
return MoveEvaluation(
move = baseEvaluation.move,
rawDamage = baseEvaluation.rawDamage,
expectedDamage = baseEvaluation.expectedDamage,
strategicValue = baseEvaluation.strategicValue + targetingBonus,
riskFactor = baseEvaluation.riskFactor,
priority = baseEvaluation.priority,
turnAdvantage = baseEvaluation.turnAdvantage,
gamePlanAlignment = baseEvaluation.gamePlanAlignment
)
}
private fun evaluateSpreadMove(
move: InBattleMove,
attacker: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
var totalDamage = 0.0
var totalStrategicValue = 0.0
val activeOpponents = allOpponents.filter { it.currentHp > 0 }
val targetCount = activeOpponents.size + (if (hitsPartner(moveData.target) && partner != null) 1 else 0)
for (opponent in activeOpponents) {
val damage = damageCalculator.calculateDamage(
move, attacker, opponent, battleState.weather,
isSpreadMove = true,
targetCount = targetCount
)
totalDamage += damage
if (damage >= opponent.currentHp) {
totalStrategicValue += 150.0
}
}
val multiTargetBonus = activeOpponents.size * 40.0
totalStrategicValue += multiTargetBonus
var partnerPenalty = 0.0
if (hitsPartner(moveData.target) && partner != null && partner.currentHp > 0) {
val partnerDamage = damageCalculator.calculateDamage(
move, attacker, partner, battleState.weather,
isSpreadMove = true,
targetCount = targetCount
)
partnerPenalty = when {
partnerDamage >= partner.currentHp -> -999999.0
partner.species in gamePlan.keyPokemon -> -partnerDamage * 5.0
partner.currentHpPercent < 0.3 -> -partnerDamage * 4.0
else -> -partnerDamage * 2.0
}
totalStrategicValue += partnerPenalty
}
val accuracy =
getEffectiveAccuracy(move, attacker, activeOpponents.firstOrNull() ?: TrackedPokemon(), battleState.weather)
val expectedDamage = totalDamage * accuracy
val riskFactor = if (partnerPenalty < -1000) {
999.0
} else if (partnerPenalty < 0) {
max(1.5, 1.0 + abs(partnerPenalty) / 100.0)
} else {
1.0
}
val gamePlanAlignment = if (activeOpponents.isNotEmpty()) {
calculateGamePlanAlignment(move, attacker, activeOpponents.first(), battleState, gamePlan)
} else {
0.0
}
return MoveEvaluation(
move = move,
rawDamage = totalDamage,
expectedDamage = expectedDamage,
strategicValue = totalStrategicValue,
riskFactor = riskFactor,
priority = moveData.priority,
turnAdvantage = 0.0,
gamePlanAlignment = gamePlanAlignment
)
}
private fun isSpreadMove(target: MoveTarget): Boolean {
return target in listOf(
MoveTarget.allAdjacent,
MoveTarget.allAdjacentFoes,
MoveTarget.all
)
}
private fun hitsPartner(target: MoveTarget): Boolean {
return target == MoveTarget.allAdjacent
}
}
@@ -0,0 +1,157 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.min
class OpponentPredictor {
private val moveHistory = mutableMapOf<String, MutableList<String>>()
private val switchHistory = mutableMapOf<String, MutableList<String>>()
private val situationResponses = mutableMapOf<String, MutableMap<String, Int>>()
private val aggressionLevel = mutableMapOf<String, Double>()
private val setupTendency = mutableMapOf<String, Double>()
private val switchFrequency = mutableMapOf<String, Double>()
fun updateMoveHistory(pokemon: String, move: String, situation: String) {
moveHistory.getOrPut(pokemon) { mutableListOf() }.add(move)
situationResponses.getOrPut(situation) { mutableMapOf() }.merge(move, 1, Int::plus)
val moveData = Moves.getByName(move)
if (moveData != null) {
val currentAggression = aggressionLevel[pokemon] ?: 0.5
val newAggression = if (moveData.power > 0) {
currentAggression * 0.9 + 0.8 * 0.1
} else {
currentAggression * 0.9 + 0.2 * 0.1
}
aggressionLevel[pokemon] = newAggression
if (move in MoveData.boostFromMoves.keys) {
val currentSetup = setupTendency[pokemon] ?: 0.3
setupTendency[pokemon] = min(1.0, currentSetup + 0.15)
}
}
}
fun updateSwitchPattern(fromPokemon: String, toPokemon: String, reason: String) {
switchHistory.getOrPut(fromPokemon) { mutableListOf() }.add(toPokemon)
situationResponses.getOrPut("switch_$reason") { mutableMapOf() }.merge(toPokemon, 1, Int::plus)
val totalActions = (moveHistory[fromPokemon]?.size ?: 0) + (switchHistory[fromPokemon]?.size ?: 0)
if (totalActions > 0) {
switchFrequency[fromPokemon] = (switchHistory[fromPokemon]?.size ?: 0).toDouble() / totalActions
}
}
fun predictNextMove(pokemon: String, situation: String): Map<String, Double> {
val responses = situationResponses[situation] ?: return emptyMap()
val total = responses.values.sum().toDouble()
return if (total > 0) {
responses.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun getSwitchProbability(pokemon: String, situation: String): Double {
val switches = switchHistory[pokemon]?.size ?: 0
val totalActions = (moveHistory[pokemon]?.size ?: 0) + switches
if (totalActions < 5) return 0.2
return min(0.8, switches.toDouble() / totalActions + 0.1)
}
private fun getAggressionLevel(pokemon: String): Double = aggressionLevel[pokemon] ?: 0.5
private fun getSetupTendency(pokemon: String): Double = setupTendency[pokemon] ?: 0.3
private fun getSwitchFrequency(pokemon: String): Double = switchFrequency[pokemon] ?: 0.2
fun predictMoveHeuristically(
opponent: TrackedPokemon,
myPokemon: TrackedPokemon
): Map<String, Double> {
val predictions = mutableMapOf<String, Double>()
val aggression = getAggressionLevel(opponent.species ?: "unknown")
for (moveName in opponent.revealedMoves) {
val moveData = Moves.getByName(moveName) ?: continue
var probability = 0.25
probability *= if (moveData.power > 0) {
aggression + 0.3
} else {
(1.0 - aggression) + 0.3
}
val myTypes = myPokemon.currentTypes ?: listOf()
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, myTypes)
probability *= (0.5 + effectiveness)
predictions[moveName] = probability
}
val total = predictions.values.sum()
return if (total > 0) {
predictions.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun predictSwitchTarget(
fromPokemon: String,
myActivePokemon: TrackedPokemon
): Map<String, Double> {
val pastSwitches = switchHistory[fromPokemon] ?: return emptyMap()
val switchCounts = mutableMapOf<String, Int>()
for (target in pastSwitches) {
switchCounts.merge(target, 1, Int::plus)
}
val total = switchCounts.values.sum().toDouble()
return if (total > 0) {
switchCounts.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun categorizePlaystyle(pokemon: String): OpponentPlaystyle {
val aggression = getAggressionLevel(pokemon)
val setupTend = getSetupTendency(pokemon)
val switchFreq = getSwitchFrequency(pokemon)
return when {
setupTend > 0.5 -> OpponentPlaystyle.SETUP_ORIENTED
aggression > 0.7 && switchFreq < 0.3 -> OpponentPlaystyle.HYPER_OFFENSIVE
switchFreq > 0.5 -> OpponentPlaystyle.MOMENTUM_BASED
aggression < 0.4 -> OpponentPlaystyle.DEFENSIVE_STALL
else -> OpponentPlaystyle.BALANCED
}
}
fun reset() {
moveHistory.clear()
switchHistory.clear()
situationResponses.clear()
aggressionLevel.clear()
setupTendency.clear()
switchFrequency.clear()
}
}
enum class OpponentPlaystyle {
HYPER_OFFENSIVE,
SETUP_ORIENTED,
MOMENTUM_BASED,
DEFENSIVE_STALL,
BALANCED
}
@@ -0,0 +1,340 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.max
class StrategyAnalyzer {
fun analyzeTeamComposition(team: List<Pokemon>): GamePlan {
val roles = team.map { identifyPokemonRole(it) }
val sweepers = roles.count { it == PokemonRole.SWEEPER }
val setupSweepers = roles.count { it == PokemonRole.SETUP_SWEEPER }
val wallbreakers = roles.count { it == PokemonRole.WALLBREAKER }
val supports = roles.count { it in listOf(PokemonRole.SUPPORT, PokemonRole.TANK) }
val revengeKillers = roles.count { it == PokemonRole.REVENGE_KILLER }
val weatherStrategy = identifyWeatherStrategy(team)
if (weatherStrategy != null) {
return createWeatherGamePlan(weatherStrategy, team)
}
val hazardStrategy = identifyHazardStrategy(team)
if (hazardStrategy != null) {
return createHazardGamePlan(team)
}
val winCondition = when {
setupSweepers >= 2 && supports >= 2 -> WinCondition.SETUP_SWEEP
sweepers + wallbreakers >= 4 -> WinCondition.AGGRESSIVE_SWEEP
supports >= 3 -> WinCondition.DEFENSIVE_STALL
revengeKillers >= 2 -> WinCondition.REVENGE_KILLING
else -> WinCondition.BALANCED
}
return createGamePlan(winCondition, team, roles)
}
private fun identifyWeatherStrategy(team: List<Pokemon>): WinCondition? {
val rainSetters = team.count { hasWeatherSettingMove(it, "rain") || hasWeatherSettingAbility(it, "rain") }
val rainAbusers = team.count { hasWeatherAbility(it, "rain") || hasWeatherMoves(it, "rain") }
val sunSetters = team.count { hasWeatherSettingMove(it, "sun") || hasWeatherSettingAbility(it, "sun") }
val sunAbusers = team.count { hasWeatherAbility(it, "sun") || hasWeatherMoves(it, "sun") }
val sandSetters = team.count { hasWeatherSettingMove(it, "sand") || hasWeatherSettingAbility(it, "sand") }
val sandAbusers = team.count { hasWeatherAbility(it, "sand") }
return when {
rainSetters >= 1 && rainAbusers >= 2 -> WinCondition.RAIN_OFFENSE
sunSetters >= 1 && sunAbusers >= 2 -> WinCondition.SUN_OFFENSE
sandSetters >= 1 && sandAbusers >= 2 -> WinCondition.SAND_OFFENSE
rainSetters >= 1 || sunSetters >= 1 || sandSetters >= 1 -> WinCondition.WEATHER_CONTROL
else -> null
}
}
private fun identifyHazardStrategy(team: List<Pokemon>): Boolean {
val hazardSetters = team.count { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.entryHazards }
}
val hazardRemovers = team.count { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.antiHazardsMoves }
}
return hazardSetters >= 2 && hazardRemovers >= 1
}
private fun createWeatherGamePlan(winCondition: WinCondition, team: List<Pokemon>): GamePlan {
val weatherType = when (winCondition) {
WinCondition.RAIN_OFFENSE -> "rain"
WinCondition.SUN_OFFENSE -> "sun"
WinCondition.SAND_OFFENSE -> "sand"
else -> "any"
}
val setters =
team.filter { hasWeatherSettingMove(it, weatherType) || hasWeatherSettingAbility(it, weatherType) }
val abusers = team.filter { hasWeatherAbility(it, weatherType) || hasWeatherMoves(it, weatherType) }
val keyPokemon = (setters + abusers).distinct().map { it.species.name }
val priorities = when (winCondition) {
WinCondition.RAIN_OFFENSE -> listOf(
"set_rain_immediately",
"preserve_rain_setter",
"sweep_with_rain_abusers",
"use_perfect_accuracy_moves",
"prevent_weather_changes"
)
WinCondition.SUN_OFFENSE -> listOf(
"set_sun_immediately",
"preserve_sun_setter",
"sweep_with_chlorophyll",
"use_solar_beam",
"prevent_weather_changes"
)
WinCondition.SAND_OFFENSE -> listOf(
"set_sandstorm",
"abuse_sand_rush",
"chip_damage_from_sand",
"prevent_weather_changes"
)
else -> listOf("maintain_weather", "use_weather_abusers")
}
val riskyMoves = listOf(
"explosion",
"selfdestruct",
"memento"
)
return GamePlan(
winCondition = winCondition,
keyPokemon = keyPokemon,
priorities = priorities,
riskyMoves = riskyMoves,
aggressionLevel = 0.8,
setupPriority = 0.9
)
}
private fun createHazardGamePlan(team: List<Pokemon>): GamePlan {
val hazardSetters = team.filter { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.entryHazards }
}.map { it.species.name }
return GamePlan(
winCondition = WinCondition.HAZARD_CONTROL,
keyPokemon = hazardSetters,
priorities = listOf(
"set_stealth_rock_early",
"stack_hazards_if_safe",
"remove_opponent_hazards",
"force_switches",
"preserve_hazard_setters"
),
riskyMoves = listOf("explosion"),
aggressionLevel = 0.6,
setupPriority = 0.7
)
}
private fun createGamePlan(winCondition: WinCondition, team: List<Pokemon>, roles: List<PokemonRole>): GamePlan {
val keyPokemon = identifyKeyPokemon(winCondition, team, roles)
val priorities = when (winCondition) {
WinCondition.SETUP_SWEEP -> listOf(
"create_setup_opportunity",
"execute_setup_moves",
"sweep_after_setup",
"preserve_setup_sweeper",
"remove_priority_threats"
)
WinCondition.AGGRESSIVE_SWEEP -> listOf(
"maintain_offensive_pressure",
"use_coverage_moves",
"preserve_sweepers",
"use_priority_to_finish",
"momentum_over_setup"
)
WinCondition.DEFENSIVE_STALL -> listOf(
"spread_status_conditions",
"use_recovery_moves",
"set_up_hazards",
"stall_out_threats",
"remove_opponent_hazards"
)
WinCondition.REVENGE_KILLING -> listOf(
"preserve_revenge_killer",
"use_priority_strategically",
"chip_into_priority_range",
"scout_with_protect",
"maintain_momentum"
)
WinCondition.HAZARD_CONTROL -> listOf(
"set_stealth_rock_early",
"stack_hazards_safely",
"remove_opponent_hazards",
"force_switches"
)
else -> listOf(
"maintain_balance",
"adapt_to_opponent",
"resource_management",
"take_calculated_risks"
)
}
val riskyMoves = when (winCondition) {
WinCondition.SETUP_SWEEP -> listOf("explosion", "memento", "healingwish", "lunardance")
WinCondition.REVENGE_KILLING -> listOf("doubleedge", "bravebird", "flareblitz")
WinCondition.DEFENSIVE_STALL -> MoveData.recoilMoves
else -> listOf("explosion", "memento")
}
val aggressionLevel = when (winCondition) {
WinCondition.AGGRESSIVE_SWEEP -> 0.9
WinCondition.SETUP_SWEEP -> 0.7
WinCondition.REVENGE_KILLING -> 0.75
WinCondition.DEFENSIVE_STALL -> 0.3
else -> 0.6
}
val setupPriority = when (winCondition) {
WinCondition.SETUP_SWEEP -> 0.95
WinCondition.DEFENSIVE_STALL -> 0.6
else -> 0.5
}
return GamePlan(
winCondition = winCondition,
keyPokemon = keyPokemon,
priorities = priorities,
riskyMoves = riskyMoves,
aggressionLevel = aggressionLevel,
setupPriority = setupPriority
)
}
private fun identifyKeyPokemon(
winCondition: WinCondition,
team: List<Pokemon>,
roles: List<PokemonRole>
): List<String> {
return when (winCondition) {
WinCondition.SETUP_SWEEP -> {
team.filterIndexed { index, _ -> roles[index] == PokemonRole.SETUP_SWEEPER }
.map { it.species.name }
}
WinCondition.AGGRESSIVE_SWEEP -> {
team.filterIndexed { index, _ -> roles[index] in listOf(PokemonRole.SWEEPER, PokemonRole.WALLBREAKER) }
.map { it.species.name }
}
WinCondition.REVENGE_KILLING -> {
team.filterIndexed { index, _ -> roles[index] == PokemonRole.REVENGE_KILLER }
.map { it.species.name }
}
WinCondition.DEFENSIVE_STALL -> {
team.filterIndexed { index, _ -> roles[index] in listOf(PokemonRole.TANK, PokemonRole.SUPPORT) }
.map { it.species.name }
}
else -> team.take(2).map { it.species.name }
}
}
fun identifyPokemonRole(pokemon: Pokemon): PokemonRole {
val attack = pokemon.species.baseStats[Stats.ATTACK] ?: 0
val spAttack = pokemon.species.baseStats[Stats.SPECIAL_ATTACK] ?: 0
val defense = pokemon.species.baseStats[Stats.DEFENCE] ?: 0
val spDefense = pokemon.species.baseStats[Stats.SPECIAL_DEFENCE] ?: 0
val speed = pokemon.species.baseStats[Stats.SPEED] ?: 0
val hp = pokemon.species.baseStats[Stats.HP] ?: 0
val maxAttack = max(attack, spAttack)
val avgDefense = (defense + spDefense) / 2.0
val bulk = hp + avgDefense
val hasSetupMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.boostFromMoves.keys }
val hasPriorityMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.priorityMoves }
val hasPivotMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.pivotMoves }
val hasRecoveryMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.selfRecoveryMoves }
return when {
hasPivotMoves -> PokemonRole.PIVOT
hasSetupMoves && speed >= 90 && maxAttack >= 90 -> PokemonRole.SETUP_SWEEPER
speed >= 110 && hasPriorityMoves -> PokemonRole.REVENGE_KILLER
speed >= 100 && maxAttack >= 100 -> PokemonRole.SWEEPER
maxAttack >= 120 -> PokemonRole.WALLBREAKER
bulk >= 180 && maxAttack < 80 -> PokemonRole.TANK
hasRecoveryMoves || hasSetupMoves -> PokemonRole.SUPPORT
else -> PokemonRole.BALANCED
}
}
private fun hasWeatherSettingMove(pokemon: Pokemon, weather: String): Boolean {
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
return when (weather) {
"rain" -> "raindance" in moves
"sun" -> "sunnyday" in moves
"sand" -> "sandstorm" in moves
else -> moves.any { it in MoveData.weatherMoves.keys }
}
}
private fun hasWeatherSettingAbility(pokemon: Pokemon, weather: String): Boolean {
val ability = pokemon.ability.name.lowercase()
return when (weather) {
"rain" -> ability == "drizzle"
"sun" -> ability == "drought"
"sand" -> ability == "sandstream"
else -> false
}
}
private fun hasWeatherAbility(pokemon: Pokemon, weather: String): Boolean {
val ability = pokemon.ability.name.lowercase()
return when (weather) {
"rain" -> ability in listOf("swiftswim", "raindish", "dryskin")
"sun" -> ability in listOf("chlorophyll", "leafguard", "flowergift", "solarpower")
"sand" -> ability in listOf("sandrush", "sandveil", "sandforce")
else -> MoveData.weatherAbilities[ability] == weather
}
}
private fun hasWeatherMoves(pokemon: Pokemon, weather: String): Boolean {
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
return when (weather) {
"rain" -> moves.any { it in listOf("thunder", "hurricane") }
"sun" -> moves.any { it in listOf("solarbeam", "hydrosteam") }
else -> false
}
}
}
enum class WinCondition {
AGGRESSIVE_SWEEP,
SETUP_SWEEP,
DEFENSIVE_STALL,
HAZARD_CONTROL,
REVENGE_KILLING,
BALANCED,
RAIN_OFFENSE,
SUN_OFFENSE,
SAND_OFFENSE,
WEATHER_CONTROL
}
data class GamePlan(
val winCondition: WinCondition,
val keyPokemon: List<String>,
val priorities: List<String>,
val riskyMoves: List<String>,
val aggressionLevel: Double = 0.6,
val setupPriority: Double = 0.5
)
@@ -0,0 +1,324 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.max
class SwitchDecisionMaker(
private val damageCalculator: DamageCalculator,
private val opponentPredictor: OpponentPredictor,
private val strategyAnalyzer: StrategyAnalyzer
) {
fun shouldVoluntarilySwitch(
myPokemon: TrackedPokemon,
opponentPokemon: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan,
availableSwitches: List<BattlePokemon>
): Boolean {
if (availableSwitches.isEmpty()) return false
val currentMatchup = evaluateMatchup(myPokemon, opponentPokemon, battleState)
val bestSwitchScore = availableSwitches.maxOfOrNull { switchOption ->
evaluateSwitchOption(
switchOption.effectedPokemon,
opponentPokemon,
battleState,
gamePlan
)
} ?: 0.0
val switchThreshold = 40.0
if (bestSwitchScore > currentMatchup.overallScore + switchThreshold) {
return true
}
val shouldSwitch = checkSwitchConditions(
myPokemon,
opponentPokemon,
battleState,
gamePlan,
currentMatchup
)
return shouldSwitch
}
fun findBestSwitch(
availableSwitches: List<BattlePokemon>,
opponentPokemon: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): BattlePokemon? {
if (availableSwitches.isEmpty()) return null
return availableSwitches.maxByOrNull { switchOption ->
evaluateSwitchOption(
switchOption.effectedPokemon,
opponentPokemon,
battleState,
gamePlan
)
}
}
fun evaluateSwitchOption(
candidate: Pokemon,
opponent: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
var score = 0.0
val candidateTracker = createTrackerPokemon(candidate)
val matchup = evaluateMatchup(candidateTracker, opponent, battleState)
score += matchup.overallScore
val hpFactor = candidateTracker.currentHpPercent
if (hpFactor < 0.5) {
score -= (1.0 - hpFactor) * 40.0
}
if (gamePlan.keyPokemon.contains(candidate.species.name)) {
score += 20.0
}
val mySpeed = getEffectiveStat(candidate, Stats.SPEED)
val oppSpeed = opponent.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (mySpeed > oppSpeed) score += 25.0
val hazardDamage = calculateEntryHazardDamage(candidate, battleState.npcHazards)
score -= hazardDamage * 0.5
val role = strategyAnalyzer.identifyPokemonRole(candidate)
score += evaluateRoleSynergy(role, gamePlan, battleState)
return score
}
private fun evaluateMatchup(
myPokemon: TrackedPokemon,
opponent: TrackedPokemon,
battleState: BattleState
): MatchupEvaluation {
val ourBestEffectiveness = calculateBestMoveEffectiveness(myPokemon, opponent)
val offensiveAdvantage = (ourBestEffectiveness - 1.0) * 40.0
val theirBestEffectiveness = calculateBestMoveEffectiveness(opponent, myPokemon)
val defensiveAdvantage = -(theirBestEffectiveness - 1.0) * 35.0
val mySpeed = myPokemon.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = opponent.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val speedAdvantage = if (mySpeed > oppSpeed) 20.0 else -15.0
val overallScore = offensiveAdvantage + defensiveAdvantage + speedAdvantage
val recommendation = when {
overallScore > 60 && myPokemon.currentHpPercent > 0.7 -> MatchupRecommendation.STAY_IN_AND_SETUP
overallScore > 30 -> MatchupRecommendation.STAY_IN_AND_ATTACK
overallScore > 0 -> MatchupRecommendation.CONSIDER_SWITCHING
overallScore > -30 -> MatchupRecommendation.PIVOT_OUT
else -> MatchupRecommendation.SWITCH_OUT
}
return MatchupEvaluation(
offensiveAdvantage = offensiveAdvantage,
defensiveAdvantage = defensiveAdvantage,
speedAdvantage = speedAdvantage,
overallScore = overallScore,
recommendation = recommendation
)
}
private fun checkSwitchConditions(
myPokemon: TrackedPokemon,
opponent: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan,
matchup: MatchupEvaluation
): Boolean {
val theirBestEffectiveness = calculateBestMoveEffectiveness(opponent, myPokemon)
if (theirBestEffectiveness >= 2.0 && myPokemon.currentHpPercent > 0.3) {
return true
}
if (myPokemon.currentHpPercent < 0.3 && matchup.overallScore < 30) {
return true
}
if (opponent.boosts.values.any { it >= 2 } && matchup.overallScore < 20) {
return true
}
if (myPokemon.species in gamePlan.keyPokemon &&
myPokemon.currentHpPercent < 0.5 &&
theirBestEffectiveness > 1.0
) {
return true
}
return false
}
private fun calculateBestMoveEffectiveness(
attacker: TrackedPokemon,
defender: TrackedPokemon
): Double {
val moves = attacker.pokemon?.moveSet?.getMoves() ?: return 1.0
return moves.maxOfOrNull { move ->
TypeEffectiveness.calculateTypeEffectiveness(
move.type,
defender.currentTypes ?: listOf()
)
} ?: 1.0
}
private fun calculateEntryHazardDamage(pokemon: Pokemon, hazards: List<String>): Double {
var damage = 0.0
val maxHP = getEffectiveStat(pokemon, Stats.HP)
for (hazard in hazards) {
when (hazard.lowercase()) {
"stealthrock" -> {
val rockEffectiveness = pokemon.types.fold(1.0) { acc, type ->
acc * TypeEffectiveness.getDamageMultiplier(
com.cobblemon.mod.common.api.types.ElementalTypes.ROCK,
type
)
}
damage += maxHP * rockEffectiveness * 0.125
}
"spikes" -> damage += maxHP * 0.125
"toxicspikes" -> {
if (!pokemon.types.any { it.name.lowercase() in listOf("poison", "steel") }) {
damage += maxHP * 0.15
}
}
"stickyweb" -> damage += 10.0
}
}
return damage
}
private fun evaluateRoleSynergy(
role: PokemonRole,
gamePlan: GamePlan,
battleState: BattleState
): Double {
return when (gamePlan.winCondition) {
WinCondition.SETUP_SWEEP -> {
when (role) {
PokemonRole.SETUP_SWEEPER -> 50.0
PokemonRole.SUPPORT -> if (battleState.gamePhase == GamePhase.EARLY_GAME) 30.0 else 10.0
else -> 0.0
}
}
WinCondition.AGGRESSIVE_SWEEP -> {
when (role) {
PokemonRole.SWEEPER, PokemonRole.WALLBREAKER -> 40.0
PokemonRole.REVENGE_KILLER -> 30.0
else -> 0.0
}
}
WinCondition.DEFENSIVE_STALL -> {
when (role) {
PokemonRole.TANK, PokemonRole.SUPPORT -> 45.0
else -> -10.0
}
}
WinCondition.REVENGE_KILLING -> {
when (role) {
PokemonRole.REVENGE_KILLER -> 60.0
PokemonRole.PIVOT -> 25.0
else -> 0.0
}
}
WinCondition.RAIN_OFFENSE, WinCondition.SUN_OFFENSE, WinCondition.SAND_OFFENSE -> {
when (role) {
PokemonRole.SWEEPER -> 35.0
else -> 0.0
}
}
else -> 0.0
}
}
private fun createTrackerPokemon(pokemon: Pokemon): TrackedPokemon {
val tracker = TrackedPokemon(
pokemon = pokemon,
originalPokemon = pokemon,
species = pokemon.species.name,
currentHp = pokemon.currentHealth,
maxHp = pokemon.maxHealth,
currentHpPercent = pokemon.currentHealth.toDouble() / pokemon.maxHealth,
currentTypes = pokemon.types.map { it.name.lowercase() }.toMutableList(),
currentAbility = pokemon.ability.name,
moves = pokemon.moveSet.getMoves(),
predictedRole = strategyAnalyzer.identifyPokemonRole(pokemon),
cachedAttack = getEffectiveStat(pokemon, Stats.ATTACK),
cachedDefense = getEffectiveStat(pokemon, Stats.DEFENCE),
cachedSpAttack = getEffectiveStat(pokemon, Stats.SPECIAL_ATTACK),
cachedSpDefense = getEffectiveStat(pokemon, Stats.SPECIAL_DEFENCE),
cachedSpeed = getEffectiveStat(pokemon, Stats.SPEED)
)
return tracker
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stats): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
}
@@ -0,0 +1,403 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
object TypeEffectiveness {
val typeEffectivenessMap: Map<ElementalType, Map<ElementalType, Double>> = mapOf(
ElementalTypes.NORMAL to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 0.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FIRE to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.WATER to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.ELECTRIC to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 2.0,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 0.0,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.GRASS to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 2.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.ICE to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 0.5,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FIGHTING to mapOf(
ElementalTypes.NORMAL to 2.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 0.5,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 0.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.POISON to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 0.5,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 0.5,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.0,
ElementalTypes.FAIRY to 2.0
),
ElementalTypes.GROUND to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 2.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 2.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FLYING to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.PSYCHIC to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 2.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 0.5,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.BUG to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 0.5,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.ROCK to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 0.5,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.GHOST to mapOf(
ElementalTypes.NORMAL to 0.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 2.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.5,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.DRAGON to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 0.0
),
ElementalTypes.DARK to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 2.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.5,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.STEEL to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 2.0
),
ElementalTypes.FAIRY to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
)
)
fun getDamageMultiplier(attackerType: ElementalType, defenderType: ElementalType): Double {
return typeEffectivenessMap[attackerType]?.get(defenderType) ?: 1.0
}
fun calculateTypeEffectiveness(moveType: ElementalType, defenderTypes: List<String>): Double {
var multiplier = 1.0
for (typeString in defenderTypes) {
val defenderType = ElementalTypes.get(typeString.lowercase())
if (defenderType != null) {
multiplier *= getDamageMultiplier(moveType, defenderType)
}
}
return multiplier
}
fun getEffectivenessCategory(effectiveness: Double): EffectivenessCategory {
return when {
effectiveness == 0.0 -> EffectivenessCategory.IMMUNE
effectiveness < 1.0 -> EffectivenessCategory.NOT_VERY_EFFECTIVE
effectiveness == 1.0 -> EffectivenessCategory.NEUTRAL
else -> EffectivenessCategory.SUPER_EFFECTIVE
}
}
enum class EffectivenessCategory {
IMMUNE, NOT_VERY_EFFECTIVE, NEUTRAL, SUPER_EFFECTIVE
}
}
@@ -31,7 +31,6 @@ class WeatherInstruction(val message: BattleMessage): InterpreterInstruction {
val weather = message.effectAt(0)?.id ?: return
val source = message.battlePokemonFromOptional(battle)
source?.let { ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), source) }
battle.dispatchWaiting(1.5F) {
val lang = when {
message.hasOptionalArgument("upkeep") -> battleLang("weather.$weather.upkeep")
@@ -143,18 +143,18 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
}
private fun canAttemptDismount(player: LocalPlayer, selectedPartyPokemon: Pokemon?): Boolean {
if (player.vehicle !is PokemonEntity) return false
val vehicle = player.vehicle as PokemonEntity
if (player != vehicle.controllingPassenger) {
return true
}
val vehicle = player.vehicle as? PokemonEntity ?: return false
if (player != vehicle.controllingPassenger) return true
val isAirRide = vehicle.ridingController?.context?.style == RidingStyle.AIR
val hasLandRide = vehicle.rideProp.behaviours?.get(RidingStyle.LAND) != null
return if (isAirRide && hasLandRide) {
false
} else {
vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
if (!Cobblemon.config.enableInFlightDismounting && isAirRide && hasLandRide) {
return false
}
return vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
}
private fun isRidingPokemon(player: LocalPlayer, ignoreControlling: Boolean = false): Boolean {
@@ -329,6 +329,9 @@ 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,9 +269,6 @@ 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,6 +14,7 @@ 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
@@ -30,7 +31,7 @@ class NPCBattleActor(
) : AIBattleActor(
gameId = npc.uuid,
pokemonList = pokemonList.let { if (npc.npc.randomizePartyOrder) it.shuffled() else it },
battleAI = StrongBattleAI(skill)
battleAI = CompetitiveBattleAI(skill)
), EntityBackedBattleActor<NPCEntity> {
override val entity = npc
override val type = ActorType.NPC
@@ -9,6 +9,7 @@
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
@@ -25,12 +26,16 @@ 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 {
@@ -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
// } else if (blockState.`is`(BlockTags.LEAVES) && blockState.block == CobblemonBlocks.SACCHARINE_LEAVES && canPathThroughLeaves()) {
// return PathType.OPEN
if (canPathThroughLeaves() && blockState.block == CobblemonBlocks.SACCHARINE_LEAVES)
return PathType.OPEN
// } else PathType.BLOCKED
// This is the 1.6.1 logic but with some optimizations
@@ -633,9 +633,6 @@ 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
@@ -755,7 +752,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 (blockState.block === CobblemonBlocks.SACCHARINE_LEAVES && canPathThroughLeaves()) {
if (canPathThroughLeaves() && blockState.block === CobblemonBlocks.SACCHARINE_LEAVES) {
PathType.OPEN
} else {
PathType.BLOCKED
@@ -6,19 +6,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokemon.properties.tags
package com.cobblemon.mod.common.pokemon.properties
import com.cobblemon.mod.common.api.properties.CustomPokemonPropertyType
import com.cobblemon.mod.common.pokemon.properties.StringProperty
object PokemonFlagProperty : CustomPokemonPropertyType<StringProperty> {
private const val KEY = "tag"
override val keys = setOf(KEY)
object LabelProperty : CustomPokemonPropertyType<StringProperty> {
override val keys = setOf("label", "tag")
override val needsKey = true
override fun fromString(value: String?) = if (value == null) null else StringProperty(KEY, value, { _, _ -> }, { pokemon, underlyingValue -> pokemon.hasLabels(underlyingValue) })
override fun fromString(value: String?) = if (value == null) null else StringProperty(keys.first(), value, { _, _ -> }, { pokemon, underlyingValue -> pokemon.hasLabels(underlyingValue) })
override fun examples() = emptySet<String>()
}
@@ -2629,6 +2629,8 @@
"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",
@@ -1,4 +1,7 @@
{
"parent": "minecraft:builtin/entity",
"gui_light": "side"
"gui_light": "side",
"textures": {
"particle": "cobblemon:item/poke_balls/cherish_ball"
}
}
@@ -0,0 +1,27 @@
package com.cobblemon.mod.common.battles.ai.competitive
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class AiBattleUtilsTest {
@Test
fun normalizesWeatherIdsToShowdownNames() {
assertEquals("sunnyday", AiBattleUtils.normalizeWeatherId("sun"))
assertEquals("sunnyday", AiBattleUtils.normalizeWeatherId("sunnyday"))
assertEquals("raindance", AiBattleUtils.normalizeWeatherId("rain"))
assertEquals("raindance", AiBattleUtils.normalizeWeatherId("raindance"))
assertEquals("sandstorm", AiBattleUtils.normalizeWeatherId("sand"))
assertEquals("snow", AiBattleUtils.normalizeWeatherId("snowscape"))
assertEquals("hail", AiBattleUtils.normalizeWeatherId("hail"))
}
@Test
fun detectsStabCaseInsensitively() {
val types = listOf("Fire", "Flying")
assertTrue(AiBattleUtils.hasStab("fire", types))
assertTrue(AiBattleUtils.hasStab("Flying", types))
assertFalse(AiBattleUtils.hasStab("water", types))
}
}