forked from mirrors/cobblemon
Compare commits
312
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11b45b3f5f | ||
|
|
6334bfe5b5 | ||
|
|
474637e776 | ||
|
|
d8dc483727 | ||
|
|
34664dd214 | ||
|
|
e54b22409a | ||
|
|
e03a3abe1d | ||
|
|
497f832995 | ||
|
|
8ee0d904ed | ||
|
|
291196eda7 | ||
|
|
b94e6d65b9 | ||
|
|
60fdd3325e | ||
|
|
eedcdafcbe | ||
|
|
34595e01a9 | ||
|
|
60b9ca7b7f | ||
|
|
83aea117c6 | ||
|
|
78b348329c | ||
|
|
b64013c995 | ||
|
|
85003c9829 | ||
|
|
d9133e854a | ||
|
|
05afe191e6 | ||
|
|
e3b5067f31 | ||
|
|
bfa2d470f0 | ||
|
|
53c0b28deb | ||
|
|
5e9208d898 | ||
|
|
23a91ec2c9 | ||
|
|
9998a328e3 | ||
|
|
9f07747cac | ||
|
|
a7998bcec5 | ||
|
|
5983b29e9c | ||
|
|
749f441913 | ||
|
|
dabbcceb9c | ||
|
|
11f8313fc3 | ||
|
|
32b813620b | ||
|
|
8147067b2e | ||
|
|
f2d2ceb422 | ||
|
|
547ceae9ed | ||
|
|
24c6544e83 | ||
|
|
a921d55812 | ||
|
|
b422ee4d29 | ||
|
|
a83867e153 | ||
|
|
523973d2c0 | ||
|
|
746160fa18 | ||
|
|
837ba9679b | ||
|
|
db7a319441 | ||
|
|
e284032c7e | ||
|
|
3db9dd02f2 | ||
|
|
e680ec8790 | ||
|
|
dc4f831e02 | ||
|
|
b3a00c40b5 | ||
|
|
f56fb0f50a | ||
|
|
ec732bc03d | ||
|
|
229abd4a13 | ||
|
|
7e92c17178 | ||
|
|
77aa3208a0 | ||
|
|
79688ebdad | ||
|
|
16d8c8bb19 | ||
|
|
e5738fecb0 | ||
|
|
a46ac372a8 | ||
|
|
b2ea715195 | ||
|
|
e6d2430405 | ||
|
|
6c425a43c6 | ||
|
|
8b6c8d27b7 | ||
|
|
a505bad554 | ||
|
|
e27e5a1d92 | ||
|
|
5e0357101d | ||
|
|
5420f7419f | ||
|
|
43ed2d3c7d | ||
|
|
93500f59ed | ||
|
|
08912fd53f | ||
|
|
92c8fc0afc | ||
|
|
fae6bdb782 | ||
|
|
bba0100542 | ||
|
|
f9b66de742 | ||
|
|
3d0f1c826a | ||
|
|
2a59add285 | ||
|
|
35e4aafe99 | ||
|
|
0b83128aa0 | ||
|
|
e2b04ff770 | ||
|
|
4dabe50d43 | ||
|
|
3dd0795b88 | ||
|
|
70f1a8207f | ||
|
|
3ac18cce6e | ||
|
|
aa0a0672d0 | ||
|
|
ec2958400b | ||
|
|
fcb6bea78b | ||
|
|
5a76a39b77 | ||
|
|
b5a4acfb7a | ||
|
|
1248230e67 | ||
|
|
096db6cdcd | ||
|
|
72e416b9c7 | ||
|
|
9e8e80965b | ||
|
|
2325b436b1 | ||
|
|
57421a2884 | ||
|
|
4fdd3eba8e | ||
|
|
e6efe800c0 | ||
|
|
95aa99e18c | ||
|
|
270484e745 | ||
|
|
719addf633 | ||
|
|
31e386871f | ||
|
|
bfd7e596d0 | ||
|
|
eab6fa9b40 | ||
|
|
643545d54c | ||
|
|
fcb017f980 | ||
|
|
dd52da7c42 | ||
|
|
ff45def0e8 | ||
|
|
f078584e56 | ||
|
|
a11382b5a1 | ||
|
|
d1b8094539 | ||
|
|
b8d79af911 | ||
|
|
98de423f91 | ||
|
|
e7cd9237f6 | ||
|
|
84e22c68c7 | ||
|
|
300f2fc53c | ||
|
|
2dfb88467b | ||
|
|
589aab3b1a | ||
|
|
e17ca3a422 | ||
|
|
0153846c7b | ||
|
|
155b4b2182 | ||
|
|
690ed845b2 | ||
|
|
f598f8d645 | ||
|
|
8e3e047646 | ||
|
|
551858df57 | ||
|
|
a7e3f4a7a1 | ||
|
|
94d64bb5c6 | ||
|
|
e569acc648 | ||
|
|
7c374b6e29 | ||
|
|
3c128d9009 | ||
|
|
f5321a34c3 | ||
|
|
2754bfc525 | ||
|
|
3d1aecce13 | ||
|
|
38cd081ad6 | ||
|
|
54a14ab805 | ||
|
|
633b45287c | ||
|
|
3619f42090 | ||
|
|
695453ffdb | ||
|
|
1f8171dd94 | ||
|
|
dd7066e8fa | ||
|
|
416f003927 | ||
|
|
3be3dc3de2 | ||
|
|
a8f273737d | ||
|
|
a185805b3f | ||
|
|
defab6eaff | ||
|
|
b3ed47fbca | ||
|
|
72fc1f1fb9 | ||
|
|
f96c69ab54 | ||
|
|
3a6ad70b76 | ||
|
|
d51039dbb8 | ||
|
|
8120270bf8 | ||
|
|
70fe1682b0 | ||
|
|
9058bde326 | ||
|
|
bf425efd40 | ||
|
|
cebff87656 | ||
|
|
d24fb13877 | ||
|
|
1d82df464a | ||
|
|
2a1d1e4474 | ||
|
|
17cab186d9 | ||
|
|
e00136052d | ||
|
|
b23237ca9a | ||
|
|
36456bfb0d | ||
|
|
d9661c356f | ||
|
|
a66fad69cf | ||
|
|
95488ac54b | ||
|
|
19f2b71273 | ||
|
|
ba6e136a0b | ||
|
|
28ac437987 | ||
|
|
a4935f19bb | ||
|
|
c3e470f1de | ||
|
|
4139337f85 | ||
|
|
25bc2dea93 | ||
|
|
9dc3884ea6 | ||
|
|
89d95c6ab8 | ||
|
|
7b70aa5a1e | ||
|
|
8191b4d9b6 | ||
|
|
84534fd041 | ||
|
|
e6e1446eb1 | ||
|
|
39fbadc4fb | ||
|
|
3c2a9c1dfd | ||
|
|
921fcbe477 | ||
|
|
c90b6c86ab | ||
|
|
3ecf25502b | ||
|
|
b808112e17 | ||
|
|
1b2d31db03 | ||
|
|
c9f8445d14 | ||
|
|
acd879ebef | ||
|
|
f6889fd8ea | ||
|
|
21ccbef6c9 | ||
|
|
9349e6a668 | ||
|
|
28a7fe6a04 | ||
|
|
18e1ed79ef | ||
|
|
117afcea32 | ||
|
|
3f89750ee5 | ||
|
|
d88be7ae05 | ||
|
|
24245331db | ||
|
|
d6d10d33e3 | ||
|
|
4d88c9b383 | ||
|
|
a3f597c589 | ||
|
|
2a3d88c12e | ||
|
|
9561f47337 | ||
|
|
f02c92db22 | ||
|
|
181ef5d50e | ||
|
|
870397ebb9 | ||
|
|
0ed413070d | ||
|
|
f66f5ffdf9 | ||
|
|
ca280a7b3e | ||
|
|
85284bff9d | ||
|
|
fa4d33f725 | ||
|
|
4ce42cdf6d | ||
|
|
1b8562ead8 | ||
|
|
b9d9b906fb | ||
|
|
e9d079a223 | ||
|
|
e2e42ee91c | ||
|
|
4b0208201b | ||
|
|
bec3f97733 | ||
|
|
16cfe37dac | ||
|
|
7381da863c | ||
|
|
b0902825fa | ||
|
|
31c489cba0 | ||
|
|
6a2f21d75a | ||
|
|
e2c42a57c7 | ||
|
|
8592b53216 | ||
|
|
49dcf0049f | ||
|
|
076bee923f | ||
|
|
aacdb33827 | ||
|
|
53e9ece930 | ||
|
|
87f08d9bab | ||
|
|
a50a6326af | ||
|
|
3c0eec833e | ||
|
|
5b0cfdffcb | ||
|
|
d788e0402e | ||
|
|
a347dc5e2a | ||
|
|
b2fe158a7b | ||
|
|
faab7e8df0 | ||
|
|
9070e198f1 | ||
|
|
45b2a0db4d | ||
|
|
65d39050ea | ||
|
|
5da16df5ae | ||
|
|
24fc4b05ab | ||
|
|
173902feb1 | ||
|
|
fd5f08fe84 | ||
|
|
d430ad6ca1 | ||
|
|
1309c18292 | ||
|
|
b6deebac97 | ||
|
|
83711f2dcb | ||
|
|
18e1a767af | ||
|
|
6ae3efb461 | ||
|
|
84e1934134 | ||
|
|
9aed9342ee | ||
|
|
e4d0bf39ca | ||
|
|
847fa6842e | ||
|
|
08aa4f29bd | ||
|
|
539218381b | ||
|
|
9dff39b4d7 | ||
|
|
1e578e94e5 | ||
|
|
ec35c9b9bb | ||
|
|
0deaeaeb7d | ||
|
|
ec3d9e1a56 | ||
|
|
814f84ae42 | ||
|
|
904be475ef | ||
|
|
5edecbe81b | ||
|
|
f357033404 | ||
|
|
4800ae6649 | ||
|
|
60b0690c1e | ||
|
|
82e31dd766 | ||
|
|
130e767e75 | ||
|
|
b1a1d509ec | ||
|
|
0698cf495e | ||
|
|
6047660405 | ||
|
|
aaf3495f83 | ||
|
|
ae6529c9fd | ||
|
|
45a96b2a42 | ||
|
|
4212903c67 | ||
|
|
be47dbcede | ||
|
|
9556c57c69 | ||
|
|
f72b65bfac | ||
|
|
6121700c9b | ||
|
|
91c968e743 | ||
|
|
8fed6f4823 | ||
|
|
34e4b39d75 | ||
|
|
a21c70ce46 | ||
|
|
8865a71f4c | ||
|
|
142cadc187 | ||
|
|
66f4c2531d | ||
|
|
3f8ddd5f7d | ||
|
|
c076e14cbd | ||
|
|
d724eb068b | ||
|
|
d3ac263e29 | ||
|
|
a6b7fe1f17 | ||
|
|
ec714ccb84 | ||
|
|
ab0a129ba1 | ||
|
|
a3f272b2aa | ||
|
|
77d91ed979 | ||
|
|
9907ba17c1 | ||
|
|
a6f0e19a44 | ||
|
|
7ee064b04d | ||
|
|
f2e4c735f7 | ||
|
|
5345bd321f | ||
|
|
eab8d0063b | ||
|
|
6877977234 | ||
|
|
60cb81362e | ||
|
|
088acb8e86 | ||
|
|
77f59f06cb | ||
|
|
a0882c6105 | ||
|
|
d50eb52172 | ||
|
|
5e5cadd314 | ||
|
|
75b06f82f0 | ||
|
|
2ab050aa60 | ||
|
|
6d2f645e4a | ||
|
|
9f937e29f2 | ||
|
|
abc61a96c8 | ||
|
|
b1d10822d4 | ||
|
|
09d984456d |
@@ -36,6 +36,7 @@ common/src/main/resources/assets/cobblemon/showdown
|
||||
# Generated files
|
||||
*/generated/resources/*
|
||||
/fabric/src/generated/resources/cobblemon-common.accesswidener
|
||||
/fabric/src/main/generated/*
|
||||
/.ignored/
|
||||
common/.fabric
|
||||
.kotlin/
|
||||
|
||||
@@ -3,11 +3,23 @@
|
||||
|
||||
### Additions
|
||||
- Added a new `cobblemon:structure`, which is a feature without any restrictions allowing the Builders to place certain structures in much more specific manner.
|
||||
- Added Configs for Infinite Use TMs and Unlocking all TMs
|
||||
- Added Type Gem blocks
|
||||
- Added Type Gem Clusters
|
||||
- Added TMs
|
||||
- Added TM Machine
|
||||
- Added Deepslate Core Block
|
||||
- Added /givetm Command
|
||||
- Added Move Dex to Pokedex
|
||||
- Added Disk Rack for storing disks and being an audio sequencer for Noteblocks
|
||||
|
||||
|
||||
### Changes
|
||||
- Data Monitor can accept Disks (TMs, Music Disks, Upgrade, Dubious Disk, etc) to change look
|
||||
|
||||
### Fixes
|
||||
- Fixed rare Pokémon being far too likely to spawn multiple times in a single spawn cycle. The dev told me that the exact bug is too complicated to explain.
|
||||
- Resolved the niche issue where Structures would spawn in an incorrect location, creating very funky world gen.
|
||||
|
||||
### Developer
|
||||
- Tweaked the spawning selector interface to take a bucket function rather than a bucket directly.
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
|
||||
|
||||
### Additions
|
||||
- Party Overlay Exp Gained Animation will now display the amount of EXP the Pokémon gained
|
||||
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys??!
|
||||
- Added new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
|
||||
- Added missing crops to Botany Pots integration
|
||||
|
||||
### New rideable Pokémon
|
||||
|
||||
@@ -13,14 +16,38 @@
|
||||
### Animation updates for the following Pokémon
|
||||
|
||||
### Changes
|
||||
- Restructured Botany Pots integration folder structure
|
||||
- Decrease default pastured mon wander radius to 32.
|
||||
- Factor in the weight of pokemon when calculating pushing forces between hitboxes.
|
||||
- Remade the Starter Selection Screen with new assets
|
||||
- Refactored berry trees to be less impactful on TPS (more performant)
|
||||
|
||||
### Fixes
|
||||
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
|
||||
- Fix occasional crash on retrieving revived fossils
|
||||
- Fixed a compatibility issue with World Game Rules which caused some log spam in specific cases.
|
||||
- Fixed improper validation of Pokémon when performing a trade.
|
||||
- Fixed issue where particle like beam can disappear when you don't see the beginning of the beam or you look away
|
||||
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
|
||||
- Fix occasional crash on retrieving revived fossils
|
||||
- Fix brushing not working on Neoforge
|
||||
- [Various Submarine Ride Fixes](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2157)
|
||||
- Fix Pastured mons despawning and running away.
|
||||
- Fix Apricorn trees crashing when other mods bonemeal it
|
||||
- [Various UFO Ride Improvements](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2156)
|
||||
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within it's menu
|
||||
- Fixed NPCs switching pokemon very often during battles
|
||||
- Fixed Campfire pot causing crashes under certain circumstances
|
||||
|
||||
### Developer
|
||||
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
|
||||
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
|
||||
|
||||
### Molang & Datapacks
|
||||
- q.item.is_enchanted()
|
||||
- q.item.has_enchantment(minecraft:sharpness, 3)
|
||||
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
|
||||
- Added `marks`, `has_mark`, & `remove_marks` as available Molang functions for pokemon
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
- Fixed players failing to join servers randomly.
|
||||
- Fixed some Pokémon having no stamina for flying.
|
||||
- Fixed `/pctake` not respecting the actual size of the PC.
|
||||
|
||||
### Developer
|
||||
- Poké Ball GUI textures now respect the ball's namespace, enabling proper texture loading for custom balls from other mods.
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.block.entity.TMShelfBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.NoteBlock;
|
||||
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(NoteBlock.class)
|
||||
public abstract class NoteBlockMixin {
|
||||
@Inject(method = "triggerEvent", at = @At("HEAD"), cancellable = true)
|
||||
private void cobblemon$triggerTMShelfSequencer(BlockState state, Level level, BlockPos pos, int type, int data, CallbackInfoReturnable<Boolean> cir) {
|
||||
final BlockPos shelfPos = pos.below();
|
||||
final BlockEntity blockEntity = level.getBlockEntity(shelfPos);
|
||||
if (!(blockEntity instanceof TMShelfBlockEntity tmShelfBlockEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!level.isClientSide) {
|
||||
final BlockPos instrumentPos = shelfPos.below();
|
||||
final SoundEvent soundEvent;
|
||||
if (level.getBlockState(instrumentPos).isAir()) {
|
||||
soundEvent = SoundEvents.NOTE_BLOCK_BIT.value();
|
||||
} else {
|
||||
final NoteBlockInstrument instrument = level.getBlockState(instrumentPos).instrument();
|
||||
final Holder<SoundEvent> holder = instrument.getSoundEvent();
|
||||
soundEvent = holder.value();
|
||||
}
|
||||
tmShelfBlockEntity.onNoteBlockPulse(level, pos, soundEvent);
|
||||
}
|
||||
|
||||
// Suppress vanilla note-block playback while it is on top of a TM Shelf.
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import com.cobblemon.mod.common.api.events.CobblemonEvents;
|
||||
import com.cobblemon.mod.common.api.events.item.LeftoversCreatedEvent;
|
||||
import com.cobblemon.mod.common.api.orientation.OrientationController;
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies;
|
||||
import com.cobblemon.mod.common.api.riding.RidingStyle;
|
||||
import com.cobblemon.mod.common.api.riding.behaviour.types.air.HoverBehaviour;
|
||||
import com.cobblemon.mod.common.api.storage.party.PartyStore;
|
||||
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore;
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags;
|
||||
@@ -59,6 +61,7 @@ import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@@ -169,6 +172,32 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
|
||||
}
|
||||
}
|
||||
|
||||
@Redirect(
|
||||
method = "getDestroySpeed",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/entity/player/Player;onGround()Z"
|
||||
)
|
||||
)
|
||||
private boolean cobblemon$considerHoverOnGround(Player player) {
|
||||
// If riding a Hover Mount, return true to bypass the 5x slowdown
|
||||
if (player.getVehicle() instanceof PokemonEntity vehicle) {
|
||||
|
||||
var ridingController = vehicle.getRidingController();
|
||||
if (ridingController == null) player.onGround();
|
||||
|
||||
var activeContext = ridingController.getContext();
|
||||
if (activeContext == null) player.onGround();
|
||||
|
||||
if (activeContext.getStyle() == RidingStyle.AIR &&
|
||||
activeContext.getBehaviour().equals(HoverBehaviour.Companion.getKEY())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Otherwise, return the actual value
|
||||
return player.onGround();
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "eat",
|
||||
at = @At(
|
||||
|
||||
@@ -23,6 +23,7 @@ import net.minecraft.client.sounds.ChannelAccess;
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
@@ -32,6 +33,7 @@ import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||
|
||||
import java.util.Iterator;
|
||||
@@ -108,6 +110,17 @@ public abstract class SoundEngineMixin implements SoundEngineDuck {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TM Shelf can intentionally push note-block pitches above vanilla's 2.0f ceiling for octave-up playback.
|
||||
* Keep vanilla clamping for all other sounds.
|
||||
*/
|
||||
@Inject(method = "calculatePitch", at = @At("HEAD"), cancellable = true)
|
||||
private void cobblemon$allowExtendedNoteblockPitch(SoundInstance sound, CallbackInfoReturnable<Float> cir) {
|
||||
if (sound.getLocation().getPath().startsWith("block.note_block.")) {
|
||||
cir.setReturnValue(Mth.clamp(sound.getPitch(), 0.5F, 4.0F));
|
||||
}
|
||||
}
|
||||
|
||||
/** Different behavior for resuming SoundInstances while a BattleMusicInstance is being played. We do not want to resume filtered sounds. */
|
||||
@Inject(method = "resume()V", at = @At("HEAD"), cancellable = true)
|
||||
public void resume(CallbackInfo cb) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.apache.commons.lang3.IntegerRange;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
@@ -32,36 +33,48 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Mixin(Structure.class)
|
||||
public abstract class StructureMixin {
|
||||
// Define a set of structures that should not spawn below Sea Level
|
||||
@Unique
|
||||
private static final Set<ResourceLocation> RESTRICTED_STRUCTURES = new HashSet<>(Arrays.asList(
|
||||
CobblemonStructureIDs.STONJOURNER_HENGE,
|
||||
CobblemonStructureIDs.LUNA_HENGE,
|
||||
CobblemonStructureIDs.SOL_HENGE
|
||||
));
|
||||
// Define a set of structures that should not spawn below Sea Level
|
||||
@Unique
|
||||
private static final Map<ResourceLocation, IntegerRange> RESTRICTED_STRUCTURES = Map.of(
|
||||
// The IntegerRange values are calculated FROM Sea Level. (Sealevel + VALUE)
|
||||
CobblemonStructureIDs.STONJOURNER_HENGE, IntegerRange.of(0, 50),
|
||||
CobblemonStructureIDs.LUNA_HENGE, IntegerRange.of(0, 50),
|
||||
CobblemonStructureIDs.SOL_HENGE, IntegerRange.of(0, 50)
|
||||
);
|
||||
|
||||
@Inject(method = "generate", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/structure/StructureStart;isValid()Z"),
|
||||
cancellable = true)
|
||||
public void cobblemon$isValid(RegistryAccess registryAccess, ChunkGenerator chunkGenerator, BiomeSource biomeSource,
|
||||
RandomState randomState, StructureTemplateManager structureTemplateManager, long seed,
|
||||
ChunkPos chunkPos, int references, LevelHeightAccessor heightAccessor,
|
||||
Predicate<Holder<Biome>> validBiome,
|
||||
CallbackInfoReturnable<StructureStart> cir,
|
||||
@Local StructureStart structureStart) {
|
||||
ResourceLocation structureKey = registryAccess.registryOrThrow(Registries.STRUCTURE).getKey((Structure) (Object) this);
|
||||
if(!RESTRICTED_STRUCTURES.contains(structureKey)) {
|
||||
return;
|
||||
}
|
||||
for (StructurePiece piece : structureStart.getPieces()) {
|
||||
if (piece.getBoundingBox().minY() < chunkGenerator.getSeaLevel()) {
|
||||
cir.setReturnValue(StructureStart.INVALID_START);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@Inject(method = "generate", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/structure/StructureStart;isValid()Z"),
|
||||
cancellable = true)
|
||||
public void cobblemon$isValid(RegistryAccess registryAccess, ChunkGenerator chunkGenerator, BiomeSource biomeSource,
|
||||
RandomState randomState, StructureTemplateManager structureTemplateManager, long seed,
|
||||
ChunkPos chunkPos, int references, LevelHeightAccessor heightAccessor,
|
||||
Predicate<Holder<Biome>> validBiome,
|
||||
CallbackInfoReturnable<StructureStart> cir,
|
||||
@Local StructureStart structureStart) {
|
||||
ResourceLocation structureKey = registryAccess.registryOrThrow(Registries.STRUCTURE).getKey((Structure) (Object) this);
|
||||
if (!RESTRICTED_STRUCTURES.containsKey(structureKey)) {
|
||||
return;
|
||||
}
|
||||
int sealevel = chunkGenerator.getSeaLevel();
|
||||
IntegerRange range = RESTRICTED_STRUCTURES.get(structureKey);
|
||||
|
||||
if (range != null) {
|
||||
int minY = sealevel + range.getMinimum();
|
||||
int maxY = sealevel + range.getMaximum();
|
||||
|
||||
for (StructurePiece piece : structureStart.getPieces()) {
|
||||
int pieceMinY = piece.getBoundingBox().minY();
|
||||
|
||||
if (pieceMinY < minY || pieceMinY > maxY) {
|
||||
cir.setReturnValue(StructureStart.INVALID_START);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,7 +313,7 @@ public abstract class MouseHandlerMixin {
|
||||
var playerRotater = (RidePassenger)player;
|
||||
playerRotater.cobblemon$setRideXRot(playerRotater.cobblemon$getRideXRot() + f);
|
||||
playerRotater.cobblemon$setRideYRot(playerRotater.cobblemon$getRideYRot() + g);
|
||||
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -90.0F, 90.0F));
|
||||
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -89.0F, 89.0F));
|
||||
playerRotater.cobblemon$setRideYRot(Mth.clamp(playerRotater.cobblemon$getRideYRot(), -105.0F, 105.0F));
|
||||
if (player.getVehicle() != null) {
|
||||
player.getVehicle().onPassengerTurned(player);
|
||||
|
||||
@@ -14,9 +14,14 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import com.llamalad7.mixinextras.sugar.Share;
|
||||
import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
|
||||
import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
@@ -89,4 +94,61 @@ public abstract class EntityMixin {
|
||||
cir.setReturnValue(customEyePos);
|
||||
}
|
||||
|
||||
|
||||
@WrapOperation(
|
||||
method = "push(Lnet/minecraft/world/entity/Entity;)V",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 0)
|
||||
)
|
||||
public void cobblemon$customPusherLogic(Entity pusher, double x, double y, double z,
|
||||
Operation<Void> original,
|
||||
@Local(argsOnly = true) Entity pushee,
|
||||
@Local(name = "e") double e, @Local(name = "d") double d,
|
||||
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
|
||||
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
|
||||
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
|
||||
pusherIsPlayerOrPokemon.set(pusher instanceof PokemonEntity || pusher instanceof Player);
|
||||
pusheeIsPlayerOrPokemon.set(pushee instanceof PokemonEntity || pushee instanceof Player);
|
||||
|
||||
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
|
||||
original.call(pusher, x, y, z);
|
||||
return;
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* Custom portion of the code:
|
||||
* Handle collisions with weight in mind. Use
|
||||
* the weight value of pokemon to determine
|
||||
* which side gets what portion of the total
|
||||
* force.
|
||||
*********************************************/
|
||||
// Weights in hectograms
|
||||
// Check to see if the pusher or pushee is a player. 200ish pounds. Steve's like a 6'6 miner
|
||||
var pusherWeight = pusher instanceof PokemonEntity pusherPokemon ? pusherPokemon.getPokemon().getSpecies().getWeight() : 900F;
|
||||
var pusheeWeight = pushee instanceof PokemonEntity pusheePokemon ? pusheePokemon.getPokemon().getSpecies().getWeight() : 900F;
|
||||
|
||||
// Calculate the portion of force received for both entities. Bigger receives less from the
|
||||
// collision than the smaller one.
|
||||
var totalWeight = pusherWeight + pusheeWeight;
|
||||
var pusherReceivedForce = pusheeWeight / totalWeight * 2F;
|
||||
pusheeReceivedForce.set((pusherWeight / totalWeight) * 2F);
|
||||
|
||||
original.call(pusher, -d * pusherReceivedForce, 0.0, -e * pusherReceivedForce);
|
||||
}
|
||||
|
||||
@WrapOperation(
|
||||
method = "push(Lnet/minecraft/world/entity/Entity;)V",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 1)
|
||||
)
|
||||
public void cobblemon$customPusheeLogic(Entity pusher, double x, double y, double z, Operation<Void> original,
|
||||
@Local(name = "e") double e, @Local(name = "d") double d,
|
||||
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
|
||||
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
|
||||
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
|
||||
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
|
||||
original.call(pusher, x, y, z);
|
||||
return;
|
||||
}
|
||||
|
||||
original.call(pusher, d * pusheeReceivedForce.get(), 0.0, e * pusheeReceivedForce.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ import com.cobblemon.mod.common.api.storage.player.adapter.DexDataMongoBackend
|
||||
import com.cobblemon.mod.common.api.storage.player.adapter.DexDataNbtBackend
|
||||
import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataJsonBackend
|
||||
import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataMongoBackend
|
||||
import com.cobblemon.mod.common.api.storage.player.adapter.TMMoveMongoBackend
|
||||
import com.cobblemon.mod.common.api.storage.player.adapter.TMMoveNbtBackend
|
||||
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonEntityTypeTags
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
@@ -109,13 +111,7 @@ import com.cobblemon.mod.common.pokemon.properties.UnaspectPropertyType
|
||||
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
|
||||
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
|
||||
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
|
||||
import com.cobblemon.mod.common.util.DataKeys
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.ifDedicatedServer
|
||||
import com.cobblemon.mod.common.util.isLaterVersion
|
||||
import com.cobblemon.mod.common.util.party
|
||||
import com.cobblemon.mod.common.util.requestWallpapers
|
||||
import com.cobblemon.mod.common.util.server
|
||||
import com.cobblemon.mod.common.util.*
|
||||
import com.cobblemon.mod.common.world.feature.CobblemonPlacedFeatures
|
||||
import com.cobblemon.mod.common.world.feature.ore.CobblemonOrePlacedFeatures
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
|
||||
@@ -244,6 +240,8 @@ object Cobblemon {
|
||||
|
||||
DATA_SYNCHRONIZED.subscribe {
|
||||
storage.onPlayerDataSync(it)
|
||||
it.tmList().scheduleFullSyncFromStores(it.party(), it.pc())
|
||||
it.pokedex().scheduleFullSyncFromStores(it.party(), it.pc())
|
||||
playerDataManager.syncAllToPlayer(it)
|
||||
starterHandler.handleJoin(it)
|
||||
it.requestWallpapers()
|
||||
@@ -306,6 +304,7 @@ object Cobblemon {
|
||||
this.config.maxPokemonLevel,
|
||||
this.config.maxPokemonFriendship,
|
||||
this.config.maxDynamaxLevel,
|
||||
this.config.unlockAllMoveDexMovesByDefault
|
||||
).sendToPlayer(player)
|
||||
}
|
||||
|
||||
@@ -375,8 +374,12 @@ object Cobblemon {
|
||||
val pokedexNbtFactory = CachedPlayerDataStoreFactory(DexDataNbtBackend())
|
||||
pokedexNbtFactory.setup(server)
|
||||
|
||||
val tmMoveFactory = CachedPlayerDataStoreFactory(TMMoveNbtBackend())
|
||||
tmMoveFactory.setup(server)
|
||||
|
||||
playerDataManager.setFactory(generalJsonFactory, PlayerInstancedDataStoreTypes.GENERAL)
|
||||
playerDataManager.setFactory(pokedexNbtFactory, PlayerInstancedDataStoreTypes.POKEDEX)
|
||||
playerDataManager.setFactory(tmMoveFactory, PlayerInstancedDataStoreTypes.TM_MOVES)
|
||||
|
||||
if (config.storageFormat == "nbt") {
|
||||
NBTStoreAdapter(pokemonStoreRoot.absolutePath, useNestedFolders = true, folderPerClass = true)
|
||||
@@ -403,8 +406,14 @@ object Cobblemon {
|
||||
val pokedexMongoFactory = CachedPlayerDataStoreFactory(DexDataMongoBackend(mongoClient, config.mongoDBDatabaseName, "PokeDexCollection"))
|
||||
pokedexMongoFactory.setup(server)
|
||||
|
||||
val tmMovesMongoFactory = CachedPlayerDataStoreFactory(
|
||||
TMMoveMongoBackend(mongoClient, config.mongoDBDatabaseName, "TMMovesCollection")
|
||||
)
|
||||
tmMovesMongoFactory.setup(server)
|
||||
|
||||
playerDataManager.setFactory(generalMongoFactory, PlayerInstancedDataStoreTypes.GENERAL)
|
||||
playerDataManager.setFactory(pokedexMongoFactory, PlayerInstancedDataStoreTypes.POKEDEX)
|
||||
playerDataManager.setFactory(tmMovesMongoFactory, PlayerInstancedDataStoreTypes.TM_MOVES)
|
||||
MongoDBStoreAdapter(mongoClient, config.mongoDBDatabaseName)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
LOGGER.error("MongoDB driver not found.")
|
||||
@@ -588,6 +597,7 @@ object Cobblemon {
|
||||
this.implementation.registerCommandArgument(cobblemonResource("mark"), MarkArgumentType::class, SingletonArgumentInfo.contextFree(MarkArgumentType::mark))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("transform_type"), TransformTypeArgumentType::class, SingletonArgumentInfo.contextFree(TransformTypeArgumentType::transformType))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("model_part"), ModelPartArgumentType::class, SingletonArgumentInfo.contextFree(ModelPartArgumentType::modelPart))
|
||||
this.implementation.registerCommandArgument(cobblemonResource("tm"), TmArgumentType::class, SingletonArgumentInfo.contextFree(TmArgumentType::tm))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
|
||||
@JvmField
|
||||
val LECTERN: BlockEntityType<LecternBlockEntity> = this.create("lectern", BlockEntityType.Builder.of(::LecternBlockEntity, CobblemonBlocks.LECTERN).build(null))
|
||||
|
||||
@JvmField
|
||||
val TM_SHELF: BlockEntityType<TMShelfBlockEntity> = this.create("tm_shelf", BlockEntityType.Builder.of(::TMShelfBlockEntity, CobblemonBlocks.TM_SHELF).build(null))
|
||||
|
||||
@JvmField
|
||||
val BERRY = this.create("berry", BlockEntityType.Builder.of(::BerryBlockEntity, *CobblemonBlocks.berries().values.toTypedArray()).build(null))
|
||||
|
||||
@@ -48,6 +51,8 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
|
||||
val SIGN: BlockEntityType<CobblemonSignBlockEntity> = this.create("sign", BlockEntityType.Builder.of(::CobblemonSignBlockEntity, CobblemonBlocks.APRICORN_SIGN, CobblemonBlocks.APRICORN_WALL_SIGN, CobblemonBlocks.SACCHARINE_SIGN, CobblemonBlocks.SACCHARINE_WALL_SIGN).build(null))
|
||||
@JvmField
|
||||
val HANGING_SIGN: BlockEntityType<CobblemonHangingSignBlockEntity> = this.create("hanging_sign", BlockEntityType.Builder.of(::CobblemonHangingSignBlockEntity, CobblemonBlocks.APRICORN_HANGING_SIGN, CobblemonBlocks.APRICORN_WALL_HANGING_SIGN, CobblemonBlocks.SACCHARINE_HANGING_SIGN, CobblemonBlocks.SACCHARINE_WALL_HANGING_SIGN).build(null))
|
||||
@JvmField
|
||||
val TM_MACHINE: BlockEntityType<TMMachineBlockEntity> = this.create("tm_machine", BlockEntityType.Builder.of(::TMMachineBlockEntity, CobblemonBlocks.TM_MACHINE).build(null))
|
||||
|
||||
@JvmField
|
||||
val GILDED_CHEST: BlockEntityType<GildedChestBlockEntity> = this.create("chest", BlockEntityType.Builder.of(::GildedChestBlockEntity,
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.cobblemon.mod.common.block.sign.CobblemonHangingSignBlock
|
||||
import com.cobblemon.mod.common.block.sign.CobblemonSignBlock
|
||||
import com.cobblemon.mod.common.block.sign.CobblemonWallHangingSignBlock
|
||||
import com.cobblemon.mod.common.block.sign.CobblemonWallSignBlock
|
||||
import com.cobblemon.mod.common.block.tmmachine.TMMachineBlock
|
||||
import com.cobblemon.mod.common.block.general.HorizontalRotationCarpetBlock
|
||||
import com.cobblemon.mod.common.block.general.HorizontalRotationalBlock
|
||||
import com.cobblemon.mod.common.mixin.invoker.*
|
||||
@@ -249,31 +250,31 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
val POKE_SNACK = this.create("poke_snack", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(BlockStateProperties.LIT)) 3 else 0 }, true))
|
||||
|
||||
@JvmField
|
||||
val TUMBLESTONE_CLUSTER = tumblestoneBlock("tumblestone_cluster", GrowableStoneBlock.STAGE_3, 7, 3, null)
|
||||
val TUMBLESTONE_CLUSTER = tumblestoneBlock("tumblestone_cluster", TumblestoneBlock.STAGE_3, 7, 3, null)
|
||||
@JvmField
|
||||
val LARGE_BUDDING_TUMBLESTONE = tumblestoneBlock("large_budding_tumblestone", GrowableStoneBlock.STAGE_2, 5, 3, TUMBLESTONE_CLUSTER)
|
||||
val LARGE_BUDDING_TUMBLESTONE = tumblestoneBlock("large_budding_tumblestone", TumblestoneBlock.STAGE_2, 5, 3, TUMBLESTONE_CLUSTER)
|
||||
@JvmField
|
||||
val MEDIUM_BUDDING_TUMBLESTONE = tumblestoneBlock("medium_budding_tumblestone", GrowableStoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_TUMBLESTONE)
|
||||
val MEDIUM_BUDDING_TUMBLESTONE = tumblestoneBlock("medium_budding_tumblestone", TumblestoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_TUMBLESTONE)
|
||||
@JvmField
|
||||
val SMALL_BUDDING_TUMBLESTONE = tumblestoneBlock("small_budding_tumblestone", GrowableStoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_TUMBLESTONE)
|
||||
val SMALL_BUDDING_TUMBLESTONE = tumblestoneBlock("small_budding_tumblestone", TumblestoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_TUMBLESTONE)
|
||||
|
||||
@JvmField
|
||||
val SKY_TUMBLESTONE_CLUSTER = tumblestoneBlock("sky_tumblestone_cluster", GrowableStoneBlock.STAGE_3, 7, 3, null)
|
||||
val SKY_TUMBLESTONE_CLUSTER = tumblestoneBlock("sky_tumblestone_cluster", TumblestoneBlock.STAGE_3, 7, 3, null)
|
||||
@JvmField
|
||||
val LARGE_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("large_budding_sky_tumblestone", GrowableStoneBlock.STAGE_2, 5, 3, SKY_TUMBLESTONE_CLUSTER)
|
||||
val LARGE_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("large_budding_sky_tumblestone", TumblestoneBlock.STAGE_2, 5, 3, SKY_TUMBLESTONE_CLUSTER)
|
||||
@JvmField
|
||||
val MEDIUM_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("medium_budding_sky_tumblestone", GrowableStoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_SKY_TUMBLESTONE)
|
||||
val MEDIUM_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("medium_budding_sky_tumblestone", TumblestoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_SKY_TUMBLESTONE)
|
||||
@JvmField
|
||||
val SMALL_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("small_budding_sky_tumblestone", GrowableStoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_SKY_TUMBLESTONE)
|
||||
val SMALL_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("small_budding_sky_tumblestone", TumblestoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_SKY_TUMBLESTONE)
|
||||
|
||||
@JvmField
|
||||
val BLACK_TUMBLESTONE_CLUSTER = tumblestoneBlock("black_tumblestone_cluster", GrowableStoneBlock.STAGE_3, 7, 3, null)
|
||||
val BLACK_TUMBLESTONE_CLUSTER = tumblestoneBlock("black_tumblestone_cluster", TumblestoneBlock.STAGE_3, 7, 3, null)
|
||||
@JvmField
|
||||
val LARGE_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("large_budding_black_tumblestone", GrowableStoneBlock.STAGE_2, 5, 3, BLACK_TUMBLESTONE_CLUSTER)
|
||||
val LARGE_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("large_budding_black_tumblestone", TumblestoneBlock.STAGE_2, 5, 3, BLACK_TUMBLESTONE_CLUSTER)
|
||||
@JvmField
|
||||
val MEDIUM_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("medium_budding_black_tumblestone", GrowableStoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_BLACK_TUMBLESTONE)
|
||||
val MEDIUM_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("medium_budding_black_tumblestone", TumblestoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_BLACK_TUMBLESTONE)
|
||||
@JvmField
|
||||
val SMALL_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("small_budding_black_tumblestone", GrowableStoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_BLACK_TUMBLESTONE)
|
||||
val SMALL_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("small_budding_black_tumblestone", TumblestoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_BLACK_TUMBLESTONE)
|
||||
|
||||
@JvmField
|
||||
val TUMBLESTONE_BLOCK = this.create("tumblestone_block", Block(BlockBehaviour.Properties.of().mapColor(MapColor.TERRACOTTA_ORANGE).strength(1.0F).sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).requiresCorrectToolForDrops().instrument(NoteBlockInstrument.BASEDRUM)))
|
||||
@@ -282,6 +283,87 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val BLACK_TUMBLESTONE_BLOCK = this.create("black_tumblestone_block", Block(BlockBehaviour.Properties.of().mapColor(MapColor.TERRACOTTA_BLACK).strength(1.0F).sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).requiresCorrectToolForDrops().instrument(NoteBlockInstrument.BASEDRUM)))
|
||||
|
||||
@JvmField
|
||||
val TYPE_GEM_CORE = this.create("type_gem_core", TypeGemCoreBlock(BlockBehaviour.Properties.of().strength(4.0f, 10.0f).requiresCorrectToolForDrops()))
|
||||
|
||||
private val typeGemBlocks = mutableMapOf<ResourceLocation, TypeGemBlock>()
|
||||
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_NORMAL = typeGemBlock("type_gem_block_normal")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_FIRE = typeGemBlock("type_gem_block_fire")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_WATER = typeGemBlock("type_gem_block_water")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_ELECTRIC = typeGemBlock("type_gem_block_electric")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_GRASS = typeGemBlock("type_gem_block_grass")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_ICE = typeGemBlock("type_gem_block_ice")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_FIGHTING = typeGemBlock("type_gem_block_fighting")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_POISON = typeGemBlock("type_gem_block_poison")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_GROUND = typeGemBlock("type_gem_block_ground")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_FLYING = typeGemBlock("type_gem_block_flying")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_PSYCHIC = typeGemBlock("type_gem_block_psychic")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_BUG = typeGemBlock("type_gem_block_bug")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_ROCK = typeGemBlock("type_gem_block_rock")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_GHOST = typeGemBlock("type_gem_block_ghost")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_DRAGON = typeGemBlock("type_gem_block_dragon")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_DARK = typeGemBlock("type_gem_block_dark")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_STEEL = typeGemBlock("type_gem_block_steel")
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_FAIRY = typeGemBlock("type_gem_block_fairy")
|
||||
|
||||
private val typeGemClusters = mutableMapOf<ResourceLocation, TypeGemClusterBlock>()
|
||||
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_NORMAL = typeGemCluster("type_gem_cluster_normal", TYPE_GEM_BLOCK_NORMAL, cobblemonResource("normal_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_FIRE = typeGemCluster("type_gem_cluster_fire", TYPE_GEM_BLOCK_FIRE, cobblemonResource("fire_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_WATER = typeGemCluster("type_gem_cluster_water", TYPE_GEM_BLOCK_WATER, cobblemonResource("water_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_ELECTRIC = typeGemCluster("type_gem_cluster_electric", TYPE_GEM_BLOCK_ELECTRIC, cobblemonResource("electric_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_GRASS = typeGemCluster("type_gem_cluster_grass", TYPE_GEM_BLOCK_GRASS, cobblemonResource("grass_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_ICE = typeGemCluster("type_gem_cluster_ice", TYPE_GEM_BLOCK_ICE, cobblemonResource("ice_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_FIGHTING = typeGemCluster("type_gem_cluster_fighting", TYPE_GEM_BLOCK_FIGHTING, cobblemonResource("fighting_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_POISON = typeGemCluster("type_gem_cluster_poison", TYPE_GEM_BLOCK_POISON, cobblemonResource("poison_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_GROUND = typeGemCluster("type_gem_cluster_ground", TYPE_GEM_BLOCK_GROUND, cobblemonResource("ground_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_FLYING = typeGemCluster("type_gem_cluster_flying", TYPE_GEM_BLOCK_FLYING, cobblemonResource("flying_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_PSYCHIC = typeGemCluster("type_gem_cluster_psychic", TYPE_GEM_BLOCK_PSYCHIC, cobblemonResource("psychic_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_BUG = typeGemCluster("type_gem_cluster_bug", TYPE_GEM_BLOCK_BUG, cobblemonResource("bug_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_ROCK = typeGemCluster("type_gem_cluster_rock", TYPE_GEM_BLOCK_ROCK, cobblemonResource("rock_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_GHOST = typeGemCluster("type_gem_cluster_ghost", TYPE_GEM_BLOCK_GHOST, cobblemonResource("ghost_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_DRAGON = typeGemCluster("type_gem_cluster_dragon", TYPE_GEM_BLOCK_DRAGON, cobblemonResource("dragon_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_DARK = typeGemCluster("type_gem_cluster_dark", TYPE_GEM_BLOCK_DARK, cobblemonResource("dark_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_STEEL = typeGemCluster("type_gem_cluster_steel", TYPE_GEM_BLOCK_STEEL, cobblemonResource("steel_gem"))
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_FAIRY = typeGemCluster("type_gem_cluster_fairy", TYPE_GEM_BLOCK_FAIRY, cobblemonResource("fairy_gem"))
|
||||
|
||||
@JvmField
|
||||
val POLISHED_TUMBLESTONE = this.create("polished_tumblestone", Block(BlockBehaviour.Properties.of().mapColor(MapColor.TERRACOTTA_ORANGE).strength(1.0F).sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).requiresCorrectToolForDrops().instrument(NoteBlockInstrument.BASEDRUM)))
|
||||
@JvmField
|
||||
@@ -456,6 +538,7 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val SOUL_CAMPFIRE = create("soul_campfire", CampfireBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.PODZOL).strength(2.0F).lightLevel{9}, true))
|
||||
|
||||
|
||||
@JvmField
|
||||
val RELIC_COIN_POUCH = create("relic_coin_pouch", CoinPouchBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.RELIC_COIN_POUCH_SOUNDS).pushReaction(PushReaction.DESTROY).noOcclusion(), true))
|
||||
@JvmField
|
||||
@@ -584,9 +667,25 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val PC = create("pc", PCBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if ((it.getValue(PCBlock.ON) as Boolean) && (it.getValue(PCBlock.PART) == PCBlock.PCPart.TOP)) 13 else 0 }))
|
||||
|
||||
val TM_MACHINE = create(
|
||||
"tm_machine",
|
||||
TMMachineBlock(
|
||||
BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.COLOR_GREEN)
|
||||
.sound(SoundType.METAL)
|
||||
.pushReaction(PushReaction.BLOCK)
|
||||
.strength(2F)
|
||||
.noOcclusion()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@JvmField
|
||||
val LECTERN = create("lectern", LecternBlock(BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).sound(SoundType.WOOD).strength(2.5F).ignitedByLava().lightLevel { if (it.getValue(LecternBlock.EMIT_LIGHT) as Boolean) 13 else 0 }))
|
||||
|
||||
@JvmField
|
||||
val TM_SHELF = create("tm_shelf", TMShelfBlock(BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).sound(SoundType.METAL).strength(2.5F)))
|
||||
|
||||
@JvmField
|
||||
val DISPLAY_CASE = create("display_case", DisplayCaseBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.DISPLAY_CASE_SOUNDS).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.STONE).strength(0.3F).requiresCorrectToolForDrops()))
|
||||
|
||||
@@ -890,4 +989,28 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
val block = BlocksInvoker.createLeavesBlock(SoundType.GRASS)
|
||||
return this.create(name, block)
|
||||
}
|
||||
|
||||
private fun typeGemBlock(name: String): TypeGemBlock {
|
||||
val typeGemBlock = this.create(name, TypeGemBlock(BlockBehaviour.Properties.of().strength(3.0f, 6.0f)))
|
||||
|
||||
val identifier = cobblemonResource(name)
|
||||
typeGemBlocks[identifier] = typeGemBlock
|
||||
|
||||
return typeGemBlock
|
||||
}
|
||||
|
||||
fun typeGemBlocks() = typeGemBlocks.toMap()
|
||||
|
||||
private fun typeGemCluster(name: String, gemBlock: Block, dropItemId: ResourceLocation): Block {
|
||||
val gemClusterBlock = this.create(name, TypeGemClusterBlock(BlockBehaviour.Properties.of().strength(2.0f, 3.0f).pushReaction(PushReaction.DESTROY), gemBlock, dropItemId))
|
||||
|
||||
TypeGemClusterBlock.gemToClusterMap[gemBlock] = gemClusterBlock
|
||||
|
||||
val identifier = cobblemonResource(name)
|
||||
typeGemClusters[identifier] = gemClusterBlock
|
||||
|
||||
return gemClusterBlock
|
||||
}
|
||||
|
||||
fun typeGemClusters() = typeGemClusters.toMap()
|
||||
}
|
||||
@@ -18,6 +18,7 @@ object CobblemonCommands {
|
||||
fun register(dispatcher: CommandDispatcher<CommandSourceStack>, registry: CommandBuildContext, selection: Commands.CommandSelection) {
|
||||
SpawnPokemon.register(dispatcher)
|
||||
SpawnNPCCommand.register(dispatcher)
|
||||
NPCBattleDebugCommand.register(dispatcher)
|
||||
GivePokemon.register(dispatcher)
|
||||
TakePokemon.register(dispatcher)
|
||||
ChangeEyeHeight.register(dispatcher)
|
||||
@@ -71,10 +72,12 @@ object CobblemonCommands {
|
||||
MarkGiveAllCommand.register(dispatcher)
|
||||
PcSearchCommand.register(dispatcher)
|
||||
PcTakeCommand.register(dispatcher)
|
||||
GiveTmCommand.register(dispatcher)
|
||||
TmCommand.register(dispatcher)
|
||||
|
||||
// Possibly lock down registration if and only if under dev environment or running in an environment
|
||||
// with a certain system environment variable set
|
||||
CobblemonInfoCommand.register(dispatcher)
|
||||
CalculateSeatPositionCommand.register(dispatcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.world.inventory.MenuType
|
||||
import net.minecraft.world.level.GameRules
|
||||
import net.minecraft.world.level.ItemLike
|
||||
import net.minecraft.world.level.biome.Biome
|
||||
|
||||
@@ -69,6 +69,12 @@ object CobblemonItemComponents : PlatformRegistry<Registry<DataComponentType<*>>
|
||||
.networkSynchronized(FoodColourComponent.PACKET_CODEC)
|
||||
.build())
|
||||
|
||||
@JvmField
|
||||
val TM_MOVE: DataComponentType<TMMoveComponent> = create("tm_move", DataComponentType.builder<TMMoveComponent>()
|
||||
.persistent(TMMoveComponent.CODEC)
|
||||
.networkSynchronized(TMMoveComponent.PACKET_CODEC)
|
||||
.build())
|
||||
|
||||
@JvmField
|
||||
val INGREDIENT: DataComponentType<IngredientComponent> = create("ingredient", DataComponentType.builder<IngredientComponent>()
|
||||
.persistent(IngredientComponent.CODEC)
|
||||
@@ -101,6 +107,7 @@ object CobblemonItemComponents : PlatformRegistry<Registry<DataComponentType<*>>
|
||||
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:flavour"), FLAVOUR)
|
||||
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:ride_boosts"), RIDE_BOOST)
|
||||
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:food_colour"), FOOD_COLOUR)
|
||||
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:tm_move"), TM_MOVE)
|
||||
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:ingredient"), INGREDIENT)
|
||||
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:food"), FOOD)
|
||||
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:mob_effects"), MOB_EFFECTS)
|
||||
|
||||
@@ -457,6 +457,10 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val PC = blockItem("pc", CobblemonBlocks.PC)
|
||||
@JvmField
|
||||
val TM_MACHINE = blockItem("tm_machine", CobblemonBlocks.TM_MACHINE)
|
||||
@JvmField
|
||||
val TM_SHELF = blockItem("tm_shelf", CobblemonBlocks.TM_SHELF)
|
||||
@JvmField
|
||||
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
|
||||
@JvmField
|
||||
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
|
||||
@@ -1208,6 +1212,83 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val BLACK_TUMBLESTONE_BLOCK = blockItem("black_tumblestone_block", CobblemonBlocks.BLACK_TUMBLESTONE_BLOCK)
|
||||
|
||||
@JvmField
|
||||
val TYPE_GEM_CORE = blockItem("type_gem_core", CobblemonBlocks.TYPE_GEM_CORE)
|
||||
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_NORMAL = blockItem("type_gem_cluster_normal", CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_FIRE = blockItem("type_gem_cluster_fire", CobblemonBlocks.TYPE_GEM_CLUSTER_FIRE)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_WATER = blockItem("type_gem_cluster_water", CobblemonBlocks.TYPE_GEM_CLUSTER_WATER)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_ELECTRIC = blockItem("type_gem_cluster_electric", CobblemonBlocks.TYPE_GEM_CLUSTER_ELECTRIC)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_GRASS = blockItem("type_gem_cluster_grass", CobblemonBlocks.TYPE_GEM_CLUSTER_GRASS)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_ICE = blockItem("type_gem_cluster_ice", CobblemonBlocks.TYPE_GEM_CLUSTER_ICE)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_FIGHTING = blockItem("type_gem_cluster_fighting", CobblemonBlocks.TYPE_GEM_CLUSTER_FIGHTING)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_POISON = blockItem("type_gem_cluster_poison", CobblemonBlocks.TYPE_GEM_CLUSTER_POISON)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_GROUND = blockItem("type_gem_cluster_ground", CobblemonBlocks.TYPE_GEM_CLUSTER_GROUND)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_FLYING = blockItem("type_gem_cluster_flying", CobblemonBlocks.TYPE_GEM_CLUSTER_FLYING)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_PSYCHIC = blockItem("type_gem_cluster_psychic", CobblemonBlocks.TYPE_GEM_CLUSTER_PSYCHIC)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_BUG = blockItem("type_gem_cluster_bug", CobblemonBlocks.TYPE_GEM_CLUSTER_BUG)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_ROCK = blockItem("type_gem_cluster_rock", CobblemonBlocks.TYPE_GEM_CLUSTER_ROCK)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_GHOST = blockItem("type_gem_cluster_ghost", CobblemonBlocks.TYPE_GEM_CLUSTER_GHOST)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_DRAGON = blockItem("type_gem_cluster_dragon", CobblemonBlocks.TYPE_GEM_CLUSTER_DRAGON)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_DARK = blockItem("type_gem_cluster_dark", CobblemonBlocks.TYPE_GEM_CLUSTER_DARK)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_STEEL = blockItem("type_gem_cluster_steel", CobblemonBlocks.TYPE_GEM_CLUSTER_STEEL)
|
||||
@JvmField
|
||||
val TYPE_GEM_CLUSTER_FAIRY = blockItem("type_gem_cluster_fairy", CobblemonBlocks.TYPE_GEM_CLUSTER_FAIRY)
|
||||
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_NORMAL = blockItem("type_gem_block_normal", CobblemonBlocks.TYPE_GEM_BLOCK_NORMAL)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_FIRE = blockItem("type_gem_block_fire", CobblemonBlocks.TYPE_GEM_BLOCK_FIRE)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_WATER = blockItem("type_gem_block_water", CobblemonBlocks.TYPE_GEM_BLOCK_WATER)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_ELECTRIC = blockItem("type_gem_block_electric", CobblemonBlocks.TYPE_GEM_BLOCK_ELECTRIC)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_GRASS = blockItem("type_gem_block_grass", CobblemonBlocks.TYPE_GEM_BLOCK_GRASS)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_ICE = blockItem("type_gem_block_ice", CobblemonBlocks.TYPE_GEM_BLOCK_ICE)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_FIGHTING = blockItem("type_gem_block_fighting", CobblemonBlocks.TYPE_GEM_BLOCK_FIGHTING)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_POISON = blockItem("type_gem_block_poison", CobblemonBlocks.TYPE_GEM_BLOCK_POISON)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_GROUND = blockItem("type_gem_block_ground", CobblemonBlocks.TYPE_GEM_BLOCK_GROUND)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_FLYING = blockItem("type_gem_block_flying", CobblemonBlocks.TYPE_GEM_BLOCK_FLYING)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_PSYCHIC = blockItem("type_gem_block_psychic", CobblemonBlocks.TYPE_GEM_BLOCK_PSYCHIC)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_BUG = blockItem("type_gem_block_bug", CobblemonBlocks.TYPE_GEM_BLOCK_BUG)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_ROCK = blockItem("type_gem_block_rock", CobblemonBlocks.TYPE_GEM_BLOCK_ROCK)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_GHOST = blockItem("type_gem_block_ghost", CobblemonBlocks.TYPE_GEM_BLOCK_GHOST)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_DRAGON = blockItem("type_gem_block_dragon", CobblemonBlocks.TYPE_GEM_BLOCK_DRAGON)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_DARK = blockItem("type_gem_block_dark", CobblemonBlocks.TYPE_GEM_BLOCK_DARK)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_STEEL = blockItem("type_gem_block_steel", CobblemonBlocks.TYPE_GEM_BLOCK_STEEL)
|
||||
@JvmField
|
||||
val TYPE_GEM_BLOCK_FAIRY = blockItem("type_gem_block_fairy", CobblemonBlocks.TYPE_GEM_BLOCK_FAIRY)
|
||||
|
||||
@JvmField
|
||||
val POKEROD_SMITHING_TEMPLATE = create("pokerod_smithing_template", SmithingTemplateItem(
|
||||
Component.translatable("item.minecraft.fishing_rod").blue(),
|
||||
@@ -1432,44 +1513,49 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
val RELIC_COIN_POUCH = blockItem("relic_coin_pouch", CobblemonBlocks.RELIC_COIN_POUCH)
|
||||
@JvmField
|
||||
val RELIC_COIN_SACK = blockItem("relic_coin_sack", CobblemonBlocks.RELIC_COIN_SACK)
|
||||
@JvmField
|
||||
val TECHNICAL_MACHINE = this.create("technical_machine", TechnicalMachineItem(Properties()))
|
||||
@JvmField
|
||||
val BLANK_TM = this.create("blank_tm", CobblemonItem(Item.Properties()))
|
||||
|
||||
// Type Gems
|
||||
@JvmField
|
||||
val NORMAL_GEM = noSettingsItem("normal_gem")
|
||||
val NORMAL_GEM = this.create("normal_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL, Item.Properties()))
|
||||
@JvmField
|
||||
val FIRE_GEM = noSettingsItem("fire_gem")
|
||||
val FIRE_GEM = this.create("fire_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_FIRE, Item.Properties()))
|
||||
@JvmField
|
||||
val WATER_GEM = noSettingsItem("water_gem")
|
||||
val WATER_GEM = this.create("water_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_WATER, Item.Properties()))
|
||||
@JvmField
|
||||
val GRASS_GEM = noSettingsItem("grass_gem")
|
||||
val GRASS_GEM = this.create("grass_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_GRASS, Item.Properties()))
|
||||
@JvmField
|
||||
val ELECTRIC_GEM = noSettingsItem("electric_gem")
|
||||
val ELECTRIC_GEM = this.create("electric_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_ELECTRIC, Item.Properties()))
|
||||
@JvmField
|
||||
val ICE_GEM = noSettingsItem("ice_gem")
|
||||
val ICE_GEM = this.create("ice_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_ICE, Item.Properties()))
|
||||
@JvmField
|
||||
val FIGHTING_GEM = noSettingsItem("fighting_gem")
|
||||
val FIGHTING_GEM = this.create("fighting_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_FIGHTING, Item.Properties()))
|
||||
@JvmField
|
||||
val POISON_GEM = noSettingsItem("poison_gem")
|
||||
val POISON_GEM = this.create("poison_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_POISON, Item.Properties()))
|
||||
@JvmField
|
||||
val GROUND_GEM = noSettingsItem("ground_gem")
|
||||
val GROUND_GEM = this.create("ground_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_GROUND, Item.Properties()))
|
||||
@JvmField
|
||||
val FLYING_GEM = noSettingsItem("flying_gem")
|
||||
val FLYING_GEM = this.create("flying_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_FLYING, Item.Properties()))
|
||||
@JvmField
|
||||
val PSYCHIC_GEM = noSettingsItem("psychic_gem")
|
||||
val PSYCHIC_GEM = this.create("psychic_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_PSYCHIC, Item.Properties()))
|
||||
@JvmField
|
||||
val BUG_GEM = noSettingsItem("bug_gem")
|
||||
val BUG_GEM = this.create("bug_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_BUG, Item.Properties()))
|
||||
@JvmField
|
||||
val ROCK_GEM = noSettingsItem("rock_gem")
|
||||
val ROCK_GEM = this.create("rock_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_ROCK, Item.Properties()))
|
||||
@JvmField
|
||||
val GHOST_GEM = noSettingsItem("ghost_gem")
|
||||
val GHOST_GEM = this.create("ghost_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_GHOST, Item.Properties()))
|
||||
@JvmField
|
||||
val DRAGON_GEM = noSettingsItem("dragon_gem")
|
||||
val DRAGON_GEM = this.create("dragon_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_DRAGON, Item.Properties()))
|
||||
@JvmField
|
||||
val DARK_GEM = noSettingsItem("dark_gem")
|
||||
val DARK_GEM = this.create("dark_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_DARK, Item.Properties()))
|
||||
@JvmField
|
||||
val STEEL_GEM = noSettingsItem("steel_gem")
|
||||
val STEEL_GEM = this.create("steel_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_STEEL, Item.Properties()))
|
||||
@JvmField
|
||||
val FAIRY_GEM = noSettingsItem("fairy_gem")
|
||||
val FAIRY_GEM = this.create("fairy_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_FAIRY, Item.Properties()))
|
||||
|
||||
//@JvmField
|
||||
//val BINDING_SOIL = blockItem("binding_soil", CobblemonBlocks.BINDING_SOIL)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common
|
||||
|
||||
|
||||
import com.cobblemon.mod.common.block.campfirepot.CookingPotMenu
|
||||
import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
|
||||
import com.cobblemon.mod.common.platform.PlatformRegistry
|
||||
import net.minecraft.core.Registry
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
@@ -19,7 +20,8 @@ import net.minecraft.world.inventory.MenuType
|
||||
|
||||
object CobblemonMenuType : PlatformRegistry<Registry<MenuType<*>>, ResourceKey<Registry<MenuType<*>>>, MenuType<*>>() {
|
||||
|
||||
val COOKING_POT = MenuType.register("cooking_pot", ::CookingPotMenu)
|
||||
val COOKING_POT = MenuType.register("cooking_pot_menu", ::CookingPotMenu)
|
||||
val TM_MACHINE = MenuType.register("tm_machine_menu", ::TMMachineMenu)
|
||||
|
||||
override val registry: Registry<MenuType<*>>
|
||||
get() = BuiltInRegistries.MENU
|
||||
|
||||
@@ -25,15 +25,15 @@ import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
|
||||
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
|
||||
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
|
||||
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
|
||||
import com.cobblemon.mod.common.client.net.effect.*
|
||||
import com.cobblemon.mod.common.client.net.gui.*
|
||||
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
|
||||
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
|
||||
import com.cobblemon.mod.common.client.net.effect.SaccharineLogBlockParticlesHandler
|
||||
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormEntityParticleHandler
|
||||
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.*
|
||||
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
|
||||
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
|
||||
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
|
||||
@@ -137,9 +137,7 @@ import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPac
|
||||
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.ui.ExpGainedDataPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.ui.*
|
||||
import com.cobblemon.mod.common.net.messages.server.*
|
||||
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
|
||||
@@ -150,6 +148,7 @@ import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.behaviour.DamageOnCollisionPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.behaviour.SetEntityBehaviourPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.block.TMMachineTeachMovePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectCancelledPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.callback.party.PartyPokemonSelectedPacket
|
||||
@@ -205,6 +204,7 @@ import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHan
|
||||
import com.cobblemon.mod.common.net.serverhandling.behaviour.DamageOnCollisionPacketHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.behaviour.SetEntityBehaviourHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.block.TMMachineTeachMoveHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectCancelledHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectedHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.callback.party.PartyPokemonSelectedHandler
|
||||
@@ -228,6 +228,8 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandl
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.*
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.*
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetItemHiddenHandler
|
||||
@@ -570,6 +572,7 @@ object CobblemonNetwork {
|
||||
|
||||
// Block entity
|
||||
list.add(PacketRegisterInfo(AdjustBlockEntityViewerCountPacket.ID, AdjustBlockEntityViewerCountPacket::decode, AdjustBlockEntityViewerCountHandler))
|
||||
list.add(PacketRegisterInfo(TMMachineTeachMovePacket.ID, TMMachineTeachMovePacket::decode, TMMachineTeachMoveHandler))
|
||||
|
||||
// Move select packets
|
||||
list.add(PacketRegisterInfo(MoveSelectedPacket.ID, MoveSelectedPacket::decode, MoveSelectedHandler))
|
||||
@@ -606,6 +609,10 @@ object CobblemonNetwork {
|
||||
// Cooking
|
||||
list.add(PacketRegisterInfo(ToggleCookingPotLidPacket.ID, ToggleCookingPotLidPacket::decode, ToggleCookingPotLidHandler))
|
||||
|
||||
// TM Machine
|
||||
list.add(PacketRegisterInfo(SetActiveTMPacket.ID, SetActiveTMPacket::decode, SetActiveTMPacketHandler))
|
||||
list.add(PacketRegisterInfo(SetTMMachineContainerDataPacket.ID, SetTMMachineContainerDataPacket::decode, SetTMMachineContainerDataPacketHandler))
|
||||
|
||||
// Behaviour Packets
|
||||
list.add(PacketRegisterInfo(SetEntityBehaviourPacket.ID, SetEntityBehaviourPacket::decode, SetEntityBehaviourHandler))
|
||||
list.add(PacketRegisterInfo(DamageOnCollisionPacket.ID, DamageOnCollisionPacket::decode, DamageOnCollisionPacketHandler))
|
||||
|
||||
@@ -200,6 +200,22 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
|
||||
@JvmField
|
||||
val TUMBLESTONE_STEP = this.create("block.tumblestone.step")
|
||||
|
||||
@JvmField
|
||||
val TM_USE = this.create("item.tm.use")
|
||||
@JvmField
|
||||
val TM_MACHINE_CRAFT = this.create("block.tm_machine.craft")
|
||||
@JvmField
|
||||
val TM_MACHINE_OFF = this.create("block.tm_machine.off")
|
||||
@JvmField
|
||||
val TM_MACHINE_ON = this.create("block.tm_machine.on")
|
||||
@JvmField
|
||||
val TM_MACHINE_OPEN = this.create("block.tm_machine.open")
|
||||
@JvmField
|
||||
val TM_MACHINE_CLOSE = this.create("block.tm_machine.close")
|
||||
@JvmField
|
||||
val TM_MACHINE_PLACE_DISC = this.create("block.tm_machine.place_disc")
|
||||
|
||||
|
||||
@JvmField
|
||||
val EVOLUTION_STONE_BLOCK_BREAK = this.create("block.evolution_stone_block.break")
|
||||
@JvmField
|
||||
|
||||
+3
-39
@@ -14,7 +14,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.advancements.critereon.ContextAwarePredicate
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import java.util.Optional
|
||||
import java.util.*
|
||||
|
||||
class PlantTumblestoneContext(val pos: BlockPos, val block: TumblestoneBlock)
|
||||
|
||||
@@ -29,42 +29,6 @@ class PlantTumblestoneCriterion(
|
||||
}
|
||||
|
||||
override fun matches(player: ServerPlayer, context: PlantTumblestoneContext): Boolean {
|
||||
return context.block.canGrow(context.pos, player.level())
|
||||
return context.block.canGrow(player.level().getBlockState(context.pos), context.pos, player.level())
|
||||
}
|
||||
}
|
||||
|
||||
///**
|
||||
// * A criterion condition for when a tumblestone block is planted
|
||||
// * @param id The identifier of the criterion
|
||||
// * @param predicate The predicate for the criterion
|
||||
// *
|
||||
// * @author Aethen
|
||||
// * @since 02/28/2024
|
||||
// */
|
||||
//class PlantTumblestoneCriterionCondition(id: Identifier, predicate: LootContextPredicate) :
|
||||
// SimpleCriterionCondition<PlantTumblestoneContext>(id, predicate) {
|
||||
// override fun toJson(json: JsonObject) {
|
||||
// // Add properties to json if needed. None needed for this criterion
|
||||
// }
|
||||
//
|
||||
// override fun fromJson(json: JsonObject) {
|
||||
// // Parse properties from json if needed. None needed for this criterion
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Checks if the tumblestone block can grow at the given position
|
||||
// * @param player The player that planted the tumblestone block
|
||||
// * @param context The context of the criterion
|
||||
// * @return True if the tumblestone block can grow at the given position, false otherwise
|
||||
// */
|
||||
// override fun matches(player: ServerPlayer, context: PlantTumblestoneContext): Boolean {
|
||||
// return context.tumbleStoneBlock.canGrow(context.pos, player.level())
|
||||
// }
|
||||
//}
|
||||
//
|
||||
///**
|
||||
// * The context of the [PlantTumblestoneCriterionCondition]
|
||||
// * @param pos The position of the tumblestone block
|
||||
// * @param tumbleStoneBlock The tumblestone block
|
||||
// */
|
||||
//open class PlantTumblestoneContext(var pos: BlockPos, var tumbleStoneBlock: TumblestoneBlock)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangV
|
||||
import com.cobblemon.mod.common.entity.OmniPathingEntity
|
||||
import com.cobblemon.mod.common.entity.ai.CobblemonRandomSurfacePos
|
||||
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.asExpression
|
||||
import com.cobblemon.mod.common.util.mainThreadRuntime
|
||||
import com.cobblemon.mod.common.util.resolveFloat
|
||||
@@ -184,6 +185,12 @@ class WanderTaskConfig : SingleTaskConfig {
|
||||
|
||||
if (targetVec == null) continue
|
||||
|
||||
// Ignore pathing targets outside of the pasture zone if the pokemon is pastured.
|
||||
val tethering = (entity as? PokemonEntity)?.tethering
|
||||
if (tethering?.canRoamTo(BlockPos.containing(targetVec)) == false) {
|
||||
continue
|
||||
}
|
||||
|
||||
pos = applyHeightConstraints(
|
||||
pos = BlockPos.containing(targetVec),
|
||||
minimumHeight = minimumHeight,
|
||||
|
||||
@@ -166,6 +166,10 @@ open class PokemonBattle(
|
||||
return otherSide.actors.all { it.type == ActorType.NPC }
|
||||
}
|
||||
|
||||
/** Whether this battle is NPC-vs-NPC. */
|
||||
val isNvN: Boolean
|
||||
get() = actors.any() && actors.all { it.type == ActorType.NPC }
|
||||
|
||||
/**
|
||||
* Gets an actor by their showdown id
|
||||
* @return the actor if found otherwise null
|
||||
@@ -505,6 +509,24 @@ open class PokemonBattle(
|
||||
|
||||
fun checkForInputDispatch() {
|
||||
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
|
||||
if (isNvN) {
|
||||
val actorsWithPendingChoices = actors.filter { actor ->
|
||||
val request = actor.request ?: return@filter false
|
||||
if (request.wait) return@filter false
|
||||
(request.active?.isNotEmpty() == true) || request.forceSwitch.any { it }
|
||||
}
|
||||
val readyToInputNvN = actorsWithPendingChoices.isNotEmpty() &&
|
||||
actorsWithPendingChoices.all { !it.mustChoose && it.responses.isNotEmpty() }
|
||||
if (readyToInputNvN && captureActions.isEmpty()) {
|
||||
actorsWithPendingChoices.forEach { it.writeShowdownResponse() }
|
||||
actorsWithPendingChoices.forEach {
|
||||
it.responses.clear()
|
||||
it.request = null
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
|
||||
if (readyToInput && captureActions.isEmpty()) {
|
||||
actors.filter { it.responses.isNotEmpty() }.forEach { it.writeShowdownResponse() }
|
||||
@@ -560,4 +582,4 @@ open class PokemonBattle(
|
||||
queryStruct.addFunction("has_rule") { params -> DoubleValue(params.getString(0) in format.ruleSet) }
|
||||
return queryStruct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+66
-13
@@ -10,15 +10,19 @@ package com.cobblemon.mod.common.api.battles.model.actor
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon.LOGGER
|
||||
import com.cobblemon.mod.common.api.battles.model.ai.BattleAI
|
||||
import com.cobblemon.mod.common.battles.DefaultActionResponse
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.battles.PassActionResponse
|
||||
import com.cobblemon.mod.common.battles.ShowdownActionRequest
|
||||
import com.cobblemon.mod.common.battles.ShowdownActionResponse
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.exception.IllegalActionChoiceException
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleFaintPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleMadeInvalidChoicePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattlePersistentStatusPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleReplacePokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleSwapPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleSwitchPokemonPacket
|
||||
@@ -30,11 +34,16 @@ abstract class AIBattleActor(
|
||||
pokemonList: List<BattlePokemon>,
|
||||
val battleAI: BattleAI
|
||||
) : BattleActor(gameId, pokemonList.toMutableList()) {
|
||||
private var lastChoiceRequest: ShowdownActionRequest? = null
|
||||
private var invalidChoiceRetries = 0
|
||||
|
||||
override fun sendUpdate(packet: NetworkPacket<*>) {
|
||||
super.sendUpdate(packet)
|
||||
|
||||
when (packet) {
|
||||
is BattleMakeChoicePacket -> this.onChoiceRequested()
|
||||
is BattleQueueRequestPacket -> this.onQueueRequest(packet.request)
|
||||
is BattleMadeInvalidChoicePacket -> this.onInvalidChoiceRequested()
|
||||
is BattlePersistentStatusPacket -> {}
|
||||
is BattleFaintPacket -> {}
|
||||
is BattleSwitchPokemonPacket -> if (!packet.isAlly) { /* TODO this might be redundant; switch on opposing site should be visible during turn */ }
|
||||
@@ -49,30 +58,31 @@ abstract class AIBattleActor(
|
||||
* Called when the AI is requested to make a choice.
|
||||
*/
|
||||
open fun onChoiceRequested() {
|
||||
refreshChoiceRetryState()
|
||||
try {
|
||||
request?.let {
|
||||
setActionResponses(it.iterate(this.activePokemon) { battleMon, moveset, forceSwitch ->
|
||||
val request = request
|
||||
if (request != null) {
|
||||
setActionResponses(request.iterate(this.activePokemon) { battleMon, moveset, forceSwitch ->
|
||||
battleAI.choose(battleMon, battle, getSide(), moveset, forceSwitch)
|
||||
})
|
||||
pokemonList.forEach { pokemon ->
|
||||
pokemon.willBeSwitchedIn = false
|
||||
}
|
||||
} ?: {
|
||||
val response = mutableListOf<ShowdownActionResponse>()
|
||||
repeat(activePokemon.size) {
|
||||
response.add(PassActionResponse)
|
||||
}
|
||||
setActionResponses(response)
|
||||
LOGGER.warn("AI requested choice, but no request was set. Returning PassActionResponses.")
|
||||
invalidChoiceRetries = 0
|
||||
} else {
|
||||
// A stale or out-of-order choice prompt should never block battle flow.
|
||||
mustChoose = false
|
||||
LOGGER.warn("AI requested choice, but no request was set. Ignoring stale choice prompt.")
|
||||
}
|
||||
} catch (exception: IllegalActionChoiceException) {
|
||||
LOGGER.error("AI was unable to choose an action, we're going to need to pass!")
|
||||
exception.printStackTrace()
|
||||
request?.let {
|
||||
setActionResponses(it.iterate(this.activePokemon) { _, _ , _->
|
||||
val request = request
|
||||
if (request != null) {
|
||||
setActionResponses(request.iterate(this.activePokemon) { _, _ , _ ->
|
||||
PassActionResponse
|
||||
})
|
||||
} ?: {
|
||||
} else {
|
||||
val response = mutableListOf<ShowdownActionResponse>()
|
||||
repeat(activePokemon.size) {
|
||||
response.add(PassActionResponse)
|
||||
@@ -81,4 +91,47 @@ abstract class AIBattleActor(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onInvalidChoiceRequested() {
|
||||
refreshChoiceRetryState()
|
||||
val request = request
|
||||
if (request == null) {
|
||||
mustChoose = false
|
||||
return
|
||||
}
|
||||
|
||||
invalidChoiceRetries++
|
||||
if (invalidChoiceRetries <= 2) {
|
||||
onChoiceRequested()
|
||||
return
|
||||
}
|
||||
|
||||
LOGGER.warn("AI produced repeated invalid choices. Falling back to default action.")
|
||||
setActionResponses(
|
||||
request.iterate(this.activePokemon) { _, _, _ ->
|
||||
DefaultActionResponse()
|
||||
}
|
||||
)
|
||||
invalidChoiceRetries = 0
|
||||
}
|
||||
|
||||
private fun onQueueRequest(request: ShowdownActionRequest) {
|
||||
val hasActionableChoice = !request.wait && ((request.active?.isNotEmpty() == true) || request.forceSwitch.any { it })
|
||||
if (!hasActionableChoice) {
|
||||
return
|
||||
}
|
||||
|
||||
// If turn prompt is missed due message ordering, AI can still proceed from the queued request.
|
||||
if (battle.turn > 0 || request.forceSwitch.any { it }) {
|
||||
mustChoose = true
|
||||
onChoiceRequested()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshChoiceRetryState() {
|
||||
if (request !== lastChoiceRequest) {
|
||||
lastChoiceRequest = request
|
||||
invalidChoiceRetries = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -76,18 +76,17 @@ abstract class BattleActor(
|
||||
fun turn() {
|
||||
val request = request ?: return
|
||||
responses.clear()
|
||||
if(activePokemon.any { it.isAlive() }) {
|
||||
mustChoose = true
|
||||
sendUpdate(BattleMakeChoicePacket())
|
||||
}
|
||||
|
||||
|
||||
val requestActive = request.active
|
||||
if (requestActive == null || requestActive.isEmpty() || request.wait) {
|
||||
this.request = null
|
||||
expectingPassActions.clear()
|
||||
return
|
||||
}
|
||||
|
||||
if (activePokemon.any { it.isAlive() }) {
|
||||
mustChoose = true
|
||||
sendUpdate(BattleMakeChoicePacket())
|
||||
}
|
||||
}
|
||||
|
||||
fun upkeep() {
|
||||
@@ -159,4 +158,4 @@ abstract class BattleActor(
|
||||
|
||||
open fun win(otherWinners: List<BattleActor>, losers: List<BattleActor>) {}
|
||||
open fun lose(winners: List<BattleActor>, otherLosers: List<BattleActor>) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ import net.minecraft.world.entity.item.ItemEntity
|
||||
import net.minecraft.world.entity.monster.Monster
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.enchantment.EnchantmentHelper
|
||||
import net.minecraft.world.level.ClipContext
|
||||
import net.minecraft.world.level.GameType
|
||||
import net.minecraft.world.level.Level
|
||||
@@ -871,6 +872,8 @@ object MoLangFunctions {
|
||||
|
||||
DoubleValue(value)
|
||||
}
|
||||
map.put("get_starter_uuid") { _ -> Cobblemon.playerDataManager.getGenericData(player).starterUUID?.let{ StringValue(it.toString()) } ?: DoubleValue.ZERO }
|
||||
map.put("has_chosen_starter") { _ -> DoubleValue(Cobblemon.playerDataManager.getGenericData(player).starterSelected) }
|
||||
}
|
||||
map
|
||||
}
|
||||
@@ -891,6 +894,15 @@ object MoLangFunctions {
|
||||
map.put("is_of") { params -> DoubleValue(holder.`is`(params.getString(0).asIdentifierDefaultingNamespace())) }
|
||||
map.put("is_in") { params -> DoubleValue(holder.`is`(TagKey.create(Registries.ITEM, params.getString(0).replace("#", "").asIdentifierDefaultingNamespace()))) }
|
||||
map.put("is_food") { params -> DoubleValue(stack.has(DataComponents.FOOD)) }
|
||||
map.put("is_enchanted") { params -> DoubleValue(stack.hasFoil()) }
|
||||
map.put("has_enchantment") { params ->
|
||||
val enchantmentId = params.getString(0).asIdentifierDefaultingNamespace()
|
||||
val minLevel = params.getIntOrNull(1) ?: 1
|
||||
val enchantmentRegistry = registryAccess.registryOrThrow(Registries.ENCHANTMENT)
|
||||
val holder = enchantmentRegistry.getHolder(enchantmentId).orElse(null) ?: return@put DoubleValue.ZERO
|
||||
val level = EnchantmentHelper.getItemEnchantmentLevel(holder, stack)
|
||||
return@put DoubleValue(level >= minLevel)
|
||||
}
|
||||
return@mutableListOf map
|
||||
}
|
||||
)
|
||||
@@ -1830,6 +1842,30 @@ object MoLangFunctions {
|
||||
map.put("remove_cosmetic_item") { _ ->
|
||||
pokemon.removeCosmeticItem()
|
||||
}
|
||||
map.put("marks") { _ -> pokemon.marks.asArrayValue { StringValue(it.identifier.toString()) } }
|
||||
map.put("has_mark") { params ->
|
||||
var hasMark = false
|
||||
val identifier = params.getString(0).asIdentifierDefaultingNamespace()
|
||||
val mark = Marks.getByIdentifier(identifier)
|
||||
if (mark != null) {
|
||||
hasMark = pokemon.marks.contains(mark)
|
||||
}
|
||||
|
||||
DoubleValue(hasMark)
|
||||
}
|
||||
map.put("remove_marks") { params ->
|
||||
var removedMark = false
|
||||
for (param in params.params) {
|
||||
val identifier = param.asString().asIdentifierDefaultingNamespace()
|
||||
val mark = Marks.getByIdentifier(identifier)
|
||||
if (mark != null) {
|
||||
pokemon.exchangeMark(mark, false)
|
||||
removedMark = true
|
||||
}
|
||||
}
|
||||
|
||||
DoubleValue(removedMark)
|
||||
}
|
||||
map.put("add_marks") { params ->
|
||||
var appliedMark = false
|
||||
for (param in params.params) {
|
||||
@@ -2814,8 +2850,8 @@ object MoLangFunctions {
|
||||
}
|
||||
variable
|
||||
}
|
||||
else -> null
|
||||
} ?: throw IllegalArgumentException("Invalid NBT element type: ${nbt.type}")
|
||||
else -> VariableStruct()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
class BenchedMoves : Iterable<BenchedMove> {
|
||||
var changeFunction: ((BenchedMoves) -> Unit) = {}
|
||||
// var moveAddedFunction: (BenchedMove) -> Unit = {}
|
||||
private var emit = true
|
||||
private val benchedMoves = mutableListOf<BenchedMove>()
|
||||
|
||||
@@ -58,6 +59,9 @@ class BenchedMoves : Iterable<BenchedMove> {
|
||||
return false
|
||||
}
|
||||
|
||||
/*if (emit) {
|
||||
moveAddedFunction(benchedMove)
|
||||
}*/
|
||||
doThenEmit { benchedMoves.add(benchedMove) }
|
||||
return true
|
||||
}
|
||||
@@ -174,4 +178,4 @@ data class BenchedMove(val moveTemplate: MoveTemplate, val ppRaisedStages: Int)
|
||||
Codec.intRange(0, 3).fieldOf(DataKeys.POKEMON_MOVESET_RAISED_PP_STAGES).forGetter(BenchedMove::ppRaisedStages)
|
||||
).apply(it, ::BenchedMove) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ class MoveSet : Iterable<Move> {
|
||||
*/
|
||||
fun getMoves() = moves.filterNotNull()
|
||||
fun getMovesWithNulls() = moves.toList()
|
||||
fun getMoveTemplates() = getMoves().map { move -> move.template }
|
||||
fun hasSpace() = moves.any { it == null }
|
||||
|
||||
/**
|
||||
@@ -222,4 +223,4 @@ class MoveSet : Iterable<Move> {
|
||||
{ moveSet -> moveSet.toList() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -67,6 +67,8 @@ object CobblemonPermissions {
|
||||
val SPAWN_POKEMON = this.create("${COMMAND_PREFIX}spawnpokemon", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
|
||||
@JvmStatic
|
||||
val SPAWN_NPC = this.create("${COMMAND_PREFIX}spawnnpc", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
|
||||
@JvmStatic
|
||||
val NPC_BATTLE_DEBUG = this.create("${COMMAND_PREFIX}npcbattledebug", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
|
||||
|
||||
|
||||
@JvmStatic
|
||||
@@ -143,6 +145,8 @@ object CobblemonPermissions {
|
||||
val CHANGE_BOX_COUNT = this.create("${COMMAND_PREFIX}boxcount", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
|
||||
@JvmStatic
|
||||
val SPECTATE_BATTLE = this.create("${COMMAND_PREFIX}spectatebattle", PermissionLevel.ALL_COMMANDS)
|
||||
@JvmStatic
|
||||
val TECHNICAL_MACHINE = this.create("${COMMAND_PREFIX}technicalmachine", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
|
||||
|
||||
@JvmStatic
|
||||
val SEE_HIDDEN_NPCS = this.create("seehiddennpcs", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
|
||||
@@ -159,4 +163,4 @@ object CobblemonPermissions {
|
||||
return permission
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ abstract class AbstractPokedexManager {
|
||||
|
||||
fun deleteSpeciesRecord(speciesId: ResourceLocation) {
|
||||
speciesRecords.remove(speciesId)
|
||||
markDirty()
|
||||
}
|
||||
|
||||
fun deleteFormRecord(speciesId: ResourceLocation, formName: String) {
|
||||
@@ -40,8 +39,6 @@ abstract class AbstractPokedexManager {
|
||||
deleteSpeciesRecord(speciesId)
|
||||
return
|
||||
}
|
||||
|
||||
markDirty()
|
||||
}
|
||||
|
||||
fun getSpeciesRecord(speciesId: ResourceLocation): SpeciesDexRecord? {
|
||||
@@ -164,8 +161,4 @@ abstract class AbstractPokedexManager {
|
||||
return newValue
|
||||
}
|
||||
}
|
||||
|
||||
open fun markDirty() {
|
||||
// Save stuff
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ package com.cobblemon.mod.common.api.pokedex
|
||||
|
||||
import com.bedrockk.molang.runtime.struct.QueryStruct
|
||||
import com.bedrockk.molang.runtime.struct.VariableStruct
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.bedrockk.molang.runtime.value.StringValue
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.events.CobblemonEvents
|
||||
import com.cobblemon.mod.common.api.events.pokemon.PokedexDataChangedEvent
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
@@ -25,6 +27,7 @@ import com.google.common.collect.Sets
|
||||
import com.mojang.datafixers.util.Either
|
||||
import com.mojang.serialization.Codec
|
||||
import com.mojang.serialization.codecs.ListCodec
|
||||
import com.mojang.serialization.codecs.PrimitiveCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
@@ -41,11 +44,13 @@ class FormDexRecord {
|
||||
instance.group(
|
||||
ListCodec(Codec.STRING, 0, 3).fieldOf("genders").forGetter { it.genders.map { it.name } },
|
||||
ListCodec(Codec.STRING, 0, 2).fieldOf("seenShinyStates").forGetter { it.seenShinyStates.toList() },
|
||||
PrimitiveCodec.INT.optionalFieldOf("highest_level", -1).forGetter { it.highestLevel },
|
||||
Codec.STRING.fieldOf("knowledge").forGetter { it.knowledge.name }
|
||||
).apply(instance) { genders, seenShinyStates, knowledge ->
|
||||
).apply(instance) { genders, seenShinyStates, highestLevel, knowledge ->
|
||||
FormDexRecord().also {
|
||||
it.genders.addAll(genders.map(Gender::valueOf))
|
||||
it.seenShinyStates.addAll(seenShinyStates)
|
||||
it.highestLevel = highestLevel
|
||||
it.knowledge = PokedexEntryProgress.valueOf(knowledge)
|
||||
}
|
||||
}
|
||||
@@ -59,6 +64,9 @@ class FormDexRecord {
|
||||
private val seenShinyStates =
|
||||
mutableSetOf<String>() // consider: radiants in the future (radiants should just be a resource pack tbh)
|
||||
|
||||
var highestLevel = -1
|
||||
private set
|
||||
|
||||
/** The current awareness of the form that the dex has. */
|
||||
var knowledge = PokedexEntryProgress.NONE
|
||||
private set
|
||||
@@ -80,11 +88,17 @@ class FormDexRecord {
|
||||
struct = QueryStruct(hashMapOf())
|
||||
.addFunction("data") { data }
|
||||
.addFunction("knowledge") { StringValue(knowledge.name) }
|
||||
.addFunction("has_seen_gender") { params -> genders.contains(Gender.valueOf(params.getString(0).uppercase())) }
|
||||
.addFunction("has_seen_gender") { params -> DoubleValue(genders.contains(Gender.valueOf(params.getString(0).uppercase()))) }
|
||||
.addFunction("highest_level") { _ -> DoubleValue(highestLevel) }
|
||||
.addFunction("set_highest_level") { params ->
|
||||
highestLevel = maxOf(highestLevel, params.getDouble(0).toInt())
|
||||
DoubleValue.ONE
|
||||
}
|
||||
}
|
||||
|
||||
fun clone() = FormDexRecord().also {
|
||||
it.genders.addAll(genders)
|
||||
it.highestLevel = highestLevel
|
||||
it.seenShinyStates.addAll(seenShinyStates)
|
||||
it.knowledge = knowledge
|
||||
}
|
||||
@@ -114,7 +128,7 @@ class FormDexRecord {
|
||||
fun addAllShinyStatesAndGenders() {
|
||||
val form = PokemonSpecies.getByIdentifier(speciesDexRecord.id)?.getFormByName(formName)
|
||||
genders.addAll(form?.possibleGenders ?: listOf(Gender.MALE, Gender.FEMALE))
|
||||
|
||||
highestLevel = Cobblemon.config.maxPokemonLevel
|
||||
seenShinyStates.addAll(listOf("shiny", "normal"))
|
||||
speciesDexRecord.onFormRecordUpdated(this)
|
||||
}
|
||||
@@ -135,6 +149,9 @@ class FormDexRecord {
|
||||
),
|
||||
ifSucceeded = {
|
||||
genders.add(pokedexEntityData.pokemon.gender)
|
||||
if (knowledge == PokedexEntryProgress.CAUGHT) {
|
||||
highestLevel = maxOf(highestLevel, pokedexEntityData.pokemon.level)
|
||||
}
|
||||
seenShinyStates.add(if (pokedexEntityData.pokemon.shiny) "shiny" else "normal")
|
||||
if (knowledge.ordinal > this.knowledge.ordinal) {
|
||||
this.knowledge = knowledge
|
||||
@@ -159,6 +176,7 @@ class FormDexRecord {
|
||||
|| (pokedexEntityData.pokemon.shiny && "shiny" !in seenShinyStates)
|
||||
|| (!pokedexEntityData.pokemon.shiny && "normal" !in seenShinyStates)
|
||||
|| knowledge.ordinal > this.knowledge.ordinal
|
||||
|| (highestLevel < pokedexEntityData.pokemon.level && knowledge == PokedexEntryProgress.CAUGHT)
|
||||
|| speciesDexRecord.wouldBeDifferent(pokedexEntityData)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
package com.cobblemon.mod.common.api.pokedex
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
|
||||
import com.cobblemon.mod.common.api.scheduling.ScheduledTask
|
||||
import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
|
||||
import com.cobblemon.mod.common.api.storage.player.InstancedPlayerData
|
||||
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
|
||||
@@ -48,10 +50,44 @@ class PokedexManager(
|
||||
fun obtain(pokemon: Pokemon) {
|
||||
val speciesId = pokemon.species.resourceIdentifier
|
||||
val formName = pokemon.form.name
|
||||
getOrCreateSpeciesRecord(speciesId).getOrCreateFormRecord(formName).caught(PokedexEntityData(pokemon = pokemon, disguise = null))
|
||||
getOrCreateSpeciesRecord(speciesId).getOrCreateFormRecord(formName).obtained(PokedexEntityData(pokemon = pokemon, disguise = null))
|
||||
}
|
||||
|
||||
override fun markDirty() {
|
||||
fun scheduleFullSyncFromStores(
|
||||
party: Iterable<Pokemon?>,
|
||||
pc: Iterable<Pokemon?>,
|
||||
batchSize: Int = 50
|
||||
) {
|
||||
val iterator = sequence {
|
||||
for (pokemon in party) {
|
||||
if (pokemon != null) yield(pokemon)
|
||||
}
|
||||
for (pokemon in pc) {
|
||||
if (pokemon != null) yield(pokemon)
|
||||
}
|
||||
}.iterator()
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
return
|
||||
}
|
||||
|
||||
ScheduledTask.Builder()
|
||||
.tracker(ServerTaskTracker)
|
||||
.interval(0F)
|
||||
.infiniteIterations()
|
||||
.execute { task ->
|
||||
var processed = 0
|
||||
while (processed < batchSize && iterator.hasNext()) {
|
||||
val pokemon = iterator.next()
|
||||
obtain(pokemon)
|
||||
processed++
|
||||
}
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
task.expire()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
|
||||
@@ -26,7 +26,8 @@ import net.minecraft.resources.ResourceLocation
|
||||
/**
|
||||
* Dex recorded information about a particular Pokémon species. This includes all forms and their information.
|
||||
* Across all forms there is a single list of known aspects which is generally used to track cosmetic variation
|
||||
* though technically things like gender and form-motivating aspects will get collected inadvertently.
|
||||
* though technically things like gender and form-motivating aspects will get collected inadvertently. The highest
|
||||
* level of the species that the player has owned is also tracked.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since August 23rd, 2024
|
||||
@@ -36,10 +37,12 @@ class SpeciesDexRecord {
|
||||
val CODEC: Codec<SpeciesDexRecord> = RecordCodecBuilder.create { instance ->
|
||||
instance.group(
|
||||
ListCodec(PrimitiveCodec.STRING, 0, 512).fieldOf("aspects").forGetter { it.aspects.toList() },
|
||||
PrimitiveCodec.INT.optionalFieldOf("highestLevel", -1).forGetter { it.highestLevel },
|
||||
Codec.unboundedMap(Codec.STRING, FormDexRecord.CODEC).fieldOf("formRecords").forGetter { it.formRecords }
|
||||
).apply(instance) { aspects, formRecords ->
|
||||
).apply(instance) { aspects, highestLevel, formRecords ->
|
||||
SpeciesDexRecord().also {
|
||||
it.aspects.addAll(aspects)
|
||||
it.highestLevel = highestLevel
|
||||
it.formRecords.putAll(formRecords)
|
||||
}
|
||||
}
|
||||
@@ -48,13 +51,16 @@ class SpeciesDexRecord {
|
||||
|
||||
@Transient
|
||||
lateinit var id: ResourceLocation
|
||||
/** The highest level of the species that the player has owned. */
|
||||
var highestLevel = -1
|
||||
private set
|
||||
private val aspects: MutableSet<String> = mutableSetOf()
|
||||
private val formRecords: MutableMap<String, FormDexRecord> = mutableMapOf()
|
||||
val isFormRecordsEmpty: Boolean
|
||||
get() = formRecords.isEmpty()
|
||||
|
||||
fun describe(): String {
|
||||
return "SpeciesDexRecord(aspects=$aspects, formRecords=$formRecords)"
|
||||
return "SpeciesDexRecord(aspects=$aspects, highestLevel=$highestLevel, formRecords=$formRecords)"
|
||||
}
|
||||
|
||||
@Transient
|
||||
@@ -77,7 +83,14 @@ class SpeciesDexRecord {
|
||||
.addFunction("is_caught") { params ->
|
||||
DoubleValue(formRecords.values.any { it.knowledge.name.equals("CAUGHT", ignoreCase = true) })
|
||||
}
|
||||
// more, also worth moving to something like MoLangFunctions.kt so it's easier to API in more values
|
||||
.addFunction("highest_level") { params ->
|
||||
DoubleValue(highestLevel)
|
||||
}
|
||||
.addFunction("set_highest_level") { params ->
|
||||
highestLevel = params.getDouble(0).toInt()
|
||||
DoubleValue.ONE
|
||||
}
|
||||
// more, also worth moving to something like MoLangFunctions.kt so it's easier to API in more values
|
||||
|
||||
@Transient
|
||||
lateinit var pokedexManager: AbstractPokedexManager
|
||||
@@ -94,10 +107,16 @@ class SpeciesDexRecord {
|
||||
|
||||
fun addInformation(pokemon: Pokemon, knowledge: PokedexEntryProgress) {
|
||||
aspects.addAll(pokemon.aspects)
|
||||
if (knowledge == PokedexEntryProgress.CAUGHT) {
|
||||
highestLevel = maxOf(highestLevel, pokemon.level)
|
||||
}
|
||||
}
|
||||
|
||||
fun addInformation(pokedexEntityData: PokedexEntityData, knowledge: PokedexEntryProgress) {
|
||||
aspects.addAll(pokedexEntityData.pokemon.aspects)
|
||||
if (knowledge == PokedexEntryProgress.CAUGHT) {
|
||||
highestLevel = maxOf(highestLevel, pokedexEntityData.pokemon.level)
|
||||
}
|
||||
}
|
||||
|
||||
fun addAspects(addedAspects: Set<String>) {
|
||||
@@ -128,6 +147,7 @@ class SpeciesDexRecord {
|
||||
|
||||
fun clone() = SpeciesDexRecord().also {
|
||||
it.aspects.addAll(aspects)
|
||||
it.highestLevel = highestLevel
|
||||
it.formRecords.putAll(formRecords.mapValues { it.value.clone() })
|
||||
}
|
||||
|
||||
@@ -139,6 +159,7 @@ class SpeciesDexRecord {
|
||||
|
||||
fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeCollection(aspects) { _, it -> buffer.writeString(it) }
|
||||
buffer.writeInt(highestLevel)
|
||||
buffer.writeInt(formRecords.size)
|
||||
for ((formName, formRecord) in formRecords) {
|
||||
buffer.writeString(formName)
|
||||
@@ -149,6 +170,7 @@ class SpeciesDexRecord {
|
||||
fun decode(buffer: RegistryFriendlyByteBuf) {
|
||||
aspects.clear()
|
||||
aspects.addAll(buffer.readCollection(Sets::newHashSetWithExpectedSize) { buffer.readString() })
|
||||
highestLevel = buffer.readInt()
|
||||
formRecords.clear()
|
||||
val numForms = buffer.readInt()
|
||||
for (i in 0 until numForms) {
|
||||
|
||||
@@ -11,5 +11,6 @@ package com.cobblemon.mod.common.api.pokedex.filter
|
||||
enum class SearchByType {
|
||||
SPECIES,
|
||||
ABILITIES,
|
||||
MOVES,
|
||||
DROPS;
|
||||
}
|
||||
+154
-1
@@ -9,14 +9,22 @@
|
||||
package com.cobblemon.mod.common.api.pokedex.filter
|
||||
|
||||
import com.cobblemon.mod.common.api.drop.ItemDropEntry
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.pokedex.AbstractPokedexManager
|
||||
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
|
||||
import com.cobblemon.mod.common.api.tms.TechnicalMachines
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.asTranslated
|
||||
import com.cobblemon.mod.common.util.itemRegistry
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
/**
|
||||
@@ -47,6 +55,23 @@ class SearchFilter(val pokedexManager: AbstractPokedexManager, val searchString:
|
||||
}
|
||||
return abilityList.any { it.contains(searchString.trim().lowercase()) }
|
||||
}
|
||||
SearchByType.MOVES -> {
|
||||
if (pokedexManager.getHighestKnowledgeFor(entry) !== PokedexEntryProgress.CAUGHT) return false
|
||||
val search = searchString.trim().lowercase()
|
||||
val learnedTMs = CobblemonClient.clientTMMoveData.learnedTMs
|
||||
val forms = pokedexManager.getCaughtForms(entry)
|
||||
val formData = if (forms.isEmpty()) {
|
||||
listOf(species.standardForm)
|
||||
} else {
|
||||
forms.map { form ->
|
||||
species.forms.find { it.name.equals(form.displayForm, ignoreCase = true) } ?: species.standardForm
|
||||
}
|
||||
}
|
||||
return formData.any { form ->
|
||||
val highestLevel = pokedexManager.getSpeciesRecord(form.species.resourceIdentifier)?.getFormRecord(form.name)?.highestLevel ?: -1
|
||||
hasDiscoveredMove(form, search, highestLevel, pokedexManager, learnedTMs)
|
||||
}
|
||||
}
|
||||
SearchByType.DROPS -> {
|
||||
if (pokedexManager.getHighestKnowledgeFor(entry) !== PokedexEntryProgress.CAUGHT) return false
|
||||
val dropsList = mutableListOf<String>()
|
||||
@@ -68,4 +93,132 @@ class SearchFilter(val pokedexManager: AbstractPokedexManager, val searchString:
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private fun hasDiscoveredMove(
|
||||
form: FormData,
|
||||
search: String,
|
||||
highestLevel: Int,
|
||||
pokedex: AbstractPokedexManager,
|
||||
learnedTMs: Set<ResourceLocation>
|
||||
): Boolean {
|
||||
val evolutionMoveLevels = buildEvolutionMoveLevelIndex(form)
|
||||
|
||||
fun matches(move: MoveTemplate): Boolean {
|
||||
return move.displayName.string.lowercase().contains(search)
|
||||
}
|
||||
|
||||
fun isLevelUpDiscovered(move: MoveTemplate, level: Int): Boolean {
|
||||
if (highestLevel >= level) return true
|
||||
|
||||
for ((evolutionSpeciesId, moveLevels) in evolutionMoveLevels) {
|
||||
val evolutionLevel = moveLevels[move.name] ?: continue
|
||||
val evolutionHighest = pokedex.getSpeciesRecord(evolutionSpeciesId)?.highestLevel ?: -1
|
||||
if (evolutionHighest >= evolutionLevel) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
form.moves.levelUpMoves.forEach { (level, moves) ->
|
||||
for (move in moves) {
|
||||
if (matches(move) && isLevelUpDiscovered(move, level)) return true
|
||||
}
|
||||
}
|
||||
|
||||
form.moves.tmMoves.forEach { move ->
|
||||
val tmId = TechnicalMachines.moveToTM[move]?.id
|
||||
val discovered = tmId == null || tmId in learnedTMs
|
||||
if (discovered && matches(move)) return true
|
||||
}
|
||||
|
||||
val alwaysDiscovered = sequenceOf(
|
||||
form.moves.tutorMoves,
|
||||
form.moves.eggMoves,
|
||||
form.moves.evolutionMoves,
|
||||
form.moves.formChangeMoves,
|
||||
form.moves.specialMoves,
|
||||
form.moves.legacyMoves
|
||||
)
|
||||
|
||||
for (moves in alwaysDiscovered) {
|
||||
for (move in moves) {
|
||||
if (matches(move)) return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun buildEvolutionMoveLevelIndex(form: FormData): Map<ResourceLocation, Map<String, Int>> {
|
||||
val evolutionForms = collectEvolutionForms(form)
|
||||
val levelsBySpecies = mutableMapOf<ResourceLocation, MutableMap<String, Int>>()
|
||||
|
||||
for (evolutionForm in evolutionForms) {
|
||||
val speciesId = evolutionForm.species.resourceIdentifier
|
||||
val moveLevels = levelsBySpecies.getOrPut(speciesId) { mutableMapOf() }
|
||||
buildMoveLevelIndex(evolutionForm).forEach { (moveName, level) ->
|
||||
val current = moveLevels[moveName]
|
||||
if (current == null || level < current) {
|
||||
moveLevels[moveName] = level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return levelsBySpecies
|
||||
}
|
||||
|
||||
private fun buildMoveLevelIndex(form: FormData): Map<String, Int> {
|
||||
val levels = mutableMapOf<String, Int>()
|
||||
form.moves.levelUpMoves.forEach { (level, moves) ->
|
||||
moves.forEach { move ->
|
||||
val current = levels[move.name]
|
||||
if (current == null || level < current) {
|
||||
levels[move.name] = level
|
||||
}
|
||||
}
|
||||
}
|
||||
return levels
|
||||
}
|
||||
|
||||
private fun collectEvolutionForms(rootForm: FormData): List<FormData> {
|
||||
val results = mutableListOf<FormData>()
|
||||
val visited = mutableSetOf<String>()
|
||||
|
||||
fun key(form: FormData): String {
|
||||
return "${form.species.resourceIdentifier}|${form.name.lowercase()}"
|
||||
}
|
||||
|
||||
fun nextEvolutions(current: FormData): Set<Evolution> {
|
||||
return if (current.evolutions.isNotEmpty()) {
|
||||
current.evolutions
|
||||
} else {
|
||||
current.species.evolutions
|
||||
}
|
||||
}
|
||||
|
||||
fun traverse(current: FormData) {
|
||||
for (evolution in nextEvolutions(current)) {
|
||||
val evolutionForm = resolveEvolutionForm(evolution) ?: continue
|
||||
val evolutionKey = key(evolutionForm)
|
||||
if (!visited.add(evolutionKey)) continue
|
||||
results.add(evolutionForm)
|
||||
traverse(evolutionForm)
|
||||
}
|
||||
}
|
||||
|
||||
traverse(rootForm)
|
||||
return results
|
||||
}
|
||||
|
||||
private fun resolveEvolutionForm(evolution: Evolution): FormData? {
|
||||
val speciesId = evolution.result.species?.asIdentifierDefaultingNamespace() ?: return null
|
||||
val species = PokemonSpecies.getByIdentifier(speciesId) ?: return null
|
||||
val formId = evolution.result.form
|
||||
return if (formId == null) {
|
||||
species.standardForm
|
||||
} else {
|
||||
species.forms.firstOrNull {
|
||||
it.formOnlyShowdownId().equals(formId, ignoreCase = true) || it.name.equals(formId, ignoreCase = true)
|
||||
} ?: species.standardForm
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.pokedex.filter
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.pokedex.AbstractPokedexManager
|
||||
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
|
||||
import com.cobblemon.mod.common.api.tms.TechnicalMachines
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
enum class SecondaryPokedexFilterType {
|
||||
ALL,
|
||||
CAUGHT,
|
||||
SEEN,
|
||||
UNDISCOVERED,
|
||||
LEVEL_UP_TM_UNDISCOVERED
|
||||
}
|
||||
|
||||
class SecondaryPokedexFilter(
|
||||
private val pokedexManager: AbstractPokedexManager,
|
||||
private val filterType: SecondaryPokedexFilterType
|
||||
) : EntryFilter() {
|
||||
|
||||
override fun test(entry: PokedexEntry): Boolean {
|
||||
return when (filterType) {
|
||||
SecondaryPokedexFilterType.ALL -> true
|
||||
SecondaryPokedexFilterType.CAUGHT -> pokedexManager.getHighestKnowledgeFor(entry) >= PokedexEntryProgress.CAUGHT
|
||||
SecondaryPokedexFilterType.SEEN -> pokedexManager.getHighestKnowledgeFor(entry) == PokedexEntryProgress.ENCOUNTERED
|
||||
SecondaryPokedexFilterType.UNDISCOVERED -> pokedexManager.getHighestKnowledgeFor(entry) == PokedexEntryProgress.NONE
|
||||
SecondaryPokedexFilterType.LEVEL_UP_TM_UNDISCOVERED -> hasUndiscoveredLevelUpTM(entry)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasUndiscoveredLevelUpTM(entry: PokedexEntry): Boolean {
|
||||
if (pokedexManager.getHighestKnowledgeFor(entry) != PokedexEntryProgress.CAUGHT) return false
|
||||
val species = PokemonSpecies.getByIdentifier(entry.speciesId) ?: return false
|
||||
val forms = pokedexManager.getCaughtForms(entry)
|
||||
val formData = if (forms.isEmpty()) {
|
||||
listOf(species.standardForm)
|
||||
} else {
|
||||
forms.map { form ->
|
||||
species.forms.find { it.name.equals(form.displayForm, ignoreCase = true) } ?: species.standardForm
|
||||
}
|
||||
}
|
||||
|
||||
return formData.any { form ->
|
||||
val highestLevel = pokedexManager.getSpeciesRecord(form.species.resourceIdentifier)?.getFormRecord(form.name)?.highestLevel ?: -1
|
||||
hasUndiscoveredLevelUpTM(form, highestLevel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasUndiscoveredLevelUpTM(
|
||||
form: FormData,
|
||||
highestLevel: Int
|
||||
): Boolean {
|
||||
val evolutionMoveLevels = buildEvolutionMoveLevelIndex(form)
|
||||
|
||||
fun isLevelUpDiscovered(move: MoveTemplate, level: Int): Boolean {
|
||||
if (highestLevel >= level) return true
|
||||
|
||||
for ((evolutionSpeciesId, moveLevels) in evolutionMoveLevels) {
|
||||
val evolutionLevel = moveLevels[move.name] ?: continue
|
||||
val evolutionHighest = pokedexManager.getSpeciesRecord(evolutionSpeciesId)?.highestLevel ?: -1
|
||||
if (evolutionHighest >= evolutionLevel) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
form.moves.levelUpMoves.forEach { (level, moves) ->
|
||||
for (move in moves) {
|
||||
if (TechnicalMachines.moveToTM[move] != null && !isLevelUpDiscovered(move, level)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun buildEvolutionMoveLevelIndex(form: FormData): Map<ResourceLocation, Map<String, Int>> {
|
||||
val evolutionForms = collectEvolutionForms(form)
|
||||
val levelsBySpecies = mutableMapOf<ResourceLocation, MutableMap<String, Int>>()
|
||||
|
||||
for (evolutionForm in evolutionForms) {
|
||||
val speciesId = evolutionForm.species.resourceIdentifier
|
||||
val moveLevels = levelsBySpecies.getOrPut(speciesId) { mutableMapOf() }
|
||||
buildMoveLevelIndex(evolutionForm).forEach { (moveName, level) ->
|
||||
val current = moveLevels[moveName]
|
||||
if (current == null || level < current) {
|
||||
moveLevels[moveName] = level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return levelsBySpecies
|
||||
}
|
||||
|
||||
private fun buildMoveLevelIndex(form: FormData): Map<String, Int> {
|
||||
val levels = mutableMapOf<String, Int>()
|
||||
form.moves.levelUpMoves.forEach { (level, moves) ->
|
||||
moves.forEach { move ->
|
||||
val current = levels[move.name]
|
||||
if (current == null || level < current) {
|
||||
levels[move.name] = level
|
||||
}
|
||||
}
|
||||
}
|
||||
return levels
|
||||
}
|
||||
|
||||
private fun collectEvolutionForms(rootForm: FormData): List<FormData> {
|
||||
val results = mutableListOf<FormData>()
|
||||
val visited = mutableSetOf<String>()
|
||||
|
||||
fun key(form: FormData): String {
|
||||
return "${form.species.resourceIdentifier}|${form.name.lowercase()}"
|
||||
}
|
||||
|
||||
fun nextEvolutions(current: FormData): Set<Evolution> {
|
||||
return if (current.evolutions.isNotEmpty()) {
|
||||
current.evolutions
|
||||
} else {
|
||||
current.species.evolutions
|
||||
}
|
||||
}
|
||||
|
||||
fun traverse(current: FormData) {
|
||||
for (evolution in nextEvolutions(current)) {
|
||||
val evolutionForm = resolveEvolutionForm(evolution) ?: continue
|
||||
val evolutionKey = key(evolutionForm)
|
||||
if (!visited.add(evolutionKey)) continue
|
||||
results.add(evolutionForm)
|
||||
traverse(evolutionForm)
|
||||
}
|
||||
}
|
||||
|
||||
traverse(rootForm)
|
||||
return results
|
||||
}
|
||||
|
||||
private fun resolveEvolutionForm(evolution: Evolution): FormData? {
|
||||
val speciesId = evolution.result.species?.asIdentifierDefaultingNamespace() ?: return null
|
||||
val species = PokemonSpecies.getByIdentifier(speciesId) ?: return null
|
||||
val formId = evolution.result.form
|
||||
return if (formId == null) {
|
||||
species.standardForm
|
||||
} else {
|
||||
species.forms.firstOrNull {
|
||||
it.formOnlyShowdownId().equals(formId, ignoreCase = true) || it.name.equals(formId, ignoreCase = true)
|
||||
} ?: species.standardForm
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,11 +316,11 @@ object PokemonSpecies : JsonDataRegistry<Species> {
|
||||
val result = mutableMapOf<String, String>()
|
||||
this.species.forEach {species ->
|
||||
val baseSpecies = ShowdownSpecies(species, null)
|
||||
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name, "")] = this.gson.toJson(baseSpecies)
|
||||
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name.lowercase(), "")] = this.gson.toJson(baseSpecies)
|
||||
species.forms.forEach { form ->
|
||||
if (form != species.standardForm) {
|
||||
val formSpecies = ShowdownSpecies(species, form)
|
||||
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name, "")] = this.gson.toJson(formSpecies)
|
||||
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name.lowercase(), "")] = this.gson.toJson(formSpecies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.pokemon.moves
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.data.ClientDataSynchronizer
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
@@ -19,9 +20,7 @@ import com.google.gson.JsonElement
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
open class Learnset : ClientDataSynchronizer<Learnset> {
|
||||
class Interpreter(
|
||||
val loadMove: (JsonElement, Learnset) -> Boolean
|
||||
) {
|
||||
class Interpreter(val loadMove: (JsonElement, Learnset) -> Boolean) {
|
||||
companion object {
|
||||
fun parseFromPrefixIntoList(prefix: String, list: (Learnset) -> MutableList<MoveTemplate>): Interpreter {
|
||||
return Interpreter { element, learnset ->
|
||||
@@ -73,8 +72,7 @@ open class Learnset : ClientDataSynchronizer<Learnset> {
|
||||
eggInterpreter,
|
||||
tutorInterpreter,
|
||||
legacyInterpreter,
|
||||
specialInterpreter,
|
||||
levelUpInterpreter,
|
||||
specialInterpreter, levelUpInterpreter,
|
||||
formChangeInterpreter
|
||||
)
|
||||
}
|
||||
@@ -99,6 +97,20 @@ open class Learnset : ClientDataSynchronizer<Learnset> {
|
||||
.flatMap { it.value }
|
||||
.toSet()
|
||||
|
||||
fun tmLearnableMoves(): MutableList<MoveTemplate> {
|
||||
val moves = mutableListOf<MoveTemplate>()
|
||||
|
||||
moves.addAll(tmMoves)
|
||||
moves.addAll(tutorMoves)
|
||||
moves.addAll(eggMoves)
|
||||
moves.addAll(getLevelUpMovesUpTo(Cobblemon.config.maxPokemonLevel))
|
||||
moves.addAll(evolutionMoves)
|
||||
moves.addAll(formChangeMoves)
|
||||
|
||||
return moves
|
||||
}
|
||||
|
||||
|
||||
fun getAllLegalMoves(): Set<MoveTemplate> {
|
||||
return levelUpMoves.values.flatten().toSet() +
|
||||
eggMoves +
|
||||
|
||||
+4
-1
@@ -119,7 +119,10 @@ class RidingController(
|
||||
private fun canTransitionToLand(): Boolean {
|
||||
if ((entity.isUnderWater)) return false
|
||||
if ((entity.isEyeInFluid(FluidTags.WATER) || entity.isEyeInFluid(FluidTags.LAVA))) return false
|
||||
return entity.onGround()
|
||||
// If the currently in the air but has no land style then don't transition and render the ride useless.
|
||||
// Hover is a good example for this.
|
||||
val shouldStayInAir = entity.pokemon.riding.behaviours?.get(RidingStyle.LAND) == null && context?.style == RidingStyle.AIR
|
||||
return entity.onGround() && !shouldStayInAir
|
||||
}
|
||||
|
||||
private fun canTransitionToLiquid(): Boolean {
|
||||
|
||||
+15
-2
@@ -213,10 +213,23 @@ class HoverBehaviour : RidingBehaviour<HoverSettings, HoverState> {
|
||||
val accel = topSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalHover.accelerationExpr!!) * 20.0)
|
||||
|
||||
var newVelocity = state.rideVelocity.get()
|
||||
val unrotateVel = vehicle.deltaMovement.yRot(vehicle.yRot)
|
||||
|
||||
val vehicleFrontVec = Vec3(0.0, 0.0, 1.0)
|
||||
val faceVel = Vec3(vehicle.deltaMovement.x, 0.0, vehicle.deltaMovement.z)
|
||||
|
||||
val d = vehicle.deltaMovement.horizontalDistance()
|
||||
val v2 = Vec3(faceVel.dot(Vec3(1.0, 0.0, 0.0)) * d, newVelocity.y, faceVel.dot(Vec3(0.0, 0.0, 1.0)) * d)
|
||||
|
||||
// Check for collisions and if found, update the speed to reflect it
|
||||
if (vehicle.verticalCollision || vehicle.horizontalCollision) {
|
||||
state.speed.set(vehicle.deltaMovement.length())
|
||||
if (vehicle.horizontalCollision) {
|
||||
val smackingIntoWallSpeed = max( vehicle.deltaMovement.horizontalDistance(), 0.2)
|
||||
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z).normalize().scale(smackingIntoWallSpeed).add(0.0, newVelocity.y, 0.0)
|
||||
}
|
||||
|
||||
|
||||
if (vehicle.verticalCollision) {
|
||||
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z)
|
||||
}
|
||||
|
||||
// align the velocity vector to be in local vehicle space
|
||||
|
||||
+26
-83
@@ -60,7 +60,7 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
|
||||
val poseProvider = PoseProvider<SubmarineSettings, SubmarineState>(PoseType.FLOAT)
|
||||
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && !state.onSurface.get()})
|
||||
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z <= 0.05 && state.onSurface.get()})
|
||||
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
|
||||
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
|
||||
|
||||
override fun isActive(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
|
||||
return vehicle.isInWater || !vehicle.onGround()
|
||||
@@ -191,11 +191,17 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
|
||||
* Calculate Forward Velocity
|
||||
*********************************************/
|
||||
val currZVel = state.rideVelocity.get().z
|
||||
val newZVel = when {
|
||||
var newZVel = when {
|
||||
driver.zza > 0.0 -> min(topSpeed, currZVel + accel)
|
||||
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel) //TODO: Might need to be applied in addition to the normal braking force?
|
||||
else -> lerp(currZVel, 0.0, 0.05)
|
||||
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel)
|
||||
else -> lerp(currZVel, 0.0, 0.02)
|
||||
}
|
||||
|
||||
// If trying to brake then add the friction to the braking force
|
||||
if (driver.zza != 0.0f && driver.zza.sign.toDouble() != newZVel.sign) {
|
||||
newZVel = lerp(newZVel, 0.0, 0.02)
|
||||
}
|
||||
|
||||
currVel = Vec3(currVel.x, currVel.y, newZVel)
|
||||
|
||||
/*********************************************
|
||||
@@ -206,14 +212,20 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
|
||||
val gravity = (9.8 / ( 20.0)) * 0.2 * 0.4
|
||||
val terminalYVel = 2.0
|
||||
val driverYInput = if(driver.jumping) 1.0 else if (driver.isShiftKeyDown) -1.0 else 0.0
|
||||
val newYVel = when {
|
||||
var newYVel = when {
|
||||
(!vehicle.isInWater) -> max(-terminalYVel, currYVel - gravity)
|
||||
//(vehicle.isInWater && !vehicle.isUnderWater && currYVel > 0.001) -> lerp(currYVel, 0.0, 0.1) // Make sure that yVel is above a threshold before doing this otherwise it'll block submerging.
|
||||
//driverYInput != 0.0 && currYVel > 0.001 && driverYInput.sign != currYVel.sign -> lerp(currYVel, 0.0, 0.05)
|
||||
driverYInput > 0.0 -> min(vertTopSpeed, currYVel + strafeAccel)
|
||||
driverYInput < 0.0 -> max(-vertTopSpeed, currYVel - strafeAccel)
|
||||
else -> lerp(currYVel, 0.0, 0.05)
|
||||
else -> lerp(currYVel, 0.0, 0.02)
|
||||
}
|
||||
|
||||
// If trying to brake then add the friction to the braking force
|
||||
if (driverYInput != 0.0 && driverYInput != newYVel.sign) {
|
||||
newYVel = lerp(newYVel, 0.0, 0.02)
|
||||
}
|
||||
|
||||
currVel = Vec3(currVel.x, newYVel, currVel.z)
|
||||
|
||||
/*********************************************
|
||||
@@ -221,11 +233,17 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
|
||||
*********************************************/
|
||||
val currXVel = state.rideVelocity.get().x
|
||||
val horzTopSpeed = strafeSpeed
|
||||
val newXVel = when {
|
||||
var newXVel = when {
|
||||
driver.xxa > 0.0 -> min(horzTopSpeed, currXVel + strafeAccel)
|
||||
driver.xxa < 0.0 -> max( -horzTopSpeed, currXVel - strafeAccel)
|
||||
else -> lerp(currXVel, 0.0, 0.01)
|
||||
else -> lerp(currXVel, 0.0, 0.02)
|
||||
}
|
||||
|
||||
// If trying to brake then add the friction to the braking force
|
||||
if (driver.xxa != 0.0f && driver.xxa.sign.toDouble() != newZVel.sign) {
|
||||
newXVel = lerp(newXVel, 0.0, 0.02)
|
||||
}
|
||||
|
||||
currVel = Vec3(newXVel, currVel.y, currVel.z)
|
||||
|
||||
// Set the new velocity
|
||||
@@ -305,81 +323,6 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
|
||||
|
||||
}
|
||||
|
||||
private fun correctOrientation(
|
||||
settings: SubmarineSettings,
|
||||
state: SubmarineState,
|
||||
vehicle: PokemonEntity,
|
||||
controller: OrientationController,
|
||||
driver: Player,
|
||||
deltaTime: Double
|
||||
) {
|
||||
|
||||
// TODO: remove if not needed
|
||||
|
||||
// // Calculate correcting roll force.
|
||||
// // If the ride is moving then have the correction rate be an equal proportion of:
|
||||
// // - how rolled are you
|
||||
// // - how long has it been since last mouse input (higher is stronger correction)
|
||||
// // Also the correction is proportional as a whole to how NOT pitched are you so you don't spin endlessly when looking up or down
|
||||
// val howRolledAmI = sqrt(abs((cos(controller.roll.toRadians()) - 1.0f) * 0.5f)) // sqrt so it trends towards one pretty quickly
|
||||
// val maxRollCorrectionRate =
|
||||
// if (state.rideVelocity.get().length() < 0.2) 10.0f * abs(cos(controller.pitch.toRadians()))
|
||||
// else 10.0f * cos(controller.pitch.toRadians()).pow(2) * (
|
||||
// howRolledAmI*1.0f +
|
||||
// (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
|
||||
// )
|
||||
// val maxRollForce = maxRollCorrectionRate / 40.0f
|
||||
// val rollArrivalDeg = 60.0f
|
||||
// val desiredRoll = 0.0f
|
||||
//
|
||||
// // Calculate the signed error (how far and in what direction)
|
||||
// val rollError = Mth.wrapDegrees(desiredRoll.angleDifference(controller.roll))
|
||||
//
|
||||
// // Use the absolute error to calculate the magnitude of the influence
|
||||
// val influenceMagnitude = (min(rollArrivalDeg, abs(rollError)) / rollArrivalDeg)
|
||||
// val desiredRollForce = maxRollCorrectionRate * influenceMagnitude * -rollError.sign
|
||||
// var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
|
||||
//
|
||||
// // Ensure the 'steering' correction force doesn't exceed our maxForce
|
||||
// if (abs(steeredRollForce) > maxRollForce) {
|
||||
// steeredRollForce = steeredRollForce.sign * maxRollForce
|
||||
// }
|
||||
//
|
||||
// // Apply the 'steered' force (its really not a steering force I need a better name)
|
||||
// state.currRollCorrectionForce.set(
|
||||
// state.currRollCorrectionForce.get() + steeredRollForce
|
||||
// )
|
||||
|
||||
//Calculate correcting pitch force.
|
||||
// val rollCorrectionTimer = vehicle.runtime.resolveDouble(settings.timeToRollCorrect ?: globalSubmarine.timeToRollCorrect!!)
|
||||
// val maxPitchCorrectionRate = 5.0f * (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
|
||||
// val maxPitchForce = maxPitchCorrectionRate / 40.0f
|
||||
// val pitchArrivalDeg = 45.0f
|
||||
// val desiredPitch = 0.0f
|
||||
//
|
||||
// if(state.rideVelocity.get().length() < 0.1 && state.noInputTime.get() > rollCorrectionTimer) {
|
||||
// val pitchErrror = Mth.wrapDegrees(desiredPitch.angleDifference(controller.pitch))
|
||||
// val arrivalInfluence = (min(pitchArrivalDeg,abs(pitchErrror)) / pitchArrivalDeg)
|
||||
// val desiredPitchForce = maxPitchCorrectionRate * arrivalInfluence * -pitchErrror.sign
|
||||
// var steeredPitchForce = desiredPitchForce - state.currPitchCorrectionForce.get()
|
||||
//
|
||||
// // Ensure the 'steering' correction force doesn't exceed our maxForce
|
||||
// if (abs(steeredPitchForce) > maxPitchForce) {
|
||||
// steeredPitchForce = steeredPitchForce.sign * maxPitchForce
|
||||
// }
|
||||
//
|
||||
// // Apply the 'steered' force (its really not a steering force I need a better name)
|
||||
// state.currPitchCorrectionForce.set(
|
||||
// state.currPitchCorrectionForce.get() + steeredPitchForce
|
||||
// )
|
||||
// } else {
|
||||
// state.currPitchCorrectionForce.set(
|
||||
// //reduce rollCorrection gradually
|
||||
// lerp(state.currPitchCorrectionForce.get(), 0.0, deltaTime * 0.98)
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
override fun rotationOnMouseXY(
|
||||
settings: SubmarineSettings,
|
||||
state: SubmarineState,
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.cobblemon.mod.common.api.spawning.preset.BestSpawnerConfig
|
||||
import com.cobblemon.mod.common.api.spawning.preset.PokemonSpawnDetailPreset
|
||||
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.FishingSpawnerFactory
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
|
||||
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
|
||||
@@ -139,19 +140,6 @@ object BestSpawner {
|
||||
|
||||
fun onServerStarted(server: MinecraftServer) {
|
||||
CobblemonSpawnPools.onServerLoad(server)
|
||||
fishingSpawner = BasicSpawner(
|
||||
name = "fishing",
|
||||
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
|
||||
).also {
|
||||
it.influences.add(
|
||||
BucketMultiplyingInfluence(
|
||||
multipliers = mapOf(
|
||||
"uncommon" to 2.25f,
|
||||
"rare" to 5.5f,
|
||||
"ultra-rare" to 5.5f,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
fishingSpawner = FishingSpawnerFactory.createSharedSpawner()
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
|
||||
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
|
||||
import com.cobblemon.mod.common.util.mutableLazy
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import com.cobblemon.mod.common.api.spawning.BestSpawner;
|
||||
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
|
||||
|
||||
|
||||
/**
|
||||
* This isn't really a factory, one FishingSpawner is created as a field on [BestSpawner] on
|
||||
* server start. However, this emulates the systems that PlayerSpawner uses so extenders can
|
||||
* add influences that affect all fishing encounters.
|
||||
*
|
||||
* Responsible for creating [BasicSpawner]s for Fishing with whatever appropriate settings.
|
||||
* You can swap over the spawn pool and the influences here, and it will apply to all newly-generated
|
||||
* [BasicSpawner]s for Fishing. Has a global every-cast influence list, as well as a context-sensitive
|
||||
* influence list.
|
||||
*
|
||||
* @author Andy
|
||||
* @since January 23rd, 2026
|
||||
*/
|
||||
object FishingSpawnerFactory {
|
||||
var spawnPool: SpawnPool by mutableLazy {
|
||||
CobblemonSpawnPools.WORLD_SPAWN_POOL
|
||||
}
|
||||
|
||||
data class Context(
|
||||
val player: ServerPlayer,
|
||||
val rodStack: ItemStack,
|
||||
val lureLevel: Int,
|
||||
val luckOfTheSeaLevel: Int,
|
||||
val world: ServerLevel,
|
||||
val pos: BlockPos,
|
||||
val spawner: BasicSpawner
|
||||
)
|
||||
|
||||
/**
|
||||
* List of influences for a new cast of the fishing rod. Context is provided at
|
||||
* build time in [buildPositionInfluences]
|
||||
*/
|
||||
var positionInfluenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
|
||||
{ ctx -> PlayerLevelRangeInfluence(ctx.player, TYPICAL_VARIATION) },
|
||||
{ ctx -> BucketNormalizingInfluence(tier = ctx.lureLevel + ctx.luckOfTheSeaLevel) }
|
||||
)
|
||||
|
||||
fun createSharedSpawner(): BasicSpawner {
|
||||
return BasicSpawner(
|
||||
name = "fishing",
|
||||
spawnPool = spawnPool
|
||||
).also { spawner ->
|
||||
spawner.influences += BucketMultiplyingInfluence(
|
||||
multipliers = mapOf(
|
||||
"uncommon" to 2.25f,
|
||||
"rare" to 5.5f,
|
||||
"ultra-rare" to 5.5f,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the per-cast influences for [FishingSpawnablePosition]
|
||||
*/
|
||||
fun buildPositionInfluences(ctx: Context): MutableList<SpawningInfluence> {
|
||||
return positionInfluenceBuilders.mapNotNullTo(mutableListOf()) { it(ctx) }
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.spawner
|
||||
|
||||
import com.cobblemon.mod.common.api.fishing.SpawnBait
|
||||
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
|
||||
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
|
||||
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
|
||||
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
|
||||
import com.cobblemon.mod.common.api.spawning.rules.SpawnRule
|
||||
import com.cobblemon.mod.common.util.mutableLazy
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
|
||||
/**
|
||||
* Responsible for creating [FixedAreaSpawner]s for PokeSnacks with whatever appropriate settings.
|
||||
* You can swap over the spawn pool and the influences here, and it will apply to all newly-generated
|
||||
* [FixedAreaSpawner]s for PokeSnacks.
|
||||
*
|
||||
* @author Andy
|
||||
* @since January 23rd, 2026
|
||||
*/
|
||||
object PokeSnackSpawnerFactory {
|
||||
var spawnPool: SpawnPool by mutableLazy {
|
||||
CobblemonSpawnPools.WORLD_SPAWN_POOL
|
||||
}
|
||||
|
||||
data class Context(
|
||||
val world: ServerLevel,
|
||||
val pos: BlockPos,
|
||||
val horizontalRadius: Int,
|
||||
val verticalRadius: Int,
|
||||
val maxPokemonPerChunk: Float,
|
||||
val name: String,
|
||||
val baitEffects: List<SpawnBait.Effect>
|
||||
)
|
||||
|
||||
/**
|
||||
* The list of influences for a new [FixedAreaSpawner] for a PokeSnack.
|
||||
* The influences will be added to the spawner in the order they are added to this list.
|
||||
*/
|
||||
var influenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
|
||||
{ ctx ->
|
||||
val stackedLureTier = ctx.baitEffects
|
||||
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
|
||||
.sumOf { it.value }
|
||||
.toInt()
|
||||
|
||||
if (stackedLureTier <= 0) null else BucketNormalizingInfluence(
|
||||
tier = stackedLureTier,
|
||||
gradient = 0.2F,
|
||||
firstTier = 1.2F
|
||||
)
|
||||
},
|
||||
{ _ ->
|
||||
BucketMultiplyingInfluence(
|
||||
mapOf(
|
||||
"uncommon" to 2.25f,
|
||||
"rare" to 5.5f,
|
||||
"ultra-rare" to 5.5f,
|
||||
)
|
||||
)
|
||||
},
|
||||
{ ctx ->
|
||||
SpawnBaitInfluence(effects = ctx.baitEffects)
|
||||
},
|
||||
)
|
||||
|
||||
fun create(ctx: Context): FixedAreaSpawner {
|
||||
val influences = influenceBuilders.mapNotNull { it(ctx) }
|
||||
|
||||
return FixedAreaSpawner(
|
||||
name = ctx.name,
|
||||
spawnPool = spawnPool,
|
||||
world = ctx.world,
|
||||
position = ctx.pos,
|
||||
horizontalRadius = ctx.horizontalRadius,
|
||||
verticalRadius = ctx.verticalRadius,
|
||||
maxPokemonPerChunk = ctx.maxPokemonPerChunk
|
||||
).also { spawner ->
|
||||
spawner.influences.addAll(influences)
|
||||
|
||||
// TODO(Andy): PokeSnacks currently bypass pokemon spawn rules, should that be true?
|
||||
// Adding this rule will change that
|
||||
spawner.influences.addAll(
|
||||
CobblemonSpawnRules.rules.values
|
||||
.filter(SpawnRule::enabled)
|
||||
.flatMap(SpawnRule::components)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ abstract class PokemonStore<T : StorePosition> : Iterable<Pokemon> {
|
||||
remove(pokemon)
|
||||
val position = getFirstAvailablePosition() ?: return false // Couldn't fit, shrug emoji
|
||||
set(position, pokemon)
|
||||
Cobblemon.playerDataManager.getTMData(uuid)?.syncTMsFromPokemon(pokemon)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -170,4 +171,4 @@ abstract class PokemonStore<T : StorePosition> : Iterable<Pokemon> {
|
||||
* each change, accessed by [Pokemon.getChangeObservable].
|
||||
*/
|
||||
abstract fun getAnyChangeObservable(): Observable<Unit>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ open class PCStore(
|
||||
val toast = Toast(
|
||||
title = lang("wallpaper_unlocked"),
|
||||
description = unlockableWallpaper.displayName?.let { "\"".text().add(it.asTranslated()).add("\"") } ?: lang("unknown_wallpaper") ,
|
||||
icon = ItemStack(CobblemonItems.PC)
|
||||
icons = listOf(ItemStack(CobblemonItems.PC))
|
||||
)
|
||||
toast.addListeners(player)
|
||||
toast.expireAfter(5F)
|
||||
|
||||
+29
-7
@@ -8,14 +8,17 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.storage.player
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.Cobblemon.MODID
|
||||
import com.cobblemon.mod.common.api.pokedex.PokedexManager
|
||||
import com.cobblemon.mod.common.api.scheduling.ScheduledTask
|
||||
import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientInstancedPlayerData
|
||||
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
|
||||
import com.cobblemon.mod.common.api.tms.TMMoveManager
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.entity.player.Player
|
||||
@@ -30,7 +33,6 @@ import net.minecraft.world.entity.player.Player
|
||||
* @author Apion
|
||||
* @since February 21, 2024
|
||||
*/
|
||||
|
||||
open class PlayerInstancedDataStoreManager {
|
||||
val saveExecutor = Executors.newSingleThreadExecutor(
|
||||
ThreadFactoryBuilder()
|
||||
@@ -53,19 +55,27 @@ open class PlayerInstancedDataStoreManager {
|
||||
//Should put this somewhere else
|
||||
saveTasks[PlayerInstancedDataStoreTypes.GENERAL] = ScheduledTask.Builder()
|
||||
.execute { saveAllOfOneType(PlayerInstancedDataStoreTypes.GENERAL) }
|
||||
.delay(30f)
|
||||
.interval(120f)
|
||||
.delay(30F)
|
||||
.interval(120F)
|
||||
.infiniteIterations()
|
||||
.tracker(ServerTaskTracker)
|
||||
.build()
|
||||
|
||||
saveTasks[PlayerInstancedDataStoreTypes.POKEDEX] = ScheduledTask.Builder()
|
||||
.execute { saveAllOfOneType(PlayerInstancedDataStoreTypes.POKEDEX) }
|
||||
.delay(30f)
|
||||
.interval(120f)
|
||||
.delay(30F)
|
||||
.interval(120F)
|
||||
.infiniteIterations()
|
||||
.tracker(ServerTaskTracker)
|
||||
.build()
|
||||
|
||||
saveTasks[PlayerInstancedDataStoreTypes.TM_MOVES] = ScheduledTask.Builder()
|
||||
.execute { saveAllOfOneType(PlayerInstancedDataStoreTypes.TM_MOVES) }
|
||||
.delay(30F)
|
||||
.interval(120F)
|
||||
.infiniteIterations()
|
||||
.tracker(ServerTaskTracker)
|
||||
.build()
|
||||
}
|
||||
|
||||
open fun get(playerId: UUID, dataType: PlayerInstancedDataStoreType): InstancedPlayerData {
|
||||
@@ -126,4 +136,16 @@ open class PlayerInstancedDataStoreManager {
|
||||
open fun getPokedexData(playerId: UUID): PokedexManager {
|
||||
return get(playerId, PlayerInstancedDataStoreTypes.POKEDEX) as PokedexManager
|
||||
}
|
||||
}
|
||||
|
||||
fun getTMData(player: ServerPlayer): TMMoveManager {
|
||||
return getTMData(player.uuid)
|
||||
}
|
||||
|
||||
fun getTMData(playerId: UUID): TMMoveManager {
|
||||
val data = get(playerId, PlayerInstancedDataStoreTypes.TM_MOVES)
|
||||
if (data !is TMMoveManager) {
|
||||
Cobblemon.LOGGER.warn("getTMData: No TMMoveManager found for $playerId (got ${data?.javaClass?.name})")
|
||||
}
|
||||
return data as TMMoveManager
|
||||
}
|
||||
}
|
||||
|
||||
+16
-3
@@ -8,8 +8,10 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.storage.player
|
||||
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientGeneralPlayerData
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientGeneralPlayerData
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
@@ -27,11 +29,22 @@ object PlayerInstancedDataStoreTypes {
|
||||
ClientPokedexManager::runAction,
|
||||
ClientPokedexManager::runIncremental
|
||||
))
|
||||
val TM_MOVES = register(PlayerInstancedDataStoreType(
|
||||
cobblemonResource("tm_moves"),
|
||||
ClientTMMoveManager::decode,
|
||||
ClientTMMoveManager::runAction,
|
||||
ClientTMMoveManager::runIncremental
|
||||
))
|
||||
|
||||
fun register(type: PlayerInstancedDataStoreType): PlayerInstancedDataStoreType {
|
||||
Cobblemon.LOGGER.debug("Registering PlayerInstancedDataStoreType: {}", type.id)
|
||||
types[type.id] = type
|
||||
return type
|
||||
}
|
||||
|
||||
fun getTypeById(id: ResourceLocation) = types[id]
|
||||
}
|
||||
fun getTypeById(id: ResourceLocation): PlayerInstancedDataStoreType? {
|
||||
val type = types[id]
|
||||
if (type == null) Cobblemon.LOGGER.warn("Unknown PlayerInstancedDataStoreType ID: {}", id)
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.storage.player.adapter
|
||||
|
||||
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
|
||||
import com.cobblemon.mod.common.api.tms.TMMoveManager
|
||||
import com.cobblemon.mod.common.util.adapters.CodecBackedAdapter
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.util.UUID
|
||||
|
||||
class TMMoveJsonBackend : JsonBackedPlayerDataStoreBackend<TMMoveManager>("tm_moves", PlayerInstancedDataStoreTypes.TM_MOVES) {
|
||||
override val gson = GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.disableHtmlEscaping()
|
||||
.registerTypeAdapter(TMMoveManager::class.java, CodecBackedAdapter(TMMoveManager.CODEC))
|
||||
.create()
|
||||
|
||||
override val classToken = TypeToken.get(TMMoveManager::class.java)
|
||||
override val defaultData = defaultDataFunc
|
||||
|
||||
override fun initialize(store: TMMoveManager) {
|
||||
store.initialize()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val defaultDataFunc = { uuid: UUID -> TMMoveManager(uuid) }
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.storage.player.adapter
|
||||
|
||||
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
|
||||
import com.cobblemon.mod.common.api.tms.TMMoveManager
|
||||
import com.cobblemon.mod.common.util.adapters.CodecBackedAdapter
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.mongodb.client.MongoClient
|
||||
import java.util.UUID
|
||||
|
||||
class TMMoveMongoBackend(
|
||||
mongoClient: MongoClient,
|
||||
databaseName: String,
|
||||
collectionName: String
|
||||
) : MongoBackedPlayerDataStoreBackend<TMMoveManager>(
|
||||
mongoClient,
|
||||
databaseName,
|
||||
collectionName,
|
||||
PlayerInstancedDataStoreTypes.TM_MOVES
|
||||
) {
|
||||
override val gson = GsonBuilder()
|
||||
.registerTypeAdapter(TMMoveManager::class.java, CodecBackedAdapter(TMMoveManager.CODEC))
|
||||
.create()
|
||||
override val classToken = TypeToken.get(TMMoveManager::class.java)
|
||||
override val defaultData = defaultDataFunc
|
||||
|
||||
override fun initialize(store: TMMoveManager) {
|
||||
store.initialize()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val defaultDataFunc = { uuid: UUID -> TMMoveManager(uuid) }
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.storage.player.adapter
|
||||
|
||||
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
|
||||
import com.cobblemon.mod.common.api.tms.TMMoveManager
|
||||
import java.util.UUID
|
||||
|
||||
class TMMoveNbtBackend : NbtBackedPlayerData<TMMoveManager>("tm_moves", PlayerInstancedDataStoreTypes.TM_MOVES) {
|
||||
override val codec = TMMoveManager.CODEC
|
||||
override val defaultData = TMMoveJsonBackend.defaultDataFunc
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.storage.player.client
|
||||
|
||||
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.net.messages.client.SetClientPlayerDataPacket
|
||||
import com.cobblemon.mod.common.util.readString
|
||||
import com.cobblemon.mod.common.util.writeString
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
class ClientTMMoveManager(
|
||||
val learnedTMs: MutableSet<ResourceLocation>
|
||||
) : ClientInstancedPlayerData {
|
||||
|
||||
override fun encode(buf: RegistryFriendlyByteBuf) {
|
||||
buf.writeCollection(learnedTMs) { _, id -> buf.writeString(id.toString()) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(buf: RegistryFriendlyByteBuf): SetClientPlayerDataPacket {
|
||||
val tms = buf.readCollection(
|
||||
{ mutableSetOf<ResourceLocation>() },
|
||||
{ buf.readString().let(ResourceLocation::tryParse) ?: ResourceLocation("minecraft", "empty") }
|
||||
)
|
||||
return SetClientPlayerDataPacket(
|
||||
PlayerInstancedDataStoreTypes.TM_MOVES,
|
||||
ClientTMMoveManager(tms)
|
||||
)
|
||||
}
|
||||
|
||||
fun runAction(data: ClientInstancedPlayerData) {
|
||||
if (data !is ClientTMMoveManager) return
|
||||
CobblemonClient.clientTMMoveData = data
|
||||
}
|
||||
|
||||
fun runIncremental(data: ClientInstancedPlayerData) {
|
||||
if (data !is ClientTMMoveManager) return
|
||||
CobblemonClient.clientTMMoveData.learnedTMs.addAll(data.learnedTMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -40,8 +40,6 @@ class CachedPlayerDataStoreFactory<T : InstancedPlayerData>(val backend: PlayerD
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun saveAll() {
|
||||
cache.forEach { (_, pd) -> backend.save(pd) }
|
||||
cache.removeIf { (uuid, _) -> uuid.getPlayer() == null }
|
||||
|
||||
@@ -91,6 +91,7 @@ object CobblemonBlockTags {
|
||||
@JvmField val TUMBLESTONE_BRICKS = createTag("tumblestone_bricks")
|
||||
@JvmField val TUMBLESTONES = createTag("tumblestones")
|
||||
@JvmField val TUMBLESTONE_HEAT_SOURCE = createTag("tumblestone_heat_source")
|
||||
@JvmField val TYPE_GEM_BLOCKS = createTag("type_gem_blocks")
|
||||
@JvmField val WALL_HANGING_SIGNS = createTag("wall_hanging_signs")
|
||||
@JvmField val WALL_SIGNS = createTag("wall_signs")
|
||||
@JvmField val WATER_STONE_ORES = createTag("water_stone_ores")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.tms
|
||||
|
||||
import com.cobblemon.mod.common.tms.obtain.DefaultObtainMethod
|
||||
import com.cobblemon.mod.common.tms.obtain.PlayerHasAdvancementObtainMethod
|
||||
import com.cobblemon.mod.common.tms.obtain.PlayerYObtainMethod
|
||||
import com.cobblemon.mod.common.tms.obtain.UnlockableObtainMethod
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
interface ObtainMethod {
|
||||
val passive: Boolean
|
||||
|
||||
/**
|
||||
* Checks if this [ObtainMethod] matches the given player.
|
||||
*/
|
||||
fun matches(player: ServerPlayer): Boolean
|
||||
|
||||
/**
|
||||
* Serializes this [ObtainMethod] to the buffer.
|
||||
*/
|
||||
fun writeToBuffer(buffer: RegistryFriendlyByteBuf)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Deserializes an [ObtainMethod] from the buffer.
|
||||
*/
|
||||
fun readFromBuffer(buffer: RegistryFriendlyByteBuf): ObtainMethod {
|
||||
val variant = buffer.readUtf()
|
||||
return when (variant) {
|
||||
"cobblemon:unlockable" -> UnlockableObtainMethod()
|
||||
"cobblemon:default" -> DefaultObtainMethod()
|
||||
"cobblemon:advancement" -> {
|
||||
val advancement = buffer.readResourceLocation()
|
||||
PlayerHasAdvancementObtainMethod(advancement)
|
||||
}
|
||||
"cobblemon:y_level" -> {
|
||||
val yLevel = buffer.readVarInt()
|
||||
val operator = buffer.readUtf()
|
||||
PlayerYObtainMethod(yLevel, operator)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown ObtainMethod variant: $variant")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.tms
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.scheduling.ScheduledTask
|
||||
import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
|
||||
import com.cobblemon.mod.common.api.storage.player.InstancedPlayerData
|
||||
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
|
||||
import com.cobblemon.mod.common.net.messages.client.SetClientPlayerDataPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.util.getPlayer
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.mojang.serialization.Codec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import java.util.UUID
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
class TMMoveManager(
|
||||
override val uuid: UUID,
|
||||
val learnedTMs: MutableSet<ResourceLocation> = mutableSetOf()
|
||||
) : InstancedPlayerData {
|
||||
|
||||
val storeType = PlayerInstancedDataStoreTypes.TM_MOVES
|
||||
|
||||
override fun toClientData() = ClientTMMoveManager(learnedTMs.toMutableSet())
|
||||
fun toClientDataFrom(set: Set<ResourceLocation>) = ClientTMMoveManager(set.toMutableSet())
|
||||
|
||||
fun syncTMsFromPokemon(pokemon: Pokemon) {
|
||||
learn(getLearnableTMsFromPokemon(pokemon))
|
||||
}
|
||||
|
||||
fun syncTMFromMove(moveTemplate: MoveTemplate) {
|
||||
val tmId = TechnicalMachines.moveToTM[moveTemplate]?.id ?: return
|
||||
learn(listOf(tmId))
|
||||
}
|
||||
|
||||
fun getLearnableTMsFromPokemon(pokemon: Pokemon): Collection<ResourceLocation> {
|
||||
return TechnicalMachines.tmMap.values
|
||||
.filter { tm -> tm.moveName in pokemon.allAccessibleMoves }
|
||||
.map(TechnicalMachine::id)
|
||||
}
|
||||
|
||||
fun scheduleFullSyncFromStores(
|
||||
party: Iterable<Pokemon?>,
|
||||
pc: Iterable<Pokemon?>,
|
||||
batchSize: Int = 25
|
||||
) {
|
||||
val iterator = sequence {
|
||||
for (pokemon in party) {
|
||||
if (pokemon != null) yield(pokemon)
|
||||
}
|
||||
for (pokemon in pc) {
|
||||
if (pokemon != null) yield(pokemon)
|
||||
}
|
||||
}.iterator()
|
||||
|
||||
if (!iterator.hasNext()) return
|
||||
|
||||
val tmIds = mutableSetOf<ResourceLocation>()
|
||||
|
||||
ScheduledTask.Builder()
|
||||
.tracker(ServerTaskTracker)
|
||||
.interval(0F)
|
||||
.infiniteIterations()
|
||||
.execute { task ->
|
||||
var processed = 0
|
||||
while (processed < batchSize && iterator.hasNext()) {
|
||||
val pokemon = iterator.next()
|
||||
for (move in pokemon.allAccessibleMoves) {
|
||||
TechnicalMachines.moveToTM[move]?.id?.let { tmIds.add(it) }
|
||||
}
|
||||
processed++
|
||||
}
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
learn(tmIds)
|
||||
task.expire()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
fun learn(tmIds: Collection<ResourceLocation>): Boolean {
|
||||
val newLearnedTms = mutableListOf<ResourceLocation>()
|
||||
for (tmId in tmIds) {
|
||||
if (learnedTMs.add(tmId)) {
|
||||
newLearnedTms.add(tmId)
|
||||
}
|
||||
}
|
||||
|
||||
if (newLearnedTms.isEmpty()) return false
|
||||
|
||||
syncClient(newLearnedTms.toSet())
|
||||
sendTMToast(newLearnedTms)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun unlearn(tmIds: Collection<ResourceLocation>): Boolean {
|
||||
val removedTms = mutableListOf<ResourceLocation>()
|
||||
|
||||
for (tmId in tmIds) {
|
||||
if (learnedTMs.remove(tmId)) {
|
||||
removedTms.add(tmId)
|
||||
}
|
||||
}
|
||||
|
||||
if (removedTms.isEmpty()) return false
|
||||
|
||||
syncClient(learnedTMs, isIncremental = false)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun sendTMToast(tmIds: List<ResourceLocation>) {
|
||||
val player = uuid.getPlayer() ?: return
|
||||
var moveName: Component = Component.empty()
|
||||
val icons = tmIds.mapNotNull {
|
||||
val tm = TechnicalMachines.tmMap[it] ?: return@mapNotNull null
|
||||
moveName = tm.translatedMoveName()
|
||||
|
||||
return@mapNotNull tm.createItemStack()
|
||||
}
|
||||
|
||||
val description = if (icons.size == 1) moveName else lang("tms.check_tmm")
|
||||
|
||||
val packet = ToastPacket(
|
||||
title = lang("tms.new_tms_learned"),
|
||||
description = description,
|
||||
icons = icons,
|
||||
frameTexture = ResourceLocation.parse("minecraft:toast/advancement"),
|
||||
progress = -1F,
|
||||
progressColor = 0x00FF00,
|
||||
// Keeping it a fixed UUID makes it so the ToastTracker merges different toasts on the client
|
||||
uuid = UUID.nameUUIDFromBytes("tm_toast".toByteArray()),
|
||||
behaviour = ToastPacket.Behaviour.SHOW_OR_UPDATE,
|
||||
durationMs = 4000L
|
||||
)
|
||||
|
||||
player.sendPacket(packet)
|
||||
}
|
||||
|
||||
fun syncClient(updateSet: Set<ResourceLocation> = learnedTMs, isIncremental: Boolean = true) {
|
||||
uuid.getPlayer()?.sendPacket(
|
||||
SetClientPlayerDataPacket(
|
||||
type = storeType,
|
||||
playerData = toClientDataFrom(updateSet),
|
||||
isIncremental = isIncremental
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CODEC: Codec<TMMoveManager> = RecordCodecBuilder.create { instance ->
|
||||
instance.group(
|
||||
Codec.STRING.xmap(UUID::fromString, UUID::toString)
|
||||
.fieldOf("uuid")
|
||||
.forGetter { it.uuid },
|
||||
Codec.list(ResourceLocation.CODEC)
|
||||
.xmap({ it.toMutableSet() }, { it.toList() })
|
||||
.fieldOf("learnedTMs")
|
||||
.forGetter { it.learnedTMs }
|
||||
).apply(instance, ::TMMoveManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.tms
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.types.ElementalType
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.item.components.TMMoveComponent
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.tms.obtain.DefaultObtainMethod
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
class TechnicalMachine(
|
||||
val moveName: MoveTemplate,
|
||||
val recipe: List<TechnicalMachineRecipe>?,
|
||||
val obtainMethods: List<ObtainMethod> = emptyList(),
|
||||
val type: String
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Filters all available [TechnicalMachines] based on three optional arguments
|
||||
*
|
||||
* @param search A [String] to check against move names
|
||||
* @param type An [ElementalType] to check for
|
||||
* @param pokemon A [Pokemon] to check the [Learnset] of
|
||||
* @return A [MutableSet] of [TechnicalMachine] that passed the filter
|
||||
*/
|
||||
fun filterTms(
|
||||
search: String?,
|
||||
type: ElementalType?,
|
||||
pokemon: Pokemon?,
|
||||
player: Player? = null,
|
||||
includeUnlearned: Boolean = false
|
||||
): MutableSet<TechnicalMachine> {
|
||||
val tms = TechnicalMachines.tmMap.values.toMutableSet()
|
||||
|
||||
type?.let {
|
||||
val iterator = tms.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val tm = iterator.next()
|
||||
if (ElementalTypes.get(tm.type) != type) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pokemon?.let {
|
||||
val iterator = tms.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val tm = iterator.next()
|
||||
if (tm.moveName !in pokemon.form.moves.tmLearnableMoves()) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
search?.let {
|
||||
val iterator = tms.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val tm = iterator.next()
|
||||
if (!tm.translatedMoveName().string.contains(search, ignoreCase = true)) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (player != null && !includeUnlearned) {
|
||||
val learnedTMs = CobblemonClient.clientTMMoveData.learnedTMs
|
||||
tms.retainAll { tm -> tm.isPassivelyObtained() || tm.id in learnedTMs }
|
||||
}
|
||||
|
||||
return tms
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var id: ResourceLocation
|
||||
|
||||
/**
|
||||
* Gets the recipe list limited to the provided amount to match the slot amount of the TM Machine.
|
||||
*
|
||||
* @param limit The amount of recipe item slots to take.
|
||||
* @return The recipe list limited to 3 ingredients.
|
||||
*/
|
||||
fun getClampedRecipe(limit: Int = 3): List<TechnicalMachineRecipe>? = recipe?.take(limit)
|
||||
|
||||
/**
|
||||
* Unlocks this [TechnicalMachine] for the player.
|
||||
*
|
||||
* @param player The [ServerPlayer] to give this [TechnicalMachine] to.
|
||||
* @return Whether the player was successfully granted the [TechnicalMachine]
|
||||
*/
|
||||
fun unlock(player: ServerPlayer): Boolean {
|
||||
if (!obtainMethods.any { it is DefaultObtainMethod }) {
|
||||
player.sendSystemMessage(lang("tms.unlock_tm", moveName.displayName))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun isPassivelyObtained() = obtainMethods.any { it is DefaultObtainMethod }
|
||||
|
||||
/**
|
||||
* Returns a [MutableComponent] of the translated move name of this [TechnicalMachine]
|
||||
*
|
||||
* @return This [TechnicalMachine]'s move name, translated
|
||||
*/
|
||||
fun translatedMoveName(): MutableComponent = moveName.displayName
|
||||
|
||||
fun createItemStack(): ItemStack {
|
||||
val stack = ItemStack(CobblemonItems.TECHNICAL_MACHINE)
|
||||
TMMoveComponent.setTMMove(stack, moveName)
|
||||
return stack
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.tms
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
|
||||
/**
|
||||
* DTO used for [TechnicalMachine] JSON loading because Gson cannot deserialize Minecraft Ingredients.
|
||||
*/
|
||||
data class TechnicalMachineDTO(
|
||||
val moveName: MoveTemplate,
|
||||
val recipe: List<TechnicalMachineRecipeDTO>?,
|
||||
val obtainMethods: List<ObtainMethod> = emptyList(),
|
||||
val type: String
|
||||
) {
|
||||
fun toTechnicalMachine(): TechnicalMachine {
|
||||
return TechnicalMachine(
|
||||
moveName = moveName,
|
||||
recipe = recipe?.map { it.toTechnicalMachineRecipe() },
|
||||
obtainMethods = obtainMethods,
|
||||
type = type
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.tms
|
||||
|
||||
import net.minecraft.world.item.crafting.Ingredient
|
||||
|
||||
/**
|
||||
* Represents the ingredients necessary to craft a [TechnicalMachine] in the [TMBlock]
|
||||
*/
|
||||
data class TechnicalMachineRecipe(
|
||||
val ingredient: Ingredient,
|
||||
val count: Int
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.tms
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.world.item.crafting.Ingredient
|
||||
|
||||
/**
|
||||
* DTO used for [TechnicalMachineRecipe] JSON loading because Gson cannot deserialize Minecraft Ingredients.
|
||||
*/
|
||||
data class TechnicalMachineRecipeDTO(
|
||||
val item: ResourceLocation?,
|
||||
val tag: ResourceLocation?,
|
||||
@SerializedName("count") private val _count: Int?,
|
||||
) {
|
||||
private val count: Int
|
||||
get() = _count ?: 1
|
||||
|
||||
fun toTechnicalMachineRecipe(): TechnicalMachineRecipe {
|
||||
val ingredient = when {
|
||||
item != null ->
|
||||
Ingredient.of(
|
||||
BuiltInRegistries.ITEM.get(item)
|
||||
)
|
||||
|
||||
tag != null ->
|
||||
Ingredient.of(
|
||||
TagKey.create(Registries.ITEM, tag)
|
||||
)
|
||||
|
||||
else ->
|
||||
error("Recipe entry must define either 'item' or 'tag'")
|
||||
}
|
||||
|
||||
return TechnicalMachineRecipe(
|
||||
ingredient = ingredient,
|
||||
count = count
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.tms
|
||||
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.util.adapters.TMObtainMethodAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
import java.util.concurrent.ExecutionException
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
|
||||
object TechnicalMachines : JsonDataRegistry<TechnicalMachine> {
|
||||
override val gson = GsonBuilder()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.registerTypeAdapter(ObtainMethod::class.java, TMObtainMethodAdapter)
|
||||
.registerTypeAdapter(MoveTemplate::class.java, MoveTemplateAdapter)
|
||||
.create()
|
||||
|
||||
override val typeToken = TypeToken.get(TechnicalMachine::class.java)
|
||||
override val resourcePath = "tms"
|
||||
override val id = cobblemonResource("technical_machines")
|
||||
override val type = PackType.SERVER_DATA
|
||||
override val observable = SimpleObservable<TechnicalMachines>()
|
||||
|
||||
val tmMap = mutableMapOf<ResourceLocation, TechnicalMachine>()
|
||||
val moveToTM = mutableMapOf<MoveTemplate, TechnicalMachine>()
|
||||
|
||||
/**
|
||||
* Overridden to deserialize a DTO first and convert it to a runtime
|
||||
* [TechnicalMachine], since Gson cannot deserialize Minecraft Ingredients
|
||||
* directly.
|
||||
*/
|
||||
override fun parse(
|
||||
reader: BufferedReader,
|
||||
identifier: ResourceLocation
|
||||
): TechnicalMachine {
|
||||
try {
|
||||
val technicalMachineDto = gson.fromJson(reader, TechnicalMachineDTO::class.java)
|
||||
val technicalMachine = technicalMachineDto.toTechnicalMachine()
|
||||
return technicalMachine
|
||||
} catch (exception: Exception) {
|
||||
throw ExecutionException(
|
||||
"Error loading Technical Machine JSON: $identifier",
|
||||
exception
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, TechnicalMachine>) {
|
||||
data.forEach { (id, tm) ->
|
||||
tmMap[id] = tm
|
||||
tm.id = id
|
||||
moveToTM[tm.moveName] = tm
|
||||
}
|
||||
}
|
||||
|
||||
fun getByResourceLocation(resourceLocation: ResourceLocation) = tmMap[resourceLocation]
|
||||
fun getAllResourceLocations() = tmMap.keys.toSet()
|
||||
override fun sync(player: ServerPlayer) {}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import net.minecraft.world.item.ItemStack
|
||||
*
|
||||
* @param title The [Component] representing the toast title, this can be a [Component.empty].
|
||||
* @param description The [Component] representing the toast description, this can be a [Component.empty].
|
||||
* @param icon The [ItemStack] that is rendered as the toast icon, this can be a [ItemStack.EMPTY].
|
||||
* @param icons The [ItemStack] that is rendered as the toast icon, this can be a [ItemStack.EMPTY].
|
||||
* @param frameTexture The texture of the frame, default is the [VANILLA_BACKGROUND_SPRITE].
|
||||
* @param progress The value of the progress bar, this accepts a value between 0.0F and 1.0F, any other value will not render a progress bar.
|
||||
* @param progressColor The progress bar color in RGB, default is the [VANILLA_PROGRESS_COLOR].
|
||||
@@ -38,7 +38,7 @@ import net.minecraft.world.item.ItemStack
|
||||
class Toast(
|
||||
title: Component,
|
||||
description: Component,
|
||||
icon: ItemStack,
|
||||
icons: List<ItemStack>,
|
||||
frameTexture: ResourceLocation = VANILLA_BACKGROUND_SPRITE,
|
||||
progress: Float = -1F,
|
||||
progressColor: Int = VANILLA_PROGRESS_COLOR
|
||||
@@ -57,7 +57,7 @@ class Toast(
|
||||
/**
|
||||
* The [ItemStack] that is rendered as the toast icon, this can be a [ItemStack.EMPTY].
|
||||
*/
|
||||
var icon: ItemStack by Delegates.observable(icon) { _, old, new -> if (old != new) this.launchUpdate() }
|
||||
var icons: List<ItemStack> by Delegates.observable(icons) { _, old, new -> if (old != new) this.launchUpdate() }
|
||||
|
||||
/**
|
||||
* The texture of the frame.
|
||||
@@ -185,7 +185,7 @@ class Toast(
|
||||
private fun toPacket(behaviour: ToastPacket.Behaviour): ToastPacket = ToastPacket(
|
||||
this.title,
|
||||
this.description,
|
||||
this.icon,
|
||||
this.icons,
|
||||
this.frameTexture,
|
||||
this.progress,
|
||||
this.progressColor,
|
||||
|
||||
@@ -29,9 +29,12 @@ class ElementalType(
|
||||
val displayName: MutableComponent,
|
||||
val hue: Int,
|
||||
val textureXMultiplier: Int,
|
||||
val primaryColor: Int,
|
||||
val secondaryColor: Int,
|
||||
val resourceLocation: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, "ui/types.png"),
|
||||
val showdownId: String = ShowdownIdentifiable.REGEX.replace(name.lowercase(), "")
|
||||
) : ShowdownIdentifiable {
|
||||
val showdownId: String = ShowdownIdentifiable.REGEX.replace(name.lowercase(), ""),
|
||||
val typeGem: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, showdownId + "_gem"),
|
||||
) : ShowdownIdentifiable {
|
||||
|
||||
override fun showdownId(): String {
|
||||
return showdownId
|
||||
|
||||
@@ -23,7 +23,9 @@ object ElementalTypes {
|
||||
name = "Normal",
|
||||
displayName = Component.translatable("cobblemon.type.normal"),
|
||||
hue = 0xE8E8DA,
|
||||
textureXMultiplier = 0
|
||||
textureXMultiplier = 0,
|
||||
primaryColor = 0x8FA689,
|
||||
secondaryColor = 0xFFFBE2
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -31,7 +33,9 @@ object ElementalTypes {
|
||||
name = "Fire",
|
||||
displayName = Component.translatable("cobblemon.type.fire"),
|
||||
hue = 0xFF6E21,
|
||||
textureXMultiplier = 1
|
||||
textureXMultiplier = 1,
|
||||
primaryColor = 0xF92D2D,
|
||||
secondaryColor = 0xF99B1D
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -39,7 +43,9 @@ object ElementalTypes {
|
||||
name = "Water",
|
||||
displayName = Component.translatable("cobblemon.type.water"),
|
||||
hue = 0x3FA5FF,
|
||||
textureXMultiplier = 2
|
||||
textureXMultiplier = 2,
|
||||
primaryColor = 0x377AF6,
|
||||
secondaryColor = 0x50DDF9
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -47,7 +53,9 @@ object ElementalTypes {
|
||||
name = "Grass",
|
||||
displayName = Component.translatable("cobblemon.type.grass"),
|
||||
hue = 0x62D14F,
|
||||
textureXMultiplier = 3
|
||||
textureXMultiplier = 3,
|
||||
primaryColor = 0x0EAE95,
|
||||
secondaryColor = 0xC6F554
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -55,7 +63,9 @@ object ElementalTypes {
|
||||
name = "Electric",
|
||||
displayName = Component.translatable("cobblemon.type.electric"),
|
||||
hue = 0xFFD314,
|
||||
textureXMultiplier = 4
|
||||
textureXMultiplier = 4,
|
||||
primaryColor = 0xF7AB19,
|
||||
secondaryColor = 0xFFF257
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -63,7 +73,9 @@ object ElementalTypes {
|
||||
name = "Ice",
|
||||
displayName = Component.translatable("cobblemon.type.ice"),
|
||||
hue = 0x54F2F2,
|
||||
textureXMultiplier = 5
|
||||
textureXMultiplier = 5,
|
||||
primaryColor = 0x41BAED,
|
||||
secondaryColor = 0x91F4F9
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -71,7 +83,9 @@ object ElementalTypes {
|
||||
name = "Fighting",
|
||||
displayName = Component.translatable("cobblemon.type.fighting"),
|
||||
hue = 0xEF565D,
|
||||
textureXMultiplier = 6
|
||||
textureXMultiplier = 6,
|
||||
primaryColor = 0xC8356B,
|
||||
secondaryColor = 0xF36A3F
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -79,7 +93,9 @@ object ElementalTypes {
|
||||
name = "Poison",
|
||||
displayName = Component.translatable("cobblemon.type.poison"),
|
||||
hue = 0xD651FF,
|
||||
textureXMultiplier = 7
|
||||
textureXMultiplier = 7,
|
||||
primaryColor = 0x7B41DD,
|
||||
secondaryColor = 0xE568F4
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -87,7 +103,9 @@ object ElementalTypes {
|
||||
name = "Ground",
|
||||
displayName = Component.translatable("cobblemon.type.ground"),
|
||||
hue = 0xF4A453,
|
||||
textureXMultiplier = 8
|
||||
textureXMultiplier = 8,
|
||||
primaryColor = 0xE08835,
|
||||
secondaryColor = 0xFFD057
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -95,7 +113,9 @@ object ElementalTypes {
|
||||
name = "Flying",
|
||||
displayName = Component.translatable("cobblemon.type.flying"),
|
||||
hue = 0xB8B2FF,
|
||||
textureXMultiplier = 9
|
||||
textureXMultiplier = 9,
|
||||
primaryColor = 0x6083F6,
|
||||
secondaryColor = 0xD5CCFA
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -103,7 +123,9 @@ object ElementalTypes {
|
||||
name = "Psychic",
|
||||
displayName = Component.translatable("cobblemon.type.psychic"),
|
||||
hue = 0xFF5E9E,
|
||||
textureXMultiplier = 10
|
||||
textureXMultiplier = 10,
|
||||
primaryColor = 0xE553CE,
|
||||
secondaryColor = 0xF69DD5
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -111,7 +133,9 @@ object ElementalTypes {
|
||||
name = "Bug",
|
||||
displayName = Component.translatable("cobblemon.type.bug"),
|
||||
hue = 0xD3D319,
|
||||
textureXMultiplier = 11
|
||||
textureXMultiplier = 11,
|
||||
primaryColor = 0x9FBE1B,
|
||||
secondaryColor = 0xE9FB5B
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -119,7 +143,9 @@ object ElementalTypes {
|
||||
name = "Rock",
|
||||
displayName = Component.translatable("cobblemon.type.rock"),
|
||||
hue = 0xB7A16E,
|
||||
textureXMultiplier = 12
|
||||
textureXMultiplier = 12,
|
||||
primaryColor = 0x9F814A,
|
||||
secondaryColor = 0xEECB7A
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -127,7 +153,9 @@ object ElementalTypes {
|
||||
name = "Ghost",
|
||||
displayName = Component.translatable("cobblemon.type.ghost"),
|
||||
hue = 0x9C80F7,
|
||||
textureXMultiplier = 13
|
||||
textureXMultiplier = 13,
|
||||
primaryColor = 0x4F57DD,
|
||||
secondaryColor = 0xC182F1
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -135,7 +163,9 @@ object ElementalTypes {
|
||||
name = "Dragon",
|
||||
displayName = Component.translatable("cobblemon.type.dragon"),
|
||||
hue = 0x7580FF,
|
||||
textureXMultiplier = 14
|
||||
textureXMultiplier = 14,
|
||||
primaryColor = 0x4E68E2,
|
||||
secondaryColor = 0x47C7F7
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -143,7 +173,9 @@ object ElementalTypes {
|
||||
name = "Dark",
|
||||
displayName = Component.translatable("cobblemon.type.dark"),
|
||||
hue = 0x587DA0,
|
||||
textureXMultiplier = 15
|
||||
textureXMultiplier = 15,
|
||||
primaryColor = 0x465B99,
|
||||
secondaryColor = 0x689EBF
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -151,7 +183,9 @@ object ElementalTypes {
|
||||
name = "Steel",
|
||||
displayName = Component.translatable("cobblemon.type.steel"),
|
||||
hue = 0xABD1F4,
|
||||
textureXMultiplier = 16
|
||||
textureXMultiplier = 16,
|
||||
primaryColor = 0x628CB9,
|
||||
secondaryColor = 0xD6F7F3
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -159,16 +193,20 @@ object ElementalTypes {
|
||||
name = "Fairy",
|
||||
displayName = Component.translatable("cobblemon.type.fairy"),
|
||||
hue = 0xFF7FE5,
|
||||
textureXMultiplier = 17
|
||||
textureXMultiplier = 17,
|
||||
primaryColor = 0xE2678C,
|
||||
secondaryColor = 0xF5A7B7
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int): ElementalType {
|
||||
fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int, primaryColor: Int, secondaryColor: Int): ElementalType {
|
||||
return ElementalType(
|
||||
name = name,
|
||||
displayName = displayName,
|
||||
hue = hue,
|
||||
textureXMultiplier = textureXMultiplier
|
||||
textureXMultiplier = textureXMultiplier,
|
||||
primaryColor = primaryColor,
|
||||
secondaryColor = secondaryColor
|
||||
).also {
|
||||
allTypes.add(it)
|
||||
}
|
||||
@@ -273,4 +311,4 @@ object ElementalTypes {
|
||||
@JvmName("getFAIRY")
|
||||
@Deprecated("Use ElementalTypes.FAIRY, provided for backwards compatibility until Cobblemon 1.8.")
|
||||
fun getFAIRY() = FAIRY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +172,11 @@ object BattleRegistry {
|
||||
messages.add(""">player ${actor.showdownId} {"name":"${actor.uuid}","team":"${actor.pokemonList.packTeam()}"}""")
|
||||
}
|
||||
|
||||
// -> Set team size
|
||||
for (actor in battle.actors.sortedBy { it.showdownId }) {
|
||||
messages.add(">${actor.showdownId} team ${actor.pokemonList.count()}")
|
||||
// -> Set team size only when Team Preview is active.
|
||||
if (BattleRules.TEAM_PREVIEW in battle.format.ruleSet) {
|
||||
for (actor in battle.actors.sortedBy { it.showdownId }) {
|
||||
messages.add(">${actor.showdownId} team ${actor.pokemonList.count()}")
|
||||
}
|
||||
}
|
||||
|
||||
// Compiles the request and sends it off
|
||||
@@ -242,4 +244,4 @@ object BattleRegistry {
|
||||
fun tick() {
|
||||
battleMap.forEachValue(Long.MAX_VALUE) { it.tick() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,573 @@
|
||||
package com.cobblemon.mod.common.battles.ai
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
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.moves.Moves
|
||||
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 currentAiSide: BattleSide? = null
|
||||
private var currentOpponentSide: BattleSide? = 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 {
|
||||
currentAiSide = aiSide
|
||||
currentOpponentSide = aiSide.getOppositeSide()
|
||||
|
||||
initializeComponents(battle)
|
||||
updateBattleState(activeBattlePokemon, battle, aiSide)
|
||||
|
||||
if (currentGamePlan == null) {
|
||||
val npcTeam = aiSide.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()
|
||||
|
||||
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", availableSwitches = getAvailableSwitches(activeBattlePokemon), chosen = 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 partner = battleTracker.p2Active.getActiveAt(1 - currentSlot)
|
||||
val opponents = battleTracker.p1Active.getAllActive()
|
||||
|
||||
val decision = doublesDecisionMaker.findBestMoveAndTarget(
|
||||
moveset = moveset,
|
||||
activeBattlePokemon = activeBattlePokemon,
|
||||
myPokemon = myPokemon,
|
||||
opponents = opponents,
|
||||
partner = partner,
|
||||
battleState = state,
|
||||
gamePlan = plan
|
||||
) ?: return makeRandomMove(moveset, activeBattlePokemon)
|
||||
|
||||
turnDecisions[currentSlot] = decision
|
||||
firstSlotDecided = true
|
||||
debugLog(
|
||||
"Doubles: move=${decision.move.id} target=${decision.targetPNX ?: "auto"} " +
|
||||
"score=${"%.2f".format(decision.evaluation.totalScore)}"
|
||||
)
|
||||
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, aiSide: BattleSide) {
|
||||
val pnx = activeBattlePokemon.getPNX()
|
||||
currentSlot = if (pnx.length >= 3) (pnx[2] - 'a').coerceAtLeast(0) else 0
|
||||
|
||||
val opponentSide = aiSide.getOppositeSide()
|
||||
if (isDoublesMode) {
|
||||
opponentSide.activePokemon.forEachIndexed { index, active ->
|
||||
active.battlePokemon?.let { battleTracker.updatePokemonSlot(battleTracker.p1Active, index, it) }
|
||||
}
|
||||
aiSide.activePokemon.forEachIndexed { index, active ->
|
||||
active.battlePokemon?.let { battleTracker.updatePokemonSlot(battleTracker.p2Active, index, it) }
|
||||
}
|
||||
|
||||
battleTracker.p1Active.activePokemon =
|
||||
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
|
||||
battleTracker.p2Active.activePokemon =
|
||||
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
|
||||
} else {
|
||||
opponentSide.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, aiSide)
|
||||
}
|
||||
|
||||
private fun createBattleState(battle: PokemonBattle, aiSide: BattleSide): BattleState? {
|
||||
val opponentSide = aiSide.getOppositeSide()
|
||||
val playerActor = opponentSide.actors.firstOrNull() ?: return null
|
||||
val aiActor = aiSide.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) 1 - currentSlot else null
|
||||
|
||||
return BattleState(
|
||||
turn = battle.turn,
|
||||
playerTeam = playerActor.pokemonList.map { it.effectedPokemon },
|
||||
npcTeam = aiActor.pokemonList.map { it.effectedPokemon },
|
||||
playerActive = playerActive,
|
||||
npcActive = npcActive,
|
||||
weather = getWeather(battle),
|
||||
terrain = getTerrain(battle),
|
||||
playerHazards = getHazards(opponentSide),
|
||||
npcHazards = getHazards(aiSide),
|
||||
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 = availableSwitches,
|
||||
opponentPokemon = battleTracker.p1Active.activePokemon,
|
||||
battleState = state,
|
||||
gamePlan = plan
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val switchTarget = bestSwitch ?: availableSwitches.first()
|
||||
switchTarget.willBeSwitchedIn = true
|
||||
logSwitchDecision("forced_switch", 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(
|
||||
myPokemon = battleTracker.p2Active.activePokemon,
|
||||
opponentPokemon = battleTracker.p1Active.activePokemon,
|
||||
battleState = state,
|
||||
gamePlan = plan,
|
||||
availableSwitches = 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 = availableSwitches,
|
||||
opponentPokemon = battleTracker.p1Active.activePokemon,
|
||||
battleState = state,
|
||||
gamePlan = 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 = move,
|
||||
attacker = battleTracker.p2Active.activePokemon,
|
||||
defender = battleTracker.p1Active.activePokemon,
|
||||
battleState = state,
|
||||
gamePlan = 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() {
|
||||
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(
|
||||
pokemon = opponentActive.species ?: "unknown",
|
||||
move = move,
|
||||
situation = situation.name.lowercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHealthChange(packet: BattleHealthChangePacket) {
|
||||
try {
|
||||
val battle = currentBattle ?: return
|
||||
val activePokemon = battle.activePokemon.find { it.getPNX() == packet.pnx } ?: return
|
||||
val newHealth = packet.newHealth.toInt()
|
||||
val isOpponentPokemon = currentOpponentSide?.actors?.contains(activePokemon.actor) == true
|
||||
val teamTracker = if (isOpponentPokemon) battleTracker.p1Active else battleTracker.p2Active
|
||||
val tracker = battleTracker.getTrackedPokemonByPNX(packet.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, isOpponentPokemon)
|
||||
detectCriticalThresholds(tracker, oldHealth, newHealth, isOpponentPokemon)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMomentum(hpChange: Int, isOpponentPokemon: Boolean) {
|
||||
val momentumChange = when {
|
||||
hpChange > 0 -> -5.0
|
||||
hpChange < 0 -> kotlin.math.abs(hpChange) / 10.0
|
||||
else -> 0.0
|
||||
}
|
||||
|
||||
if (isOpponentPokemon) {
|
||||
battleTracker.p2Active.momentum += momentumChange
|
||||
battleTracker.p1Active.momentum -= momentumChange
|
||||
} else {
|
||||
battleTracker.p2Active.momentum -= momentumChange
|
||||
battleTracker.p1Active.momentum += momentumChange
|
||||
}
|
||||
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,
|
||||
isOpponentPokemon: Boolean
|
||||
) {
|
||||
if (newHealth <= 0 && oldHealth > 0) {
|
||||
if (isOpponentPokemon) {
|
||||
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 (_: 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? {
|
||||
val weather = battle.contextManager.get(BattleContext.Type.WEATHER)?.firstOrNull()?.id
|
||||
return AiBattleUtils.normalizeWeatherId(weather)
|
||||
}
|
||||
|
||||
private fun getTerrain(battle: PokemonBattle): String? {
|
||||
return battle.contextManager.get(BattleContext.Type.TERRAIN)?.firstOrNull()?.id
|
||||
}
|
||||
|
||||
private fun getHazards(side: BattleSide): List<String> {
|
||||
return side.contextManager.get(BattleContext.Type.HAZARD)?.map { it.id } ?: 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,
|
||||
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 = candidate.effectedPokemon,
|
||||
opponent = opponent,
|
||||
battleState = state,
|
||||
gamePlan = 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 System.getProperty("cobblemon.ai.debug", "false").toBooleanStrictOrNull() ?: false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
package com.cobblemon.mod.common.battles.ai
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
|
||||
import com.cobblemon.mod.common.api.battles.model.ai.BattleAI
|
||||
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 java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
class NewCompetitiveBattleAI(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 = NewEnhancedActiveTracker()
|
||||
private val opponentPredictor = NewOpponentPredictor()
|
||||
private val strategyAnalyzer = NewStrategyAnalyzer()
|
||||
private lateinit var damageCalculator: NewDamageCalculator
|
||||
private lateinit var moveEvaluator: NewMoveEvaluator
|
||||
private lateinit var switchDecisionMaker: NewSwitchDecisionMaker
|
||||
private lateinit var doublesDecisionMaker: NewDoublesDecisionMaker
|
||||
|
||||
private var currentBattle: PokemonBattle? = null
|
||||
private var currentGamePlan: GamePlan? = null
|
||||
private var battleState: BattleState? = null
|
||||
private var lastMoveEvaluation: NewMoveEvaluator.MoveEvaluation? = null
|
||||
|
||||
private var currentAiSide: BattleSide? = null
|
||||
private var currentOpponentSide: BattleSide? = null
|
||||
|
||||
private var isDoublesMode: Boolean = false
|
||||
private var currentSlot: Int = 0
|
||||
private var currentTurn: Int = -1
|
||||
private val turnDecisions: MutableMap<Int, NewDoublesDecisionMaker.MoveTargetPair> = mutableMapOf()
|
||||
private var firstSlotDecided: Boolean = false
|
||||
private val lastVoluntarySwitchTurnByActor: MutableMap<UUID, Int> = mutableMapOf()
|
||||
private val lastVoluntarySwitchFromByActor: MutableMap<UUID, String> = mutableMapOf()
|
||||
private val lastVoluntarySwitchToByActor: MutableMap<UUID, String> = mutableMapOf()
|
||||
|
||||
override fun choose(
|
||||
activeBattlePokemon: ActiveBattlePokemon,
|
||||
battle: PokemonBattle,
|
||||
aiSide: BattleSide,
|
||||
moveset: ShowdownMoveset?,
|
||||
forceSwitch: Boolean
|
||||
): ShowdownActionResponse {
|
||||
try {
|
||||
currentAiSide = aiSide
|
||||
currentOpponentSide = aiSide.getOppositeSide()
|
||||
|
||||
initializeComponents(battle)
|
||||
updateBattleState(activeBattlePokemon, battle, aiSide)
|
||||
|
||||
if (currentGamePlan == null) {
|
||||
val npcTeam = aiSide.actors.first().pokemonList.map { it.effectedPokemon }
|
||||
currentGamePlan = strategyAnalyzer.analyzeTeamComposition(npcTeam)
|
||||
}
|
||||
|
||||
if (battle.turn != currentTurn) {
|
||||
currentTurn = battle.turn
|
||||
turnDecisions.clear()
|
||||
firstSlotDecided = false
|
||||
}
|
||||
|
||||
logDecisionContext(activeBattlePokemon, forceSwitch)
|
||||
|
||||
if (forceSwitch || activeBattlePokemon.isGone()) {
|
||||
return handleForcedSwitch(activeBattlePokemon)
|
||||
}
|
||||
|
||||
if (!passesSkillCheck()) {
|
||||
debugLog("Skill check failed; selecting random move.")
|
||||
return makeRandomMove(moveset, activeBattlePokemon)
|
||||
}
|
||||
|
||||
updateOpponentModel()
|
||||
|
||||
if (isDoublesMode) {
|
||||
return chooseForDoubles(activeBattlePokemon, moveset)
|
||||
}
|
||||
|
||||
val switchGateOpen = shouldAttemptSwitch()
|
||||
if (switchGateOpen) {
|
||||
val shouldVoluntarySwitch = shouldSwitch(activeBattlePokemon)
|
||||
debugLog("Switch Gate: open | evaluator=$shouldVoluntarySwitch | ${switchDecisionMaker.latestSwitchRationale()}")
|
||||
if (shouldVoluntarySwitch) {
|
||||
val diagnostics = switchDecisionMaker.latestDiagnosticsSnapshot()
|
||||
val actorId = activeBattlePokemon.actor.uuid
|
||||
val currentName = activeBattlePokemon.battlePokemon?.effectedPokemon?.species?.name ?: "unknown"
|
||||
val lastSwitchTurn = lastVoluntarySwitchTurnByActor[actorId] ?: Int.MIN_VALUE
|
||||
val switchedLastTurn = battle.turn - lastSwitchTurn <= 1
|
||||
val pingPongCandidate =
|
||||
lastVoluntarySwitchToByActor[actorId] == currentName
|
||||
|
||||
if (switchedLastTurn && !diagnostics.severeDisadvantage) {
|
||||
debugLog("Switch Guard: blocked (switched last turn and matchup is not severe).")
|
||||
} else if (pingPongCandidate && !diagnostics.severeDisadvantage) {
|
||||
debugLog("Switch Guard: blocked (prevents immediate switch loop).")
|
||||
} else {
|
||||
val switchTarget = findBestSwitch(activeBattlePokemon)
|
||||
if (switchTarget != null) {
|
||||
switchTarget.willBeSwitchedIn = true
|
||||
logSwitchDecision("voluntary_switch", availableSwitches = getAvailableSwitches(activeBattlePokemon), chosen = switchTarget)
|
||||
debugLog("Chosen Action: SWITCH -> ${switchTarget.effectedPokemon.species.name}")
|
||||
lastVoluntarySwitchTurnByActor[actorId] = battle.turn
|
||||
lastVoluntarySwitchFromByActor[actorId] = currentName
|
||||
lastVoluntarySwitchToByActor[actorId] = switchTarget.effectedPokemon.species.name
|
||||
return SwitchActionResponse(switchTarget.uuid)
|
||||
}
|
||||
debugLog("Switch Gate: evaluator requested switch, but no legal target found. Staying in.")
|
||||
}
|
||||
} else {
|
||||
debugLog("Switch Gate: staying in; move evaluation selected instead.")
|
||||
}
|
||||
} else {
|
||||
debugLog("Switch Gate: closed by tempo/skill gate; evaluating moves.")
|
||||
}
|
||||
|
||||
val bestMove = chooseBestMove(moveset, activeBattlePokemon)
|
||||
logMoveDecision("move_select", bestMove, activeBattlePokemon)
|
||||
return executeMove(bestMove, activeBattlePokemon)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return PassActionResponse
|
||||
}
|
||||
}
|
||||
|
||||
private fun chooseForDoubles(
|
||||
activeBattlePokemon: ActiveBattlePokemon,
|
||||
moveset: ShowdownMoveset?
|
||||
): ShowdownActionResponse {
|
||||
val state = battleState ?: return PassActionResponse
|
||||
val plan = currentGamePlan ?: return PassActionResponse
|
||||
val myPokemon = battleTracker.p2Active.getActiveAt(currentSlot) ?: return PassActionResponse
|
||||
val partner = battleTracker.p2Active.getActiveAt(1 - currentSlot)
|
||||
val opponents = battleTracker.p1Active.getAllActive()
|
||||
|
||||
val decision = doublesDecisionMaker.findBestMoveAndTarget(
|
||||
moveset = moveset,
|
||||
activeBattlePokemon = activeBattlePokemon,
|
||||
myPokemon = myPokemon,
|
||||
opponents = opponents,
|
||||
partner = partner,
|
||||
battleState = state,
|
||||
gamePlan = plan
|
||||
) ?: return makeRandomMove(moveset, activeBattlePokemon)
|
||||
|
||||
turnDecisions[currentSlot] = decision
|
||||
firstSlotDecided = true
|
||||
debugLog(
|
||||
"Doubles Decision: move=${decision.move.id} target=${decision.targetPNX ?: "auto"} " +
|
||||
"score=${"%.2f".format(decision.evaluation.totalScore)}"
|
||||
)
|
||||
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 = NewDamageCalculator(battle)
|
||||
moveEvaluator = NewMoveEvaluator(damageCalculator, opponentPredictor, isDoublesMode)
|
||||
switchDecisionMaker = NewSwitchDecisionMaker(damageCalculator, opponentPredictor, strategyAnalyzer)
|
||||
doublesDecisionMaker = NewDoublesDecisionMaker(moveEvaluator, damageCalculator)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBattleState(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle, aiSide: BattleSide) {
|
||||
val pnx = activeBattlePokemon.getPNX()
|
||||
currentSlot = if (pnx.length >= 3) (pnx[2] - 'a').coerceAtLeast(0) else 0
|
||||
|
||||
val opponentSide = aiSide.getOppositeSide()
|
||||
if (isDoublesMode) {
|
||||
opponentSide.activePokemon.forEachIndexed { index, active ->
|
||||
active.battlePokemon?.let { battleTracker.updatePokemonSlot(battleTracker.p1Active, index, it) }
|
||||
}
|
||||
aiSide.activePokemon.forEachIndexed { index, active ->
|
||||
active.battlePokemon?.let { battleTracker.updatePokemonSlot(battleTracker.p2Active, index, it) }
|
||||
}
|
||||
|
||||
battleTracker.p1Active.activePokemon =
|
||||
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
|
||||
battleTracker.p2Active.activePokemon =
|
||||
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
|
||||
} else {
|
||||
opponentSide.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, aiSide)
|
||||
}
|
||||
|
||||
private fun createBattleState(battle: PokemonBattle, aiSide: BattleSide): BattleState? {
|
||||
val opponentSide = aiSide.getOppositeSide()
|
||||
val playerActor = opponentSide.actors.firstOrNull() ?: return null
|
||||
val aiActor = aiSide.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) 1 - currentSlot else null
|
||||
|
||||
return BattleState(
|
||||
turn = battle.turn,
|
||||
playerTeam = playerActor.pokemonList.map { it.effectedPokemon },
|
||||
npcTeam = aiActor.pokemonList.map { it.effectedPokemon },
|
||||
playerActive = playerActive,
|
||||
npcActive = npcActive,
|
||||
weather = getWeather(battle),
|
||||
terrain = getTerrain(battle),
|
||||
playerHazards = getHazards(opponentSide),
|
||||
npcHazards = getHazards(aiSide),
|
||||
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): ShowdownActionResponse {
|
||||
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
|
||||
if (availableSwitches.isEmpty()) {
|
||||
debugLog("Forced Switch: no legal switches available; forfeiting.")
|
||||
return ForfeitActionResponse()
|
||||
}
|
||||
|
||||
val state = battleState
|
||||
val plan = currentGamePlan
|
||||
val bestSwitch = if (state != null && plan != null) {
|
||||
switchDecisionMaker.findBestSwitch(
|
||||
availableSwitches = availableSwitches,
|
||||
opponentPokemon = battleTracker.p1Active.activePokemon,
|
||||
battleState = state,
|
||||
gamePlan = plan
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val switchTarget = bestSwitch ?: availableSwitches.first()
|
||||
switchTarget.willBeSwitchedIn = true
|
||||
logSwitchDecision("forced_switch", availableSwitches, switchTarget)
|
||||
debugLog("Chosen Action: FORCED SWITCH -> ${switchTarget.effectedPokemon.species.name}")
|
||||
return SwitchActionResponse(switchTarget.uuid)
|
||||
}
|
||||
|
||||
private fun shouldSwitch(activeBattlePokemon: ActiveBattlePokemon): Boolean {
|
||||
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
|
||||
if (availableSwitches.isEmpty()) return false
|
||||
val state = battleState ?: return false
|
||||
val plan = currentGamePlan ?: return false
|
||||
return switchDecisionMaker.shouldVoluntarilySwitch(
|
||||
myPokemon = battleTracker.p2Active.activePokemon,
|
||||
opponentPokemon = battleTracker.p1Active.activePokemon,
|
||||
battleState = state,
|
||||
gamePlan = plan,
|
||||
availableSwitches = availableSwitches
|
||||
)
|
||||
}
|
||||
|
||||
private fun findBestSwitch(activeBattlePokemon: ActiveBattlePokemon): BattlePokemon? {
|
||||
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
|
||||
val state = battleState ?: return null
|
||||
val plan = currentGamePlan ?: return null
|
||||
return switchDecisionMaker.findBestSwitch(
|
||||
availableSwitches = availableSwitches,
|
||||
opponentPokemon = battleTracker.p1Active.activePokemon,
|
||||
battleState = state,
|
||||
gamePlan = plan
|
||||
)
|
||||
}
|
||||
|
||||
private fun chooseBestMove(
|
||||
moveset: ShowdownMoveset?,
|
||||
activeBattlePokemon: ActiveBattlePokemon
|
||||
): 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 = move,
|
||||
attacker = battleTracker.p2Active.activePokemon,
|
||||
defender = battleTracker.p1Active.activePokemon,
|
||||
battleState = state,
|
||||
gamePlan = 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) {
|
||||
debugLog("Chosen Action: MOVE ${move.id} (no target selection needed)")
|
||||
MoveActionResponse(move.id)
|
||||
} else {
|
||||
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull() ?: target.random()
|
||||
debugLog("Chosen Action: MOVE ${move.id} -> target ${(chosenTarget as ActiveBattlePokemon).getPNX()}")
|
||||
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeRandomMove(moveset: ShowdownMoveset?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
|
||||
if (moveset == null) {
|
||||
debugLog("Fallback Action: PASS (no moveset available)")
|
||||
return PassActionResponse
|
||||
}
|
||||
val usableMoves = moveset.moves.filter { it.canBeUsed() }
|
||||
val move = usableMoves.randomOrNull() ?: run {
|
||||
debugLog("Fallback Action: PASS (no usable moves)")
|
||||
return PassActionResponse
|
||||
}
|
||||
debugLog("Fallback Action: random move '${move.id}'")
|
||||
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() {
|
||||
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(
|
||||
pokemon = opponentActive.species ?: "unknown",
|
||||
move = move,
|
||||
situation = situation.name.lowercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHealthChange(packet: BattleHealthChangePacket) {
|
||||
try {
|
||||
val battle = currentBattle ?: return
|
||||
val activePokemon = battle.activePokemon.find { it.getPNX() == packet.pnx } ?: return
|
||||
val newHealth = packet.newHealth.toInt()
|
||||
val isOpponentPokemon = currentOpponentSide?.actors?.contains(activePokemon.actor) == true
|
||||
val teamTracker = if (isOpponentPokemon) battleTracker.p1Active else battleTracker.p2Active
|
||||
val tracker = battleTracker.getTrackedPokemonByPNX(packet.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, isOpponentPokemon)
|
||||
detectCriticalThresholds(tracker, oldHealth, newHealth, isOpponentPokemon)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMomentum(hpChange: Int, isOpponentPokemon: Boolean) {
|
||||
val momentumChange = when {
|
||||
hpChange > 0 -> -5.0
|
||||
hpChange < 0 -> kotlin.math.abs(hpChange) / 10.0
|
||||
else -> 0.0
|
||||
}
|
||||
|
||||
if (isOpponentPokemon) {
|
||||
battleTracker.p2Active.momentum += momentumChange
|
||||
battleTracker.p1Active.momentum -= momentumChange
|
||||
} else {
|
||||
battleTracker.p2Active.momentum -= momentumChange
|
||||
battleTracker.p1Active.momentum += momentumChange
|
||||
}
|
||||
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,
|
||||
isOpponentPokemon: Boolean
|
||||
) {
|
||||
if (newHealth <= 0 && oldHealth > 0) {
|
||||
if (isOpponentPokemon) {
|
||||
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 (_: 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? {
|
||||
val weather = battle.contextManager.get(BattleContext.Type.WEATHER)?.firstOrNull()?.id
|
||||
return NewAiBattleUtils.normalizeWeatherId(weather)
|
||||
}
|
||||
|
||||
private fun getTerrain(battle: PokemonBattle): String? {
|
||||
return battle.contextManager.get(BattleContext.Type.TERRAIN)?.firstOrNull()?.id
|
||||
}
|
||||
|
||||
private fun getHazards(side: BattleSide): List<String> {
|
||||
return side.contextManager.get(BattleContext.Type.HAZARD)?.map { it.id } ?: 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 (!shouldLogDecision() || 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("Move Decision: reason=$reason | self=$self vs $opp | move=${move.id} | plan=$plan")
|
||||
return
|
||||
}
|
||||
debugLog(
|
||||
"Move Decision: ${move.id} selected | score=${"%.2f".format(eval.totalScore)} " +
|
||||
"| expectedDamage=${"%.1f".format(eval.expectedDamage)} | strategic=${"%.1f".format(eval.strategicValue)} " +
|
||||
"| risk=${"%.2f".format(eval.riskFactor)} | context=$self vs $opp | plan=$plan"
|
||||
)
|
||||
}
|
||||
|
||||
private fun logMoveRanking(evaluations: List<NewMoveEvaluator.MoveEvaluation>) {
|
||||
if (!shouldLogDecision() || 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)
|
||||
if (!aiDebugEnabled) {
|
||||
val compact = top.joinToString(" | ") { eval ->
|
||||
"${eval.move.id}=${"%.1f".format(eval.totalScore)}"
|
||||
}
|
||||
debugLog("Move Ranking: plan=$plan, situation=$situation -> $compact")
|
||||
return
|
||||
}
|
||||
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("Move Ranking (detailed): plan=$plan situation=$situation top=[$details]")
|
||||
}
|
||||
|
||||
private fun logSwitchDecision(
|
||||
reason: String,
|
||||
availableSwitches: List<BattlePokemon>,
|
||||
chosen: BattlePokemon?
|
||||
) {
|
||||
if (!shouldLogDecision()) 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 = candidate.effectedPokemon,
|
||||
opponent = opponent,
|
||||
battleState = state,
|
||||
gamePlan = 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("Switch Decision: reason=$reason | chosen=$chosenName | options=[$ranked]")
|
||||
}
|
||||
|
||||
private fun debugLog(message: String) {
|
||||
if (!shouldLogDecision()) return
|
||||
val battleIdShort = currentBattle?.battleId?.toString()?.take(8) ?: "unknown"
|
||||
val turn = currentBattle?.turn ?: -1
|
||||
Cobblemon.LOGGER.info("[NewCompetitiveBattleAI battle=$battleIdShort turn=$turn] $message")
|
||||
}
|
||||
|
||||
private fun isAiDebugEnabled(): Boolean {
|
||||
return System.getProperty("cobblemon.ai.debug", "false").toBooleanStrictOrNull() ?: false
|
||||
}
|
||||
|
||||
private fun shouldLogDecision(): Boolean {
|
||||
val battle = currentBattle
|
||||
val isNpcVsNpc = battle?.actors?.all { it.type == ActorType.NPC } == true
|
||||
return aiDebugEnabled || isNpcVsNpc
|
||||
}
|
||||
|
||||
private fun logDecisionContext(activeBattlePokemon: ActiveBattlePokemon, forceSwitch: Boolean) {
|
||||
if (!shouldLogDecision()) return
|
||||
val self = activeBattlePokemon.battlePokemon?.effectedPokemon?.species?.name ?: "unknown"
|
||||
val opp = battleTracker.p1Active.activePokemon.species ?: "unknown"
|
||||
val selfHp = formatPct(battleTracker.p2Active.activePokemon.currentHpPercent)
|
||||
val oppHp = formatPct(battleTracker.p1Active.activePokemon.currentHpPercent)
|
||||
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
|
||||
val situation = battleState?.let { categorizeSituation(it).name } ?: "unknown"
|
||||
val gamePhase = battleState?.gamePhase?.name ?: "unknown"
|
||||
debugLog(
|
||||
"Context: $self ($selfHp) vs $opp ($oppHp) | plan=$plan | situation=$situation | phase=$gamePhase " +
|
||||
"| forceSwitch=$forceSwitch | doubles=$isDoublesMode | skill=$skillLevel"
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatPct(value: Double): String {
|
||||
val pct = (value.coerceIn(0.0, 1.0) * 100).toInt()
|
||||
return "$pct%"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.battles.*
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.AIUtility
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.ActiveTracker
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.BattleAITags
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.TrackerPokemon
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.pokemon.status.PersistentStatus
|
||||
@@ -31,7 +32,6 @@ import kotlin.collections.associateWith
|
||||
import kotlin.collections.partition
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
/**
|
||||
* AI that tries to choose the best move for the given situations. Based off of the Pokemon Trainer Tournament Simulator Github
|
||||
* https://github.com/cRz-Shadows/Pokemon_Trainer_Tournament_Simulator/blob/main/pokemon-showdown/sim/examples/Simulation-test-1.ts#L330
|
||||
@@ -49,7 +49,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
private val hpWeightConsideration = 0.25 // how much HP difference is a consideration for switchins
|
||||
private val hpFractionCoefficient = 0.4 // how much HP differences should be taken into account for switch ins
|
||||
private val boostWeightCoefficient = 1 // the amount of boosts considered a baseline to be removed
|
||||
private val switchOutMatchupThreshold = 0 // todo change this to get it feeling just right (-7 never switches)
|
||||
private val switchOutMatchupThreshold = -2 // todo change this to get it feeling just right (-7 never switches)
|
||||
private val selfKoMoveMatchupThreshold = 0.3
|
||||
private val trickRoomThreshold = 85
|
||||
private val recoveryMoveThreshold = 0.50
|
||||
@@ -175,6 +175,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
(opponent.pokemon?.status?.status?.showdownName ?: opponent.currentStatus) == Statuses.BURN.showdownName && moveData.damageCategory == DamageCategories.PHYSICAL -> 0.5
|
||||
else -> 1.0
|
||||
}
|
||||
val abilityMoveTagMultiplier = BattleAITags.abilityMoveTagDamageMultiplier(
|
||||
attacker.currentAbility?.name ?: attacker.pokemon?.ability?.name,
|
||||
moveData.name
|
||||
)
|
||||
val hitsExpected = expectedHits(moveData)
|
||||
|
||||
var damage = (((((2 * pokemonLevel) / 5 ) + 2) * movePower * statRatio) / 50 + 2)
|
||||
@@ -182,6 +186,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
damage *= STAB
|
||||
damage *= damageTypeMultiplier
|
||||
damage *= burn
|
||||
damage *= abilityMoveTagMultiplier
|
||||
damage *= hitsExpected
|
||||
|
||||
return damage
|
||||
@@ -219,19 +224,14 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
.filter { it.second.canBeSentOut() }
|
||||
|
||||
if (forceSwitch || activeBattlePokemon.isGone()) {
|
||||
if (battle.turn == 1) {
|
||||
val switchTo = activeBattlePokemon.actor.pokemonList.filter { it.canBeSentOut() }.randomOrNull()
|
||||
?: return DefaultActionResponse()
|
||||
switchTo.willBeSwitchedIn = true
|
||||
return SwitchActionResponse(switchTo.uuid)
|
||||
}
|
||||
else {
|
||||
val bestEstimation = availableSwitches.maxByOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
|
||||
?: return PassActionResponse
|
||||
// When forced to switch, pick the Pokemon with the best matchup estimation
|
||||
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
|
||||
?: return PassActionResponse
|
||||
val bestSwitch = availableSwitches.firstOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) == bestSwitchScore }
|
||||
?: return PassActionResponse
|
||||
|
||||
bestEstimation.second.willBeSwitchedIn = true
|
||||
return SwitchActionResponse(bestEstimation.second.uuid)
|
||||
}
|
||||
bestSwitch.second.willBeSwitchedIn = true
|
||||
return SwitchActionResponse(bestSwitch.second.uuid)
|
||||
}
|
||||
if (moveset == null) {
|
||||
return PassActionResponse
|
||||
@@ -259,9 +259,9 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
|
||||
if (!checkSkillLevel()){
|
||||
val move = availableMoves
|
||||
.filter { it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
|
||||
.randomOrNull()
|
||||
?: return MoveActionResponse("struggle")
|
||||
.filter { it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
|
||||
.randomOrNull()
|
||||
?: return MoveActionResponse("struggle")
|
||||
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
@@ -270,7 +270,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
if (checkSwitchOutSkill() && shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
|
||||
considerSwitching(activeBattlePokemon, activeTrackerPokemon, opponents, availableMoves, availableSwitches, battle, aiSide)
|
||||
}
|
||||
activeTrackerPokemon.firstTurn = false
|
||||
|
||||
// If HP is below 30% and not switching out, always use the most damaging move
|
||||
if (activeTrackerPokemon.currentHpPercent < 0.3 && !shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
|
||||
@@ -296,8 +295,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
return chooseMove(fakeOut.first, activeBattlePokemon, targets.firstOrNull { validTarget != null && validTarget.id == it.battlePokemon!!.uuid})
|
||||
}
|
||||
|
||||
activeTrackerPokemon.firstTurn = false
|
||||
|
||||
|
||||
// Explosion/Self destruct
|
||||
availableMoves.firstOrNull {
|
||||
@@ -315,7 +312,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
|
||||
// Deal with non-weather related field changing effects
|
||||
for (move in availableMoves) {
|
||||
|
||||
|
||||
// Tailwind
|
||||
if (move.first.id == "tailwind" && move.first.id != activeTracker.alliedSide.tailwindCondition &&
|
||||
availableSwitches.size > 2) {
|
||||
@@ -324,21 +321,21 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
|
||||
// Trick room
|
||||
if (move.first.id == "trickroom" && move.first.id != activeTracker.currentRoom
|
||||
&& availableSwitches.count { statEstimationActive(it.first, Stats.SPEED) <= trickRoomThreshold } >= 2) {
|
||||
&& availableSwitches.count { statEstimationActive(it.first, Stats.SPEED) <= trickRoomThreshold } >= 2) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
|
||||
// todo find a way to get list of active screens
|
||||
// Aurora veil
|
||||
if (move.first.id == "auroraveil" && move.first.id != activeTracker.alliedSide.screenCondition
|
||||
&& activeTracker.currentWeather in listOf("hail", "snow")) {
|
||||
&& activeTracker.currentWeather in listOf("hail", "snow")) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
|
||||
// todo find a way to get list of active screens
|
||||
// Light Screen
|
||||
if (move.first.id == "lightscreen" && move.first.id != activeTracker.alliedSide.screenCondition
|
||||
&& opponents.any { (it.species?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: 0) > (it.species?.baseStats?.get(Stats.ATTACK) ?: 0) }
|
||||
&& opponents.any { (it.species?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: 0) > (it.species?.baseStats?.get(Stats.ATTACK) ?: 0) }
|
||||
&& availableSwitches.size > 1) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
@@ -356,13 +353,13 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
for (move in availableMoves) {
|
||||
// Setup
|
||||
if (nOppRemainingMons >= 3 && move.first.id in AIUtility.entryHazards
|
||||
&& !activeTracker.opponentSide.sideHazards.contains(move.first.id)) {
|
||||
&& !activeTracker.opponentSide.sideHazards.contains(move.first.id)) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
|
||||
// Removal
|
||||
if (nRemainingMons >= 2 && move.first.id in AIUtility.antiHazardsMoves
|
||||
&& activeTracker.alliedSide.sideHazards.isNotEmpty()) {
|
||||
&& activeTracker.alliedSide.sideHazards.isNotEmpty()) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
}
|
||||
@@ -372,10 +369,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
// Court Change
|
||||
for (move in availableMoves) {
|
||||
if (move.first.id == "courtchange"
|
||||
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|
||||
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
|
||||
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
|
||||
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
|
||||
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|
||||
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
|
||||
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
|
||||
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
}
|
||||
@@ -383,7 +380,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
// Strength Sap
|
||||
for (move in availableMoves) {
|
||||
if (move.first.id == "strengthsap" && activeTrackerPokemon.currentHpPercent < 0.5
|
||||
&& activeTrackerPokemon.species!!.baseStats.getOrDefault(Stats.ATTACK, 0) > 80) {
|
||||
&& activeTrackerPokemon.species!!.baseStats.getOrDefault(Stats.ATTACK, 0) > 80) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
}
|
||||
@@ -391,10 +388,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
// Belly Drum
|
||||
for (move in availableMoves) {
|
||||
if (move.first.id == "bellydrum"
|
||||
&& (activeTrackerPokemon.currentHpPercent > 0.6
|
||||
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|
||||
|| activeTrackerPokemon.currentHpPercent > 0.8)
|
||||
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
|
||||
&& (activeTrackerPokemon.currentHpPercent > 0.6
|
||||
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|
||||
|| activeTrackerPokemon.currentHpPercent > 0.8)
|
||||
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
}
|
||||
@@ -404,8 +401,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
for (move in availableMoves) {
|
||||
AIUtility.weatherSetupMoves[move.first.id]?.let { requiredWeather ->
|
||||
if (activeTracker.currentWeather != requiredWeather.lowercase() &&
|
||||
!(activeTracker.currentWeather == "primordialsea" && requiredWeather == "raindance") &&
|
||||
!(activeTracker.currentWeather == "desolateland" && requiredWeather == "sunnyday")) {
|
||||
!(activeTracker.currentWeather == "primordialsea" && requiredWeather == "raindance") &&
|
||||
!(activeTracker.currentWeather == "desolateland" && requiredWeather == "sunnyday")) {
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
}
|
||||
@@ -431,8 +428,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
// Stall out side conditions
|
||||
if ((activeTracker.opponentSide.screenCondition != null || activeTracker.opponentSide.tailwindCondition != null) &&
|
||||
(activeTracker.alliedSide.screenCondition == null || activeTracker.alliedSide.tailwindCondition == null) ||
|
||||
opponents.any { it.currentStatus == null } &&
|
||||
activeTrackerPokemon.protectCount == 0 && opponents.none { it.currentAbility?.name == "unseenfist"}) {
|
||||
opponents.any { it.currentStatus == null } &&
|
||||
activeTrackerPokemon.protectCount == 0 && opponents.none { it.currentAbility?.name == "unseenfist"}) {
|
||||
activeTrackerPokemon.protectCount = 3
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
@@ -460,10 +457,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
activeTrackerPokemon.firstTurn = false
|
||||
|
||||
val move = availableMoves
|
||||
.filter { it.first.canBeUsed() }
|
||||
.filter { it.first.mustBeUsed() || it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
|
||||
.randomOrNull()
|
||||
?: return MoveActionResponse("struggle")
|
||||
.filter { it.first.canBeUsed() }
|
||||
.filter { it.first.mustBeUsed() || it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
|
||||
.randomOrNull()
|
||||
?: return MoveActionResponse("struggle")
|
||||
|
||||
return chooseMove(move.first, activeBattlePokemon)
|
||||
}
|
||||
@@ -520,6 +517,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
return false
|
||||
|
||||
val activeTrackerPokemon = activeTracker.alliedSide.activePokemon.first { it.id == activeBattlePokemon.battlePokemon!!.uuid }
|
||||
|
||||
// Don't switch out if we just switched in
|
||||
if (activeTrackerPokemon.firstTurn)
|
||||
return false
|
||||
val actorTracker = activeTracker.alliedSide.actors.first { activeTrackerPokemon in it.activePokemon }
|
||||
val availableSwitches = actorTracker.party.filter { it.currentHp!! > 0 }
|
||||
val currentAbility = activeTrackerPokemon.pokemon!!.ability
|
||||
@@ -532,7 +533,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
// Only switch if a switch-in is significantly better than the current Pokémon
|
||||
val currentScore = estimateMatchup(activeBattlePokemon, side, battle)
|
||||
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, side, battle, it) } ?: currentScore
|
||||
val improvementThreshold = currentScore.absoluteValue * 0.25 + 2
|
||||
val improvementThreshold = currentScore.absoluteValue * 0.5 + 3
|
||||
if (bestSwitchScore <= currentScore + improvementThreshold) {
|
||||
return false
|
||||
}
|
||||
@@ -567,7 +568,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
if (availableSwitches.isEmpty() || availableSwitches.none { estimateMatchup(activeBattlePokemon, side, battle, it) > 0 })
|
||||
return false
|
||||
|
||||
|
||||
|
||||
|
||||
// ...and a 'good' reason to switch out
|
||||
if (opponentActiveTracker.any { it.boosts.getOrDefault(Stats.ACCURACY, 0) <= accuracySwitchThreshold } ||
|
||||
@@ -996,5 +997,3 @@ class StrongBattleAI(skill: Int) : BattleAI {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.BattleAITags
|
||||
|
||||
object AiBattleUtils {
|
||||
fun normalizeWeatherId(weather: String?): String? {
|
||||
val id = weather?.lowercase() ?: return null
|
||||
return when (id) {
|
||||
"sun", "sunny", "sunnyday", "harshsunlight", "desolateland" -> "sunnyday"
|
||||
"rain", "raindance", "heavyrain", "primordialsea", "raining" -> "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 }
|
||||
}
|
||||
|
||||
fun parseTagPercent(tag: String, prefix: String): Int? {
|
||||
if (!tag.startsWith(prefix)) {
|
||||
return null
|
||||
}
|
||||
return tag.removePrefix(prefix).toIntOrNull()
|
||||
}
|
||||
|
||||
fun abilityHasTag(abilityId: String?, tag: String): Boolean {
|
||||
if (abilityId.isNullOrBlank()) {
|
||||
return false
|
||||
}
|
||||
return tag.lowercase() in BattleAITags.getAbilityTags(abilityId)
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
+474
@@ -0,0 +1,474 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
|
||||
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.ElementalTypes
|
||||
import com.cobblemon.mod.common.battles.InBattleMove
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.BattleAITags
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import java.util.UUID
|
||||
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
|
||||
if (moveData.power <= 0) return 0.0
|
||||
|
||||
var power = moveData.power.toDouble()
|
||||
if (isSpreadMove && targetCount > 1) {
|
||||
power *= 0.75
|
||||
}
|
||||
|
||||
if (checkAllImmunities(moveData, attacker, defender) == 0.0) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
val level = attacker.pokemon?.level ?: 50
|
||||
val attackStat = getAttackStat(moveData, attacker).coerceAtLeast(1.0)
|
||||
val defenseStat = getDefenseStat(moveData, defender).coerceAtLeast(1.0)
|
||||
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, weather)
|
||||
damage *= calculateAbilityMultiplier(attacker, defender, moveData)
|
||||
damage *= calculateItemMultiplier(attacker, defender, moveData)
|
||||
damage *= calculateBurnMultiplier(attacker, moveData)
|
||||
damage *= calculateScreenMultiplier(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, attacker, 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,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon
|
||||
): Boolean {
|
||||
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
|
||||
.map { it.lowercase() }
|
||||
val attackerAbilityTags = BattleAITags.getAbilityTags(
|
||||
attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
|
||||
)
|
||||
val ignoresGhostImmunity = "ignore_ghost_immunity_normal_fighting" in attackerAbilityTags
|
||||
|
||||
return when (moveData.elementalType) {
|
||||
ElementalTypes.GROUND -> "flying" in defenderTypes
|
||||
ElementalTypes.ELECTRIC -> "ground" in defenderTypes
|
||||
ElementalTypes.NORMAL, ElementalTypes.FIGHTING -> "ghost" in defenderTypes && !ignoresGhostImmunity
|
||||
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()
|
||||
|
||||
val moveTags = BattleAITags.getMoveTags(moveData.name)
|
||||
val defenderAbilityTags = BattleAITags.getAbilityTags(defenderAbility)
|
||||
val attackerAbilityTags = BattleAITags.getAbilityTags(attackerAbility)
|
||||
val ignoresAbility = "ignores_ability" in moveTags || "ignores_ability_effects" in attackerAbilityTags
|
||||
|
||||
if (ignoresAbility && defenderAbility != "wonderguard") {
|
||||
return false
|
||||
}
|
||||
|
||||
if ("immune_type_${moveData.elementalType.name.lowercase()}" in defenderAbilityTags) {
|
||||
return true
|
||||
}
|
||||
defenderAbilityTags
|
||||
.filter { it.startsWith("immune_move_tag_") }
|
||||
.forEach { immunityTag ->
|
||||
val guardedTag = immunityTag.removePrefix("immune_move_tag_")
|
||||
if (guardedTag in moveTags) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if ("blocks_opponent_priority" in defenderAbilityTags && moveData.priority > 0) {
|
||||
return true
|
||||
}
|
||||
if ("status_move_immunity" in defenderAbilityTags && moveData.power <= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (defenderAbilityTags.isNotEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Legacy fallback when ability tags are not present for this ability id.
|
||||
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", "armortail" -> 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)
|
||||
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()
|
||||
val defenderAbilityTags = BattleAITags.getAbilityTags(defenderAbility)
|
||||
val moveTags = BattleAITags.getMoveTags(moveData.name)
|
||||
|
||||
if ("powder" in moveTags && "grass" in defenderTypes) {
|
||||
return true
|
||||
}
|
||||
|
||||
val inflictsParalysis = moveTags.any { it.startsWith("paralysis_") }
|
||||
val inflictsBurn = moveTags.any { it.startsWith("burn_") }
|
||||
val inflictsToxic = moveTags.any { it.startsWith("toxic_") }
|
||||
val inflictsPoison = inflictsToxic || moveTags.any { it.startsWith("poison_") }
|
||||
val inflictsSleep = moveTags.any { it.startsWith("sleep_") }
|
||||
|
||||
if ("status_move_immunity" in defenderAbilityTags && moveData.power <= 0 &&
|
||||
(inflictsParalysis || inflictsBurn || inflictsPoison || inflictsSleep)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return when {
|
||||
inflictsParalysis -> {
|
||||
"electric" in defenderTypes ||
|
||||
"ground" in defenderTypes ||
|
||||
defenderAbility in listOf("limber", "voltabsorb", "lightningrod", "motordrive")
|
||||
}
|
||||
inflictsBurn -> {
|
||||
"fire" in defenderTypes ||
|
||||
defenderAbility in listOf("waterveil", "waterbubble", "flashfire")
|
||||
}
|
||||
inflictsPoison -> {
|
||||
"poison" in defenderTypes ||
|
||||
"steel" in defenderTypes ||
|
||||
defenderAbility in listOf("immunity", "poisonheal", "pastelveil")
|
||||
}
|
||||
inflictsSleep -> {
|
||||
defenderAbility in listOf("insomnia", "vitalspirit", "comatose", "sweetveil")
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateCriticalMultiplier(criticalHit: Boolean): Double = 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.lowercase() } ?: return 1.0
|
||||
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
|
||||
val attackerAbilityTags = BattleAITags.getAbilityTags(attackerAbility)
|
||||
return if (AiBattleUtils.hasStab(moveData.elementalType.name, pokemonTypes)) {
|
||||
if ("stab_boost" in attackerAbilityTags) 2.0 else 1.5
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateWeatherMultiplier(moveData: MoveTemplate, weather: String?): Double {
|
||||
return when (AiBattleUtils.normalizeWeatherId(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
|
||||
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
|
||||
val defenderAbility = defender.currentAbility?.lowercase() ?: defender.pokemon?.ability?.name?.lowercase()
|
||||
val attackerAbilityTags = BattleAITags.getAbilityTags(attackerAbility)
|
||||
val defenderTypes = defender.currentTypes ?: listOf()
|
||||
val activeWeather = AiBattleUtils.normalizeWeatherId(
|
||||
battle?.contextManager?.get(BattleContext.Type.WEATHER)?.firstOrNull()?.id
|
||||
)
|
||||
val moveType = moveData.elementalType.name.lowercase()
|
||||
|
||||
if ("conditional_type_boost" in attackerAbilityTags) {
|
||||
val hpConditionMet = "conditional_hp_low" !in attackerAbilityTags || attacker.currentHpPercent <= 0.33
|
||||
val weatherCondition = attackerAbilityTags
|
||||
.firstOrNull { it.startsWith("conditional_weather_") }
|
||||
?.removePrefix("conditional_weather_")
|
||||
val weatherConditionMet = weatherCondition == null ||
|
||||
AiBattleUtils.normalizeWeatherId(weatherCondition) == activeWeather
|
||||
val boostedTypes = attackerAbilityTags
|
||||
.filter { it.startsWith("type_") }
|
||||
.map { it.removePrefix("type_") }
|
||||
.toSet()
|
||||
if (hpConditionMet && weatherConditionMet && (boostedTypes.isEmpty() || moveType in boostedTypes)) {
|
||||
multiplier *= 1.5
|
||||
}
|
||||
}
|
||||
|
||||
if ("physical_boost" in attackerAbilityTags) {
|
||||
val statusName = attacker.pokemon?.status?.status?.showdownName?.lowercase()
|
||||
val conditionMet = when {
|
||||
"conditional_status" in attackerAbilityTags -> statusName != null
|
||||
"conditional_poison" in attackerAbilityTags ->
|
||||
statusName == "poison" || statusName == "tox" || statusName == "poisonbadly"
|
||||
"conditional_burn" in attackerAbilityTags -> statusName == "burn"
|
||||
else -> true
|
||||
}
|
||||
if (conditionMet && moveData.damageCategory == DamageCategories.PHYSICAL) {
|
||||
multiplier *= 1.5
|
||||
}
|
||||
}
|
||||
|
||||
when (defenderAbility) {
|
||||
"thickfat" -> if (moveData.elementalType == ElementalTypes.FIRE || moveData.elementalType == ElementalTypes.ICE) multiplier *= 0.5
|
||||
"filter", "solidrock" -> {
|
||||
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, defenderTypes)
|
||||
if (effectiveness > 1.0) multiplier *= 0.75
|
||||
}
|
||||
}
|
||||
|
||||
multiplier *= BattleAITags.abilityMoveTagDamageMultiplier(attackerAbility, moveData.name)
|
||||
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 {
|
||||
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
|
||||
val attackerAbilityTags = BattleAITags.getAbilityTags(attackerAbility)
|
||||
val ignoresBurnPenalty = "conditional_status" in attackerAbilityTags && "physical_boost" in attackerAbilityTags
|
||||
return if (attacker.pokemon?.status?.status?.showdownName == "burn" &&
|
||||
moveData.damageCategory == DamageCategories.PHYSICAL &&
|
||||
!ignoresBurnPenalty
|
||||
) {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateScreenMultiplier(defender: TrackedPokemon, moveData: MoveTemplate): Double {
|
||||
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
|
||||
|
||||
return when (moveData.damageCategory) {
|
||||
DamageCategories.PHYSICAL -> when {
|
||||
hasAuroraVeil || hasReflect -> 0.5
|
||||
else -> 1.0
|
||||
}
|
||||
DamageCategories.SPECIAL -> when {
|
||||
hasAuroraVeil || hasLightScreen -> 0.5
|
||||
else -> 1.0
|
||||
}
|
||||
else -> 1.0
|
||||
}
|
||||
}
|
||||
|
||||
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 + baseDamage * 2.0 * accuracy + baseDamage * 3.0 * accuracy * accuracy
|
||||
}
|
||||
moveName in MoveData.multiHitMoves.keys -> {
|
||||
val hitRange = MoveData.multiHitMoves[moveName] ?: mapOf(1 to 1)
|
||||
val minHits = hitRange.keys.minOrNull() ?: 1
|
||||
val maxHits = hitRange.values.maxOrNull() ?: 1
|
||||
when {
|
||||
minHits == 2 && maxHits == 5 -> baseDamage * 3.0
|
||||
minHits == maxHits -> baseDamage * minHits
|
||||
else -> baseDamage * ((minHits + maxHits) / 2.0)
|
||||
}
|
||||
}
|
||||
else -> baseDamage
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSoundMove(moveData: MoveTemplate): Boolean {
|
||||
val tags = BattleAITags.getMoveTags(moveData.name)
|
||||
return "sound" in tags
|
||||
}
|
||||
|
||||
private fun isBallOrBombMove(moveData: MoveTemplate): Boolean {
|
||||
val tags = BattleAITags.getMoveTags(moveData.name)
|
||||
return "bullet" in tags
|
||||
}
|
||||
|
||||
private fun isPowderMove(moveData: MoveTemplate): Boolean {
|
||||
val tags = BattleAITags.getMoveTags(moveData.name)
|
||||
return "powder" in tags
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
|
||||
import com.cobblemon.mod.common.battles.InBattleMove
|
||||
import com.cobblemon.mod.common.battles.ShowdownMoveset
|
||||
|
||||
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) {
|
||||
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))
|
||||
continue
|
||||
}
|
||||
|
||||
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 } ?: return@mapNotNull null
|
||||
val damage = damageCalculator.calculateDamage(move, myPokemon, targetPokemon, battleState.weather)
|
||||
val targetScore = scoreTarget(targetPokemon, myPokemon, opponents, battleState)
|
||||
target to (damage + targetScore)
|
||||
}
|
||||
return targetScores.maxByOrNull { it.second }?.first?.getPNX()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
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
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.BattleAITags
|
||||
|
||||
object MoveData {
|
||||
private val statTagRegex = Regex("^stat_(atk|def|spatk|spdef|speed|accuracy|evasion)_(-?\\d+)(?:_(\\d+))?$")
|
||||
|
||||
private val knownAntiHazardsFallback = listOf("rapidspin", "defog", "tidyup")
|
||||
private val knownAntiBoostFallback = listOf("clearsmog", "haze", "roar", "whirlwind")
|
||||
|
||||
private fun moveTags(moveId: String): Set<String> = BattleAITags.getMoveTags(moveId)
|
||||
|
||||
private fun allMoveIds(): List<String> = Moves.all().map { it.name.lowercase() }
|
||||
|
||||
private fun hasAnyTag(moveId: String, tags: Set<String>): Boolean {
|
||||
val moveTags = moveTags(moveId)
|
||||
return tags.any { it in moveTags }
|
||||
}
|
||||
|
||||
val multiHitMoves: Map<String, Map<Int, Int>> by lazy {
|
||||
allMoveIds().mapNotNull { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
val min = tags.firstOrNull { it.startsWith("multi_hit_min_") }?.removePrefix("multi_hit_min_")?.toIntOrNull()
|
||||
val max = tags.firstOrNull { it.startsWith("multi_hit_max_") }?.removePrefix("multi_hit_max_")?.toIntOrNull()
|
||||
when {
|
||||
min != null && max != null -> moveId to mapOf(min to max)
|
||||
"multi_hit" in tags -> moveId to mapOf(2 to 5)
|
||||
else -> null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private val statusByMoveId: Map<String, String> by lazy {
|
||||
allMoveIds().mapNotNull { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
when {
|
||||
tags.any { it.startsWith("sleep_") } -> moveId to Statuses.SLEEP.showdownName
|
||||
tags.any { it.startsWith("paralysis_") } -> moveId to Statuses.PARALYSIS.showdownName
|
||||
tags.any { it.startsWith("burn_") } -> moveId to Statuses.BURN.showdownName
|
||||
tags.any { it.startsWith("freeze_") } -> moveId to Statuses.FROZEN.showdownName
|
||||
tags.any { it.startsWith("toxic_") } -> moveId to Statuses.POISON_BADLY.showdownName
|
||||
tags.any { it.startsWith("poison_") } -> moveId to Statuses.POISON.showdownName
|
||||
tags.any { it.startsWith("confusion_") } -> moveId to Statuses.CONFUSE.showdownName
|
||||
tags.any { it.startsWith("attract_") } -> moveId to Statuses.ATTRACT.showdownName
|
||||
"partiallytrapped_100" in tags -> moveId to "partiallytrapped"
|
||||
"curse_100" in tags -> moveId to "cursed"
|
||||
"leech_100" in tags -> moveId to "leech"
|
||||
else -> null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
fun statusForMove(moveId: String): String? = statusByMoveId[moveId.lowercase()]
|
||||
|
||||
fun isStatusMove(moveId: String): Boolean {
|
||||
val normalizedId = moveId.lowercase()
|
||||
val tags = moveTags(normalizedId)
|
||||
return "category_status" in tags || statusByMoveId.containsKey(normalizedId)
|
||||
}
|
||||
|
||||
val boostFromMoves: Map<String, Map<Stat, Int>> by lazy {
|
||||
val result = mutableMapOf<String, MutableMap<Stat, Int>>()
|
||||
for (moveId in allMoveIds()) {
|
||||
val tags = moveTags(moveId)
|
||||
for (tag in tags) {
|
||||
val match = statTagRegex.find(tag) ?: continue
|
||||
val stat = when (match.groupValues[1]) {
|
||||
"atk" -> Stats.ATTACK
|
||||
"def" -> Stats.DEFENCE
|
||||
"spatk" -> Stats.SPECIAL_ATTACK
|
||||
"spdef" -> Stats.SPECIAL_DEFENCE
|
||||
"speed" -> Stats.SPEED
|
||||
"accuracy" -> Stats.ACCURACY
|
||||
"evasion" -> Stats.EVASION
|
||||
else -> continue
|
||||
}
|
||||
val stages = match.groupValues[2].toIntOrNull() ?: continue
|
||||
result.computeIfAbsent(moveId) { mutableMapOf() }[stat] = stages
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
val weatherMoves: Map<String, String> by lazy {
|
||||
allMoveIds().mapNotNull { moveId ->
|
||||
val weatherTag = moveTags(moveId).firstOrNull { it.startsWith("sets_weather_") } ?: return@mapNotNull null
|
||||
moveId to weatherTag.removePrefix("sets_weather_")
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
val weatherDependentMoves: Map<String, String> by lazy {
|
||||
allMoveIds().mapNotNull { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
val requiredWeather = tags.firstOrNull { it.startsWith("context_weather_") }?.removePrefix("context_weather_")
|
||||
if (requiredWeather != null) {
|
||||
moveId to requiredWeather
|
||||
} else if ("context_weather_sensitive" in tags) {
|
||||
moveId to "any"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
// Kept as explicit fallback until ability tags add canonical weather-condition keys across all abilities.
|
||||
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: List<String> by lazy {
|
||||
allMoveIds().filter { hasAnyTag(it, setOf("hazard_set", "sets_side_condition")) }
|
||||
}
|
||||
|
||||
val antiHazardsMoves: List<String> by lazy {
|
||||
val tagged = allMoveIds().filter { hasAnyTag(it, setOf("hazard_clear", "removes_hazards")) }.toSet()
|
||||
(tagged + knownAntiHazardsFallback).toList().filter { Moves.getByName(it) != null }
|
||||
}
|
||||
|
||||
val antiBoostMoves: List<String> by lazy {
|
||||
val tagged = allMoveIds().filter { hasAnyTag(it, setOf("clear_boosts", "anti_boost", "resets_stats")) }.toSet()
|
||||
(tagged + knownAntiBoostFallback).toList().filter { Moves.getByName(it) != null }
|
||||
}
|
||||
|
||||
val pivotMoves: List<String> by lazy {
|
||||
allMoveIds().filter { "pivot" in moveTags(it) }
|
||||
}
|
||||
|
||||
val setupMoves: Set<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"screen_set" in tags ||
|
||||
"sets_terrain" in tags ||
|
||||
"sets_weather" in tags ||
|
||||
"sets_pseudo_weather" in tags ||
|
||||
tags.any { it.startsWith("stat_") && !it.contains("_-") && "target_self" in tags }
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
val selfRecoveryMoves: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"healing_move" in tags && "target_self" in tags
|
||||
}
|
||||
}
|
||||
|
||||
val priorityMoves: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"priority_positive" in tags || tags.any { it.startsWith("priority_") && (it.removePrefix("priority_").toIntOrNull() ?: 0) > 0 }
|
||||
}
|
||||
}
|
||||
|
||||
val recoilMoves: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"recoil_move" in tags || tags.any { it.startsWith("recoil_") }
|
||||
}
|
||||
}
|
||||
|
||||
val selfKoMoves: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
"self_ko" in moveTags(moveId)
|
||||
}
|
||||
}
|
||||
|
||||
val crashRiskMoves: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
"crash_risk" in moveTags(moveId)
|
||||
}
|
||||
}
|
||||
|
||||
val highRiskMoves: List<String> by lazy {
|
||||
(recoilMoves + selfKoMoves + crashRiskMoves).distinct()
|
||||
}
|
||||
}
|
||||
+633
@@ -0,0 +1,633 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.pokemon.status.Statuses
|
||||
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.battles.ai.strongBattleAI.BattleAITags
|
||||
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 DebugDetails(
|
||||
val hasStab: Boolean,
|
||||
val effectiveness: Double,
|
||||
val accuracy: Double,
|
||||
val priority: Int,
|
||||
val isStatusMove: Boolean,
|
||||
val isSetup: Boolean,
|
||||
val setsHazards: Boolean,
|
||||
val removesHazards: Boolean,
|
||||
val setsWeather: Boolean,
|
||||
val isRecovery: Boolean,
|
||||
val isPivot: Boolean,
|
||||
val isRisky: Boolean
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
fun buildDebugDetails(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): DebugDetails {
|
||||
val moveData = Moves.getByName(move.id)
|
||||
?: return DebugDetails(
|
||||
hasStab = false,
|
||||
effectiveness = 1.0,
|
||||
accuracy = 0.0,
|
||||
priority = 0,
|
||||
isStatusMove = false,
|
||||
isSetup = false,
|
||||
setsHazards = false,
|
||||
removesHazards = false,
|
||||
setsWeather = false,
|
||||
isRecovery = false,
|
||||
isPivot = false,
|
||||
isRisky = false
|
||||
)
|
||||
|
||||
val moveTags = BattleAITags.getMoveTags(move.id)
|
||||
val attackerTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name.lowercase() } ?: emptyList()
|
||||
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, defender.currentTypes ?: emptyList())
|
||||
val priority = moveData.priority
|
||||
val risk = calculateRiskFactor(move, attacker, defender, battleState, gamePlan)
|
||||
|
||||
return DebugDetails(
|
||||
hasStab = AiBattleUtils.hasStab(moveData.elementalType.name, attackerTypes),
|
||||
effectiveness = effectiveness,
|
||||
accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather),
|
||||
priority = priority,
|
||||
isStatusMove = MoveData.isStatusMove(move.id.lowercase()) || moveData.power <= 0,
|
||||
isSetup = move.id.lowercase() in MoveData.boostFromMoves.keys,
|
||||
setsHazards = move.id.lowercase() in MoveData.entryHazards || "hazard_set" in moveTags,
|
||||
removesHazards = move.id.lowercase() in MoveData.antiHazardsMoves,
|
||||
setsWeather = move.id.lowercase() in MoveData.weatherMoves.keys,
|
||||
isRecovery = move.id.lowercase() in MoveData.selfRecoveryMoves,
|
||||
isPivot = move.id.lowercase() in MoveData.pivotMoves,
|
||||
isRisky = risk > 1.4
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEffectiveAccuracy(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
weather: String?
|
||||
): Double {
|
||||
val moveData = Moves.getByName(move.id) ?: return 0.0
|
||||
val moveTags = BattleAITags.getMoveTags(move.id)
|
||||
var accuracy = moveData.accuracy / 100.0
|
||||
val normalizedWeather = AiBattleUtils.normalizeWeatherId(weather)
|
||||
|
||||
if ("never_miss" in moveTags || "accuracy_true" in moveTags || "accuracy_perfect" in moveTags) {
|
||||
accuracy = 1.0
|
||||
}
|
||||
|
||||
when (normalizedWeather) {
|
||||
"raindance" -> {
|
||||
if ("context_weather_sensitive" in moveTags &&
|
||||
("type_electric" in moveTags || "type_flying" in moveTags)
|
||||
) {
|
||||
accuracy = 1.0
|
||||
}
|
||||
}
|
||||
"hail", "snow" -> {
|
||||
if ("context_weather_sensitive" in moveTags && "type_ice" in moveTags) {
|
||||
accuracy = 1.0
|
||||
}
|
||||
}
|
||||
"sunnyday" -> {
|
||||
if ("context_weather_sensitive" in moveTags &&
|
||||
("type_electric" in moveTags || "type_flying" in moveTags)
|
||||
) {
|
||||
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
|
||||
val moveId = move.id.lowercase()
|
||||
|
||||
if (moveId in MoveData.weatherMoves.keys) {
|
||||
value += evaluateWeatherSetting(move, gamePlan, battleState)
|
||||
}
|
||||
if (moveId in MoveData.boostFromMoves.keys) {
|
||||
value += evaluateSetupMove(move, attacker, defender, battleState, gamePlan)
|
||||
}
|
||||
if (MoveData.statusForMove(moveId) != null) {
|
||||
value += evaluateStatusMove(move, attacker, defender, battleState, gamePlan)
|
||||
}
|
||||
if (moveId in MoveData.entryHazards) {
|
||||
value += evaluateHazardSetting(battleState, gamePlan)
|
||||
}
|
||||
if (moveId 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 (moveId in MoveData.selfRecoveryMoves) {
|
||||
value += evaluateRecoveryMove(attacker, battleState, gamePlan)
|
||||
}
|
||||
if (moveId 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
|
||||
val moveTags = BattleAITags.getMoveTags(move.id)
|
||||
var alignment = 0.0
|
||||
val moveId = move.id.lowercase()
|
||||
val normalizedWeather = AiBattleUtils.normalizeWeatherId(battleState.weather)
|
||||
|
||||
if (moveId in gamePlan.riskyMoves || moveId in MoveData.highRiskMoves) {
|
||||
alignment -= 100.0
|
||||
}
|
||||
|
||||
alignment += when (gamePlan.winCondition) {
|
||||
WinCondition.RAIN_OFFENSE -> {
|
||||
var score = 0.0
|
||||
if (MoveData.weatherMoves[moveId] == "rain") score += 200.0
|
||||
if (normalizedWeather == "raindance") {
|
||||
if ("type_water" in moveTags) score += 50.0
|
||||
if ("context_weather_sensitive" in moveTags &&
|
||||
("type_electric" in moveTags || "type_flying" in moveTags)
|
||||
) {
|
||||
score += 100.0
|
||||
}
|
||||
}
|
||||
score
|
||||
}
|
||||
WinCondition.SUN_OFFENSE -> {
|
||||
var score = 0.0
|
||||
if (MoveData.weatherMoves[moveId] == "sun") score += 200.0
|
||||
if (normalizedWeather == "sunnyday") {
|
||||
if ("type_fire" in moveTags) score += 50.0
|
||||
if ("context_weather_sensitive" in moveTags &&
|
||||
("type_grass" in moveTags || "type_normal" in moveTags)
|
||||
) {
|
||||
score += 100.0
|
||||
}
|
||||
}
|
||||
score
|
||||
}
|
||||
WinCondition.SETUP_SWEEP -> {
|
||||
var score = 0.0
|
||||
if (moveId 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.isStatusMove(moveId)) score += 70.0
|
||||
if (moveId in MoveData.selfRecoveryMoves) score += 60.0
|
||||
if (moveData.power > 80) score -= 30.0
|
||||
score
|
||||
}
|
||||
WinCondition.HAZARD_CONTROL -> {
|
||||
var score = 0.0
|
||||
if ("hazard_set" in moveTags && battleState.playerHazards.isEmpty()) {
|
||||
score += 250.0
|
||||
}
|
||||
if ("hazard_set" in moveTags) score += 100.0
|
||||
if ("hazard_clear" in moveTags || "removes_hazards" in moveTags) score += 120.0
|
||||
score
|
||||
}
|
||||
WinCondition.REVENGE_KILLING -> {
|
||||
var score = 0.0
|
||||
if (moveData.priority > 0) score += 120.0
|
||||
if (defender.currentHp > 50 && moveData.power in 40.0..80.0) score += 40.0
|
||||
score
|
||||
}
|
||||
else -> 0.0
|
||||
}
|
||||
|
||||
alignment += if (moveData.power > 0) {
|
||||
moveData.power * gamePlan.aggressionLevel * 0.3
|
||||
} else {
|
||||
(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
|
||||
|
||||
val moveTags = BattleAITags.getMoveTags(move.id)
|
||||
if ("recoil_move" in moveTags || move.id.lowercase() in MoveData.recoilMoves) {
|
||||
risk += if (attacker.currentHpPercent < 0.5) 0.5 else 0.2
|
||||
}
|
||||
if ("self_ko" in moveTags || move.id.lowercase() in MoveData.selfKoMoves) {
|
||||
risk += when {
|
||||
attacker.species in gamePlan.keyPokemon -> 2.0
|
||||
attacker.currentHpPercent < 0.3 -> 0.2
|
||||
else -> 0.8
|
||||
}
|
||||
}
|
||||
if ("crash_risk" in moveTags) {
|
||||
risk += if (attacker.currentHpPercent < 0.5) 0.4 else 0.2
|
||||
}
|
||||
if ("reliability_low" in moveTags) {
|
||||
risk += 0.25
|
||||
}
|
||||
if ("delay_1" in moveTags && attacker.currentHpPercent < 0.5) {
|
||||
risk += 0.2
|
||||
}
|
||||
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 weatherType = MoveData.weatherMoves[move.id.lowercase()] ?: 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 = boosts.values.sum() * 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
|
||||
val status = MoveData.statusForMove(move.id.lowercase())
|
||||
return when (status) {
|
||||
Statuses.PARALYSIS.showdownName -> if (defender.predictedRole == PokemonRole.SWEEPER) 90.0 else 50.0
|
||||
Statuses.BURN.showdownName -> if (defender.predictedRole in listOf(PokemonRole.WALLBREAKER, PokemonRole.SWEEPER)) 80.0 else 40.0
|
||||
Statuses.POISON_BADLY.showdownName, Statuses.POISON.showdownName ->
|
||||
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) 100.0 else 60.0
|
||||
Statuses.SLEEP.showdownName -> 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)
|
||||
val targetingBonus = if (primaryTarget.threatLevel > 5) 20.0 else 0.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
|
||||
}
|
||||
}
|
||||
|
||||
totalStrategicValue += activeOpponents.size * 40.0
|
||||
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 = when {
|
||||
partnerPenalty < -1000 -> 999.0
|
||||
partnerPenalty < 0 -> max(1.5, 1.0 + abs(partnerPenalty) / 100.0)
|
||||
else -> 1.0
|
||||
}
|
||||
val gamePlanAlignment = activeOpponents.firstOrNull()?.let {
|
||||
calculateGamePlanAlignment(move, attacker, it, battleState, gamePlan)
|
||||
} ?: 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
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.BattleAITags
|
||||
|
||||
object NewAiBattleUtils {
|
||||
private val percentSuffixRegex = Regex("_(\\d+)$")
|
||||
|
||||
fun normalizeWeatherId(weather: String?): String? = AiBattleUtils.normalizeWeatherId(weather)
|
||||
|
||||
fun hasStab(moveTypeName: String, pokemonTypes: List<String>): Boolean =
|
||||
AiBattleUtils.hasStab(moveTypeName, pokemonTypes)
|
||||
|
||||
fun moveTags(moveId: String?): Set<String> = BattleAITags.getMoveTags(moveId)
|
||||
|
||||
fun abilityTags(abilityId: String?): Set<String> = BattleAITags.getAbilityTags(abilityId)
|
||||
|
||||
fun parseTagPercent(tag: String, prefix: String): Int? = AiBattleUtils.parseTagPercent(tag, prefix)
|
||||
|
||||
fun parseTrailingPercent(tag: String): Int? =
|
||||
percentSuffixRegex.find(tag)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
|
||||
fun hasTag(tags: Set<String>, tag: String): Boolean = tag.lowercase() in tags
|
||||
|
||||
fun hasTagPrefix(tags: Set<String>, prefix: String): Boolean =
|
||||
tags.any { it.startsWith(prefix.lowercase()) }
|
||||
|
||||
fun anyTag(tags: Set<String>, candidates: Set<String>): Boolean =
|
||||
candidates.any { it in tags }
|
||||
|
||||
fun firstTagValue(tags: Set<String>, prefix: String): String? =
|
||||
tags.firstOrNull { it.startsWith(prefix) }?.removePrefix(prefix)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
|
||||
typealias NewBattleState = BattleState
|
||||
typealias NewTrackedPokemon = TrackedPokemon
|
||||
typealias NewTeamTracker = TeamTracker
|
||||
typealias NewGamePhase = GamePhase
|
||||
typealias NewPokemonRole = PokemonRole
|
||||
typealias NewBattleSituation = BattleSituation
|
||||
typealias NewMatchupEvaluation = MatchupEvaluation
|
||||
typealias NewMatchupRecommendation = MatchupRecommendation
|
||||
|
||||
class NewEnhancedActiveTracker {
|
||||
private val delegate = EnhancedActiveTracker()
|
||||
|
||||
var p1Active: TeamTracker
|
||||
get() = delegate.p1Active
|
||||
set(value) {
|
||||
delegate.p1Active = value
|
||||
}
|
||||
|
||||
var p2Active: TeamTracker
|
||||
get() = delegate.p2Active
|
||||
set(value) {
|
||||
delegate.p2Active = value
|
||||
}
|
||||
|
||||
fun updatePokemonSlot(team: TeamTracker, slotIndex: Int, battlePokemon: BattlePokemon) {
|
||||
delegate.updatePokemonSlot(team, slotIndex, battlePokemon)
|
||||
}
|
||||
|
||||
fun getTrackedPokemonByPNX(pnx: String, team: TeamTracker): TrackedPokemon? {
|
||||
return delegate.getTrackedPokemonByPNX(pnx, team)
|
||||
}
|
||||
|
||||
fun updatePokemon(tracker: TrackedPokemon, battlePokemon: BattlePokemon) {
|
||||
delegate.updatePokemon(tracker, battlePokemon)
|
||||
}
|
||||
|
||||
fun updateParty(team: TeamTracker, pokemon: TrackedPokemon) {
|
||||
delegate.updateParty(team, pokemon)
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.battles.InBattleMove
|
||||
|
||||
class NewDamageCalculator(battle: PokemonBattle?) {
|
||||
internal val delegate = DamageCalculator(battle)
|
||||
|
||||
fun calculateDamage(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
weather: String? = null,
|
||||
criticalHit: Boolean = false,
|
||||
isSpreadMove: Boolean = false,
|
||||
targetCount: Int = 1
|
||||
): Double {
|
||||
val base = delegate.calculateDamage(move, attacker, defender, weather, criticalHit, isSpreadMove, targetCount)
|
||||
if (base <= 0.0) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
val moveData = Moves.getByName(move.id) ?: return base
|
||||
val moveTags = NewAiBattleUtils.moveTags(move.id)
|
||||
var adjusted = base
|
||||
|
||||
if ("delay_1" in moveTags) {
|
||||
adjusted *= 0.92
|
||||
}
|
||||
if ("recharge_turn" in moveTags) {
|
||||
adjusted *= 0.87
|
||||
}
|
||||
if ("ohko" in moveTags) {
|
||||
adjusted *= 0.45
|
||||
}
|
||||
if ("context_weather_sensitive" in moveTags) {
|
||||
val normalizedWeather = NewAiBattleUtils.normalizeWeatherId(weather)
|
||||
if (normalizedWeather == null) {
|
||||
adjusted *= 0.95
|
||||
}
|
||||
}
|
||||
if (moveData.power <= 0 && moveTags.any { it.startsWith("stat_") }) {
|
||||
adjusted *= 0.1
|
||||
}
|
||||
|
||||
return adjusted
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
|
||||
import com.cobblemon.mod.common.battles.InBattleMove
|
||||
import com.cobblemon.mod.common.battles.ShowdownMoveset
|
||||
|
||||
class NewDoublesDecisionMaker(
|
||||
private val moveEvaluator: NewMoveEvaluator,
|
||||
private val damageCalculator: NewDamageCalculator
|
||||
) {
|
||||
data class MoveTargetPair(
|
||||
val move: InBattleMove,
|
||||
val targetPNX: String?,
|
||||
val evaluation: NewMoveEvaluator.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) {
|
||||
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))
|
||||
continue
|
||||
}
|
||||
|
||||
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 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 } ?: return@mapNotNull null
|
||||
val damage = damageCalculator.calculateDamage(move, myPokemon, targetPokemon, battleState.weather)
|
||||
target to (damage + scoreTarget(targetPokemon, myPokemon))
|
||||
}
|
||||
return targetScores.maxByOrNull { it.second }?.first?.getPNX()
|
||||
}
|
||||
|
||||
private fun scoreTarget(target: TrackedPokemon, myPokemon: TrackedPokemon): Double {
|
||||
var score = target.threatLevel * 20.0
|
||||
if (target.cachedSpeed > myPokemon.cachedSpeed && target.currentHpPercent < 0.5) {
|
||||
score += 80.0
|
||||
}
|
||||
if (target.boosts.values.any { it >= 2 }) {
|
||||
score += 100.0
|
||||
}
|
||||
if (target.currentHpPercent < 0.35) {
|
||||
score += 50.0
|
||||
}
|
||||
return score
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.abilities.Abilities
|
||||
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 NewMoveData {
|
||||
private val statTagRegex = Regex("^stat_(atk|def|spatk|spdef|speed|accuracy|evasion)_(-?\\d+)(?:_(\\d+))?$")
|
||||
|
||||
private val knownAntiHazardsFallback = listOf("rapidspin", "defog", "tidyup", "courtchange", "mortalspin")
|
||||
private val knownAntiBoostFallback = listOf("clearsmog", "haze", "topsyturvy", "roar", "whirlwind")
|
||||
private val knownWeatherAbilityFallback = 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 "snow"
|
||||
)
|
||||
|
||||
private fun moveTags(moveId: String): Set<String> = NewAiBattleUtils.moveTags(moveId)
|
||||
|
||||
private fun allMoveIds(): List<String> = Moves.all().map { it.name.lowercase() }
|
||||
|
||||
val multiHitMoves: Map<String, Map<Int, Int>> by lazy {
|
||||
allMoveIds().mapNotNull { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
val min = tags.firstOrNull { it.startsWith("multi_hit_min_") }?.removePrefix("multi_hit_min_")?.toIntOrNull()
|
||||
val max = tags.firstOrNull { it.startsWith("multi_hit_max_") }?.removePrefix("multi_hit_max_")?.toIntOrNull()
|
||||
when {
|
||||
min != null && max != null -> moveId to mapOf(min to max)
|
||||
"multi_hit" in tags -> moveId to mapOf(2 to 5)
|
||||
else -> null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private val statusByMoveId: Map<String, String> by lazy {
|
||||
allMoveIds().mapNotNull { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
when {
|
||||
tags.any { it.startsWith("sleep_") } -> moveId to Statuses.SLEEP.showdownName
|
||||
tags.any { it.startsWith("paralysis_") } -> moveId to Statuses.PARALYSIS.showdownName
|
||||
tags.any { it.startsWith("burn_") } -> moveId to Statuses.BURN.showdownName
|
||||
tags.any { it.startsWith("freeze_") } -> moveId to Statuses.FROZEN.showdownName
|
||||
tags.any { it.startsWith("toxic_") } -> moveId to Statuses.POISON_BADLY.showdownName
|
||||
tags.any { it.startsWith("poison_") } -> moveId to Statuses.POISON.showdownName
|
||||
tags.any { it.startsWith("confusion_") } -> moveId to Statuses.CONFUSE.showdownName
|
||||
tags.any { it.startsWith("attract_") } -> moveId to Statuses.ATTRACT.showdownName
|
||||
"partiallytrapped_100" in tags -> moveId to "partiallytrapped"
|
||||
"curse_100" in tags -> moveId to "cursed"
|
||||
"leechseed_100" in tags -> moveId to "leech"
|
||||
else -> null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
fun statusForMove(moveId: String): String? = statusByMoveId[moveId.lowercase()]
|
||||
|
||||
fun isStatusMove(moveId: String): Boolean {
|
||||
val normalized = moveId.lowercase()
|
||||
val tags = moveTags(normalized)
|
||||
return "category_status" in tags || statusByMoveId.containsKey(normalized)
|
||||
}
|
||||
|
||||
val boostFromMoves: Map<String, Map<Stat, Int>> by lazy {
|
||||
val result = mutableMapOf<String, MutableMap<Stat, Int>>()
|
||||
for (moveId in allMoveIds()) {
|
||||
val tags = moveTags(moveId)
|
||||
for (tag in tags) {
|
||||
val match = statTagRegex.find(tag) ?: continue
|
||||
val stat = when (match.groupValues[1]) {
|
||||
"atk" -> Stats.ATTACK
|
||||
"def" -> Stats.DEFENCE
|
||||
"spatk" -> Stats.SPECIAL_ATTACK
|
||||
"spdef" -> Stats.SPECIAL_DEFENCE
|
||||
"speed" -> Stats.SPEED
|
||||
"accuracy" -> Stats.ACCURACY
|
||||
"evasion" -> Stats.EVASION
|
||||
else -> continue
|
||||
}
|
||||
val stages = match.groupValues[2].toIntOrNull() ?: continue
|
||||
result.computeIfAbsent(moveId) { mutableMapOf() }[stat] = stages
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
val weatherMoves: Map<String, String> by lazy {
|
||||
allMoveIds().mapNotNull { moveId ->
|
||||
val weatherTag = moveTags(moveId).firstOrNull { it.startsWith("sets_weather_") } ?: return@mapNotNull null
|
||||
moveId to weatherTag.removePrefix("sets_weather_")
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
val weatherDependentMoves: Map<String, String> by lazy {
|
||||
allMoveIds().mapNotNull { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
val required = tags.firstOrNull { it.startsWith("context_weather_") }?.removePrefix("context_weather_")
|
||||
when {
|
||||
required != null -> moveId to required
|
||||
"context_weather_sensitive" in tags -> moveId to "any"
|
||||
else -> null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
val weatherAbilities: Map<String, String> by lazy {
|
||||
val fromTags = Abilities.all()
|
||||
.map { it.name.lowercase() }
|
||||
.mapNotNull { abilityId ->
|
||||
val abilityTags = NewAiBattleUtils.abilityTags(abilityId)
|
||||
if (
|
||||
"weather_setter" !in abilityTags &&
|
||||
"weather_interaction" !in abilityTags &&
|
||||
"self_chip_weather" !in abilityTags
|
||||
) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val weather = abilityTags.firstOrNull { it.startsWith("conditional_weather_") }
|
||||
?.removePrefix("conditional_weather_")
|
||||
?: "any"
|
||||
abilityId to weather
|
||||
}
|
||||
.toMap()
|
||||
knownWeatherAbilityFallback + fromTags
|
||||
}
|
||||
|
||||
val entryHazards: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"hazard_set" in tags || tags.any { it.startsWith("sets_side_condition_") && it !in setOf("sets_side_condition_mist", "sets_side_condition_safeguard") }
|
||||
}
|
||||
}
|
||||
|
||||
val antiHazardsMoves: List<String> by lazy {
|
||||
val tagged = allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"hazard_clear" in tags || "removes_hazards" in tags || "context_anti_hazard" in tags
|
||||
}.toSet()
|
||||
(tagged + knownAntiHazardsFallback).toList().filter { Moves.getByName(it) != null }
|
||||
}
|
||||
|
||||
val antiBoostMoves: List<String> by lazy {
|
||||
val tagged = allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"clear_boosts" in tags || "anti_boost" in tags || "resets_stats" in tags || "phaze" in tags
|
||||
}.toSet()
|
||||
(tagged + knownAntiBoostFallback).toList().filter { Moves.getByName(it) != null }
|
||||
}
|
||||
|
||||
val pivotMoves: List<String> by lazy {
|
||||
allMoveIds().filter { "pivot" in moveTags(it) }
|
||||
}
|
||||
|
||||
val setupMoves: Set<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"screen_set" in tags ||
|
||||
"sets_terrain" in tags ||
|
||||
"sets_weather" in tags ||
|
||||
"sets_pseudo_weather" in tags ||
|
||||
tags.any { it.startsWith("stat_") && !it.contains("_-") && "target_self" in tags }
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
val selfRecoveryMoves: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
tags.any { it.startsWith("heal_") } && "target_self" in tags
|
||||
}
|
||||
}
|
||||
|
||||
val priorityMoves: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
moveTags(moveId).any { it.startsWith("priority_") && (it.removePrefix("priority_").toIntOrNull() ?: 0) > 0 }
|
||||
}
|
||||
}
|
||||
|
||||
val recoilMoves: List<String> by lazy {
|
||||
allMoveIds().filter { moveId ->
|
||||
val tags = moveTags(moveId)
|
||||
"recoil_move" in tags || tags.any { it.startsWith("recoil_") }
|
||||
}
|
||||
}
|
||||
|
||||
val selfKoMoves: List<String> by lazy {
|
||||
allMoveIds().filter { "self_ko" in moveTags(it) }
|
||||
}
|
||||
|
||||
val crashRiskMoves: List<String> by lazy {
|
||||
allMoveIds().filter { "crash_risk" in moveTags(it) }
|
||||
}
|
||||
|
||||
val highRiskMoves: List<String> by lazy {
|
||||
(recoilMoves + selfKoMoves + crashRiskMoves).distinct()
|
||||
}
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.battles.InBattleMove
|
||||
import kotlin.math.max
|
||||
|
||||
class NewMoveEvaluator(
|
||||
private val damageCalculator: NewDamageCalculator,
|
||||
private val opponentPredictor: NewOpponentPredictor,
|
||||
private val isDoublesMode: Boolean = false
|
||||
) {
|
||||
private val delegate = MoveEvaluator(damageCalculator.delegate, opponentPredictor.delegate, isDoublesMode)
|
||||
private val priorityRegex = Regex("^priority_(-?\\d+)$")
|
||||
private val healRegex = Regex("^heal_(\\d+)$")
|
||||
private val chanceRegex = Regex("_(\\d+)$")
|
||||
private val statRegex = Regex("^stat_(atk|def|spatk|spdef|speed|accuracy|evasion)_(-?\\d+)(?:_(\\d+))?$")
|
||||
private val recoilRegex = Regex("^recoil_(\\d+)$")
|
||||
|
||||
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 DebugDetails(
|
||||
val hasStab: Boolean,
|
||||
val effectiveness: Double,
|
||||
val accuracy: Double,
|
||||
val priority: Int,
|
||||
val isStatusMove: Boolean,
|
||||
val isSetup: Boolean,
|
||||
val setsHazards: Boolean,
|
||||
val removesHazards: Boolean,
|
||||
val setsWeather: Boolean,
|
||||
val isRecovery: Boolean,
|
||||
val isPivot: Boolean,
|
||||
val isRisky: Boolean
|
||||
)
|
||||
|
||||
fun evaluateMove(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): MoveEvaluation {
|
||||
val base = delegate.evaluateMove(move, attacker, defender, battleState, gamePlan)
|
||||
val moveTags = NewAiBattleUtils.moveTags(move.id)
|
||||
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
|
||||
val attackerAbilityTags = NewAiBattleUtils.abilityTags(attackerAbility)
|
||||
val defenderAbilityTags = NewAiBattleUtils.abilityTags(defender.currentAbility?.lowercase() ?: defender.pokemon?.ability?.name?.lowercase())
|
||||
|
||||
val utilityBonus = tagUtilityBonus(
|
||||
moveTags = moveTags,
|
||||
attacker = attacker,
|
||||
defender = defender,
|
||||
battleState = battleState,
|
||||
attackerAbilityTags = attackerAbilityTags,
|
||||
defenderAbilityTags = defenderAbilityTags
|
||||
)
|
||||
val riskPenalty = tagRiskPenalty(moveTags, attacker, battleState)
|
||||
val expectedDamage = applyTagExpectedDamageAdjustments(base.expectedDamage, moveTags, attackerAbility)
|
||||
val gamePlanBonus = tagGamePlanBonus(moveTags, gamePlan, battleState)
|
||||
|
||||
return MoveEvaluation(
|
||||
move = base.move,
|
||||
rawDamage = base.rawDamage,
|
||||
expectedDamage = expectedDamage,
|
||||
strategicValue = base.strategicValue + utilityBonus,
|
||||
riskFactor = max(0.1, base.riskFactor + riskPenalty),
|
||||
priority = max(base.priority, parsePriority(moveTags, base.priority)),
|
||||
turnAdvantage = base.turnAdvantage,
|
||||
gamePlanAlignment = base.gamePlanAlignment + gamePlanBonus
|
||||
)
|
||||
}
|
||||
|
||||
fun evaluateMoveForDoubles(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
primaryTarget: TrackedPokemon,
|
||||
allOpponents: List<TrackedPokemon>,
|
||||
partner: TrackedPokemon?,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): MoveEvaluation {
|
||||
val base = delegate.evaluateMoveForDoubles(
|
||||
move,
|
||||
attacker,
|
||||
primaryTarget,
|
||||
allOpponents,
|
||||
partner,
|
||||
battleState,
|
||||
gamePlan
|
||||
)
|
||||
val moveTags = NewAiBattleUtils.moveTags(move.id)
|
||||
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
|
||||
val attackerAbilityTags = NewAiBattleUtils.abilityTags(attackerAbility)
|
||||
|
||||
var strategic = base.strategicValue
|
||||
var risk = base.riskFactor
|
||||
if ("target_opponents" in moveTags || "target_all" in moveTags) {
|
||||
strategic += allOpponents.count { it.currentHp > 0 } * 10.0
|
||||
}
|
||||
if ("target_all" in moveTags && partner != null && partner.currentHpPercent < 0.4) {
|
||||
risk += 0.35
|
||||
}
|
||||
if ("pivot" in moveTags) {
|
||||
strategic += 12.0
|
||||
}
|
||||
if ("move_tag_damage_synergy" in attackerAbilityTags && moveTags.any { it in TAG_SYNERGY_KEYS }) {
|
||||
strategic += 14.0
|
||||
}
|
||||
|
||||
return MoveEvaluation(
|
||||
move = base.move,
|
||||
rawDamage = base.rawDamage,
|
||||
expectedDamage = applyTagExpectedDamageAdjustments(base.expectedDamage, moveTags, attackerAbility),
|
||||
strategicValue = strategic,
|
||||
riskFactor = max(0.1, risk + tagRiskPenalty(moveTags, attacker, battleState)),
|
||||
priority = max(base.priority, parsePriority(moveTags, base.priority)),
|
||||
turnAdvantage = base.turnAdvantage,
|
||||
gamePlanAlignment = base.gamePlanAlignment + tagGamePlanBonus(moveTags, gamePlan, battleState)
|
||||
)
|
||||
}
|
||||
|
||||
fun buildDebugDetails(
|
||||
move: InBattleMove,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): DebugDetails {
|
||||
val details = delegate.buildDebugDetails(move, attacker, defender, battleState, gamePlan)
|
||||
return DebugDetails(
|
||||
hasStab = details.hasStab,
|
||||
effectiveness = details.effectiveness,
|
||||
accuracy = details.accuracy,
|
||||
priority = details.priority,
|
||||
isStatusMove = details.isStatusMove,
|
||||
isSetup = details.isSetup,
|
||||
setsHazards = details.setsHazards,
|
||||
removesHazards = details.removesHazards,
|
||||
setsWeather = details.setsWeather,
|
||||
isRecovery = details.isRecovery,
|
||||
isPivot = details.isPivot,
|
||||
isRisky = details.isRisky
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyTagExpectedDamageAdjustments(
|
||||
expectedDamage: Double,
|
||||
moveTags: Set<String>,
|
||||
attackerAbilityId: String?
|
||||
): Double {
|
||||
var adjusted = expectedDamage
|
||||
if (adjusted <= 0.0) {
|
||||
return adjusted
|
||||
}
|
||||
if ("move_tag_damage_synergy" in NewAiBattleUtils.abilityTags(attackerAbilityId) &&
|
||||
moveTags.any { it in TAG_SYNERGY_KEYS }
|
||||
) {
|
||||
adjusted *= 1.1
|
||||
}
|
||||
if ("delay_1" in moveTags) adjusted *= 0.9
|
||||
if ("recharge_turn" in moveTags) adjusted *= 0.85
|
||||
return adjusted
|
||||
}
|
||||
|
||||
private fun tagUtilityBonus(
|
||||
moveTags: Set<String>,
|
||||
attacker: TrackedPokemon,
|
||||
defender: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
attackerAbilityTags: Set<String>,
|
||||
defenderAbilityTags: Set<String>
|
||||
): Double {
|
||||
var bonus = 0.0
|
||||
|
||||
if (moveTags.any { it.startsWith("heal_") } || "heal_variable" in moveTags) {
|
||||
val heal = parseHealPercent(moveTags).coerceAtLeast(25)
|
||||
bonus += (1.0 - attacker.currentHpPercent).coerceAtLeast(0.0) * heal
|
||||
if (attacker.currentHpPercent <= 0.35) bonus += 24.0
|
||||
if (attacker.currentHpPercent >= 0.9) bonus -= 10.0
|
||||
}
|
||||
|
||||
if ("target_self" in moveTags) {
|
||||
val setupGain = moveTags.mapNotNull { statRegex.find(it)?.groupValues?.getOrNull(2)?.toIntOrNull() }
|
||||
.filter { it > 0 }
|
||||
.sum()
|
||||
bonus += setupGain * 12.0
|
||||
if (battleState.gamePhase == GamePhase.EARLY_GAME || battleState.gamePhase == GamePhase.MID_GAME) {
|
||||
bonus += setupGain * 8.0
|
||||
}
|
||||
}
|
||||
|
||||
val statusChance = parseStatusChance(moveTags)
|
||||
if (statusChance > 0 && defender.pokemon?.status == null && "status_move_immunity" !in defenderAbilityTags) {
|
||||
bonus += statusChance * 0.35
|
||||
}
|
||||
|
||||
if ("hazard_set" in moveTags && battleState.playerHazards.isEmpty()) {
|
||||
bonus += 40.0
|
||||
}
|
||||
if (("hazard_clear" in moveTags || "removes_hazards" in moveTags) && battleState.npcHazards.isNotEmpty()) {
|
||||
bonus += 35.0 + battleState.npcHazards.size * 8.0
|
||||
}
|
||||
|
||||
val priority = parsePriority(moveTags, 0)
|
||||
if (priority > 0 && attacker.currentHpPercent < 0.5) {
|
||||
bonus += 10.0 + (priority * 6.0)
|
||||
}
|
||||
|
||||
if ("protect_like" in moveTags || "invincible" in moveTags) {
|
||||
if (attacker.currentHpPercent < 0.45) bonus += 20.0
|
||||
if (battleState.gamePhase == GamePhase.ENDGAME) bonus += 8.0
|
||||
}
|
||||
|
||||
if ("pivot" in moveTags) {
|
||||
bonus += if (attacker.currentHpPercent < 0.4) 20.0 else 8.0
|
||||
}
|
||||
|
||||
if ("sets_weather" in moveTags && attackerAbilityTags.any { it == "weather_interaction" || it.startsWith("conditional_weather_") }) {
|
||||
bonus += 22.0
|
||||
}
|
||||
if ("sets_terrain" in moveTags && attackerAbilityTags.any { it == "terrain_interaction" || it.startsWith("type_") }) {
|
||||
bonus += 12.0
|
||||
}
|
||||
if ("move_tag_damage_synergy" in attackerAbilityTags && moveTags.any { it in TAG_SYNERGY_KEYS }) {
|
||||
bonus += 14.0
|
||||
}
|
||||
if ("secondary_chance_boost" in attackerAbilityTags && moveTags.any { it.startsWith("secondary_chance_") }) {
|
||||
bonus += 10.0
|
||||
}
|
||||
if ("status_move_immunity" in attackerAbilityTags && "category_status" in moveTags) {
|
||||
bonus += 6.0
|
||||
}
|
||||
|
||||
return bonus
|
||||
}
|
||||
|
||||
private fun tagGamePlanBonus(moveTags: Set<String>, gamePlan: GamePlan, battleState: BattleState): Double {
|
||||
var bonus = 0.0
|
||||
when (gamePlan.winCondition) {
|
||||
WinCondition.HAZARD_CONTROL -> {
|
||||
if ("hazard_set" in moveTags) bonus += 40.0
|
||||
if ("hazard_clear" in moveTags || "removes_hazards" in moveTags) bonus += 35.0
|
||||
}
|
||||
WinCondition.RAIN_OFFENSE, WinCondition.SUN_OFFENSE, WinCondition.SAND_OFFENSE, WinCondition.WEATHER_CONTROL -> {
|
||||
if ("sets_weather" in moveTags) bonus += 32.0
|
||||
if ("context_weather_sensitive" in moveTags && battleState.weather != null) bonus += 14.0
|
||||
}
|
||||
WinCondition.SETUP_SWEEP -> {
|
||||
if ("target_self" in moveTags && moveTags.any { it.startsWith("stat_") && !it.contains("_-") }) bonus += 28.0
|
||||
}
|
||||
WinCondition.DEFENSIVE_STALL -> {
|
||||
if (moveTags.any { it.startsWith("heal_") } || "heal_variable" in moveTags) bonus += 20.0
|
||||
if (moveTags.any { it.startsWith("toxic_") || it.startsWith("poison_") }) bonus += 16.0
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
return bonus
|
||||
}
|
||||
|
||||
private fun tagRiskPenalty(moveTags: Set<String>, attacker: TrackedPokemon, battleState: BattleState): Double {
|
||||
var penalty = 0.0
|
||||
val hpRiskFactor = (1.0 - attacker.currentHpPercent).coerceAtLeast(0.1)
|
||||
|
||||
if ("self_ko" in moveTags) penalty += 1.6
|
||||
if ("crash_risk" in moveTags) penalty += 0.35
|
||||
if ("recharge_turn" in moveTags) penalty += 0.45
|
||||
if ("delay_1" in moveTags && attacker.currentHpPercent < 0.55) penalty += 0.2
|
||||
|
||||
moveTags.forEach {
|
||||
val recoil = recoilRegex.find(it)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
if (recoil != null) {
|
||||
penalty += (recoil / 100.0) * (0.8 + hpRiskFactor)
|
||||
}
|
||||
}
|
||||
|
||||
val minAccuracy = moveTags
|
||||
.filter { it.startsWith("accuracy_") && it != "accuracy_true" }
|
||||
.mapNotNull { it.removePrefix("accuracy_").toIntOrNull() }
|
||||
.minOrNull()
|
||||
if (minAccuracy != null && minAccuracy < 90) {
|
||||
penalty += ((90 - minAccuracy) / 100.0)
|
||||
}
|
||||
if (battleState.gamePhase == GamePhase.ENDGAME && "self_ko" in moveTags) {
|
||||
penalty += 0.8
|
||||
}
|
||||
|
||||
return penalty
|
||||
}
|
||||
|
||||
private fun parsePriority(tags: Set<String>, fallback: Int): Int {
|
||||
return tags.mapNotNull { priorityRegex.find(it)?.groupValues?.getOrNull(1)?.toIntOrNull() }.firstOrNull() ?: fallback
|
||||
}
|
||||
|
||||
private fun parseHealPercent(tags: Set<String>): Int {
|
||||
return tags.mapNotNull { healRegex.find(it)?.groupValues?.getOrNull(1)?.toIntOrNull() }.maxOrNull() ?: 0
|
||||
}
|
||||
|
||||
private fun parseStatusChance(tags: Set<String>): Int {
|
||||
return tags.filter {
|
||||
it.startsWith("burn_") ||
|
||||
it.startsWith("paralysis_") ||
|
||||
it.startsWith("sleep_") ||
|
||||
it.startsWith("poison_") ||
|
||||
it.startsWith("toxic_") ||
|
||||
it.startsWith("freeze_") ||
|
||||
it.startsWith("confusion_")
|
||||
}.mapNotNull { chanceRegex.find(it)?.groupValues?.getOrNull(1)?.toIntOrNull() }.maxOrNull() ?: 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG_SYNERGY_KEYS = setOf("biting", "punching", "sound", "bullet", "wind", "pulse", "dance")
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
class NewOpponentPredictor {
|
||||
internal val delegate = OpponentPredictor()
|
||||
|
||||
fun updateMoveHistory(pokemon: String, move: String, situation: String) {
|
||||
delegate.updateMoveHistory(pokemon, move, situation)
|
||||
}
|
||||
|
||||
fun updateSwitchPattern(fromPokemon: String, toPokemon: String, reason: String) {
|
||||
delegate.updateSwitchPattern(fromPokemon, toPokemon, reason)
|
||||
}
|
||||
|
||||
fun predictNextMove(pokemon: String, situation: String): Map<String, Double> {
|
||||
return delegate.predictNextMove(pokemon, situation)
|
||||
}
|
||||
|
||||
fun getSwitchProbability(pokemon: String, situation: String): Double {
|
||||
return delegate.getSwitchProbability(pokemon, situation)
|
||||
}
|
||||
|
||||
fun predictMoveHeuristically(opponent: TrackedPokemon, myPokemon: TrackedPokemon): Map<String, Double> {
|
||||
return delegate.predictMoveHeuristically(opponent, myPokemon)
|
||||
}
|
||||
|
||||
fun predictSwitchTarget(fromPokemon: String, myActivePokemon: TrackedPokemon): Map<String, Double> {
|
||||
return delegate.predictSwitchTarget(fromPokemon, myActivePokemon)
|
||||
}
|
||||
|
||||
fun categorizePlaystyle(pokemon: String): OpponentPlaystyle {
|
||||
return delegate.categorizePlaystyle(pokemon)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
delegate.reset()
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
|
||||
class NewStrategyAnalyzer {
|
||||
internal val delegate = StrategyAnalyzer()
|
||||
|
||||
fun analyzeTeamComposition(team: List<Pokemon>): GamePlan {
|
||||
return delegate.analyzeTeamComposition(team)
|
||||
}
|
||||
|
||||
fun identifyPokemonRole(pokemon: Pokemon): PokemonRole {
|
||||
return delegate.identifyPokemonRole(pokemon)
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
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.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
|
||||
class NewSwitchDecisionMaker(
|
||||
private val damageCalculator: NewDamageCalculator,
|
||||
private val opponentPredictor: NewOpponentPredictor,
|
||||
private val strategyAnalyzer: NewStrategyAnalyzer
|
||||
) {
|
||||
private val delegate = SwitchDecisionMaker(
|
||||
damageCalculator.delegate,
|
||||
opponentPredictor.delegate,
|
||||
strategyAnalyzer.delegate
|
||||
)
|
||||
|
||||
data class SwitchDecisionDiagnostics(
|
||||
val shouldSwitch: Boolean,
|
||||
val reason: String,
|
||||
val currentScore: Double,
|
||||
val bestScore: Double,
|
||||
val scoreGain: Double,
|
||||
val severeDisadvantage: Boolean,
|
||||
val strongCounterFound: Boolean,
|
||||
val currentThreat: Double,
|
||||
val bestThreat: Double,
|
||||
val currentPressure: Double,
|
||||
val bestPressure: Double,
|
||||
val bestSwitchName: String?
|
||||
)
|
||||
|
||||
private var lastDiagnostics = SwitchDecisionDiagnostics(
|
||||
shouldSwitch = false,
|
||||
reason = "No decision computed yet.",
|
||||
currentScore = 0.0,
|
||||
bestScore = 0.0,
|
||||
scoreGain = 0.0,
|
||||
severeDisadvantage = false,
|
||||
strongCounterFound = false,
|
||||
currentThreat = 1.0,
|
||||
bestThreat = 1.0,
|
||||
currentPressure = 1.0,
|
||||
bestPressure = 1.0,
|
||||
bestSwitchName = null
|
||||
)
|
||||
|
||||
fun shouldVoluntarilySwitch(
|
||||
myPokemon: TrackedPokemon,
|
||||
opponentPokemon: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan,
|
||||
availableSwitches: List<BattlePokemon>
|
||||
): Boolean {
|
||||
if (availableSwitches.isEmpty()) {
|
||||
lastDiagnostics = lastDiagnostics.copy(
|
||||
shouldSwitch = false,
|
||||
reason = "No legal bench options available."
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
val currentPokemon = myPokemon.pokemon
|
||||
if (currentPokemon == null) {
|
||||
lastDiagnostics = lastDiagnostics.copy(
|
||||
shouldSwitch = false,
|
||||
reason = "Current battler data missing."
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
val scored = availableSwitches.map { option ->
|
||||
option to evaluateSwitchOption(option.effectedPokemon, opponentPokemon, battleState, gamePlan)
|
||||
}.sortedByDescending { it.second }
|
||||
|
||||
val bestPair = scored.first()
|
||||
val bestSwitch = bestPair.first.effectedPokemon
|
||||
val bestScore = bestPair.second
|
||||
val currentScore = evaluateSwitchOption(currentPokemon, opponentPokemon, battleState, gamePlan)
|
||||
val scoreGain = bestScore - currentScore
|
||||
|
||||
val currentThreat = defensiveThreatMultiplier(opponentPokemon, currentPokemon)
|
||||
val bestThreat = defensiveThreatMultiplier(opponentPokemon, bestSwitch)
|
||||
val currentPressure = offensivePressureMultiplier(currentPokemon, opponentPokemon)
|
||||
val bestPressure = offensivePressureMultiplier(bestSwitch, opponentPokemon)
|
||||
|
||||
val mySpeed = effectiveStat(currentPokemon, Stats.SPEED)
|
||||
val oppSpeed = opponentPokemon.pokemon?.let { effectiveStat(it, Stats.SPEED) } ?: opponentPokemon.cachedSpeed
|
||||
val speedDisadvantage = oppSpeed > mySpeed * 1.08
|
||||
val lowHealth = myPokemon.currentHpPercent <= 0.55
|
||||
val opponentBoosted = opponentPokemon.boosts.values.any { it >= 2 }
|
||||
val currentPositionPoor = currentScore <= 12.0
|
||||
|
||||
val severeDisadvantage =
|
||||
(currentThreat >= 2.0 && (speedDisadvantage || lowHealth || opponentBoosted)) ||
|
||||
(currentThreat >= 1.5 && lowHealth) ||
|
||||
(opponentBoosted && currentPositionPoor) ||
|
||||
(myPokemon.currentHpPercent <= 0.35 && currentPositionPoor)
|
||||
|
||||
val threatImprovement = currentThreat - bestThreat
|
||||
val pressureImprovement = bestPressure - currentPressure
|
||||
val strongCounterFound =
|
||||
scoreGain >= 45.0 &&
|
||||
bestScore >= 70.0 &&
|
||||
currentScore <= 25.0 &&
|
||||
(
|
||||
threatImprovement >= 0.5 ||
|
||||
pressureImprovement >= 0.75 ||
|
||||
effectiveStat(bestSwitch, Stats.SPEED) > oppSpeed * 1.15
|
||||
)
|
||||
|
||||
val shouldSwitch =
|
||||
(severeDisadvantage && scoreGain >= 18.0) ||
|
||||
strongCounterFound
|
||||
|
||||
val reason = when {
|
||||
shouldSwitch && severeDisadvantage && strongCounterFound ->
|
||||
"Switching: severe matchup risk and a strong counter exists."
|
||||
shouldSwitch && severeDisadvantage ->
|
||||
"Switching: severe disadvantage in current matchup."
|
||||
shouldSwitch && strongCounterFound ->
|
||||
"Switching: found a clearly better counter from bench."
|
||||
else ->
|
||||
"Staying in: disadvantage not severe enough and no clear counter upgrade."
|
||||
}
|
||||
|
||||
lastDiagnostics = SwitchDecisionDiagnostics(
|
||||
shouldSwitch = shouldSwitch,
|
||||
reason = reason,
|
||||
currentScore = currentScore,
|
||||
bestScore = bestScore,
|
||||
scoreGain = scoreGain,
|
||||
severeDisadvantage = severeDisadvantage,
|
||||
strongCounterFound = strongCounterFound,
|
||||
currentThreat = currentThreat,
|
||||
bestThreat = bestThreat,
|
||||
currentPressure = currentPressure,
|
||||
bestPressure = bestPressure,
|
||||
bestSwitchName = bestSwitch.species.name
|
||||
)
|
||||
return shouldSwitch
|
||||
}
|
||||
|
||||
fun findBestSwitch(
|
||||
availableSwitches: List<BattlePokemon>,
|
||||
opponentPokemon: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): BattlePokemon? {
|
||||
return delegate.findBestSwitch(availableSwitches, opponentPokemon, battleState, gamePlan)
|
||||
}
|
||||
|
||||
fun evaluateSwitchOption(
|
||||
candidate: Pokemon,
|
||||
opponent: TrackedPokemon,
|
||||
battleState: BattleState,
|
||||
gamePlan: GamePlan
|
||||
): Double {
|
||||
return delegate.evaluateSwitchOption(candidate, opponent, battleState, gamePlan)
|
||||
}
|
||||
|
||||
fun latestSwitchRationale(): String {
|
||||
val d = lastDiagnostics
|
||||
return buildString {
|
||||
append(d.reason)
|
||||
append(" ")
|
||||
append(
|
||||
"currentScore=${"%.1f".format(d.currentScore)}, " +
|
||||
"bestScore=${"%.1f".format(d.bestScore)}, " +
|
||||
"gain=${"%.1f".format(d.scoreGain)}"
|
||||
)
|
||||
append(" | ")
|
||||
append(
|
||||
"currentThreat=${"%.2f".format(d.currentThreat)}, " +
|
||||
"bestThreat=${"%.2f".format(d.bestThreat)}, " +
|
||||
"currentPressure=${"%.2f".format(d.currentPressure)}, " +
|
||||
"bestPressure=${"%.2f".format(d.bestPressure)}"
|
||||
)
|
||||
append(" | ")
|
||||
append(
|
||||
"severeDisadvantage=${d.severeDisadvantage}, " +
|
||||
"strongCounterFound=${d.strongCounterFound}, " +
|
||||
"bestSwitch=${d.bestSwitchName ?: "none"}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun latestDiagnosticsSnapshot(): SwitchDecisionDiagnostics = lastDiagnostics
|
||||
|
||||
private fun offensivePressureMultiplier(attacker: Pokemon, defender: TrackedPokemon): Double {
|
||||
val defenderTypes = defender.currentTypes ?: defender.pokemon?.types?.map { it.name.lowercase() } ?: listOf()
|
||||
return bestMoveEffectiveness(attacker.moveSet.getMoves(), defenderTypes)
|
||||
}
|
||||
|
||||
private fun defensiveThreatMultiplier(opponent: TrackedPokemon, candidate: Pokemon): Double {
|
||||
val candidateTypes = candidate.types.map { it.name.lowercase() }
|
||||
val opponentMoves = opponent.moves.ifEmpty { opponent.pokemon?.moveSet?.getMoves() ?: listOf() }
|
||||
return bestMoveEffectiveness(opponentMoves, candidateTypes)
|
||||
}
|
||||
|
||||
private fun bestMoveEffectiveness(moves: List<Move>, defenderTypes: List<String>): Double {
|
||||
if (moves.isEmpty() || defenderTypes.isEmpty()) return 1.0
|
||||
return moves.maxOfOrNull { move ->
|
||||
TypeEffectiveness.calculateTypeEffectiveness(move.type, defenderTypes)
|
||||
} ?: 1.0
|
||||
}
|
||||
|
||||
private fun effectiveStat(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.types.ElementalType
|
||||
|
||||
object NewTypeEffectiveness {
|
||||
fun getDamageMultiplier(attackerType: ElementalType, defenderType: ElementalType): Double =
|
||||
TypeEffectiveness.getDamageMultiplier(attackerType, defenderType)
|
||||
|
||||
fun calculateTypeEffectiveness(moveType: ElementalType, defenderTypes: List<String>): Double =
|
||||
TypeEffectiveness.calculateTypeEffectiveness(moveType, defenderTypes)
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
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 (MoveData.boostFromMoves.containsKey(move.lowercase())) {
|
||||
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
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.BattleAITags
|
||||
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) {
|
||||
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 { move ->
|
||||
val id = move.name.lowercase()
|
||||
id in MoveData.entryHazards || "hazard_set" in BattleAITags.getMoveTags(id)
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
return GamePlan(
|
||||
winCondition = winCondition,
|
||||
keyPokemon = keyPokemon,
|
||||
priorities = priorities,
|
||||
riskyMoves = MoveData.highRiskMoves,
|
||||
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 -> MoveData.selfKoMoves
|
||||
WinCondition.REVENGE_KILLING -> MoveData.highRiskMoves
|
||||
WinCondition.DEFENSIVE_STALL -> MoveData.highRiskMoves
|
||||
else -> MoveData.selfKoMoves
|
||||
}
|
||||
|
||||
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 moves.any { moveId -> canonicalWeather(MoveData.weatherMoves[moveId]) == weather }
|
||||
}
|
||||
|
||||
private fun hasWeatherSettingAbility(pokemon: Pokemon, weather: String): Boolean {
|
||||
val ability = pokemon.ability.name.lowercase()
|
||||
val abilityTags = BattleAITags.getAbilityTags(ability)
|
||||
if ("weather_setter" !in abilityTags) {
|
||||
return false
|
||||
}
|
||||
val conditional = abilityTags.firstOrNull { it.startsWith("conditional_weather_") }?.removePrefix("conditional_weather_")
|
||||
return conditional == null || canonicalWeather(conditional) == weather
|
||||
}
|
||||
|
||||
private fun hasWeatherAbility(pokemon: Pokemon, weather: String): Boolean {
|
||||
val ability = pokemon.ability.name.lowercase()
|
||||
val abilityTags = BattleAITags.getAbilityTags(ability)
|
||||
if (abilityTags.any { it == "weather_interaction" || it == "self_chip_weather" }) {
|
||||
val conditional = abilityTags.firstOrNull { it.startsWith("conditional_weather_") }?.removePrefix("conditional_weather_")
|
||||
if (conditional == null) {
|
||||
return true
|
||||
}
|
||||
return canonicalWeather(conditional) == weather
|
||||
}
|
||||
return canonicalWeather(MoveData.weatherAbilities[ability]) == weather
|
||||
}
|
||||
|
||||
private fun hasWeatherMoves(pokemon: Pokemon, weather: String): Boolean {
|
||||
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
|
||||
return moves.any { moveId -> canonicalWeather(MoveData.weatherDependentMoves[moveId]) == weather }
|
||||
}
|
||||
|
||||
private fun canonicalWeather(raw: String?): String? {
|
||||
return when (raw?.lowercase()) {
|
||||
null -> null
|
||||
"rain", "raindance", "raining", "heavyrain" -> "rain"
|
||||
"sun", "sunny", "sunnyday", "harshsunlight", "desolateland" -> "sun"
|
||||
"sand", "sandstorm" -> "sand"
|
||||
"hail" -> "hail"
|
||||
"snow", "snowscape" -> "snow"
|
||||
else -> raw.lowercase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return checkSwitchConditions(
|
||||
myPokemon,
|
||||
opponentPokemon,
|
||||
battleState,
|
||||
gamePlan,
|
||||
currentMatchup
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
return 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)
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.cobblemon.mod.common.battles.ai.competitive
|
||||
|
||||
import com.cobblemon.mod.common.api.types.ElementalType
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.battles.ai.strongBattleAI.AIUtility
|
||||
|
||||
object TypeEffectiveness {
|
||||
fun getDamageMultiplier(attackerType: ElementalType, defenderType: ElementalType): Double {
|
||||
return AIUtility.getDamageMultiplier(attackerType, defenderType)
|
||||
}
|
||||
|
||||
fun calculateTypeEffectiveness(moveType: ElementalType, defenderTypes: List<String>): Double {
|
||||
var multiplier = 1.0
|
||||
for (typeString in defenderTypes) {
|
||||
val defenderType = ElementalTypes.get(typeString.lowercase()) ?: continue
|
||||
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
|
||||
}
|
||||
}
|
||||
+2
@@ -132,6 +132,7 @@ class TrackerActor(val uuid: UUID) {
|
||||
actor.activePokemon.filter { newActive -> activePokemon.none { it.id == newActive.battlePokemon?.uuid } }.forEach {
|
||||
val active = party.firstOrNull { trackMon -> trackMon.id == it.battlePokemon?.uuid } ?: return@forEach
|
||||
party.remove(active)
|
||||
active.firstTurn = true // Reset firstTurn when switching in
|
||||
activePokemon.add(active)
|
||||
}
|
||||
val swappedOut = activePokemon.filter { trackMon -> actor.activePokemon.none { it.battlePokemon?.uuid == trackMon.id } }
|
||||
@@ -145,6 +146,7 @@ class TrackerActor(val uuid: UUID) {
|
||||
actor.activePokemon.filter { newActive -> activePokemon.none { it.id == newActive.battlePokemon?.uuid } }.forEach {
|
||||
val active = party.firstOrNull { trackMon -> trackMon.id == it.battlePokemon?.uuid } ?: return@forEach
|
||||
party.remove(active)
|
||||
active.firstTurn = true // Reset firstTurn when switching in
|
||||
activePokemon.add(active)
|
||||
}
|
||||
val swappedOut = activePokemon.filter { trackMon -> actor.activePokemon.none { it.battlePokemon?.uuid == trackMon.id } }
|
||||
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.battles.ai.strongBattleAI
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.data.DataRegistry
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
import net.minecraft.server.packs.resources.ResourceManager
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Registry for optional tags that can be attached to moves and abilities for StrongBattleAI heuristics.
|
||||
*/
|
||||
object BattleAITags : DataRegistry {
|
||||
override val id = cobblemonResource("battle_ai_tags")
|
||||
override val type = PackType.SERVER_DATA
|
||||
override val observable = SimpleObservable<BattleAITags>()
|
||||
private val gson = GsonBuilder()
|
||||
.disableHtmlEscaping()
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
|
||||
private val moveTagsById = mutableMapOf<String, MutableSet<String>>()
|
||||
private val abilityTagsById = mutableMapOf<String, MutableSet<String>>()
|
||||
private val abilityTagMultipliersById = mutableMapOf<String, MutableMap<String, Double>>()
|
||||
|
||||
private val abilityFolders = listOf(
|
||||
"battle_ai/abilities",
|
||||
"battle_ai/abilties",
|
||||
"battleai/abilities",
|
||||
"battleai/abilties"
|
||||
)
|
||||
private val moveFolders = listOf(
|
||||
"battle_ai/moves",
|
||||
"battleai/moves"
|
||||
)
|
||||
private const val DEFAULT_BOOST_TAG_MULTIPLIER = 1.5
|
||||
|
||||
override fun sync(player: ServerPlayer) {}
|
||||
|
||||
override fun reload(manager: ResourceManager) {
|
||||
moveTagsById.clear()
|
||||
abilityTagsById.clear()
|
||||
abilityTagMultipliersById.clear()
|
||||
|
||||
loadMoveTags(manager)
|
||||
loadAbilityTags(manager)
|
||||
|
||||
Cobblemon.LOGGER.info(
|
||||
"Loaded battle AI tags: {} moves, {} abilities, {} ability-tag multipliers",
|
||||
moveTagsById.size,
|
||||
abilityTagsById.size,
|
||||
abilityTagMultipliersById.values.sumOf { it.size }
|
||||
)
|
||||
observable.emit(this)
|
||||
}
|
||||
|
||||
fun getMoveTags(moveId: String?): Set<String> {
|
||||
val normalizedMoveId = normalizeId(moveId) ?: return emptySet()
|
||||
return moveTagsById[normalizedMoveId]?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
fun getAbilityTags(abilityId: String?): Set<String> {
|
||||
val normalizedAbilityId = normalizeId(abilityId) ?: return emptySet()
|
||||
return abilityTagsById[normalizedAbilityId]?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
fun abilityMoveTagDamageMultiplier(abilityId: String?, moveId: String?): Double {
|
||||
val normalizedAbilityId = normalizeId(abilityId) ?: return 1.0
|
||||
val abilityTags = getAbilityTags(normalizedAbilityId)
|
||||
val moveTags = getMoveTags(moveId)
|
||||
if (abilityTags.isEmpty() || moveTags.isEmpty()) {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
var multiplier = 1.0
|
||||
val appliedMoveTags = mutableSetOf<String>()
|
||||
|
||||
abilityTagMultipliersById[normalizedAbilityId]?.forEach { (moveTag, tagMultiplier) ->
|
||||
if (moveTag in moveTags) {
|
||||
multiplier *= tagMultiplier
|
||||
appliedMoveTags += moveTag
|
||||
}
|
||||
}
|
||||
|
||||
abilityTags
|
||||
.asSequence()
|
||||
.filter { it.startsWith("boost_") }
|
||||
.map { it.removePrefix("boost_") }
|
||||
.filter { it.isNotBlank() && it in moveTags && it !in appliedMoveTags }
|
||||
.forEach {
|
||||
multiplier *= DEFAULT_BOOST_TAG_MULTIPLIER
|
||||
appliedMoveTags += it
|
||||
}
|
||||
|
||||
if ("boost" in abilityTags) {
|
||||
abilityTags
|
||||
.asSequence()
|
||||
.filter { it != "boost" && !it.startsWith("boost_") }
|
||||
.filter { it in moveTags && it !in appliedMoveTags }
|
||||
.forEach {
|
||||
multiplier *= DEFAULT_BOOST_TAG_MULTIPLIER
|
||||
appliedMoveTags += it
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier
|
||||
}
|
||||
|
||||
private fun normalizeId(id: String?): String? = normalizeTag(id)
|
||||
|
||||
private fun normalizeTags(tags: Iterable<String>): Set<String> {
|
||||
return tags.mapNotNull(::normalizeTag).toSet()
|
||||
}
|
||||
|
||||
private fun normalizeTag(tag: String?): String? {
|
||||
val normalizedTag = tag?.trim()?.lowercase()
|
||||
return if (normalizedTag.isNullOrBlank()) null else normalizedTag
|
||||
}
|
||||
|
||||
private fun loadMoveTags(manager: ResourceManager) {
|
||||
moveFolders.forEach { folder ->
|
||||
manager.listResources(folder) { it.path.endsWith(".json") }.forEach { (identifier, resource) ->
|
||||
val moveId = normalizeId(File(identifier.path).nameWithoutExtension) ?: return@forEach
|
||||
resource.open().use { stream ->
|
||||
stream.bufferedReader().use { reader ->
|
||||
try {
|
||||
val entry = gson.fromJson(reader, MoveTagEntry::class.java) ?: MoveTagEntry()
|
||||
val tags = normalizeTags(entry.tags)
|
||||
if (tags.isNotEmpty()) {
|
||||
moveTagsById.computeIfAbsent(moveId) { mutableSetOf() }.addAll(tags)
|
||||
}
|
||||
} catch (exception: JsonSyntaxException) {
|
||||
Cobblemon.LOGGER.error("Failed to parse move battle AI tags for {}", identifier, exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAbilityTags(manager: ResourceManager) {
|
||||
abilityFolders.forEach { folder ->
|
||||
manager.listResources(folder) { it.path.endsWith(".json") }.forEach { (identifier, resource) ->
|
||||
val abilityId = normalizeId(File(identifier.path).nameWithoutExtension) ?: return@forEach
|
||||
resource.open().use { stream ->
|
||||
stream.bufferedReader().use { reader ->
|
||||
try {
|
||||
val entry = gson.fromJson(reader, AbilityTagEntry::class.java) ?: AbilityTagEntry()
|
||||
val tags = normalizeTags(entry.tags)
|
||||
if (tags.isNotEmpty()) {
|
||||
abilityTagsById.computeIfAbsent(abilityId) { mutableSetOf() }.addAll(tags)
|
||||
}
|
||||
val tagMultipliers = entry.tagMultipliers
|
||||
.mapNotNull { (tag, multiplier) ->
|
||||
val normalizedTag = normalizeTag(tag) ?: return@mapNotNull null
|
||||
if (multiplier <= 0.0) return@mapNotNull null
|
||||
normalizedTag to multiplier
|
||||
}
|
||||
.toMap()
|
||||
if (tagMultipliers.isNotEmpty()) {
|
||||
abilityTagMultipliersById
|
||||
.computeIfAbsent(abilityId) { mutableMapOf() }
|
||||
.putAll(tagMultipliers)
|
||||
}
|
||||
} catch (exception: JsonSyntaxException) {
|
||||
Cobblemon.LOGGER.error("Failed to parse ability battle AI tags for {}", identifier, exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AbilityTagEntry(
|
||||
@SerializedName("tags")
|
||||
val tags: List<String> = emptyList(),
|
||||
@SerializedName("tag_multipliers")
|
||||
val tagMultipliers: Map<String, Double> = emptyMap()
|
||||
)
|
||||
|
||||
data class MoveTagEntry(
|
||||
@SerializedName("tags")
|
||||
val tags: List<String> = emptyList()
|
||||
)
|
||||
+8
-6
@@ -23,17 +23,19 @@ import com.cobblemon.mod.common.util.battleLang
|
||||
* @author Yaseen
|
||||
* @since April 22nd, 2023
|
||||
*/
|
||||
class ErrorInstruction(val battleActor: BattleActor, val message: BattleMessage): InterpreterInstruction {
|
||||
class ErrorInstruction(val battleActor: BattleActor, val message: BattleMessage) : InterpreterInstruction {
|
||||
|
||||
override fun invoke(battle: PokemonBattle) {
|
||||
battle.log("Error Instruction")
|
||||
battle.dispatchGo {
|
||||
//TODO: some lang stuff for the error messages (Whats the protocol for adding to other langs )
|
||||
//Also is it okay to ignore the team preview error for now? - You bet!
|
||||
val lang = when(message.rawMessage) {
|
||||
"|error|[Unavailable choice] Can't switch: The active Pokémon is trapped" -> battleLang("error.pokemon_is_trapped").red()
|
||||
// TODO: add localized messages for all error variants.
|
||||
val lang = when (message.rawMessage) {
|
||||
"|error|[Invalid choice] Can't choose for Team Preview: You're not in a Team Preview phase" -> return@dispatchGo
|
||||
"|error|[Invalid choice] Can't do anything: It's not your turn" -> return@dispatchGo
|
||||
"|error|[Invalid choice] Can't undo: A trapping/disabling effect would cause undo to leak information" -> return@dispatchGo
|
||||
"|error|[Unavailable choice] Can't switch: The active Pokémon is trapped" ->
|
||||
battleLang("error.pokemon_is_trapped").red()
|
||||
|
||||
else -> battle.createUnimplemented(message)
|
||||
}
|
||||
battleActor.sendMessage(lang)
|
||||
@@ -41,4 +43,4 @@ class ErrorInstruction(val battleActor: BattleActor, val message: BattleMessage)
|
||||
battleActor.sendUpdate(BattleMadeInvalidChoicePacket())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@ class RequestInstruction(val battleActor: BattleActor, val message: BattleMessag
|
||||
request.sanitize(battle, battleActor)
|
||||
battle.dispatchGo {
|
||||
// This request won't be acted on until the start of next turn
|
||||
battleActor.sendUpdate(BattleQueueRequestPacket(request))
|
||||
battleActor.request = request
|
||||
battleActor.sendUpdate(BattleQueueRequestPacket(request))
|
||||
battleActor.responses.clear()
|
||||
// We need to send this out because 'upkeep' isn't received until the request is handled since the turn won't swap
|
||||
if (request.forceSwitch.contains(true)) {
|
||||
@@ -49,4 +49,4 @@ class RequestInstruction(val battleActor: BattleActor, val message: BattleMessag
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+1
-10
@@ -10,17 +10,9 @@ package com.cobblemon.mod.common.battles.interpreter.instructions
|
||||
|
||||
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.scheduling.afterOnServer
|
||||
import com.cobblemon.mod.common.api.text.aqua
|
||||
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
|
||||
import com.cobblemon.mod.common.battles.dispatch.DispatchResult
|
||||
import com.cobblemon.mod.common.battles.dispatch.GO
|
||||
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleInitializePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleMusicPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleSetTeamPokemonPacket
|
||||
import com.cobblemon.mod.common.util.battleLang
|
||||
|
||||
/**
|
||||
@@ -36,10 +28,9 @@ class TurnInstruction(val message: BattleMessage) : InterpreterInstruction {
|
||||
val turnNumber = message.argumentAt(0)?.toInt() ?: return
|
||||
|
||||
battle.dispatch {
|
||||
battle.sendToActors(BattleMakeChoicePacket())
|
||||
battle.broadcastChatMessage(battleLang("turn", turnNumber).aqua())
|
||||
battle.turn(turnNumber)
|
||||
GO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,10 @@ class ApricornBlock(settings: Properties, val apricorn: Apricorn) : HorizontalDi
|
||||
override fun isBonemealSuccess(world: Level, random: RandomSource, pos: BlockPos, state: BlockState) = true
|
||||
|
||||
override fun performBonemeal(world: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
|
||||
world.setBlock(pos, state.setValue(AGE, state.getValue(AGE) + 1), 2)
|
||||
val curAge = state.getValue(AGE)
|
||||
val newAge = curAge + 1
|
||||
if (newAge > MAX_AGE) return
|
||||
world.setBlock(pos, state.setValue(AGE, newAge), UPDATE_CLIENTS)
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
@@ -186,17 +189,17 @@ class ApricornBlock(settings: Properties, val apricorn: Apricorn) : HorizontalDi
|
||||
* This uses [Block.dropResources] to handle the drops.
|
||||
* It will also reset the [BlockState] of this block at the given location to the start of growth.
|
||||
*
|
||||
* @param world The [World] the apricorn is in.
|
||||
* @param world The [Level] the apricorn is in.
|
||||
* @param state The [BlockState] of the apricorn.
|
||||
* @param pos The [BlockPos] of the apricorn.
|
||||
* @return The [BlockState] after harvest.
|
||||
*/
|
||||
fun harvest(world: Level, state: BlockState, pos: BlockPos): BlockState {
|
||||
// Uses loot tables, to change the drops use 'data/cobblemon/loot_tables/blocks/<color>_apricorn.json'
|
||||
Block.dropResources(state, world, pos)
|
||||
dropResources(state, world, pos)
|
||||
// Don't use default as we want to keep the facing
|
||||
val resetState = state.setValue(AGE, MIN_AGE)
|
||||
world.setBlock(pos, resetState, Block.UPDATE_CLIENTS)
|
||||
world.setBlock(pos, resetState, UPDATE_CLIENTS)
|
||||
return resetState
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,32 @@ class BerryBlock(private val berryIdentifier: ResourceLocation, settings: Proper
|
||||
|
||||
override fun isBonemealSuccess(world: Level, random: RandomSource, pos: BlockPos, state: BlockState) = !this.isMaxAge(state)
|
||||
|
||||
override fun <T : BlockEntity> getTicker(world: Level, blockState: BlockState, blockWithEntityType: BlockEntityType<T>): BlockEntityTicker<T>? = createTickerHelper(blockWithEntityType, CobblemonBlockEntities.BERRY, BerryBlockEntity.TICKER)
|
||||
// Use Random Tick instead of BlockEntityTicker for performance optimization
|
||||
override fun <T : BlockEntity> getTicker(world: Level, blockState: BlockState, blockWithEntityType: BlockEntityType<T>): BlockEntityTicker<T>? = null
|
||||
|
||||
// Enable Random Tick: only immature and non-rooted berries need to tick
|
||||
override fun isRandomlyTicking(state: BlockState): Boolean {
|
||||
return state.getValue(AGE) < FRUIT_AGE && !state.getValue(IS_ROOTED)
|
||||
}
|
||||
|
||||
// Random Tick handler: uses time compensation to ensure correct growth speed
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
val blockEntity = world.getBlockEntity(pos) as? BerryBlockEntity ?: return
|
||||
|
||||
// processTick calculates elapsed time and updates timer, returns whether growth is needed
|
||||
if (blockEntity.processTick(world, pos, state) && !world.getBlockTicks().hasScheduledTick(pos, this)) {
|
||||
// Schedule growth for next tick to avoid tick scheduler conflicts during randomTick
|
||||
world.scheduleTick(pos, this, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduled tick handler: performs the actual growth
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun tick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
if (state.getValue(IS_ROOTED)) return
|
||||
growHelper(world, random, pos, state)
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
|
||||
@@ -21,6 +21,7 @@ import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.DirectionalBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.Property
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
@@ -42,6 +43,8 @@ abstract class GrowableStoneBlock(
|
||||
val nextStage: Block?
|
||||
) : DirectionalBlock(settings) {
|
||||
|
||||
abstract val growthChance: Int
|
||||
|
||||
private val upShape: VoxelShape = Block.box(
|
||||
xzOffset.toDouble(),
|
||||
0.0,
|
||||
@@ -95,20 +98,26 @@ abstract class GrowableStoneBlock(
|
||||
.setValue(FACING, Direction.DOWN))
|
||||
}
|
||||
|
||||
abstract fun canGrow(pos: BlockPos, world: BlockGetter): Boolean
|
||||
abstract fun canGrow(state: BlockState, pos: BlockPos, world: BlockGetter): Boolean
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(FACING)
|
||||
}
|
||||
|
||||
override fun isRandomlyTicking(state: BlockState): Boolean = stage < MAX_STAGE
|
||||
|
||||
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
if (world.random.nextInt(5) == 0 && canGrow(pos, world)) {
|
||||
if (world.random.nextInt(growthChance) == 0 && canGrow(state, pos, world)) {
|
||||
val block = nextStage
|
||||
|
||||
if (block != null) {
|
||||
val newState = block.defaultBlockState().setValue(FACING, state.getValue(FACING)) as BlockState
|
||||
var newState = block.defaultBlockState()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (property in state.properties) {
|
||||
if (newState.hasProperty(property)) {
|
||||
newState = newState.setValue(property as Property<Comparable<Any>>, state.getValue(property) as Comparable<Any>)
|
||||
}
|
||||
}
|
||||
|
||||
world.setBlockAndUpdate(pos, newState)
|
||||
}
|
||||
}
|
||||
@@ -155,14 +164,4 @@ abstract class GrowableStoneBlock(
|
||||
else -> upShape
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val STAGE_0 = 0
|
||||
const val STAGE_1 = 1
|
||||
const val STAGE_2 = 2
|
||||
const val STAGE_3 = 3
|
||||
|
||||
const val MAX_STAGE = STAGE_3
|
||||
const val MIN_STAGE = STAGE_0
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@ import com.mojang.serialization.MapCodec
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.util.StringRepresentable
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResult
|
||||
import net.minecraft.world.ItemInteractionResult
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.Level
|
||||
@@ -28,6 +33,7 @@ import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty
|
||||
import net.minecraft.world.level.pathfinder.PathComputationType
|
||||
import net.minecraft.world.phys.BlockHitResult
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
@@ -35,7 +41,8 @@ import net.minecraft.world.phys.shapes.VoxelShape
|
||||
class MonitorBlock(settings: Properties) : MultiblockBlock(settings) {
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(HORIZONTAL_FACING, Direction.NORTH))
|
||||
.setValue(HORIZONTAL_FACING, Direction.NORTH)
|
||||
.setValue(SCREEN, MonitorScreen.OFF))
|
||||
}
|
||||
|
||||
override fun createMultiBlockEntity(
|
||||
@@ -55,6 +62,47 @@ class MonitorBlock(settings: Properties) : MultiblockBlock(settings) {
|
||||
return defaultBlockState().setValue(HORIZONTAL_FACING, blockPlaceContext.horizontalDirection)
|
||||
}
|
||||
|
||||
override fun useWithoutItem(
|
||||
state: BlockState,
|
||||
level: Level,
|
||||
pos: BlockPos,
|
||||
player: Player,
|
||||
hit: BlockHitResult
|
||||
): InteractionResult {
|
||||
val entity = level.getBlockEntity(pos) as? FossilMultiblockEntity ?: return InteractionResult.PASS
|
||||
return entity.handleUseWithoutItem(state, level, pos, player)
|
||||
}
|
||||
|
||||
override fun useItemOn(
|
||||
stack: ItemStack,
|
||||
state: BlockState,
|
||||
level: Level,
|
||||
pos: BlockPos,
|
||||
player: Player,
|
||||
hand: InteractionHand,
|
||||
hit: BlockHitResult
|
||||
): ItemInteractionResult {
|
||||
val entity = level.getBlockEntity(pos) as? FossilMultiblockEntity
|
||||
?: return ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION
|
||||
val result = entity.handleUseItem(stack, state, level, pos, player, hand)
|
||||
if (result == ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION) {
|
||||
return if (entity.handleUseWithoutItem(state, level, pos, player).consumesAction()) {
|
||||
ItemInteractionResult.sidedSuccess(level.isClientSide)
|
||||
} else {
|
||||
ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onRemove(state: BlockState, level: Level, pos: BlockPos, newState: BlockState, movedByPiston: Boolean) {
|
||||
if (state.block != newState.block && !level.isClientSide && !movedByPiston) {
|
||||
val entity = level.getBlockEntity(pos) as? FossilMultiblockEntity
|
||||
entity?.dropDisk(level, pos, state)
|
||||
}
|
||||
super.onRemove(state, level, pos, newState, movedByPiston)
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(HORIZONTAL_FACING)
|
||||
builder.add(SCREEN)
|
||||
@@ -143,8 +191,27 @@ class MonitorBlock(settings: Properties) : MultiblockBlock(settings) {
|
||||
BLUE_PROGRESS_7,
|
||||
BLUE_PROGRESS_8,
|
||||
BLUE_PROGRESS_9,
|
||||
GREEN_PROGRESS_9;
|
||||
GREEN_PROGRESS_9,
|
||||
TM_NORMAL,
|
||||
TM_FIRE,
|
||||
TM_WATER,
|
||||
TM_GRASS,
|
||||
TM_ELECTRIC,
|
||||
TM_ICE,
|
||||
TM_FIGHTING,
|
||||
TM_POISON,
|
||||
TM_GROUND,
|
||||
TM_FLYING,
|
||||
TM_PSYCHIC,
|
||||
TM_BUG,
|
||||
TM_ROCK,
|
||||
TM_GHOST,
|
||||
TM_DRAGON,
|
||||
TM_DARK,
|
||||
TM_STEEL,
|
||||
TM_FAIRY,
|
||||
MUSIC;
|
||||
|
||||
override fun getSerializedName(): String = this.name.lowercase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.block
|
||||
|
||||
import com.cobblemon.mod.common.block.entity.TMShelfBlockEntity
|
||||
import com.mojang.serialization.MapCodec
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResult
|
||||
import net.minecraft.world.ItemInteractionResult
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.block.*
|
||||
import net.minecraft.world.level.block.entity.BlockEntity
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty
|
||||
import net.minecraft.world.Containers
|
||||
import net.minecraft.world.phys.BlockHitResult
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class TMShelfBlock(properties: Properties) : BaseEntityBlock(properties) {
|
||||
companion object {
|
||||
val CODEC: MapCodec<TMShelfBlock> = simpleCodec(::TMShelfBlock)
|
||||
val FACING: DirectionProperty = HorizontalDirectionalBlock.FACING
|
||||
val SLOT_OCCUPIED_PROPERTIES = (0 until 14).map {
|
||||
BooleanProperty.create("slot_${it}_occupied")
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val baseState = stateDefinition.any().setValue(FACING, Direction.NORTH)
|
||||
registerDefaultState(SLOT_OCCUPIED_PROPERTIES.fold(baseState) { state, prop ->
|
||||
state.setValue(prop, false)
|
||||
})
|
||||
}
|
||||
|
||||
override fun codec() = CODEC
|
||||
|
||||
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity {
|
||||
return TMShelfBlockEntity(pos, state)
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(FACING)
|
||||
SLOT_OCCUPIED_PROPERTIES.forEach { builder.add(it) }
|
||||
}
|
||||
|
||||
override fun getRenderShape(state: BlockState): RenderShape = RenderShape.MODEL
|
||||
|
||||
override fun getStateForPlacement(ctx: BlockPlaceContext): BlockState {
|
||||
return defaultBlockState().setValue(FACING, ctx.horizontalDirection.opposite)
|
||||
}
|
||||
|
||||
override fun useWithoutItem(
|
||||
state: BlockState, level: Level, pos: BlockPos,
|
||||
player: Player, hit: BlockHitResult
|
||||
): InteractionResult {
|
||||
val entity = level.getBlockEntity(pos) as? TMShelfBlockEntity ?: return InteractionResult.PASS
|
||||
return entity.handleUseWithoutItem(state, level, pos, player, hit)
|
||||
}
|
||||
|
||||
override fun useItemOn(
|
||||
stack: ItemStack, state: BlockState, level: Level, pos: BlockPos,
|
||||
player: Player, hand: InteractionHand, hit: BlockHitResult
|
||||
): ItemInteractionResult {
|
||||
val entity = level.getBlockEntity(pos) as? TMShelfBlockEntity ?: return ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION
|
||||
return entity.handleUseItem(stack, state, level, pos, player, hit)
|
||||
}
|
||||
|
||||
override fun hasAnalogOutputSignal(state: BlockState) = true
|
||||
|
||||
override fun getAnalogOutputSignal(state: BlockState, level: Level, pos: BlockPos): Int {
|
||||
val entity = level.getBlockEntity(pos) as? TMShelfBlockEntity
|
||||
return entity?.items?.count { !it.isEmpty }?.coerceIn(0, 14) ?: 0
|
||||
}
|
||||
|
||||
override fun onRemove(state: BlockState, level: Level, pos: BlockPos, newState: BlockState, movedByPiston: Boolean) {
|
||||
if (!state.`is`(newState.block)) {
|
||||
if (!level.isClientSide && !movedByPiston) {
|
||||
val entity = level.getBlockEntity(pos) as? TMShelfBlockEntity
|
||||
if (entity != null) {
|
||||
for (index in entity.items.indices) {
|
||||
val stack = entity.items[index]
|
||||
if (!stack.isEmpty) {
|
||||
Containers.dropItemStack(level, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, stack)
|
||||
entity.items[index] = ItemStack.EMPTY
|
||||
}
|
||||
}
|
||||
level.updateNeighbourForOutputSignal(pos, this)
|
||||
}
|
||||
}
|
||||
super.onRemove(state, level, pos, newState, movedByPiston)
|
||||
} else {
|
||||
super.onRemove(state, level, pos, newState, movedByPiston)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getShape(
|
||||
state: BlockState, level: BlockGetter,
|
||||
pos: BlockPos, context: net.minecraft.world.phys.shapes.CollisionContext
|
||||
): VoxelShape = Block.box(0.0, 0.0, 0.0, 16.0, 16.0, 16.0)
|
||||
}
|
||||
@@ -44,15 +44,25 @@ class TumblestoneBlock(
|
||||
PrimitiveCodec.INT.fieldOf("xzOffset").forGetter { it.xzOffset },
|
||||
Block.CODEC.fieldOf("nextStage").forGetter { it.nextStage }
|
||||
).apply(it, ::TumblestoneBlock) }
|
||||
|
||||
const val STAGE_0 = 0
|
||||
const val STAGE_1 = 1
|
||||
const val STAGE_2 = 2
|
||||
const val STAGE_3 = 3
|
||||
|
||||
const val MAX_STAGE = STAGE_3
|
||||
const val MIN_STAGE = STAGE_0
|
||||
}
|
||||
|
||||
override val growthChance = 5
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.DOWN)
|
||||
.setValue(WATERLOGGED, false))
|
||||
}
|
||||
|
||||
override fun canGrow(pos: BlockPos, world: BlockGetter): Boolean {
|
||||
override fun canGrow(state: BlockState, pos: BlockPos, world: BlockGetter): Boolean {
|
||||
if (stage == MAX_STAGE) return false
|
||||
val iterator: Iterator<BlockPos> =
|
||||
BlockPos.betweenClosed(pos.offset(-1, -1, -1), pos.offset(1, 1, 1))
|
||||
@@ -66,6 +76,8 @@ class TumblestoneBlock(
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isRandomlyTicking(state: BlockState): Boolean = stage < MAX_STAGE
|
||||
|
||||
override fun codec(): MapCodec<out DirectionalBlock> {
|
||||
return CODEC
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user