Compare commits

...
93 Commits
Author SHA1 Message Date
Plastered_Crab 071f05337c Scale tab in Implementation GUI moves test wool in X direction based on hitbox size of pokemon to prevent overlap 2026-03-02 15:47:59 -08:00
Plastered_Crab e9d274fbfd Scale tab in Implementation GUI renders test wool block nicely 2026-03-02 15:23:45 -08:00
Plastered_Crab 5709527bf2 Merge branch 'main' into ta-daaa 2026-03-02 13:17:58 -08:00
Polymeta e6d2430405 Merge branch 'BWG-tag-updates' into 'main'
Fixes to BWG compat.

See merge request cable-mc/cobblemon!2172
2026-03-02 09:42:03 +00:00
Veraxiel 6c425a43c6 Fixes to BWG compat. 2026-03-01 22:37:30 -05:00
Polymeta a505bad554 [skip-ci] tidy up changelog 2026-02-27 10:56:57 +00:00
Polymeta e27e5a1d92 Merge branch 'ai-switch-fix' into 'main'
Stop the constant switching from AI NPC Trainers

See merge request cable-mc/cobblemon!2128
2026-02-27 10:55:00 +00:00
Hiroku 5e0357101d Merge branch 'feat/fishing-spawner-factory' into 'main'
PokeSnack and Fishing Spawner factories

See merge request cable-mc/cobblemon!2136
2026-02-27 02:11:53 +00:00
Andy 5420f7419f re-run licenseFormat 2026-02-26 19:14:03 -05:00
Andy 43ed2d3c7d local backmerge 2026-02-26 19:12:17 -05:00
Andy 93500f59ed Remove changelog lines from backmerge and run licenseFormat 2026-02-26 19:10:45 -05:00
Polymeta 08912fd53f Merge branch '#1945-brewing-stand-crash' into 'main'
#1945 Brewing Stand Shift Click Crash NeoForge 1.7.3 [1.21.1]

See merge request cable-mc/cobblemon!2163
2026-02-27 00:08:36 +00:00
CyrusandPolymeta 92c8fc0afc #1945 Brewing Stand Shift Click Crash NeoForge 1.7.3 [1.21.1] 2026-02-27 00:08:36 +00:00
Polymeta fae6bdb782 Merge branch 'main' into 'main'
Move BYG compat to Biomes We've Gone and Nether Descent

See merge request cable-mc/cobblemon!2149
2026-02-26 22:52:33 +00:00
J.T. McQuiggandPolymeta bba0100542 Move BYG compat to Biomes We've Gone and Nether Descent 2026-02-26 22:52:33 +00:00
Andy f9b66de742 Add fishing spawner factory to register influences to 2026-02-26 15:54:09 +00:00
Andy 3d0f1c826a Merge branch 'main' into feat/fishing-spawner-factory
update with cobblemon head
2026-02-26 10:52:48 -05:00
Andy 2a59add285 Merge branch cobblemon:main into main 2026-02-26 15:50:16 +00:00
Polymeta 0b83128aa0 Merge branch 'main' into 'main'
Fixes brushing for neoforge

Closes #1939

See merge request cable-mc/cobblemon!2154
2026-02-26 15:43:03 +00:00
Polymeta e2b04ff770 Merge branch 'ufo-fixes-and-improvements' into 'main'
UFO Fixes and Improvements

See merge request cable-mc/cobblemon!2156
2026-02-26 15:29:38 +00:00
Jackson BowesandPolymeta 4dabe50d43 UFO Fixes and Improvements 2026-02-26 15:29:37 +00:00
Hugin Juliansson Zachariasen 3dd0795b88 Merge branch cobblemon:main into main 2026-02-26 14:14:56 +00:00
Polymeta 70f1a8207f fix a apricorn bonemeal crash
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2026-02-26 12:17:32 +00:00
Polymeta 3ac18cce6e Merge branch 'main' into 'main'
Berry Growth Performance Optimization

See merge request cable-mc/cobblemon!2142
2026-02-26 12:15:32 +00:00
Polymeta aa0a0672d0 Merge branch 'show-my-pokemon-exp-gain' into 'main'
Party Overlay EXP display and evoltion unlocked Sound

See merge request cable-mc/cobblemon!2140
2026-02-26 11:59:48 +00:00
GertimoshkaandPolymeta ec2958400b Party Overlay EXP display and evoltion unlocked Sound 2026-02-26 11:59:48 +00:00
Polymeta fcb6bea78b Merge branch 'selecting-starters-in-style' into 'main'
The Cooler Starter Selection GUI

See merge request cable-mc/cobblemon!2164
2026-02-26 11:58:31 +00:00
GertimoshkaandPolymeta 5a76a39b77 The Cooler Starter Selection GUI 2026-02-26 11:58:31 +00:00
Jackson Bowes b5a4acfb7a Merge branch 'weight-influenced-collision' into 'main'
Weight influenced collision

See merge request cable-mc/cobblemon!2158
2026-02-26 01:49:56 -06:00
Jackson Bowes 1248230e67 Merge branch 'pasture-wander' into 'main'
No more pastured mons despawning

See merge request cable-mc/cobblemon!2155
2026-02-26 01:47:17 -06:00
Jackson Bowes 096db6cdcd No more pastured mons despawning 2026-02-26 01:47:16 -06:00
Jackson Bowes 72e416b9c7 Merge branch 'sub-drifting-fix' into 'main'
Submarine bug fixes

See merge request cable-mc/cobblemon!2157
2026-02-26 01:44:58 -06:00
Polymeta 9e8e80965b Merge branch 'fix/missing-botanypots-entries' into 'main'
Updated Botany Pots integration

See merge request cable-mc/cobblemon!2166
2026-02-26 07:41:03 +00:00
GitoidoandPolymeta 2325b436b1 Updated Botany Pots integration 2026-02-26 07:41:02 +00:00
Jackson Bowes 57421a2884 Merge branch 'scrolling_textures' into 'main'
Scrolling Texture Layer

See merge request cable-mc/cobblemon!2168
2026-02-25 21:54:47 -06:00
jackowes 4fdd3eba8e - changelog entry 2026-02-25 20:24:29 -06:00
jackowes e6efe800c0 - Clean up mixin
- add changelog entry
2026-02-25 19:45:52 -06:00
Polymeta 95aa99e18c Merge branch 'fix/particle-disapearing' into 'main'
fix: adapting the boundingBox to the visual if it's a beam it should have a...

See merge request cable-mc/cobblemon!2167
2026-02-26 01:16:09 +00:00
TerrainwaxandPolymeta 270484e745 fix: adapting the boundingBox to the visual if it's a beam it should have a... 2026-02-26 01:16:09 +00:00
Polymeta 719addf633 Merge branch 'molang-or-dishonor' into 'main'
Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions

See merge request cable-mc/cobblemon!2159
2026-02-25 23:05:54 +00:00
Polymeta 31e386871f Merge branch 'little-dose-of-molang' into 'main'
Enchanting Molang

See merge request cable-mc/cobblemon!2162
2026-02-25 23:00:15 +00:00
Tyzillion bfd7e596d0 changelog 2026-02-25 17:52:39 -05:00
Tyzillion 643545d54c Added the ability to set a layer in the resolver as a scrolling texture. 2026-02-25 16:41:50 -05:00
Tyzillion fcb017f980 New Molang functions to check if an item is enchanted or has a certain enchantment. 2026-02-23 08:48:51 -05:00
Plastered_Crab bc9f28811d Implementation Tab Animation option improvements 2026-02-19 13:43:41 -08:00
Plastered_Crab ff9eb60cd3 Implementation Tab Animation option improvements 2026-02-19 12:39:39 -08:00
Spicy dd52da7c42 oops 2026-02-18 20:57:23 -06:00
Spicy ff45def0e8 Added has_chosen_starter & get_starter_uuid as available Molang functions 2026-02-18 20:42:40 -06:00
Plastered_Crab de0b53e529 Implementation tabs updated. Walk Speed tab blocks rendering 2026-02-18 16:15:05 -08:00
jackowes f078584e56 - Add weight influenced collision 2026-02-18 15:24:43 -06:00
jackowes a11382b5a1 - Braking now works on Submarine (whoops).
- Strafing now has the same friction as forward movement (funny whoops).
- Submarine's surfacewater animations now play.
- Submarine camera no longer freaks out when looking up or down on the surface.
2026-02-17 20:28:55 -06:00
Plastered_Crab 56ecbfe3c0 Move tab testing 2026-02-16 15:45:35 -08:00
Plastered_Crab 62f03c536d Polishing up of the General Tab 2026-02-16 12:02:54 -08:00
Plastered_Crab 2fa03b6013 Adjust position of elements 2026-02-16 08:40:53 -08:00
Plastered_Crab 59af4053a1 Held Item Item Slot Searching 2026-02-15 22:09:33 -08:00
Plastered_Crab 1aba8c52b6 Held Item Item Slot Renders now 2026-02-15 21:11:23 -08:00
Plastered_Crab 6a386937c8 Held Item Item Slot 2026-02-15 20:01:58 -08:00
Plastered_Crab 00132f6428 Cosmetic Item Slot 2026-02-15 19:10:55 -08:00
Plastered_Crab 7fe222422b Cosmetic Item filtering 2026-02-15 18:37:11 -08:00
Plastered_Crab 812f389231 Cosmetic Item fixes 2026-02-15 18:08:17 -08:00
Plastered_Crab 24acddf750 Aspects Tab adjustments 2026-02-15 16:22:51 -08:00
Plastered_Crab 1d696c58be Abra kadabra 2026-02-15 10:59:27 -08:00
Paul ef4fbecad6 Merge remote-tracking branch 'origin/1.8.0' into 1.8.0 2026-02-14 14:30:41 +10:00
Paul 96075ed34a Merge remote-tracking branch 'origin/main' into 1.8.0 2026-02-14 14:30:11 +10:00
Hugin Juliansson Zachariasen 155b4b2182 Added changelog doc 2026-02-13 17:50:46 +00:00
Faering666 690ed845b2 Fixes brushing for neoforge 2026-02-13 15:55:51 +01:00
Konoha 1f8171dd94 Experimental Hanging fix 2026-02-03 13:51:41 -05:00
Polymeta 3c2bf0e2b3 Merge branch 'cobblemon-structure' into '1.8.0'
Slippy can Cook now!

See merge request cable-mc/cobblemon!2144
2026-01-30 09:45:31 +00:00
AramandPolymeta ac695b1092 Slippy can Cook now! 2026-01-30 09:45:31 +00:00
Konoha b3ed47fbca Adjust thresholds and scoring 2026-01-26 15:00:50 -05:00
Loopiwc 3a6ad70b76 fix: Use scheduleTick in randomTick to avoid tick scheduler NPE
- Schedule growth to next tick instead of direct setBlock in randomTick
- Add hasScheduledTick check to prevent duplicate scheduling
- Add IS_ROOTED guard in tick() handler
2026-01-27 03:35:11 +08:00
Loopiwc 8120270bf8 perf: Replace BlockEntityTicker with Random Tick + time compensation for Berry growth
- Remove per-tick BlockEntityTicker to reduce server load
- Add processTick() method with gameTime-based compensation
- Track lastTickTime in NBT for accurate growth calculation
- Berry growth timing remains accurate via elapsed time batching
2026-01-26 16:30:43 +08:00
Andy 70fe1682b0 remove whitespace in pokesnack block entity 2026-01-23 10:49:24 -05:00
Andy 9058bde326 remove whitespace from changelog 2026-01-23 10:41:52 -05:00
Andy bf425efd40 Merge branch 'feat/snack-spawner-factory' into 'main'
Added PokeSnackSpawnerFactory so influences can be registered to pokesnacks

See merge request ppVon/cobblemon!1
2026-01-23 15:39:21 +00:00
Andy cebff87656 update changelog 2026-01-23 03:49:09 -05:00
Andy d24fb13877 Add fishing spawner factory to register influences to 2026-01-23 03:47:34 -05:00
Andy 1d82df464a Added PokeSnackSpawnerFactory so influences can be registered to pokesnacks 2026-01-23 02:19:02 -05:00
Polymeta f450d9d2d6 Merge branch 'fix-bucket-reuse' into '1.8.0'
Fixed multi-spawns in a single pass using the same bucket for all of them

See merge request cable-mc/cobblemon!2104
2026-01-22 13:21:39 +00:00
Paul 0752ab3cac Fixed woopsies in changelog 2026-01-22 22:23:01 +10:00
Konoha 2a1d1e4474 fix: AI constant switching 2026-01-20 21:09:30 -05:00
Hiroku fe35a63769 Merge branch 'complex-movesets' into '1.8.0'
Complex movesets, parties, and pools

See merge request cable-mc/cobblemon!2086
2026-01-19 04:09:56 +00:00
Paul 95d238ada0 Fixed multi-spawns in a single pass using the same bucket for all of them 2026-01-02 22:38:46 +10:00
Paul aafdb7688a Added more molang expressions to a bunch of composed pool properties, fixed some bugs 2025-12-15 14:22:59 +10:00
Paul 73b53f9702 Added egg move selector 2025-12-15 12:07:06 +10:00
Paul 33317a57c7 more doco changes [skip-ci] 2025-12-14 20:33:34 +10:00
Paul 72d8f26cdb Change silly default example weight to the actual default weight 2025-12-14 20:27:30 +10:00
Paul aea1afe145 Rearrange doco 2025-12-14 20:26:21 +10:00
Paul 0fec45b7ea Added npcAspects condition for party pool entries 2025-12-14 20:24:52 +10:00
Paul af98b823aa Documentation 2025-12-14 20:13:04 +10:00
Paul 07d9da2ca9 Added last_suitable_offensive move selector, added alpha moveset builder 2025-12-14 17:29:47 +10:00
Paul c2c86b2ef1 Stack all the cobblemon:move_weights.json from any datapacks onto one big map, like tags 2025-12-14 17:05:58 +10:00
Paul 22733e7117 Added more advanced moveset and party construction for NPCs 2025-12-08 19:31:48 +10:00
389 changed files with 11457 additions and 2271 deletions
+20
View File
@@ -0,0 +1,20 @@
# Changelog
## [1.8.0 (MONTH Xth, 2026)](#1-8-0)
### Additions
- Added a new `cobblemon:structure`, which is a feature without any restrictions allowing the Builders to place certain structures in much more specific manner.
### Changes
### Fixes
- Fixed rare Pokémon being far too likely to spawn multiple times in a single spawn cycle. The dev told me that the exact bug is too complicated to explain.
### Developer
- Tweaked the spawning selector interface to take a bucket function rather than a bucket directly.
### Molang & Datapacks
### Particles
### Localization
- Updated translations for:
+16
View File
@@ -0,0 +1,16 @@
### Additions
### Changes
- Wild Pokémon now reliably have at least one offensive move, if they can have offensive moves at that level.
### Fixes
### Developer
### Molang & Datapacks
- Added `moveset_builders` datapack folder for different styles of movesets.
- Added `party_pools` datapack folder for defining groups of Pokémon for the purposes of complex NPC party definitions.
- Added `party_compositions` datapack folder for arranging parties on NPCs that rely on party pools.
- Added `composed_pool` NPC party provider type which combines a party pool with a party composition to create more complex NPC parties.
### Particles
+23
View File
@@ -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,16 +16,36 @@
### 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
### 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
### Localization
- Updated translations for:
-1
View File
@@ -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.
@@ -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(
@@ -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());
}
}
@@ -74,6 +74,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val NPC_EDITOR = create("npc_editor", CobblemonItem(Item.Properties().stacksTo(1)))
@JvmField
val POKEEDIT_WAND = create("pokeedit_wand", PokeEditWandItem())
@JvmField
val pokeBalls = mutableListOf<PokeBallItem>()
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.moves.MoveSelector
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderAdapter
import com.cobblemon.mod.common.util.adapters.StringToObjectAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonMovesetBuilders : JsonDataRegistry<MovesetBuilder> {
override val gson: Gson = GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderAdapter)
.registerTypeAdapter(MoveSelector::class.java, StringToObjectAdapter(MoveSelector.selectors))
.create()
override val typeToken = TypeToken.get(MovesetBuilder::class.java)
override val resourcePath = "moveset_builders"
override val id: ResourceLocation = cobblemonResource("moveset_builders")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<CobblemonMovesetBuilders>()
val movesetBuilders = mutableMapOf<ResourceLocation, MovesetBuilder>()
fun getOrThrow(id: ResourceLocation): MovesetBuilder {
return movesetBuilders[id]
?: throw IllegalArgumentException("Unknown MovesetBuilder id: $id")
}
override fun sync(player: ServerPlayer) { /* These don't sync. But what if they did? Nah probably shouldn't sync. */ }
override fun reload(data: Map<ResourceLocation, MovesetBuilder>) {
movesetBuilders.clear()
data.forEach { (id, value) -> value.id = id }
movesetBuilders.putAll(data)
observable.emit(this)
}
}
@@ -170,6 +170,7 @@ import com.cobblemon.mod.common.net.messages.server.pasture.UnpastureAllPokemonP
import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.StartScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetActiveMarkPacket
@@ -227,6 +228,7 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHa
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokeedit.PokeEditActionHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
@@ -561,6 +563,9 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(StartScanningPacket.ID, StartScanningPacket::decode, StartScanningHandler))
list.add(PacketRegisterInfo(FinishScanningPacket.ID, FinishScanningPacket::decode, FinishScanningHandler))
// PokeEdit actions
list.add(PacketRegisterInfo(PokeEditActionPacket.ID, PokeEditActionPacket::decode, PokeEditActionHandler))
// Pasture
list.add(PacketRegisterInfo(PasturePokemonPacket.ID, PasturePokemonPacket::decode, PasturePokemonHandler))
list.add(PacketRegisterInfo(UnpasturePokemonPacket.ID, UnpasturePokemonPacket::decode, UnpasturePokemonHandler))
@@ -619,4 +624,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.npc.PartyComposition
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonPartyCompositions : JsonDataRegistry<PartyComposition> {
override val id: ResourceLocation = cobblemonResource("party_compositions")
override val type = PackType.SERVER_DATA
override val typeToken: TypeToken<PartyComposition> = TypeToken.get(PartyComposition::class.java)
override val resourcePath: String = "party_compositions"
override val observable = SimpleObservable<CobblemonPartyCompositions>()
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.setPrettyPrinting()
.create()
val partyCompositions = mutableMapOf<ResourceLocation, PartyComposition>()
override fun sync(player: ServerPlayer) {
// probably worth syncing which exist, yeah. the deeper details probably not. I'll get to this eventually.
}
override fun reload(data: Map<ResourceLocation, PartyComposition>) {
data.entries.forEach { it.value.id = it.key }
partyCompositions.clear()
partyCompositions.putAll(data)
observable.emit(this)
Cobblemon.LOGGER.info("Loaded ${data.size} party compositions.")
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderReferenceAdapter
import com.cobblemon.mod.common.util.adapters.PokemonPropertiesAdapter
import com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonPartyPools : JsonDataRegistry<PartyPool> {
override val id: ResourceLocation = cobblemonResource("party_pools")
override val type = PackType.SERVER_DATA
override val typeToken: TypeToken<PartyPool> = TypeToken.get(PartyPool::class.java)
override val resourcePath: String = "party_pools"
override val observable = SimpleObservable<CobblemonPartyPools>()
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, PokemonPropertiesAdapter(saveLong = false))
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(ScriptableIntRange::class.java, ScriptableIntRangeAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.setPrettyPrinting()
.create()
val partyPools = mutableMapOf<ResourceLocation, PartyPool>()
override fun sync(player: ServerPlayer) {
// probably worth syncing. Knowing the details might help exploit the server? lot of effort for little though.
}
override fun reload(data: Map<ResourceLocation, PartyPool>) {
data.entries.forEach { it.value.id = it.key }
partyPools.clear()
partyPools.putAll(data)
observable.emit(this)
}
}
@@ -17,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,
@@ -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
}
)
@@ -1929,8 +1941,7 @@ object MoLangFunctions {
return@put DoubleValue(if (pokemon.addRideBoost(stat, value)) 1.0 else 0.0)
}
map.put("initialize_moveset") { params ->
val preferLatest = params.getBooleanOrNull(0) ?: true
pokemon.initializeMoveset(preferLatest)
pokemon.initializeMovesetFromDefault()
return@put DoubleValue.ONE
}
map.put("validate_moveset") { params ->
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
/**
* This is all you need tbh. A moveset builder that picks moves from up to 4 slots of move selectors.
*
* @author Hiroku
* @since December 5th, 2025
*/
class DefaultMovesetBuilder : MovesetBuilder {
companion object {
val TYPE = cobblemonResource("default")
}
override val type: ResourceLocation = TYPE
override var id: ResourceLocation = cobblemonResource("temp") // replaced on load.
val slot1: List<MoveSelector> = listOf()
val slot2: List<MoveSelector> = listOf()
val slot3: List<MoveSelector> = listOf()
val slot4: List<MoveSelector> = listOf()
override fun build(form: FormData, level: Int): MoveSet {
val chosenMoves = mutableSetOf<MoveTemplate>()
tryAddMoveFromSlot(form, level, slot1, chosenMoves)
tryAddMoveFromSlot(form, level, slot2, chosenMoves)
tryAddMoveFromSlot(form, level, slot3, chosenMoves)
tryAddMoveFromSlot(form, level, slot4, chosenMoves)
if (chosenMoves.isEmpty()) {
chosenMoves.add(Moves.getExceptional())
}
val moveset = MoveSet()
chosenMoves.map(MoveTemplate::create).forEach(moveset::add)
return moveset
}
fun tryAddMoveFromSlot(form: FormData, level: Int, selectors: List<MoveSelector>, chosenMoves: MutableSet<MoveTemplate>) {
for (selector in selectors) {
val move = selector(form, form.moves, level, chosenMoves)
if (move != null) {
chosenMoves.add(move)
return
}
}
}
}
@@ -0,0 +1,148 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.pokemon.moves.Learnset
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.weightedSelection
/**
* A function that selects a move from a given [Learnset] for a particular [FormData] at a certain level,
* taking into account any moves that have already been chosen for the moveset.
*
* @author Hiroku
* @since December 5th, 2025
*/
fun interface MoveSelector {
operator fun invoke(form: FormData, learnset: Learnset, level: Int, chosenMoves: Set<MoveTemplate>): MoveTemplate?
companion object {
val NONE = MoveSelector { _, _, _, _ -> null }
val LAST_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it] ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory != DamageCategories.STATUS } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_SUITABLE_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
val suitableCategories = if (form.baseStats[Stats.ATTACK]!! > form.baseStats[Stats.SPECIAL_ATTACK]!!) {
setOf(DamageCategories.PHYSICAL)
} else if (form.baseStats[Stats.SPECIAL_ATTACK]!! > form.baseStats[Stats.ATTACK]!!) {
setOf(DamageCategories.SPECIAL)
} else {
setOf(DamageCategories.PHYSICAL, DamageCategories.SPECIAL)
}
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory in suitableCategories } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_STATUS = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory == DamageCategories.STATUS } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val STAB = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory != DamageCategories.STATUS }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.PHYSICAL }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.SPECIAL }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val physicalMoves = moves.filter { it.damageCategory == DamageCategories.PHYSICAL }
return@MoveSelector physicalMoves.weightedSelection { it.getSelectionWeight(form) }
}
val SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val specialMoves = moves.filter { it.damageCategory == DamageCategories.SPECIAL }
return@MoveSelector specialMoves.weightedSelection { it.getSelectionWeight(form) }
}
val OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val offensiveMoves = moves.filter { it.damageCategory != DamageCategories.STATUS }
return@MoveSelector offensiveMoves.weightedSelection { it.getSelectionWeight(form) }
}
val RANDOM_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val STATUS = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val statusMoves = moves.filter { it.damageCategory == DamageCategories.STATUS }
return@MoveSelector statusMoves.weightedSelection { it.getSelectionWeight(form) }
}
val TM = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_TM = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
val stabMoves = moves.filter { it.elementalType in form.types }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val EGG = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.eggMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val selectors = mutableMapOf(
"none" to NONE,
"last_levelup" to LAST_LEVELUP,
"last_offensive" to LAST_OFFENSIVE,
"last_suitable_offensive" to LAST_SUITABLE_OFFENSIVE,
"last_status" to LAST_STATUS,
"levelup" to RANDOM_LEVELUP,
"stab" to STAB,
"stab_physical" to STAB_PHYSICAL,
"stab_special" to STAB_SPECIAL,
"physical" to PHYSICAL,
"special" to SPECIAL,
"offensive" to OFFENSIVE,
"status" to STATUS,
"tm" to TM,
"stab_tm" to STAB_TM,
"egg" to EGG
)
}
}
@@ -12,12 +12,12 @@ import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.moves.categories.DamageCategory
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.codec.CodecUtils
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.lang
@@ -53,7 +53,8 @@ open class MoveTemplate(
val pp: Int,
val priority: Int,
val critRatio: Double,
val effectChances: Array<Double>
val effectChances: Array<Double>,
val weight: Float
) {
val struct: MoStruct by lazy {
QueryStruct(hashMapOf())
@@ -69,9 +70,9 @@ open class MoveTemplate(
.addFunction("max_pp") { DoubleValue(maxPp) }
.addFunction("priority") { DoubleValue(priority) }
.addFunction("crit_ratio") { DoubleValue(critRatio) }
.addFunction("weight") { DoubleValue(weight.toDouble()) }
}
val displayName: MutableComponent
get() = lang("move.$name")
val description: MutableComponent
@@ -89,7 +90,8 @@ open class MoveTemplate(
pp = 5,
priority = 0,
critRatio = 0.0,
effectChances = emptyArray()
effectChances = emptyArray(),
weight = 0F,
)
companion object {
@@ -160,4 +162,12 @@ open class MoveTemplate(
}
return this.elementalType
}
fun getSelectionWeight(form: FormData): Float {
return if (this in form.signatureMoves) {
weight * MovesetBuilder.signatureMoveWeightMultiplier
} else {
weight
}
}
}
@@ -10,15 +10,15 @@ package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.data.MovesRegistrySyncPacket
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.fromJson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import net.minecraft.resources.ResourceLocation
@@ -46,6 +46,14 @@ object Moves : DataRegistry {
this.idMapping.clear()
this.moveScripts.clear()
val weights = mutableMapOf<String, Number>()
manager.getResourceStack(cobblemonResource("move_weights.json")).forEach {
it.open().use {
val map = GsonBuilder().create().fromJson<Map<String, Number>>(it.reader())
weights.putAll(map)
}
}
ShowdownService.service.resetRegistryData("move")
manager.listResources("moves") { it.path.endsWith(".js") }.forEach { (identifier, resource) ->
resource.open().use { stream ->
@@ -75,6 +83,7 @@ object Moves : DataRegistry {
val priority = jsMove.get("priority").asInt
val critRatio = jsMove.get("critRatio")?.asDouble ?: 1.0
val effectChances = arrayListOf<Double>()
val weight = weights[id]?.toFloat() ?: 50F
val secondariesMember = jsMove.get("secondaries")
val secondaryMember = jsMove.get("secondary")
if (secondariesMember != null && secondariesMember is JsonArray) {
@@ -92,7 +101,20 @@ object Moves : DataRegistry {
effectChances += secondaryMember.get("chance").asDouble
}
}
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
val move = MoveTemplate(
name = id,
num = num,
elementalType = elementalType,
damageCategory = damageCategory,
power = power,
target = target,
accuracy = accuracy,
pp = pp,
priority = priority,
critRatio = critRatio,
effectChances = effectChances.toTypedArray(),
weight = weight
)
this.register(move)
} catch (e: Exception) {
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
@@ -113,7 +135,7 @@ object Moves : DataRegistry {
@JvmStatic
fun getByNameOrDummy(name: String) = allMoves[name.lowercase()] ?: MoveTemplate.dummy(name.lowercase())
@JvmStatic
fun getExceptional() = getByName("tackle") ?: allMoves.values.random()
fun getExceptional() = getByName("tackle") ?: allMoves.values.first()
@JvmStatic
fun count() = allMoves.size
@JvmStatic
@@ -129,5 +151,4 @@ object Moves : DataRegistry {
this.allMoves[move.name] = move
this.idMapping[move.num] = move
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
/**
* Builders a moveset for a particular [FormData] and level. Probably only really need [DefaultMovesetBuilder] though.
*
* @author Hiroku
* @since December 5th, 2025
*/
interface MovesetBuilder {
/** The ID of this builder. It's autofilled from the registry loader. */
var id: ResourceLocation
/** The subtype designation for this moveset builder. Generally unused once read from JSON. */
val type: ResourceLocation
/** Builds a brand new moveset for the [form] and [level]. */
fun build(form: FormData, level: Int): MoveSet
companion object {
/** Weight multiplier for moves that are in the form's signatureMoves list. Makes them way more likely to be chosen. */
var signatureMoveWeightMultiplier = 5F
/** Subtypes of MovesetBuilder. I personally doubt you'll need any other subtypes, but hey. */
val movesetBuilderTypes = mutableMapOf<ResourceLocation, Class<out MovesetBuilder>>(
cobblemonResource("default") to DefaultMovesetBuilder::class.java
)
}
}
@@ -8,12 +8,14 @@
package com.cobblemon.mod.common.api.npc
import com.cobblemon.mod.common.api.npc.partyproviders.ComposedPoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.PoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.ScriptPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.SimplePartyProvider
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.google.gson.JsonElement
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
/**
@@ -25,14 +27,15 @@ import net.minecraft.server.level.ServerPlayer
*/
interface NPCPartyProvider {
companion object {
val types = mutableMapOf<String, (String) -> NPCPartyProvider>(
SimplePartyProvider.TYPE to { SimplePartyProvider() },
PoolPartyProvider.TYPE to { PoolPartyProvider() },
ScriptPartyProvider.TYPE to { ScriptPartyProvider() }
val types = mutableMapOf(
SimplePartyProvider.TYPE to ::SimplePartyProvider,
PoolPartyProvider.TYPE to ::PoolPartyProvider,
ScriptPartyProvider.TYPE to ::ScriptPartyProvider,
ComposedPoolPartyProvider.TYPE to ::ComposedPoolPartyProvider
)
}
val type: String
val type: ResourceLocation
val isStatic: Boolean
fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer> = emptyList()): NPCPartyStore
// Why did I opt for manual JSON loading??? I must have had a reason but I can't remember. Maybe for S2C? Use a codec doofus
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import kotlin.random.Random
/**
* A kind of definition for how to compose a party of Pokémon given a [PartyPool].
*
* @author Hiroku
* @since December 7th, 2025
*/
class PartyComposition {
var id = cobblemonResource("dummy_composition")
// val displayName = "cobblemon.party_composition.$id" // dunno if i will use this yet
val scrambleOrder: Boolean = false
val slot1 = listOf<String>()
val slot2 = listOf<String>()
val slot3 = listOf<String>()
val slot4 = listOf<String>()
val slot5 = listOf<String>()
val slot6 = listOf<String>()
fun compose(pool: PartyPool, level: Int, aspects: Set<String>, desiredPokemonCount: Int, runtime: MoLangRuntime, random: Random = Random.Default): List<Pokemon> {
val chosenEntries = mutableListOf<PartyPool.PoolEntry>()
val availableEntries = pool.entries.filter { level in it.npcLevels.resolve(runtime) && (it.npcAspects.isEmpty() || it.npcAspects.all(aspects::contains)) }
for (labelSet in listOf(slot1, slot2, slot3, slot4, slot5, slot6)) {
pool.tryChoosingEntry(labelSet, runtime, random, availableEntries, chosenEntries)
if (chosenEntries.size >= desiredPokemonCount) {
break
}
}
val finalEntries = if (scrambleOrder) {
chosenEntries.shuffled(random)
} else {
chosenEntries
}
val pokemon = finalEntries.map { entry ->
val levelVariation = entry.levelVariation.resolve(runtime)
val lvlVariation = if (levelVariation.first == levelVariation.last) {
levelVariation.first
} else {
levelVariation.random(random)
}
val finalLevel = level + lvlVariation
val properties = entry.pokemon.copy()
properties.level = properties.level ?: finalLevel
val pokemon = properties.create()
entry.movesetBuilders.randomOrNull()?.let(pokemon::initializeMovesetFrom)
pokemon
}
return pokemon
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.toProperties
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
/**
* A pool of Pokémon to choose from, typically for generating a party for an NPC. The entries
* of the pool work on a weight and label system which is used as part of a [PartyComposition].
*
* @author Hiroku
* @since December 7th, 2025
*/
class PartyPool {
var id = cobblemonResource("dummy")
val displayName = "cobblemon.party_pool.$id"
val entries = mutableListOf<PoolEntry>()
class PoolEntry {
val pokemon: PokemonProperties = "pikachu".toProperties()
val labels: List<String> = emptyList()
val npcAspects: List<String> = emptyList()
val required: List<String> = emptyList()
val excluded: List<String> = emptyList()
val maxSelectableTimes: ExpressionLike = "6".asExpressionLike()
val levelVariation: ScriptableIntRange = ScriptableIntRange(0, 0)
val npcLevels: ScriptableIntRange = ScriptableIntRange(1, 100)
val movesetBuilders: List<MovesetBuilder> = emptyList()
val weight: ExpressionLike = "50".asExpressionLike()
}
/**
* Goes through each of the labels in order, trying to find a valid choice. Once it finds one, it skips the later
* labels and adds the value to [chosenEntries] and returns. If none of the labels could find a valid entry, nothing is added.
*/
fun tryChoosingEntry(
labels: List<String>,
runtime: MoLangRuntime,
random: Random,
availableEntries: List<PoolEntry>,
chosenEntries: MutableList<PoolEntry>,
) {
for (label in labels) {
val entries = availableEntries
.filter { label in it.labels || label.lowercase() == "any" }
.filter { entry ->
// Check required labels
entry.required.all { it in labels } &&
// Check excluded labels
entry.excluded.none { it in labels } &&
// Check max selectable times
chosenEntries.count { it == entry } < entry.maxSelectableTimes.resolveFloat(runtime)
}
if (entries.isNotEmpty()) {
val selectedEntry = entries.weightedSelection(random) { it.weight.resolveFloat(runtime) } ?: return
chosenEntries.add(selectedEntry)
return
}
}
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc.partyproviders
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.CobblemonPartyCompositions
import com.cobblemon.mod.common.CobblemonPartyPools
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.api.npc.PartyComposition
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import kotlin.random.Random
import kotlin.ranges.random
import net.minecraft.server.level.ServerPlayer
/**
* An [NPCPartyProvider] which uses a [PartyComposition] and [PartyPool] to generate a party.
*
* There are controls for minimum and maximum Pokémon count, as well as whether to use a fixed random seed for the NPC.
*
* @author Hiroku
* @since December 8th, 2025
*/
class ComposedPoolPartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("composed_pool")
}
override val type = TYPE
override var isStatic: Boolean = true
var useFixedRandom: Boolean = false
var minPokemon: Expression = "1".asExpression()
var maxPokemon: Expression = "6".asExpression()
lateinit var pool: PartyPool
lateinit var composition: PartyComposition
override fun loadFromJSON(json: JsonElement) {
json as JsonObject
isStatic = json.getAsJsonPrimitive("isStatic")?.asBoolean ?: true
useFixedRandom = json.getAsJsonPrimitive("useFixedRandom")?.asBoolean ?: false
pool = CobblemonPartyPools.partyPools[json.get("pool").asString.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown PartyPool id: ${json.get("pool").asString}")
composition = CobblemonPartyCompositions.partyCompositions[json.get("composition").asString.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown PartyComposition id: ${json.get("composition").asString}")
minPokemon = json.getAsJsonPrimitive("minPokemon").asString?.asExpression() ?: "1".asExpression()
maxPokemon = json.getAsJsonPrimitive("maxPokemon").asString?.asExpression() ?: "6".asExpression()
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
val runtime = MoLangRuntime().setup().withQueryValue("npc", npc.struct)
val random = if (useFixedRandom) Random(npc.uuid.hashCode()) else Random.Default
runtime.withQueryValue("level", DoubleValue(level))
runtime.withQueryValue("players", players.asArrayValue { it.asMoLangValue() })
if (players.size == 1) {
// This is for the convenience, most cases will be pvn one player
runtime.withQueryValue("player", players.first().asMoLangValue())
}
val minPokemon = runtime.resolveInt(this.minPokemon)
val maxPokemon = runtime.resolveInt(this.maxPokemon)
val desiredPokemonCount = (minPokemon..maxPokemon).random(random)
val pokemonList = composition.compose(
pool = pool,
level = level,
aspects = npc.aspects,
desiredPokemonCount = desiredPokemonCount,
runtime = runtime,
random = random
)
val partyStore = NPCPartyStore(npc)
pokemonList.forEach(partyStore::add)
return partyStore
}
}
@@ -48,7 +48,7 @@ import kotlin.random.Random
*/
class PoolPartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "pool"
val TYPE = cobblemonResource("pool")
}
@Transient
@@ -150,10 +150,10 @@ class PoolPartyProvider : NPCPartyProvider {
val instance = selected.pokemon.copy().also { it.level = it.level ?: dictatedLevel ?: randomLevel }.create()
party.add(instance)
}
if (party.none()) {
LOGGER.error("${npc} has no Pokemon on Party")
}
if (party.none()) {
LOGGER.error("$npc had no Pokémon produced by their party provider")
}
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
@@ -28,7 +28,7 @@ import net.minecraft.server.level.ServerPlayer
class ScriptPartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "script"
val TYPE = cobblemonResource("script")
}
override val type = TYPE
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getBooleanOrNull
import com.cobblemon.mod.common.util.toProperties
import com.google.gson.JsonElement
@@ -29,7 +30,7 @@ import net.minecraft.server.level.ServerPlayer
*/
class SimplePartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "simple"
val TYPE = cobblemonResource("simple")
}
@Transient
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.pokemon
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.abilities.Ability
import com.cobblemon.mod.common.api.events.CobblemonEvents
@@ -148,6 +149,13 @@ open class PokemonProperties {
props.originalTrainerType = OriginalTrainerType.entries.toList().parsePropertyOfCollection(keyPairs, listOf("originaltrainertype", "ottype"), labelsOptional = true) { it.name.lowercase() }
props.originalTrainer = parsePlayerProperty(keyPairs, listOf("originaltrainer", "ot"))
props.moves = parseString(keyPairs, listOf("moves"))?.split(",")
props.movesetBuilders = parseString(keyPairs, listOf("moveset_builders", "movesetbuilders", "moveset_builder", "movesetbuilder"))?.split(",")?.mapNotNull {
try {
it.asIdentifierDefaultingNamespace()
} catch (_: ResourceLocationException) {
null
}
}
props.heldItem = parseString(keyPairs, listOf("helditem", "held_item"))
props.minPerfectIVs = parseIntProperty(keyPairs, listOf("min_perfect_ivs"))?.coerceIn(0, Stats.PERMANENT.size)
props.scaleModifier = parseFloatProperty(keyPairs, listOf("scale_modifier"))
@@ -356,6 +364,7 @@ open class PokemonProperties {
var originalTrainerType: OriginalTrainerType? = null
var originalTrainer: String? = null // Original Trainer by Username or UUID
var moves: List<String>? = null
var movesetBuilders: List<ResourceLocation>? = null
var heldItem: String? = null
var cosmeticItem: String? = null
var scaleModifier: Float? = null
@@ -462,7 +471,7 @@ open class PokemonProperties {
}
moves?.let { moves ->
if (pokemon.moveSet.getMoves().isEmpty()) {
pokemon.initializeMoveset(preferLatest = true)
pokemon.initializeMovesetFromDefault()
}
val moveTemplates = moves.mapNotNull(Moves::getByName).shuffled()
val replaceableIndices = (0..3).filterNot { pokemon.moveSet[it]?.template?.let(moveTemplates::contains) == true }.shuffled()
@@ -475,6 +484,13 @@ open class PokemonProperties {
pokemon.moveSet.update()
}
}
movesetBuilders?.let { movesetBuilders ->
val builders = movesetBuilders.mapNotNull { CobblemonMovesetBuilders.movesetBuilders[it] }
if (builders.isNotEmpty()) {
val movesetBuilder = builders.random()
pokemon.initializeMovesetFrom(movesetBuilder)
}
}
heldItem?.let { itemKey ->
val server = server() ?: return@let
val parser = ItemParser(server.registryAccess())
@@ -678,6 +694,7 @@ open class PokemonProperties {
originalTrainerType?.let { nbt.putInt(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.ordinal) }
originalTrainer?.let { nbt.putString(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
moves?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
movesetBuilders?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
heldItem?.let {nbt.putString(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
scaleModifier?.let { nbt.putFloat(DataKeys.POKEMON_SCALE_MODIFIER, it) }
val custom = ListTag()
@@ -712,6 +729,7 @@ open class PokemonProperties {
originalTrainerType = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) OriginalTrainerType.valueOf(tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) else null
originalTrainer = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER)) tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER) else null
moves = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVES)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVES).split(",") else null
movesetBuilders = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS).split(",").map { it.asIdentifierDefaultingNamespace() } else null
heldItem = if (tag.contains(DataKeys.POKEMON_PROPERTIES_HELDITEM)) tag.getString(DataKeys.POKEMON_PROPERTIES_HELDITEM) else null
scaleModifier = if (tag.contains(DataKeys.POKEMON_SCALE_MODIFIER)) tag.getFloat(DataKeys.POKEMON_SCALE_MODIFIER) else null
val custom = tag.getList(DataKeys.POKEMON_PROPERTIES_CUSTOM, Tag.TAG_STRING.toInt())
@@ -747,6 +765,7 @@ open class PokemonProperties {
originalTrainerType?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.name) }
originalTrainer?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
moves?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
movesetBuilders?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
heldItem?.let {json.addProperty(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
scaleModifier?.let { json.addProperty(DataKeys.POKEMON_SCALE_MODIFIER, it) }
val custom = JsonArray()
@@ -781,6 +800,7 @@ open class PokemonProperties {
originalTrainerType = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)?.asString?.let { OriginalTrainerType.valueOf(it) }
originalTrainer = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER)?.asString
moves = json.get(DataKeys.POKEMON_PROPERTIES_MOVES)?.asString?.split(",")
movesetBuilders = json.get(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)?.asString?.split(",")?.map { it.asIdentifierDefaultingNamespace() }
heldItem = json.get(DataKeys.POKEMON_PROPERTIES_HELDITEM)?.asString
scaleModifier = json.get(DataKeys.POKEMON_SCALE_MODIFIER)?.asFloat
val custom = json.get(DataKeys.POKEMON_PROPERTIES_CUSTOM)?.asJsonArray
@@ -820,6 +840,7 @@ open class PokemonProperties {
originalTrainer?.let { pieces.add("originaltrainer=$it") }
customProperties.forEach { pieces.add(it.asString()) }
moves?.let { pieces.add("moves=${it.joinToString(separator = ",")}") }
movesetBuilders?.let { pieces.add("movesetbuilders=${it.joinToString(separator = ",") { source -> source.toString() }}") }
heldItem?.let {pieces.add("helditem=$it")}
scaleModifier?.let { pieces.add("scale_modifier=$it") }
return pieces.joinToString(separator)
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.api.drop.ItemDropMethod
import com.cobblemon.mod.common.api.entity.EntityDimensionsAdapter
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies.getByIdentifier
@@ -131,6 +132,7 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(RidingBehaviourSettings::class.java, RidingBehaviourSettingsAdapter)
.registerTypeAdapter(RideSoundSettingsList::class.java, RideSoundSettingsListAdapter)
.registerTypeAdapter(ObtainableItemCondition::class.java, ObtainableItemConditionAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.disableHtmlEscaping()
.enableComplexMapKeySerialization()
.create()
@@ -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 {
@@ -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
@@ -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()
}
}
@@ -44,7 +44,7 @@ import kotlin.random.Random
* @since July 10th, 2022
*/
open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelectionData> {
class SelectingSpawnInformation {
class SelectingSpawnInformation(val spawnDetail: SpawnDetail) {
val spawnablePositions = mutableMapOf<SpawnablePosition, Float>()
var highestWeight = 0F
@@ -58,61 +58,81 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
fun chooseSpawnablePosition() = spawnablePositions.entries.weightedSelection { it.value }!!.key
}
class SpawnablePositionSelectionData(
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
var percentSum: Float
): SpawnSelectionData {
val size: Int
get() = spawnToSpawnablePosition.size
class SpawnablePositionBucketSelectionData(
val selectingSpawnInformation: MutableList<SelectingSpawnInformation>
) {
var percentSum: Float = 0F
val seenSpawns = mutableListOf<SpawnDetail>()
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every spawnable position the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
seenSpawns.add(spawnDetail)
percentSum += spawnDetail.percentage
}
val spawnInformation = selectingSpawnInformation.find { it.spawnDetail == spawnDetail }
?: SelectingSpawnInformation(spawnDetail).also { selectingSpawnInformation.add(it) }
spawnInformation.add(spawnablePosition, spawnablePosition.getWeight(spawnDetail))
}
fun remove(spawnInformation: SelectingSpawnInformation) {
selectingSpawnInformation -= spawnInformation
if (spawnInformation.spawnDetail.percentage > 0) {
percentSum -= spawnInformation.spawnDetail.percentage
}
}
}
class SpawnablePositionSelectionData(
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>,
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData>,
): SpawnSelectionData {
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
fun getDataForBucket(bucket: SpawnBucket): SpawnablePositionBucketSelectionData {
val existing = selectingSpawnInformation[bucket]
if (existing != null) {
return existing
}
val bucketSpawnInformation = SpawnablePositionBucketSelectionData(mutableListOf())
spawnablePositions.forEach { spawnablePosition ->
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
bucketSpawnInformation.add(it, spawnablePosition)
}
}
selectingSpawnInformation[bucket] = bucketSpawnInformation
return bucketSpawnInformation
}
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation.filter { shouldRemove(it.spawnDetail) } }
.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
}
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
val toRemove = spawnToSpawnablePosition.entries.filter { (spawnDetail, positionData) ->
positionData.spawnablePositions.removeIf { shouldRemove(spawnDetail, it.key) }
val toRemove = selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation }.filter { positionData ->
positionData.spawnablePositions.removeIf { shouldRemove(positionData.spawnDetail, it.key) }
positionData.highestWeight = positionData.spawnablePositions.maxOfOrNull { it.value } ?: 0F
positionData.spawnablePositions.isEmpty()
}
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
toRemove.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
}
}
override fun getSelectionData(
spawner: Spawner,
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): SpawnablePositionSelectionData {
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation> = mutableMapOf()
var percentSum = 0F
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData> = mutableMapOf()
spawnablePositions.forEach { spawnablePosition ->
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every spawnable position the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnToSpawnablePosition.containsKey(it)) {
percentSum += it.percentage
}
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(
it,
::SelectingSpawnInformation
)
selectingSpawnInformation.add(spawnablePosition, spawnablePosition.getWeight(it))
}
}
return SpawnablePositionSelectionData(spawnToSpawnablePosition, percentSum)
return SpawnablePositionSelectionData(spawner, spawnablePositions, selectingSpawnInformation)
}
override fun selectSpawnAction(
@@ -120,8 +140,8 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
bucket: SpawnBucket,
selectionData: SpawnablePositionSelectionData,
): SpawnAction<*>? {
val spawnToSpawnablePosition = selectionData.spawnToSpawnablePosition
var percentSum = selectionData.percentSum
val selectingSpawnInformation = selectionData.getDataForBucket(bucket) ?: return null
var percentSum = selectingSpawnInformation.percentSum
// First pass is doing percentage checks.
if (percentSum > 0) {
@@ -129,7 +149,7 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
if (percentSum > 100) {
Cobblemon.LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
This means you don't understand how this option works.
""".trimIndent()
)
@@ -140,13 +160,13 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
* It's [0, 1) and I want (0, 1]
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
*/
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
val selectedPercentage = 100 - Random.nextFloat() * 100
percentSum = 0F
for ((spawnDetail, info) in spawnToSpawnablePosition) {
if (spawnDetail.percentage > 0) {
percentSum += spawnDetail.percentage
for (info in selectingSpawnInformation.selectingSpawnInformation) {
if (info.spawnDetail.percentage > 0) {
percentSum += info.spawnDetail.percentage
if (percentSum >= selectedPercentage) {
return spawnDetail.choose(
return info.spawnDetail.choose(
spawnablePosition = info.chooseSpawnablePosition(),
bucket = bucket,
selectionData = selectionData
@@ -156,9 +176,9 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
}
}
val selectedSpawn = spawnToSpawnablePosition.entries.toList().weightedSelection { it.value.highestWeight }!!
return selectedSpawn.key.choose(
spawnablePosition = selectedSpawn.value.chooseSpawnablePosition(),
val selectedSpawn = selectingSpawnInformation.selectingSpawnInformation.toList().weightedSelection { it.highestWeight }!!
return selectedSpawn.spawnDetail.choose(
spawnablePosition = selectedSpawn.chooseSpawnablePosition(),
bucket = bucket,
selectionData = selectionData
)
@@ -169,16 +189,16 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
if (selectionData.size == 0) {
if (selectionData.selectingSpawnInformation.isEmpty()) {
return emptyMap()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
for ((spawnDetail, info) in selectionData.spawnToSpawnablePosition.entries) {
totalWeights[spawnDetail] = info.highestWeight
for (info in selectionData.selectingSpawnInformation) {
totalWeights[info.spawnDetail] = info.highestWeight
}
return totalWeights
@@ -18,7 +18,6 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.util.removeIf
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
import kotlin.reflect.full.createInstance
/**
* A spawning selector that compiles a distinct list of all spawn details that
@@ -33,9 +32,9 @@ import kotlin.reflect.full.createInstance
* of an entire spawnable position type to favour whichever are the more populous.
*
* The weight for a spawn when doing primary selection is whichever spawnable position-influenced
* weight is highest (as weight multipliers exist per spawnable position) and then the
* selection of which spawnable position to spawn the primary selected spawn uses the spawnable
* position-adjusted weight.
* weight is highest (as weight multipliers exist per spawnable position). After the spawn has been
* chosen, the selection of which spawnable position to spawn it at is selected via another weighted
* selection using spawnable position-adjusted weights.
*
* At a glance:
* - Spawn detail selection is flat across spawnable position quantity
@@ -45,7 +44,6 @@ import kotlin.reflect.full.createInstance
* @since July 10th, 2022
*/
open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnablePositionWeightedSelector.SeparatedSelectionData> {
open fun getWeight(spawnablePositionType: SpawnablePositionType<*>) = spawnablePositionType.getWeight()
class SelectingSpawnInformation {
val spawnablePositionWeights = mutableMapOf<SpawnablePosition, Float>()
@@ -62,13 +60,25 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
fun chooseSpawnablePosition() = spawnablePositionWeights.keys.toList().weightedSelection { spawnablePositionWeights[it]!! }!!
}
class SpawnablePositionSelectionData(
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
var percentSum: Float
) {
class SpawnablePositionSelectionData {
val spawnToSpawnablePosition = mutableMapOf<SpawnDetail, SelectingSpawnInformation>()
var percentSum: Float = 0F
val seenSpawns = mutableListOf<SpawnDetail>()
val size: Int
get() = spawnToSpawnablePosition.size
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
// position type, otherwise the percentage will get amplified for every spawnable position the thing
// was possible, completely ruining the point of this pre-selection percentage.
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
percentSum += spawnDetail.percentage
seenSpawns.add(spawnDetail)
}
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(spawnDetail, ::SelectingSpawnInformation)
selectingSpawnInformation.add(spawnDetail, spawnablePosition, 1F)
}
fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
@@ -89,67 +99,68 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
}
class SeparatedSelectionData(
val spawnablePositionTypeToSpawns: MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>
): SpawnSelectionData {
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>
) : SpawnSelectionData {
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
val bucketData = mutableMapOf<SpawnBucket, MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>>()
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnDetails(shouldRemove) }
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
bucketData.values.forEach {
it.values.forEach { it.removeSpawnDetails(shouldRemove) }
it.removeIf { it.value.size == 0 }
}
}
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnablePositions(shouldRemove) }
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
bucketData.values.forEach {
it.values.forEach { it.removeSpawnablePositions(shouldRemove) }
it.removeIf { it.value.size == 0 }
}
}
fun getDataForBucket(bucket: SpawnBucket): MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData> {
bucketData[bucket]?.let { return it }
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
spawnablePositions.forEach { spawnablePosition ->
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
if (possible.isNotEmpty()) {
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns
.getOrPut(spawnablePositionType, ::SpawnablePositionSelectionData)
possible.forEach { spawnablePositionSelectionData.add(it, spawnablePosition) }
}
}
bucketData[bucket] = spawnablePositionTypeToSpawns
return spawnablePositionTypeToSpawns
}
}
override fun getSelectionData(
spawner: Spawner,
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): SeparatedSelectionData {
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
spawnablePositions.forEach { spawnablePosition ->
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
if (possible.isNotEmpty()) {
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns.getOrPut(spawnablePositionType) { SpawnablePositionSelectionData(mutableMapOf(), 0F) }
possible.forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
// position type, otherwise the percentage will get amplified for every spawnable position the thing
// was possible, completely ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnablePositionSelectionData.spawnToSpawnablePosition.containsKey(it)) {
spawnablePositionSelectionData.percentSum += it.percentage
}
val selectingSpawnInformation = spawnablePositionSelectionData.spawnToSpawnablePosition.getOrPut(
it,
SelectingSpawnInformation::class::createInstance
)
selectingSpawnInformation.add(it, spawnablePosition, getWeight(spawnablePositionType))
}
}
}
return SeparatedSelectionData(spawnablePositionTypeToSpawns)
}
) = SeparatedSelectionData(spawner, spawnablePositions)
override fun selectSpawnAction(
spawner: Spawner,
bucket: SpawnBucket,
selectionData: SeparatedSelectionData
): SpawnAction<*>? {
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
val bucketData = selectionData.getDataForBucket(bucket)
if (bucketData.isEmpty()) {
return null
}
// Which spawnable position type should we use?
val spawnablePositionSelectionData = selectionData.spawnablePositionTypeToSpawns.entries.toList()
.weightedSelection { getWeight(it.key) * it.value.size }
val spawnablePositionSelectionData = bucketData.entries.toList()
.weightedSelection { it.key.getWeight() * it.value.size }
?.value
?: return null
@@ -161,7 +172,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
if (percentSum > 100) {
LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
This means you don't understand how this option works.
""".trimIndent()
)
@@ -173,7 +184,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
* It's [0, 1) and I want (0, 1]
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
*/
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
val selectedPercentage = 100 - Random.nextFloat() * 100
percentSum = 0F
for ((spawnDetail, info) in spawnToSpawnablePosition) {
if (spawnDetail.percentage > 0) {
@@ -224,7 +235,8 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
val percentageWeight = (rescaledTotalWeight - totalWeight) / percentSum
for ((spawnDetail, info) in spawnToSpawnablePosition.entries) {
totalWeights[spawnDetail] = info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
totalWeights[spawnDetail] =
info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
}
return totalWeights
@@ -235,18 +247,18 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
if (selectionData.isEmpty()) {
return mapOf()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
val totalSpawnablePositionWeight = selectionData.spawnablePositionTypeToSpawns.keys.sumOf { getWeight(it).toDouble() }.toFloat()
val totalSpawnablePositionWeight = selectionData.keys.sumOf { it.getWeight().toDouble() }.toFloat()
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData.spawnablePositionTypeToSpawns) {
val spawnPositionWeightCorrection = getWeight(spawnablePositionType) / totalSpawnablePositionWeight
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData) {
val spawnPositionWeightCorrection = spawnablePositionType.getWeight() / totalSpawnablePositionWeight
val spawnPositionProbabilities = getProbabilitiesFromSpawnablePositionType(spawner, spawnPositionSelectionData)
spawnPositionProbabilities.entries.forEach {
@@ -29,7 +29,7 @@ interface SpawningSelector<T : SpawnSelectionData> {
val DEFAULT = FlatSpawnablePositionWeightedSelector()
}
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
fun getSelectionData(spawner: Spawner, spawnablePositions: List<SpawnablePosition>): T
fun selectSpawnAction(
spawner: Spawner,
@@ -37,12 +37,18 @@ interface SpawningSelector<T : SpawnSelectionData> {
selectionData: T
): SpawnAction<*>?
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
fun select(
spawner: Spawner,
bucketSelector: () -> SpawnBucket,
spawnablePositions: List<SpawnablePosition>,
maxSpawns: Int
): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, spawnablePositions)
val spawnActions = selectionData.spawnActions
while (spawnActions.size < maxSpawns) {
val bucket = bucketSelector()
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -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) }
}
}
@@ -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)
)
}
}
}
@@ -29,13 +29,6 @@ import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.Any
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.plus
import kotlin.math.max
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
@@ -95,10 +88,9 @@ interface Spawner {
): SpawnAction<*>? {
influences.removeIf { it.isExpired() }
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(
spawner = this,
bucket = bucket,
bucketSelector = { chooseBucket(cause, spawnablePosition.influences) },
spawnablePositions = listOf(spawnablePosition),
maxSpawns = 1
).firstOrNull()
@@ -116,9 +108,9 @@ interface Spawner {
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
ENTITY_LIMIT_CHUNK_RANGE * 16.0,
1000.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
ENTITY_LIMIT_CHUNK_RANGE * 16.0
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
@@ -140,11 +132,10 @@ interface Spawner {
val zone = generator.generate(this, constrainedArea)
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
val influences = influences + zone.unconditionalInfluences
val bucket = chooseBucket(zoneInput.cause, influences)
return selector.select(
spawner = this,
bucket = bucket,
bucketSelector = { chooseBucket(zoneInput.cause, influences) },
spawnablePositions = spawnablePositions,
maxSpawns = maxSpawns
)
@@ -31,7 +31,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 +48,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
private val hpWeightConsideration = 0.25 // how much HP difference is a consideration for switchins
private val hpFractionCoefficient = 0.4 // how much HP differences should be taken into account for switch ins
private val boostWeightCoefficient = 1 // the amount of boosts considered a baseline to be removed
private val switchOutMatchupThreshold = 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
@@ -219,19 +218,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 +253,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 +264,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 +289,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 +306,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 +315,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 +347,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 +363,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 +374,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 +382,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 +395,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 +422,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 +451,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 +511,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 +527,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 +562,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 } ||
@@ -995,6 +990,4 @@ class StrongBattleAI(skill: Int) : BattleAI {
return MoveActionResponse(move.id, chosenTarget.getPNX())
}
}
}
}
@@ -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 } }
@@ -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()
@@ -65,6 +65,9 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
private val growthPoints = arrayListOf<ResourceLocation>()
var mulchVariant = MulchVariant.NONE
// For Random Tick compensation: records the game time of last tick
var lastTickTime: Long = 0L
/**
* The idea behind the growth point sequence is it's a 16-long string of
* hexadecimal numbers. They represent the order of the growth points that
@@ -267,6 +270,7 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
} catch (ignored: ResourceLocationException) {}
}
this.mulchDuration = nbt.getInt(MULCH_DURATION)
this.lastTickTime = nbt.getLong(LAST_TICK_TIME)
this.wasLoading = false
if (nbt.contains(GROWTH_POINTS_SEQUENCE)) {
growthPointSequence = nbt.getString(GROWTH_POINTS_SEQUENCE)
@@ -287,6 +291,7 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
nbt.putInt(MULCH_DURATION, mulchDuration)
nbt.putString(GROWTH_POINTS_SEQUENCE, growthPointSequence)
nbt.putString(MULCH_VARIANT, mulchVariant.toString())
nbt.putLong(LAST_TICK_TIME, lastTickTime)
}
override fun setChanged() {
@@ -346,24 +351,39 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
return
}
/**
* Process Random Tick compensation logic.
* Calculates elapsed time since last tick and batch-decrements stageTimer.
*/
fun processTick(world: ServerLevel, pos: BlockPos, state: BlockState): Boolean {
if (state.getValue(BerryBlock.IS_ROOTED)) return false
val currentTime = world.gameTime
val elapsed = if (lastTickTime > 0L) {
(currentTime - lastTickTime).toInt().coerceAtLeast(1)
} else {
1 // First tick, only decrement by 1
}
lastTickTime = currentTime
// Batch decrement the timer
if (stageTimer > 0) {
stageTimer = (stageTimer - elapsed).coerceAtLeast(0)
}
// Return whether growth is needed
return stageTimer <= 0
}
//Using a similar approach to ComputerCraft, implement this client side
interface RenderState : AutoCloseable {
var needsRebuild: Boolean
}
companion object {
internal val TICKER = BlockEntityTicker<BerryBlockEntity> { world, pos, state, blockEntity ->
if (world.isClientSide) return@BlockEntityTicker
if (state.getValue(BerryBlock.IS_ROOTED)) return@BlockEntityTicker
if (blockEntity.stageTimer >= 0) {
blockEntity.stageTimer--
}
if (blockEntity.stageTimer <= 0) {
(state.block as BerryBlock).growHelper(world as ServerLevel, world.random, pos, state)
}
}
//private const val LIFE_CYCLES = "life_cycles"
private const val GROWTH_POINTS = "GrowthPoints"
private const val LAST_TICK_TIME = "LastTickTime"
private const val GROWTH_POINTS_SEQUENCE = "GrowthPointsSequence"
private const val GROWTH_TIMER = "GrowthTimer"
private const val STAGE_TIMER = "StageTimer"
@@ -29,6 +29,7 @@ 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.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PokeSnackSpawnerFactory
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.components.BaitEffectsComponent
@@ -62,41 +63,22 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
}
val spawner: FixedAreaSpawner by lazy {
FixedAreaSpawner(
name = "poke_snack_spawner_${level?.dimension()?.location()}}_$blockPos",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL,
world = level as ServerLevel,
position = blockPos,
val server = level as ServerLevel
val baitEffects = getBaitEffects()
val ctx = PokeSnackSpawnerFactory.Context(
world = server,
pos = blockPos,
horizontalRadius = RADIUS,
verticalRadius = RADIUS,
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk
).also {
it.influences.add(this)
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk,
name = "poke_snack_spawner_${server.dimension().location()}_$blockPos",
baitEffects = baitEffects
)
val baitEffects = getBaitEffects()
val stackedLureTier = baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
if (stackedLureTier > 0) {
it.influences += BucketNormalizingInfluence(
tier = stackedLureTier,
gradient = 0.2F,
firstTier = 1.2F
)
}
it.influences += BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
it.influences += SpawnBaitInfluence(effects = baitEffects)
PokeSnackSpawnerFactory.create(ctx).also { spawner ->
spawner.influences.add(this)
}
}
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.darkGray
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
@@ -229,7 +230,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
var levelScrollOffset = 0F
var levelScrollPositionOffset = 0F
var levelScrollSize = 17F
if (expGainedData != null && expGainedData.oldLevel != null) {
if (expGainedData?.oldLevel != null) {
val ticksWithDelta = (expGainedData.ticks + partialDeltaTicks) - (PartyOverlayDataControl.BAR_UPDATE_BEFORE_TIME + PartyOverlayDataControl.BAR_FLASH_TIME)
val transition = ticksWithDelta / PartyOverlayDataControl.LEVEL_UP_PORTRAIT_TIME
if ((0F..<1F).contains(transition)) {
@@ -403,10 +404,48 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
blue = expBlue
)
//Handling EXP Text
var expGained = 0
var expTextTransparency = 0F
var expTextOffset = 0F
if (expGainedData != null) {
expGained = expGainedData.expGained
val ticksWithDelta = expGainedData.ticks + partialDeltaTicks
if (ticksWithDelta <= PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn) {
val transition = ticksWithDelta / PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn
val step = (floor(transition * 3)) + 1
expTextTransparency = step * 0.25F
expTextOffset = 4 - step
} else if (ticksWithDelta <= PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn + PartyOverlayDataControl.EXP_POPUP_TIME.hold) {
expTextTransparency = 1F
expTextOffset = 0F
} else {
val adjustedTicks = ticksWithDelta - (PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn + PartyOverlayDataControl.EXP_POPUP_TIME.hold)
val transition = adjustedTicks / PartyOverlayDataControl.EXP_POPUP_TIME.fadeOut
val step = (floor(transition * 3)) + 1
expTextTransparency = 1F - (step * 0.25F)
expTextOffset = -step
}
}
if (expTextTransparency > 0) {
drawScaledText(
context = context,
text = lang("ui.exp.number", expGained.toString()).bold(),
x = panelX + selectedOffsetX + 57F,
y = indexY + 17F + expTextOffset,
scale = SCALE,
shadow = true,
opacity = expTextTransparency
)
}
//Handling Popups
var ticksForMove = -1F
var ticksForEvo = -1F
//Handling Popups
if (expGainedData != null) {
val ticksWithDelta = expGainedData.ticks + partialDeltaTicks
val ticks = ticksWithDelta - (PartyOverlayDataControl.BAR_UPDATE_BEFORE_TIME + PartyOverlayDataControl.BAR_FLASH_TIME)
@@ -437,7 +476,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
return 0F to 1F
} else {
val adjustedPopupTicks = ticks - (PartyOverlayDataControl.POPUP_TIME.fadeIn + PartyOverlayDataControl.POPUP_TIME.hold)
val transition = adjustedPopupTicks / PartyOverlayDataControl.POPUP_TIME.fadeIn
val transition = adjustedPopupTicks / PartyOverlayDataControl.POPUP_TIME.fadeOut
val step = (floor(transition * 3)) + 1
return (-step) to (1F - (step * 0.25F))
}
@@ -451,7 +490,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
blitk(
matrixStack = matrices,
texture = newEvoPopup,
x = (panelX + selectedOffsetX + 56.5F) / SCALE,
x = (panelX + selectedOffsetX + (if (expTextTransparency > 0 && movePopupTransparency > 0) 78F else 56.5F)) / SCALE,
y = (indexY + 4 + evoPopupYOffset) / SCALE,
width = 37,
height = 20,
@@ -464,7 +503,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
matrixStack = matrices,
texture = newMovesPopup,
x = (panelX + selectedOffsetX + 56.5F) / SCALE,
y = (indexY + 17 + movePopupYOffset) / SCALE,
y = (indexY + (if (expTextTransparency > 0) 4 else 17) + movePopupYOffset) / SCALE,
width = 37,
height = 20,
scale = SCALE,
@@ -19,10 +19,10 @@ object PartyOverlayDataControl {
const val BAR_UPDATE_BEFORE_TIME = 15
const val BAR_UPDATE_AFTER_TIME = 15
const val BAR_FLASH_TIME = 2
const val BAR_UPDATE_NO_LEVEL_TIME = BAR_UPDATE_BEFORE_TIME + BAR_UPDATE_AFTER_TIME + BAR_FLASH_TIME
val POPUP_TIME = FadeTimings(3, 40, 3)
const val LEVEL_UP_PORTRAIT_TIME = 10
val BAR_UPDATE_NO_LEVEL_TIME = BAR_UPDATE_BEFORE_TIME + BAR_UPDATE_AFTER_TIME + BAR_FLASH_TIME
val EXP_POPUP_TIME = FadeTimings(3, BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total() - 6, 3)
private enum class RenderTarget {
NONE,
@@ -35,21 +35,19 @@ object PartyOverlayDataControl {
private var barFillupSound: SoundInstance? = null
private var levelUpJingleSound: SoundInstance? = null
private var levelUpJingleSoundType: SoundEvent? = null
fun pokemonGainedExp(pokemonUuid : UUID, oldLevel: Int?, expGained: Int, countOfMovesLearned: Int, countOfEvosUnlocked: Int = 0) {
var time = BAR_UPDATE_NO_LEVEL_TIME
if (countOfMovesLearned > 0 || countOfEvosUnlocked > 0) {
time = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
}
val data = overlayData[pokemonUuid]
val newData = ExpGainedData(oldLevel, expGained, countOfMovesLearned, countOfEvosUnlocked).also { it.ticksMax = time }
val newData = ExpGainedData(oldLevel, expGained, countOfMovesLearned, countOfEvosUnlocked).also { it.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total() }
if (data != null) {
if (data.movesGainedData != null) {
if (data.movesGainedData != null) { //Absorb Data
newData.countOfMovesLearned += data.movesGainedData!!.movesCount
data.movesGainedData = null
}
if (data.evoGainedData != null) {
if (data.evoGainedData != null) { //Absorb Data
newData.countOfEvosUnlocked += data.evoGainedData!!.evolutionCount
data.evoGainedData = null
}
val expData = data.expGainedData
@@ -72,8 +70,7 @@ object PartyOverlayDataControl {
val data = overlayData[pokemonUuid]
val newData = EvolutionGainedData(evoCount).also { it.ticksMax = POPUP_TIME.total() }
if (data != null) {
if (data.expGainedData != null && data.expGainedData!!.ticks < BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME) {
data.expGainedData!!.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
if (data.expGainedData != null) {
data.expGainedData!!.countOfEvosUnlocked += evoCount
return
}
@@ -93,8 +90,7 @@ object PartyOverlayDataControl {
val data = overlayData[pokemonUuid]
val newData = MovesGainedData(movesCount).also { it.ticksMax = POPUP_TIME.total() }
if (data != null) {
if (data.expGainedData != null && data.expGainedData!!.ticks < BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME) {
data.expGainedData!!.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
if (data.expGainedData != null) {
data.expGainedData!!.countOfMovesLearned += movesCount
return
}
@@ -137,13 +133,28 @@ object PartyOverlayDataControl {
fun tick(paused: Boolean) {
val deletion = mutableListOf<UUID>()
if (paused) return
if (levelUpJingleSound != null && !Minecraft.getInstance().soundManager.isActive(levelUpJingleSound!!)) {
levelUpJingleSound = null
levelUpJingleSoundType = null
}
overlayData.forEach { (id, data) ->
if (data.expGainedData != null) { // We update this in order and render these in order, and as such we don't tick others
val expGainedData = data.expGainedData!!
expGainedData.ticks += 1
if (expGainedData.ticks == BAR_UPDATE_BEFORE_TIME && expGainedData.oldLevel != null) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.LEVELUP)
if (expGainedData.countOfEvosUnlocked > 0) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.EVOLUTION_NOTIFICATION)
levelUpJingleSoundType = CobblemonSounds.EVOLUTION_NOTIFICATION
} else {
if (levelUpJingleSoundType != CobblemonSounds.EVOLUTION_NOTIFICATION) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.LEVELUP)
levelUpJingleSoundType = CobblemonSounds.LEVELUP
}
}
}
if (expGainedData.ticks >= expGainedData.ticksMax) {
data.expGainedData = null
@@ -166,6 +177,7 @@ object PartyOverlayDataControl {
}
if (getCurrentRenderTarget(id) == RenderTarget.NONE) deletion.add(id)
}
deletion.forEach {
overlayData.remove(it)
}
@@ -216,7 +228,7 @@ object PartyOverlayDataControl {
//SOUNDS
private fun playSound(soundEvent: SoundEvent): SoundInstance? {
if (!PartyOverlay.canRender()) return null
val soundInstance = SimpleSoundInstance.forUI(soundEvent, 1F)
val soundInstance = SimpleSoundInstance.forUI(soundEvent, 1F, 1F)
Minecraft.getInstance().soundManager.play(soundInstance)
return soundInstance
}
@@ -47,6 +47,7 @@ fun drawProfilePokemon(
a: Float = 1F,
headYaw: Float = 0f,
headPitch: Float = 0f,
blockLight: Int = 13
) = drawProfilePokemon(
species = renderablePokemon.species.resourceIdentifier,
matrixStack = matrixStack,
@@ -63,6 +64,7 @@ fun drawProfilePokemon(
a = a,
headYaw = headYaw,
headPitch = headPitch,
blockLight = blockLight
)
fun drawProfilePokemon(
@@ -82,6 +84,7 @@ fun drawProfilePokemon(
a: Float = 1F,
headYaw: Float = 0f,
headPitch: Float = 0f,
blockLight: Int = 13
) {
RenderSystem.applyModelViewMatrix()
matrixStack.scale(scale, scale, -scale)
@@ -138,7 +141,7 @@ fun drawProfilePokemon(
val light1 = Vector3f(-1F, 1F, 1.0F)
val light2 = Vector3f(1.3F, -1F, 1.0F)
RenderSystem.setShaderLights(light1, light2)
val packedLight = LightTexture.pack(11, 7)
val packedLight = LightTexture.pack(blockLight, 0)
val colour = toHex(r, g, b, a)
model.withLayerContext(bufferSource, state, VaryingModelRepository.getLayers(species, state)) {
@@ -0,0 +1,153 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui
import com.cobblemon.mod.common.api.gui.blitk
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.events.GuiEventListener
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.network.chat.CommonComponents
import net.minecraft.resources.ResourceLocation
import kotlin.math.floor
open class VariableObjectSelectionList(x: Int, y: Int, width: Int, height: Int): AbstractWidget(x, y, width, height, CommonComponents.EMPTY) {
val children = mutableListOf<Entry>()
var listHeight = 0
var scrollAmount = 0.0
fun updateScroll(offset: Double) {
setScroll(scrollAmount + offset)
}
fun getMaxScroll(): Int {
return (listHeight - height).coerceAtLeast(0)
}
fun setScroll(scroll: Double) {
scrollAmount = scroll.coerceIn(0.0, getMaxScroll().toDouble())
}
fun removeEntry(index: Int) {
listHeight -= children[index].height
children.removeAt(index)
updatePositions()
}
fun removeEntry(entry: Entry) {
listHeight -= entry.height
children.remove(entry)
updatePositions()
}
fun removeLastEntry() {
listHeight -= children.last().height
children.removeLast()
}
fun updatePositions() {
var stackHeight = 0
children.forEach {
it.scrolledY = stackHeight
stackHeight += it.height
}
updateScroll(0.0)
}
fun addEntry(entry: Entry) {
children.add(entry)
entry.scrolledY = listHeight
listHeight += entry.height
}
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
var stackHeight = -scrollAmount.toInt()
guiGraphics.enableScissor(x, y, x + width, y + height)
children.forEachIndexed { index, entry ->
if (stackHeight in 1..<height || stackHeight + entry.height in 1..<height) {
entry.render(guiGraphics, index, x, y + stackHeight, mouseX, mouseY, partialTick)
}
stackHeight += entry.height
}
guiGraphics.disableScissor()
}
fun getEntryAtPosition(mouseX: Int, mouseY: Int): Entry? {
if (mouseX in x..(x + width) && mouseY in y..(y + height)) {
var stackHeight = -scrollAmount
children.forEach {
if (mouseY.toDouble() in (y + stackHeight)..(y + stackHeight + it.height)) {
return it
}
stackHeight += it.height
}
}
return null
}
fun ensureVisible(index: Int) {
val entry = children[index]
if (entry.scrolledY - scrollAmount < 0) {
setScroll(entry.scrolledY.toDouble())
} else if (entry.scrolledY + entry.height > height) {
setScroll(entry.scrolledY.toDouble() + entry.height)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
updateScroll(-verticalAmount * (listHeight / children.size / 2))
return true
}
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
val entry = getEntryAtPosition(pMouseX.toInt(), pMouseY.toInt())
if (entry != null) {
return entry.mouseClicked(pMouseX, pMouseY, pButton)
}
return false
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
return children.any {
it.mouseReleased(pMouseX, pMouseY, pButton)
} || super.mouseReleased(pMouseX, pMouseY, pButton)
}
fun getRowTop(entry: Entry): Int {
var stackHeight = -scrollAmount.toInt()
children.forEach {
if (it == entry) {
return y + stackHeight
}
stackHeight += it.height
}
return y
}
override fun defaultButtonNarrationText(pNarrationElementOutput: NarrationElementOutput) {}
override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {}
abstract inner class Entry(var height: Int): GuiEventListener {
var scrolledY: Int = 0
private var widgetFocused = false
override fun setFocused(focused: Boolean) {
this.widgetFocused = focused
}
override fun isFocused(): Boolean = widgetFocused
abstract fun render(guiGraphics: GuiGraphics, index: Int, x: Int, y: Int, mouseX: Int, mouseY: Int, partialTick: Float)
}
inner class EmptySeparator(height: Int): Entry(height) {
override fun render(guiGraphics: GuiGraphics, index: Int, x: Int, y: Int, mouseX: Int, mouseY: Int, partialTick: Float) {}
}
}
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.client.sounds.SoundManager
import net.minecraft.network.chat.CommonComponents
import net.minecraft.resources.ResourceLocation
class VariableObjectSelectionScrollbar(var parent: VariableObjectSelectionList, x: Int, y: Int, width: Int, height: Int): AbstractWidget(x, y, width, height, CommonComponents.EMPTY) {
companion object {
private val scrollerResource: ResourceLocation = ResourceLocation("minecraft", "textures/gui/sprites/widget/scroller.png")
private val scrollerBackgroundResource: ResourceLocation = ResourceLocation("minecraft", "textures/gui/sprites/widget/scroller_background.png")
private const val SCROLLER_HEIGHT = 32
}
private var scrolling = false
private var scrollHoldOffset = 0.0
fun scrollbarVisible(): Boolean = parent.getMaxScroll() > 0
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
if (scrollbarVisible()) {
val scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
blitk(
matrixStack = guiGraphics.pose(),
texture = scrollerBackgroundResource,
x = x,
y = y,
width = width,
height = height,
)
blitk(
matrixStack = guiGraphics.pose(),
texture = scrollerResource,
x = x,
y = y + scrollPosition,
width = width,
height = SCROLLER_HEIGHT,
textureWidth = 6,
textureHeight = SCROLLER_HEIGHT
)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
return parent.mouseScrolled(mouseX, mouseY, scrollX, scrollY)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX.toInt() in x..(x + width) && mouseY.toInt() in y..(y + height)) {
if (button == 0) {
var scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
if (mouseY !in (y + scrollPosition)..(y + scrollPosition + SCROLLER_HEIGHT)) {
val scrollRatio =
((mouseY - y - SCROLLER_HEIGHT / 2) / (height - SCROLLER_HEIGHT)).coerceIn(0.0, 1.0)
parent.setScroll(scrollRatio * parent.getMaxScroll())
scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
}
scrollHoldOffset = mouseY - y - scrollPosition
scrolling = true
return true
}
}
return false
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double): Boolean {
if (button == 0 && scrolling) {
val scrollRatio = ((mouseY - y - scrollHoldOffset) / (height - SCROLLER_HEIGHT)).coerceIn(0.0,1.0)
parent.setScroll(scrollRatio * parent.getMaxScroll())
return true
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && scrolling) {
scrolling = false
return true
}
return false
}
override fun playDownSound(handler: SoundManager) {}
override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {}
}
@@ -138,7 +138,8 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
pp = gimmickTemplate?.pp ?: moveTemplate.pp,
priority = gimmickTemplate?.priority ?: moveTemplate.priority,
critRatio = gimmickTemplate?.critRatio ?: moveTemplate.critRatio,
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances,
weight = gimmickTemplate?.weight ?: moveTemplate.weight
)
}
}
@@ -132,4 +132,4 @@ abstract class InfoTextScrollWidget(val pX: Int, val pY: Int): ScrollingWidget<I
return Component.literal(text)
}
}
}
}
@@ -375,7 +375,8 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state
state = state,
blockLight = 15
)
matrices.popPose()
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.pokemon.EVs
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.IVs
import com.cobblemon.mod.common.pokemon.Species
data class PokeEditDraft(
var species: Species,
var form: FormData = species.standardForm,
var shiny: Boolean = false,
var gender: Gender = defaultGenderFor(form),
var level: Int = 1,
var originalTrainer: String = "",
var abilityIndex: Int = 0,
var ivs: IVs = IVs(),
var evs: EVs = EVs(),
var cosmeticItem: String? = null,
var cosmeticAspects: Set<String> = emptySet(),
var heldItem: String? = null,
val forcedAspects: MutableSet<String> = mutableSetOf(),
val choiceFeatures: MutableMap<String, String> = mutableMapOf(),
val flagFeatures: MutableSet<String> = mutableSetOf(),
val activeMoves: MutableList<MoveTemplate?> = MutableList(4) { null },
val benchedMoves: MutableSet<MoveTemplate> = mutableSetOf()
) {
companion object {
fun defaultGenderFor(form: FormData): Gender {
return when {
form.maleRatio !in 0F..1F -> Gender.GENDERLESS
form.maleRatio == 0F -> Gender.FEMALE
else -> Gender.MALE
}
}
}
}
@@ -0,0 +1,600 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.client.gui.ExitButton
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_ICON_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAnimationListWidget
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAnimationTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditEVIVTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditGeneralTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditLocatorListWidget
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditLocatorTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditMovesTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditImplementationBlankTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditWalkSpeedTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditSpeciesList
import com.cobblemon.mod.common.client.gui.pokedex.widgets.SearchWidget
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditAction
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
import com.cobblemon.mod.common.pokemon.OriginalTrainerType
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
class PokeEditGUI private constructor() : Screen(Component.translatable("cobblemon.ui.pokeedit.title")) {
companion object {
private val screenBackground = cobblemonResource("textures/gui/pokedex/pokedex_screen.png")
private val baseBackground = cobblemonResource("textures/gui/pokedex/pokedex_base_red.png")
private val tabSelectArrow = cobblemonResource("textures/gui/pokedex/select_arrow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private val tabIcons = arrayOf(
cobblemonResource("textures/gui/pokedex/tab_info.png"),
cobblemonResource("textures/gui/pokedex/tab_stats.png"),
cobblemonResource("textures/gui/pokedex/tab_moves.png"),
cobblemonResource("textures/gui/pokedex/tab_species.png"),
cobblemonResource("textures/gui/pokedex/tab_locations.png"),
)
const val TAB_GENERAL = 0
const val TAB_EV_IV = 1
const val TAB_MOVES = 2
const val TAB_ASPECTS = 3
const val TAB_IMPLEMENTATION_BUTTON = 4
const val IMPL_TAB_LOCATORS = 0
const val IMPL_TAB_ANIMATION = 1
const val IMPL_TAB_SCALE = 2
const val IMPL_TAB_WALK_SPEED = 3
fun open() {
Minecraft.getInstance().setScreen(PokeEditGUI())
}
}
private val allSpecies = PokemonSpecies.species.sortedBy { it.nationalPokedexNumber }
private var filteredSpecies = allSpecies
private lateinit var searchWidget: SearchWidget
private lateinit var speciesList: PokeEditSpeciesList
private lateinit var locatorListWidget: PokeEditLocatorListWidget
private lateinit var animationListWidget: PokeEditAnimationListWidget
private var highlightedLocator: String? = null
private var animationTab: PokeEditAnimationTab? = null
private val tabButtons: MutableList<ScaledButton> = mutableListOf()
private var tabIndex = TAB_GENERAL
private var implementationMode = false
private var implementationTabIndex = IMPL_TAB_LOCATORS
private lateinit var tabElement: AbstractWidget
private lateinit var createButton: SummaryButton
private lateinit var spawnButton: SummaryButton
private lateinit var exitButton: ExitButton
private lateinit var implementationButton: SummaryButton
private lateinit var implementationExitButton: SummaryButton
private val implementationTabButtons: MutableList<SummaryButton> = mutableListOf()
private val draft = PokeEditDraft(allSpecies.firstOrNull() ?: PokemonSpecies.random())
override fun renderBlurredBackground(delta: Float) {}
override fun renderMenuBackground(context: GuiGraphics) {}
override fun isPauseScreen() = false
override fun init() {
super.init()
clearWidgets()
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
if (::searchWidget.isInitialized) removeWidget(searchWidget)
searchWidget = SearchWidget(x + 26, y + 28, 128, 11, update = ::updateFilters)
addRenderableWidget(searchWidget)
if (::speciesList.isInitialized) removeWidget(speciesList)
speciesList = PokeEditSpeciesList(x + 26, y + 39, ::setSelectedSpecies)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
locatorListWidget = PokeEditLocatorListWidget(
pX = x + 26,
pY = y + 190,
listWidth = 139,
listHeight = 151,
onSelectLocator = { name ->
highlightedLocator = name
(tabElement as? PokeEditLocatorTab)?.setHighlightedLocator(name)
}
)
locatorListWidget.setLocators(emptyList())
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
animationListWidget = PokeEditAnimationListWidget(
pX = x + 26,
pY = y + 190,
listWidth = 139,
listHeight = 151,
isPlaying = { name -> animationTab?.isAnimationPlaying(name) == true },
isLooping = { name -> animationTab?.isAnimationLooping(name) == true },
getDuration = { name -> animationTab?.getAnimationDuration(name) ?: 0F },
getProgress = { name -> animationTab?.getAnimationProgress(name) ?: 0F },
onPlay = { name -> animationTab?.playAnimation(name) },
onPause = { name -> animationTab?.pauseAnimation(name) },
onToggleLoop = { name, enabled -> animationTab?.setAnimationLooping(name, enabled) }
)
animationListWidget.setAnimations(emptyList())
setUpTabs()
setUpActionButtons()
setUpImplementationControls()
setImplementationMode(implementationMode, force = true)
updateFilters(true)
if (implementationMode) {
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
displayTab(tabIndex, recreate = true)
}
}
override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
renderBackground(context, mouseX, mouseY, delta)
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
blitk(
matrixStack = matrices,
texture = baseBackground,
x = x,
y = y,
width = BASE_WIDTH,
height = BASE_HEIGHT
)
blitk(
matrixStack = matrices,
texture = screenBackground,
x = x,
y = y,
width = BASE_WIDTH,
height = BASE_HEIGHT
)
drawScaledText(
context = context,
text = lang("ui.pokeedit.title"),
x = x + 192,
y = y + 15,
shadow = true
)
super.render(context, mouseX, mouseY, delta)
tabButtons.forEach { it.render(context, mouseX, mouseY, delta) }
if (!implementationMode) {
// Draw the tab arrow above overlay widgets so it stays visible.
blitk(
matrixStack = matrices,
texture = tabSelectArrow,
x = (x + 198 + (25 * tabIndex)) / SCALE,
y = (y + 177) / SCALE,
width = 12,
height = 6,
scale = SCALE
)
}
}
private fun updateFilters(init: Boolean = false) {
val search = searchWidget.value.trim()
filteredSpecies = if (search.isEmpty()) {
allSpecies
} else {
allSpecies.filter { species ->
species.translatedName.string.contains(search, true)
|| species.resourceIdentifier.path.contains(search, true)
}
}
speciesList.setEntries(filteredSpecies)
if (init || draft.species !in filteredSpecies) {
filteredSpecies.firstOrNull()?.let { setSelectedSpecies(it) }
}
}
private fun setSelectedSpecies(species: com.cobblemon.mod.common.pokemon.Species) {
if (draft.species == species) return
draft.species = species
draft.form = species.standardForm
draft.gender = PokeEditDraft.defaultGenderFor(draft.form)
draft.shiny = false
draft.abilityIndex = 0
val aspectOptions = PokeEditAspectsTab.collectAspectOptions(
species,
draft.form,
draft.gender,
draft.shiny
)
draft.forcedAspects.retainAll(aspectOptions.toggles)
draft.flagFeatures.retainAll(aspectOptions.flagKeys)
draft.choiceFeatures.entries.removeIf { (key, choice) ->
val group = aspectOptions.choiceGroups.firstOrNull { it.key == key } ?: return@removeIf true
choice !in group.choices
}
val cosmetic = aspectOptions.cosmeticChoices.firstOrNull { it.itemId.toString() == draft.cosmeticItem }
draft.cosmeticItem = cosmetic?.itemId?.toString()
draft.cosmeticAspects = cosmetic?.aspects?.toSet() ?: emptySet()
if (implementationMode) {
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
displayTab(tabIndex, recreate = true)
}
}
private fun setUpTabs() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
tabButtons.clear()
for (i in tabIcons.indices) {
tabButtons.add(
ScaledButton(
x + 197F + (i * 25F),
y + 181.5F,
TAB_ICON_SIZE,
TAB_ICON_SIZE,
resource = tabIcons[i],
clickAction = {
if (i == TAB_IMPLEMENTATION_BUTTON) {
setImplementationMode(true)
} else {
displayTab(i)
}
}
)
)
}
tabButtons.forEach(::addRenderableWidget)
}
private fun setUpActionButtons() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val actionY = y + 200F
val exitX = x + 312F
val spawnX = exitX - 56F
val createX = spawnX - 56F
val implX = x + 26F
if (::createButton.isInitialized) removeWidget(createButton)
createButton = SummaryButton(
buttonX = createX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { sendPokeEditAction(PokeEditAction.CREATE) },
text = lang("ui.pokeedit.give"),
resource = actionButtonResource
)
addRenderableWidget(createButton)
if (::spawnButton.isInitialized) removeWidget(spawnButton)
spawnButton = SummaryButton(
buttonX = spawnX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { sendPokeEditAction(PokeEditAction.SPAWN) },
text = lang("ui.pokeedit.spawn"),
resource = actionButtonResource
)
addRenderableWidget(spawnButton)
if (::exitButton.isInitialized) removeWidget(exitButton)
exitButton = ExitButton(pX = exitX.toInt(), pY = y + 200) {
Minecraft.getInstance().setScreen(null)
}
addRenderableWidget(exitButton)
if (::implementationButton.isInitialized) removeWidget(implementationButton)
implementationButton = SummaryButton(
buttonX = implX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { setImplementationMode(true) },
text = Component.literal("Impl"),
resource = actionButtonResource,
renderRequirement = { !implementationMode },
clickRequirement = { !implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
addRenderableWidget(implementationButton)
}
private fun setUpImplementationControls() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val sideX = (x - 60F).coerceAtLeast(6F)
if (::implementationExitButton.isInitialized) removeWidget(implementationExitButton)
implementationExitButton = SummaryButton(
buttonX = sideX,
buttonY = y + 28F,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { setImplementationMode(false) },
text = Component.literal("Back"),
resource = actionButtonResource,
renderRequirement = { implementationMode },
clickRequirement = { implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
addRenderableWidget(implementationExitButton)
implementationTabButtons.forEach(::removeWidget)
implementationTabButtons.clear()
val baseY = y + 48F
val labels = listOf(
Component.literal("Locators"),
Component.literal("Animation"),
Component.literal("Scale"),
Component.literal("Walk Speed")
)
labels.forEachIndexed { index, label ->
val button = SummaryButton(
buttonX = sideX,
buttonY = baseY + (index * 18F),
buttonWidth = 54,
buttonHeight = 15,
clickAction = { displayImplementationTab(index) },
text = label,
resource = actionButtonResource,
renderRequirement = { implementationMode },
clickRequirement = { implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
implementationTabButtons.add(button)
addRenderableWidget(button)
}
}
private fun displayTab(newIndex: Int, recreate: Boolean = false) {
if (implementationMode) {
return
}
if (tabIndex == newIndex && ::tabElement.isInitialized && !recreate) {
return
}
tabIndex = newIndex
if (::tabElement.isInitialized) removeWidget(tabElement)
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val tabX = x + 180
val tabY = y + 28
tabElement = when (newIndex) {
TAB_GENERAL -> PokeEditGeneralTab(tabX, tabY, draft) { displayTab(tabIndex, recreate = true) }
TAB_EV_IV -> PokeEditEVIVTab(tabX, tabY, draft)
TAB_MOVES -> PokeEditMovesTab(tabX, tabY, draft)
TAB_ASPECTS -> PokeEditAspectsTab(tabX, tabY, draft)
else -> PokeEditGeneralTab(tabX, tabY, draft) { displayTab(tabIndex, recreate = true) }
}
addRenderableWidget(tabElement)
bringControlsToFront()
}
private fun displayImplementationTab(newIndex: Int, recreate: Boolean = false) {
if (!implementationMode) {
return
}
if (implementationTabIndex == newIndex && ::tabElement.isInitialized && !recreate) {
return
}
implementationTabIndex = newIndex
if (::tabElement.isInitialized) removeWidget(tabElement)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
animationTab = null
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val tabX = x + 180
val tabY = y + 28
tabElement = when (newIndex) {
IMPL_TAB_LOCATORS -> PokeEditLocatorTab(
tabX,
tabY,
draft,
highlightedLocator,
::updateLocatorList
)
IMPL_TAB_ANIMATION -> PokeEditAnimationTab(tabX, tabY, draft) { animations ->
animationListWidget.setAnimations(animations)
}
IMPL_TAB_SCALE -> PokeEditImplementationBlankTab(tabX, tabY, draft, Component.literal("Scale"))
IMPL_TAB_WALK_SPEED -> PokeEditWalkSpeedTab(tabX, tabY, draft)
else -> PokeEditLocatorTab(
tabX,
tabY,
draft,
highlightedLocator,
::updateLocatorList
)
}
addRenderableWidget(tabElement)
when (newIndex) {
IMPL_TAB_LOCATORS -> addRenderableWidget(locatorListWidget)
IMPL_TAB_ANIMATION -> {
animationTab = tabElement as? PokeEditAnimationTab
animationListWidget.setAnimations(animationTab?.getAnimations().orEmpty())
addRenderableWidget(animationListWidget)
}
}
bringControlsToFront()
}
private fun setImplementationMode(enabled: Boolean, force: Boolean = false) {
if (!force && implementationMode == enabled) return
implementationMode = enabled
if (implementationMode) {
if (::searchWidget.isInitialized) removeWidget(searchWidget)
if (::speciesList.isInitialized) removeWidget(speciesList)
implementationTabIndex = IMPL_TAB_LOCATORS
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
if (::searchWidget.isInitialized) addRenderableWidget(searchWidget)
if (::speciesList.isInitialized) addRenderableWidget(speciesList)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
displayTab(tabIndex, recreate = true)
}
tabButtons.forEach { it.isVisible = !implementationMode }
if (!implementationMode) {
searchWidget.isFocused = false
}
bringControlsToFront()
}
private fun bringControlsToFront() {
if (::createButton.isInitialized) {
removeWidget(createButton)
addRenderableWidget(createButton)
}
if (::spawnButton.isInitialized) {
removeWidget(spawnButton)
addRenderableWidget(spawnButton)
}
if (::exitButton.isInitialized) {
removeWidget(exitButton)
addRenderableWidget(exitButton)
}
if (::implementationButton.isInitialized) {
removeWidget(implementationButton)
addRenderableWidget(implementationButton)
}
if (tabButtons.isEmpty()) return
tabButtons.forEach { removeWidget(it) }
tabButtons.forEach(::addRenderableWidget)
}
private fun sendPokeEditAction(action: PokeEditAction) {
val properties = buildDraftProperties()
val moveNames = draft.activeMoves.map { it?.name ?: "" }
val cosmeticItem = draft.cosmeticItem?.trim().orEmpty()
PokeEditActionPacket(action, properties.asString(), moveNames, cosmeticItem).sendToServer()
Minecraft.getInstance().setScreen(null)
}
private fun updateLocatorList(locators: Collection<String>) {
if (::locatorListWidget.isInitialized) {
locatorListWidget.setLocators(locators)
}
}
private fun buildDraftProperties(): PokemonProperties {
val props = PokemonProperties()
props.species = draft.species.resourceIdentifier.toString()
props.form = draft.form.formOnlyShowdownId()
props.shiny = draft.shiny
props.gender = draft.gender
props.level = draft.level
val ot = draft.originalTrainer.trim()
if (ot.isBlank()) {
props.originalTrainerType = OriginalTrainerType.NONE
} else {
props.originalTrainerType = OriginalTrainerType.NPC
props.originalTrainer = ot
}
props.heldItem = draft.heldItem?.takeIf { it.isNotBlank() }
props.ivs = draft.ivs
props.evs = draft.evs
val abilities = draft.form.abilities.map { it.template }
props.ability = abilities.getOrNull(draft.abilityIndex)?.name
val customProperties = mutableListOf<com.cobblemon.mod.common.api.properties.CustomPokemonProperty>()
draft.forcedAspects.mapNotNullTo(customProperties) { aspect -> AspectPropertyType.fromString(aspect) }
draft.flagFeatures.forEach { key ->
val provider = SpeciesFeatures.getFeature(key) as? FlagSpeciesFeatureProvider ?: return@forEach
provider.fromString("true")?.let { customProperties.add(it) }
}
draft.choiceFeatures.forEach { (key, choice) ->
val provider = SpeciesFeatures.getFeature(key) as? ChoiceSpeciesFeatureProvider ?: return@forEach
provider.fromString(choice)?.let { customProperties.add(it) }
}
props.customProperties = customProperties
props.updateAspects()
return props
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (focused !is EditBox && (minecraft?.options?.keyInventory?.matches(keyCode, scanCode) == true)) {
onClose()
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
return try {
super.mouseClicked(mouseX, mouseY, button)
} catch (_: ConcurrentModificationException) {
false
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (implementationMode) {
if (implementationTabIndex == IMPL_TAB_LOCATORS && ::locatorListWidget.isInitialized) {
if (locatorListWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)) return true
}
if (implementationTabIndex == IMPL_TAB_ANIMATION && ::animationListWidget.isInitialized) {
if (animationListWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)) return true
}
}
return super.mouseScrolled(mouseX, mouseY, amount, verticalAmount)
}
}
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.abilities.AbilityTemplate
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
import com.cobblemon.mod.common.client.gui.pokedex.widgets.InfoTextScrollWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
class PokeEditAbilityTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, 139, 163, lang("ui.pokeedit.abilities")) {
companion object {
private val arrowLeft = cobblemonResource("textures/gui/pokedex/info_arrow_left.png")
private val arrowRight = cobblemonResource("textures/gui/pokedex/info_arrow_right.png")
}
private val description = object : InfoTextScrollWidget(pX, pY + 60) {}
private val leftButton = ScaledButton(
pX + 2.5F,
pY + 30F,
7,
10,
arrowLeft,
clickAction = { switchAbility(false) }
).also { addWidget(it) }
private val rightButton = ScaledButton(
pX + 133F,
pY + 30F,
7,
10,
arrowRight,
clickAction = { switchAbility(true) }
).also { addWidget(it) }
private var abilitiesList: List<AbilityTemplate> = emptyList()
init {
refreshAbilities()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
refreshAbilities()
if (abilitiesList.isEmpty()) return
val ability = abilitiesList[draft.abilityIndex.coerceIn(0, abilitiesList.lastIndex)]
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = Component.translatable("cobblemon.ui.pokedex.info.ability", ability.displayName.asTranslated()).bold(),
x = x + 9,
y = y + 30,
shadow = true
)
description.setText(listOf(ability.description))
description.render(context, mouseX, mouseY, delta)
leftButton.render(context, mouseX, mouseY, delta)
rightButton.render(context, mouseX, mouseY, delta)
}
private fun refreshAbilities() {
val form = draft.form
abilitiesList = form.abilities.map { it.template }
if (draft.abilityIndex !in abilitiesList.indices) {
draft.abilityIndex = 0
}
}
private fun switchAbility(nextIndex: Boolean) {
if (abilitiesList.isEmpty()) return
val size = abilitiesList.size
draft.abilityIndex = if (nextIndex) {
(draft.abilityIndex + 1) % size
} else {
(draft.abilityIndex - 1 + size) % size
}
}
}
@@ -0,0 +1,362 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
import java.util.Locale
class PokeEditAnimationListWidget(
val pX: Int,
val pY: Int,
val listWidth: Int,
val listHeight: Int,
private val isPlaying: (String) -> Boolean,
private val isLooping: (String) -> Boolean,
private val getDuration: (String) -> Float,
private val getProgress: (String) -> Float,
private val onPlay: (String) -> Unit,
private val onPause: (String) -> Unit,
private val onToggleLoop: (String, Boolean) -> Unit
) : ScrollingWidget<PokeEditAnimationListWidget.AnimationRow>(
left = pX,
top = pY - listHeight,
width = listWidth,
height = listHeight,
slotHeight = ROW_HEIGHT
) {
companion object {
private const val ROW_HEIGHT = 12
private const val TEXT_SCALE = 0.5F
private const val BORDER_COLOR = 0xFF1B1B1B.toInt()
private const val FILL_COLOR = 0xFF3B3B3B.toInt()
private const val TEXT_COLOR = 0xFFFCFCFC.toInt()
private const val BUTTON_WIDTH = 12
private const val BUTTON_HEIGHT = 9
private const val BUTTON_GAP = 3
private const val BUTTON_PAD_RIGHT = 4
private const val BUTTON_PAD_TOP = 1
private const val BUTTON_ACTIVE = 0xFF6BD46B.toInt()
private const val BUTTON_INACTIVE = 0xFF5B5B5B.toInt()
private const val BUTTON_BORDER = 0xFF1B1B1B.toInt()
private const val LOOP_BOX_SIZE = 8
private const val LOOP_GAP = 3
private const val LOOP_LABEL_SCALE = 0.45F
private const val PROGRESS_HEIGHT = 2
private const val PROGRESS_BG = 0xFF2C2C2C.toInt()
private const val PROGRESS_FG = 0xFF8AD5FF.toInt()
private const val DURATION_SCALE = 0.45F
private const val DURATION_WIDTH = 28
}
private val listTop = pY - listHeight
private val listBottom = pY
private var draggingScrollbar = false
fun setAnimations(animations: Collection<String>) {
clearEntries()
animations.sorted().forEach { addEntry(AnimationRow(it)) }
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.fill(pX - 1, listTop - 1, pX + listWidth + 1, listBottom + 1, BORDER_COLOR)
context.fill(pX, listTop, pX + listWidth, listBottom, FILL_COLOR)
focused = if (mouseX in pX..(pX + listWidth) && mouseY in listTop..listBottom) {
getEntryAtPosition(mouseX.toDouble(), mouseY.toDouble())
} else {
null
}
context.enableScissor(pX, listTop, pX + listWidth, listBottom)
renderListItems(context, mouseX, mouseY, delta)
context.disableScissor()
if (maxScroll > 0) {
renderScrollbar(context, mouseX, mouseY, delta)
}
}
override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val rowX = rowLeft
for (index in 0 until itemCount) {
val rowY = getRowTop(index)
val rowBottom = getRowBottom(index)
if (rowBottom >= listTop && rowY <= listBottom) {
val entry = getEntry(index)
if (entry is AnimationRow) {
entry.isPlaying = isPlaying(entry.name)
entry.isLooping = isLooping(entry.name)
entry.durationSeconds = getDuration(entry.name)
entry.progress = getProgress(entry.name)
}
renderItem(context, mouseX, mouseY, delta, index, rowX, rowY, rowWidth, itemHeight)
}
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
val maxScroll = (itemCount * itemHeight - listHeight).coerceAtLeast(0)
if (maxScroll <= 0) return true
val step = if (verticalAmount != 0.0) verticalAmount else amount
scrollAmount = Mth.clamp(scrollAmount - step * itemHeight, 0.0, maxScroll.toDouble())
return true
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
|| button != 0
) {
return false
}
if (isOverScrollbar(mouseX, mouseY) && maxScroll > 0) {
draggingScrollbar = true
return true
}
val localY = mouseY.toInt() - listTop + scrollAmount.toInt()
if (localY < 0) return false
val index = localY / itemHeight
if (index !in 0 until itemCount) return false
val entry = getEntry(index)
if (entry is AnimationRow) {
val rowX = rowLeft
val rowY = getRowTop(index)
val (playX, pauseX, loopX, buttonY, durationX) = buttonPositions(rowX, rowY)
val inPlay = mouseX.toInt() in playX..(playX + BUTTON_WIDTH)
&& mouseY.toInt() in buttonY..(buttonY + BUTTON_HEIGHT)
val inPause = mouseX.toInt() in pauseX..(pauseX + BUTTON_WIDTH)
&& mouseY.toInt() in buttonY..(buttonY + BUTTON_HEIGHT)
val inLoop = mouseX.toInt() in loopX..(loopX + LOOP_BOX_SIZE)
&& mouseY.toInt() in buttonY..(buttonY + LOOP_BOX_SIZE)
if (inPlay) {
onPlay(entry.name)
return true
}
if (inPause) {
onPause(entry.name)
return true
}
if (inLoop) {
val newValue = !isLooping(entry.name)
onToggleLoop(entry.name, newValue)
return true
}
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && draggingScrollbar) {
draggingScrollbar = false
return true
}
return super.mouseReleased(mouseX, mouseY, button)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!draggingScrollbar || maxScroll <= 0) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
val barHeight = listBottom - listTop
var thumbHeight = ((barHeight * barHeight).toFloat() / maxPosition.toFloat()).toInt()
thumbHeight = Mth.clamp(thumbHeight, 16, barHeight - 4)
val trackHeight = barHeight - thumbHeight
if (trackHeight <= 0) return true
val clampedY = Mth.clamp(mouseY.toInt() - listTop - thumbHeight / 2, 0, trackHeight)
scrollAmount = (clampedY.toDouble() * maxScroll.toDouble()) / trackHeight.toDouble()
return true
}
override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
return mouseX in pX.toDouble()..(pX + listWidth).toDouble()
&& mouseY in listTop.toDouble()..listBottom.toDouble()
}
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val yStart = listTop
val barHeight = listBottom - listTop
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 16, barHeight - 4)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
if (yTop < yStart) yTop = yStart
context.fill(xLeft + 1, yStart, xRight - 1, listBottom, 0xFF7AEEF1.toInt())
context.fill(xLeft, yTop, xRight, yTop + yBottom, 0xFF3A96B6.toInt())
}
override fun getRowTop(index: Int): Int {
return listTop - scrollAmount.toInt() + (index * itemHeight)
}
override fun getRowBottom(index: Int): Int {
return getRowTop(index) + itemHeight
}
override fun getBottom(): Int {
return listBottom
}
private fun isOverScrollbar(mouseX: Double, mouseY: Double): Boolean {
val xLeft = scrollbarPosition
val xRight = xLeft + 3
return mouseX >= xLeft && mouseX <= xRight && mouseY >= listTop && mouseY <= listBottom
}
private fun buttonPositions(rowX: Int, rowY: Int): List<Int> {
val loopX = rowX + listWidth - BUTTON_PAD_RIGHT - LOOP_BOX_SIZE
val pauseX = loopX - LOOP_GAP - BUTTON_WIDTH
val playX = pauseX - BUTTON_GAP - BUTTON_WIDTH
val buttonY = rowY + BUTTON_PAD_TOP
val durationX = playX - BUTTON_GAP - DURATION_WIDTH
return listOf(playX, pauseX, loopX, buttonY, durationX)
}
class AnimationRow(val name: String) : Slot<AnimationRow>() {
var isPlaying: Boolean = false
var isLooping: Boolean = false
var durationSeconds: Float = 0F
var progress: Float = 0F
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
val bg = if (hovered) 0xFF2F2F2F.toInt() else null
if (bg != null) {
context.fill(x + 1, y, x + entryWidth - 1, y + ROW_HEIGHT, bg)
}
val loopX = x + entryWidth - BUTTON_PAD_RIGHT - LOOP_BOX_SIZE
val pauseX = loopX - LOOP_GAP - BUTTON_WIDTH
val playX = pauseX - BUTTON_GAP - BUTTON_WIDTH
val durationX = playX - BUTTON_GAP - DURATION_WIDTH
val buttonY = y + BUTTON_PAD_TOP
drawButton(context, playX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, !isPlaying)
drawPlayIcon(context, playX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT)
drawButton(context, pauseX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, isPlaying)
drawPauseIcon(context, pauseX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT)
drawLoopBox(context, loopX, buttonY, isLooping)
drawDuration(context, durationX, buttonY, durationSeconds)
drawScaledText(
context = context,
text = name.text(),
x = x + 3,
y = y + 2,
scale = TEXT_SCALE,
colour = TEXT_COLOR,
shadow = false
)
drawProgressBar(context, x + 2, y + ROW_HEIGHT - PROGRESS_HEIGHT, entryWidth - 4, progress)
}
override fun getNarration(): Component {
return Component.literal(name)
}
private fun drawButton(
context: GuiGraphics,
x: Int,
y: Int,
width: Int,
height: Int,
active: Boolean
) {
val fill = if (active) BUTTON_ACTIVE else BUTTON_INACTIVE
context.fill(x - 1, y - 1, x + width + 1, y + height + 1, BUTTON_BORDER)
context.fill(x, y, x + width, y + height, fill)
}
private fun drawLoopBox(context: GuiGraphics, x: Int, y: Int, enabled: Boolean) {
val fill = if (enabled) BUTTON_ACTIVE else BUTTON_INACTIVE
context.fill(x - 1, y - 1, x + LOOP_BOX_SIZE + 1, y + LOOP_BOX_SIZE + 1, BUTTON_BORDER)
context.fill(x, y, x + LOOP_BOX_SIZE, y + LOOP_BOX_SIZE, fill)
drawLoopIcon(context, x, y, LOOP_BOX_SIZE)
}
private fun drawPlayIcon(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
val iconWidth = 6
val iconHeight = 7
val startX = x + (width - iconWidth) / 2
val startY = y + (height - iconHeight) / 2
for (i in 0 until iconWidth) {
val top = startY + i / 2
val bottom = startY + iconHeight - i / 2
context.fill(startX + i, top, startX + i + 1, bottom, TEXT_COLOR)
}
}
private fun drawPauseIcon(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
val barWidth = 2
val barHeight = 7
val gap = 2
val startX = x + (width - (barWidth * 2 + gap)) / 2
val startY = y + (height - barHeight) / 2
context.fill(startX, startY, startX + barWidth, startY + barHeight, TEXT_COLOR)
context.fill(startX + barWidth + gap, startY, startX + barWidth * 2 + gap, startY + barHeight, TEXT_COLOR)
}
private fun drawLoopIcon(context: GuiGraphics, x: Int, y: Int, size: Int) {
val startX = x + 1
val startY = y + 1
val endX = x + size - 1
val endY = y + size - 1
context.fill(startX, startY, endX, startY + 1, TEXT_COLOR)
context.fill(startX, endY - 1, endX, endY, TEXT_COLOR)
context.fill(startX, startY, startX + 1, endY, TEXT_COLOR)
context.fill(endX - 1, startY, endX, endY, TEXT_COLOR)
context.fill(endX - 2, startY, endX, startY + 2, TEXT_COLOR)
}
private fun drawDuration(context: GuiGraphics, x: Int, y: Int, duration: Float) {
val text = String.format(Locale.US, "%.2fs", duration.coerceAtLeast(0F))
drawScaledText(
context = context,
text = text.text(),
x = x + DURATION_WIDTH / 2,
y = y + 2,
scale = DURATION_SCALE,
colour = TEXT_COLOR,
maxCharacterWidth = DURATION_WIDTH,
centered = true,
shadow = false
)
}
private fun drawProgressBar(context: GuiGraphics, x: Int, y: Int, width: Int, progress: Float) {
val clamped = Mth.clamp(progress, 0F, 1F)
context.fill(x, y, x + width, y + PROGRESS_HEIGHT, PROGRESS_BG)
context.fill(x, y, x + (width * clamped).toInt(), y + PROGRESS_HEIGHT, PROGRESS_FG)
}
}
}
@@ -0,0 +1,496 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PrimaryAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.util.toHex
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.platform.Lighting
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.client.Minecraft
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditAnimationTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
private val onAnimationsChanged: (List<String>) -> Unit
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, Component.literal("Animation")) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE, PoseType.STAND, PoseType.WALK, PoseType.SLEEP)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var availableAnimations: List<String> = emptyList()
private val activeAnimationsByName = mutableMapOf<String, ActiveAnimation>()
private val loopingAnimations = mutableMapOf<String, Boolean>()
private val animationStartTimes = mutableMapOf<String, Float>()
private val animationDurations = mutableMapOf<String, Float>()
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = context.pose(),
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
renderPokemon(context, delta)
refreshAnimationList()
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
fun getAnimations(): List<String> = availableAnimations
fun isAnimationPlaying(name: String): Boolean = activeAnimationsByName.containsKey(name)
fun isAnimationLooping(name: String): Boolean = loopingAnimations[name] == true
fun getAnimationDuration(name: String): Float {
return animationDurations[name] ?: resolveDuration(name).also { animationDurations[name] = it }
}
fun getAnimationProgress(name: String): Float {
val duration = getAnimationDuration(name)
if (duration <= 0F) return 0F
val start = animationStartTimes[name] ?: return 0F
val elapsed = state.animationSeconds - start
val raw = if (isAnimationLooping(name)) {
(elapsed % duration) / duration
} else {
elapsed / duration
}
return raw.coerceIn(0F, 1F)
}
fun playAnimation(name: String) {
if (activeAnimationsByName.containsKey(name)) return
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
val pose = resolvePoseAnimation(model, name) ?: return
PoseActiveAnimation(pose)
}
state.addActiveAnimation(active)
activeAnimationsByName[name] = active
animationStartTimes[name] = state.animationSeconds
animationDurations[name] = active.duration
}
fun pauseAnimation(name: String) {
val active = activeAnimationsByName.remove(name) ?: return
state.activeAnimations.remove(active)
animationStartTimes.remove(name)
}
fun setAnimationLooping(name: String, enabled: Boolean) {
loopingAnimations[name] = enabled
}
private fun refreshAnimationList() {
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val names = mutableSetOf<String>()
names.addAll(model.animations.keys)
model.poses.values.forEach { pose ->
names.addAll(pose.namedAnimations.keys)
names.add(pose.poseName)
}
val sorted = names.sorted()
if (sorted != availableAnimations) {
availableAnimations = sorted
loopingAnimations.keys.retainAll(sorted.toSet())
animationDurations.keys.retainAll(sorted.toSet())
animationStartTimes.keys.retainAll(sorted.toSet())
onAnimationsChanged(sorted)
}
}
private fun renderPokemon(context: GuiGraphics, delta: Float) {
buildRenderable()?.let { renderable ->
state.currentAspects = renderable.aspects
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
val matrices = context.pose()
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
renderAnimationModel(context, renderable, delta)
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
matrices.popPose()
context.disableScissor()
}
}
private fun renderAnimationModel(context: GuiGraphics, renderable: RenderablePokemon, delta: Float) {
val speciesId = renderable.species.resourceIdentifier
val model = VaryingModelRepository.getPoser(speciesId, state)
val texture = VaryingModelRepository.getTexture(speciesId, state)
val renderContext = RenderContext()
model.context = renderContext
VaryingModelRepository.getTextureNoSubstitute(speciesId, state).let { renderContext.put(RenderContext.TEXTURE, it) }
val baseScale = PokemonSpecies.getByIdentifier(speciesId)!!.getForm(state.currentAspects).baseScale
renderContext.put(RenderContext.SCALE, baseScale)
renderContext.put(RenderContext.SPECIES, speciesId)
renderContext.put(RenderContext.ASPECTS, state.currentAspects)
renderContext.put(RenderContext.RENDER_STATE, RenderContext.RenderState.PROFILE)
renderContext.put(RenderContext.POSABLE_STATE, state)
renderContext.put(RenderContext.DO_QUIRKS, false)
state.currentModel = model
if (state.currentPose == null) {
state.currentPose = model.getPose(PoseType.PROFILE)?.poseName
}
state.updatePartialTicks(delta)
RenderSystem.applyModelViewMatrix()
val matrices = context.pose()
val rotationVector = Vector3f(13F, rotationY, 0F)
val rotation = Quaternionf().fromEulerXYZDegrees(rotationVector)
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.mulPose(rotation)
model.setDefault()
state.preRender()
state.renderMarkers.clear()
runActiveAnimations(renderContext, model)
model.updateLocators(null, state)
Lighting.setupForEntityInInventory()
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val buffer = bufferSource.getBuffer(RenderType.entityCutout(texture))
val light1 = Vector3f(-1F, 1F, 1.0F)
val light2 = Vector3f(1.3F, -1F, 1.0F)
RenderSystem.setShaderLights(light1, light2)
val packedLight = LightTexture.pack(11, 7)
val colour = toHex(1F, 1F, 1F, 1F)
model.withLayerContext(bufferSource, state, VaryingModelRepository.getLayers(speciesId, state)) {
model.render(renderContext, matrices, buffer, packedLight, OverlayTexture.NO_OVERLAY, colour)
bufferSource.endBatch()
}
model.setDefault()
Lighting.setupFor3DItems()
matrices.popPose()
}
private fun runActiveAnimations(context: RenderContext, model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel) {
val toRestart = mutableListOf<String>()
val iterator = activeAnimationsByName.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
val animation = entry.value
val stillRunning = animation.run(
context,
model,
state,
0F,
0F,
state.animationSeconds,
0F,
0F,
1F
)
if (!stillRunning) {
state.activeAnimations.remove(animation)
iterator.remove()
animationStartTimes.remove(entry.key)
if (loopingAnimations[entry.key] == true) {
toRestart.add(entry.key)
}
}
}
if (toRestart.isNotEmpty()) {
toRestart.forEach { name ->
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
val pose = resolvePoseAnimation(model, name) ?: return@forEach
PoseActiveAnimation(pose)
}
state.addActiveAnimation(active)
activeAnimationsByName[name] = active
animationStartTimes[name] = state.animationSeconds
animationDurations[name] = active.duration
}
}
}
private fun resolvePoseAnimation(
model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel,
name: String
): Pose? {
return model.poses[name] ?: model.poses.values.firstOrNull { it.poseName.equals(name, ignoreCase = true) }
}
private fun resolveDuration(name: String): Float {
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
return 0F
}
return active.duration
}
private class PoseActiveAnimation(private val pose: Pose) : ActiveAnimation {
override val isTransition: Boolean = false
override val duration: Float = 0F
override val enduresPrimaryAnimations: Boolean = true
override fun run(
context: RenderContext,
model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel,
state: com.cobblemon.mod.common.client.render.models.blockbench.PosableState,
limbSwing: Float,
limbSwingAmount: Float,
ageInTicks: Float,
headYaw: Float,
headPitch: Float,
intensity: Float
): Boolean {
if (!pose.isSuitable(state)) return true
pose.apply(context, model, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch)
return true
}
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
val prev = renderablePokemon
if (prev == null || prev.species != species || prev.aspects != aspects) {
activeAnimationsByName.clear()
state.activeAnimations.clear()
}
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -0,0 +1,620 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.api.conditional.RegistryLikeIdentifierCondition
import com.cobblemon.mod.common.api.conditional.RegistryLikeTagCondition
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.substitute
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.language.I18n
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.util.Mth
class PokeEditAspectsTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.aspects")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val SLOT_HEIGHT = 16
private const val SCALE = 0.7F
private const val TOP_MARGIN = 10
private const val BOTTOM_MARGIN = 10
private const val COLUMN_GAP = 6
data class ChoiceGroup(
val key: String,
val choices: List<String>,
val aspectFormat: String
)
data class CosmeticChoice(
val itemId: ResourceLocation,
val aspects: List<String>,
val label: MutableComponent
)
data class AspectOptions(
val toggles: Set<String>,
val choiceGroups: List<ChoiceGroup>,
val flagKeys: Set<String>,
val cosmeticChoices: List<CosmeticChoice>
)
fun collectAspectOptions(
species: Species,
form: FormData? = null,
gender: Gender? = null,
shiny: Boolean? = null
): AspectOptions {
val toggles = mutableSetOf<String>()
val choiceGroups = mutableListOf<ChoiceGroup>()
val flagKeys = mutableSetOf<String>()
val forms = if (species.forms.isEmpty()) listOf(species.standardForm) else species.forms
forms.forEach { form -> toggles.addAll(form.aspects) }
SpeciesFeatures.getFeaturesFor(species).forEach { provider ->
when (provider) {
is ChoiceSpeciesFeatureProvider -> {
if (provider.isAspect && provider.keys.isNotEmpty() && provider.choices.isNotEmpty()) {
choiceGroups.add(ChoiceGroup(provider.keys.first(), provider.choices, provider.aspectFormat))
}
}
is FlagSpeciesFeatureProvider -> {
if (provider.isAspect && provider.keys.isNotEmpty()) {
flagKeys.add(provider.keys.first())
}
}
}
}
toggles.removeAll(flagKeys)
val filteredChoiceGroups = choiceGroups
.filterNot { it.key in setOf("cosmetic", "cosmetic_item", "cosmeticitem") }
.sortedBy { it.key }
return AspectOptions(
toggles = toggles.filter { it.isNotBlank() && it != "cosmetic" && it != "cosmetic_item" && it != "cosmeticitem" }.toSet(),
choiceGroups = filteredChoiceGroups,
flagKeys = flagKeys.filter { it.isNotBlank() }.toSet(),
cosmeticChoices = collectCosmeticChoices(species, form, gender, shiny)
)
}
fun collectCosmeticChoices(
species: Species,
form: FormData?,
gender: Gender?,
shiny: Boolean?
): List<CosmeticChoice> {
val pokemon = Pokemon().also {
it.isClient = true
it.species = species
it.form = form ?: species.standardForm
if (gender != null) it.gender = gender
if (shiny != null) it.shiny = shiny
}
val registry = BuiltInRegistries.ITEM
val choices = linkedMapOf<ResourceLocation, CosmeticChoice>()
CobblemonCosmeticItems.findValidForPokemon(pokemon).forEach { assignment ->
assignment.cosmeticItems.forEach { cosmetic ->
val aspects = cosmetic.aspects.filter { it.isNotBlank() }
when (val condition = cosmetic.consumedItem) {
is RegistryLikeIdentifierCondition<*> -> {
val id = condition.identifier
val label = registry.getOptional(id)
.map { it.defaultInstance.hoverName.copy() }
.orElseGet { Component.literal(id.toString()) }
choices.putIfAbsent(id, CosmeticChoice(id, aspects, label))
}
is RegistryLikeTagCondition<*> -> {
@Suppress("UNCHECKED_CAST")
val tag = condition.tag as? TagKey<net.minecraft.world.item.Item> ?: return@forEach
val tagItems = registry.getTag(tag).orElse(null) ?: return@forEach
tagItems.forEach { holder ->
val item = holder.value()
val id = registry.getKey(item) ?: return@forEach
val label = item.defaultInstance.hoverName.copy()
choices.putIfAbsent(id, CosmeticChoice(id, aspects, label))
}
}
}
}
}
return choices.values.sortedBy { it.label.string.lowercase() }
}
fun choiceAspectId(key: String, choice: String): String? {
val provider = SpeciesFeatures.getFeature(key) as? ChoiceSpeciesFeatureProvider ?: return null
return provider.aspectFormat.substitute("choice", choice)
}
}
private sealed class AspectItem {
data class Group(val group: ChoiceGroup) : AspectItem()
data class Toggle(val id: String, val source: ToggleSource) : AspectItem()
data class Cosmetic(val choices: List<CosmeticChoice>) : AspectItem()
}
private sealed class OptionItem {
data class Choice(val group: ChoiceGroup, val choice: String) : OptionItem()
data class ClearChoice(val group: ChoiceGroup) : OptionItem()
data class Toggle(val aspect: AspectItem.Toggle, val enabled: Boolean) : OptionItem()
data class CosmeticChoice(val choice: PokeEditAspectsTab.Companion.CosmeticChoice) : OptionItem()
object ClearCosmetic : OptionItem()
}
private enum class ToggleSource {
FORCED_ASPECT,
FLAG_FEATURE
}
private val listX = x + 4
private val listBottom = y + HEIGHT - BOTTOM_MARGIN
private val listAreaWidth = WIDTH - 8
private val listTop = y + TOP_MARGIN
private val listHeight = listBottom - listTop
private val leftWidth = (listAreaWidth - COLUMN_GAP) / 2
private val rightWidth = listAreaWidth - leftWidth - COLUMN_GAP
private val leftX = listX
private val rightX = listX + leftWidth + COLUMN_GAP
private val aspectList = AspectList(
leftX,
listTop,
leftWidth,
listHeight
).also { addWidget(it) }
private val optionList = OptionList(
rightX,
listTop,
rightWidth,
listHeight
).also { addWidget(it) }
private var aspectOptions: AspectOptions = collectAspectOptions(
draft.species,
draft.form,
draft.gender,
draft.shiny
)
private var selectedAspect: AspectItem? = null
private var lastSpeciesId: ResourceLocation? = null
init {
rebuildAspectEntries()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val currentSpeciesId = draft.species.resourceIdentifier
if (lastSpeciesId != currentSpeciesId) {
refreshAspects()
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.aspects").bold(),
x = x + (WIDTH / 2.0),
y = y + 2,
scale = SCALE,
centered = true
)
context.fill(
leftX,
listTop,
leftX + leftWidth,
listBottom,
0x22000000
)
context.fill(
rightX,
listTop,
rightX + rightWidth,
listBottom,
0x22000000
)
aspectList.render(context, mouseX, mouseY, delta)
optionList.render(context, mouseX, mouseY, delta)
}
private fun refreshAspects() {
aspectOptions = collectAspectOptions(
draft.species,
draft.form,
draft.gender,
draft.shiny
)
val cosmetic = aspectOptions.cosmeticChoices.firstOrNull { it.itemId.toString() == draft.cosmeticItem }
draft.cosmeticItem = cosmetic?.itemId?.toString()
draft.cosmeticAspects = cosmetic?.aspects?.toSet() ?: emptySet()
val valid = buildAspectEntries()
if (selectedAspect !in valid) {
selectedAspect = valid.firstOrNull()
}
rebuildAspectEntries()
lastSpeciesId = draft.species.resourceIdentifier
}
private fun buildAspectEntries(): List<AspectItem> {
val entries = mutableListOf<AspectItem>()
aspectOptions.choiceGroups.sortedBy { it.key }.forEach { entries.add(AspectItem.Group(it)) }
aspectOptions.flagKeys.sortedBy { it }.forEach { entries.add(AspectItem.Toggle(it, ToggleSource.FLAG_FEATURE)) }
aspectOptions.toggles.sortedBy { it }.forEach { entries.add(AspectItem.Toggle(it, ToggleSource.FORCED_ASPECT)) }
return entries
}
private fun rebuildAspectEntries() {
val entries = buildAspectEntries()
aspectList.setEntries(entries)
if (selectedAspect == null) {
selectedAspect = entries.firstOrNull()
}
rebuildOptionEntries()
}
private fun rebuildOptionEntries() {
optionList.setEntries(buildOptionEntries(selectedAspect))
}
private fun buildOptionEntries(aspect: AspectItem?): List<OptionItem> {
if (aspect == null) return emptyList()
return when (aspect) {
is AspectItem.Group -> {
val entries = mutableListOf<OptionItem>()
entries.add(OptionItem.ClearChoice(aspect.group))
aspect.group.choices.forEach { choice ->
entries.add(OptionItem.Choice(aspect.group, choice))
}
entries
}
is AspectItem.Cosmetic -> {
val entries = mutableListOf<OptionItem>()
entries.add(OptionItem.ClearCosmetic)
aspect.choices.forEach { choice ->
entries.add(OptionItem.CosmeticChoice(choice))
}
entries
}
is AspectItem.Toggle -> {
listOf(
OptionItem.Toggle(aspect, enabled = true),
OptionItem.Toggle(aspect, enabled = false)
)
}
}
}
private fun humanizeId(raw: String): String {
return raw
.replace('_', ' ')
.replace('-', ' ')
.split(" ")
.filter { it.isNotBlank() }
.joinToString(" ") { part ->
part.lowercase().replaceFirstChar { ch -> if (ch.isLowerCase()) ch.titlecase() else ch.toString() }
}
}
private fun labelForId(id: String): MutableComponent {
val key = "cobblemon.ui.aspect.$id"
return if (I18n.exists(key)) {
Component.translatable(key)
} else {
Component.literal(humanizeId(id))
}
}
private fun isAspectActive(entry: AspectItem): Boolean = when (entry) {
is AspectItem.Toggle -> when (entry.source) {
ToggleSource.FORCED_ASPECT -> entry.id in draft.forcedAspects
ToggleSource.FLAG_FEATURE -> entry.id in draft.flagFeatures
}
is AspectItem.Group -> entry.group.key in draft.choiceFeatures
is AspectItem.Cosmetic -> draft.cosmeticItem != null
}
private fun labelFor(entry: AspectItem): MutableComponent = when (entry) {
is AspectItem.Group -> {
val name = labelForId(entry.group.key).string
val choice = draft.choiceFeatures[entry.group.key]?.let { labelForId(it).string }
Component.literal(if (choice != null) "$name: $choice" else name)
}
is AspectItem.Toggle -> labelForId(entry.id)
is AspectItem.Cosmetic -> {
val base = lang("cosmetic_item").string
val choice = draft.cosmeticItem?.let { id ->
entry.choices.firstOrNull { it.itemId.toString() == id }?.label?.string
}
Component.literal(if (choice != null) "$base: $choice" else base)
}
}
private fun labelFor(option: OptionItem): MutableComponent = when (option) {
is OptionItem.ClearChoice -> lang("ui.none")
is OptionItem.Choice -> labelForId(option.choice)
is OptionItem.Toggle -> if (option.enabled) lang("ui.enabled") else lang("ui.disabled")
is OptionItem.CosmeticChoice -> option.choice.label
OptionItem.ClearCosmetic -> lang("ui.none")
}
private fun selectAspect(entry: AspectItem) {
if (selectedAspect == entry) return
selectedAspect = entry
rebuildOptionEntries()
}
private fun handleOption(option: OptionItem) {
when (option) {
is OptionItem.ClearChoice -> {
draft.choiceFeatures.remove(option.group.key)
}
is OptionItem.Choice -> {
draft.choiceFeatures[option.group.key] = option.choice
}
is OptionItem.Toggle -> {
when (option.aspect.source) {
ToggleSource.FORCED_ASPECT -> {
if (option.enabled) {
draft.forcedAspects.add(option.aspect.id)
} else {
draft.forcedAspects.remove(option.aspect.id)
}
}
ToggleSource.FLAG_FEATURE -> {
if (option.enabled) {
draft.flagFeatures.add(option.aspect.id)
} else {
draft.flagFeatures.remove(option.aspect.id)
}
}
}
}
is OptionItem.CosmeticChoice -> {
draft.cosmeticItem = option.choice.itemId.toString()
draft.cosmeticAspects = option.choice.aspects.toSet()
}
OptionItem.ClearCosmetic -> {
draft.cosmeticItem = null
draft.cosmeticAspects = emptySet()
}
}
}
private inner class AspectList(
pX: Int,
pY: Int,
width: Int,
height: Int
) : ScrollingWidget<AspectList.AspectEntry>(
width = width,
height = height,
left = pX,
top = pY - height,
slotHeight = SLOT_HEIGHT + 2,
scrollBarWidth = 3
) {
private val listX = pX
fun setEntries(aspects: List<AspectItem>) {
clearEntries()
aspects.forEach { aspect -> addEntry(AspectEntry(aspect)) }
scrollAmount = 0.0
}
override fun getScrollbarPosition(): Int = listX + width - 3
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val barHeight = this.bottom - this.y
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
if (yTop < this.y) yTop = this.y
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
inner class AspectEntry(
private val entry: AspectItem
) : Slot<AspectEntry>() {
private var x: Int = 0
private var y: Int = 0
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y + 1
if (selectedAspect == entry) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
} else if (isAspectActive(entry)) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x4436B33F)
} else if (hovered) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = labelFor(entry),
x = this.x + 4,
y = this.y + 3,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isMouseOver(mouseX, mouseY)) {
selectAspect(entry)
return true
}
return false
}
override fun getNarration(): Component {
return labelFor(entry)
}
}
}
private inner class OptionList(
pX: Int,
pY: Int,
width: Int,
height: Int
) : ScrollingWidget<OptionList.OptionEntry>(
width = width,
height = height,
left = pX,
top = pY - height,
slotHeight = SLOT_HEIGHT + 2,
scrollBarWidth = 3
) {
private val listX = pX
fun setEntries(options: List<OptionItem>) {
clearEntries()
options.forEach { option -> addEntry(OptionEntry(option)) }
scrollAmount = 0.0
}
override fun getScrollbarPosition(): Int = listX + width - 3
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val barHeight = this.bottom - this.y
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
if (yTop < this.y) yTop = this.y
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
inner class OptionEntry(
private val option: OptionItem
) : Slot<OptionEntry>() {
private var x: Int = 0
private var y: Int = 0
private fun isSelectedOption(): Boolean = when (option) {
is OptionItem.ClearChoice -> {
draft.choiceFeatures[option.group.key] == null
}
is OptionItem.Choice -> {
draft.choiceFeatures[option.group.key] == option.choice
}
is OptionItem.Toggle -> {
val isEnabled = when (option.aspect.source) {
ToggleSource.FORCED_ASPECT -> option.aspect.id in draft.forcedAspects
ToggleSource.FLAG_FEATURE -> option.aspect.id in draft.flagFeatures
}
isEnabled == option.enabled
}
is OptionItem.CosmeticChoice -> {
draft.cosmeticItem == option.choice.itemId.toString()
}
OptionItem.ClearCosmetic -> {
draft.cosmeticItem == null
}
}
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y + 1
if (isSelectedOption()) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x4436B33F)
} else if (hovered) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = labelFor(option),
x = this.x + 4,
y = this.y + 3,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isMouseOver(mouseX, mouseY)) {
handleOption(option)
return true
}
return false
}
override fun getNarration(): Component {
return labelFor(option)
}
}
}
}
@@ -0,0 +1,335 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.pokemon.EVs
import com.cobblemon.mod.common.pokemon.IVs
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.BufferUploader
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.Tesselator
import com.mojang.blaze3d.vertex.VertexFormat
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.MutableComponent
import net.minecraft.world.phys.Vec2
import org.joml.Vector3f
class PokeEditEVIVTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.pokeedit.stats")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val CHART_WIDTH = 134
private const val CHART_HEIGHT = 148
private const val SCALE = 0.5F
private const val TAB_MARKER_WIDTH = 8
private const val TAB_IV = 0
private const val TAB_EV = 1
private val statsBaseResource = cobblemonResource("textures/gui/summary/summary_stats_chart_base.png")
private val statsChartResource = cobblemonResource("textures/gui/summary/summary_stats_chart.png")
private val tabMarkerResource = cobblemonResource("textures/gui/summary/summary_stats_tab_marker.png")
private val statLabels = listOf(
lang("ui.stats.hp").bold() to Stats.HP,
lang("ui.stats.atk").bold() to Stats.ATTACK,
lang("ui.stats.def").bold() to Stats.DEFENCE,
lang("ui.stats.speed").bold() to Stats.SPEED,
lang("ui.stats.sp_def").bold() to Stats.SPECIAL_DEFENCE,
lang("ui.stats.sp_atk").bold() to Stats.SPECIAL_ATTACK
)
private val hexagonVerticesOffset = listOf(
Pair(67.0, 10.5), // 12 o'clock
Pair(122.0, 42.5), // 2 o'clock
Pair(122.0, 93.5), // 4 o'clock
Pair(67.0, 124.5), // 6 o'clock
Pair(12.0, 93.5), // 8 o'clock
Pair(12.0, 42.5) // 10 o'clock
)
}
private var tabIndex = TAB_IV
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val chartX = x + 2
val chartY = y + 6
blitk(
matrixStack = context.pose(),
texture = statsBaseResource,
x = chartX,
y = chartY,
width = CHART_WIDTH,
height = CHART_HEIGHT
)
blitk(
matrixStack = context.pose(),
texture = statsChartResource,
x = (chartX + 25.5) / SCALE,
y = (chartY + 22) / SCALE,
width = 166,
height = 192,
scale = SCALE
)
if (tabIndex == TAB_IV) {
drawStatPolygon(
chartX.toFloat(),
chartY.toFloat(),
statLabels.map { (_, stat) -> draft.ivs.getOrDefault(stat).toFloat() / IVs.MAX_VALUE.toFloat() },
colour = Vector3f(216F / 255F, 100F / 255F, 1F)
)
} else {
drawStatPolygon(
chartX.toFloat(),
chartY.toFloat(),
statLabels.map { (_, stat) -> draft.evs.getOrDefault(stat).toFloat() / EVs.MAX_STAT_VALUE.toFloat() },
colour = Vector3f(1F, 1F, 100F / 255F)
)
}
val labelsHovered = statLabelsHovered(chartX, chartY, mouseX, mouseY)
val labels = statLabels.map { (label, stat) ->
if (labelsHovered) {
if (tabIndex == TAB_IV) {
"${draft.ivs.getOrDefault(stat)}/${IVs.MAX_VALUE}".text()
} else {
"${draft.evs.getOrDefault(stat)}/${EVs.MAX_STAT_VALUE}".text()
}
} else {
label
}
}
renderPolygonLabels(
context = context,
chartX = chartX.toDouble(),
chartY = chartY.toDouble(),
labels = labels,
verticesOffset = hexagonVerticesOffset
)
val values = statLabels.map { (_, stat) ->
if (tabIndex == TAB_IV) {
draft.ivs.getOrDefault(stat).toString().text()
} else {
draft.evs.getOrDefault(stat).toString().text()
}
}
renderPolygonLabels(
context = context,
chartX = chartX.toDouble(),
chartY = chartY.toDouble(),
labels = values,
verticesOffset = hexagonVerticesOffset,
offsetY = 5.5
)
if (tabIndex == TAB_EV) {
drawScaledText(
context = context,
text = lang("ui.pokeedit.evs.total", draft.evs.total(), EVs.MAX_TOTAL_VALUE),
x = chartX + 10,
y = chartY + 126,
scale = SCALE,
shadow = true
)
}
renderTabs(context, chartX, chartY)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val tabIndexAtPos = getTabIndexFromPos(mouseX, mouseY)
if (tabIndexAtPos != -1 && tabIndexAtPos != tabIndex) {
tabIndex = tabIndexAtPos
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
val chartX = x + 2
val chartY = y + 6
val stat = getHoveredStat(chartX, chartY, mouseX.toInt(), mouseY.toInt()) ?: return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
val step = when {
Screen.hasControlDown() -> 25
Screen.hasShiftDown() -> 10
else -> 1
}
val delta = if (verticalAmount > 0) step else -step
if (tabIndex == TAB_IV) {
val current = draft.ivs.getOrDefault(stat)
val next = (current + delta).coerceIn(0, IVs.MAX_VALUE)
draft.ivs[stat] = next
} else {
val current = draft.evs.getOrDefault(stat)
val totalWithout = draft.evs.total() - current
val maxForStat = minOf(EVs.MAX_STAT_VALUE, EVs.MAX_TOTAL_VALUE - totalWithout)
val next = (current + delta).coerceIn(0, maxForStat)
draft.evs[stat] = next
}
return true
}
private fun renderTabs(context: GuiGraphics, chartX: Int, chartY: Int) {
val tabWidth = 40
val tabLeft = chartX + ((CHART_WIDTH - (tabWidth * 2)) / 2)
val tabCenters = listOf(
tabLeft + (tabWidth / 2),
tabLeft + tabWidth + (tabWidth / 2)
)
val tabY = chartY + 137
drawScaledText(
context = context,
text = lang("ui.stats.ivs").bold(),
x = tabCenters[0],
y = tabY,
scale = SCALE,
colour = if (tabIndex == TAB_IV) 0x00FFFFFF else 0x008F8F8F,
centered = true
)
drawScaledText(
context = context,
text = lang("ui.stats.evs").bold(),
x = tabCenters[1],
y = tabY,
scale = SCALE,
colour = if (tabIndex == TAB_EV) 0x00FFFFFF else 0x008F8F8F,
centered = true
)
blitk(
matrixStack = context.pose(),
texture = tabMarkerResource,
x = (tabCenters[tabIndex] - (TAB_MARKER_WIDTH / 2)) / SCALE,
y = (tabY - 3) / SCALE,
width = TAB_MARKER_WIDTH,
height = 4,
scale = SCALE
)
}
private fun getTabIndexFromPos(mouseX: Double, mouseY: Double): Int {
val chartX = x + 2
val chartY = y + 6
val tabWidth = 40
val tabLeft = chartX + ((CHART_WIDTH - (tabWidth * 2)) / 2)
val tabTop = chartY + 134.0
val tabBottom = tabTop + 9.0
if (mouseY !in tabTop..tabBottom) return -1
if (mouseX in tabLeft.toDouble()..(tabLeft + tabWidth).toDouble()) return TAB_IV
if (mouseX in (tabLeft + tabWidth).toDouble()..(tabLeft + tabWidth * 2).toDouble()) return TAB_EV
return -1
}
private fun getHoveredStat(chartX: Int, chartY: Int, mouseX: Int, mouseY: Int): Stat? {
statLabels.forEachIndexed { index, (_, stat) ->
val pos = hexagonVerticesOffset[index]
if (mouseX.toDouble() in (chartX + pos.first - 12)..(chartX + pos.first + 12)
&& mouseY.toDouble() in (chartY + pos.second - 1)..(chartY + pos.second + 10)
) {
return stat
}
}
return null
}
private fun statLabelsHovered(chartX: Int, chartY: Int, mouseX: Int, mouseY: Int): Boolean {
return getHoveredStat(chartX, chartY, mouseX, mouseY) != null
}
private fun renderPolygonLabels(
context: GuiGraphics,
chartX: Double,
chartY: Double,
labels: List<MutableComponent>,
verticesOffset: List<Pair<Double, Double>>,
offsetY: Double = 0.0
) {
if (labels.size != verticesOffset.size) return
labels.forEachIndexed { index, label ->
drawScaledText(
context = context,
text = label,
x = chartX + verticesOffset[index].first,
y = chartY + verticesOffset[index].second + offsetY,
scale = SCALE,
centered = true
)
}
}
private fun drawTriangle(colour: Vector3f, v1: Vec2, v2: Vec2, v3: Vec2, opacity: Float = 0.6F) {
RenderSystem.setShaderTexture(0, com.cobblemon.mod.common.client.CobblemonResources.WHITE)
RenderSystem.setShaderColor(colour.x, colour.y, colour.z, opacity)
val bufferBuilder = Tesselator.getInstance().begin(VertexFormat.Mode.TRIANGLES, DefaultVertexFormat.POSITION)
bufferBuilder.addVertex(v1.x, v1.y, 10F)
bufferBuilder.addVertex(v2.x, v2.y, 10F)
bufferBuilder.addVertex(v3.x, v3.y, 10F)
BufferUploader.drawWithShader(bufferBuilder.buildOrThrow())
RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
}
private fun drawStatPolygon(chartX: Float, chartY: Float, ratios: List<Float>, colour: Vector3f) {
if (ratios.size != 6) return
val topVertexY = chartY + 22F
val centerX = chartX + 67F
val centerY = topVertexY + 48F
val radius = centerY - topVertexY
val startAngleDeg = -90.0
val angleStepDeg = 360.0 / 6
val anglesDeg = List(6) { i -> startAngleDeg + i * angleStepDeg }
val anglesRad = anglesDeg.map { Math.toRadians(it) }
val maxPoints = anglesRad.map { angle ->
Vec2(
centerX + radius * Math.cos(angle).toFloat(),
centerY + radius * Math.sin(angle).toFloat()
)
}
val coercedRatios = ratios.map { it.coerceIn(5 / radius, 1F) }
val vertices = maxPoints.mapIndexed { index, maxPoint ->
Vec2(
centerX + (maxPoint.x - centerX) * coercedRatios[index],
centerY + (maxPoint.y - centerY) * coercedRatios[index]
)
}
val centerPoint = Vec2(centerX, centerY)
RenderSystem.disableDepthTest()
for (i in vertices.indices) {
val nextIndex = (i + 1) % vertices.size
drawTriangle(colour, vertices[i], centerPoint, vertices[nextIndex])
}
RenderSystem.enableDepthTest()
}
}
@@ -0,0 +1,354 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.mojang.blaze3d.platform.Lighting
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditImplementationBlankTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
title: Component
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, title) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val CUBE_BASE_OFFSET_X = 1.0F
private const val CUBE_SIZE_OFFSET_MULTIPLIER = 0.18F
private const val CUBE_OFFSET_Z = -0.5F
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
DoubleValue(hasNonWearableAttachment())
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(hasWearableHatAttachment())
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(hasWearableFaceAttachment())
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var attachmentStacks: List<ItemStack> = emptyList()
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = context.pose(),
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
buildRenderable()?.let { renderable ->
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
val matrices = context.pose()
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
val rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, rotationY, 0F))
val cubeOffsetX = getCubeOffsetX()
matrices.translate((-cubeOffsetX / 2F).toDouble(), 0.0, 0.0)
renderReferenceCube(renderable, matrices, rotation, cubeOffsetX)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf(rotation),
state = state,
scale = MODEL_DRAW_SCALE
)
if (attachmentStacks.isNotEmpty()) {
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
attachmentStacks.forEach { stack ->
heldItemRenderer.renderOnModel(
stack,
state,
matrices,
bufferSource,
light = 0xF000F0,
frontLight = true
)
}
bufferSource.endBatch()
}
matrices.popPose()
context.disableScissor()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = parseItemStack(draft.heldItem)
val cosmeticStack = parseItemStack(draft.cosmeticItem)
attachmentStacks = buildAttachmentStacks(heldStack, cosmeticStack)
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun parseItemStack(rawId: String?): ItemStack {
return rawId
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
}
private fun buildAttachmentStacks(heldStack: ItemStack, cosmeticStack: ItemStack): List<ItemStack> {
if (heldStack.isEmpty && cosmeticStack.isEmpty) return emptyList()
val stacks = mutableListOf<ItemStack>()
if (!heldStack.isEmpty) {
stacks.add(heldStack)
}
if (!cosmeticStack.isEmpty && (heldStack.isEmpty || cosmeticStack.item != heldStack.item)) {
stacks.add(cosmeticStack)
}
return stacks
}
private fun hasNonWearableAttachment(): Boolean {
return attachmentStacks.any { stack ->
!stack.isEmpty && !stack.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !stack.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
}
}
private fun hasWearableHatAttachment(): Boolean {
return attachmentStacks.any { stack -> !stack.isEmpty && stack.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) }
}
private fun hasWearableFaceAttachment(): Boolean {
return attachmentStacks.any { stack -> !stack.isEmpty && stack.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS) }
}
private fun renderReferenceCube(
renderable: RenderablePokemon,
matrices: com.mojang.blaze3d.vertex.PoseStack,
rotation: Quaternionf,
cubeOffsetX: Float
) {
val model = VaryingModelRepository.getPoser(renderable.species.resourceIdentifier, state)
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.mulPose(rotation)
matrices.mulPose(com.mojang.math.Axis.XP.rotationDegrees(180F))
matrices.translate(cubeOffsetX.toDouble(), 0.0, CUBE_OFFSET_Z.toDouble())
RenderSystem.applyModelViewMatrix()
RenderSystem.enableDepthTest()
RenderSystem.disableCull()
Lighting.setupForEntityInInventory()
Minecraft.getInstance().blockRenderer.renderSingleBlock(
Blocks.RED_WOOL.defaultBlockState(),
matrices,
bufferSource,
LightTexture.pack(15, 15),
OverlayTexture.NO_OVERLAY
)
bufferSource.endBatch()
Lighting.setupFor3DItems()
RenderSystem.enableCull()
matrices.popPose()
}
private fun getCubeOffsetX(): Float {
val hitboxWidth = draft.form.hitbox.width * draft.form.baseScale
val hitboxHeight = draft.form.hitbox.height * draft.form.baseScale
val hitboxSize = maxOf(hitboxWidth, hitboxHeight)
val extraOffset = (hitboxSize - 1.0F).coerceAtLeast(0.0F) * CUBE_SIZE_OFFSET_MULTIPLIER
return CUBE_BASE_OFFSET_X + extraOffset
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -0,0 +1,227 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
class PokeEditLocatorListWidget(
val pX: Int,
val pY: Int,
val listWidth: Int,
val listHeight: Int,
private val onSelectLocator: (String?) -> Unit
) : ScrollingWidget<PokeEditLocatorListWidget.LocatorRow>(
left = pX,
top = pY - listHeight,
width = listWidth,
height = listHeight,
slotHeight = ROW_HEIGHT
) {
companion object {
private const val ROW_HEIGHT = 9
private const val TEXT_SCALE = 0.5F
private const val BORDER_COLOR = 0xFF1B1B1B.toInt()
private const val FILL_COLOR = 0xFF3B3B3B.toInt()
private const val TEXT_COLOR = 0xFFFCFCFC.toInt()
}
private val listTop = pY - listHeight
private val listBottom = pY
private var selectedLocator: String? = null
private var draggingScrollbar = false
fun setLocators(locators: Collection<String>) {
clearEntries()
locators.sorted().forEach { addEntry(LocatorRow(it)) }
if (selectedLocator != null && locators.none { it == selectedLocator }) {
selectedLocator = null
onSelectLocator(null)
}
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.fill(pX - 1, listTop - 1, pX + listWidth + 1, listBottom + 1, BORDER_COLOR)
context.fill(pX, listTop, pX + listWidth, listBottom, FILL_COLOR)
focused = if (mouseX in pX..(pX + listWidth) && mouseY in listTop..listBottom) {
getEntryAtPosition(mouseX.toDouble(), mouseY.toDouble())
} else {
null
}
context.enableScissor(pX, listTop, pX + listWidth, listBottom)
renderListItems(context, mouseX, mouseY, delta)
context.disableScissor()
if (maxScroll > 0) {
renderScrollbar(context, mouseX, mouseY, delta)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
val maxScroll = (itemCount * itemHeight - listHeight).coerceAtLeast(0)
if (maxScroll <= 0) return true
val step = if (verticalAmount != 0.0) verticalAmount else amount
scrollAmount = Mth.clamp(scrollAmount - step * itemHeight, 0.0, maxScroll.toDouble())
return true
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
if (button != 0) {
return false
}
if (isOverScrollbar(mouseX, mouseY) && maxScroll > 0) {
draggingScrollbar = true
return true
}
val localY = mouseY.toInt() - listTop + scrollAmount.toInt()
if (localY < 0) return false
val index = localY / itemHeight
if (index !in 0 until itemCount) return false
val entry = getEntry(index)
if (entry is LocatorRow) {
selectedLocator = entry.name
onSelectLocator(selectedLocator)
return true
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && draggingScrollbar) {
draggingScrollbar = false
return true
}
return super.mouseReleased(mouseX, mouseY, button)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!draggingScrollbar || maxScroll <= 0) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
val barHeight = listBottom - listTop
var thumbHeight = ((barHeight * barHeight).toFloat() / maxPosition.toFloat()).toInt()
thumbHeight = Mth.clamp(thumbHeight, 16, barHeight - 4)
val trackHeight = barHeight - thumbHeight
if (trackHeight <= 0) return true
val clampedY = Mth.clamp(mouseY.toInt() - listTop - thumbHeight / 2, 0, trackHeight)
scrollAmount = (clampedY.toDouble() * maxScroll.toDouble()) / trackHeight.toDouble()
return true
}
override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
return mouseX in pX.toDouble()..(pX + listWidth).toDouble()
&& mouseY in listTop.toDouble()..listBottom.toDouble()
}
override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val rowX = rowLeft
for (index in 0 until itemCount) {
val rowY = getRowTop(index)
val rowBottom = getRowBottom(index)
if (rowBottom >= listTop && rowY <= listBottom) {
val entry = getEntry(index)
if (entry is LocatorRow) {
entry.isSelected = entry.name == selectedLocator
}
renderItem(context, mouseX, mouseY, delta, index, rowX, rowY, rowWidth, itemHeight)
}
}
}
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val yStart = listTop
val barHeight = listBottom - listTop
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 16, barHeight - 4)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
if (yTop < yStart) yTop = yStart
context.fill(xLeft + 1, yStart, xRight - 1, listBottom, 0xFF7AEEF1.toInt())
context.fill(xLeft, yTop, xRight, yTop + yBottom, 0xFF3A96B6.toInt())
}
override fun getRowTop(index: Int): Int {
return listTop - scrollAmount.toInt() + (index * itemHeight)
}
override fun getRowBottom(index: Int): Int {
return getRowTop(index) + itemHeight
}
override fun getBottom(): Int {
return listBottom
}
private fun isOverScrollbar(mouseX: Double, mouseY: Double): Boolean {
val xLeft = scrollbarPosition
val xRight = xLeft + 3
return mouseX >= xLeft && mouseX <= xRight && mouseY >= listTop && mouseY <= listBottom
}
class LocatorRow(val name: String) : Slot<LocatorRow>() {
var isSelected: Boolean = false
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
val bg = when {
isSelected -> 0xFF4A4A4A.toInt()
hovered -> 0xFF2F2F2F.toInt()
else -> null
}
if (bg != null) {
context.fill(x + 1, y, x + entryWidth - 1, y + ROW_HEIGHT, bg)
}
drawScaledText(
context = context,
text = name.text(),
x = x + 3,
y = y + 2,
scale = TEXT_SCALE,
colour = TEXT_COLOR,
shadow = false
)
}
override fun getNarration(): Component {
return Component.literal(name)
}
}
}
@@ -0,0 +1,476 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LevelRenderer
import net.minecraft.client.renderer.RenderType
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Matrix4f
import org.joml.Quaternionf
import org.joml.Vector4f
import org.joml.Vector3f
class PokeEditLocatorTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
private var highlightedLocator: String?,
private val onDumpLocators: (List<String>) -> Unit
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, "locators".text()) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private const val LOCATOR_CUBE_SIZE = 0.1
private const val LOCATOR_TOOLTIP_RADIUS = 8
private const val BUTTON_WIDTH = 54
private const val BUTTON_HEIGHT = 15
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val AXIS_LINE_LENGTH = 0.35
private const val LEGEND_SCALE = 0.5F
private const val LEGEND_X = 6
private const val LEGEND_Y = 60
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var showLocators = false
private var showLocatorAxes = false
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var cachedLocatorNames: List<String> = emptyList()
private val showLocatorsButton = SummaryButton(
buttonX = pX + 6F,
buttonY = pY + 6F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocators = true },
text = "Locators On".text(),
resource = actionButtonResource,
renderRequirement = { !showLocators },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val hideLocatorsButton = SummaryButton(
buttonX = pX + 66F,
buttonY = pY + 6F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocators = false },
text = "Locators Off".text(),
resource = actionButtonResource,
renderRequirement = { showLocators },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val showLocatorAxesButton = SummaryButton(
buttonX = pX + 6F,
buttonY = pY + 24F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocatorAxes = true },
text = "Axes On".text(),
resource = actionButtonResource,
renderRequirement = { !showLocatorAxes },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val hideLocatorAxesButton = SummaryButton(
buttonX = pX + 66F,
buttonY = pY + 24F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocatorAxes = false },
text = "Axes Off".text(),
resource = actionButtonResource,
renderRequirement = { showLocatorAxes },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
showLocatorsButton.buttonActive = showLocators
hideLocatorsButton.buttonActive = !showLocators
showLocatorAxesButton.buttonActive = showLocatorAxes
hideLocatorAxesButton.buttonActive = !showLocatorAxes
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = matrices,
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
buildRenderable()?.let { renderable ->
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
val rotationVector = Vector3f(13F, rotationY, 0F)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state,
scale = MODEL_DRAW_SCALE
)
refreshLocatorList()
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
if (showLocators) {
renderLocators(context, mouseX, mouseY, showLocatorAxes)
}
matrices.popPose()
context.disableScissor()
}
showLocatorsButton.render(context, mouseX, mouseY, delta)
hideLocatorsButton.render(context, mouseX, mouseY, delta)
showLocatorAxesButton.render(context, mouseX, mouseY, delta)
hideLocatorAxesButton.render(context, mouseX, mouseY, delta)
if (showLocators && showLocatorAxes) {
renderAxisLegend(context)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun renderLocators(context: GuiGraphics, mouseX: Int, mouseY: Int, showAxes: Boolean) {
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val buffer = bufferSource.getBuffer(RenderType.lines())
val baseMatrix = Matrix4f(context.pose().last().pose())
var closestName: String? = null
var closestDistance = LOCATOR_TOOLTIP_RADIUS.toFloat()
for ((name, matrix) in state.locatorStates.entries) {
val highlight = name == highlightedLocator
val origin = matrix.getOrigin()
context.pose().pushPose()
context.pose().mulPose(matrix.matrix)
LevelRenderer.renderLineBox(
context.pose(),
buffer,
AABB.ofSize(Vec3.ZERO, LOCATOR_CUBE_SIZE, LOCATOR_CUBE_SIZE, LOCATOR_CUBE_SIZE),
if (highlight) 1F else 0F,
if (highlight) 0F else 1F,
0F,
1F
)
if (showAxes) {
renderLocatorAxes(context, buffer)
}
context.pose().popPose()
val screenPos = projectToScreen(baseMatrix, origin) ?: continue
val dx = screenPos.first - mouseX
val dy = screenPos.second - mouseY
val dist = kotlin.math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
if (dist <= closestDistance) {
closestDistance = dist
closestName = name
}
}
bufferSource.endBatch(RenderType.lines())
if (closestName != null) {
context.renderTooltip(Minecraft.getInstance().font, Component.literal(closestName), mouseX, mouseY)
}
}
fun setHighlightedLocator(name: String?) {
highlightedLocator = name
}
private fun refreshLocatorList() {
val names = state.locatorStates.keys.sorted()
if (names != cachedLocatorNames) {
cachedLocatorNames = names
onDumpLocators(names)
}
}
private fun renderLocatorAxes(context: GuiGraphics, buffer: com.mojang.blaze3d.vertex.VertexConsumer) {
val pose = context.pose().last().pose()
val length = AXIS_LINE_LENGTH.toFloat()
addAxisLine(buffer, pose, 0f, 0f, 0f, length, 0f, 0f, 1f, 0f, 0f)
addAxisLine(buffer, pose, 0f, 0f, 0f, 0f, length, 0f, 0f, 1f, 0f)
addAxisLine(buffer, pose, 0f, 0f, 0f, 0f, 0f, length, 0f, 0f, 1f)
}
private fun addAxisLine(
buffer: com.mojang.blaze3d.vertex.VertexConsumer,
pose: Matrix4f,
x1: Float,
y1: Float,
z1: Float,
x2: Float,
y2: Float,
z2: Float,
r: Float,
g: Float,
b: Float
) {
buffer.addVertex(pose, x1, y1, z1).setColor(r, g, b, 1f).setNormal(0f, 1f, 0f)
buffer.addVertex(pose, x2, y2, z2).setColor(r, g, b, 1f).setNormal(0f, 1f, 0f)
}
private fun renderAxisLegend(context: GuiGraphics) {
val legendX = x + LEGEND_X
val legendY = y + LEGEND_Y
context.fill(legendX - 2, legendY - 2, legendX + 46, legendY + 16, 0xAA1B1B1B.toInt())
drawLegendLine(context, legendX, legendY, 0xFFFF4D4D.toInt(), "X")
drawLegendLine(context, legendX, legendY + 5, 0xFF4DFF4D.toInt(), "Y")
drawLegendLine(context, legendX, legendY + 10, 0xFF4D4DFF.toInt(), "Z")
}
private fun drawLegendLine(context: GuiGraphics, x: Int, y: Int, color: Int, label: String) {
context.fill(x, y + 2, x + 6, y + 3, color)
drawScaledText(
context = context,
text = Component.literal(label),
x = x + 8,
y = y,
scale = LEGEND_SCALE,
colour = 0xFFFFFFFF.toInt(),
shadow = false
)
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun projectToScreen(baseMatrix: Matrix4f, position: Vec3): Pair<Float, Float>? {
val projection = RenderSystem.getProjectionMatrix()
val vec = Vector4f(position.x.toFloat(), position.y.toFloat(), position.z.toFloat(), 1f)
vec.mul(baseMatrix)
vec.mul(projection)
if (vec.w == 0f) return null
vec.div(vec.w)
val window = Minecraft.getInstance().window
val screenX = (vec.x * 0.5f + 0.5f) * window.guiScaledWidth.toFloat()
val screenY = (1f - (vec.y * 0.5f + 0.5f)) * window.guiScaledHeight.toFloat()
return Pair(screenX, screenY)
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -0,0 +1,659 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.MoveCategoryIcon
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.common.SummaryScrollList
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MoveDescriptionScrollList
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.ReorderMoveButton
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.toRGB
import java.math.RoundingMode
import java.text.DecimalFormat
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.util.Mth
class PokeEditMovesTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.moves")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val BASE_WIDTH = 134
private const val BASE_HEIGHT = 148
private const val BASE_SCALE = 0.80F
private const val SLOT_SPACING = 3
private const val SCALE = 0.5F
private val decimalFormat = DecimalFormat("#.##").also {
it.roundingMode = RoundingMode.CEILING
}
private val movesBaseResource = cobblemonResource("textures/gui/summary/summary_moves_base.png")
private val moveResource = cobblemonResource("textures/gui/summary/summary_move.png")
private val moveOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay.png")
private val moveSelectedOverlayResource = cobblemonResource("textures/gui/summary/summary_move_selected_overlay.png")
private val moveSwapButtonResource = cobblemonResource("textures/gui/summary/summary_move_swap.png")
private val moveAddButtonResource = cobblemonResource("textures/gui/summary/summary_move_add.png")
private val movesPowerIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_power.png")
private val movesAccuracyIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_accuracy.png")
private val movesEffectIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_effect.png")
private val moveListResource = cobblemonResource("textures/gui/summary/summary_move_condensed.png")
private val moveListOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay_condensed.png")
private const val MOVE_WIDTH = 108
private const val MOVE_HEIGHT = 22
private const val MOVE_LIST_SLOT_HEIGHT = 18
private const val MOVE_LIST_SLOT_SPACING = 3
private const val MOVE_LIST_X_OFFSET = 0
private const val MOVE_LIST_Y_OFFSET = 0
}
private val baseScaledWidth = (BASE_WIDTH * BASE_SCALE).toInt()
private val baseScaledHeight = (BASE_HEIGHT * BASE_SCALE).toInt()
private val moveWidthScaled = (MOVE_WIDTH * BASE_SCALE).toInt()
private val moveHeightScaled = (MOVE_HEIGHT * BASE_SCALE).toInt()
private val slotSpacingScaled = (SLOT_SPACING * BASE_SCALE).toInt().coerceAtLeast(1)
private val baseX = x + (WIDTH - baseScaledWidth) / 2 - 10
private val baseY = y + 7
private val slotX = baseX + (13 * BASE_SCALE).toInt()
private val slotY = baseY + (6 * BASE_SCALE).toInt()
private val descriptionListX = (baseX + (69 * BASE_SCALE).toInt())
.coerceAtMost(baseX + baseScaledWidth - 60)
private val descriptionListY = baseY + (113 * BASE_SCALE).toInt()
private val slots = (0 until 4).map { index ->
MoveSlot(
slotX,
slotY + (moveHeightScaled + slotSpacingScaled) * index,
index
).also { addWidget(it) }
}
private val descriptionList = MoveDescriptionScrollList(
descriptionListX,
descriptionListY,
5
)
private val movePicker = MovePickerList(
baseX + baseScaledWidth + MOVE_LIST_X_OFFSET,
baseY + (11 * BASE_SCALE).toInt() + MOVE_LIST_Y_OFFSET
)
private var selectedSlotIndex = -1
private var pickerSlotIndex = -1
private var movePickerOpen = false
private var lastDescriptionKey: String? = null
init {
refreshMoves()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
blitk(
matrixStack = matrices,
texture = movesBaseResource,
x = baseX,
y = baseY,
width = baseScaledWidth,
height = baseScaledHeight
)
slots.forEach { it.render(context, mouseX, mouseY, delta) }
renderMoveInfo(context)
renderDescription(context, mouseX, mouseY, delta)
if (movePickerOpen) {
movePicker.renderWidget(context, mouseX, mouseY, delta)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (movePickerOpen) {
val handled = movePicker.mouseClicked(mouseX, mouseY, button)
if (handled) return true
if (!movePicker.isMouseOver(mouseX, mouseY)) {
closeMovePicker()
}
}
if (descriptionList.isHovered) {
descriptionList.mouseClicked(mouseX, mouseY, button)
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
if (movePickerOpen) {
if (movePicker.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) return true
}
if (descriptionList.isHovered) {
descriptionList.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (movePickerOpen) {
if (movePicker.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true
}
if (descriptionList.isHovered) {
descriptionList.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun refreshMoves() {
val moves = Moves.all().sortedBy { it.displayName.string }
movePicker.setEntries(moves)
}
private fun selectedMove(): MoveTemplate? {
return if (selectedSlotIndex in draft.activeMoves.indices) draft.activeMoves[selectedSlotIndex] else null
}
private fun renderMoveInfo(context: GuiGraphics) {
val move = selectedMove()
val mcFont = Minecraft.getInstance().font
// Power
blitk(
matrixStack = context.pose(),
texture = movesPowerIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (114.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.power"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (115 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
val powerText = if (move != null && move.power.toInt() > 0) move.power.toInt().toString() else "-"
val powerComponent = Component.literal(powerText)
drawScaledText(
context = context,
text = powerComponent,
x = (baseX + (62.5 * BASE_SCALE)) - (mcFont.width(powerComponent) * SCALE * BASE_SCALE),
y = baseY + (115 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
// Accuracy
blitk(
matrixStack = context.pose(),
texture = movesAccuracyIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (125.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.accuracy"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (126 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
val accuracyText = if (move != null) formatPercent(move.accuracy) else "-"
val accuracyComponent = Component.literal(accuracyText)
drawScaledText(
context = context,
text = accuracyComponent,
x = (baseX + (62.5 * BASE_SCALE)) - (mcFont.width(accuracyComponent) * SCALE * BASE_SCALE),
y = baseY + (126 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
// Category
blitk(
matrixStack = context.pose(),
texture = movesEffectIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (136.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.category"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
if (move != null) {
MoveCategoryIcon(
x = baseX + (51 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
text = move.damageCategory.displayName.copy(),
x = baseX + (61 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
}
}
private fun renderDescription(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val move = selectedMove()
val nextKey = move?.name ?: ""
if (nextKey != lastDescriptionKey) {
descriptionList.setMoveDescription(move?.description ?: Component.literal(""))
lastDescriptionKey = nextKey
}
if (move != null) {
descriptionList.renderWidget(context, mouseX, mouseY, delta)
}
}
private fun formatPercent(input: Double): String {
if (input <= 0) return "-"
return "${decimalFormat.format(input)}%"
}
private fun toggleMovePicker(slotIndex: Int) {
selectedSlotIndex = slotIndex
if (movePickerOpen && pickerSlotIndex == slotIndex) {
closeMovePicker()
return
}
movePickerOpen = true
pickerSlotIndex = slotIndex
}
private fun closeMovePicker() {
movePickerOpen = false
pickerSlotIndex = -1
}
private fun assignMove(slotIndex: Int, move: MoveTemplate) {
draft.activeMoves.indices.forEach { index ->
if (index != slotIndex && draft.activeMoves[index] == move) {
draft.activeMoves[index] = null
}
}
draft.activeMoves[slotIndex] = move
}
private fun reorderMove(slotIndex: Int, up: Boolean) {
val size = draft.activeMoves.size
if (size == 0) return
val target = if (up) {
if (slotIndex > 0) slotIndex - 1 else size - 1
} else {
if (slotIndex < size - 1) slotIndex + 1 else 0
}
val temp = draft.activeMoves[target]
draft.activeMoves[target] = draft.activeMoves[slotIndex]
draft.activeMoves[slotIndex] = temp
if (selectedSlotIndex == slotIndex) selectedSlotIndex = target
else if (selectedSlotIndex == target) selectedSlotIndex = slotIndex
if (pickerSlotIndex == slotIndex) pickerSlotIndex = target
else if (pickerSlotIndex == target) pickerSlotIndex = slotIndex
}
private inner class MoveSlot(
pX: Int,
pY: Int,
private val slotIndex: Int
) : SoundlessWidget(pX, pY, moveWidthScaled, moveHeightScaled, Component.literal("move-slot-$slotIndex")) {
private val moveUpButton = ReorderMoveButton(x, y, true) {
reorderMove(slotIndex, true)
}.apply { addWidget(this) }
private val moveDownButton = ReorderMoveButton(x, y, false) {
reorderMove(slotIndex, false)
}.apply { addWidget(this) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val move = draft.activeMoves[slotIndex]
val matrices = context.pose()
isHovered = mouseX >= x && mouseY >= y && mouseX < x + width && mouseY < y + height
if (move != null) {
val rgb = move.elementalType.hue.toRGB()
if (selectedSlotIndex == slotIndex) {
blitk(
matrixStack = matrices,
texture = moveSelectedOverlayResource,
x = x - 1,
y = y - 1,
width = moveWidthScaled + 2,
height = moveHeightScaled + 2
)
}
blitk(
matrixStack = matrices,
texture = moveResource,
x = x,
y = y,
width = moveWidthScaled,
height = moveHeightScaled,
vOffset = if (isHovered) moveHeightScaled else 0,
textureHeight = moveHeightScaled * 2,
red = rgb.first,
green = rgb.second,
blue = rgb.third
)
blitk(
matrixStack = matrices,
texture = moveOverlayResource,
x = x,
y = y,
width = moveWidthScaled,
height = moveHeightScaled
)
val ppText = Component.literal("${move.pp}/${move.maxPp}").bold()
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = ppText,
x = x + (93 * BASE_SCALE),
y = y + (13 * BASE_SCALE),
centered = true,
scale = BASE_SCALE
)
TypeIcon(
x = x + (2 * BASE_SCALE),
y = y + (2 * BASE_SCALE),
type = move.elementalType
).render(context)
MoveCategoryIcon(
x = x + (66 * BASE_SCALE),
y = y + (13.5 * BASE_SCALE),
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = move.displayName.bold(),
x = x + (28 * BASE_SCALE),
y = y + (2 * BASE_SCALE),
shadow = true,
scale = BASE_SCALE
)
moveUpButton.render(context, mouseX, mouseY, delta)
moveDownButton.render(context, mouseX, mouseY, delta)
}
renderSwapButton(context, mouseX, mouseY, move)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isSwapHovered(mouseX, mouseY)) {
toggleMovePicker(slotIndex)
return true
}
if (isHovered) {
selectedSlotIndex = slotIndex
}
return super.mouseClicked(mouseX, mouseY, button)
}
private fun renderSwapButton(context: GuiGraphics, mouseX: Int, mouseY: Int, move: MoveTemplate?) {
val selected = movePickerOpen && pickerSlotIndex == slotIndex
val swapOffsetX = 114.5F * BASE_SCALE
val swapOffsetY = 6.5F * BASE_SCALE
val swapWidth = 12F * BASE_SCALE
val swapHeight = 18F * BASE_SCALE
blitk(
matrixStack = context.pose(),
texture = if (move == null) moveAddButtonResource else moveSwapButtonResource,
x = (x + swapOffsetX) / SCALE,
y = (y + swapOffsetY) / SCALE,
width = swapWidth,
height = swapHeight,
vOffset = if (isSwapHovered(mouseX.toDouble(), mouseY.toDouble()) || selected) swapHeight else 0F,
textureHeight = swapHeight * 2,
scale = SCALE
)
}
private fun isSwapHovered(mouseX: Double, mouseY: Double): Boolean {
val swapOffsetX = 114.5F * BASE_SCALE
val swapOffsetY = 6.5F * BASE_SCALE
val swapWidth = 12F * BASE_SCALE
val swapHeight = 18F * BASE_SCALE
val swapX = x + swapOffsetX
val swapY = y + swapOffsetY
return mouseX.toFloat() in (swapX..(swapX + (swapWidth * SCALE)))
&& mouseY.toFloat() in (swapY..(swapY + (swapHeight * SCALE) - 0.5F))
}
}
private inner class MovePickerList(
listX: Int,
listY: Int
) : SummaryScrollList<MovePickerList.MoveEntry>(
listX,
listY,
lang("ui.moves.switch"),
MOVE_LIST_SLOT_HEIGHT + MOVE_LIST_SLOT_SPACING
) {
private var entries: List<MoveTemplate> = emptyList()
fun setEntries(moves: List<MoveTemplate>) {
clearEntries()
entries = moves
moves.forEach { move -> addEntry(MoveEntry(move)) }
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
if (!movePickerOpen) return
super.renderWidget(context, mouseX, mouseY, partialTicks)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (!movePickerOpen) return false
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
if (!movePickerOpen) return false
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!movePickerOpen) return false
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
inner class MoveEntry(private val move: MoveTemplate) : Entry<MoveEntry>() {
override fun getNarration(): MutableComponent = move.displayName.copy()
override fun render(
context: GuiGraphics,
index: Int,
rowTop: Int,
rowLeft: Int,
rowWidth: Int,
rowHeight: Int,
mouseX: Int,
mouseY: Int,
isHovered: Boolean,
partialTicks: Float
) {
val matrices = context.pose()
val tweakedRowTop = rowTop - (MOVE_LIST_SLOT_SPACING / 2) + 1
val rgb = move.elementalType.hue.toRGB()
blitk(
matrixStack = matrices,
texture = moveListResource,
x = rowLeft,
y = tweakedRowTop,
height = MOVE_LIST_SLOT_HEIGHT,
width = rowWidth,
vOffset = if (isHovered) MOVE_LIST_SLOT_HEIGHT else 0,
textureHeight = MOVE_LIST_SLOT_HEIGHT * 2,
red = rgb.first,
green = rgb.second,
blue = rgb.third
)
blitk(
matrixStack = matrices,
texture = moveListOverlayResource,
x = rowLeft,
y = tweakedRowTop,
height = MOVE_LIST_SLOT_HEIGHT,
width = rowWidth
)
TypeIcon(
x = rowLeft - 9,
y = tweakedRowTop,
type = move.elementalType
).render(context)
MoveCategoryIcon(
x = rowLeft + 77,
y = tweakedRowTop + 1.5,
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
text = move.displayName,
x = rowLeft + 14,
y = tweakedRowTop + 3.5,
scale = SCALE,
shadow = true
)
blitk(
matrixStack = matrices,
texture = movesPowerIconResource,
x = (rowLeft + 10) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
blitk(
matrixStack = matrices,
texture = movesAccuracyIconResource,
x = (rowLeft + 30) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
blitk(
matrixStack = matrices,
texture = movesEffectIconResource,
x = (rowLeft + 53.5) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
val movePower = if (move.power.toInt() > 0) move.power.toInt().toString() else "-"
drawScaledText(
context = context,
text = Component.literal(movePower),
x = rowLeft + 16.5,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
drawScaledText(
context = context,
text = Component.literal(formatPercent(move.accuracy)),
x = rowLeft + 37,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
drawScaledText(
context = context,
text = Component.literal(formatPercent(move.effectChances.firstOrNull() ?: 0.0)),
x = rowLeft + 60.5,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
val pp = move.maxPp
drawScaledText(
context = context,
text = lang("ui.moves.pp", pp),
x = rowLeft + 76,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(d: Double, e: Double, i: Int): Boolean {
if (isMouseOver(d, e) && pickerSlotIndex >= 0) {
assignMove(pickerSlotIndex, move)
selectedSlotIndex = pickerSlotIndex
closeMovePicker()
return true
}
return false
}
}
}
}
@@ -0,0 +1,212 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SPACING
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditSpeciesList(
val pX: Int,
val pY: Int,
val onSelect: (Species) -> Unit
) : ScrollingWidget<PokeEditSpeciesList.SpeciesRow>(
width = 139,
height = SCROLL_BASE_HEIGHT,
left = pX,
top = pY - SCROLL_BASE_HEIGHT,
slotHeight = SCROLL_SLOT_SIZE + 2
) {
private var entries: List<Species> = emptyList()
fun setEntries(species: List<Species>) {
clearEntries()
entries = species
species.chunked(5).forEach { chunk ->
addEntry(SpeciesRow(chunk, onSelect))
}
scrollAmount = 0.0
}
override fun addEntry(entry: SpeciesRow): Int = super.addEntry(entry)
override fun getScrollbarPosition(): Int = pX + width - 3
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val barHeight = this.bottom - this.y
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
if (yTop < this.y) yTop = this.y
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
class SpeciesRow(
private val speciesList: List<Species>,
private val onSelect: (Species) -> Unit
) : Slot<SpeciesRow>() {
companion object {
private val slotResource = cobblemonResource("textures/gui/pokedex/pokedex_slot.png")
private val slotHighlight = cobblemonResource("textures/gui/pokedex/slot_select.png")
}
private val runtime = MoLangRuntime().setupClient().setup().also {
it.environment.query.addFunction("get_pokedex") { null }
}
private var x: Int = 0
private var y: Int = 0
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y
speciesList.forEachIndexed { indexInRow, species ->
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * indexInRow)
val startPosY = y + SCROLL_SLOT_SPACING + 1
blitk(
matrixStack = context.pose(),
texture = slotResource,
x = startPosX,
y = startPosY,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE
)
if (getHoveredSlotIndex(mouseX, mouseY) == indexInRow) {
blitk(
matrixStack = context.pose(),
texture = slotHighlight,
x = startPosX,
y = startPosY,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE,
vOffset = SCROLL_SLOT_SIZE,
textureHeight = SCROLL_SLOT_SIZE * 2
)
}
val form = species.standardForm
val aspects = mutableSetOf<String>()
aspects.addAll(form.aspects)
if (form.maleRatio in 0F..1F) {
aspects.add(Gender.MALE.name.lowercase())
} else {
aspects.add(Gender.GENDERLESS.name.lowercase())
}
context.enableScissor(
startPosX + 1,
startPosY + 1,
startPosX + SCROLL_SLOT_SIZE - 1,
startPosY + SCROLL_SLOT_SIZE - 2
)
val state = FloatingState().also { it.currentAspects = aspects }
val matrices = context.pose()
matrices.pushPose()
matrices.translate(startPosX + (SCROLL_SLOT_SIZE / 2.0), startPosY + 1.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
drawProfilePokemon(
renderablePokemon = RenderablePokemon(species, aspects),
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
partialTicks = 0F,
scale = 4.5F
)
matrices.popPose()
context.disableScissor()
matrices.pushPose()
matrices.translate(0.0, 0.0, 100.0)
val pokemonNumber = species.nationalPokedexNumber.toString().padStart(4, '0').text()
drawScaledText(
context = context,
text = pokemonNumber,
x = startPosX + 1.5,
y = startPosY + 2.5,
shadow = true,
scale = SCALE
)
matrices.popPose()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val hoverIndex = getHoveredSlotIndex(mouseX.toInt(), mouseY.toInt())
if (hoverIndex in speciesList.indices) {
onSelect(speciesList[hoverIndex])
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(com.cobblemon.mod.common.CobblemonSounds.POKEDEX_CLICK, 1.0F))
}
return true
}
private fun getHoveredSlotIndex(mouseX: Int, mouseY: Int): Int {
speciesList.forEachIndexed { index, _ ->
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * index)
val startPosY = y + SCROLL_SLOT_SPACING + 1
if (mouseX in startPosX..(startPosX + SCROLL_SLOT_SIZE)
&& mouseY in startPosY..(startPosY + SCROLL_SLOT_SIZE)
) {
return index
}
}
return -1
}
override fun getNarration(): Component {
return if (speciesList.isNotEmpty()) {
"${speciesList.first().name}-${speciesList.last().name}".text()
} else {
"".text()
}
}
}
}
@@ -0,0 +1,445 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.util.resolveFloat
import com.mojang.math.Axis
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.platform.Lighting
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import org.joml.Quaternionf
import org.joml.Vector3f
import java.util.Locale
class PokeEditWalkSpeedTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, Component.literal("Walk Speed")) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val SPEED_INPUT_WIDTH = 40
private const val SPEED_INPUT_HEIGHT = 10
private const val SPEED_LABEL_SCALE = 0.5F
private const val BUTTON_WIDTH = 54
private const val BUTTON_HEIGHT = 15
private const val BLOCK_SPACING = 1.0F
private const val BLOCK_PLANE_OFFSET = 2.3
private const val BLOCK_COLUMNS = 7
private const val BLOCK_ROWS = 3
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.WALK, PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var cachedSpeedKey: String? = null
private var defaultWalkSpeed = 0.35F
private var customWalkSpeed: Float? = null
private var syncingSpeedInput = false
private val walkSpeedInput = EditBox(
Minecraft.getInstance().font,
pX + 6,
pY + 18,
SPEED_INPUT_WIDTH,
SPEED_INPUT_HEIGHT,
Component.literal("speed")
).also { editBox ->
editBox.setBordered(false)
editBox.setTextColor(0xFFFFFF)
editBox.setTextColorUneditable(0xFFFFFF)
editBox.setMaxLength(6)
editBox.active = true
editBox.visible = true
editBox.setFilter { value ->
value.isBlank() || value.matches(Regex("\\d*(\\.\\d*)?"))
}
editBox.setResponder { value ->
if (syncingSpeedInput) return@setResponder
customWalkSpeed = value.toFloatOrNull()
}
addWidget(editBox)
}
private val resetSpeedButton = SummaryButton(
buttonX = pX + 52F,
buttonY = pY + 14F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { resetWalkSpeed() },
text = Component.literal("Reset"),
resource = actionButtonResource,
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
refreshDefaultWalkSpeed()
walkSpeedInput.active = true
walkSpeedInput.visible = true
if (!walkSpeedInput.isFocused && customWalkSpeed == null) {
val formatted = formatSpeed(defaultWalkSpeed)
if (walkSpeedInput.value != formatted) {
syncingSpeedInput = true
walkSpeedInput.value = formatted
syncingSpeedInput = false
}
}
val matrices = context.pose()
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = matrices,
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
walkSpeedInput.render(context, mouseX, mouseY, delta)
resetSpeedButton.render(context, mouseX, mouseY, delta)
drawScaledText(
context = context,
text = Component.literal("Walk Speed"),
x = x + 6,
y = y + 6,
scale = SPEED_LABEL_SCALE,
colour = 0xFFFFFFFF.toInt(),
shadow = false
)
buildRenderable()?.let { renderable ->
state.currentAspects = renderable.aspects
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
renderWalkBlocks(context, renderable, effectiveWalkSpeed(), delta)
val rotationVector = Vector3f(13F, rotationY, 0F)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state,
scale = MODEL_DRAW_SCALE
)
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
matrices.popPose()
context.disableScissor()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun resetWalkSpeed() {
customWalkSpeed = null
syncingSpeedInput = true
walkSpeedInput.value = formatSpeed(defaultWalkSpeed)
syncingSpeedInput = false
}
private fun refreshDefaultWalkSpeed() {
val key = "${draft.species.resourceIdentifier}#${draft.form.formOnlyShowdownId()}"
if (key == cachedSpeedKey) return
cachedSpeedKey = key
defaultWalkSpeed = resolveDefaultWalkSpeed()
if (customWalkSpeed == null) {
syncingSpeedInput = true
walkSpeedInput.value = formatSpeed(defaultWalkSpeed)
syncingSpeedInput = false
}
}
private fun resolveDefaultWalkSpeed(): Float {
return try {
state.runtime.resolveFloat(draft.form.behaviour.moving.walk.walkSpeed).coerceAtLeast(0.0F)
} catch (_: Exception) {
0.35F
}
}
private fun effectiveWalkSpeed(): Float {
val speed = customWalkSpeed ?: defaultWalkSpeed
return if (speed.isFinite() && speed > 0F) speed else defaultWalkSpeed
}
private fun formatSpeed(value: Float): String = String.format(Locale.US, "%.2f", value)
private fun renderWalkBlocks(context: GuiGraphics, renderable: RenderablePokemon, speed: Float, delta: Float) {
val model = VaryingModelRepository.getPoser(renderable.species.resourceIdentifier, state)
val ticks = (Minecraft.getInstance().level?.gameTime ?: (System.currentTimeMillis() / 50L)).toFloat() + delta
val offset = (ticks * speed) % BLOCK_SPACING
val travel = offset
val halfCols = BLOCK_COLUMNS / 2
val halfRows = BLOCK_ROWS / 2
val blockState = Blocks.GRASS_BLOCK.defaultBlockState()
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val packedLight = LightTexture.pack(15, 15)
val hitboxHeight = draft.form.hitbox.height * draft.form.baseScale
val blockPlaneY = (-hitboxHeight * 0.5F + BLOCK_PLANE_OFFSET).toDouble()
val matrices = context.pose()
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.translate(0.0, blockPlaneY, 0.0)
val rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, rotationY, 0F))
matrices.mulPose(rotation)
RenderSystem.applyModelViewMatrix()
RenderSystem.enableDepthTest()
RenderSystem.disableCull()
Lighting.setupForEntityInInventory()
for (xIndex in -halfRows..halfRows) {
for (zIndex in -halfCols..halfCols) {
val z = zIndex * BLOCK_SPACING + travel
matrices.pushPose()
matrices.translate((xIndex * BLOCK_SPACING).toDouble(), 0.0, z.toDouble())
matrices.mulPose(Axis.XP.rotationDegrees(180F))
Minecraft.getInstance().blockRenderer.renderSingleBlock(
blockState,
matrices,
bufferSource,
packedLight,
OverlayTexture.NO_OVERLAY
)
matrices.popPose()
}
}
bufferSource.endBatch()
Lighting.setupFor3DItems()
RenderSystem.enableCull()
matrices.popPose()
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (PoseType.WALK in poseList) {
selectedPoseIndex = poseList.indexOf(PoseType.WALK)
} else if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -15,18 +15,19 @@ import com.cobblemon.mod.common.api.gui.ColourLibrary
import com.cobblemon.mod.common.api.gui.MultiLineLabelK
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.ExitButton
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.VariableObjectSelectionScrollbar
import com.cobblemon.mod.common.client.gui.startselection.widgets.CategoryList
import com.cobblemon.mod.common.client.gui.startselection.widgets.ExitButton
import com.cobblemon.mod.common.client.gui.startselection.widgets.preview.ArrowButton
import com.cobblemon.mod.common.client.gui.startselection.widgets.preview.SelectionButton
import com.cobblemon.mod.common.client.gui.startselection.widgets.SelectionButton
import com.cobblemon.mod.common.client.gui.startselection.widgets.preview.StarterRoundabout
import com.cobblemon.mod.common.client.gui.summary.Summary.Companion.PARTY
import com.cobblemon.mod.common.client.gui.summary.widgets.ModelWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.type.DualTypeWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.type.SingleTypeWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.type.TypeWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.config.starter.RenderableStarterCategory
import com.cobblemon.mod.common.net.messages.server.SelectStarterPacket
@@ -34,13 +35,14 @@ import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.toRGB
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.toasts.Toast
import net.minecraft.client.gui.screens.Screen
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.sounds.SoundEvent
import java.io.FileNotFoundException
/**
* Starterselection Screen Thingy
@@ -49,32 +51,31 @@ import net.minecraft.network.chat.Component
* @since 2022-06-18
*/
class StarterSelectionScreen(private val categories: List<RenderableStarterCategory>): Screen("cobblemon.ui.starter.title".asTranslated()), CobblemonRenderable {
companion object {
// Size of UI at scale 1
private const val BASE_WIDTH = 200
private const val BASE_HEIGHT = 175
private const val BASE_WIDTH = 239
private const val BASE_HEIGHT = 197
// Resources
private val base = cobblemonResource("textures/gui/starterselection/starterselection_base.png")
private val baseUnderlay = cobblemonResource("textures/gui/starterselection/starterselection_base_underlay.png")
private val baseFrame = cobblemonResource("textures/gui/starterselection/starterselection_base_frame.png")
private val baseResource = cobblemonResource("textures/gui/starterselection/base.png")
private val backgroundResource = cobblemonResource("textures/gui/starterselection/background.png")
private val backgroundBallResource = cobblemonResource("textures/gui/starterselection/background_poke_ball.png")
// Type Backgrounds
private val singleTypeBackground = cobblemonResource("textures/gui/starterselection/starterselection_type_slot1.png")
private val doubleTypeBackground = cobblemonResource("textures/gui/starterselection/starterselection_type_slot2.png")
private val platformResource = cobblemonResource("textures/gui/starterselection/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private val typeSpacerResource = cobblemonResource("textures/gui/starterselection/type_spacer_single.png")
private val ballResource = cobblemonResource("textures/item/poke_balls/poke_ball.png")
}
private var currentSelection = 0
private lateinit var currentCategory: RenderableStarterCategory
var currentSelection = 0
lateinit var currentCategory: RenderableStarterCategory
private lateinit var modelWidget: ModelWidget
private lateinit var currentPokemon: RenderablePokemon
private lateinit var typeWidget: TypeWidget
private lateinit var starterRoundaboutCenter: StarterRoundabout
private lateinit var starterRoundaboutLeft: StarterRoundabout
private lateinit var starterRoundaboutRight: StarterRoundabout
override fun renderBlurredBackground(delta: Float) { }
var ticksElapsed = 0
var currentBallBackgroundFrame = 0
override fun renderBlurredBackground(delta: Float) {}
override fun renderMenuBackground(context: GuiGraphics) {}
@@ -95,55 +96,24 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
return
}
addRenderableWidget(
CategoryList(
paneWidth = 71, paneHeight = BASE_HEIGHT - 11,
topOffset = 6,
entryHeight = 20, entryWidth = 57,
categories = categories,
listX = x - 2, listY = y + 8,
starterSelectionScreen = this
)
)
val rightButton = ArrowButton(
pX = x + 183, pY = y + 151,
pWidth = 9, pHeight = 14,
right = true
) {
right()
}
val leftButton = ArrowButton(
pX = x + 72, pY = y + 151,
pWidth = 9, pHeight = 14,
right = false
) {
left()
}
addRenderableWidget(rightButton)
addRenderableWidget(leftButton)
currentCategory = categories.first()
currentPokemon = currentCategory.pokemon[currentSelection]
with(currentPokemon) {
modelWidget = ModelWidget(
pX = x + 85, pY = y + 50,
pWidth = 102, pHeight = 100,
pX = x + 6, pY = y + 17,
pWidth = 118, pHeight = 100,
pokemon = this,
baseScale = 2.0f,
baseScale = 2.7f,
playCryOnClick = true,
offsetY = -12.0
)
typeWidget = typeWidget(this, x, y)
}
addRenderableWidget(modelWidget)
val selectionButton = SelectionButton(
pX = x + 106, pY = y + 124,
pX = x + 13, pY = y + 180,
pWidth = SelectionButton.BUTTON_WIDTH, pHeight = SelectionButton.BUTTON_HEIGHT
) {
CobblemonNetwork.sendToServer(
@@ -152,182 +122,214 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
selected = currentSelection
)
)
Minecraft.getInstance().setScreen(null)
playSound(CobblemonSounds.GUI_CLICK)
onClose()
}
addRenderableWidget(selectionButton)
starterRoundaboutCenter = StarterRoundabout(
pX = x + 119, pY = height / 2 + 84,
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentPokemon,
rotationVector = this.modelWidget.rotationVector
)
starterRoundaboutLeft = StarterRoundabout(
pX = x + 89, pY = height / 2 + 84,
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentCategory.pokemon[leftOfCurrentSelection()],
clickAction = { _, _ -> this.left() },
rotationVector = this.modelWidget.rotationVector
)
starterRoundaboutRight = StarterRoundabout(
pX = x + 149, pY = height / 2 + 84,
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentCategory.pokemon[rightOfCurrentSelection()],
clickAction = { _, _ -> this.right() },
rotationVector = this.modelWidget.rotationVector
)
addRenderableWidget(starterRoundaboutLeft)
addRenderableWidget(starterRoundaboutCenter)
addRenderableWidget(starterRoundaboutRight)
// Add Exit Button
addRenderableWidget(
ExitButton(
pX = x + 181, pY = y + 2,
pWidth = 16, pHeight = 12,
pXTexStart = 0, pYTexStart = 0, pYDiffText = 0
) {
Minecraft.getInstance().setScreen(null)
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
ExitButton(pX = x + 210, pY = y + 181) {
playSound(CobblemonSounds.GUI_CLICK)
onClose()
}
)
val categoryList = CategoryList(
paneWidth = 97, paneHeight = 145,
categories = categories,
listX = x + 134, listY = y + 27,
starterSelectionScreen = this
)
addRenderableWidget(categoryList)
addRenderableWidget(VariableObjectSelectionScrollbar(
parent = categoryList,
x = x + 231,
y = y + 27,
width = 3,
height = 145
))
}
override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
// Render Underlay
//Background
blitk(
matrixStack = matrices,
texture = baseUnderlay,
x = x, y = y,
width = BASE_WIDTH, height = BASE_HEIGHT
texture = backgroundResource,
x = x + 6,
y = y + 17,
width = 118,
height = 100
)
// Render Base
blitk(
matrixStack = matrices,
texture = base,
x = x, y = y,
width = BASE_WIDTH, height = BASE_HEIGHT
texture = backgroundBallResource,
x = x + 10.5,
y = y + 12.5,
width = 109,
height = 109,
textureHeight = 1744,
vOffset = currentBallBackgroundFrame * 109
)
// Render Frame
val (r, g, b) = currentPokemon.form.primaryType.hue.toRGB()
//Platform
blitk(
matrixStack = matrices,
texture = baseFrame,
red = r,
green = g,
blue = b,
x = x, y = y,
width = BASE_WIDTH, height = BASE_HEIGHT
texture = platformResource,
x = x + 8.5,
y = y + 88,
width = 113,
height = 32
)
// Render Text
val typePlatform = getPlatformResource(currentPokemon.form.primaryType)
if (typePlatform != null) {
blitk(
matrixStack = matrices,
texture = typePlatform,
x = x + 8.5,
y = y + 82,
width = 113,
height = 30
)
}
blitk(
matrixStack = matrices,
texture = platformShadow,
x = (x + 43) / 0.5F,
y = (y + 93.5) / 0.5F,
width = 90,
height = 20,
scale = 0.5F
)
//Base
blitk(
matrixStack = matrices,
texture = baseResource,
x = x,
y = y,
width = BASE_WIDTH,
height = BASE_HEIGHT
)
//Type Icon
TypeIcon(
x = x + 65,
y = y + 120,
type = currentPokemon.form.primaryType,
secondaryType = currentPokemon.form.secondaryType,
centeredX = true
).render(context)
if (currentPokemon.form.secondaryType == null) {
blitk(
matrixStack = matrices,
texture = typeSpacerResource,
x = x + 47,
y = y + 123,
width = 36,
height = 12
)
}
//Name
blitk(
matrixStack = matrices,
texture = ballResource,
x = (x + 4) / 0.5F,
y = (y + 3) / 0.5F,
width = 16,
height = 16,
scale = 0.5F
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = currentPokemon.species.translatedName.bold(),
x = x + 14,
y = y + 2,
shadow = true
)
// Pokédex Number
// Add preceding zeroes if Pokédex number is less than 4 digits
var dexNo = currentPokemon.species.nationalPokedexNumber.toString()
while (dexNo.length < 4) {
dexNo = "0$dexNo"
}
dexNo = "#$dexNo"
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = dexNo.text().bold(),
x = x + 79,
y = y + 1,
shadow = true
)
//"Title"
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.starter.title").bold(),
x = x + 125, y = y + 3F,
centered = true,
scale = 1.4F,
maxCharacterWidth = 120,
shadow = true
x = x + 172,
y = y + 11,
shadow = true,
centered = true
)
// Render Name
val pokemonName = currentPokemon.species.translatedName
val scale = 0.8F
drawScaledText(
context = context,
text = pokemonName,
centered = true,
scale = scale,
maxCharacterWidth = 50,
x = x + 94,
y = y + 19.5,
shadow = false
)
//Description Text
val smallTextScale = 0.5F
// Render Description
val scale2 = 0.60F
matrices.pushPose()
matrices.scale(scale2, scale2, 1F)
// TODO use all pokedex lines across multiple clickable pages in this screen
matrices.scale(smallTextScale, smallTextScale, 1F)
MultiLineLabelK.create(
component = currentPokemon.form.pokedex.first().asTranslated(),
width = 127,
maxLines = 4
width = 114 / smallTextScale,
maxLines = 5
).renderLeftAligned(
context = context,
x = (x + 119) / scale2 + 4, y = (y + 18) / scale2 + 4.0,
ySpacing = (8.0 / scale2) - 1.25,
colour = ColourLibrary.WHITE, shadow = false
x = (x + 8) / smallTextScale,
y = (y + 143) / smallTextScale,
ySpacing = 5.5 / smallTextScale,
colour = ColourLibrary.WHITE,
shadow = true
)
matrices.popPose()
// Render the type background
blitk(
matrixStack = matrices,
texture = currentPokemon.form.secondaryType?.let { doubleTypeBackground } ?: singleTypeBackground,
x = (currentPokemon.form.secondaryType?.let { x + 76.75 } ?: (x + 85.25)), y = y + 29.4,
width = currentPokemon.form.secondaryType?.let { 35.25 } ?: 19, height = 19.25
)
// Render the type widget
typeWidget.render(context, mouseX, mouseY, delta)
// Render the rest
super.render(context, mouseX, mouseY, delta)
}
fun changeCategory(category: RenderableStarterCategory) {
fun getPlatformResource(type: ElementalType): ResourceLocation? {
return try {
cobblemonResource("textures/gui/starterselection/starter_platform_base_${type.showdownId}.png")
} catch (error: FileNotFoundException) {
null
}
}
fun selectPokemon(category: RenderableStarterCategory, slot: Int) {
currentCategory = category
currentSelection = 0
updateSelection()
}
private fun right() {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
currentSelection = rightOfCurrentSelection()
updateSelection()
}
private fun rightOfCurrentSelection() : Int = if (currentSelection + 1 <= currentCategory.pokemon.size - 1) currentSelection + 1 else 0
private fun left() {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
currentSelection = leftOfCurrentSelection()
updateSelection()
}
private fun leftOfCurrentSelection() : Int = if (currentSelection - 1 == -1) currentCategory.pokemon.size - 1 else currentSelection - 1
private fun updateSelection() {
currentSelection = slot
currentPokemon = currentCategory.pokemon[currentSelection].also {
modelWidget.pokemon = it
typeWidget = typeWidget(it, (width - BASE_WIDTH) / 2, (height - BASE_HEIGHT) / 2)
}
starterRoundaboutLeft.pokemon = currentCategory.pokemon[leftOfCurrentSelection()]
starterRoundaboutCenter.pokemon = currentPokemon
starterRoundaboutRight.pokemon = currentCategory.pokemon[rightOfCurrentSelection()]
}
private fun typeWidget(pokemon: RenderablePokemon, x: Int, y: Int) : TypeWidget {
return pokemon.form.secondaryType?.let {
DualTypeWidget(
pX = x + 77, pY = y + 30,
pWidth = 18, pHeight = 18,
pMessage = Component.literal("What?"),
mainType = pokemon.form.primaryType, secondaryType = it
)
} ?: SingleTypeWidget(
pX = x + 85, pY = y + 30,
pWidth = 18, pHeight = 18,
type = pokemon.form.primaryType,
renderText = false
)
override fun tick() {
ticksElapsed++
val delay = 3
if (ticksElapsed % delay == 0) currentBallBackgroundFrame++
if (currentBallBackgroundFrame == 16) currentBallBackgroundFrame = 0
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
@@ -340,4 +342,8 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
}
override fun isPauseScreen() = true
fun playSound(soundEvent: SoundEvent) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(soundEvent, 1.0F))
}
}
@@ -8,126 +8,212 @@
package com.cobblemon.mod.common.client.gui.startselection.widgets
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
import com.cobblemon.mod.common.client.gui.VariableObjectSelectionList
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.startselection.StarterSelectionScreen
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.config.starter.RenderableStarterCategory
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.ObjectSelectionList
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
import org.joml.Quaternionf
import org.joml.Vector3f
class CategoryList(
private val paneWidth: Int,
private val paneHeight: Int,
topOffset: Int,
private val entryWidth: Int,
entryHeight: Int,
private val categories: List<RenderableStarterCategory>,
val listX: Int,
val listY: Int,
private val minecraft: Minecraft = Minecraft.getInstance(),
private val starterSelectionScreen: StarterSelectionScreen
) : ObjectSelectionList<CategoryList.Category>(
minecraft,
) : VariableObjectSelectionList(
listX,
listY,
paneWidth,
paneHeight,
topOffset,
entryHeight
paneHeight
), CobblemonRenderable {
companion object {
private const val CATEGORY_BUTTON_WIDTH = 51.5f
private const val CATEGORY_BUTTON_HEIGHT = 16f
private const val ENTRY_X_OFFSET = 10f
private val categoryResource = cobblemonResource("textures/gui/starterselection/starterselection_slot.png")
private val headerResource = cobblemonResource("textures/gui/starterselection/selection_container_header.png")
private val footerResource = cobblemonResource("textures/gui/starterselection/selection_container_footer.png")
private val edgeResource = cobblemonResource("textures/gui/starterselection/selection_container_edge.png")
private val containerResource = cobblemonResource("textures/gui/starterselection/selection_container.png")
private val buttonResource = cobblemonResource("textures/gui/starterselection/starter_selection.png")
private val buttonActiveResource = cobblemonResource("textures/gui/starterselection/starter_selection_active.png")
const val MARGIN = 4
const val HEADER_HEIGHT = 7
const val BUTTON_SEPERATION = 3
const val CONTAINER_BUTTON_SIZE = 25
const val CONTAINER_WIDTH = 89
}
init {
this.x = listX
this.y = listY
this.correctSize()
createEntries().forEach { addEntry(it) }
//this.setRenderBackground(false)
addEntry(EmptySeparator(MARGIN))
createEntries().forEach {
addEntry(it)
addEntry(EmptySeparator(MARGIN))
}
}
private fun createEntries() = categories.map {
Category(it)
private fun createEntries() = categories.map { category ->
val rows = (category.pokemon.size - 1) / 3 + 1
val height = CONTAINER_BUTTON_SIZE * rows + BUTTON_SEPERATION * (rows + 1) + HEADER_HEIGHT + 3
Category(category, height)
}
override fun renderListBackground(context: GuiGraphics) {}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
super.renderWidget(context, mouseX, mouseY, delta)
correctSize()
}
private fun correctSize() {
this.setRectangle(this.paneWidth, this.paneHeight, this.listX, this.listY)
}
private fun scale(n: Int): Int = (this.minecraft.window.guiScale * n).toInt()
override fun getRowWidth() = this.entryWidth
override fun getScrollbarPosition(): Int {
return this.listX + this.width - 5
}
inner class Category(private val category: RenderableStarterCategory) : Entry<Category>() {
inner class Category(private val category: RenderableStarterCategory, height: Int) : Entry(height) {
override fun render(
context: GuiGraphics,
guiGraphics: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
y: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
partialTick: Float
) {
val matrices = context.pose()
val isHovered = mouseX >= x && mouseY >= y && mouseX < x + entryWidth && mouseY < y + (entryHeight - 1)
if (isHovered) {
blitk(
matrixStack = matrices,
x = x + 2.5f, y = y,
texture = categoryResource,
width = CATEGORY_BUTTON_WIDTH, height = CATEGORY_BUTTON_HEIGHT,
red = 0.75f, green = 0.75f, blue = 0.75f
)
} else
blitk(
matrixStack = matrices,
x = x + 2.5f, y = y,
texture = categoryResource,
width = CATEGORY_BUTTON_WIDTH, height = CATEGORY_BUTTON_HEIGHT
)
drawScaledText(
context = context,
text = category.displayNameText,
x = x + 28,
y = y + 4.5F,
scale = 1F,
maxCharacterWidth = 50,
shadow = true,
centered = true
val rows = (category.pokemon.size - 1) / 3 + 1
val matrices = guiGraphics.pose()
//Header
blitk(
matrixStack = matrices,
texture = headerResource,
x = x + MARGIN,
y = y,
width = CONTAINER_WIDTH,
height = HEADER_HEIGHT
)
val smallTextScale = 0.5F
drawScaledText(
context = guiGraphics,
text = category.displayNameText.bold(),
x = x + MARGIN + CONTAINER_WIDTH / 2,
y = y + 2,
shadow = true,
centered = true,
scale = smallTextScale
)
//Container
var stackHeight = HEADER_HEIGHT
blitk(
matrixStack = matrices,
texture = edgeResource,
x = x + MARGIN,
y = y + stackHeight,
width = CONTAINER_WIDTH,
height = 1
)
stackHeight += 1
val containerInnerHeight = CONTAINER_BUTTON_SIZE * rows + BUTTON_SEPERATION * (rows + 1)
blitk(
matrixStack = matrices,
texture = containerResource,
x = x + MARGIN,
y = y + stackHeight,
width = CONTAINER_WIDTH,
height = containerInnerHeight
)
stackHeight += containerInnerHeight
blitk(
matrixStack = matrices,
texture = edgeResource,
x = x + MARGIN,
y = y + stackHeight,
width = CONTAINER_WIDTH,
height = 1
)
stackHeight += 1
//Footer
blitk(
matrixStack = matrices,
texture = footerResource,
x = x + MARGIN,
y = y + stackHeight,
width = CONTAINER_WIDTH,
height = 1
)
//Buttons
for (index in 0..<category.pokemon.size) {
val row = index / 3
val column = index % 3
val startX = x + MARGIN + 5 + column * (CONTAINER_BUTTON_SIZE + 2)
val startY = y + HEADER_HEIGHT + 1 + BUTTON_SEPERATION + row * (BUTTON_SEPERATION + CONTAINER_BUTTON_SIZE)
val active = starterSelectionScreen.currentCategory == category && starterSelectionScreen.currentSelection == index
val hovered = getEntryAtPosition(mouseX, mouseY) == this && mouseX in startX..(startX + CONTAINER_BUTTON_SIZE) && mouseY in startY..(startY + CONTAINER_BUTTON_SIZE)
blitk(
matrixStack = matrices,
texture = if (active) buttonActiveResource else buttonResource,
x = startX,
y = startY,
width = CONTAINER_BUTTON_SIZE,
height = CONTAINER_BUTTON_SIZE,
textureHeight = CONTAINER_BUTTON_SIZE * 2,
vOffset = if (hovered) CONTAINER_BUTTON_SIZE else 0
)
matrices.pushPose()
matrices.translate(startX + (CONTAINER_BUTTON_SIZE / 2.0), startY.toDouble() - 6, 0.0)
matrices.scale(3.5F, 3.5F, 1F)
val state = FloatingState()
drawProfilePokemon(
species = category.pokemon[index].species.resourceIdentifier,
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
scale = 4.5F,
partialTicks = 0F
)
matrices.popPose()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
starterSelectionScreen.changeCategory(category = category)
minecraft.soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
return true
val entryY = getRowTop(this)
val hoveredButton = getButtonHovered(x, entryY, mouseX, mouseY)
if (hoveredButton != -1) {
starterSelectionScreen.selectPokemon(category, hoveredButton)
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
return true
}
return false
}
override fun getNarration(): Component {
return Component.literal("Yes")
private fun getButtonHovered(x: Int, y: Int, mouseX: Double, mouseY: Double): Int {
for (index in 0..<category.pokemon.size) {
val row = index / 3
val column = index % 3
val startX = x + MARGIN + 5 + column * (CONTAINER_BUTTON_SIZE + 2)
val startY = y + HEADER_HEIGHT + 1 + BUTTON_SEPERATION + row * (BUTTON_SEPERATION + CONTAINER_BUTTON_SIZE)
if (getEntryAtPosition(mouseX.toInt(), mouseY.toInt()) == this && mouseX.toInt() in startX..(startX + CONTAINER_BUTTON_SIZE) && mouseY.toInt() in startY..(startY + CONTAINER_BUTTON_SIZE)) {
return index
}
}
return -1
}
}
}
@@ -6,11 +6,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.startselection.widgets.preview
package com.cobblemon.mod.common.client.gui.startselection.widgets
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.ColourLibrary
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
@@ -26,45 +28,39 @@ class SelectionButton(
pX: Int, pY: Int,
pWidth: Int, pHeight: Int,
onPress: OnPress
): Button(pX, pY, pWidth, pHeight, Component.literal("SelectionButton"), onPress, DEFAULT_NARRATION), CobblemonRenderable {
): Button(pX, pY, pWidth, pHeight, Component.literal("SelectionButton"), onPress, DEFAULT_NARRATION),
CobblemonRenderable {
companion object {
private val buttonTexture = cobblemonResource("textures/gui/starterselection/starterselection_button.png")
const val BUTTON_WIDTH = 56
const val BUTTON_HEIGHT = 12
private const val SCALE = 0.7f
private val buttonResource = cobblemonResource("textures/gui/starterselection/choose_button.png")
const val BUTTON_WIDTH = 106
const val BUTTON_HEIGHT = 14
const val TEXT_HEIGHT = 10
}
override fun playDownSound(soundManager: SoundManager) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
}
override fun playDownSound(soundManager: SoundManager) {}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
if (isHovered)
blitk(
matrixStack = matrices,
texture = buttonTexture,
x = x + 0.6, y = y + 0.6,
width = BUTTON_WIDTH - 0.25, height = BUTTON_HEIGHT - 0.25,
red = 0.75f, green = 0.75f, blue = 0.75f
)
else
blitk(
matrixStack = matrices,
texture = buttonTexture,
x = x + 0.6, y = y + 0.6,
width = BUTTON_WIDTH - 0.25, height = BUTTON_HEIGHT - 0.25
)
blitk(
matrixStack = matrices,
texture = buttonResource,
x = x,
y = y,
width = BUTTON_WIDTH,
height = BUTTON_HEIGHT,
textureHeight = BUTTON_HEIGHT * 2,
vOffset = if (isHovered) BUTTON_HEIGHT else 0
)
drawScaledText(
context = context,
text = lang("ui.starter.choosebutton"),
x = x + BUTTON_WIDTH / 2, y = y + BUTTON_HEIGHT / 2 - 2.4,
colour = ColourLibrary.WHITE,
centered = true,
maxCharacterWidth = 68,
scale = SCALE,
shadow = true
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.starter.choosebutton").bold(),
x = x + BUTTON_WIDTH / 2,
y = y + BUTTON_HEIGHT / 2 - TEXT_HEIGHT / 2,
shadow = true,
centered = true
)
}
}
@@ -31,6 +31,7 @@ class ModelWidget(
var offsetY: Double = 0.0,
val playCryOnClick: Boolean = false,
val shouldFollowCursor: Boolean = false,
val blockLight: Int = 13
): SoundlessWidget(pX, pY, pWidth, pHeight, Component.literal("Summary - ModelWidget")) {
companion object {
@@ -115,6 +116,7 @@ class ModelWidget(
partialTicks = partialTicks,
headYaw = currentYawAndPitch.first,
headPitch = currentYawAndPitch.second,
blockLight = blockLight
)
heldItemRenderer.renderOnModel(
@@ -309,6 +309,7 @@ class SnowstormParticle(
boundingBox = AABB.ofSize(Vec3(x, y, z), radius, radius, radius)
if (dx == 0.0 && dy == 0.0 && dz == 0.0) {
updatePosition()
expandBoundingBoxForCulling(radius)
return
}
@@ -366,6 +367,16 @@ class SnowstormParticle(
}
}
updatePosition()
expandBoundingBoxForCulling(radius)
}
private fun expandBoundingBoxForCulling(collisionRadius: Double) {
val visualSizeX = abs(runtime.resolveDouble(storm.effect.particle.sizeX))
val visualSizeY = abs(runtime.resolveDouble(storm.effect.particle.sizeY))
val visualExtent = maxOf(visualSizeX, visualSizeY)
if (visualExtent > collisionRadius) {
boundingBox = AABB.ofSize(Vec3(x, y, z), visualExtent, visualExtent, visualExtent)
}
}
fun updatePosition() {
@@ -290,8 +290,14 @@ class ModelLayer {
val emissive: Boolean = false
val translucent: Boolean = false
val translucent_cull: Boolean = false
val scrolling: ScrollingTextureSettings? = null
}
class ScrollingTextureSettings(
val speedU: Float = 0.01F,
val speedV: Float = 0.01F
)
enum class SpriteType {
PORTRAIT,
PROFILE
@@ -20,6 +20,7 @@ import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.AnimatedModelTextureSupplier
import com.cobblemon.mod.common.client.render.ModelLayer
import com.cobblemon.mod.common.client.render.ScrollingTextureSettings
import com.cobblemon.mod.common.client.render.models.blockbench.animation.*
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
@@ -48,6 +49,7 @@ import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.blaze3d.vertex.VertexFormat
import com.mojang.math.Axis
import net.minecraft.Util
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderStateShard
@@ -453,15 +455,16 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
val provider = bufferProvider
if (provider != null) {
for (layer in currentLayers) {
var renderLayer : RenderType
if (layer.texture is AnimatedModelTextureSupplier && layer.texture.interpolation) {
//Handle Interpolation
val texture = layer.texture.interpolatedTexture(currentState ?: FloatingState()) ?: continue
renderLayer = makeLayer(DynamicStateShard(texture), layer.emissive, layer.translucent, layer.translucent_cull)
}
else {
val texture = layer.texture?.invoke(currentState ?: FloatingState()) ?: continue
renderLayer = getLayer(texture, layer.emissive, layer.translucent, layer.translucent_cull)
val texture = layer.texture?.invoke(currentState ?: FloatingState()) ?: continue
val scrolling = layer.scrolling
val renderLayer: RenderType = if (scrolling != null) {
getScrollingLayer(texture, scrolling)
} else if (layer.texture is AnimatedModelTextureSupplier && layer.texture.interpolation) {
val interpolatedTexture = layer.texture.interpolatedTexture(currentState ?: FloatingState()) ?: continue
makeLayer(DynamicStateShard(interpolatedTexture), layer.emissive, layer.translucent, layer.translucent_cull)
} else {
getLayer(texture, layer.emissive, layer.translucent, layer.translucent_cull)
}
val consumer = provider.getBuffer(renderLayer)
val tint = layer.tint
@@ -500,7 +503,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
!emissive && translucent && translucentCull -> RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_CULL_SHADER
!emissive && translucent -> RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_SHADER
!emissive && !translucent -> RenderStateShard.RENDERTYPE_ENTITY_CUTOUT_SHADER
else -> RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER // This one should be changed to maybe a custom shader? Translucent stuffs with things
else -> RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER
}
)
.setTextureState(texture)
@@ -533,6 +536,12 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
}
}
fun getScrollingLayer(texture: ResourceLocation, scrolling: ScrollingTextureSettings): RenderType {
val gameTime = Util.getMillis().toFloat() / 1000f
val offsetU = (gameTime * scrolling.speedU) % 1.0f
val offsetV = (gameTime * scrolling.speedV) % 1.0f
return RenderType.breezeWind(texture, offsetU, offsetV)
}
/** Applies the given pose's [ModelPartTransformation]s to the model, if there is a matching pose. */
fun applyPose(state: PosableState, pose: Pose, intensity: Float) = pose.transformedParts.forEach { it.apply(state, intensity) }
@@ -247,7 +247,7 @@ class CobblemonConfig {
var pastureBlockUpdateTicks = 40
@CobblemonConfigField(Category.World, lang = "pasture_max_wander_distance", SERVER)
var pastureMaxWanderDistance = 64
var pastureMaxWanderDistance = 32
@CobblemonConfigField(Category.World, lang = "pasture_max_per_chunk", SERVER)
var pastureMaxPerChunk = 4F
@@ -126,6 +126,14 @@ class StarterConfig {
PokemonProperties.parse("Fuecoco level=10"),
PokemonProperties.parse("Quaxly level=10"),
)
),
StarterCategory(
name = "Special",
displayName = "cobblemon.starterselection.category.special",
pokemon = mutableListOf(
PokemonProperties.parse("Pikachu level=10"),
PokemonProperties.parse("Eevee level=10")
)
)
)
}
@@ -14,6 +14,9 @@ import com.cobblemon.mod.common.CobblemonBehaviours
import com.cobblemon.mod.common.CobblemonCallbacks
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.CobblemonMechanics
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.CobblemonPartyCompositions
import com.cobblemon.mod.common.CobblemonPartyPools
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.CobblemonUnlockableWallpapers
import com.cobblemon.mod.common.api.abilities.Abilities
@@ -78,12 +81,15 @@ object CobblemonDataProvider : DataProvider {
this.register(SpeciesFeatureAssignments, reloadable = false)
this.register(ActionEffects, reloadable = true)
this.register(Moves, reloadable = false)
this.register(CobblemonMovesetBuilders, reloadable = false)
this.register(Abilities, reloadable = false)
this.register(CobblemonBehaviours, reloadable = false)
this.register(PokemonSpecies, reloadable = false)
this.register(SpeciesAdditions, reloadable = false)
this.register(PokeBalls, reloadable = false)
this.register(PropertiesCompletionProvider, reloadable = false)
this.register(CobblemonPartyPools, reloadable = false)
this.register(CobblemonPartyCompositions, reloadable = false)
this.register(SpawnDetailPresets, reloadable = true)
this.register(CobblemonSpawnRules, reloadable = true)
this.register(CobblemonMechanics, reloadable = true)
@@ -27,6 +27,7 @@ import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluenc
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.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FishingSpawnerFactory
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.client.sound.EntitySoundTracker
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -762,7 +763,6 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
lureLevel: Int
) {
val spawner = BestSpawner.fishingSpawner
val bucketInfluence = BucketNormalizingInfluence(tier = lureLevel + luckOfTheSeaLevel)
val spawnCause = FishingSpawnCause(
spawner = spawner,
@@ -771,14 +771,21 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
lureLevel = lureLevel,
)
val spawnablePosition = FishingSpawnablePosition(
cause = spawnCause,
val ctx = FishingSpawnerFactory.Context(
player = player,
rodStack = rodItemStack,
lureLevel = lureLevel,
luckOfTheSeaLevel = luckOfTheSeaLevel,
world = level() as ServerLevel,
pos = position().toBlockPos(),
influences = mutableListOf(
PlayerLevelRangeInfluence(player, TYPICAL_VARIATION),
bucketInfluence
)
spawner = spawner
)
val spawnablePosition = FishingSpawnablePosition(
cause = spawnCause,
world = ctx.world,
pos = ctx.pos,
influences = FishingSpawnerFactory.buildPositionInfluences(ctx)
)
val result = spawner.calculateSpawnActionForPosition(
@@ -191,7 +191,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
}
}
} else {
setNoGravity(false)
isNoGravity = false
deltaMovement = Vec3.ZERO
}
}
@@ -164,6 +164,7 @@ import net.minecraft.world.item.ItemUtils
import net.minecraft.world.item.Items
import net.minecraft.world.level.Level
import net.minecraft.world.level.LightLayer
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.level.block.SuspiciousEffectHolder
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.gameevent.GameEvent
@@ -677,13 +678,10 @@ open class PokemonEntity(
yBodyRot = (spawnDirection * 1000F).toInt() / 1000F
}
if (this.tethering != null && !this.tethering!!.box.contains(this.x, this.y, this.z)) {
this.tethering = null
this.pokemon.recall()
}
jumping = false
checkPastureTether()
//This is so that pokemon in the pasture block are ALWAYS in sync with the pokemon box
//Before, pokemon entities in pastures would hold an old ref to a pokemon obj and changes to that would not appear to the underlying file
if (this.tethering != null && age % 20 == 0) {
@@ -705,6 +703,40 @@ open class PokemonEntity(
schedulingTracker.update(1 / 20F)
}
// Check to see if the entity needs recalled or repositioned if pastured.
fun checkPastureTether() {
// Teleport a pastured mon back to the pasture block if they are out of range
if (this.tethering != null && !this.tethering!!.box.contains(this.x, this.y, this.z)) {
// Gather the ideal position (behind the pastureBlock)
val pastureBlockPos = this.tethering!!.pasturePos
val state = this.level().getBlockState(pastureBlockPos)
// Check to ensure we are working with a pastureblock here and it hasn't been broken
if (state.block == CobblemonBlocks.PASTURE) {
val direction = state.getValue(HorizontalDirectionalBlock.FACING)
val width = this.boundingBox.xsize
val tpPos = pastureBlockPos.offset(direction.normal.multiply(ceil(width).toInt() + 1)).center
val heightAllowance = 4.0 // Allow spawning up to x blocks above the pastureBlock y level
// Attempt tp and recall if it fails
if (!this.randomTeleport(tpPos.x, tpPos.y + heightAllowance, tpPos.z, false)) {
// If the teleport was not successful then recall the mon
this.tethering = null
this.pokemon.recall()
} else {
// Stop any now invalid navigation post teleport
this.navigation.stop()
}
} else {
// If the block is destroyed or something else went wrong.
this.tethering = null
this.pokemon.recall()
}
}
}
override fun customServerAiStep() {
this.getBrain().tick(level() as ServerLevel, this)
// PokemonBrain.updateActivities(this)
@@ -763,6 +763,7 @@ object CobblemonItemGroups {
CobblemonItems.pokeBalls.forEach(entries::accept)
CobblemonItems.pokeRods.forEach(entries::accept)
CobblemonItems.pokedexes.forEach(entries::accept)
entries.accept(CobblemonItems.POKEEDIT_WAND)
}
private fun blocksInjections(injector: Injector) {
@@ -0,0 +1,48 @@
/*
* 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.item.interactive
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditGUI
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.CobblemonItem
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResult
import net.minecraft.world.InteractionResultHolder
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
class PokeEditWandItem : CobblemonItem(Properties().stacksTo(1)) {
override fun use(world: Level, player: Player, usedHand: InteractionHand): InteractionResultHolder<ItemStack> {
val stack = player.getItemInHand(usedHand)
if (!player.isCreative) return InteractionResultHolder.pass(stack)
if (world.isClientSide) {
PokeEditGUI.open()
}
return InteractionResultHolder.sidedSuccess(stack, world.isClientSide)
}
override fun interactLivingEntity(
stack: ItemStack,
player: Player,
entity: LivingEntity,
usedHand: InteractionHand
): InteractionResult {
if (!player.isCreative) return InteractionResult.PASS
if (entity is PokemonEntity) {
if (entity.level().isClientSide) {
PokeEditGUI.open()
}
return InteractionResult.sidedSuccess(entity.level().isClientSide)
}
return InteractionResult.PASS
}
}
@@ -13,7 +13,11 @@ import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeEnumConstant
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacket<MoveTemplate, MovesRegistrySyncPacket>(moves) {
@@ -33,6 +37,7 @@ class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacke
buffer.writeDouble(entry.critRatio)
buffer.writeInt(entry.effectChances.size)
entry.effectChances.forEach { chance -> buffer.writeDouble(chance) }
buffer.writeFloat(entry.weight)
}
override fun decodeEntry(buffer: RegistryFriendlyByteBuf): MoveTemplate {
@@ -50,7 +55,21 @@ class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacke
repeat(buffer.readInt()) {
effectChances += buffer.readDouble()
}
return MoveTemplate(name, num, type, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
val weight = buffer.readFloat()
return MoveTemplate(
name = name,
num = num,
elementalType = type,
damageCategory = damageCategory,
power = power,
target = target,
accuracy = accuracy,
pp = pp,
priority = priority,
critRatio = critRatio,
effectChances = effectChances.toTypedArray(),
weight = weight
)
}
override fun synchronizeDecoded(entries: Collection<MoveTemplate>) {
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.pokeedit
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeEnumConstant
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
enum class PokeEditAction {
CREATE,
SPAWN
}
class PokeEditActionPacket(
val action: PokeEditAction,
val properties: String,
val moves: List<String>,
val cosmeticItem: String
) : NetworkPacket<PokeEditActionPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeEnumConstant(action)
buffer.writeString(properties)
buffer.writeInt(moves.size)
moves.forEach(buffer::writeString)
buffer.writeString(cosmeticItem)
}
companion object {
val ID = cobblemonResource("pokeedit_action")
fun decode(buffer: RegistryFriendlyByteBuf): PokeEditActionPacket {
val action = buffer.readEnumConstant(PokeEditAction::class.java)
val properties = buffer.readString()
val moves = List(buffer.readInt()) { buffer.readString() }
val cosmeticItem = buffer.readString()
return PokeEditActionPacket(action, properties, moves, cosmeticItem)
}
}
}
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.serverhandling.pokeedit
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.moves.MoveSet
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditAction
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toBlockPos
import net.minecraft.network.chat.Component
import net.minecraft.commands.arguments.item.ItemParser
import com.mojang.brigadier.StringReader
import net.minecraft.world.item.ItemStack
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.MobSpawnType
import net.minecraft.world.level.Level
object PokeEditActionHandler : ServerNetworkPacketHandler<PokeEditActionPacket> {
override fun handle(packet: PokeEditActionPacket, server: MinecraftServer, player: ServerPlayer) {
val properties = PokemonProperties.parse(packet.properties)
if (properties.species == null) {
player.sendSystemMessage(commandLang("spawnpokemon.nospecies").red())
return
}
when (packet.action) {
PokeEditAction.CREATE -> {
if (!Cobblemon.permissionValidator.hasPermission(player, CobblemonPermissions.GIVE_POKEMON_SELF)) {
player.sendSystemMessage(lang("requires_permission").red())
return
}
val pokemon = properties.create(player)
applyMovesIfProvided(pokemon, packet.moves)
applyCosmeticIfProvided(pokemon, packet.cosmeticItem, server, player)
Cobblemon.storage.getParty(player).add(pokemon)
}
PokeEditAction.SPAWN -> {
if (!Cobblemon.permissionValidator.hasPermission(player, CobblemonPermissions.SPAWN_POKEMON)) {
player.sendSystemMessage(lang("requires_permission").red())
return
}
val world = player.serverLevel()
val pos = player.position()
val blockPos = pos.toBlockPos()
if (!Level.isInSpawnableBounds(blockPos)) {
player.sendSystemMessage(Component.literal("Invalid position").red())
return
}
try {
val pokemonEntity = properties.createEntity(world, player)
applyMovesIfProvided(pokemonEntity.pokemon, packet.moves)
applyCosmeticIfProvided(pokemonEntity.pokemon, packet.cosmeticItem, server, player)
pokemonEntity.moveTo(pos.x, pos.y, pos.z, pokemonEntity.yRot, pokemonEntity.xRot)
pokemonEntity.entityData.set(PokemonEntity.SPAWN_DIRECTION, pokemonEntity.random.nextFloat() * 360F)
pokemonEntity.finalizeSpawn(world, world.getCurrentDifficultyAt(blockPos), MobSpawnType.COMMAND, null)
world.addFreshEntity(pokemonEntity)
} catch (e: Exception) {
e.printStackTrace()
player.sendSystemMessage(Component.literal("Unable to spawn at the given position").red())
}
}
}
}
private fun applyMovesIfProvided(pokemon: com.cobblemon.mod.common.pokemon.Pokemon, moves: List<String>) {
if (moves.none { it.isNotBlank() }) return
val templates = moves.take(MoveSet.MOVE_COUNT).map { name ->
if (name.isBlank()) null else Moves.getByName(name)
}
pokemon.moveSet.doWithoutEmitting {
for (i in 0 until MoveSet.MOVE_COUNT) {
pokemon.moveSet.setMove(i, null)
}
templates.forEachIndexed { index, template ->
template?.let { pokemon.moveSet.setMove(index, it.create()) }
}
}
pokemon.moveSet.update()
}
private fun applyCosmeticIfProvided(
pokemon: com.cobblemon.mod.common.pokemon.Pokemon,
cosmeticItem: String,
server: MinecraftServer,
player: ServerPlayer
) {
val trimmed = cosmeticItem.trim()
if (trimmed.isEmpty()) return
val parser = ItemParser(server.registryAccess())
val result = try {
parser.parse(StringReader(trimmed))
} catch (_: Exception) {
player.sendSystemMessage(Component.literal("Invalid cosmetic item: $trimmed").red())
return
}
val stack = ItemStack(result.item)
stack.applyComponents(result.components)
if (stack.isEmpty) return
val isValid = CobblemonCosmeticItems.findValidCosmeticForPokemonAndItem(
server.registryAccess(),
pokemon,
stack
) != null
if (!isValid) {
player.sendSystemMessage(Component.literal("Item is not a valid cosmetic for this pokemon.").red())
return
}
pokemon.swapCosmeticItem(stack, decrement = false)
}
}
@@ -104,6 +104,10 @@ class FormData(
private var _baseAI: MutableList<BehaviourConfig>? = null,
@SerializedName("ai")
private var _ai: MutableList<BehaviourConfig>? = null,
@SerializedName("signatureMoves")
private var _signatureMoves: MutableList<MoveTemplate>? = null,
@SerializedName("defaultWildMovesetBuilder")
private val _defaultWildMovesetBuilder: ResourceLocation? = null,
val requiredMove: String? = null,
val requiredItem: String? = null,
/** For forms that can accept different items (e.g. Arceus-Grass: Meadow Plate or Grassium-Z). */
@@ -235,6 +239,10 @@ class FormData(
get() = _baseAI ?: species.baseAI
val ai: List<BehaviourConfig>
get() = _ai ?: species.ai
val signatureMoves: List<MoveTemplate>
get() = _signatureMoves ?: species.signatureMoves
val defaultWildMovesetBuilder: ResourceLocation
get() = _defaultWildMovesetBuilder ?: species.defaultWildMovesetBuilder
fun eyeHeight(entity: PokemonEntity): Float {
return this.resolveEyeHeight(entity) ?: return this.species.eyeHeight(entity)
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.pokemon
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToPlayers
import com.cobblemon.mod.common.CobblemonSounds
@@ -34,6 +35,7 @@ import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.api.moves.MoveSet
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.Natures
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
@@ -594,6 +596,9 @@ open class Pokemon : ShowdownIdentifiable {
onChange(MarkingsUpdatePacket({ this }, value))
}
// Last flower fed to a Mooshtank
var lastFlowerFed: ItemStack = ItemStack.EMPTY
fun asRenderablePokemon() = RenderablePokemon(species, aspects, if (heldItemVisible) heldItem else ItemStack.EMPTY)
/**
@@ -1564,10 +1569,8 @@ open class Pokemon : ShowdownIdentifiable {
originalTrainer = fakeTrainerName
}
fun refreshOriginalTrainer()
{
when (originalTrainerType)
{
fun refreshOriginalTrainer() {
when (originalTrainerType) {
OriginalTrainerType.PLAYER -> {
UUID.fromString(originalTrainer)?.let { uuid ->
server()?.profileCache?.get(uuid)?.orElse(null)?.name?.let {
@@ -1584,8 +1587,7 @@ open class Pokemon : ShowdownIdentifiable {
}
}
fun removeOriginalTrainer()
{
fun removeOriginalTrainer() {
originalTrainer = null
originalTrainerType = OriginalTrainerType.NONE
originalTrainerName = null
@@ -1627,14 +1629,11 @@ open class Pokemon : ShowdownIdentifiable {
species = species
checkGender()
if (moveSet.getMoves().isEmpty()) {
initializeMoveset()
initializeMovesetFromDefault()
}
return this
}
// Last flower fed to a Mooshtank
var lastFlowerFed: ItemStack = ItemStack.EMPTY
fun checkGender() {
var reassess = false
if (form.maleRatio !in 0F..1F && gender != Gender.GENDERLESS) {
@@ -1810,6 +1809,20 @@ open class Pokemon : ShowdownIdentifiable {
moveSet.update()
}
fun initializeMovesetFromDefault() {
initializeMovesetFrom(movesetBuilder = CobblemonMovesetBuilders.getOrThrow(form.defaultWildMovesetBuilder))
}
fun initializeMovesetFrom(movesetBuilder: MovesetBuilder) {
val newMoveset = movesetBuilder.build(form = form, level = level)
moveSet.copyFrom(newMoveset)
}
@Deprecated(
message = "Will be removed within potentially 1 title update",
replaceWith = ReplaceWith("initializeMovesetFromDefault"),
level = DeprecationLevel.ERROR
)
fun initializeMoveset(preferLatest: Boolean = true) {
val possibleMoves = form.moves.getLevelUpMovesUpTo(level).toMutableList()
moveSet.doWithoutEmitting {
@@ -22,6 +22,7 @@ import com.cobblemon.mod.common.api.drop.DropTable
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addSpeciesFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
@@ -42,6 +43,7 @@ import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
import com.cobblemon.mod.common.pokemon.ai.PokemonBehaviour
import com.cobblemon.mod.common.pokemon.lighthing.LightingData
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEntityDimensions
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readIdentifier
@@ -111,6 +113,8 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
var implemented = false
var baseAI: MutableList<BehaviourConfig>? = null
var ai = mutableListOf<BehaviourConfig>()
var signatureMoves = mutableListOf<MoveTemplate>()
var defaultWildMovesetBuilder = cobblemonResource("wild")
/**
* The height in decimeters
@@ -87,6 +87,7 @@ object DataKeys {
const val POKEMON_PROPERTIES_MOVES = "Moves"
const val POKEMON_PROPERTIES_HELDITEM = "HeldItem"
const val POKEMON_PROPERTIES_MOVESET_BUILDERS = "MovesetBuilders"
// Entity effects
const val ENTITY_EFFECTS = "EntityEffects"
@@ -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.util
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molang.ExpressionLike
/**
* An IntRange where the minimum and maximum values are defined by MoLang expressions. When deserializing from
* JSON the [com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter] will figure out if it's a single
* value in which case it's not using any scripting stuff. Otherwise, if it's an object with "min" and "max" properties
* it will respect them as expressions.
*
* @author Hiroku
* @since December 15th, 2025
*/
class ScriptableIntRange(
val min: ExpressionLike = "0".asExpressionLike(),
val max: ExpressionLike = "0".asExpressionLike()
) {
constructor(min: Int, max: Int): this(
min = min.toString().asExpressionLike(),
max = max.toString().asExpressionLike()
)
fun resolve(runtime: MoLangRuntime): IntRange {
val minimum = min.resolveInt(runtime)
val maximum = max.resolveInt(runtime)
if (minimum > maximum) {
Cobblemon.LOGGER.error("ScriptableIntRange(${min.getString()}, ${max.getString()}) resolved a minimum ($minimum) greater than maximum ($maximum). Returning empty range.")
return IntRange.EMPTY
}
return minimum..maximum
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
object MovesetBuilderAdapter : JsonDeserializer<MovesetBuilder> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MovesetBuilder {
val jsonObject = json.asJsonObject
val type = jsonObject.get("type")?.asString ?: "cobblemon:default"
val clazz = MovesetBuilder.movesetBuilderTypes[type.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown MovesetBuilder type: $type")
return context.deserialize(json, clazz)
}
}
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
object MovesetBuilderReferenceAdapter : JsonDeserializer<MovesetBuilder> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MovesetBuilder {
return CobblemonMovesetBuilders.getOrThrow(json.asString.asIdentifierDefaultingNamespace())
}
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
@@ -16,8 +17,8 @@ import java.lang.reflect.Type
object NPCPartyProviderAdapter : JsonDeserializer<NPCPartyProvider> {
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): NPCPartyProvider {
val typeName = if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString
return NPCPartyProvider.types[typeName]?.invoke(typeName)?.also { it.loadFromJSON(json) }
val typeName = (if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString).asIdentifierDefaultingNamespace()
return NPCPartyProvider.types[typeName]?.invoke()?.also { it.loadFromJSON(json) }
?: throw IllegalStateException("Unable to find party provider by type: $typeName")
}
}
@@ -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.util.adapters
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import java.lang.reflect.Type
/**
* Parses a [ScriptableIntRange] by two paths. One is if it's a primitive in which
* case they're just doing a normal [IntRange] and it will parse it using the same
* method as IntRange normally gets and then returns a ScriptableIntRange of literals.
*
* The other path is if they provide an object with "min" and "max" in which case
* each property is deserialized as an [ExpressionLike].
*
* @author Hiroku
* @since December 15th, 2025
*/
object ScriptableIntRangeAdapter : JsonDeserializer<ScriptableIntRange> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): ScriptableIntRange {
if (json is JsonPrimitive) {
IntRangeAdapter.deserialize(json, typeOfT, context).let {
return ScriptableIntRange(
it.first.toString().asExpressionLike(),
it.last.toString().asExpressionLike()
)
}
} else {
val obj = json.asJsonObject
val min = obj.get("min").asString
val max = obj.get("max").asString
return ScriptableIntRange(
min.toString().asExpressionLike(),
max.toString().asExpressionLike()
)
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
/**
* A simple adapter that maps string values to objects based on a provided mapping.
*
* I could swear I already made something like this before.
*
* @author Hiroku
* @since December 5th, 2025
*/
class StringToObjectAdapter<T>(
val mapping: MutableMap<String, T>
) : JsonDeserializer<T> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T = mapping[json.asString]
?: throw IllegalArgumentException("Unknown mapping for string: ${json.asString}")
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.world.feature
import com.cobblemon.mod.common.platform.PlatformRegistry
import com.cobblemon.mod.common.world.feature.structure.CobblemonFeature
import net.minecraft.core.Registry
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
@@ -28,4 +29,7 @@ object CobblemonFeatures : PlatformRegistry<Registry<Feature<*>>, ResourceKey<Re
val MINT_FEATURE = create("mint", MintBlockFeature())
@JvmField
val BERRY_GROVE_FEATURE = CobblemonFeatures.create("berry_grove", BerryGroveFeature())
@JvmField
val COBBLEMON_FEATURE = create("structure", CobblemonFeature())
}
@@ -0,0 +1,63 @@
/*
* 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.world.feature.structure
import net.minecraft.core.BlockPos
import net.minecraft.core.Vec3i
import net.minecraft.util.RandomSource
import net.minecraft.world.level.WorldGenLevel
import net.minecraft.world.level.block.Mirror
import net.minecraft.world.level.block.Rotation
import net.minecraft.world.level.levelgen.feature.Feature
import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager
class CobblemonFeature : Feature<CobblemonFeatureConfiguration>(CobblemonFeatureConfiguration.CODEC) {
override fun place(context: FeaturePlaceContext<CobblemonFeatureConfiguration>): Boolean {
val randomSource: RandomSource = context.random()
val worldGenLevel: WorldGenLevel = context.level()
val blockPos: BlockPos = context.origin()
val rotation: Rotation = Rotation.getRandom(randomSource)
val cobblemonFeatureConfiguration = context.config()
val i = randomSource.nextInt(cobblemonFeatureConfiguration.cobblemonStructures.size)
val structureTemplateManager: StructureTemplateManager = worldGenLevel.level.server.structureManager
val structureTemplate: StructureTemplate =
structureTemplateManager.getOrCreate(cobblemonFeatureConfiguration.cobblemonStructures[i])
val structurePlaceSettings = StructurePlaceSettings()
.setRotation(rotation)
.setRandom(randomSource)
val vec3i: Vec3i = structureTemplate.getSize(rotation)
if (vec3i.x == 0 && vec3i.y == 0 && vec3i.z == 0) {
return false
}
val blockPos2: BlockPos = blockPos.offset(-vec3i.x / 2, 0, -vec3i.z / 2)
val blockPos3 = structureTemplate.getZeroPositionWithTransform(blockPos2, Mirror.NONE, rotation)
structurePlaceSettings.clearProcessors()
val processors = (cobblemonFeatureConfiguration.cobblemonProcessors.value() as StructureProcessorList).list()
processors.forEach { structurePlaceSettings.addProcessor(it) }
return structureTemplate.placeInWorld(
worldGenLevel,
blockPos3,
blockPos3,
structurePlaceSettings,
randomSource,
2
)
}
}
@@ -0,0 +1,37 @@
/*
* 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.world.feature.structure
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.Holder
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorType
class CobblemonFeatureConfiguration(
val cobblemonStructures: List<ResourceLocation>,
val cobblemonProcessors: Holder<StructureProcessorList>,
) : FeatureConfiguration {
init {
if (cobblemonStructures.isEmpty()) {
throw IllegalArgumentException("Cobblemon structure lists need at least one entry")
}
}
companion object {
val CODEC: Codec<CobblemonFeatureConfiguration> = RecordCodecBuilder.create { instance ->
instance.group(
ResourceLocation.CODEC.listOf().fieldOf("cobblemon_structures").forGetter { it.cobblemonStructures },
StructureProcessorType.LIST_CODEC.fieldOf("cobblemon_processors").forGetter { it.cobblemonProcessors },
).apply(instance, ::CobblemonFeatureConfiguration)
}
}
}
@@ -1157,6 +1157,24 @@
"cobblemon.ui.moves.pp": "%1$sPP",
"cobblemon.ui.moves.forget": "Forget Move",
"cobblemon.ui.pokeedit.title": "PokeEdit",
"cobblemon.ui.pokeedit.general": "General",
"cobblemon.ui.pokeedit.abilities": "Abilities",
"cobblemon.ui.pokeedit.stats": "EVs/IVs",
"cobblemon.ui.pokeedit.moves": "Moves",
"cobblemon.ui.aspects": "Aspects",
"cobblemon.ui.enabled": "Enabled",
"cobblemon.ui.disabled": "Disabled",
"cobblemon.ui.none": "None",
"cobblemon.ui.cosmetic_slot": "Cosmetic Slot",
"cobblemon.ui.held_item_slot": "Held Item",
"cobblemon.ui.ot": "OT",
"cobblemon.ui.pokeedit.moves.active": "Active Moves",
"cobblemon.ui.pokeedit.moves.moveset": "Moveset",
"cobblemon.ui.pokeedit.give": "Give",
"cobblemon.ui.pokeedit.spawn": "Spawn",
"cobblemon.ui.pokeedit.evs.total": "EV Total: %1$s/%2$s",
"cobblemon.ui.stats": "Stats",
"cobblemon.ui.stats.stat": "Stat",
"cobblemon.ui.stats.ivs": "IVs",
@@ -1214,6 +1232,7 @@
"cobblemon.ui.party.party_select_description": "Press %1$s/%2$s or hold %3$s and scroll.",
"cobblemon.ui.power": "Power",
"cobblemon.ui.accuracy": "Accuracy",
"cobblemon.ui.category": "Category",
"cobblemon.ui.effect": "Effect",
"cobblemon.ui.pokemon": "Pokémon",
"cobblemon.ui.exit": "Exit",
@@ -2122,6 +2141,7 @@
"cobblemon.ui.status.psn": "PSN",
"cobblemon.ui.status.slp": "SLP",
"cobblemon.ui.status.tox": "PSN",
"cobblemon.ui.exp.number": "+%1$s §oEXP",
"cobblemon.battle.ui.fight": "Fight",
"cobblemon.battle.ui.switch": "Switch",
"cobblemon.battle.ui.capture": "Catch",
@@ -3403,6 +3423,7 @@
"cobblemon.starterselection.category.galar": "Galar",
"cobblemon.starterselection.category.hisui_bias": "Hisui",
"cobblemon.starterselection.category.paldea": "Paldea",
"cobblemon.starterselection.category.special": "Special",
"cobblemon.stat.hp.name": "HP",
"cobblemon.stat.attack.name": "Attack",
@@ -0,0 +1,6 @@
{
"parent": "item/handheld",
"textures": {
"layer0": "cobblemon:item/pokeedit_wand"
}
}
@@ -161,7 +161,7 @@
"sounds": [ { "name": "cobblemon:item/pokedex/pokedex_scan_zoom_increment", "volume": 0.7 } ]
},
"evolution.notification": {
"sounds": [ { "name": "cobblemon:evolution/evolution_notification", "volume": 0.7 } ]
"sounds": [ { "name": "cobblemon:evolution/evolution_notification", "volume": 1.0 } ]
},
"evolution.full": {
"sounds": [ { "name": "cobblemon:evolution/evolution_full", "volume": 0.9 } ]
@@ -173,7 +173,7 @@
"sounds": [ { "name": "cobblemon:gui/levelup_start", "volume": 0.15 } ]
},
"gui.levelup": {
"sounds": [ { "name": "cobblemon:gui/levelup", "volume": 0.65 } ]
"sounds": [ { "name": "cobblemon:gui/levelup", "volume": 0.55 } ]
},
"gui.click": {
"sounds": [ "cobblemon:gui/click" ]

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