Compare commits

...
Author SHA1 Message Date
Islana 17ca9a31f1 Fix for forewarn not displaying moves with spaces in them. 2023-08-19 03:55:54 -04:00
Islana c851f57e36 Fix for forewarn not displaying moves with spaces in them. 2023-08-19 03:53:31 -04:00
Islana 2b3497e63d Fix for forewarn not displaying moves with spaces in them. 2023-08-19 03:47:11 -04:00
Islana c014efa9a6 Merge remote-tracking branch 'origin/main' 2023-08-18 23:48:01 -04:00
Hiroku 6dc4b6af24 Merge branch 'Zackghast7-main-patch-07680' into 'main'
Updated evYield for overqwil.json

See merge request cable-mc/cobblemon!586
2023-08-14 05:12:06 +00:00
Zack 071278caa2 Updated evYield for overqwil.json 2023-08-12 16:37:57 +00:00
Hiroku 12a29d1289 Merge branch 'Zackghast7-main-patch-64696' into 'main'
Update wyrdeer.json with the correct evs

See merge request cable-mc/cobblemon!585
2023-08-11 22:05:15 +00:00
Zack bc85fdb778 Update wyrdeer.json with the correct evs 2023-08-11 22:00:37 +00:00
Hiroku 0d0d659ce5 Merge branch 'added-sounds' into 'main'
New Healing Machine sound, shortened Healing Machine's heal time, updated PC and UI sounds

See merge request cable-mc/cobblemon!584
2023-08-11 21:42:15 +00:00
SterrezoandHiroku 535e0b6f87 New Healing Machine sound, shortened Healing Machine's heal time, updated PC and UI sounds 2023-08-11 21:42:15 +00:00
Licious dd8cb49295 Merge branch 'spawning-improvements' into 'main'
Fixed Pokémon spawning in walls, improve canSeeSky spawn condition, use new...

See merge request cable-mc/cobblemon!583
2023-08-11 19:24:56 +00:00
Paul 2c5968986d Fixed Pokémon spawning in walls, improve canSeeSky spawn condition, use new FlatSelector selection algorithm, remove terrible width spawn condition 2023-08-11 21:12:27 +10:00
Licious 7b29d31009 Merge branch 'pokemon_loot_gamerule' into 'main'
Add doPokemonLoot gamerule

See merge request cable-mc/cobblemon!576
2023-08-09 07:53:17 +00:00
MiaandLicious 8820a546db Add doPokemonLoot gamerule 2023-08-09 07:53:17 +00:00
Nick c4eb422722 Merge branch 'new-held-items' into 'main'
Added in five more held items

See merge request cable-mc/cobblemon!581
2023-08-08 22:48:19 +00:00
DeltricandNick d8f4638806 Added in five more held items 2023-08-08 22:48:19 +00:00
Hiroku f92aa1777c Merge branch 'fix/pokemon-property-extractor' into 'main'
add createPokemonProperties(List)

See merge request cable-mc/cobblemon!578
2023-08-07 03:35:13 +00:00
FawksXandHiroku 8d864a3ff8 add createPokemonProperties(List) 2023-08-07 03:35:13 +00:00
Paul 17a1409619 Fixed stat hex colour bleeding into text 2023-08-06 16:18:56 +10:00
Paul 51f92e1635 Further improvements to stat hex 2023-08-06 16:14:51 +10:00
Hiroku 9c3f8c9e8f Merge branch 'main' into 'main'
Fixes issue #394. Also fixes an (unlisted?) issue where special ivs/evs are not allocatable via pokegive

Closes #394

See merge request cable-mc/cobblemon!574
2023-08-06 00:44:12 +00:00
ApionandHiroku 923f40ab68 Fixes issue #394. Also fixes an (unlisted?) issue where special ivs/evs are not allocatable via pokegive 2023-08-06 00:44:12 +00:00
Paul cd7c3a0319 Merge remote-tracking branch 'origin/main' 2023-08-06 10:17:02 +10:00
Paul 8311dd8785 Fixed chance of sheared Pokémon dropping no wool 2023-08-06 09:37:53 +10:00
Hiroku f489cd096f Merge branch 'solve_playerdata_cache' into 'main'
Solve playerdata cache

See merge request cable-mc/cobblemon!575
2023-08-05 21:46:43 +00:00
Pedro SouzaandHiroku 0a4dc34895 Solve playerdata cache 2023-08-05 21:46:42 +00:00
Paul b1bc867ef8 Merge remote-tracking branch 'origin/main' 2023-08-05 17:17:29 +10:00
Paul 3f6af25afa Added dire hit and guard spec to creative menu 2023-08-05 17:17:17 +10:00
Hiroku 9946c39de0 Merge branch 'sound_updates' into 'main'
Sound updates

See merge request cable-mc/cobblemon!573
2023-08-04 21:48:21 +00:00
LilyandHiroku 037ba953b7 Sound updates 2023-08-04 21:48:21 +00:00
Hiroku 5b7b9342e5 Merge branch 'plant-issues' into 'main'
Various new crop fixes

Closes #396 and #347

See merge request cable-mc/cobblemon!569
2023-08-04 06:35:03 +00:00
LiciousandHiroku c52b99270b Various new crop fixes 2023-08-04 06:35:03 +00:00
Licious 2e67e804a7 Merge branch 'healing-items' into 'main'
Added healing items / bag items

See merge request cable-mc/cobblemon!333
2023-07-31 12:01:18 +00:00
LandonandLicious de58d11f9e Added healing items / bag items 2023-07-31 12:01:17 +00:00
Islana f63970163a Merge remote-tracking branch 'origin/main' 2023-06-28 00:08:07 -04:00
Islana b34adc6b57 Shedinja revive Fix 2023-06-28 00:07:13 -04:00
415 changed files with 8888 additions and 2246 deletions
+6 -1
View File
@@ -29,6 +29,7 @@
- Added the `/pokemonrestart <reset_starters>` and the `/pokemonrestartother <player> <reset_starters>` command allowing command block/mcfunction users to reset a players Pokémon data.
- Added support for "isBattle" and "isTouchingWater" properties on resource pack Pokémon poses. This allows your custom Pokémon to be posed differently when in battle.
- Added support for "isVisible" on a transformed part on resource pack Pokémon poses. This allows your custom Pokémon to have bones disappear in specific poses, such as hiding Greninja's throwing star when not in a battle pose.
- Added the `doPokemonLoot` gamerule to toggle Pokémon dropping items/exp on death.
### Pokémon Added
#### Gen 2
@@ -190,9 +191,11 @@
- Clicking categories of the Stat sub-section or the party reorder button in the Summary screen will now produce a click sound.
- Updated PC Recipe.
- Improved Pokémon AI and movement.
- Friendship will slowly increase when Pokémon are shoulder-mounted.
### Fixes
- Fixed spawning moon phase dependent Pokémon only when the moon phase is wrong
- Fixed large Pokémon spawning partially inside walls where they suffocate.
- Fixed messages for entry hazards, screens, Tailwind, Perish Song, Destiny Bond, Shed Skin, Uproar, Forewarn, Disguise, Arena Trap, Yawn, Curse, Clamp, Whirlpool, Liquid Ooze, Miracle Eye and Safeguard.
- Fixed Porygon not evolving with an Upgrade.
- Fixed super sized Pumpkaboo not having any moves.
@@ -220,7 +223,9 @@
- Fixed Pokémon variants and layers not rendering correctly when shouldered and playing on a dedicated server, existing shoulders affected will need to be retrieved and shouldered again.
- Shoulder effects will now stay applied through situations that remove potion effects such as drinking milk.
- Fixed Shedninja evolving to use the consumed Pokeball and removed the held item.
- Fixed non-existent species in spawn pool file causing random species to spawn
- Fixed non-existent species in spawn pool file causing random species to spawn.
- Fixed shearing Pokémon dropping 0-2 wool instead of 1-3.
- Fixed some alignment issues in the stat hexagon of the summary menu.
### Developer
- Added SpawnEvent
@@ -8,14 +8,12 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.block.ApricornBlock;
import com.cobblemon.mod.common.block.ShearableBlock;
import kotlin.Unit;
import net.minecraft.block.BlockState;
import net.minecraft.block.dispenser.ShearsDispenserBehavior;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.event.GameEvent;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -24,15 +22,12 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ShearsDispenserBehavior.class)
public abstract class ShearsDispenserBehaviorMixin {
// This exists so we can execute our custom interaction only if no one redirected shears interactions to overwrite the vanilla ones.
// This exists, so we can execute our custom interaction only if no one redirected shears interactions to overwrite the vanilla ones.
@Inject(method = "tryShearBlock", at = @At("HEAD"), cancellable = true)
private static void cobblemon$tryApricornHarvest(ServerWorld world, BlockPos pos, CallbackInfoReturnable<Boolean> cir) {
final BlockState state = world.getBlockState(pos);
if (state.getBlock() instanceof ApricornBlock apricornBlock && state.get(ApricornBlock.Companion.getAGE()) == ApricornBlock.MAX_AGE) {
world.playSound(null, pos, SoundEvents.ENTITY_SHEEP_SHEAR, SoundCategory.BLOCKS, 1F, 1F);
apricornBlock.harvest(world, state, pos);
world.emitGameEvent(null, GameEvent.SHEAR, pos);
cir.setReturnValue(true);
if (state.getBlock() instanceof ShearableBlock shearableBlock) {
cir.setReturnValue(shearableBlock.attemptShear(world, state, pos, () -> Unit.INSTANCE));
}
}
@@ -33,4 +33,9 @@ public interface BlocksInvoker {
throw new UnsupportedOperationException();
}
@Invoker("createFlowerPotBlock")
static FlowerPotBlock createFlowerPotBlock(Block flower, FeatureFlag... requiredFeatures) {
throw new UnsupportedOperationException();
}
}
@@ -54,13 +54,19 @@ import com.cobblemon.mod.common.api.storage.pc.PCStore
import com.cobblemon.mod.common.api.storage.pc.link.PCLinkManager
import com.cobblemon.mod.common.api.storage.player.PlayerDataStoreManager
import com.cobblemon.mod.common.api.tags.CobblemonEntityTypeTags
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.ShowdownThread
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.command.argument.*
import com.cobblemon.mod.common.command.argument.MoveArgumentType
import com.cobblemon.mod.common.command.argument.PartySlotArgumentType
import com.cobblemon.mod.common.command.argument.PokemonArgumentType
import com.cobblemon.mod.common.command.argument.PokemonPropertiesArgumentType
import com.cobblemon.mod.common.command.argument.PokemonStoreArgumentType
import com.cobblemon.mod.common.command.argument.SpawnBucketArgumentType
import com.cobblemon.mod.common.config.CobblemonConfig
import com.cobblemon.mod.common.config.LastChangedVersion
import com.cobblemon.mod.common.config.constraint.IntConstraint
@@ -85,10 +91,25 @@ import com.cobblemon.mod.common.pokemon.properties.tags.PokemonFlagProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.trade.TradeManager
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.ifDedicatedServer
import com.cobblemon.mod.common.util.isLaterVersion
import com.cobblemon.mod.common.util.party
import com.cobblemon.mod.common.util.removeAmountIf
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.world.feature.CobblemonPlacedFeatures
import com.cobblemon.mod.common.world.feature.ore.CobblemonOrePlacedFeatures
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.UUID
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import net.minecraft.client.MinecraftClient
import net.minecraft.command.argument.serialize.ConstantArgumentSerializer
import net.minecraft.entity.data.TrackedDataHandlerRegistry
@@ -98,15 +119,6 @@ import net.minecraft.registry.RegistryKey
import net.minecraft.util.WorldSavePath
import net.minecraft.world.World
import org.apache.logging.log4j.LogManager
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.*
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
object Cobblemon {
const val MODID = "cobblemon"
@@ -284,9 +296,9 @@ object Cobblemon {
var pokeball = Items.AIR
player.inventory.combinedInventory.forEach {
it.forEach {
itemStack -> if (itemStack.item is PokeBallItem && pokeball == Items.AIR) {
pokeball = itemStack.item as PokeBallItem
}
itemStack -> if (itemStack.item is PokeBallItem && pokeball == Items.AIR) {
pokeball = itemStack.item as PokeBallItem
}
}
}
player.inventory.removeAmountIf(1) { it.item is PokeBallItem }
@@ -301,7 +313,7 @@ object Cobblemon {
}
}
PokemonSpecies.observable.subscribe {
BagItems.observable.subscribe {
LOGGER.info("Starting dummy Showdown battle to force it to pre-load data.")
battleRegistry.startBattle(
BattleFormat.GEN_9_SINGLES,
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.mixin.invoker.*
import com.cobblemon.mod.common.platform.PlatformRegistry
import net.minecraft.block.*
import net.minecraft.block.piston.PistonBehavior
import net.minecraft.entity.effect.StatusEffects
import net.minecraft.registry.Registries
import net.minecraft.registry.Registry
import net.minecraft.registry.RegistryKey
@@ -26,7 +27,7 @@ import net.minecraft.registry.RegistryKeys
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.util.math.intprovider.UniformIntProvider
@Suppress("SameParameterValue", "HasPlatformType", "MemberVisibilityCanBePrivate")
@Suppress("SameParameterValue", "HasPlatformType", "MemberVisibilityCanBePrivate", "unused")
object CobblemonBlocks : PlatformRegistry<Registry<Block>, RegistryKey<Registry<Block>>, Block>() {
override val registry: Registry<Block> = Registries.BLOCK
@@ -130,7 +131,7 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, RegistryKey<Registry<
val YELLOW_APRICORN_SAPLING = this.create("yellow_apricorn_sapling", ApricornSaplingBlock(PLANT_PROPERTIES, Apricorn.YELLOW))
@JvmField
val MEDICINAL_LEEK_CROP = this.create("medicinal_leek_crop", MedicinalLeekBlock(AbstractBlock.Settings.create().pistonBehavior(PistonBehavior.DESTROY).burnable().mapColor(MapColor.DULL_RED).noCollision().ticksRandomly().breakInstantly().sounds(BlockSoundGroup.CROP)))
val MEDICINAL_LEEK = this.create("medicinal_leek", MedicinalLeekBlock(AbstractBlock.Settings.create().pistonBehavior(PistonBehavior.DESTROY).burnable().mapColor(MapColor.DULL_RED).noCollision().ticksRandomly().breakInstantly().sounds(BlockSoundGroup.CROP)))
@JvmField
val ENERGY_ROOT = this.create("energy_root", EnergyRootBlock(AbstractBlock.Settings.create().pistonBehavior(PistonBehavior.DESTROY).burnable().mapColor(MapColor.DIRT_BROWN).noCollision().ticksRandomly().breakInstantly().sounds(BlockSoundGroup.ROOTS)))
@JvmField
@@ -209,6 +210,10 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, RegistryKey<Registry<
@JvmField
val VIVICHOKE_SEEDS = this.create("vivichoke_seeds", VivichokeBlock(AbstractBlock.Settings.create().pistonBehavior(PistonBehavior.DESTROY).burnable().mapColor(MapColor.DARK_GREEN).noCollision().ticksRandomly().breakInstantly().sounds(BlockSoundGroup.CROP)))
@JvmField
val PEP_UP_FLOWER = this.create("pep_up_flower", FlowerBlock(StatusEffects.LEVITATION, 10, AbstractBlock.Settings.create().mapColor(MapColor.DARK_GREEN).noCollision().breakInstantly().sounds(BlockSoundGroup.GRASS).offset(AbstractBlock.OffsetType.XZ).pistonBehavior(PistonBehavior.DESTROY)))
@JvmField
val POTTED_PEP_UP_FLOWER = this.create("potted_pep_up_flower", BlocksInvoker.createFlowerPotBlock(PEP_UP_FLOWER))
/**
* Returns a map of all the blocks that can be stripped with an axe in the format of input - output.
@@ -8,24 +8,25 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.mulch.MulchVariant
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.Natures
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.block.MintBlock.MintType
import com.cobblemon.mod.common.item.*
import com.cobblemon.mod.common.item.group.CobblemonItemGroups
import com.cobblemon.mod.common.item.interactive.CandyItem
import com.cobblemon.mod.common.item.interactive.EnergyRoot
import com.cobblemon.mod.common.item.interactive.LinkCableItem
import com.cobblemon.mod.common.item.interactive.MintItem
import com.cobblemon.mod.common.item.interactive.RemedyItem
import com.cobblemon.mod.common.item.interactive.VitaminItem
import com.cobblemon.mod.common.item.battle.DireHitItem
import com.cobblemon.mod.common.item.battle.GuardSpecItem
import com.cobblemon.mod.common.item.battle.XStatItem
import com.cobblemon.mod.common.item.interactive.*
import com.cobblemon.mod.common.item.interactive.PotionItem
import com.cobblemon.mod.common.platform.PlatformRegistry
import com.cobblemon.mod.common.pokeball.PokeBall
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import net.minecraft.block.Block
import net.minecraft.block.ComposterBlock
import net.minecraft.entity.effect.StatusEffectInstance
import net.minecraft.entity.effect.StatusEffects
import net.minecraft.item.*
import net.minecraft.registry.Registries
import net.minecraft.registry.Registry
@@ -248,17 +249,26 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, RegistryKey<Registry<It
@JvmField
val ZINC = create("zinc", VitaminItem(Stats.SPECIAL_DEFENCE))
@JvmField
val MEDICINAL_LEEK = create("medicinal_leek", MedicinalLeekItem(CobblemonBlocks.MEDICINAL_LEEK_CROP))
val MEDICINAL_LEEK = create("medicinal_leek", MedicinalLeekItem(CobblemonBlocks.MEDICINAL_LEEK, Item.Settings().food(FoodComponent.Builder().snack().hunger(1).saturationModifier(0.2f).build())))
@JvmField
val ROASTED_LEEK = create("roasted_leek", Item(Item.Settings().food(FoodComponent.Builder().snack().hunger(3).saturationModifier(0.2f).build())))
val ROASTED_LEEK = create("roasted_leek", Item(Item.Settings().food(FoodComponent.Builder().snack().hunger(3).saturationModifier(0.3f).build())))
@JvmField
val BRAISED_VIVICHOKE = create("braised_vivichoke", Item(Item.Settings().food(FoodComponent.Builder().hunger(6).saturationModifier(0.6f).build())))
val BRAISED_VIVICHOKE = create("braised_vivichoke", Item(Item.Settings().maxCount(1).food(FoodComponent.Builder().hunger(6).saturationModifier(0.6f).build())))
@JvmField
val ENERGY_ROOT = create("energy_root", EnergyRoot(CobblemonBlocks.ENERGY_ROOT))
val VIVICHOKE_DIP = create("vivichoke_dip", StewItem(Item.Settings().maxCount(1)
.food(FoodComponent.Builder()
.hunger(10)
.saturationModifier(1.2F)
.statusEffect(StatusEffectInstance(StatusEffects.ABSORPTION, 600, 0), 1F)
.build()
)
))
@JvmField
val ENERGY_ROOT = create("energy_root", EnergyRootItem(CobblemonBlocks.ENERGY_ROOT, Item.Settings().food(FoodComponent.Builder().hunger(1).snack().saturationModifier(0.2f).build())))
@JvmField
val REVIVAL_HERB = create("revival_herb", RevivalHerbItem(CobblemonBlocks.REVIVAL_HERB))
@JvmField
val PEP_UP_FLOWER = create("pep_up_flower", PepUpFlowerItem(Item.Settings()))
val PEP_UP_FLOWER = blockItem("pep_up_flower", CobblemonBlocks.PEP_UP_FLOWER)
@JvmField
val MEDICINAL_BREW = create("medicinal_brew", Item(Item.Settings()))
@JvmField
@@ -267,18 +277,30 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, RegistryKey<Registry<It
val FINE_REMEDY = create("fine_remedy", RemedyItem(RemedyItem.FINE))
@JvmField
val SUPERB_REMEDY = create("superb_remedy", RemedyItem(RemedyItem.SUPERB))
@JvmField
val HEAL_POWDER = create("heal_powder", Item(Item.Settings()))
val POTION = create("potion", PotionItem(PotionType.POTION))
@JvmField
val LEEK_AND_POTATO_STEW = create("leek_and_potato_stew", StewItem(Item.Settings().food(FoodComponent.Builder().hunger(8).saturationModifier(0.6f).build())))
val SUPER_POTION = create("super_potion", PotionItem(PotionType.SUPER_POTION))
@JvmField
val REVIVE = create("revive", Item(Item.Settings()))
val HYPER_POTION = create("hyper_potion", PotionItem(PotionType.HYPER_POTION))
@JvmField
val MAX_REVIVE = create("max_revive", Item(Item.Settings()))
val MAX_POTION = create("max_potion", PotionItem(PotionType.MAX_POTION))
@JvmField
val PP_UP = create("pp_up", Item(Item.Settings()))
val FULL_RESTORE = create("full_restore", PotionItem(PotionType.FULL_RESTORE))
@JvmField
val PP_MAX = create("pp_max", Item(Item.Settings()))
val HEAL_POWDER = create("heal_powder", HealPowderItem())
@JvmField
val LEEK_AND_POTATO_STEW = create("leek_and_potato_stew", StewItem(Item.Settings().food(FoodComponent.Builder().hunger(8).saturationModifier(0.6f).build()).maxCount(1)))
@JvmField
val REVIVE = create("revive", ReviveItem(max = false))
@JvmField
val MAX_REVIVE = create("max_revive", ReviveItem(max = true))
@JvmField
val PP_UP = create("pp_up", PPUpItem(1))
@JvmField
val PP_MAX = create("pp_max", PPUpItem(3))
@JvmField
val RED_MINT_SEEDS = blockItem("red_mint_seeds", MintType.RED.getCropBlock())
@@ -348,12 +370,28 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, RegistryKey<Registry<It
@JvmField
val SERIOUS_MINT = create("serious_mint", MintItem(Natures.SERIOUS))
@JvmField val X_ACCURACY = create("x_accuracy", CobblemonItem(Item.Settings()))
@JvmField val X_ATTACK = create("x_attack", CobblemonItem(Item.Settings()))
@JvmField val X_DEFENSE = create("x_defense", CobblemonItem(Item.Settings()))
@JvmField val X_SP_ATK = create("x_sp_atk", CobblemonItem(Item.Settings()))
@JvmField val X_SP_DEF = create("x_sp_def", CobblemonItem(Item.Settings()))
@JvmField val X_SPEED = create("x_speed", CobblemonItem(Item.Settings()))
@JvmField val X_ACCURACY = create("x_${Stats.ACCURACY.identifier.path}", XStatItem(Stats.ACCURACY))
@JvmField val X_ATTACK = create("x_${Stats.ATTACK.identifier.path}", XStatItem(Stats.ATTACK))
@JvmField val X_DEFENSE = create("x_${Stats.DEFENCE.identifier.path}", XStatItem(Stats.DEFENCE))
@JvmField val X_SP_ATK = create("x_${Stats.SPECIAL_ATTACK.identifier.path}", XStatItem(Stats.SPECIAL_ATTACK))
@JvmField val X_SP_DEF = create("x_${Stats.SPECIAL_DEFENCE.identifier.path}", XStatItem(Stats.SPECIAL_DEFENCE))
@JvmField val X_SPEED = create("x_${Stats.SPEED.identifier.path}", XStatItem(Stats.SPEED))
@JvmField val DIRE_HIT = create("dire_hit", DireHitItem())
@JvmField val GUARD_SPEC = create("guard_spec", GuardSpecItem())
@JvmField val BURN_HEAL = create("burn_heal", StatusCureItem("item.cobblemon.burn_heal", Statuses.BURN))
@JvmField val PARALYZE_HEAL = create("paralyze_heal", StatusCureItem("item.cobblemon.paralyze_heal", Statuses.PARALYSIS))
@JvmField val ICE_HEAL = create("ice_heal", StatusCureItem("item.cobblemon.ice_heal", Statuses.FROZEN))
@JvmField val ANTIDOTE = create("antidote", StatusCureItem("item.cobblemon.antidote", Statuses.POISON, Statuses.POISON_BADLY))
@JvmField val AWAKENING = create("awakening", StatusCureItem("item.cobblemon.awakening", Statuses.SLEEP))
@JvmField val FULL_HEAL = create("full_heal", StatusCureItem("item.cobblemon.full_heal"))
@JvmField val ETHER = create("ether", EtherItem(max = false))
@JvmField val MAX_ETHER = create("max_ether", EtherItem(max = true))
@JvmField val ELIXIR = create("elixir", ElixirItem(max = false))
@JvmField val MAX_ELIXIR = create("max_elixir", ElixirItem(max = true))
/**
* Evolution Ores and Stones
@@ -425,7 +463,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, RegistryKey<Registry<It
@JvmField
val ASSAULT_VEST = heldItem("assault_vest")
@JvmField
val BIG_ROOT = create("big_root", BigRoot(CobblemonBlocks.BIG_ROOT))
val BIG_ROOT = blockItem("big_root", CobblemonBlocks.BIG_ROOT)
@JvmField
val BLACK_BELT = heldItem("black_belt")
@JvmField
@@ -488,26 +526,44 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, RegistryKey<Registry<It
val TWISTED_SPOON = heldItem("twisted_spoon")
@JvmField
val WISE_GLASSES = heldItem("wise_glasses")
@JvmField
val MENTAL_HERB = heldItem("mental_herb")
@JvmField
val MIRROR_HERB = heldItem("mirror_herb")
@JvmField
val POWER_HERB = heldItem("power_herb")
@JvmField
val WHITE_HERB = heldItem("white_herb")
@JvmField
val BRIGHT_POWDER = heldItem("bright_powder")
@JvmField
val METAL_POWDER = heldItem("metal_powder")
@JvmField
val QUICK_POWDER = heldItem("quick_powder")
@JvmField
val DESTINY_KNOT = heldItem("destiny_knot")
@JvmField
val EVERSTONE = heldItem("everstone")
// Mulch
@JvmField
val MULCH_BASE = noSettingsItem("mulch_base")
@JvmField
val COARSE_MULCH = noSettingsItem("coarse_mulch")
val COARSE_MULCH = mulchItem("coarse_mulch", MulchVariant.COARSE)
@JvmField
val GROWTH_MULCH = noSettingsItem("growth_mulch")
val GROWTH_MULCH = mulchItem("growth_mulch", MulchVariant.GROWTH)
@JvmField
val HUMID_MULCH = noSettingsItem("humid_mulch")
val HUMID_MULCH = mulchItem("humid_mulch", MulchVariant.HUMID)
@JvmField
val LOAMY_MULCH = noSettingsItem("loamy_mulch")
val LOAMY_MULCH = mulchItem("loamy_mulch", MulchVariant.LOAMY)
@JvmField
val PEAT_MULCH = noSettingsItem("peat_mulch")
val PEAT_MULCH = mulchItem("peat_mulch", MulchVariant.PEAT)
@JvmField
val RICH_MULCH = noSettingsItem("rich_mulch")
val RICH_MULCH = mulchItem("rich_mulch", MulchVariant.RICH)
@JvmField
val SANDY_MULCH = noSettingsItem("sandy_mulch")
val SANDY_MULCH = mulchItem("sandy_mulch", MulchVariant.SANDY)
@JvmField
val SURPRISE_MULCH = noSettingsItem("surprise_mulch")
val SURPRISE_MULCH = mulchItem("surprise_mulch", MulchVariant.SURPRISE)
// Misc
@JvmField
@@ -517,6 +573,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, RegistryKey<Registry<It
private fun noSettingsItem(name: String): CobblemonItem = this.create(name, CobblemonItem(Item.Settings()))
private fun mulchItem(name: String, mulchVariant: MulchVariant): MulchItem = this.create(name, MulchItem(mulchVariant))
private fun pokeBallItem(pokeBall: PokeBall): PokeBallItem {
val item = create(pokeBall.name.path, PokeBallItem(pokeBall))
pokeBall.item = item
@@ -18,6 +18,9 @@ import com.cobblemon.mod.common.client.net.data.UnlockReloadPacketHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
import com.cobblemon.mod.common.client.net.callback.move.OpenMoveCallbackHandler
import com.cobblemon.mod.common.client.net.callback.party.OpenPartyCallbackHandler
import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallbackHandler
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
@@ -49,6 +52,9 @@ import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.net.messages.client.battle.*
import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPacket
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallbackPacket
import com.cobblemon.mod.common.net.messages.client.data.AbilityRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.data.MovesRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.data.PropertiesCompletionRegistrySyncPacket
@@ -98,6 +104,12 @@ import com.cobblemon.mod.common.net.messages.server.RequestMoveSwapPacket
import com.cobblemon.mod.common.net.messages.server.SelectStarterPacket
import com.cobblemon.mod.common.net.messages.server.SendOutPokemonPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.callback.party.PartyPokemonSelectedPacket
import com.cobblemon.mod.common.net.messages.server.callback.party.PartySelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyMoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyPokemonMoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.pasture.PasturePokemonPacket
import com.cobblemon.mod.common.net.messages.server.pasture.UnpastureAllPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPacket
@@ -122,6 +134,12 @@ import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.callback.party.PartyPokemonSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.callback.party.PartySelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyMoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyPokemonMoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
@@ -236,7 +254,7 @@ object CobblemonNetwork : NetworkManager {
this.createClientBound(BattleCaptureStartPacket.ID, BattleCaptureStartPacket::decode, BattleCaptureStartHandler)
this.createClientBound(BattleCaptureEndPacket.ID, BattleCaptureEndPacket::decode, BattleCaptureEndHandler)
this.createClientBound(BattleCaptureShakePacket.ID, BattleCaptureShakePacket::decode, BattleCaptureShakeHandler)
this.createClientBound(BattleApplyCaptureResponsePacket.ID, BattleApplyCaptureResponsePacket::decode, BattleApplyCaptureResponseHandler)
this.createClientBound(BattleApplyPassResponsePacket.ID, BattleApplyPassResponsePacket::decode, BattleApplyPassResponseHandler)
this.createClientBound(BattleChallengeNotificationPacket.ID, BattleChallengeNotificationPacket::decode, BattleChallengeNotificationHandler)
this.createClientBound(BattleUpdateTeamPokemonPacket.ID, BattleUpdateTeamPokemonPacket::decode, BattleUpdateTeamPokemonHandler)
this.createClientBound(BattlePersistentStatusPacket.ID, BattlePersistentStatusPacket::decode, BattlePersistentStatusHandler)
@@ -282,6 +300,15 @@ object CobblemonNetwork : NetworkManager {
// Behaviours
this.createClientBound(PokemonCryPacket.ID, PokemonCryPacket::decode, PokemonCryHandler)
// Move select packets
this.createClientBound(OpenMoveCallbackPacket.ID, OpenMoveCallbackPacket::decode, OpenMoveCallbackHandler)
// Party select packets
this.createClientBound(OpenPartyCallbackPacket.ID, OpenPartyCallbackPacket::decode, OpenPartyCallbackHandler)
// Party move select packets
this.createClientBound(OpenPartyMoveCallbackPacket.ID, OpenPartyMoveCallbackPacket::decode, OpenPartyMoveCallbackHandler)
}
override fun registerServerBound() {
@@ -332,6 +359,18 @@ object CobblemonNetwork : NetworkManager {
this.createServerBound(PasturePokemonPacket.ID, PasturePokemonPacket::decode, PasturePokemonHandler)
this.createServerBound(UnpasturePokemonPacket.ID, UnpasturePokemonPacket::decode, UnpasturePokemonHandler)
this.createServerBound(UnpastureAllPokemonPacket.ID, UnpastureAllPokemonPacket::decode, UnpastureAllPokemonHandler)
// Move select packets
this.createServerBound(MoveSelectedPacket.ID, MoveSelectedPacket::decode, MoveSelectedHandler)
this.createServerBound(MoveSelectCancelledPacket.ID, MoveSelectCancelledPacket::decode, MoveSelectCancelledHandler)
// Party select packets
this.createServerBound(PartyPokemonSelectedPacket.ID, PartyPokemonSelectedPacket::decode, PartyPokemonSelectedHandler)
this.createServerBound(PartySelectCancelledPacket.ID, PartySelectCancelledPacket::decode, PartySelectCancelledHandler)
// Party move select packets
this.createServerBound(PartyPokemonMoveSelectedPacket.ID, PartyPokemonMoveSelectedPacket::decode, PartyPokemonMoveSelectedHandler)
this.createServerBound(PartyMoveSelectCancelledPacket.ID, PartyMoveSelectCancelledPacket::decode, PartyMoveSelectCancelledHandler)
}
private inline fun <reified T : NetworkPacket<T>> createClientBound(identifier: Identifier, noinline decoder: (PacketByteBuf) -> T, handler: ClientNetworkPacketHandler<T>) {
@@ -34,6 +34,8 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, RegistryKey<Regi
val PC_DROP = this.create("pc.drop")
@JvmField
val PC_RELEASE = this.create("pc.release")
@JvmField
val PC_CLICK = this.create("pc.click")
@JvmField
val HEALING_MACHINE_ACTIVE = this.create("healing_machine.active")
@@ -68,7 +70,13 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, RegistryKey<Regi
val PVW_BATTLE = this.create("battle.pvw.default")
@JvmField
val MINT_USE = this.create("mint.use")
val MEDICINE_HERB_USE = this.create("medicine_herb.use")
@JvmField
val MEDICINE_LIQUID_USE = this.create("medicine_liquid.use")
@JvmField
val MEDICINE_PILLS_USE = this.create("medicine_pills.use")
@JvmField
val MEDICINE_SPRAY_USE = this.create("medicine_spray.use")
private fun create(name: String): SoundEvent = this.create(name, SoundEvent.of(cobblemonResource(name)))
@@ -37,7 +37,7 @@ object Abilities : DataRegistry {
PotentialAbility.types.add(CommonAbilityType)
PotentialAbility.types.add(HiddenAbilityType)
this.abilityMap.clear()
val abilitiesJson = ShowdownService.get().getAbilityIds()
val abilitiesJson = ShowdownService.service.getAbilityIds()
for (i in 0 until abilitiesJson.size()) {
val id = abilitiesJson[i].asString
val ability = AbilityTemplate(id)
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import java.util.UUID
/**
* A class responsible for parsing a raw simulator protocol message received from Showdown.
@@ -25,7 +26,6 @@ import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
* @since December 31st, 2022
*/
class BattleMessage(rawMessage: String) {
/**
* The ID of the action received in the message.
*/
@@ -53,11 +53,6 @@ class BattleMessage(rawMessage: String) {
*/
private val optionalArgumentMatcher = Regex("^\\$OPTIONAL_ARG_START([^]]+)$OPTIONAL_ARG_END")
/**
* Pattern to match a Showdown position.
*/
private val pnxMatcher = Regex("p\\d[a-c]")
init {
this.parse(rawMessage)
}
@@ -122,6 +117,10 @@ class BattleMessage(rawMessage: String) {
return this
}
fun pokemonByUuid(index: Int, battle: PokemonBattle): BattlePokemon? {
return this.argumentAt(index)?.let { UUID.fromString(it) }?.let { uuid -> battle.actors.flatMap { it.pokemonList }.find { it.uuid == uuid } }
}
/**
* Queries an argument at the given [index] for a 'pnx' that will be parsed into a [BattleActor] and [ActiveBattlePokemon].
*
@@ -165,12 +164,14 @@ class BattleMessage(rawMessage: String) {
* @return A 'pnx' String representing position and a 'uuid' String representing the unique Pokemon if parsed correctly, otherwise null.
*/
fun pnxAndUuid(index: Int): Pair<String, String>? {
val argument = this.argumentAt(index)?.takeIf { it.length >= 3 }?.split(":")?.takeIf { it.size == 2 } ?: return null
val pnx = argument[0].takeIf { it.matches(pnxMatcher) } ?: return null
val argument = this.argumentAt(index)?.takeIf { it.length >= 2 }?.split(":")?.takeIf { it.size == 2 } ?: return null
val pnx = argument[0].takeIf { it.matches(PNX_MATCHER) } ?: return null
val uuid = argument[1].trim()
return pnx to uuid
}
/**
* Attempts to parse an [Effect] from an argument at the given [index].
*
@@ -238,6 +239,14 @@ class BattleMessage(rawMessage: String) {
private const val OPTIONAL_ARG_START = "["
private const val OPTIONAL_ARG_END = "]"
}
/**
* Pattern to match a Showdown position e.g. p2a, p1b
*/
val PNX_MATCHER = Regex("p\\d[a-c]")
/**
* Pattern to match a Showdown side position e.g. p2, p1
*/
val PN_MATCHER = Regex("p\\d")
}
}
@@ -52,6 +52,7 @@ interface Effect {
ABILITY("ability:"),
ITEM("item:"),
MOVE("move:"),
BAGITEM("bagitem:"),
PURE(""),
}
@@ -34,8 +34,8 @@ import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.WaitDispatch
import com.cobblemon.mod.common.battles.interpreter.ContextManager
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMusicPacket
@@ -49,6 +49,7 @@ import net.minecraft.text.Text
import java.io.File
import kotlin.io.path.Path
import kotlin.io.path.writeLines
import net.minecraft.server.network.ServerPlayerEntity
/**
* Individual battle instance
@@ -149,6 +150,11 @@ open class PokemonBattle(
return actors.find { actor -> actor.uuid == actorId }
}
/**
* Gets the first battle actor whom the given player controls, or null if there is no such actor.
*/
fun getActor(player: ServerPlayerEntity) = actors.firstOrNull { it.isForPlayer(player) }
/**
* Gets a BattleActor and an [ActiveBattlePokemon] from a pnx key, e.g. p2a
*
@@ -181,7 +187,7 @@ open class PokemonBattle(
fun writeShowdownAction(vararg messages: String) {
log(messages.joinToString("\n"))
ShowdownService.get().send(battleId, messages.toList().toTypedArray())
ShowdownService.service.send(battleId, messages.toList().toTypedArray())
}
fun turn(newTurnNumber: Int) {
@@ -443,4 +449,4 @@ open class PokemonBattle(
LOGGER.error("Missing interpretation on '{}' action: \nPublic » {}\nPrivate » {}", publicMessage.id, publicMessage.rawMessage, privateMessage.rawMessage)
return Text.literal("Missing interpretation on '${publicMessage.id}' action please report to the developers").red()
}
}
}
@@ -11,12 +11,13 @@ package com.cobblemon.mod.common.api.battles.model.actor
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.BallActionResponse
import com.cobblemon.mod.common.battles.ForcePassActionResponse
import com.cobblemon.mod.common.battles.ShowdownActionRequest
import com.cobblemon.mod.common.battles.ShowdownActionResponse
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.exception.IllegalActionChoiceException
import com.cobblemon.mod.common.net.messages.client.battle.BattleApplyPassResponsePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import java.util.UUID
@@ -40,7 +41,7 @@ abstract class BattleActor(
var request: ShowdownActionRequest? = null
var responses = mutableListOf<ShowdownActionResponse>()
var expectingCaptureActions = 0
val expectingPassActions = mutableListOf<ShowdownActionResponse>()
var mustChoose = false
/** For when battles start, it's the number of Pokémon that are still in the process of being sent out (animation wise) */
@@ -48,6 +49,16 @@ abstract class BattleActor(
abstract val type: ActorType
fun canFitForcedAction() = mustChoose && request?.let { request ->
val countMovable = (request.active?.count() ?: 0) - request.forceSwitch.count { it }
return@let countMovable > expectingPassActions.size && !battle.ended
} ?: false
fun forceChoose(response: ShowdownActionResponse) {
expectingPassActions.add(response)
sendUpdate(BattleApplyPassResponsePacket())
}
fun getSide() = if (this in battle.side1.actors) battle.side1 else battle.side2
open fun getPlayerUUIDs(): Iterable<UUID> = emptyList()
@@ -63,7 +74,7 @@ abstract class BattleActor(
val requestActive = request.active
if (requestActive == null || requestActive.isEmpty() || request.wait) {
this.request = null
expectingCaptureActions = 0
expectingPassActions.clear()
return
}
}
@@ -81,23 +92,24 @@ abstract class BattleActor(
fun setActionResponses(responses: List<ShowdownActionResponse>) {
val request = request ?: return
val originalCaptureActions = expectingCaptureActions
val originalPassActions = expectingPassActions.toList()
responses.forEachIndexed { index, response ->
val activeBattlePokemon = activePokemon.let { if (it.size > index) it[index] else return }
val showdownMoveSet = request.active?.let { if (it.size > index) it[index] else null }
val forceSwitch = request.forceSwitch.let { if (it.size > index) it[index] else false }
if (!response.isValid(activeBattlePokemon, showdownMoveSet, forceSwitch)) {
expectingCaptureActions = originalCaptureActions
expectingPassActions.clear()
expectingPassActions.addAll(originalPassActions)
throw IllegalActionChoiceException(this, "Invalid action choice for ${activeBattlePokemon.battlePokemon!!.getName().string}: $response")
} else if (response is BallActionResponse) {
expectingCaptureActions--
} else if (response is ForcePassActionResponse) {
this.responses.add(expectingPassActions.removeAt(0))
} else {
this.responses.add(response)
}
this.responses.add(response)
}
if (expectingCaptureActions > 0) {
if (expectingPassActions.size > 0) {
throw IllegalActionChoiceException(this, "Invalid action choice: a capture was expected. Are you hacking me?")
}
expectingCaptureActions = originalCaptureActions
mustChoose = false
battle.checkForInputDispatch()
}
@@ -113,7 +125,7 @@ abstract class BattleActor(
}
responses.clear()
request = null
expectingCaptureActions = 0
expectingPassActions.clear()
battle.writeShowdownAction(">$showdownId ${showdownMessages.joinToString()}")
}
@@ -0,0 +1,133 @@
/*
* 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.callback
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPacket
import com.cobblemon.mod.common.util.readSizedInt
import com.cobblemon.mod.common.util.writeSizedInt
import java.util.UUID
import net.minecraft.network.PacketByteBuf
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.Text
/**
* Used for opening move selection screens for players and handling their choice. Currently
* only supports up to 4 moves to select from.
*
* @author Hiroku
* @since July 1st, 2023
*/
object MoveSelectCallbacks {
val callbacks = mutableMapOf<UUID, MoveSelectCallback>()
@JvmOverloads
fun create(
player: ServerPlayerEntity,
title: Text = "".text(),
possibleMoves: List<MoveSelectDTO>,
cancel: (ServerPlayerEntity) -> Unit = {},
handler: (ServerPlayerEntity, index: Int, MoveSelectDTO) -> Unit
) {
val callback = MoveSelectCallback(
shownMoves = possibleMoves,
cancel = cancel,
handler = handler
)
callbacks[player.uuid] = callback
player.sendPacket(OpenMoveCallbackPacket(callback.uuid, title.copy(), possibleMoves))
}
@JvmOverloads
fun create(
player: ServerPlayerEntity,
moves: List<Move>,
canSelect: (Move) -> Boolean = { true },
cancel: (ServerPlayerEntity) -> Unit = {},
handler: (Move) -> Unit
) = create(
player = player,
possibleMoves = moves.map { battleMove -> MoveSelectDTO(battleMove).also { it.enabled = canSelect(battleMove) } },
cancel = cancel,
handler = { _, index, _ -> handler(moves[index]) }
)
@JvmOverloads
fun createBattleSelect(
player: ServerPlayerEntity,
moves: List<InBattleMove>,
canSelect: (InBattleMove) -> Boolean = { true },
cancel: (ServerPlayerEntity) -> Unit = {},
handler: (InBattleMove) -> Unit
) = create(
player = player,
possibleMoves = moves.map { battleMove -> MoveSelectDTO(battleMove).also { it.enabled = canSelect(battleMove) } },
cancel = cancel,
handler = { _, index, _ -> handler(moves[index]) }
)
fun handleCancelled(player: ServerPlayerEntity, uuid: UUID) {
val callback = callbacks[player.uuid] ?: return
if (callback.uuid != uuid) {
return
}
callbacks.remove(player.uuid)
callback.cancel(player)
}
fun handleCallback(player: ServerPlayerEntity, uuid: UUID, index: Int) {
val callback = callbacks[player.uuid] ?: return
callbacks.remove(player.uuid)
if (callback.uuid != uuid) {
Cobblemon.LOGGER.warn("A move select callback ran but with a mismatching UUID from ${player.gameProfile.name}. Hacking attempts?")
} else if (index >= callback.shownMoves.size) {
Cobblemon.LOGGER.warn("${player.gameProfile.name} used move select callback with an index that was out of bounds. Hacking attempts? Tried $index, possible size was ${callback.shownMoves.size}")
} else if (!callback.shownMoves[index].enabled) {
Cobblemon.LOGGER.warn("${player.gameProfile.name} used move select callback with a move that is not enabled. Hacking attempts?")
} else {
callback.handler(player, index, callback.shownMoves[index])
}
}
}
class MoveSelectCallback(
val uuid: UUID = UUID.randomUUID(),
val shownMoves: List<MoveSelectDTO>,
val cancel: (ServerPlayerEntity) -> Unit = {},
val handler: (ServerPlayerEntity, Int, MoveSelectDTO) -> Unit
)
class MoveSelectDTO(val moveTemplate: MoveTemplate, var enabled: Boolean, val pp: Int = -1, val ppMax: Int = -1) {
@JvmOverloads
constructor(move: Move, enabled: Boolean = true): this(moveTemplate = move.template, enabled = enabled, pp = move.currentPp, ppMax = move.maxPp)
@JvmOverloads
constructor(move: InBattleMove, enabled: Boolean = true): this(moveTemplate = Moves.getByNameOrDummy(move.move), enabled = enabled, pp = move.pp, ppMax = move.maxpp)
constructor(buffer: PacketByteBuf): this(
moveTemplate = Moves.getByNameOrDummy(buffer.readString()),
enabled = buffer.readBoolean(),
pp = buffer.readSizedInt(IntSize.BYTE),
ppMax = buffer.readSizedInt(IntSize.BYTE)
)
fun writeToBuffer(buffer: PacketByteBuf) {
buffer.writeString(moveTemplate.name)
buffer.writeBoolean(enabled)
buffer.writeSizedInt(IntSize.BYTE, pp)
buffer.writeSizedInt(IntSize.BYTE, ppMax)
}
}
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.callback
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallbackPacket
import com.cobblemon.mod.common.pokemon.Pokemon
import java.util.UUID
import net.minecraft.server.network.ServerPlayerEntity
/**
* Used for opening a party select screen for players which routes to move selection when they choose a Pokémon.
* You can do the same sort of thing with combined [MoveSelectCallbacks] and [PartySelectCallbacks] but it isn't
* as smooth an experience.
*
* @author Hiroku
* @since July 29th, 2023
*/
object PartyMoveSelectCallbacks {
val callbacks = mutableMapOf<UUID, PartyMoveSelectCallback>()
@JvmOverloads
fun create(
player: ServerPlayerEntity,
pokemon: List<Pair<PartySelectPokemonDTO, List<MoveSelectDTO>>>,
cancel: (ServerPlayerEntity) -> Unit = {},
handler: (ServerPlayerEntity, pokemonIndex: Int, PartySelectPokemonDTO, moveIndex: Int, MoveSelectDTO) -> Unit
) {
val callback = PartyMoveSelectCallback(
pokemon = pokemon,
cancel = cancel,
handler = handler
)
callbacks[player.uuid] = callback
player.sendPacket(OpenPartyMoveCallbackPacket(callback.uuid, callback.pokemon))
}
@JvmOverloads
fun createFromPokemon(
player: ServerPlayerEntity,
pokemon: List<Pokemon>,
moves: (Pokemon) -> List<Move> = { it.moveSet.getMoves() },
canSelectPokemon: (Pokemon) -> Boolean = { true },
canSelectMove: (Pokemon, Move) -> Boolean = { _, _ -> true },
cancel: (ServerPlayerEntity) -> Unit = {},
handler: (Pokemon, Move) -> Unit
) {
val pokemonList = mutableListOf<Pair<PartySelectPokemonDTO, List<MoveSelectDTO>>>()
for (pk in pokemon) {
val enabled = canSelectPokemon(pk)
val moveList = moves(pk).map { MoveSelectDTO(it, enabled = canSelectMove(pk, it)) }
pokemonList.add(PartySelectPokemonDTO(pk, enabled) to moveList)
}
create(
player = player,
pokemon = pokemonList,
cancel = cancel,
handler = { _, pkIndex, _, moveIndex, _ ->
handler(pokemon[pkIndex], pokemon[pkIndex].moveSet[moveIndex] ?: return@create)
}
)
}
fun handleCancelled(player: ServerPlayerEntity, uuid: UUID) {
val callback = callbacks[player.uuid] ?: return
if (callback.uuid != uuid) {
return
}
callbacks.remove(player.uuid)
callback.cancel(player)
}
fun handleCallback(player: ServerPlayerEntity, uuid: UUID, pokemonIndex: Int, moveIndex: Int) {
val callback = callbacks[player.uuid] ?: return
callbacks.remove(player.uuid)
if (callback.uuid != uuid) {
return Cobblemon.LOGGER.warn("A party move select callback ran but with a mismatching UUID from ${player.gameProfile.name}. Hacking attempts?")
} else if (pokemonIndex >= callback.pokemon.size) {
return Cobblemon.LOGGER.warn("${player.gameProfile.name} used party move select callback with a Pokémon index that was out of bounds. Hacking attempts? Tried $pokemonIndex, possible size was ${callback.pokemon.size}")
}
val (pokemon, moves) = callback.pokemon[pokemonIndex]
if (!pokemon.enabled) {
return Cobblemon.LOGGER.warn("${player.gameProfile.name} used party move select callback with a Pokémon that is not enabled. Hacking attempts?")
} else if (moveIndex >= moves.size) {
return Cobblemon.LOGGER.warn("${player.gameProfile.name} used party move select callback with a move index that was out of bounds. Hacking attempts? Tried $pokemonIndex-$moveIndex, possible size was ${moves.size}")
}
val move = moves[moveIndex]
if (!move.enabled) {
Cobblemon.LOGGER.warn("${player.gameProfile.name} used party move select callback with a move that is not enabled. Hacking attempts?")
} else {
callback.handler(player, pokemonIndex, pokemon, moveIndex, move)
}
}
}
class PartyMoveSelectCallback(
val uuid: UUID = UUID.randomUUID(),
val pokemon: List<Pair<PartySelectPokemonDTO, List<MoveSelectDTO>>>,
val cancel: (ServerPlayerEntity) -> Unit = {},
val handler: (ServerPlayerEntity, pokemonIndex: Int, PartySelectPokemonDTO, moveIndex: Int, MoveSelectDTO) -> Unit
)
@@ -0,0 +1,152 @@
/*
* 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.callback
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonPropertyExtractor
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
import com.cobblemon.mod.common.pokemon.Pokemon
import java.util.UUID
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.network.PacketByteBuf
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.Text
/**
* Used for opening party Pokémon selection screens for players and handling their choice. Currently
* only supports up to 6 Pokémon to select from.
*
* @author Hiroku
* @since July 1st, 2023
*/
object PartySelectCallbacks {
val callbacks = mutableMapOf<UUID, PartySelectCallback>()
@JvmOverloads
fun create(
player: ServerPlayerEntity,
title: Text = "".text(),
pokemon: List<PartySelectPokemonDTO>,
cancel: (ServerPlayerEntity) -> Unit = {},
handler: (ServerPlayerEntity, index: Int) -> Unit
) {
val callback = PartySelectCallback(
shownPokemon = pokemon,
cancel = cancel,
handler = handler
)
callbacks[player.uuid] = callback
player.sendPacket(
OpenPartyCallbackPacket(
uuid = callback.uuid,
title = title.copy(),
pokemon = pokemon
)
)
}
@JvmOverloads
fun createBattleSelect(
player: ServerPlayerEntity,
pokemon: List<BattlePokemon>,
canSelect: (BattlePokemon) -> Boolean,
cancel: (ServerPlayerEntity) -> Unit = {},
handler: (BattlePokemon) -> Unit
) = create(
player = player,
cancel = cancel,
pokemon = pokemon.map { pk -> PartySelectPokemonDTO(pk.effectedPokemon).also { it.enabled = canSelect(pk) } }
) { _, index -> handler(pokemon[index]) }
@JvmOverloads
fun createFromPokemon(
player: ServerPlayerEntity,
pokemon: List<Pokemon>,
canSelect: (Pokemon) -> Boolean,
cancel: (ServerPlayerEntity) -> Unit = {},
handler: (Pokemon) -> Unit
) = create(
player = player,
cancel = cancel,
pokemon = pokemon.map { pk -> PartySelectPokemonDTO(pk).also { it.enabled = canSelect(pk) } }
) { _, index -> handler(pokemon[index]) }
fun handleCancelled(player: ServerPlayerEntity, uuid: UUID) {
val callback = callbacks[player.uuid] ?: return
if (callback.uuid != uuid) {
return
}
callbacks.remove(player.uuid)
callback.cancel(player)
}
fun handleCallback(player: ServerPlayerEntity, uuid: UUID, index: Int) {
val callback = callbacks[player.uuid] ?: return
callbacks.remove(player.uuid)
if (callback.uuid != uuid) {
Cobblemon.LOGGER.warn("A party select callback ran but with a mismatching UUID from ${player.gameProfile.name}. Hacking attempts?")
} else if (index >= callback.shownPokemon.size) {
Cobblemon.LOGGER.warn("${player.gameProfile.name} used party select callback with an out of bounds index. Hacking attempts? Tried $index, Pokémon list size was ${callback.shownPokemon.size}")
} else if (!callback.shownPokemon[index].enabled) {
Cobblemon.LOGGER.warn("${player.gameProfile.name} used party select callback with a Pokémon that is not enabled. Hacking attempts?")
} else {
callback.handler(player, index)
}
}
}
class PartySelectCallback(
val uuid: UUID = UUID.randomUUID(),
val shownPokemon: List<PartySelectPokemonDTO>,
val cancel: (ServerPlayerEntity) -> Unit = {},
val handler: (ServerPlayerEntity, index: Int) -> Unit
)
open class PartySelectPokemonDTO(
val pokemonProperties: PokemonProperties,
val heldItem: ItemStack = ItemStack.EMPTY,
var hpRatio: Float = 1F,
var enabled: Boolean
) {
@JvmOverloads
constructor(pokemon: Pokemon, enabled: Boolean = true): this(
pokemonProperties = pokemon.createPokemonProperties(
PokemonPropertyExtractor.ASPECTS,
PokemonPropertyExtractor.SPECIES,
PokemonPropertyExtractor.LEVEL,
PokemonPropertyExtractor.NICKNAME,
PokemonPropertyExtractor.POKEBALL,
PokemonPropertyExtractor.STATUS
),
heldItem = pokemon.heldItemNoCopy(),
hpRatio = pokemon.currentHealth / pokemon.hp.toFloat(),
enabled = enabled
)
constructor(buffer: PacketByteBuf): this(
pokemonProperties = PokemonProperties().loadFromNBT(buffer.readNbt() as NbtCompound),
heldItem = buffer.readItemStack(),
hpRatio = buffer.readFloat(),
enabled = buffer.readBoolean()
)
fun writeToBuffer(buffer: PacketByteBuf) {
buffer.writeNbt(pokemonProperties.saveToNBT())
buffer.writeItemStack(heldItem)
buffer.writeFloat(hpRatio)
buffer.writeBoolean(enabled)
}
}
@@ -11,12 +11,13 @@ package com.cobblemon.mod.common.api.events.world
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.block.BigRootBlock
import com.cobblemon.mod.common.block.EnergyRootBlock
import net.minecraft.block.BlockState
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos
/**
* Event fired when a [BigRootBlock] attempts to spread. The event includes the original root position
* as well as the position of the new root. [isEnergyRoot] controls whether the new root will be an
* as well as the position of the new root. [resultingSpread] controls whether the new root will be an
* [EnergyRootBlock]. The new root position and whether it is an energy root is mutable.
*
* Cancelling this event will prevent propagation from occurring.
@@ -28,5 +29,5 @@ class BigRootPropagatedEvent(
val world: ServerWorld,
val pos: BlockPos,
var newRootPosition: BlockPos,
var isEnergyRoot: Boolean
var resultingSpread: BlockState
) : Cancelable()
@@ -0,0 +1,149 @@
/*
* 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.item
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.callback.MoveSelectCallbacks
import com.cobblemon.mod.common.api.callback.PartyMoveSelectCallbacks
import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.BagItemActionResponse
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.battle.BagItem
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.getBattleState
import com.cobblemon.mod.common.util.isHeld
import com.cobblemon.mod.common.util.isLookingAt
import com.cobblemon.mod.common.util.party
import net.minecraft.item.ItemStack
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.TypedActionResult
import net.minecraft.util.math.Box
/**
* Interface to make it easier to define items that will, upon use, prompt you to
* select a Pokémon and move for the item to be used on. This can be by interacting
* with the Pokémon directly (which will skip the Pokémon selection screen) or by
* interacting with air which will first show a Pokémon selection screen.
*
* @author Hiroku
* @since July 29th, 2023
*/
interface PokemonAndMoveSelectingItem {
fun use(player: ServerPlayerEntity, stack: ItemStack): TypedActionResult<ItemStack>? {
val entity = player.world
.getOtherEntities(player, Box.of(player.pos, 16.0, 16.0, 16.0))
.filter { player.isLookingAt(it, stepDistance = 0.1F) }
.minByOrNull { it.distanceTo(player) } as? PokemonEntity?
player.getBattleState()?.let { (_, actor) ->
if (bagItem == null) {
return TypedActionResult.fail(stack)
}
if (!actor.canFitForcedAction()) {
player.sendMessage(battleLang("bagitem.cannot").red())
return TypedActionResult.fail(stack)
}
val battlePokemon = actor.pokemonList.find { it.effectedPokemon == entity?.pokemon }
if (entity == null) {
return interactGeneralBattle(player, stack, actor)
} else if (battlePokemon != null) {
return interactWithSpecificBattle(player, stack, battlePokemon)
}
} ?: run {
if (entity != null) {
val pokemon = entity.pokemon
if (entity.ownerUuid == player.uuid) {
return interactWithSpecific(player, stack, pokemon)
} else {
return TypedActionResult.fail(stack)
}
} else {
return interactGeneral(player, stack)
}
}
return null
}
val bagItem: BagItem?
fun applyToPokemon(player: ServerPlayerEntity, stack: ItemStack, pokemon: Pokemon, move: Move)
fun applyToBattlePokemon(player: ServerPlayerEntity, stack: ItemStack, battlePokemon: BattlePokemon, move: Move) {
val battle = battlePokemon.actor.battle
val bagItem = bagItem
if (!battlePokemon.actor.canFitForcedAction()) {
player.sendMessage(battleLang("bagitem.cannot").red())
} else if (!bagItem!!.canUse(battle, battlePokemon)) {
player.sendMessage(battleLang("bagitem.invalid").red())
} else {
battlePokemon.actor.forceChoose(BagItemActionResponse(bagItem, battlePokemon, move.template.name))
if (!player.isCreative) {
stack.decrement(1)
}
}
}
fun canUseOnPokemon(pokemon: Pokemon): Boolean
fun canUseOnBattlePokemon(battlePokemon: BattlePokemon): Boolean = bagItem!!.canUse(battlePokemon.actor.battle, battlePokemon)
fun canUseOnMove(pokemon: Pokemon, move: Move): Boolean = canUseOnMove(move)
fun canUseOnMove(move: Move): Boolean
fun interactWithSpecific(player: ServerPlayerEntity, stack: ItemStack, pokemon: Pokemon): TypedActionResult<ItemStack>? {
MoveSelectCallbacks.create(
player = player,
moves = pokemon.moveSet.toList(),
canSelect = ::canUseOnMove,
handler = { move -> if (stack.isHeld(player)) applyToPokemon(player, stack, pokemon, move) }
)
return TypedActionResult.success(stack)
}
fun interactWithSpecificBattle(player: ServerPlayerEntity, stack: ItemStack, battlePokemon: BattlePokemon): TypedActionResult<ItemStack>? {
return if (canUseOnBattlePokemon(battlePokemon)) {
MoveSelectCallbacks.create(
player = player,
moves = battlePokemon.moveSet.getMoves(),
canSelect = ::canUseOnMove,
handler = { applyToBattlePokemon(player, stack, battlePokemon, it) }
)
TypedActionResult.success(stack)
} else {
player.sendMessage(battleLang("bagitem.invalid").red())
TypedActionResult.fail(stack)
}
}
fun interactGeneral(player: ServerPlayerEntity, stack: ItemStack): TypedActionResult<ItemStack>? {
PartyMoveSelectCallbacks.createFromPokemon(
player = player,
pokemon = player.party().toList(),
canSelectPokemon = ::canUseOnPokemon,
canSelectMove = ::canUseOnMove,
handler = { pk, mv -> if (stack.isHeld(player)) applyToPokemon(player, stack, pk, mv) }
)
return TypedActionResult.success(stack)
}
fun interactGeneralBattle(player: ServerPlayerEntity, stack: ItemStack, actor: BattleActor): TypedActionResult<ItemStack>? {
PartyMoveSelectCallbacks.createFromPokemon(
player = player,
pokemon = actor.pokemonList.map { it.effectedPokemon },
moves = { pk -> actor.pokemonList.find { it.effectedPokemon == pk }!!.moveSet.getMoves() },
canSelectPokemon = { pk -> canUseOnBattlePokemon(actor.pokemonList.find { it.effectedPokemon == pk }!!) },
canSelectMove = ::canUseOnMove,
handler = { pk, mv -> applyToBattlePokemon(player, stack, actor.pokemonList.find { it.effectedPokemon == pk }!!, mv) }
)
return TypedActionResult.success(stack)
}
}
@@ -0,0 +1,136 @@
/*
* 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.item
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.callback.PartySelectCallbacks
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.BagItemActionResponse
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.battle.BagItem
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.getBattleState
import com.cobblemon.mod.common.util.isHeld
import com.cobblemon.mod.common.util.isLookingAt
import com.cobblemon.mod.common.util.party
import net.minecraft.item.ItemStack
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.TypedActionResult
import net.minecraft.util.math.Box
/**
* Interface to make it easier to define items that will, upon use, prompt you to
* select a Pokémon for it to be used on. This can be by interacting with the Pokémon
* directly (which will skip the Pokémon selection screen) or by interacting with air
* which will first show a Pokémon selection screen.
*
* Works for battle items too.
*
* @author Hiroku
* @since July 29th, 2023
*/
interface PokemonSelectingItem {
fun use(player: ServerPlayerEntity, stack: ItemStack): TypedActionResult<ItemStack> {
val entity = player.world
.getOtherEntities(player, Box.of(player.pos, 16.0, 16.0, 16.0))
.filter { player.isLookingAt(it, stepDistance = 0.1F) }
.minByOrNull { it.distanceTo(player) } as? PokemonEntity?
player.getBattleState()?.let { (_, actor) ->
if (bagItem == null) return TypedActionResult.fail(stack)
val battlePokemon = actor.pokemonList.find { it.effectedPokemon == entity?.pokemon }
if (!actor.canFitForcedAction()) {
player.sendMessage(battleLang("bagitem.cannot").red())
return TypedActionResult.fail(stack)
}
if (entity == null) {
return interactGeneralBattle(player, stack, actor)
} else if (battlePokemon != null) {
return interactWithSpecificBattle(player, stack, battlePokemon)
}
} ?: run {
if (entity != null) {
val pokemon = entity.pokemon
if (entity.ownerUuid == player.uuid) {
return applyToPokemon(player, stack, pokemon) ?: TypedActionResult.pass(stack)
} else {
return TypedActionResult.fail(stack)
}
} else {
return interactGeneral(player, stack)
}
}
return TypedActionResult.pass(stack)
}
val bagItem: BagItem?
fun applyToPokemon(player: ServerPlayerEntity, stack: ItemStack, pokemon: Pokemon): TypedActionResult<ItemStack>?
fun applyToBattlePokemon(player: ServerPlayerEntity, stack: ItemStack, battlePokemon: BattlePokemon) {
val battle = battlePokemon.actor.battle
val bagItem = bagItem
if (!battlePokemon.actor.canFitForcedAction()) {
player.sendMessage(battleLang("bagitem.cannot").red())
} else if (!bagItem!!.canUse(battle, battlePokemon)) {
player.sendMessage(battleLang("bagitem.invalid").red())
} else {
battlePokemon.actor.forceChoose(BagItemActionResponse(bagItem, battlePokemon))
if (!player.isCreative) {
stack.decrement(1)
}
}
}
fun canUseOnPokemon(pokemon: Pokemon): Boolean
fun canUseOnBattlePokemon(battlePokemon: BattlePokemon): Boolean = bagItem!!.canUse(battlePokemon.actor.battle, battlePokemon)
fun interactWithSpecificBattle(player: ServerPlayerEntity, stack: ItemStack, battlePokemon: BattlePokemon): TypedActionResult<ItemStack> {
return if (canUseOnBattlePokemon(battlePokemon)) {
applyToBattlePokemon(player, stack, battlePokemon)
TypedActionResult.success(stack)
} else {
player.sendMessage(battleLang("bagitem.invalid").red())
TypedActionResult.fail(stack)
}
}
fun interactGeneral(player: ServerPlayerEntity, stack: ItemStack): TypedActionResult<ItemStack> {
PartySelectCallbacks.createFromPokemon(
player = player,
pokemon = player.party().toList(),
canSelect = ::canUseOnPokemon,
handler = { pk ->
if (stack.isHeld(player)) {
applyToPokemon(player, stack, pk)
if (!player.isCreative) {
stack.decrement(1)
}
}
}
)
return TypedActionResult.success(stack)
}
fun interactGeneralBattle(player: ServerPlayerEntity, stack: ItemStack, actor: BattleActor): TypedActionResult<ItemStack> {
PartySelectCallbacks.createBattleSelect(
player = player,
pokemon = actor.pokemonList,
canSelect = { pk -> canUseOnBattlePokemon(actor.pokemonList.find { it.effectedPokemon == pk.effectedPokemon }!!) },
handler = { pk -> applyToBattlePokemon(player, stack, actor.pokemonList.find { it.effectedPokemon == pk.effectedPokemon }!!) }
)
return TypedActionResult.success(stack)
}
}
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.readSizedInt
import com.cobblemon.mod.common.util.writeSizedInt
import com.google.gson.JsonObject
import kotlin.math.ceil
import kotlin.properties.Delegates
import net.minecraft.nbt.NbtCompound
import net.minecraft.network.PacketByteBuf
@@ -29,17 +30,31 @@ open class Move(
currentPp: Int,
raisedPpStages: Int = 0
) {
private var emit = true
val observable = SimpleObservable<Move>()
var currentPp: Int by Delegates.observable(currentPp) { _, oldValue, newValue ->
if (oldValue != newValue) {
observable.emit(this)
update()
}
}
var raisedPpStages: Int by Delegates.observable(raisedPpStages) { _, oldValue, newValue ->
if (oldValue != newValue) {
update()
}
}
fun doThenUpdate(action: () -> Unit) {
val oldEmit = emit
emit = false
action()
emit = oldEmit
update()
}
fun update() {
if (emit) {
observable.emit(this)
}
}
@@ -71,6 +86,20 @@ open class Move(
val maxPp: Int
get() = template.pp + raisedPpStages * template.pp / 5
/** Raises the max PP and rescales the current PP value. Returns false if it was already maxed out. */
fun raiseMaxPP(amount: Int): Boolean {
val oldPp = maxPp
doThenUpdate {
val ppRatio = currentPp / maxPp.toFloat()
raisedPpStages += amount
if (raisedPpStages > 3) {
raisedPpStages = 3
}
currentPp = ceil(ppRatio * maxPp).toInt()
}
return oldPp != maxPp
}
fun saveToNBT(nbt: NbtCompound): NbtCompound {
nbt.putString(DataKeys.POKEMON_MOVESET_MOVENAME, name)
nbt.putInt(DataKeys.POKEMON_MOVESET_MOVEPP, currentPp)
@@ -128,6 +128,7 @@ class MoveSet : Iterable<Move> {
observable.emit(this)
}
}
fun doWithoutEmitting(action: () -> Unit) {
val previousEmit = emit
emit = false
@@ -38,7 +38,7 @@ object Moves : DataRegistry {
override fun reload(manager: ResourceManager) {
this.allMoves.clear()
this.idMapping.clear()
val movesJson = ShowdownService.get().getMoves()
val movesJson = ShowdownService.service.getMoves()
for (i in 0 until movesJson.size()) {
val jsMove = movesJson[i].asJsonObject
val id = jsMove.get("id").asString
@@ -0,0 +1,45 @@
/*
* 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.mulch
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.item.MulchItem
/**
* Represents the different types of Mulch implemented in the mod.
*
*/
enum class MulchVariant {
COARSE,
GROWTH,
HUMID,
LOAMY,
PEAT,
RICH,
SANDY,
SURPRISE;
/**
* Resolves the item version of the Mulch.
*
* @return The [MulchItem] equivalent.
*/
fun item(): MulchItem = when (this) {
COARSE -> CobblemonItems.COARSE_MULCH
GROWTH -> CobblemonItems.GROWTH_MULCH
HUMID -> CobblemonItems.HUMID_MULCH
LOAMY -> CobblemonItems.LOAMY_MULCH
PEAT -> CobblemonItems.PEAT_MULCH
RICH -> CobblemonItems.RICH_MULCH
SANDY -> CobblemonItems.SANDY_MULCH
SURPRISE -> CobblemonItems.SURPRISE_MULCH
}
}
@@ -0,0 +1,45 @@
/*
* 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.mulch
import net.minecraft.block.BlockState
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.random.Random
/**
* Represents something that can have mulch applied to it.
* Typically, this will be a block implementation.
*/
interface Mulchable {
/**
* Checks if mulch can be applied to a specific spot in the world.
*
* @param world The affected [ServerWorld].
* @param pos The [BlockPos] being checked.
* @param state The [BlockState] of the block at the [pos] in the [world].
* @param variant The [MulchVariant] being applied.
* @return If the application is possible.
*/
fun canHaveMulchApplied(world: ServerWorld, pos: BlockPos, state: BlockState, variant: MulchVariant): Boolean
/**
* Applies the mulch to the specified spot.
* This is expected to be invoked if [canHaveMulchApplied] is true.
*
* @param world The affected [ServerWorld].
* @param random The [Random] instance being used in the interaction.
* @param pos The [BlockPos] being checked.
* @param state The [BlockState] of the block at the [pos] in the [world].
* @param variant The [MulchVariant] being applied.
*/
fun applyMulch(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState, variant: MulchVariant)
}
@@ -13,9 +13,12 @@ import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.aspect.AspectProvider
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.api.properties.CustomPokemonProperty
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.*
import com.cobblemon.mod.common.pokemon.status.PersistentStatus
import com.cobblemon.mod.common.pokemon.status.PersistentStatusContainer
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.isInt
@@ -81,12 +84,13 @@ open class PokemonProperties {
props.pokeball = parseIdentifierOfRegistry(keyPairs, listOf("pokeball")) { identifier -> PokeBalls.getPokeBall(identifier)?.name?.toString() }
props.nature = parseIdentifierOfRegistry(keyPairs, listOf("nature")) { identifier -> Natures.getNature(identifier)?.name?.toString() }
props.ability = parseStringOfRegistry(keyPairs, listOf("ability")) { Abilities.get(it)?.name }
props.status = parseStringOfRegistry(keyPairs, listOf("status")) { (Statuses.getStatus(it) ?: Statuses.getStatus(it.asIdentifierDefaultingNamespace()))?.showdownName }
props.nickname = parseText(keyPairs, listOf("nickname", "nick"))
val maybeIVs = IVs()
val maybeEVs = EVs()
Stats.PERMANENT.forEach{ stat ->
val statName = stat.displayName.string.lowercase()
val statName = stat.toString().lowercase()
parseIntProperty(keyPairs, listOf("${statName}_iv"))?.coerceIn(0, IVs.MAX_VALUE)?.let { maybeIVs[stat] = it }
parseIntProperty(keyPairs, listOf("${statName}_ev"))?.coerceIn(0, EVs.MAX_STAT_VALUE)?.let { maybeEVs[stat] = it }
}
@@ -241,6 +245,7 @@ open class PokemonProperties {
var nature: String? = null
var ability: String? = null
var aspects: Set<String> = emptySet()
var status: String? = null
var ivs: IVs? = null
var evs: EVs? = null
@@ -278,6 +283,7 @@ open class PokemonProperties {
pokeball?.let { PokeBalls.getPokeBall(it.asIdentifierDefaultingNamespace())?.let { pokeball -> pokemon.caughtBall = pokeball } }
nature?.let { Natures.getNature(it.asIdentifierDefaultingNamespace())?.let { nature -> pokemon.nature = nature } }
ability?.let { Abilities.getOrException(it).create(true).let { ability -> pokemon.ability = ability } }
status?.let { Statuses.getStatus(it)?.let { pokemon.applyStatus(it as PersistentStatus) } }
customProperties.forEach { it.apply(pokemon) }
ivs?.let { ivs ->
ivs.forEach { stat ->
@@ -312,6 +318,7 @@ open class PokemonProperties {
pokeball?.let { PokeBalls.getPokeBall(it.asIdentifierDefaultingNamespace())?.let { pokeball -> pokemonEntity.pokemon.caughtBall = pokeball } }
nature?.let { Natures.getNature(it.asIdentifierDefaultingNamespace())?.let { nature -> pokemonEntity.pokemon.nature = nature } }
ability?.let { Abilities.getOrException(it).create(true).let { ability -> pokemonEntity.pokemon.ability = ability } }
status?.let { Statuses.getStatus(it)?.let { pokemonEntity.pokemon.applyStatus(it as PersistentStatus) } }
customProperties.forEach { it.apply(pokemonEntity) }
ivs?.let { ivs ->
ivs.forEach { stat ->
@@ -348,7 +355,8 @@ open class PokemonProperties {
friendship?.takeIf { it != pokemon.friendship }?.let { return false }
pokeball?.takeIf { it != pokemon.caughtBall.name.toString() }?.let { return false }
nature?.takeIf { it != pokemon.nature.name.toString() }?.let { return false }
ability?.takeIf { it != pokemon.ability.name }?. let { return false }
ability?.takeIf { it != pokemon.ability.name }?.let { return false }
status?.takeIf { it != pokemon.status?.status?.showdownName }?.let { return false }
ivs?.forEach{ stat ->
if (stat.value != pokemon.ivs[stat.key]) { return false }
}
@@ -379,7 +387,8 @@ open class PokemonProperties {
friendship?.takeIf { it != pokemonEntity.pokemon.friendship }?.let { return false }
pokeball?.takeIf { it != pokemonEntity.pokemon.caughtBall.name.toString() }?.let { return false }
nature?.takeIf { it != pokemonEntity.pokemon.nature.name.toString() }?.let { return false }
ability?.takeIf { it != pokemonEntity.pokemon.ability.name }?. let { return false }
ability?.takeIf { it != pokemonEntity.pokemon.ability.name }?.let { return false }
status?.takeIf { it != pokemonEntity.pokemon.status?.status?.showdownName }?.let { return false }
ivs?.forEach{ stat ->
if (stat.value != pokemonEntity.pokemon.ivs[stat.key]) { return false }
}
@@ -410,7 +419,8 @@ open class PokemonProperties {
friendship?.takeIf { it != properties.friendship }?.let { return false }
pokeball?.takeIf { it != properties.pokeball }?.let { return false }
nature?.takeIf { it != properties.nature }?.let { return false }
ability?.takeIf { it != properties.ability }?. let { return false }
ability?.takeIf { it != properties.ability }?.let { return false }
status?.takeIf { it != properties.status }?.let { return false }
ivs?.let{ ivs ->
ivs.forEach{ stat ->
//If the potential subset has IV and the main set does not then it cant be a subset
@@ -460,6 +470,7 @@ open class PokemonProperties {
pokeball?.let { nbt.putString(DataKeys.POKEMON_CAUGHT_BALL, it) }
nature?.let { nbt.putString(DataKeys.POKEMON_NATURE, it) }
ability?.let { nbt.putString(DataKeys.POKEMON_ABILITY, it) }
status?.let { nbt.putString(DataKeys.POKEMON_STATUS_NAME, it) }
ivs?.let { nbt.put(DataKeys.POKEMON_IVS, it.saveToNBT(NbtCompound())) }
evs?.let { nbt.put(DataKeys.POKEMON_EVS, it.saveToNBT(NbtCompound())) }
val custom = NbtList()
@@ -480,6 +491,7 @@ open class PokemonProperties {
pokeball = if (tag.contains(DataKeys.POKEMON_CAUGHT_BALL)) tag.getString(DataKeys.POKEMON_CAUGHT_BALL) else null
nature = if (tag.contains(DataKeys.POKEMON_NATURE)) tag.getString(DataKeys.POKEMON_NATURE) else null
ability = if (tag.contains(DataKeys.POKEMON_ABILITY)) tag.getString(DataKeys.POKEMON_ABILITY) else null
status = if (tag.contains(DataKeys.POKEMON_STATUS_NAME)) tag.getString(DataKeys.POKEMON_STATUS_NAME) else null
ivs = if (tag.contains(DataKeys.POKEMON_IVS)) ivs?.loadFromNBT(tag.getCompound(DataKeys.POKEMON_IVS)) as IVs? else null
evs = if (tag.contains(DataKeys.POKEMON_EVS)) evs?.loadFromNBT(tag.getCompound(DataKeys.POKEMON_EVS)) as EVs? else null
val custom = tag.getList(DataKeys.POKEMON_PROPERTIES_CUSTOM, NbtElement.STRING_TYPE.toInt())
@@ -502,6 +514,7 @@ open class PokemonProperties {
pokeball?.let { json.addProperty(DataKeys.POKEMON_CAUGHT_BALL, it) }
nature?.let { json.addProperty(DataKeys.POKEMON_NATURE, it) }
ability?.let { json.addProperty(DataKeys.POKEMON_ABILITY, it) }
status?.let { json.addProperty(DataKeys.POKEMON_STATUS_NAME, it) }
ivs?.let { json.add(DataKeys.POKEMON_IVS, it.saveToJSON(JsonObject())) }
evs?.let { json.add(DataKeys.POKEMON_EVS, it.saveToJSON(JsonObject())) }
val custom = JsonArray()
@@ -523,6 +536,7 @@ open class PokemonProperties {
pokeball = json.get(DataKeys.POKEMON_CAUGHT_BALL)?.asString
nature = json.get(DataKeys.POKEMON_NATURE)?.asString
ability = json.get(DataKeys.POKEMON_ABILITY)?.asString
status = json.get(DataKeys.POKEMON_STATUS_NAME)?.asString
ivs?.loadFromJSON(json.getAsJsonObject(DataKeys.POKEMON_IVS))
evs?.loadFromJSON(json.getAsJsonObject(DataKeys.POKEMON_EVS))
val custom = json.get(DataKeys.POKEMON_PROPERTIES_CUSTOM)?.asJsonArray
@@ -544,6 +558,7 @@ open class PokemonProperties {
pokeball?.let { pieces.add("pokeball=$it") }
nature?.let { pieces.add("nature=$it") }
ability?.let { pieces.add("ability=$it") }
status?.let { pieces.add("status=$it") }
ivs?.forEach{ stat ->
pieces.add("${stat.key}_iv=${stat.value}")
}
@@ -21,28 +21,37 @@ import com.cobblemon.mod.common.pokemon.Pokemon
fun interface PokemonPropertyExtractor {
companion object {
@JvmField
val SPECIES = PokemonPropertyExtractor { pokemon, properties -> properties.species = pokemon.species.resourceIdentifier.toString() }
@JvmField
val FORM = PokemonPropertyExtractor { pokemon, properties -> properties.form = pokemon.form.formOnlyShowdownId() }
@JvmField
val SHINY = PokemonPropertyExtractor { pokemon, properties -> properties.shiny = pokemon.shiny }
@JvmField
val ASPECTS = PokemonPropertyExtractor { pokemon, properties -> properties.aspects = pokemon.aspects }
@JvmField
val LEVEL = PokemonPropertyExtractor { pokemon, properties -> properties.level = pokemon.level }
@JvmField
val GENDER = PokemonPropertyExtractor { pokemon, properties -> properties.gender = pokemon.gender }
@JvmField
val FRIENDSHIP = PokemonPropertyExtractor { pokemon, properties -> properties.friendship = pokemon.friendship }
@JvmField
val POKEBALL = PokemonPropertyExtractor { pokemon, properties -> properties.pokeball = pokemon.caughtBall.name.toString() }
@JvmField
val NATURE = PokemonPropertyExtractor { pokemon, properties -> properties.nature = pokemon.nature.name.toString() }
@JvmField
val ABILITY = PokemonPropertyExtractor { pokemon, properties -> properties.ability = pokemon.ability.name }
val ALL = mutableListOf<PokemonPropertyExtractor>()
@JvmField
val ALL = arrayOf(SPECIES, FORM, SHINY, ASPECTS, LEVEL, GENDER, FRIENDSHIP, POKEBALL, NATURE, ABILITY)
val SPECIES = add { pokemon, properties -> properties.species = pokemon.species.resourceIdentifier.toString() }
@JvmField
val FORM = add { pokemon, properties -> properties.form = pokemon.form.formOnlyShowdownId() }
@JvmField
val SHINY = add { pokemon, properties -> properties.shiny = pokemon.shiny }
@JvmField
val ASPECTS = add { pokemon, properties -> properties.aspects = pokemon.aspects }
@JvmField
val LEVEL = add { pokemon, properties -> properties.level = pokemon.level }
@JvmField
val GENDER = add { pokemon, properties -> properties.gender = pokemon.gender }
@JvmField
val FRIENDSHIP = add { pokemon, properties -> properties.friendship = pokemon.friendship }
@JvmField
val POKEBALL = add { pokemon, properties -> properties.pokeball = pokemon.caughtBall.name.toString() }
@JvmField
val NATURE = add { pokemon, properties -> properties.nature = pokemon.nature.name.toString() }
@JvmField
val ABILITY = add { pokemon, properties -> properties.ability = pokemon.ability.name }
@JvmField
val NICKNAME = add { pokemon, properties -> properties.nickname = pokemon.nickname }
@JvmField
val STATUS = add { pokemon, properties -> properties.status = pokemon.status?.status?.showdownName }
fun add(extractor: PokemonPropertyExtractor): PokemonPropertyExtractor {
ALL.add(extractor)
return extractor
}
}
operator fun invoke(pokemon: Pokemon, properties: PokemonProperties)
@@ -115,12 +115,14 @@ object PokemonSpecies : JsonDataRegistry<Species> {
SpeciesAdditions.observable.subscribe {
this.species.forEach(Species::initialize)
this.species.forEach(Species::resolveEvolutionMoves)
Cobblemon.showdownThread.queue { ShowdownService.get().registerSpecies() }
// Reload this with the mod
CobblemonEmptyHeldItemManager.load()
CobblemonHeldItemManager.load()
Cobblemon.LOGGER.info("Loaded {} Pokémon species", this.speciesByIdentifier.size)
this.observable.emit(this)
Cobblemon.showdownThread.queue {
it.registerSpecies()
it.indicateSpeciesInitialized()
// Reload this with the mod
CobblemonHeldItemManager.load()
Cobblemon.LOGGER.info("Loaded {} Pokémon species", this.speciesByIdentifier.size)
this.observable.emit(this)
}
}
}
@@ -37,6 +37,11 @@ interface Stat {
*/
val type: Type
/**
* The name used by Showdown to store the stats (attack is 'atk', for example)
*/
val showdownId: String
/**
* Represents the type of this stat.
*/
@@ -18,16 +18,16 @@ import net.minecraft.util.Identifier
* An enumeration of the default implemented [Stat]s.
* Contains all the traditional stats in official Pokémon games.
*/
enum class Stats(override val identifier: Identifier, override val displayName: Text, override val type: Stat.Type) : Stat {
enum class Stats(override val identifier: Identifier, override val displayName: Text, override val type: Stat.Type, override val showdownId: String) : Stat {
HP(cobblemonResource("hp"), lang("stat.hp.name"), Stat.Type.PERMANENT),
ATTACK(cobblemonResource("attack"), lang("stat.attack.name"), Stat.Type.PERMANENT),
DEFENCE(cobblemonResource("defence"), lang("stat.defence.name"), Stat.Type.PERMANENT),
SPECIAL_ATTACK(cobblemonResource("special_attack"), lang("stat.special_attack.name"), Stat.Type.PERMANENT),
SPECIAL_DEFENCE(cobblemonResource("special_defence"), lang("stat.special_defence.name"), Stat.Type.PERMANENT),
SPEED(cobblemonResource("speed"), lang("stat.speed.name"), Stat.Type.PERMANENT),
EVASION(cobblemonResource("evasion"), lang("stat.evasion.name"), Stat.Type.BATTLE_ONLY),
ACCURACY(cobblemonResource("accuracy"), lang("stat.accuracy.name"), Stat.Type.BATTLE_ONLY);
HP(cobblemonResource("hp"), lang("stat.hp.name"), Stat.Type.PERMANENT, "hp"),
ATTACK(cobblemonResource("attack"), lang("stat.attack.name"), Stat.Type.PERMANENT, "atk"),
DEFENCE(cobblemonResource("defence"), lang("stat.defence.name"), Stat.Type.PERMANENT, "def"),
SPECIAL_ATTACK(cobblemonResource("special_attack"), lang("stat.special_attack.name"), Stat.Type.PERMANENT, "spa"),
SPECIAL_DEFENCE(cobblemonResource("special_defence"), lang("stat.special_defence.name"), Stat.Type.PERMANENT, "spd"),
SPEED(cobblemonResource("speed"), lang("stat.speed.name"), Stat.Type.PERMANENT, "spe"),
EVASION(cobblemonResource("evasion"), lang("stat.evasion.name"), Stat.Type.BATTLE_ONLY, "evasion"),
ACCURACY(cobblemonResource("accuracy"), lang("stat.accuracy.name"), Stat.Type.BATTLE_ONLY, "accuracy");
companion object {
@@ -14,9 +14,9 @@ import com.cobblemon.mod.common.api.spawning.mixins.CachedOnlyChunkAccessor
import com.cobblemon.mod.common.api.spawning.prospecting.SpawningProspector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningArea
import com.cobblemon.mod.common.api.tags.CobblemonBlockTags
import net.minecraft.block.Blocks
import net.minecraft.entity.LivingEntity
import net.minecraft.registry.tag.BlockTags
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Box
import net.minecraft.util.math.ChunkSectionPos.getSectionCoord
@@ -97,8 +97,7 @@ object CobblemonSpawningProspector : SpawningProspector {
skyLevel[x - area.baseX][z - area.baseZ] = y
}
// TODO don't just check solid, have some property somewhere modifiable that excludes some blocks from occluding
if (state.isSolid && !state.isIn(BlockTags.LEAVES)) {
if (state.fluidState.isEmpty && !state.isIn(CobblemonBlockTags.SEES_SKY)) {
canSeeSky = false
}
}
@@ -22,8 +22,6 @@ import net.minecraft.block.Block
* @since February 7th, 2022
*/
abstract class AreaTypeSpawningCondition<T : AreaSpawningContext> : SpawningCondition<T>() {
var minWidth: Int? = null
var maxWidth: Int? = null
var minHeight: Int? = null
var maxHeight: Int? = null
var neededNearbyBlocks: MutableList<RegistryLikeCondition<Block>>? = null
@@ -31,10 +29,6 @@ abstract class AreaTypeSpawningCondition<T : AreaSpawningContext> : SpawningCond
override fun fits(ctx: T, detail: SpawnDetail): Boolean {
if (!super.fits(ctx, detail)) {
return false
} else if (minWidth != null && ctx.width < minWidth!!) {
return false
} else if (maxWidth != null && ctx.width > maxWidth!!) {
return false
} else if (minHeight != null && ctx.height < minHeight!!) {
return false
} else if (maxHeight != null && ctx.height > maxHeight!!) {
@@ -49,8 +43,6 @@ abstract class AreaTypeSpawningCondition<T : AreaSpawningContext> : SpawningCond
override fun copyFrom(other: SpawningCondition<*>, merger: Merger) {
super.copyFrom(other, merger)
if (other is AreaTypeSpawningCondition) {
merger.mergeSingle(minWidth, other.minWidth)
merger.mergeSingle(maxWidth, other.maxWidth)
merger.mergeSingle(minHeight, other.minHeight)
merger.mergeSingle(maxHeight, other.maxHeight)
neededNearbyBlocks = merger.merge(neededNearbyBlocks, other.neededNearbyBlocks)?.toMutableList()
@@ -12,9 +12,7 @@ import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
import com.cobblemon.mod.common.api.spawning.context.GroundedSpawningContext
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.util.Merger
import com.cobblemon.mod.common.util.asResource
import net.minecraft.block.Block
import net.minecraft.util.Identifier
/**
* Base type for a spawning condition that applies to some kind of [GroundedSpawningContext]. This
@@ -29,6 +27,10 @@ abstract class GroundedTypeSpawningCondition<T : GroundedSpawningContext> : Area
override fun fits(ctx: T, detail: SpawnDetail): Boolean {
return if (!super.fits(ctx, detail)) {
false
} else if (minHeight != null && ctx.height < minHeight!!) {
return false
} else if (maxHeight != null && ctx.height > maxHeight!!) {
return false
} else !(neededBaseBlocks != null && neededBaseBlocks!!.none { it.fits(ctx.baseBlock.block, ctx.blockRegistry) })
}
@@ -30,6 +30,10 @@ abstract class SubmergedTypeSpawningCondition<T : SubmergedSpawningContext> : Ar
override fun fits(ctx: T, detail: SpawnDetail): Boolean {
return if (!super.fits(ctx, detail)) {
false
} else if (minHeight != null && ctx.height < minHeight!!) {
return false
} else if (maxHeight != null && ctx.height > maxHeight!!) {
return false
} else if (minDepth != null && ctx.depth < minDepth!!) {
false
} else if (maxDepth != null && ctx.depth > maxDepth!!) {
@@ -10,13 +10,14 @@ package com.cobblemon.mod.common.api.spawning.context
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.WorldSlice
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import kotlin.math.ceil
import kotlin.math.floor
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.ChunkPos
import net.minecraft.world.World
/**
* A [SpawningContext] that is for a particular area, and therefore has spatial properties.
@@ -31,8 +32,6 @@ open class AreaSpawningContext(
override val light: Int,
override val canSeeSky: Boolean,
override val influences: MutableList<SpawningInfluence>,
/** Space horizontally (diameter) */
val width: Int,
/** Space above. */
val height: Int,
val nearbyBlocks: List<BlockState>,
@@ -43,4 +42,44 @@ open class AreaSpawningContext(
override fun getStructureCache(pos: BlockPos): StructureChunkCache {
return slice.getStructureCache(pos)
}
/**
* Returns true if the given block can be occupied by a spawn in this context.
* This is not considering the provided state as what the entity would be on top of,
* but rather the space its hitbox would fill.
*/
open fun isSafeSpace(world: ServerWorld, pos: BlockPos, state: BlockState): Boolean = !state.isFullCube(world, pos)
override fun postFilter(detail: SpawnDetail): Boolean {
if (!super.postFilter(detail)) {
return false
}
if (detail.width > 1 || detail.height > 1) {
val sizeX = detail.width.takeIf { it > 0 } ?: 1
val sizeY = detail.height.takeIf { it > 0 } ?: 1
val minX = floor(position.x + 0.5 - (sizeX - 1) / 2F).toInt() - 1
val maxX = ceil(position.x + 0.5 + (sizeX + 1) / 2F).toInt() + 1
val maxY = ceil(position.y + (sizeY + 1) / 2F).toInt() + 1
val minZ = floor(position.z + 0.5 - (sizeX - 1) / 2F).toInt() - 1
val maxZ = ceil(position.z + 0.5 + (sizeX + 1) / 2F).toInt() + 1
val mutable = BlockPos.Mutable()
for (x in minX until maxX) {
for (y in (position.y + 1)..maxY) {
for (z in minZ until maxZ) {
val state = world.getBlockState(mutable.set(x, y, z))
if (!isSafeSpace(world, mutable, state)) {
return false
}
}
}
}
}
return true
}
}
@@ -12,9 +12,9 @@ import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.WorldSlice
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import net.minecraft.block.BlockState
import net.minecraft.registry.tag.FluidTags
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
/**
* A type of area based spawning context with a floor.
@@ -29,11 +29,10 @@ abstract class FlooredSpawningContext(
light: Int,
canSeeSky: Boolean,
influences: MutableList<SpawningInfluence>,
width: Int,
height: Int,
nearbyBlocks: List<BlockState>,
slice: WorldSlice
) : AreaSpawningContext(cause, world, position, light, canSeeSky, influences, width, height, nearbyBlocks, slice) {
) : AreaSpawningContext(cause, world, position, light, canSeeSky, influences, height, nearbyBlocks, slice) {
/** The block that the spawning is occurring on. */
val baseBlock = slice.getBlockState(position.x, position.y, position.z)
}
@@ -51,11 +50,10 @@ open class GroundedSpawningContext(
light: Int,
canSeeSky: Boolean,
influences: MutableList<SpawningInfluence>,
width: Int,
height: Int,
nearbyBlocks: List<BlockState>,
slice: WorldSlice
) : FlooredSpawningContext(cause, world, position, light, canSeeSky, influences, width, height, nearbyBlocks, slice)
) : FlooredSpawningContext(cause, world, position, light, canSeeSky, influences, height, nearbyBlocks, slice)
/**
* A spawning context that occurs at the bottom of a body of water.
@@ -70,11 +68,12 @@ open class SeafloorSpawningContext(
light: Int,
canSeeSky: Boolean,
influences: MutableList<SpawningInfluence>,
width: Int,
height: Int,
nearbyBlocks: List<BlockState>,
slice: WorldSlice
) : FlooredSpawningContext(cause, world, position, light, canSeeSky, influences, width, height, nearbyBlocks, slice)
) : FlooredSpawningContext(cause, world, position, light, canSeeSky, influences, height, nearbyBlocks, slice) {
override fun isSafeSpace(world: ServerWorld, pos: BlockPos, state: BlockState) = state.fluidState.isIn(FluidTags.WATER)
}
/**
* A spawning context that occurs at the bottom of bodies of lava.
@@ -89,11 +88,12 @@ open class LavafloorSpawningContext(
light: Int,
canSeeSky: Boolean,
influences: MutableList<SpawningInfluence>,
width: Int,
height: Int,
nearbyBlocks: List<BlockState>,
slice: WorldSlice
) : FlooredSpawningContext(cause, world, position, light, canSeeSky, influences, width, height, nearbyBlocks, slice)
) : FlooredSpawningContext(cause, world, position, light, canSeeSky, influences, height, nearbyBlocks, slice) {
override fun isSafeSpace(world: ServerWorld, pos: BlockPos, state: BlockState) = state.fluidState.isIn(FluidTags.LAVA)
}
open class SurfaceSpawningContext(
cause: SpawnCause,
@@ -102,9 +102,8 @@ open class SurfaceSpawningContext(
light: Int,
canSeeSky: Boolean,
influences: MutableList<SpawningInfluence>,
width: Int,
height: Int,
val depth: Int,
nearbyBlocks: List<BlockState>,
slice: WorldSlice
) : FlooredSpawningContext(cause, world, position, light, canSeeSky, influences, width, height, nearbyBlocks, slice)
) : FlooredSpawningContext(cause, world, position, light, canSeeSky, influences, height, nearbyBlocks, slice)
@@ -24,7 +24,6 @@ import net.minecraft.server.world.ServerWorld
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.ChunkPos
import net.minecraft.world.World
import net.minecraft.world.biome.Biome
import net.minecraft.world.gen.StructureAccessor
import net.minecraft.world.gen.structure.Structure
@@ -142,6 +141,13 @@ abstract class SpawningContext {
/** Returns true if none of the influences.affectSpawnable return false */
return influences.none { !it.affectSpawnable(detail, this) }
}
/**
* Filters a spawning detail by some extra, more expensive condition defined by the context itself.
* @return true if the [SpawnDetail] is acceptable by the context's own logic.
*/
open fun postFilter(detail: SpawnDetail): Boolean = true
open fun afterSpawn(entity: Entity) {
influences.forEach { it.affectSpawn(entity) }
}
@@ -14,7 +14,6 @@ import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import net.minecraft.block.BlockState
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
/**
* A type of area based spawning context with a fluid base block.
@@ -29,11 +28,12 @@ open class SubmergedSpawningContext(
light: Int,
canSeeSky: Boolean,
influences: MutableList<SpawningInfluence>,
width: Int,
height: Int,
val depth: Int,
nearbyBlocks: List<BlockState>,
slice: WorldSlice
) : AreaSpawningContext(cause, world, position, light, canSeeSky, influences, width, height, nearbyBlocks, slice) {
) : AreaSpawningContext(cause, world, position, light, canSeeSky, influences, height, nearbyBlocks, slice) {
val fluid = slice.getBlockState(position.x, position.y, position.z).fluidState
override fun isSafeSpace(world: ServerWorld, pos: BlockPos, state: BlockState) = state.fluidState.fluid == fluid.fluid
}
@@ -61,7 +61,6 @@ object GroundedSpawningContextCalculator : FlooredSpawningContextCalculator<Grou
light = getLight(input),
canSeeSky = getCanSeeSky(input),
influences = input.spawner.copyInfluences(),
width = getHorizontalSpace(input, surroundingCondition, config.maxHorizontalSpace, offsetY = 1),
height = getHeight(input, surroundingCondition, config.maxVerticalSpace, offsetY = 1),
slice = input.slice,
nearbyBlocks = getNearbyBlocks(input)
@@ -88,7 +87,6 @@ object SeafloorSpawningContextCalculator : FlooredSpawningContextCalculator<Seaf
light = getLight(input),
canSeeSky = getCanSeeSky(input),
influences = input.spawner.copyInfluences(),
width = getHorizontalSpace(input, surroundingCondition, config.maxHorizontalSpace, offsetY = 1),
height = getHeight(input, surroundingCondition, config.maxVerticalSpace, offsetY = 1),
slice = input.slice,
nearbyBlocks = getNearbyBlocks(input)
@@ -115,7 +113,6 @@ object LavafloorSpawningContextCalculator : FlooredSpawningContextCalculator<Lav
light = getLight(input),
canSeeSky = getCanSeeSky(input),
influences = input.spawner.copyInfluences(),
width = getHorizontalSpace(input, surroundingCondition, config.maxHorizontalSpace, offsetY = 1),
height = getHeight(input, surroundingCondition, config.maxVerticalSpace, offsetY = 1),
slice = input.slice,
nearbyBlocks = getNearbyBlocks(input)
@@ -143,7 +140,6 @@ object SurfaceSpawningContextCalculator : FlooredSpawningContextCalculator<Surfa
light = getLight(input),
canSeeSky = getCanSeeSky(input),
influences = input.spawner.copyInfluences(),
width = getHorizontalSpace(input, surroundingCondition, config.maxHorizontalSpace, offsetY = 1),
height = getHeight(input, surroundingCondition, config.maxVerticalSpace / 2, offsetY = 1),
depth = getDepth(input, baseCondition, config.maxVerticalSpace / 2),
slice = input.slice,
@@ -47,7 +47,6 @@ object SubmergedSpawningContextCalculator : AreaSpawningContextCalculator<Submer
light = getLight(input),
canSeeSky = getCanSeeSky(input),
influences = input.spawner.copyInfluences(),
width = getHorizontalSpace(input, fluidCondition, config.maxHorizontalSpace),
height = getHeight(input, fluidCondition, ceil(config.maxVerticalSpace / 2F)),
depth = getDepth(input, fluidCondition, ceil(config.maxVerticalSpace / 2F)),
slice = input.slice,
@@ -15,9 +15,11 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.lang
import com.google.gson.annotations.SerializedName
import kotlin.math.ceil
import net.minecraft.text.MutableText
/**
@@ -38,6 +40,10 @@ class PokemonSpawnDetail : SpawnDetail() {
val drops: DropTable? = null
val heldItems: MutableList<PossibleHeldItem>? = null
private val pokemonExample: Pokemon by lazy { pokemon.create() }
// Calculate the size based off the hitbox unless it's been explicitly set
/* todo breadcrumbing, ai */
@@ -68,6 +74,14 @@ class PokemonSpawnDetail : SpawnDetail() {
species.secondaryType?.let { listOf(species.primaryType.name.lowercase(), it.name.lowercase()) }
?: listOf(species.primaryType.name.lowercase())
)
if (height == -1) {
height = ceil(pokemonExample.form.hitbox.height * pokemonExample.form.baseScale).toInt()
}
if (width == -1) {
width = ceil(pokemonExample.form.hitbox.width * pokemonExample.form.baseScale).toInt()
}
}
}
}
@@ -42,6 +42,8 @@ abstract class SpawnDetail : ModDependant {
var anticonditions = mutableListOf<SpawningCondition<*>>()
var compositeCondition: CompositeSpawningCondition? = null
var weightMultipliers = mutableListOf<WeightMultiplier>()
var width = -1
var height = -1
var weight = -1F
var percentage = -1F
@@ -63,7 +65,10 @@ abstract class SpawnDetail : ModDependant {
return false
} else if (compositeCondition?.satisfiedBy(ctx, this) == false) {
return false
} else if (!ctx.postFilter(this)) {
return false
}
return true
}
@@ -21,7 +21,7 @@ import kotlin.reflect.full.createInstance
* A spawning selector that compiles a distinct list of all spawn details that
* are possible across the context list, chooses which context type to spawn based
* on a weighted selection of the number of spawns possible in it, performs a weighted
* selection of them of spawns in that context type, and then chooses which of its contexts
* selection of spawns in that context type, and then chooses which of its contexts
* to spawn them on based on their context-adjusted weights.
*
* The goal of this algorithm is to be kinder to spawns that are only possible
@@ -78,11 +78,12 @@ open class FlatContextWeightedSelector : SpawningSelector {
val possible = spawner.getMatchingSpawns(ctx)
if (possible.isNotEmpty()) {
val contextSelectionData = contextTypesToSpawns.getOrPut(contextType) { ContextSelectionData(mutableMapOf(), 0F) }
possible.forEach {
if (it.percentage > 0) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this context
// type, otherwise the percentage will get amplified for every context the thing was possible,
// completely ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !contextSelectionData.spawnsToContexts.containsKey(it)) {
contextSelectionData.percentSum += it.percentage
}
@@ -107,8 +108,8 @@ open class FlatContextWeightedSelector : SpawningSelector {
if (selectionData.isEmpty()) {
return null
}
// Which context should we use?
// Which context type should we use?
val contextSelectionData = selectionData.entries.toList().weightedSelection { getWeight(it.key) * it.value.size }!!.value
val spawnsToContexts = contextSelectionData.spawnsToContexts
@@ -0,0 +1,143 @@
/*
* 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.selection
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.context.RegisteredSpawningContext
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
/**
* A spawning selector that compiles a distinct list of all spawn details that
* are possible across any context type and performs a weighted selection of spawns.
*
* The goal of this algorithm is to be kinder to spawns that are only possible
* in very specific locational conditions or context types by not letting the scarcity
* of suitable locations hurt its chances of spawning. It differs from the
* [FlatContextWeightedSelector] because here a spawning context type only appearing once
* will not make it any less likely to be selected.
*
* When choosing which context a specific spawn detail will spawn at once that spawn detail
* has been chosen, it does a flat random across the possible contexts.
*
* At a glance:
* - Spawn detail selection is flat across context quantity
* - The popularity of specific context types has no bearing on the spawn chance.
*
* @author Hiroku
* @since July 10th, 2022
*/
open class FlatSelector : SpawningSelector {
open fun getWeight(contextType: RegisteredSpawningContext<*>) = contextType.getWeight()
protected class ContextSelectionData(
val spawnsToContexts: MutableMap<SpawnDetail, MutableList<SpawningContext>>,
var percentSum: Float
) {
val size: Int
get() = spawnsToContexts.size
}
protected fun getSelectionData(
spawner: Spawner,
contexts: List<SpawningContext>
): ContextSelectionData {
val spawnsToContexts: MutableMap<SpawnDetail, MutableList<SpawningContext>> = mutableMapOf()
var percentSum = 0F
contexts.forEach { ctx ->
spawner.getMatchingSpawns(ctx).forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every context the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnsToContexts.containsKey(it)) {
percentSum += it.percentage
}
val selectingSpawnInformation = spawnsToContexts.getOrPut(
it,
::mutableListOf
)
selectingSpawnInformation.add(ctx)
}
}
return ContextSelectionData(spawnsToContexts, percentSum)
}
override fun select(
spawner: Spawner,
contexts: List<SpawningContext>
): Pair<SpawningContext, SpawnDetail>? {
val selectionData = getSelectionData(spawner, contexts)
if (selectionData.size == 0) {
return null
}
val spawnsToContexts = selectionData.spawnsToContexts
var percentSum = selectionData.percentSum
// First pass is doing percentage checks.
if (percentSum > 0) {
if (percentSum > 100) {
Cobblemon.LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
This means you don't understand how this option works.
""".trimIndent()
)
return null
}
/*
* 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
percentSum = 0F
for ((spawnDetail, info) in spawnsToContexts) {
if (spawnDetail.percentage > 0) {
percentSum += spawnDetail.percentage
if (percentSum >= selectedPercentage) {
return info.random() to spawnDetail
}
}
}
}
val selectedSpawn = spawnsToContexts.entries.toList().weightedSelection { it.key.weight }!!
return selectedSpawn.value.random() to selectedSpawn.key
}
override fun getTotalWeights(
spawner: Spawner,
contexts: List<SpawningContext>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, contexts)
if (selectionData.size == 0) {
return mapOf()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
for (spawnDetail in selectionData.spawnsToContexts.keys) {
totalWeights[spawnDetail] = spawnDetail.weight
}
return totalWeights
}
}
@@ -13,7 +13,7 @@ import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
/**
* Interface responsible for taking all of the potential spawns across many contexts, and applying some kind of
* Interface responsible for taking all the potential spawns across many contexts, and applying some kind of
* selection process to choose one. It is also responsible for generating a name to percentage probability for the given
* spawn information for checking spawns under specific conditions.
*
@@ -15,7 +15,7 @@ import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.selection.FlatContextWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.FlatSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.entity.Entity
@@ -32,7 +32,7 @@ abstract class TickingSpawner(
var spawns: SpawnPool,
val manager: SpawnerManager
) : Spawner {
private var selector: SpawningSelector = FlatContextWeightedSelector()
private var selector: SpawningSelector = FlatSelector()
override val influences = mutableListOf<SpawningInfluence>()
override fun canSpawn() = active
@@ -247,9 +247,6 @@ open class PartyStore(override val uuid: UUID) : PokemonStore<PartyPosition>() {
fun toBattleTeam(clone: Boolean = false, checkHealth: Boolean = true, leadingPokemon: UUID? = null) = mapNotNull {
// TODO Other 'able to battle' checks
if (checkHealth && it.currentHealth <= 0) {
return@mapNotNull null
}
return@mapNotNull if (clone) {
BattlePokemon.safeCopyOf(it)
} else {
@@ -24,10 +24,15 @@ class JsonPlayerDataStoreFactory : PlayerDataStoreFactory {
fun setup(server: MinecraftServer) {
adapter.setup(server);
}
override fun load(uuid: UUID): PlayerData {
if(cache.contains(uuid))
return cache[uuid]!!;
return adapter.load(uuid);
return if(cache.contains(uuid))
cache[uuid]!!;
else {
val data = adapter.load(uuid);
cache[uuid] = data
data
}
}
override fun save(data: PlayerData) {
@@ -9,74 +9,32 @@
package com.cobblemon.mod.common.api.tags
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.registry.Registry
import net.minecraft.registry.Registries
import net.minecraft.registry.RegistryKeys
import net.minecraft.registry.tag.TagKey
/**
* A collection of the Cobblemon [TagKey]s related to the [Registry.BLOCK].
* A collection of the Cobblemon [TagKey]s related to the [Registries.BLOCK].
*
* @author Licious
* @since October 29th, 2022
*/
@Suppress("HasPlatformType", "unused")
object CobblemonBlockTags {
@JvmField
val APRICORN_LEAVES = createTag("apricorn_leaves")
@JvmField
val APRICORN_LOGS = createTag("apricorn_logs")
@JvmField
val APRICORN_SAPLINGS = createTag("apricorn_saplings")
@JvmField
val APRICORNS = createTag("apricorns")
@JvmField
val DRIPSTONE_GROWABLE = createTag("dripstone_growable")
@JvmField
val DRIPSTONE_REPLACEABLES = createTag("dripstone_replaceables")
// ToDo remove in 1.3
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val FENCE_GATES = createTag("fence_gates")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val FENCES = createTag("fences")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val LEAVES = createTag("leaves")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val LOGS = createTag("logs")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val LOGS_THAT_BURN = createTag("logs_that_burn")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val PLANKS = createTag("planks")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val SAPLINGS = createTag("saplings")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val STANDING_SIGNS = createTag("standing_signs")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val WALL_SIGNS = createTag("wall_signs")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val WOODEN_BUTTONS = createTag("wooden_buttons")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val WOODEN_FENCES = createTag("wooden_fences")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val WOODEN_PRESSURE_PLATES = createTag("wooden_pressure_plates")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val WOODEN_SLABS = createTag("wooden_slabs")
@Deprecated(message = "This tag no longer exists", level = DeprecationLevel.ERROR)
@JvmField
val WOODEN_STAIRS = createTag("wooden_stairs")
@JvmField val APRICORN_LEAVES = createTag("apricorn_leaves")
@JvmField val APRICORN_LOGS = createTag("apricorn_logs")
@JvmField val APRICORN_SAPLINGS = createTag("apricorn_saplings")
@JvmField val APRICORNS = createTag("apricorns")
@JvmField val CROPS = createTag("crops")
@JvmField val DRIPSTONE_GROWABLE = createTag("dripstone_growable")
@JvmField val DRIPSTONE_REPLACEABLES = createTag("dripstone_replaceables")
@JvmField val FLOWERS = createTag("flowers")
@JvmField val MEDICINAL_LEEK_PLANTABLE = createTag("medicinal_leek_plantable")
@JvmField val MINTS = createTag("mints")
@JvmField val ROOTS_SPREADABLE = createTag("roots_spreadable")
@JvmField val SMALL_FLOWERS = createTag("small_flowers")
@JvmField val SEES_SKY = createTag("sees_sky")
private fun createTag(name: String) = TagKey.of(RegistryKeys.BLOCK, cobblemonResource(name))
@@ -38,6 +38,10 @@ object CobblemonItemTags {
@JvmField
val LUCKY_EGG = create("held/lucky_egg")
@JvmField
val DESTINY_KNOT = create("held/destiny_knot")
@JvmField
val EVERSTONE = create("held/everstone")
@JvmField
val EVOLUTION_STONES = create("evolution_stones")
@JvmField
val EVOLUTION_ITEMS = create("evolution_items")
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.PrioritizedList
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.item.battle.BagItem
import com.cobblemon.mod.common.item.battle.BagItemConvertible
import com.cobblemon.mod.common.util.cobblemonResource
import java.io.File
import net.minecraft.item.ItemStack
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.ResourceType
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
/**
* A registry for [BagItem]s that could be parsed from [ItemStack]s. This registry is used as the resource loading
* mechanism for bag item scripts, but add to [bagItems] to map from stacks to [BagItem]s.
*
* @author Hiroku
* @since June 26th, 2023
*/
object BagItems : DataRegistry {
override val id = cobblemonResource("bag_items")
override val type = ResourceType.SERVER_DATA
override val observable = SimpleObservable<BagItems>()
override fun sync(player: ServerPlayerEntity) {}
val bagItems = PrioritizedList<BagItemConvertible>()
internal val bagItemsScripts = mutableMapOf<String, String>() // itemId to JavaScript
init {
observable.subscribe {
Cobblemon.showdownThread.queue(ShowdownService::registerBagItems)
}
}
fun getConvertibleForStack(stack: ItemStack): BagItemConvertible? {
return bagItems.firstOrNull { it.getBagItem(stack) != null }
}
override fun reload(manager: ResourceManager) {
manager.findResources("bag_items") { it.path.endsWith(".js") }.forEach { (identifier, resource) ->
resource.inputStream.use { stream ->
stream.bufferedReader().use { reader ->
val resolvedIdentifier = Identifier(identifier.namespace, File(identifier.path).nameWithoutExtension)
val js = reader.readText()
bagItemsScripts[resolvedIdentifier.path] = js
}
}
}
this.observable.emit(this)
}
}
@@ -16,9 +16,9 @@ import com.cobblemon.mod.common.api.pokemon.helditem.HeldItemProvider
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.battle.BattleChallengeExpiredPacket
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.google.gson.GsonBuilder
import java.time.Instant
import java.util.Optional
@@ -199,7 +199,7 @@ object BattleRegistry {
}
// Compiles the request and sends it off
ShowdownService.get().startBattle(battle, messages.toTypedArray())
ShowdownService.service.startBattle(battle, messages.toTypedArray())
CobblemonEvents.BATTLE_STARTED_POST.post(BattleStartedPostEvent(battle))
return battle
}
@@ -213,7 +213,7 @@ object BattleRegistry {
}
fun getBattleByParticipatingPlayer(serverPlayerEntity: ServerPlayerEntity) : PokemonBattle? {
return battleMap.values.find { it.actors.any { it.isForPlayer(serverPlayerEntity) } }
return battleMap.values.find { it.getActor(serverPlayerEntity) != null }
}
fun tick() {
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.item.battle.BagItem
import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.util.readSizedInt
import com.cobblemon.mod.common.util.writeSizedInt
@@ -67,8 +69,9 @@ enum class ShowdownActionResponseType(val loader: (PacketByteBuf) -> ShowdownAct
SWITCH({ SwitchActionResponse(UUID.randomUUID()) }),
MOVE({ MoveActionResponse("", null) }),
DEFAULT({ DefaultActionResponse() }),
BALL({ BallActionResponse() }),
PASS({ PassActionResponse });
FORCE_PASS({ ForcePassActionResponse() }),
PASS({ PassActionResponse }),
HEAL_ITEM({ HealItemActionResponse("potion") });
}
abstract class ShowdownActionResponse(val type: ShowdownActionResponseType) {
@@ -141,6 +144,27 @@ data class MoveActionResponse(var moveName: String, var targetPnx: String? = nul
}
}
data class HealItemActionResponse(var item: String) : ShowdownActionResponse(ShowdownActionResponseType.FORCE_PASS) {
override fun saveToBuffer(buffer: PacketByteBuf) {
super.saveToBuffer(buffer)
buffer.writeString(item)
}
override fun loadFromBuffer(buffer: PacketByteBuf): ShowdownActionResponse {
super.loadFromBuffer(buffer)
item = buffer.readString()
return this
}
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean): Boolean {
return !forceSwitch
}
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?): String {
return "healitem ${activeBattlePokemon.getPNX()} $item"
}
}
data class SwitchActionResponse(var newPokemonId: UUID) : ShowdownActionResponse(ShowdownActionResponseType.SWITCH) {
override fun saveToBuffer(buffer: PacketByteBuf) {
super.saveToBuffer(buffer)
@@ -168,6 +192,7 @@ data class SwitchActionResponse(var newPokemonId: UUID) : ShowdownActionResponse
return "switch ${activeBattlePokemon.actor.pokemonList.indexOfFirst { it.uuid == newPokemonId } + 1}"
}
}
class DefaultActionResponse: ShowdownActionResponse(ShowdownActionResponseType.DEFAULT) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "default"
@@ -177,7 +202,11 @@ object PassActionResponse : ShowdownActionResponse(ShowdownActionResponseType.PA
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "pass"
}
class BallActionResponse() : ShowdownActionResponse(ShowdownActionResponseType.BALL) {
/**
* Only meant to be used when the player is capturing or using an item - they are forced to pass
*/
class ForcePassActionResponse : ShowdownActionResponse(ShowdownActionResponseType.FORCE_PASS) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean): Boolean {
if (forceSwitch) {
return false
@@ -185,11 +214,32 @@ class BallActionResponse() : ShowdownActionResponse(ShowdownActionResponseType.B
return false
}
return activeBattlePokemon.actor.expectingCaptureActions-- > 0
return activeBattlePokemon.actor.expectingPassActions.size > 0
}
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "pass"
}
class BagItemActionResponse(val bagItem: BagItem, val target: BattlePokemon, val data: String? = null): ShowdownActionResponse(ShowdownActionResponseType.FORCE_PASS) {
override fun isValid(
activeBattlePokemon: ActiveBattlePokemon,
showdownMoveSet: ShowdownMoveset?,
forceSwitch: Boolean
): Boolean {
if (forceSwitch) {
return false
} else if (showdownMoveSet == null) {
return false
}
return activeBattlePokemon.actor.expectingPassActions.size > 0
}
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?): String {
return "useitem ${target.uuid} ${bagItem.itemName} ${bagItem.getShowdownInput(target.actor, target, data)}"
}
}
class ShowdownMoveset {
lateinit var moves: List<InBattleMove>
var trapped = false
@@ -240,6 +290,7 @@ class ShowdownPokemon {
lateinit var baseAbility: String
lateinit var pokeball: String
lateinit var ability: String
var reviving: Boolean = false
val uuid: UUID by lazy { UUID.fromString(details.split(",")[1].trim()) }
fun saveToBuffer(buffer: PacketByteBuf) {
@@ -247,6 +298,7 @@ class ShowdownPokemon {
buffer.writeString(details)
buffer.writeString(condition)
buffer.writeBoolean(active)
buffer.writeBoolean(reviving)
buffer.writeSizedInt(IntSize.U_BYTE, moves.size)
moves.forEach(buffer::writeString)
buffer.writeString(baseAbility)
@@ -259,6 +311,7 @@ class ShowdownPokemon {
details = buffer.readString()
condition = buffer.readString()
active = buffer.readBoolean()
reviving = buffer.readBoolean()
repeat(times = buffer.readSizedInt(IntSize.U_BYTE)) {
moves.add(buffer.readString())
}
@@ -34,12 +34,7 @@ import com.cobblemon.mod.common.pokemon.evolution.progress.DamageTakenEvolutionP
import com.cobblemon.mod.common.pokemon.evolution.progress.RecoilEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.UseMoveEvolutionProgress
import com.cobblemon.mod.common.pokemon.status.PersistentStatus
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.runOnServer
import com.cobblemon.mod.common.util.swap
import com.cobblemon.mod.common.util.*
import java.util.UUID
import java.util.concurrent.CompletableFuture
import kotlin.math.roundToInt
@@ -74,6 +69,7 @@ object ShowdownInterpreter {
updateInstructions["|win|"] = this::handleWinInstruction
updateInstructions["|move|"] = this::handleMoveInstruction
updateInstructions["|cant|"] = this::handleCantInstruction
updateInstructions["|bagitem|"] = this::handleBagItemInstruction
updateInstructions["|-supereffective|"] = this::handleSuperEffectiveInstruction
updateInstructions["|-resisted|"] = this::handleResistInstruction
updateInstructions["|-crit"] = this::handleCritInstruction
@@ -155,7 +151,7 @@ object ShowdownInterpreter {
val boostBucket = if (isBoost) BattleContext.Type.BOOST else BattleContext.Type.UNBOOST
val context = getContextFromAction(message, boostBucket, battle)
// TODO: replace with context that tracks detailed information such as # of stages
repeat(stages) {pokemon.contextManager.add(context)}
repeat(stages) { pokemon.contextManager.add(context) }
battle.minorBattleActions[pokemon.uuid] = message
}
}
@@ -533,6 +529,24 @@ object ShowdownInterpreter {
}
}
/**
* Format:
* |bagitem|POKEMON|ITEMNAME
*
* POKEMON had ITEMNAME used on it from the 'bag'.
*/
private fun handleBagItemInstruction(battle: PokemonBattle, message: BattleMessage, remainingLines: MutableList<String>) {
battle.dispatchGo {
val pokemon = message.pokemonByUuid(0, battle)!!
val item = message.argumentAt(1)!!
val ownerName = pokemon.actor.getName()
val itemName = item.asTranslated()
battle.broadcastChatMessage(battleLang("bagitem.use", ownerName, itemName, pokemon.getName()))
}
}
/**
* Format:
* |win|USER
@@ -664,7 +678,7 @@ object ShowdownInterpreter {
val effectID = message.effectAt(1)?.id ?: return@dispatchWaiting
val name = pokemon.getName()
// Move may be null as it's not always given
val moveName = message.argumentAt(2)?.let { Moves.getByName(it)?.displayName } ?: Text.EMPTY
val moveName = message.argumentAt(2)?.let { Moves.getByName(it.replace(" ",""))?.displayName } ?: "".text()
val lang = when (effectID) {
// TODO: in the games they use a generic image because there is a popup of the ability and the sprite of the mon, it may be good to have a similar system here
@@ -935,7 +949,7 @@ object ShowdownInterpreter {
}
// Includes revealed move
"forewarn" -> {
val moveName = message.argumentAt(2)?.let { Moves.getByName(it)?.displayName } ?: Text.EMPTY
val moveName = message.argumentAt(2)?.let { Moves.getByName(it.replace(" ",""))?.displayName } ?: "".text()
battleLang("activate.forewarn", sourceName, moveName)
}
"focussash", "focusband" -> battleLang("activate.focusband", pokemonName, message.effectAt(1)!!.typelessData)
@@ -1336,8 +1350,7 @@ object ShowdownInterpreter {
* The specified Pokémon POKEMON has taken damage, and is now at HP STATUS
*/
fun handleDamageInstruction(battle: PokemonBattle, actor: BattleActor, publicMessage: BattleMessage, privateMessage: BattleMessage) {
val (pnx, _) = publicMessage.pnxAndUuid(0) ?: return
val battlePokemon = privateMessage.getBattlePokemon(0, battle) ?: return
val battlePokemon = publicMessage.getBattlePokemon(0, battle) ?: return
if (privateMessage.optionalArgument("from")?.equals("recoil", true) == true) {
battlePokemon.effectedPokemon.let { pokemon ->
val recoilProgress = RecoilEvolutionProgress()
@@ -1403,7 +1416,8 @@ object ShowdownInterpreter {
GO
}
}
battle.sendSidedUpdate(actor, BattleHealthChangePacket(pnx, remainingHealth.toFloat()), BattleHealthChangePacket(pnx, newHealthRatio))
privateMessage.pnxAndUuid(0)?.let { (pnx, _) -> battle.sendSidedUpdate(actor, BattleHealthChangePacket(pnx, remainingHealth.toFloat()), BattleHealthChangePacket(pnx, newHealthRatio)) }
battle.minorBattleActions[battlePokemon.uuid] = privateMessage
WaitDispatch(1F)
}
@@ -1509,7 +1523,7 @@ object ShowdownInterpreter {
* The specified Pokémon POKEMON has healed damage, and is now at HP STATUS.
*/
private fun handleHealInstruction(battle: PokemonBattle, actor: BattleActor, publicMessage: BattleMessage, privateMessage: BattleMessage) {
val (pnx, _) = privateMessage.pnxAndUuid(0) ?: return
val pnx = privateMessage.pnxAndUuid(0)?.first
val battlePokemon = privateMessage.getBattlePokemon(0, battle) ?: return
val rawHpAndStatus = privateMessage.argumentAt(1)?.split(" ") ?: return
val rawHpRatio = rawHpAndStatus.getOrNull(0) ?: return
@@ -1520,7 +1534,9 @@ object ShowdownInterpreter {
broadcastOptionalAbility(battle, effect, pokemonName)
battle.dispatchWaiting {
battle.sendSidedUpdate(actor, BattleHealthChangePacket(pnx, newHealth.toFloat()), BattleHealthChangePacket(pnx, newHealthRatio))
if (pnx != null) {
battle.sendSidedUpdate(actor, BattleHealthChangePacket(pnx, newHealth.toFloat()), BattleHealthChangePacket(pnx, newHealthRatio))
}
val silent = privateMessage.hasOptionalArgument("silent")
if (!silent) {
val lang = when {
@@ -1547,7 +1563,9 @@ object ShowdownInterpreter {
}
}
}
else -> battleLang("heal.generic", battlePokemon.getName())
else -> {
battleLang("heal.generic", battlePokemon.getName())
}
}
battle.broadcastChatMessage(lang)
}
@@ -1559,7 +1577,9 @@ object ShowdownInterpreter {
val status = Statuses.getStatus(rawStatus) ?: return@dispatchWaiting
if (status is PersistentStatus) {
battlePokemon.effectedPokemon.applyStatus(status)
battle.sendUpdate(BattlePersistentStatusPacket(pnx, status))
if (pnx != null) {
battle.sendUpdate(BattlePersistentStatusPacket(pnx, status))
}
if (!silent) {
status.applyMessage.let { battle.broadcastChatMessage(it.asTranslated(battlePokemon.getName())) }
}
@@ -17,19 +17,19 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
private val latch = CountDownLatch(1)
private val whenReady : Queue<Runnable> = LinkedList()
private val whenReady : Queue<(ShowdownService) -> Unit> = LinkedList()
fun launch() {
this.start()
this.latch.await()
for (action in whenReady) {
action.run()
action(ShowdownService.service)
}
}
fun queue(action: Runnable) {
fun queue(action: (ShowdownService) -> Unit) {
if (this.latch.count == 0L) {
action.run()
action(ShowdownService.service)
} else {
this.whenReady.add(action)
}
@@ -37,7 +37,7 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
override fun run() {
LOGGER.info("Starting showdown service...")
ShowdownService.get().openConnection()
ShowdownService.service.openConnection()
LOGGER.info("Showdown has been started!")
this.latch.countDown()
}
@@ -10,9 +10,8 @@ package com.cobblemon.mod.common.battles.runner
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.runner.graal.GraalShowdownService
import com.cobblemon.mod.common.battles.runner.socket.SocketShowdownService
import com.google.gson.JsonArray
import java.util.*
import java.util.UUID
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service.
@@ -32,13 +31,11 @@ interface ShowdownService {
fun getMoves(): JsonArray
fun getItemIds(): JsonArray
fun registerSpecies()
fun registerBagItems()
fun indicateSpeciesInitialized() {}
companion object {
private var service: ShowdownService? = null
fun get(): ShowdownService {
if (service == null) service = GraalShowdownService()
return service!!
}
val service: ShowdownService by lazy { GraalShowdownService() }
}
}
@@ -11,16 +11,28 @@ package com.cobblemon.mod.common.battles.runner.graal
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.google.gson.Gson
import com.google.gson.JsonArray
import java.io.File
import java.io.IOException
import java.net.URI
import java.nio.file.AccessMode
import java.nio.file.DirectoryStream
import java.nio.file.LinkOption
import java.nio.file.OpenOption
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.attribute.FileAttribute
import java.util.UUID
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.HostAccess
import org.graalvm.polyglot.HostAccess.Export
import org.graalvm.polyglot.PolyglotAccess
import org.graalvm.polyglot.Value
import java.io.File
import java.util.*
import org.graalvm.polyglot.io.FileSystem
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service via
@@ -49,13 +61,38 @@ class GraalShowdownService : ShowdownService {
}
private fun createContext() {
val access = HostAccess.newBuilder(HostAccess.ALL).build()
val wd = Paths.get("./showdown")
val access = HostAccess.newBuilder(HostAccess.EXPLICIT)
.allowIterableAccess(true)
.allowArrayAccess(true)
.allowListAccess(true)
.allowMapAccess(true)
.build()
context = Context.newBuilder("js")
.allowIO(true)
.fileSystem(object : FileSystem
{
val default = FileSystem.newDefaultFileSystem()
override fun parsePath(uri: URI) = default.parsePath(uri)
override fun parsePath(path: String) = default.parsePath(path)
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) = default.createDirectory(dir, *attrs)
override fun delete(path: Path) = default.delete(path)
override fun newByteChannel(path: Path, options: MutableSet<out OpenOption>, vararg attrs: FileAttribute<*>) = default.newByteChannel(path, options, *attrs)
override fun newDirectoryStream(dir: Path, filter: DirectoryStream.Filter<in Path>) = default.newDirectoryStream(dir, filter)
override fun toAbsolutePath(path: Path) = default.toAbsolutePath(path)
override fun toRealPath(path: Path, vararg linkOptions: LinkOption) = default.toRealPath(path, *linkOptions)
override fun readAttributes(path: Path, attributes: String, vararg options: LinkOption) = default.readAttributes(path, attributes, *options)
override fun checkAccess(path: Path, modes: MutableSet<out AccessMode>, vararg linkOptions: LinkOption) {
if (!path.toRealPath(LinkOption.NOFOLLOW_LINKS).startsWith(wd.toRealPath(LinkOption.NOFOLLOW_LINKS))) {
Cobblemon.LOGGER.error("Hacked JS files in datapacks or some weird file system setup that Hiroku failed to anticipate.")
throw IOException("Someone has put hacked JS files into datapacks because file access is being attempted outside of controlled folders.")
}
}
}
)
.allowExperimentalOptions(true)
.allowPolyglotAccess(PolyglotAccess.ALL)
.allowHostAccess(access)
.allowAllAccess(true)
.allowCreateThread(true)
.logHandler(GraalLogger)
.option("engine.WarnInterpreterOnly", "false")
@@ -130,14 +167,28 @@ class GraalShowdownService : ShowdownService {
receiveSpeciesDataFn.execute(jsArray)
}
override fun indicateSpeciesInitialized() {
val afterCobbledSpeciesInitFn = this.context.getBindings("js").getMember("afterCobbledSpeciesInit")
afterCobbledSpeciesInitFn.execute()
}
override fun registerBagItems() {
val receiveBagItemDataFn = this.context.getBindings("js").getMember("receiveBagItemData")
for ((itemId, js) in BagItems.bagItemsScripts) {
receiveBagItemDataFn.execute(itemId, js.replace("\n", " "))
}
}
private fun sendToShowdown(battleId: UUID, messages: Array<String>) {
sendBattleMessageFunction.execute(battleId.toString(), messages)
}
@Export
fun sendFromShowdown(battleId: String, message: String) {
ShowdownInterpreter.interpretMessage(UUID.fromString(battleId), message)
}
@Export
fun log(message: String) {
Cobblemon.LOGGER.info(message)
}
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.battles.runner.socket
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.pokemon.FormData
@@ -23,7 +24,7 @@ import java.io.OutputStreamWriter
import java.net.InetAddress
import java.net.Socket
import java.nio.charset.Charset
import java.util.*
import java.util.UUID
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service via
@@ -60,7 +61,7 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
writer.write(">startbattle ${battle.battleId}\n")
writer.flush()
acknowledge { Cobblemon.LOGGER.error("Failed to start battle!") }
send(battle.battleId, messages)
}
@@ -102,7 +103,7 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
}
override fun getAbilityIds(): JsonArray {
writer.write(">getCobbledAbilityIds\n")
writer.write(">getCobbledAbilityIds")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
@@ -116,7 +117,7 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
}
override fun getItemIds(): JsonArray {
writer.write(">getCobbledItemIds\n")
writer.write(">getCobbledItemIds")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
@@ -124,14 +125,17 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
private fun sendSpeciesData(species: Species, form: FormData?) {
writer.write(">receiveSpeciesData ${gson.toJson(PokemonSpecies.ShowdownSpecies(species, form))}\n")
writer.flush()
val ack = CharArray(3)
reader.read(ack) // Wait for showdown to acknowledge.
assert(String(ack) == "ACK")
acknowledge()
}
private fun sendBagItem(itemId: String, js: String) {
writer.write(">receiveBagItemData $itemId $js")
acknowledge { Cobblemon.LOGGER.error("Failed to send bag item to Showdown: $itemId") }
}
override fun registerSpecies() {
writer.write(">resetSpeciesData\n")
acknowledge()
PokemonSpecies.species.forEach { species ->
sendSpeciesData(species, null)
species.forms.forEach { form ->
@@ -141,4 +145,25 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
}
}
}
fun acknowledge(ifFails: () -> Unit = {}) {
writer.flush()
val ack = CharArray(3)
reader.read(ack)
if (String(ack) != "ACK") {
ifFails()
}
}
override fun registerBagItems() {
for ((itemId, js) in BagItems.bagItemsScripts) {
sendBagItem(itemId, js.replace("\n", " "))
}
}
override fun indicateSpeciesInitialized() {
writer.write(">afterCobbledSpeciesInit")
acknowledge()
}
}
@@ -23,6 +23,7 @@ import net.minecraft.item.ItemPlacementContext
import net.minecraft.item.ItemStack
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.server.world.ServerWorld
import net.minecraft.sound.SoundCategory
import net.minecraft.sound.SoundEvents
import net.minecraft.state.StateManager
import net.minecraft.state.property.IntProperty
@@ -42,7 +43,8 @@ import net.minecraft.world.WorldView
import net.minecraft.world.event.GameEvent
// Note we cannot make this inherit from CocoaBlock since our age properties differ, it is however safe to copy most of the logic from it
class ApricornBlock(settings: Settings, val apricorn: Apricorn) : HorizontalFacingBlock(settings), Fertilizable {
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
class ApricornBlock(settings: Settings, val apricorn: Apricorn) : HorizontalFacingBlock(settings), Fertilizable, ShearableBlock {
init {
this.defaultState = this.stateManager.defaultState
@@ -104,7 +106,6 @@ class ApricornBlock(settings: Settings, val apricorn: Apricorn) : HorizontalFaci
return null
}
@Deprecated("Deprecated in Java")
override fun getStateForNeighborUpdate(state: BlockState, direction: Direction, neighborState: BlockState, world: WorldAccess, pos: BlockPos, neighborPos: BlockPos): BlockState? {
return if (direction == state.get(FACING) && !state.canPlaceAt(world, pos)) Blocks.AIR.defaultState
else super.getStateForNeighborUpdate(state, direction, neighborState, world, pos, neighborPos)
@@ -122,10 +123,8 @@ class ApricornBlock(settings: Settings, val apricorn: Apricorn) : HorizontalFaci
builder.add(FACING, AGE)
}
@Deprecated("Deprecated in Java")
override fun canPathfindThrough(state: BlockState, world: BlockView, pos: BlockPos, type: NavigationType) = false
@Deprecated("Deprecated in Java")
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hit: BlockHitResult): ActionResult {
if (state.get(AGE) != MAX_AGE) {
return super.onUse(state, world, pos, player, hand, hit)
@@ -166,6 +165,17 @@ class ApricornBlock(settings: Settings, val apricorn: Apricorn) : HorizontalFaci
return resetState
}
override fun attemptShear(world: World, state: BlockState, pos: BlockPos, successCallback: () -> Unit): Boolean {
if (state.get(AGE) == MAX_AGE) {
return false
}
world.playSound(null, pos, SoundEvents.ENTITY_SHEEP_SHEAR, SoundCategory.BLOCKS, 1F, 1F)
this.harvest(world, state, pos)
successCallback()
world.emitGameEvent(null, GameEvent.SHEAR, pos)
return true
}
companion object {
val AGE: IntProperty = Properties.AGE_3
@@ -8,110 +8,42 @@
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.world.BigRootPropagatedEvent
import net.minecraft.block.Block
import net.minecraft.block.BlockRenderType
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.block.Fertilizable
import net.minecraft.block.ShapeContext
import net.minecraft.registry.tag.BlockTags
import net.minecraft.server.world.ServerWorld
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.util.math.random.Random
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.minecraft.world.WorldView
class BigRootBlock(settings: Settings) : Block(settings), Fertilizable {
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
class BigRootBlock(settings: Settings) : RootBlock(settings) {
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape = AABB
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hit: BlockHitResult): ActionResult {
val stack = player.getStackInHand(hand)
if (stack.isOf(Items.SHEARS) && this.attemptShear(world, state, pos) { stack.damage(1, player) { affected -> affected.sendToolBreakStatus(hand) } }) {
return ActionResult.success(world.isClient)
}
return super.onUse(state, world, pos, player, hand, hit)
}
override fun shearedResultingState(): BlockState = Blocks.HANGING_ROOTS.defaultState
override fun shearedDrop(): ItemStack = Items.STRING.defaultStack
companion object {
const val MAX_PROPAGATING_LIGHT_LEVEL = 11
private val AABB = VoxelShapes.cuboid(0.2, 0.3, 0.2, 0.8, 1.0, 0.8)
}
init {
this.defaultState = stateManager.defaultState
}
override fun getOutlineShape(
state: BlockState,
world: BlockView,
pos: BlockPos,
context: ShapeContext
) = AABB
override fun hasRandomTicks(state: BlockState) = true
override fun randomTick(state: BlockState, world: ServerWorld, pos: BlockPos, random: Random) {
// Check for propagation
if (random.nextDouble() < Cobblemon.config.bigRootPropagationChance && world.getLightLevel(pos) < MAX_PROPAGATING_LIGHT_LEVEL) {
spreadFrom(world, pos, random)
}
}
override fun canPlaceAt(state: BlockState, world: WorldView, pos: BlockPos): Boolean {
return world.getBlockState(pos.up()).isIn(BlockTags.DIRT) && world.isAir(pos)
}
override fun getStateForNeighborUpdate(
state: BlockState,
direction: Direction,
neighborState: BlockState,
world: WorldAccess,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
return if (!world.getBlockState(pos.up()).isIn(BlockTags.DIRT)) {
Blocks.AIR.defaultState
} else {
super.getStateForNeighborUpdate(state, direction, neighborState, world, pos, neighborPos)
}
}
fun spreadFrom(world: ServerWorld, pos: BlockPos, random: Random) {
for (xDiff in -1..1) {
for (zDiff in -1..1) {
if (xDiff == 0 && zDiff == 0) {
continue
}
val adjacent = pos.add(xDiff, 0, zDiff)
if (canPlaceAt(world.getBlockState(adjacent), world, adjacent)) {
val isEnergyRoot = random.nextFloat() < Cobblemon.config.energyRootChance
val event = BigRootPropagatedEvent(
world = world,
pos = pos,
newRootPosition = adjacent,
isEnergyRoot = isEnergyRoot
)
CobblemonEvents.BIG_ROOT_PROPAGATED.postThen(
event = event,
ifCanceled = {},
ifSucceeded = { ev ->
world.setBlockState(
ev.newRootPosition,
if (ev.isEnergyRoot) CobblemonBlocks.ENERGY_ROOT.defaultState else defaultState
)
}
)
return
}
}
}
}
override fun isFertilizable(world: WorldView, pos: BlockPos, state: BlockState, isClient: Boolean) = true
override fun canGrow(world: World, random: Random, pos: BlockPos, state: BlockState) = true
override fun grow(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState) {
spreadFrom(world, pos, random)
}
override fun getRenderType(state: BlockState) = BlockRenderType.MODEL
}
@@ -8,62 +8,29 @@
package com.cobblemon.mod.common.block
import net.minecraft.block.Block
import net.minecraft.block.BlockRenderType
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.CobblemonItems
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.block.Fertilizable
import net.minecraft.block.ShapeContext
import net.minecraft.registry.tag.BlockTags
import net.minecraft.server.world.ServerWorld
import net.minecraft.item.ItemStack
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.util.math.random.Random
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.minecraft.world.WorldView
class EnergyRootBlock(settings: Settings) : Block(settings), Fertilizable {
@Suppress("OVERRIDE_DEPRECATION")
class EnergyRootBlock(settings: Settings) : RootBlock(settings) {
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape = AABB
override fun shearedResultingState(): BlockState = CobblemonBlocks.BIG_ROOT.defaultState
override fun shearedDrop(): ItemStack = CobblemonItems.ENERGY_ROOT.defaultStack
companion object {
private val AABB = VoxelShapes.cuboid(0.2, 0.1, 0.2, 0.8, 1.0, 0.8)
}
init {
this.defaultState = stateManager.defaultState
}
override fun canPlaceAt(state: BlockState, world: WorldView, pos: BlockPos): Boolean {
return world.getBlockState(pos.up()).isIn(BlockTags.DIRT) && world.isAir(pos)
}
override fun getStateForNeighborUpdate(
state: BlockState,
direction: Direction,
neighborState: BlockState,
world: WorldAccess,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
return if (!world.getBlockState(pos.up()).isIn(BlockTags.DIRT)) {
Blocks.AIR.defaultState
} else {
super.getStateForNeighborUpdate(state, direction, neighborState, world, pos, neighborPos)
}
}
override fun getOutlineShape(
state: BlockState,
world: BlockView,
pos: BlockPos,
context: ShapeContext
) = AABB
// Maybe turn into Giant Root? idk
override fun isFertilizable(world: WorldView, pos: BlockPos, state: BlockState, isClient: Boolean) = false
override fun canGrow(world: World, random: Random, pos: BlockPos, state: BlockState) = true
override fun grow(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState) {}
override fun getRenderType(state: BlockState) = BlockRenderType.MODEL
}
@@ -9,61 +9,69 @@
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.tags.CobblemonBlockTags
import net.minecraft.block.*
import net.minecraft.fluid.Fluids
import net.minecraft.item.ItemStack
import net.minecraft.item.ItemConvertible
import net.minecraft.registry.tag.FluidTags
import net.minecraft.server.world.ServerWorld
import net.minecraft.state.StateManager
import net.minecraft.state.property.IntProperty
import net.minecraft.state.property.Properties
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.random.Random
import net.minecraft.util.shape.VoxelShape
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldView
import kotlin.math.min
class MedicinalLeekBlock(settings: Settings) : PlantBlock(settings), Fertilizable {
@Suppress("OVERRIDE_DEPRECATION")
class MedicinalLeekBlock(settings: Settings) : CropBlock(settings) {
init {
defaultState = stateManager.defaultState
.with(AGE, 0)
}
override fun getAgeProperty(): IntProperty = AGE
override fun getPickStack(world: BlockView, pos: BlockPos, state: BlockState) = ItemStack(CobblemonItems.MEDICINAL_LEEK)
private fun isMature(state: BlockState) = state.get(AGE) == MATURE_AGE
override fun isFertilizable(world: WorldView, pos: BlockPos, state: BlockState, isClient: Boolean) = !isMature(state)
override fun canGrow(world: World, random: Random, pos: BlockPos, state: BlockState) = true
override fun grow(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState) {
world.setBlockState(pos, state.with(AGE, min(state.get(AGE) + 1, MATURE_AGE)), NOTIFY_LISTENERS)
}
@Deprecated("Deprecated in Java")
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext) = AGE_TO_SHAPE[state.get(AGE)]
override fun hasRandomTicks(state: BlockState) = !isMature(state)
@Deprecated("Deprecated in Java")
override fun randomTick(state: BlockState, world: ServerWorld, pos: BlockPos, random: Random) {
if (world.getBaseLightLevel(pos, 0) >= 0) {
if (isMature(state)) return
if (random.nextInt(50) != 0) return
val currentAge = state.get(AGE)
world.setBlockState(pos, defaultState.with(AGE, currentAge + 1), NOTIFY_LISTENERS)
}
}
override fun getMaxAge(): Int = MATURE_AGE
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
builder.add(AGE)
builder.add(this.ageProperty)
}
override fun canPlantOnTop(floor: BlockState, world: BlockView, pos: BlockPos): Boolean {
val fluidState = world.getFluidState(pos)
val fluidState2 = world.getFluidState(pos.up())
return (fluidState.fluid === Fluids.WATER) && fluidState2.fluid === Fluids.EMPTY
override fun getSeedsItem(): ItemConvertible = CobblemonItems.MEDICINAL_LEEK
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape = AGE_TO_SHAPE[this.getAge(state)]
override fun randomTick(state: BlockState, world: ServerWorld, pos: BlockPos, random: Random) {
// This is specified as growing fast like sugar cane
// They have 15 age stages until they grow upwards, this is an attempt at a chance based but likely event
if (this.isMature(state) || random.nextInt(4) != 0) {
return
}
this.applyGrowth(world, pos, state)
}
// These 3 are still around for the sake of compatibility, vanilla won't trigger it but some mods might
// We implement applyGrowth & getGrowthAmount for them
override fun isFertilizable(world: WorldView?, pos: BlockPos?, state: BlockState?, isClient: Boolean): Boolean = false
override fun applyGrowth(world: World, pos: BlockPos, state: BlockState) {
world.setBlockState(pos, state.with(this.ageProperty, (this.getAge(state) + 1).coerceAtMost(this.maxAge)), NOTIFY_LISTENERS)
}
override fun getGrowthAmount(world: World): Int = 1
override fun canPlaceAt(state: BlockState, world: WorldView, pos: BlockPos): Boolean {
// We don't care about the sky & light level, sugar cane doesn't either
return this.canPlantOnTop(state, world, pos)
}
override fun canPlantOnTop(state: BlockState, world: BlockView, pos: BlockPos): Boolean {
val down = pos.down()
val floor = world.getBlockState(down)
val fluidState = world.getFluidState(down)
return floor.isIn(CobblemonBlockTags.MEDICINAL_LEEK_PLANTABLE) && fluidState.level == 8 && fluidState.isIn(FluidTags.WATER)
}
companion object {
const val MATURE_AGE = 3
val AGE: IntProperty = Properties.AGE_3
val AGE_TO_SHAPE = arrayOf(
@@ -72,5 +80,6 @@ class MedicinalLeekBlock(settings: Settings) : PlantBlock(settings), Fertilizabl
createCuboidShape(0.0, 0.0, 0.0, 16.0, 8.0, 16.0),
createCuboidShape(0.0, 0.0, 0.0, 16.0, 11.0, 16.0)
)
}
}
@@ -10,53 +10,43 @@ package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.CobblemonItems
import kotlin.math.min
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.block.Fertilizable
import net.minecraft.block.PlantBlock
import net.minecraft.block.ShapeContext
import com.cobblemon.mod.common.item.MintLeafItem
import net.minecraft.block.*
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.item.ItemConvertible
import net.minecraft.registry.tag.BlockTags
import net.minecraft.server.world.ServerWorld
import net.minecraft.state.StateManager
import net.minecraft.state.property.BooleanProperty
import net.minecraft.state.property.IntProperty
import net.minecraft.state.property.Properties
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.random.Random
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldView
class MintBlock(private val mintType: MintType, settings: Settings) : PlantBlock(settings), Fertilizable {
@Suppress("OVERRIDE_DEPRECATION", "MemberVisibilityCanBePrivate")
class MintBlock(private val mintType: MintType, settings: Settings) : CropBlock(settings), Fertilizable {
init {
defaultState = stateManager.defaultState
.with(AGE, 0)
.with(IS_WILD, false)
}
override fun getPickStack(world: BlockView, pos: BlockPos, state: BlockState) = ItemStack(mintType.getSeed())
private fun isMature(state: BlockState) = state.get(AGE) == MATURE_AGE
private fun isWild(state: BlockState) = state.get(IS_WILD)
override fun isFertilizable(world: WorldView, pos: BlockPos, state: BlockState, isClient: Boolean) = !isMature(state)
override fun canGrow(world: World, random: Random, pos: BlockPos, state: BlockState) = true
override fun grow(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState) {
world.setBlockState(pos, state.with(AGE, min(state.get(AGE) + 1, MATURE_AGE)), NOTIFY_LISTENERS)
// DO NOT use withAge
// Explanation for these 2 beautiful copy pasta are basically that we need to keep the blockstate and that's not possible with the default impl :(
override fun randomTick(state: BlockState, world: ServerWorld, pos: BlockPos, random: Random) {
if (world.getBaseLightLevel(pos, 0) < 9 || this.isMature(state) || random.nextInt(50) != 0) {
return
}
this.applyGrowth(world, pos, state, false)
}
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext) = AGE_TO_SHAPE[state.get(AGE)]
override fun hasRandomTicks(state: BlockState) = !isMature(state)
override fun randomTick(state: BlockState, world: ServerWorld, pos: BlockPos, random: Random) {
if (world.getBaseLightLevel(pos, 0) >= 0) {
if (isMature(state)) return
if (random.nextInt(50) != 0) return
val currentAge = state.get(AGE)
world.setBlockState(pos, defaultState.with(AGE, currentAge + 1), NOTIFY_LISTENERS)
}
override fun applyGrowth(world: World, pos: BlockPos, state: BlockState) {
this.applyGrowth(world, pos, state, true)
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
@@ -64,15 +54,29 @@ class MintBlock(private val mintType: MintType, settings: Settings) : PlantBlock
builder.add(IS_WILD)
}
override fun canPlantOnTop(floor: BlockState, world: BlockView, pos: BlockPos): Boolean {
val topState = world.getBlockState(pos.up())
if (topState.block is MintBlock && isWild(topState)) {
return true
}
return floor.isOf(Blocks.FARMLAND)
override fun canPlaceAt(state: BlockState, world: WorldView, pos: BlockPos): Boolean {
val floor = world.getBlockState(pos.down())
// A bit of a copy pasta but we don't have access to the BlockState being attempted to be placed above on the canPlantOnTop
return (world.getBaseLightLevel(pos, 0) >= 8 || world.isSkyVisible(pos)) && ((this.isWild(state) && floor.isIn(BlockTags.DIRT)) || this.canPlantOnTop(floor, world, pos))
}
override fun getSeedsItem(): ItemConvertible = this.mintType.getSeed()
override fun getGrowthAmount(world: World): Int = 1
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape = AGE_TO_SHAPE[this.getAge(state)]
fun isWild(state: BlockState): Boolean = state.get(IS_WILD)
private fun applyGrowth(world: World, pos: BlockPos, state: BlockState, useRandomGrowthAmount: Boolean) {
val growthAmount = if (useRandomGrowthAmount) this.getGrowthAmount(world) else 1
val newAge = (this.getAge(state) + growthAmount).coerceAtMost(MATURE_AGE)
world.setBlockState(pos, state.with(AGE, newAge), NOTIFY_LISTENERS)
}
@Suppress("unused")
enum class MintType {
RED,
BLUE,
CYAN,
@@ -80,49 +84,59 @@ class MintBlock(private val mintType: MintType, settings: Settings) : PlantBlock
GREEN,
WHITE;
fun getSeed(): Item {
return when (this) {
RED -> CobblemonItems.RED_MINT_SEEDS
BLUE -> CobblemonItems.BLUE_MINT_SEEDS
CYAN -> CobblemonItems.CYAN_MINT_SEEDS
PINK -> CobblemonItems.PINK_MINT_SEEDS
GREEN -> CobblemonItems.GREEN_MINT_SEEDS
WHITE -> CobblemonItems.WHITE_MINT_SEEDS
}
fun getSeed(): Item = when (this) {
RED -> CobblemonItems.RED_MINT_SEEDS
BLUE -> CobblemonItems.BLUE_MINT_SEEDS
CYAN -> CobblemonItems.CYAN_MINT_SEEDS
PINK -> CobblemonItems.PINK_MINT_SEEDS
GREEN -> CobblemonItems.GREEN_MINT_SEEDS
WHITE -> CobblemonItems.WHITE_MINT_SEEDS
}
fun getLeaf(): Item {
return when (this) {
RED -> CobblemonItems.RED_MINT_LEAF
BLUE -> CobblemonItems.BLUE_MINT_LEAF
CYAN -> CobblemonItems.CYAN_MINT_LEAF
PINK -> CobblemonItems.PINK_MINT_LEAF
GREEN -> CobblemonItems.GREEN_MINT_LEAF
WHITE -> CobblemonItems.WHITE_MINT_LEAF
}
fun getLeaf(): MintLeafItem = when (this) {
RED -> CobblemonItems.RED_MINT_LEAF
BLUE -> CobblemonItems.BLUE_MINT_LEAF
CYAN -> CobblemonItems.CYAN_MINT_LEAF
PINK -> CobblemonItems.PINK_MINT_LEAF
GREEN -> CobblemonItems.GREEN_MINT_LEAF
WHITE -> CobblemonItems.WHITE_MINT_LEAF
}
fun getCropBlock(): Block {
return when (this) {
RED -> CobblemonBlocks.RED_MINT
BLUE -> CobblemonBlocks.BLUE_MINT
CYAN -> CobblemonBlocks.CYAN_MINT
PINK -> CobblemonBlocks.PINK_MINT
GREEN -> CobblemonBlocks.GREEN_MINT
WHITE -> CobblemonBlocks.WHITE_MINT
}
fun getCropBlock(): MintBlock = when (this) {
RED -> CobblemonBlocks.RED_MINT
BLUE -> CobblemonBlocks.BLUE_MINT
CYAN -> CobblemonBlocks.CYAN_MINT
PINK -> CobblemonBlocks.PINK_MINT
GREEN -> CobblemonBlocks.GREEN_MINT
WHITE -> CobblemonBlocks.WHITE_MINT
}
}
companion object {
const val MATURE_AGE = 3
val AGE: IntProperty = Properties.AGE_3
val AGE: IntProperty = CropBlock.AGE
const val MATURE_AGE = MAX_AGE
val IS_WILD: BooleanProperty = BooleanProperty.of("is_wild")
private val AGE_0_SHAPE = VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.1, 1.0)
private val AGE_1_TO_2_SHAPE = VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.2, 1.0)
private val AGE_3_SHAPE = VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.3, 1.0)
private val AGE_4_SHAPE = VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.4, 1.0)
private val AGE_5_SHAPE = VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.5, 1.0)
private val AGE_6_SHAPE = VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.6, 1.0)
private val AGE_7_SHAPE = VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.7, 1.0)
val AGE_TO_SHAPE = arrayOf(
createCuboidShape(5.0, -1.0, 5.0, 11.0, 1.0, 11.0),
createCuboidShape(3.0, -1.0, 3.0, 13.0, 2.0, 13.0),
createCuboidShape(3.0, -1.0, 3.0, 13.0, 4.0, 13.0),
createCuboidShape(3.0, -1.0, 3.0, 13.0, 8.0, 13.0)
AGE_0_SHAPE,
AGE_1_TO_2_SHAPE,
AGE_1_TO_2_SHAPE,
AGE_3_SHAPE,
AGE_4_SHAPE,
AGE_5_SHAPE,
AGE_6_SHAPE,
AGE_7_SHAPE
)
}
}
@@ -8,80 +8,148 @@
package com.cobblemon.mod.common.block
import kotlin.math.min
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.mulch.MulchVariant
import com.cobblemon.mod.common.api.mulch.Mulchable
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.block.Fertilizable
import net.minecraft.block.PlantBlock
import net.minecraft.block.CropBlock
import net.minecraft.block.ShapeContext
import net.minecraft.item.ItemConvertible
import net.minecraft.registry.tag.BlockTags
import net.minecraft.server.world.ServerWorld
import net.minecraft.state.StateManager
import net.minecraft.state.property.BooleanProperty
import net.minecraft.state.property.EnumProperty
import net.minecraft.state.property.IntProperty
import net.minecraft.util.StringIdentifiable
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.random.Random
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldView
class RevivalHerbBlock(settings: Settings) : PlantBlock(settings), Fertilizable {
companion object {
const val MIN_AGE = 0
const val MAX_AGE = 8
val AGE = IntProperty.of("age", MIN_AGE, MAX_AGE)
val IS_WILD = BooleanProperty.of("is_wild")
val STAGE_0_AABB = VoxelShapes.cuboid(0.0, 0.0, 0.0, 1.0, 0.2, 1.0)
val STAGE_1_AABB = VoxelShapes.cuboid(0.0, 0.0, 0.0, 1.0, 0.4, 1.0)
val STAGE_2_AABB = VoxelShapes.cuboid(0.0, 0.0, 0.0, 1.0, 0.6, 1.0)
val STAGE_3_AABB = VoxelShapes.cuboid(0.0, 0.0, 0.0, 1.0, 0.8, 1.0)
}
@Suppress("OVERRIDE_DEPRECATION", "MemberVisibilityCanBePrivate", "unused")
class RevivalHerbBlock(settings: Settings) : CropBlock(settings), Mulchable {
init {
defaultState = this.stateManager.defaultState.with(AGE, MIN_AGE).with(IS_WILD, false)
defaultState = this.stateManager.defaultState.with(AGE, MIN_AGE)
.with(IS_WILD, false)
.with(MUTATION, Mutation.NONE)
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
builder.add(AGE)
builder.add(IS_WILD)
builder.add(MUTATION)
}
override fun canGrow(world: World, random: Random, pos: BlockPos, state: BlockState): Boolean {
return state.get(AGE) < MAX_AGE
}
override fun grow(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState) {
world.setBlockState(pos, state.with(AGE, min(state.get(AGE) + 1, MAX_AGE)), NOTIFY_LISTENERS)
}
override fun isFertilizable(world: WorldView, pos: BlockPos, state: BlockState, isClient: Boolean): Boolean {
return state.get(AGE) < MAX_AGE
}
override fun getAgeProperty(): IntProperty = AGE
override fun canPlaceAt(state: BlockState, world: WorldView, pos: BlockPos): Boolean {
val down = world.getBlockState(pos.down())
val existing = world.getBlockState(pos)
return if (existing.isAir || existing.block == Blocks.MOSS_CARPET) {
(down.isIn(BlockTags.DIRT) && state.get(IS_WILD)) || down.block == Blocks.FARMLAND
} else {
false
val floor = world.getBlockState(pos.down())
// A bit of a copy pasta but we don't have access to the BlockState being attempted to be placed above on the canPlantOnTop
return (world.getBaseLightLevel(pos, 0) >= 8 || world.isSkyVisible(pos)) && ((state.get(IS_WILD) && floor.isIn(BlockTags.DIRT)) || this.canPlantOnTop(floor, world, pos))
}
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape = AGE_SHAPES.getOrNull(this.getAge(state)) ?: VoxelShapes.fullCube()
override fun getSeedsItem(): ItemConvertible = CobblemonItems.REVIVAL_HERB
override fun canHaveMulchApplied(world: ServerWorld, pos: BlockPos, state: BlockState, variant: MulchVariant): Boolean =
variant == MulchVariant.SURPRISE && this.getAge(state) <= MUTABLE_MAX_AGE && !this.isMutated(state)
override fun applyMulch(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState, variant: MulchVariant) {
val picked = Mutation.values().filterNot { it == Mutation.NONE }.random()
world.setBlockState(pos, state.with(MUTATION, picked).with(AGE, MUTABLE_MAX_AGE + 1))
}
/**
* Resolves the current [Mutation] of the given [state].
*
* @param state The [BlockState] being queried.
* @return The current [Mutation].
*/
fun mutationOf(state: BlockState): Mutation = state.get(MUTATION)
/**
* Checks if the given [state] has a [Mutation] different from [Mutation.NONE].
*
* @param state The [BlockState] being queried.
* @return If a mutation has occurred.
*/
fun isMutated(state: BlockState): Boolean = this.mutationOf(state) != Mutation.NONE
override fun getMaxAge(): Int = MAX_AGE
// DO NOT use withAge
// Explanation for these 2 beautiful copy pasta are basically that we need to keep the blockstate and that's not possible with the default impl :(
override fun randomTick(state: BlockState, world: ServerWorld, pos: BlockPos, random: Random) {
if (world.getBaseLightLevel(pos, 0) < 9 || this.isMature(state)) {
return
}
val currentMoisture = getAvailableMoisture(this, world, pos)
if (random.nextInt((25F / currentMoisture).toInt() + 1) == 0) {
this.applyGrowth(world, pos, state, false)
}
}
override fun hasRandomTicks(state: BlockState) = state.get(AGE) < MAX_AGE
override fun getOutlineShape(
state: BlockState,
world: BlockView,
pos: BlockPos,
context: ShapeContext
) = when (state.get(AGE)) {
1, 2 -> STAGE_0_AABB
3, 4, 5 -> STAGE_1_AABB
6, 7, 8 -> STAGE_2_AABB
else -> STAGE_3_AABB
override fun applyGrowth(world: World, pos: BlockPos, state: BlockState) {
this.applyGrowth(world, pos, state, true)
}
private fun applyGrowth(world: World, pos: BlockPos, state: BlockState, useRandomGrowthAmount: Boolean) {
val growthAmount = if (useRandomGrowthAmount) this.getGrowthAmount(world) else 1
val newAge = (this.getAge(state) + growthAmount).coerceAtMost(this.maxAge)
world.setBlockState(pos, state.with(AGE, newAge), NOTIFY_LISTENERS)
}
/**
* Represents the possible mutation states of this plant.
*/
enum class Mutation : StringIdentifiable {
NONE,
MENTAL,
POWER,
WHITE,
MIRROR;
override fun asString(): String = this.name.lowercase()
}
companion object {
const val MIN_AGE = 0
const val MAX_AGE = 8
/**
* This represents the max age the plant can be at before no longer being able to mutate.
*/
const val MUTABLE_MAX_AGE = 6
val AGE = IntProperty.of("age", MIN_AGE, MAX_AGE)
val IS_WILD = BooleanProperty.of("is_wild")
val MUTATION = EnumProperty.of("mutation", Mutation::class.java)
/**
* The [VoxelShape] equivalent to a certain age.
* Highest index is [MAX_AGE].
*/
val AGE_SHAPES = arrayOf(
VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.1, 1.0),
VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.2, 1.0),
VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.3, 1.0),
VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.4, 1.0),
VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.5, 1.0),
VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.7, 1.0),
VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.7, 1.0),
VoxelShapes.cuboid(0.0, -0.9, 0.0, 1.0, 0.9, 1.0),
VoxelShapes.fullCube()
)
}
}
@@ -0,0 +1,176 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.world.BigRootPropagatedEvent
import com.cobblemon.mod.common.api.tags.CobblemonBlockTags
import net.minecraft.block.*
import net.minecraft.item.ItemStack
import net.minecraft.server.world.ServerWorld
import net.minecraft.sound.SoundCategory
import net.minecraft.sound.SoundEvents
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.util.math.random.Random
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.minecraft.world.WorldView
import net.minecraft.world.event.GameEvent
@Suppress("OVERRIDE_DEPRECATION", "UNUSED_PARAMETER", "MemberVisibilityCanBePrivate", "DEPRECATION")
abstract class RootBlock(settings: Settings) : Block(settings), Fertilizable, ShearableBlock {
private val possibleDirections = setOf(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST)
override fun hasRandomTicks(state: BlockState) = true
override fun randomTick(state: BlockState, world: ServerWorld, pos: BlockPos, random: Random) {
// Check for propagation
if (random.nextDouble() < Cobblemon.config.bigRootPropagationChance && world.getLightLevel(pos) < MAX_PROPAGATING_LIGHT_LEVEL) {
this.spreadFrom(world, pos, random)
}
}
override fun canPlaceAt(state: BlockState, world: WorldView, pos: BlockPos): Boolean = this.canGoOn(state, world, pos) { true }
override fun getStateForNeighborUpdate(
state: BlockState,
direction: Direction,
neighborState: BlockState,
world: WorldAccess,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
return if (direction == Direction.UP && !this.canPlaceAt(state, world, pos)) Blocks.AIR.defaultState else super.getStateForNeighborUpdate(state, direction, neighborState, world, pos, neighborPos)
}
override fun isFertilizable(world: WorldView, pos: BlockPos, state: BlockState, isClient: Boolean) = this.canSpread(world, pos, state)
override fun canGrow(world: World, random: Random, pos: BlockPos, state: BlockState) = this.canSpread(world, pos, state)
override fun grow(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState) {
this.spreadFrom(world, pos, random)
}
override fun getRenderType(state: BlockState) = BlockRenderType.MODEL
fun spreadFrom(world: ServerWorld, pos: BlockPos, random: Random) {
val possibleDirections = this.possibleDirections.toMutableSet()
while (possibleDirections.isNotEmpty()) {
val picked = possibleDirections.random()
possibleDirections.remove(picked)
val adjacent = pos.offset(picked)
if (this.canSpreadTo(this.defaultState, world, adjacent)) {
val resultingSpread = this.spreadingRoot(random)
val event = BigRootPropagatedEvent(
world = world,
pos = pos,
newRootPosition = adjacent,
resultingSpread = resultingSpread
)
CobblemonEvents.BIG_ROOT_PROPAGATED.postThen(
event = event,
ifSucceeded = { ev ->
world.setBlockState(
ev.newRootPosition,
ev.resultingSpread
)
}
)
break
}
}
}
override fun attemptShear(world: World, state: BlockState, pos: BlockPos, successCallback: () -> Unit): Boolean {
// We always allow the shearing at the moment but hey if it ever changes at least it's easy to do so.
world.playSound(null, pos, SoundEvents.ENTITY_SHEEP_SHEAR, SoundCategory.BLOCKS, 1F, 1F)
world.setBlockState(pos, this.shearedResultingState())
val shearedDrop = this.shearedDrop()
if (!shearedDrop.isEmpty) {
dropStack(world, pos, shearedDrop)
}
world.emitGameEvent(null, GameEvent.SHEAR, pos)
return true
}
/**
* Checks if this block can spread to any neighbouring blocks.
*
* @param world The [WorldView] being queried.
* @param pos The [BlockPos] the block is currently on.
* @param state The [BlockState] of the block.
* @return If any direction supports spreading.
*/
protected fun canSpread(world: WorldView, pos: BlockPos, state: BlockState): Boolean = this.possibleDirections.any { direction ->
val adjacent = pos.offset(direction)
this.canSpreadTo(this.defaultState, world, adjacent)
}
/**
* Checks if the given coordinates allow for a root to spread to.
*
* @param state The base root [BlockState].
* @param world The [WorldView] being queried.
* @param pos The [BlockPos] being queried.
* @return If the given coordinates allow for a root to spread onto.
*/
protected fun canSpreadTo(state: BlockState, world: WorldView, pos: BlockPos): Boolean {
val existingState = world.getBlockState(pos)
// Isn't this useless since it's always air or replaceable if this is happening, not! See the spread implementation for the need of a valid block in the target pos as well.
return (existingState.isAir || existingState.isReplaceable) && this.canGoOn(state, world, pos) { ceiling -> ceiling.isIn(CobblemonBlockTags.ROOTS_SPREADABLE) }
}
/**
* Picks the [BlockState] that results from a spread of this block.
*
* @param random The [Random] instance used during a spread attempt.
* @return The [BlockState] that will represent the spread root.
*/
protected fun spreadingRoot(random: Random): BlockState = if (random.nextFloat() < Cobblemon.config.energyRootChance) CobblemonBlocks.ENERGY_ROOT.defaultState else this.defaultState
/**
* Checks if the given coordinates allow for a root to be placed with some context.
*
* @param state The base root [BlockState].
* @param world The [WorldView] being queried.
* @param pos The [BlockPos] being queried.
* @param ceilingValidator An extra condition to validate if the block it will be placed on allows for it based on context.
* @return If the given coordinates allow for a root to be set.
*/
protected fun canGoOn(state: BlockState, world: WorldView, pos: BlockPos, ceilingValidator: (ceiling: BlockState) -> Boolean): Boolean {
val up = pos.up()
val upState = world.getBlockState(up)
return upState.isSideSolidFullSquare(world, up, Direction.DOWN) && ceilingValidator(upState)
}
/**
* Resolves the [BlockState] when sheared.
*
* @return The resulting [BlockState] when this [ShearableBlock] is sheared.
*/
protected abstract fun shearedResultingState(): BlockState
/**
* Resolves the [ItemStack] drop when sheared.
*
* @return The resulting [ItemStack] to be dropped when this [ShearableBlock] is sheared.
*/
protected abstract fun shearedDrop(): ItemStack
companion object {
const val MAX_PROPAGATING_LIGHT_LEVEL = 11
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import net.minecraft.block.BlockState
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
/**
* Represents a block that can be sheared.
* This is used to compliment our Mixin implementation for Dispenser compatibility.
*/
interface ShearableBlock {
/**
* Attempts to shear this block.
*
* @param world The [World] where the shear is being attempted.
* @param state The current [BlockState] of this block.
* @param pos The [BlockPos] of this block.
* @param successCallback The callback invoked if the shear attempt was successful.
* @return If the shearing was successful.
*/
fun attemptShear(world: World, state: BlockState, pos: BlockPos, successCallback: () -> Unit = {}): Boolean
}
@@ -9,77 +9,53 @@
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.block.VivichokeBlock
import kotlin.math.min
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.block.Fertilizable
import net.minecraft.block.PlantBlock
import net.minecraft.block.CropBlock
import net.minecraft.block.ShapeContext
import net.minecraft.item.ItemStack
import net.minecraft.server.world.ServerWorld
import net.minecraft.state.StateManager
import net.minecraft.state.property.BooleanProperty
import net.minecraft.state.property.IntProperty
import net.minecraft.state.property.Properties
import net.minecraft.item.ItemConvertible
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.random.Random
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldView
class VivichokeBlock(settings: Settings) : PlantBlock(settings), Fertilizable {
init {
defaultState = stateManager.defaultState
.with(AGE, 0)
}
@Suppress("OVERRIDE_DEPRECATION")
class VivichokeBlock(settings: Settings) : CropBlock(settings) {
override fun getPickStack(world: BlockView, pos: BlockPos, state: BlockState) = ItemStack(CobblemonItems.VIVICHOKE)
private fun isMature(state: BlockState) = state.get(AGE) == MATURE_AGE
override fun isFertilizable(world: WorldView, pos: BlockPos, state: BlockState, isClient: Boolean) = !isMature(state)
override fun canGrow(world: World, random: Random, pos: BlockPos, state: BlockState) = true
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape = AGE_TO_SHAPE.getOrElse(state.get(this.ageProperty)) { VoxelShapes.fullCube() }
override fun grow(world: ServerWorld, random: Random, pos: BlockPos, state: BlockState) {
world.setBlockState(pos, state.with(AGE, min(state.get(AGE) + 1, MATURE_AGE)), NOTIFY_LISTENERS)
}
// This is a design choice, they shouldn't grow more than a single stage at a time.
override fun getGrowthAmount(world: World): Int = 1
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext) = AGE_TO_SHAPE[state.get(AGE)]
override fun hasRandomTicks(state: BlockState) = !isMature(state)
override fun randomTick(state: BlockState, world: ServerWorld, pos: BlockPos, random: Random) {
if (world.getBaseLightLevel(pos, 0) >= 0) {
if (isMature(state)) return
if (random.nextInt(50) != 0) return
val currentAge = state.get(AGE)
world.setBlockState(pos, defaultState.with(AGE, currentAge + 1), NOTIFY_LISTENERS)
}
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
builder.add(AGE)
}
override fun canPlantOnTop(floor: BlockState, world: BlockView, pos: BlockPos): Boolean {
return floor.isOf(Blocks.FARMLAND)
}
override fun getSeedsItem(): ItemConvertible = CobblemonItems.VIVICHOKE_SEEDS
companion object {
const val MATURE_AGE = 7
val AGE: IntProperty = Properties.AGE_7
val STAGE_0_SHAPE = createCuboidShape(5.0, -1.0, 5.0, 11.0, 1.0, 11.0)
val STAGE_1_SHAPE = createCuboidShape(3.0, -1.0, 3.0, 13.0, 2.0, 13.0)
val STAGE_2_SHAPE = createCuboidShape(3.0, -1.0, 3.0, 13.0, 4.0, 13.0)
val STAGE_3_SHAPE = createCuboidShape(3.0, -1.0, 3.0, 13.0, 8.0, 13.0)
private val STAGE_0_SHAPE = createCuboidShape(6.0, -1.0, 6.0, 10.0, 2.0, 10.0)
private val STAGE_1_SHAPE = createCuboidShape(6.0, -1.0, 6.0, 10.0, 5.0, 10.0)
private val STAGE_2_SHAPE = createCuboidShape(6.0, -1.0, 6.0, 10.0, 7.0, 10.0)
private val STAGE_3_SHAPE = createCuboidShape(6.0, -1.0, 6.0, 10.0, 9.0, 10.0)
private val STAGE_4_SHAPE = createCuboidShape(6.0, -1.0, 6.0, 10.0, 7.0, 10.0)
private val STAGE_5_SHAPE = createCuboidShape(6.0, -1.0, 6.0, 10.0, 8.0, 10.0)
private val STAGE_6_SHAPE = createCuboidShape(6.0, -1.0, 6.0, 10.0, 9.0, 10.0)
private val STAGE_7_SHAPE = VoxelShapes.union(
createCuboidShape(6.0, -1.0, 6.0, 10.0, 10.0, 10.0),
createCuboidShape(5.5, 10.0, 5.5, 10.5, 14.0, 10.5)
)
/**
* An array with the equivalent [VoxelShape] of a growth stage as the index.
*/
val AGE_TO_SHAPE = arrayOf(
STAGE_0_SHAPE,
STAGE_1_SHAPE,
STAGE_1_SHAPE,
STAGE_1_SHAPE,
STAGE_2_SHAPE,
STAGE_2_SHAPE,
STAGE_2_SHAPE,
STAGE_3_SHAPE,
STAGE_4_SHAPE,
STAGE_5_SHAPE,
STAGE_6_SHAPE,
STAGE_7_SHAPE
)
}
}
@@ -92,7 +92,7 @@ class HealingMachineBlockEntity(
}
}
this.currentUser = user
this.healTimeLeft = 60
this.healTimeLeft = 24
markUpdated()
}
@@ -45,7 +45,6 @@ import net.minecraft.client.render.entity.EntityRenderer
import net.minecraft.client.render.entity.EntityRendererFactory
import net.minecraft.client.render.entity.LivingEntityRenderer
import net.minecraft.client.render.entity.model.PlayerEntityModel
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack
import net.minecraft.resource.ResourceManager
@@ -99,6 +98,7 @@ object CobblemonClient {
PlatformEvents.CLIENT_ITEM_TOOLTIP.subscribe { event ->
val stack = event.stack
val lines = event.lines
@Suppress("DEPRECATION")
if (stack.item.registryEntry.key.isPresent && stack.item.registryEntry.key.get().value.namespace == Cobblemon.MODID) {
if (stack.nbt?.getBoolean(DataKeys.HIDE_TOOLTIP) == true) {
return@subscribe
@@ -150,7 +150,7 @@ object CobblemonClient {
CobblemonBlocks.WHITE_APRICORN,
CobblemonBlocks.YELLOW_APRICORN,
CobblemonBlocks.HEALING_MACHINE,
CobblemonBlocks.MEDICINAL_LEEK_CROP,
CobblemonBlocks.MEDICINAL_LEEK,
CobblemonBlocks.HEALING_MACHINE,
CobblemonBlocks.RED_MINT,
CobblemonBlocks.BLUE_MINT,
@@ -163,6 +163,8 @@ object CobblemonClient {
CobblemonBlocks.BIG_ROOT,
CobblemonBlocks.REVIVAL_HERB,
CobblemonBlocks.VIVICHOKE_SEEDS,
CobblemonBlocks.PEP_UP_FLOWER,
CobblemonBlocks.POTTED_PEP_UP_FLOWER
)
}
@@ -174,10 +176,11 @@ object CobblemonClient {
}
}
@Suppress("UNCHECKED_CAST")
fun onAddLayer(skinMap: Map<String, EntityRenderer<out PlayerEntity>>?) {
var renderer: LivingEntityRenderer<PlayerEntity, PlayerEntityModel<PlayerEntity>>? = skinMap?.get("default") as LivingEntityRenderer<PlayerEntity, PlayerEntityModel<PlayerEntity>>
renderer?.addFeature(PokemonOnShoulderRenderer(renderer))
renderer = skinMap.get("slim") as LivingEntityRenderer<PlayerEntity, PlayerEntityModel<PlayerEntity>>?
renderer = skinMap["slim"] as LivingEntityRenderer<PlayerEntity, PlayerEntityModel<PlayerEntity>>?
renderer?.addFeature(PokemonOnShoulderRenderer(renderer))
}
@@ -56,25 +56,36 @@ class BattleSwitchPokemonSelection(
) {
fun isHovered(mouseX: Double, mouseY: Double) = mouseX in x..(x + SWITCH_TILE_WIDTH) && mouseY in (y..(y + SWITCH_TILE_HEIGHT))
fun render(context: DrawContext, mouseX: Double, mouseY: Double, deltaTicks: Float) {
CobblemonClient.battleOverlay.drawBattleTile(
context = context,
x = x,
y = y,
reversed = false,
species = pokemon.species,
level = pokemon.level,
aspects = pokemon.aspects,
displayName = pokemon.getDisplayName(),
gender = pokemon.gender,
status = pokemon.status?.status,
maxHealth = pokemon.hp,
health = pokemon.currentHealth.toFloat(),
isFlatHealth = true,
state = null,
colour = null,
opacity = selection.opacity,
partialTicks = deltaTicks
)
val healthRatioSplits = showdownPokemon.condition.split(" ")[0].split("/")
try {
val (hp, maxHp) = if (healthRatioSplits.size == 1) {
0 to 0
} else {
healthRatioSplits[0].toInt() to pokemon.hp
}
CobblemonClient.battleOverlay.drawBattleTile(
context = context,
x = x,
y = y,
reversed = false,
species = pokemon.species,
level = pokemon.level,
aspects = pokemon.aspects,
displayName = pokemon.getDisplayName(),
gender = pokemon.gender,
status = pokemon.status?.status,
maxHealth = maxHp,
health = hp.toFloat(),
isFlatHealth = true,
state = null,
colour = null,
opacity = selection.opacity,
partialTicks = deltaTicks
)
} catch (exception: Exception) {
println(showdownPokemon.condition)
throw exception
}
}
}
@@ -89,7 +100,7 @@ class BattleSwitchPokemonSelection(
}
.filter { it.second.uuid !in battleGUI.actor!!.activePokemon.map { it.battlePokemon?.uuid } }
.filter { it.second.uuid !in switchingInPokemon }
.filter { it.second.currentHealth > 0 }
.filter { "fnt" !in it.first.condition || it.first.reviving }
showdownPokemonToPokemon.forEachIndexed { index, (showdownPokemon, pokemon) ->
val row = index / 2
@@ -8,22 +8,33 @@
package com.cobblemon.mod.common.client.gui.interact.moveselect
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.callback.MoveSelectDTO
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.client.gui.ExitButton
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.UUID
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.sound.SoundEvent
import net.minecraft.text.MutableText
import net.minecraft.text.Text
class MoveSelectConfiguration(
val title: MutableText,
val moves: List<MoveSelectDTO>,
val onCancel: (MoveSelectGUI) -> Unit,
val onBack: (MoveSelectGUI) -> Unit,
val onSelect: (MoveSelectGUI, MoveSelectDTO) -> Unit,
)
class MoveSelectGUI(
pokemon: Pokemon
val config: MoveSelectConfiguration
) : Screen(Text.translatable("cobblemon.ui.interact.moveselect")) {
companion object {
const val WIDTH = 122
@@ -32,19 +43,43 @@ class MoveSelectGUI(
private val baseBackgroundResource = cobblemonResource("textures/gui/interact/move_select.png")
}
private val moves = pokemon.moveSet.getMoves()
var closed = false
constructor(
title: MutableText,
moves: List<MoveSelectDTO>,
uuid: UUID
): this(
MoveSelectConfiguration(
title = title,
moves = moves,
onSelect = { gui, it ->
CobblemonNetwork.sendToServer(MoveSelectedPacket(uuid = uuid, moves.indexOf(it)))
gui.closeProperly()
},
onCancel = { CobblemonNetwork.sendToServer(MoveSelectCancelledPacket(uuid = uuid)) },
onBack = MoveSelectGUI::close
)
)
fun closeProperly() {
closed = true
close()
}
override fun init() {
val x = (width - WIDTH) / 2
val y = (height - HEIGHT) / 2
moves.forEachIndexed { index, move ->
config.moves.forEachIndexed { index, move ->
addDrawableChild(
MoveSlotButton(
x = x + 7,
y = y + ((MoveSlotButton.HEIGHT + 3) * index),
move = move,
enabled = shouldBeEnabled(move)
y = y + 7 + ((MoveSlotButton.HEIGHT + 3) * index),
move = move.moveTemplate,
pp = move.pp,
ppMax = move.ppMax,
enabled = move.enabled
) { onPress(move) }
)
}
@@ -56,7 +91,7 @@ class MoveSelectGUI(
pY = y + 115
) {
playSound(CobblemonSounds.GUI_CLICK)
MinecraftClient.getInstance().setScreen(null)
config.onBack(this)
}
)
@@ -80,13 +115,23 @@ class MoveSelectGUI(
super.render(context, mouseX, mouseY, partialTicks)
}
private fun onPress(move: Move) {
private fun onPress(move: MoveSelectDTO) {
if (!move.enabled) {
return
}
playSound(CobblemonSounds.GUI_CLICK)
config.onSelect(this, move)
}
private fun shouldBeEnabled(move: Move): Boolean {
return true
override fun close() {
if (!closed) {
config.onCancel(this)
}
super.close()
}
override fun shouldCloseOnEsc() = true
fun playSound(soundEvent: SoundEvent) {
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(soundEvent, 1.0F))
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.gui.interact.moveselect
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.Move
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.api.text.gold
@@ -23,29 +24,35 @@ import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.toRGB
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.ButtonWidget
import net.minecraft.client.sound.SoundManager
import net.minecraft.text.Text
import net.minecraft.util.math.MathHelper
class MoveSlotButton(
x: Int, y: Int,
val move: Move,
val move: MoveTemplate,
val pp: Int,
val ppMax: Int,
val enabled: Boolean = true,
onPress: PressAction
) : ButtonWidget(x, y, WIDTH, HEIGHT, Text.literal("Move"), onPress, NarrationSupplier { "".text() }) {
companion object {
private val moveResource = cobblemonResource("ui/summary/summary_move.png")
private val moveOverlayResource = cobblemonResource("ui/summary/summary_move_overlay.png")
private val moveResource = cobblemonResource("textures/gui/summary/summary_move.png")
private val moveOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay.png")
const val WIDTH = 108
const val HEIGHT = 22
}
override fun render(context: DrawContext, pMouseX: Int, pMouseY: Int, pPartialTicks: Float) {
hovered = pMouseX >= x && pMouseY >= y && pMouseX < x + width && pMouseY < y + height
hovered = pMouseX >= x && pMouseY >= y && pMouseX < x + width && pMouseY < y + height && enabled
val moveTemplate = Moves.getByNameOrDummy(move.name)
val rgb = moveTemplate.elementalType.hue.toRGB()
val alpha = if (enabled) 1.0 else 0.5
val matrices = context.matrices
blitk(
matrixStack = matrices,
@@ -58,7 +65,8 @@ class MoveSlotButton(
textureHeight = HEIGHT * 2,
red = rgb.first,
green = rgb.second,
blue = rgb.third
blue = rgb.third,
alpha = alpha
)
blitk(
@@ -67,24 +75,27 @@ class MoveSlotButton(
x = x,
y = y,
width = WIDTH,
height = HEIGHT
height = HEIGHT,
alpha = alpha
)
var movePPText = Text.literal("${move.currentPp}/${move.maxPp}").bold()
if (pp != -1 && ppMax != -1) {
var movePPText = Text.literal("$pp/$ppMax").bold()
if (move.currentPp <= MathHelper.floor(move.maxPp / 2F)) {
movePPText = if (move.currentPp == 0) movePPText.red() else movePPText.gold()
if (pp <= MathHelper.floor(ppMax / 2F)) {
movePPText = if (pp == 0) movePPText.red() else movePPText.gold()
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = movePPText,
x = x + 93,
y = y + 13,
centered = true
)
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = movePPText,
x = x + 93,
y = y + 13,
centered = true
)
// Type Icon
TypeIcon(
x = x + 2,
@@ -109,4 +120,6 @@ class MoveSlotButton(
shadow = true
)
}
override fun playDownSound(soundManager: SoundManager) {}
}
@@ -0,0 +1,144 @@
/*
* 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.interact.partyselect
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.callback.MoveSelectDTO
import com.cobblemon.mod.common.api.callback.PartySelectPokemonDTO
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.client.gui.ExitButton
import com.cobblemon.mod.common.client.gui.interact.moveselect.MoveSelectGUI
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.callback.party.PartyPokemonSelectedPacket
import com.cobblemon.mod.common.net.messages.server.callback.party.PartySelectCancelledPacket
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.UUID
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.sound.SoundEvent
import net.minecraft.text.MutableText
import net.minecraft.text.Text
class PartySelectConfiguration(
val title: MutableText,
val pokemon: List<PartySelectPokemonDTO>,
val onCancel: (PartySelectGUI) -> Unit,
val onBack: (PartySelectGUI) -> Unit,
val onSelect: (PartySelectGUI, PartySelectPokemonDTO) -> Unit,
)
class PartySelectGUI(
val config: PartySelectConfiguration
) : Screen(Text.translatable("cobblemon.ui.interact.moveselect")) {
companion object {
const val WIDTH = 102
const val HEIGHT = 115
private val baseBackgroundResource = cobblemonResource("textures/gui/interact/party_select.png")
}
var closed = false
constructor(
title: MutableText,
pokemon: List<PartySelectPokemonDTO>,
uuid: UUID
): this(
PartySelectConfiguration(
title = title,
pokemon = pokemon,
onSelect = { gui, it ->
CobblemonNetwork.sendToServer(PartyPokemonSelectedPacket(uuid = uuid, pokemon.indexOf(it)))
gui.closeProperly()
},
onCancel = { CobblemonNetwork.sendToServer(PartySelectCancelledPacket(uuid = uuid)) },
onBack = PartySelectGUI::close
)
)
fun closeProperly() {
closed = true
close()
}
override fun init() {
val x = (width - WIDTH) / 2
val y = (height - HEIGHT) / 2
config.pokemon.forEachIndexed { index, pokemon ->
val slotX = x + 5 + (index % 2) * (PartySlotButton.WIDTH + 2)
val slotY = y + 5 + (index / 2) * (PartySlotButton.HEIGHT + 2)
addDrawableChild(
PartySlotButton(
parent = this,
x = slotX,
y = slotY,
pokemon = pokemon.pokemonProperties,
heldItem = pokemon.heldItem,
hpRatio = pokemon.hpRatio,
enabled = pokemon.enabled
) { onPress(pokemon) }
)
}
// Add Exit Button
addDrawableChild(
ExitButton(
pX = x + 72,
pY = y + 96
) {
playSound(CobblemonSounds.GUI_CLICK)
config.onBack(this)
}
)
super.init()
}
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, partialTicks: Float) {
val x = (width - WIDTH) / 2
val y = (height - HEIGHT) / 2
blitk(
matrixStack = context.matrices,
texture = baseBackgroundResource,
x = x,
y = y,
width = WIDTH,
height = HEIGHT
)
// Render all added Widgets
super.render(context, mouseX, mouseY, partialTicks)
}
private fun onPress(pokemon: PartySelectPokemonDTO) {
if (!pokemon.enabled) {
return
}
playSound(CobblemonSounds.GUI_CLICK)
config.onSelect(this, pokemon)
}
override fun close() {
if (!closed) {
config.onCancel(this)
}
super.close()
}
override fun shouldCloseOnEsc() = true
fun playSound(soundEvent: SoundEvent) {
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(soundEvent, 1.0F))
}
}
@@ -0,0 +1,182 @@
/*
* 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.interact.partyselect
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.summary.widgets.PartySlotWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.getDepletableRedGreen
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonFloatingState
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.ButtonWidget
import net.minecraft.client.gui.widget.ButtonWidget.NarrationSupplier
import net.minecraft.client.sound.SoundManager
import net.minecraft.item.ItemStack
import net.minecraft.text.Text
import org.joml.Quaternionf
import org.joml.Vector3f
class PartySlotButton(
x: Int, y: Int,
val pokemon: PokemonProperties,
val hpRatio: Float,
val heldItem: ItemStack,
val enabled: Boolean = true,
val parent: PartySelectGUI,
onPress: PressAction
) : ButtonWidget(x, y, WIDTH, HEIGHT, Text.literal("Pokemon"), onPress, NarrationSupplier { "".text() }) {
companion object {
private val slotResource = cobblemonResource("textures/gui/summary/summary_party_slot.png")
private val slotFaintedResource = cobblemonResource("textures/gui/summary/summary_party_slot_fainted.png")
private val slotEmptyResource = cobblemonResource("textures/gui/summary/summary_party_slot_empty.png")
const val WIDTH = 46
const val HEIGHT = 27
}
var animationSeconds = 0F
var lastTime = System.currentTimeMillis()
val state = PokemonFloatingState()
private val renderablePokemon = pokemon.asRenderablePokemon()
override fun render(context: DrawContext, pMouseX: Int, pMouseY: Int, pPartialTicks: Float) {
hovered = pMouseX >= x && pMouseY >= y && pMouseX < x + width && pMouseY < y + height && enabled
val alpha = if (enabled) 1.0 else 0.5
val matrices = context.matrices
blitk(
matrixStack = matrices,
texture = if (hpRatio <= 0F) slotFaintedResource else slotResource,
x = x,
y = y,
width = width,
height = height,
vOffset = if (hovered) height else 0,
textureHeight = height * 2,
alpha = alpha
)
context.matrices.push()
context.matrices.translate(x.toDouble() + 13, y.toDouble() - 2, 0.0)
// if (!hovered) {
// state.reset()
// }
drawProfilePokemon(
renderablePokemon = renderablePokemon,
matrixStack = context.matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
scale = 10F,
partialTicks = if (!hovered) 0F else pPartialTicks
)
context.matrices.pop()
val halfScale = 0.5F
val ballIcon = cobblemonResource("textures/gui/ball/" + pokemon.pokeball!!.asIdentifierDefaultingNamespace().path + ".png")
val ballHeight = 22
blitk(
matrixStack = matrices,
texture = ballIcon,
x = (x - 2) / halfScale,
y = (y - 3) / halfScale,
height = ballHeight,
width = 18,
textureHeight = ballHeight * 2,
scale = halfScale
)
val status = pokemon.status
if (hpRatio > 0F && status != null) {
blitk(
matrixStack = matrices,
texture = cobblemonResource("textures/gui/party/status_$status.png"),
x = x + 42,
y = y + 5,
height = 14,
width = 4
)
}
val barWidthMax = 37
val barWidth = hpRatio * barWidthMax
val (red, green) = getDepletableRedGreen(hpRatio)
blitk(
matrixStack = matrices,
texture = CobblemonResources.WHITE,
x = x + 4,
y = y + 25,
width = barWidth,
height = 1,
textureWidth = barWidth / hpRatio,
uOffset = barWidthMax - barWidth,
red = red * 0.8F,
green = green * 0.8F,
blue = 0.27F
)
if ("male" in pokemon.aspects || "female" in pokemon.aspects) {
blitk(
matrixStack = matrices,
texture = if ("male" in pokemon.aspects) PartySlotWidget.genderIconMale else PartySlotWidget.genderIconFemale,
x = (x + 40) / halfScale,
y = (y + 20) / halfScale,
height = 7,
width = 5,
scale = halfScale
)
}
drawScaledText(
context = context,
text = lang("ui.lv.number", pokemon.level!!),
x = x + 31,
y = y + 13,
centered = true,
shadow = true,
scale = halfScale
)
// Held Item
if (!heldItem.isEmpty) {
renderScaledGuiItemIcon(
itemStack = heldItem,
x = x + 14.0,
y = y + 9.5,
scale = 0.5,
matrixStack = matrices
)
}
// Pokémon Name
val displayName = pokemon.nickname ?: renderablePokemon.species.translatedName
drawScaledText(
context = context,
text = displayName.copy(),
x = x + 4,
y = y + HEIGHT - 7,
scale = halfScale
)
}
override fun playDownSound(soundManager: SoundManager) {}
}
@@ -48,7 +48,7 @@ class NavigationButton(
}
override fun playDownSound(soundManager: SoundManager) {
soundManager.play(PositionedSoundInstance.master(CobblemonSounds.GUI_CLICK, 1.0F))
soundManager.play(PositionedSoundInstance.master(CobblemonSounds.PC_CLICK, 1.0F))
}
fun isHovered(mouseX: Double, mouseY: Double) = mouseX.toFloat() in (x.toFloat()..(x.toFloat() + (WIDTH * SCALE))) && mouseY.toFloat() in (y.toFloat()..(y.toFloat() + (HEIGHT * SCALE)))
@@ -98,7 +98,7 @@ class StorageWidget(
onPress = {
if (!displayConfirmRelease) {
displayConfirmRelease = true
playSound(CobblemonSounds.GUI_CLICK)
playSound(CobblemonSounds.PC_CLICK)
}
}
)
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.gui.startselection
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.ColourLibrary
import com.cobblemon.mod.common.api.gui.MultiLineLabelK
import com.cobblemon.mod.common.api.gui.blitk
@@ -35,6 +36,7 @@ import com.cobblemon.mod.common.util.math.toRGB
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
@@ -184,6 +186,7 @@ class StarterSelectionScreen private constructor(): Screen("cobblemon.ui.starter
pXTexStart = 0, pYTexStart = 0, pYDiffText = 0
) {
MinecraftClient.getInstance().setScreen(null)
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(CobblemonSounds.GUI_CLICK, 1.0F))
}
)
}
@@ -281,6 +284,7 @@ class StarterSelectionScreen private constructor(): Screen("cobblemon.ui.starter
}
private fun right() {
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(CobblemonSounds.GUI_CLICK, 1.0F))
currentSelection = rightOfCurrentSelection()
updateSelection()
}
@@ -288,6 +292,7 @@ class StarterSelectionScreen private constructor(): Screen("cobblemon.ui.starter
private fun rightOfCurrentSelection() : Int = if (currentSelection + 1 <= currentCategory.pokemon.size - 1) currentSelection + 1 else 0
private fun left() {
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(CobblemonSounds.GUI_CLICK, 1.0F))
currentSelection = leftOfCurrentSelection()
updateSelection()
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.client.gui.startselection.widgets
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.client.gui.startselection.StarterSelectionScreen
import com.cobblemon.mod.common.client.render.drawScaledText
@@ -16,6 +17,7 @@ import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
@@ -131,6 +133,7 @@ class CategoryList(
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
starterSelectionScreen.changeCategory(category = category)
minecraft.soundManager.play(PositionedSoundInstance.master(CobblemonSounds.GUI_CLICK, 1.0F))
return true
}
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.TexturedButtonWidget
import net.minecraft.client.sound.SoundManager
import net.minecraft.client.util.math.MatrixStack
class ExitButton(
pX: Int, pY: Int,
@@ -38,4 +39,6 @@ class ExitButton(
}
}
override fun playDownSound(soundManager: SoundManager) { }
}
@@ -8,10 +8,14 @@
package com.cobblemon.mod.common.client.gui.startselection.widgets.preview
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.TexturedButtonWidget
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.client.sound.SoundManager
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.util.Identifier
class ArrowButton(
@@ -32,6 +36,10 @@ class ArrowButton(
private const val ARROW_BUTTON_HEIGHT = 14f
}
override fun playDownSound(soundManager: SoundManager?) {
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(CobblemonSounds.GUI_CLICK, 1.0F))
}
override fun renderButton(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
hovered = mouseX >= x && mouseY >= y && mouseX < x + width && mouseY < y + height
if (isHovered) {
@@ -8,13 +8,17 @@
package com.cobblemon.mod.common.client.gui.startselection.widgets.preview
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.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.ButtonWidget
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.client.sound.SoundManager
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
@@ -31,6 +35,10 @@ class SelectionButton(
private const val SCALE = 0.7f
}
override fun playDownSound(soundManager: SoundManager?) {
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(CobblemonSounds.GUI_CLICK, 1.0F))
}
override fun renderButton(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.matrices
if (isHovered)
@@ -21,6 +21,8 @@ import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.systems.RenderSystem
import kotlin.math.cos
import kotlin.math.sin
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.render.BufferRenderer
@@ -97,6 +99,7 @@ class StatWidget(
bufferBuilder.vertex(v2.x.toDouble(), v2.y.toDouble(), 10.0).next()
bufferBuilder.vertex(v3.x.toDouble(), v3.y.toDouble(), 10.0).next()
BufferRenderer.drawWithGlobalProgram(bufferBuilder.end())
RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
}
private fun drawStatHexagon(stats: Map<Stat, Int>, colour: Vector3f, maximum: Int) {
@@ -108,45 +111,49 @@ class StatWidget(
val hexRightX = x + 108.5
val hexCenterX = (hexLeftX + hexRightX) / 2
val hexCenterY = (hexTopY + hexBottomY) / 2
val minTriangleSize = 8F
val minXTriangleLen = sin(Math.toRadians(61.0)).toFloat() * minTriangleSize * 0.95F
val minYTriangleLen = cos(Math.toRadians(60.0)).toFloat() * minTriangleSize
val triangleWidth = (hexCenterX - hexLeftX).toFloat()
val triangleHeight = ((hexBottomY - hexTopY) / 2).toFloat()
val triangleLongEdge = (hexCenterY - hexTopY - minTriangleSize).toFloat()
val triangleMediumEdge = (triangleLongEdge * sin(Math.toRadians(61.0))).toFloat()
val triangleShortEdge = (triangleLongEdge * cos(Math.toRadians(61.0))).toFloat()
val hpRatio = (stats.getOrDefault(Stats.HP, 0).toFloat() / maximum).coerceIn(0.075F, 1F)
val atkRatio = (stats.getOrDefault(Stats.ATTACK, 0).toFloat() / maximum).coerceIn(0.075F, 1F)
val defRatio = (stats.getOrDefault(Stats.DEFENCE, 0).toFloat() / maximum).coerceIn(0.075F, 1F)
val spAtkRatio = (stats.getOrDefault(Stats.SPECIAL_ATTACK, 0).toFloat() / maximum).coerceIn(0.075F, 1F)
val spDefRatio = (stats.getOrDefault(Stats.SPECIAL_DEFENCE, 0).toFloat() / maximum).coerceIn(0.075F, 1F)
val spdRatio = (stats.getOrDefault(Stats.SPEED, 0).toFloat() / maximum).coerceIn(0.075F, 1F)
val hpRatio = (stats.getOrDefault(Stats.HP, 0).toFloat() / maximum).coerceIn(0F, 1F)
val atkRatio = (stats.getOrDefault(Stats.ATTACK, 0).toFloat() / maximum).coerceIn(0F, 1F)
val defRatio = (stats.getOrDefault(Stats.DEFENCE, 0).toFloat() / maximum).coerceIn(0F, 1F)
val spAtkRatio = (stats.getOrDefault(Stats.SPECIAL_ATTACK, 0).toFloat() / maximum).coerceIn(0F, 1F)
val spDefRatio = (stats.getOrDefault(Stats.SPECIAL_DEFENCE, 0).toFloat() / maximum).coerceIn(0F, 1F)
val spdRatio = (stats.getOrDefault(Stats.SPEED, 0).toFloat() / maximum).coerceIn(0F, 1F)
val hpPoint = Vec2f(
hexCenterX.toFloat(),
hexCenterY.toFloat() - hpRatio * triangleHeight
hexCenterY.toFloat() - minTriangleSize - hpRatio * triangleLongEdge
)
val attackPoint = Vec2f(
hexCenterX.toFloat() + atkRatio * triangleWidth,
hexCenterY.toFloat() - atkRatio * triangleHeight / 2
hexCenterX.toFloat() + minXTriangleLen + atkRatio * triangleMediumEdge,
hexCenterY.toFloat() - minYTriangleLen - atkRatio * triangleShortEdge
)
val defencePoint = Vec2f(
hexCenterX.toFloat() + defRatio * triangleWidth,
hexCenterY.toFloat() + defRatio * triangleHeight / 2
hexCenterX.toFloat() + minXTriangleLen + defRatio * triangleMediumEdge,
hexCenterY.toFloat() + minYTriangleLen + defRatio * triangleShortEdge
)
val specialAttackPoint = Vec2f(
hexCenterX.toFloat() - spAtkRatio * triangleWidth,
hexCenterY.toFloat() - spAtkRatio * triangleHeight / 2
hexCenterX.toFloat() - minXTriangleLen - spAtkRatio * triangleMediumEdge,
hexCenterY.toFloat() - minYTriangleLen - spAtkRatio * triangleShortEdge
)
val specialDefencePoint = Vec2f(
hexCenterX.toFloat() - spDefRatio * triangleWidth,
hexCenterY.toFloat() + spDefRatio * triangleHeight / 2
hexCenterX.toFloat() - minXTriangleLen - spDefRatio * triangleMediumEdge,
hexCenterY.toFloat() + minYTriangleLen + spDefRatio * triangleShortEdge
)
val speedPoint = Vec2f(
hexCenterX.toFloat(),
hexCenterY.toFloat() + spdRatio * triangleHeight
hexCenterY.toFloat() + minTriangleSize + spdRatio * triangleLongEdge
)
val centerPoint = Vec2f(
@@ -521,4 +528,4 @@ class StatWidget(
}
return -1
}
}
}
@@ -9,17 +9,17 @@
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.battles.BallActionResponse
import com.cobblemon.mod.common.battles.ForcePassActionResponse
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.net.messages.client.battle.BattleApplyCaptureResponsePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleApplyPassResponsePacket
import net.minecraft.client.MinecraftClient
object BattleApplyCaptureResponseHandler : ClientNetworkPacketHandler<BattleApplyCaptureResponsePacket> {
override fun handle(packet: BattleApplyCaptureResponsePacket, client: MinecraftClient) {
object BattleApplyPassResponseHandler : ClientNetworkPacketHandler<BattleApplyPassResponsePacket> {
override fun handle(packet: BattleApplyPassResponsePacket, client: MinecraftClient) {
val battle = CobblemonClient.battle ?: return
val req = battle.pendingActionRequests.firstOrNull { it.response == null } ?: return
val res = BallActionResponse()
val res = ForcePassActionResponse()
val gui = MinecraftClient.getInstance().currentScreen
if (gui is BattleGUI) {
gui.selectAction(req, res)
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.callback.move
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.interact.moveselect.MoveSelectGUI
import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPacket
import net.minecraft.client.MinecraftClient
object OpenMoveCallbackHandler : ClientNetworkPacketHandler<OpenMoveCallbackPacket> {
override fun handle(packet: OpenMoveCallbackPacket, client: MinecraftClient) {
client.setScreen(MoveSelectGUI(packet.title, packet.moves, packet.uuid))
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.callback.party
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.interact.partyselect.PartySelectGUI
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
import net.minecraft.client.MinecraftClient
object OpenPartyCallbackHandler : ClientNetworkPacketHandler<OpenPartyCallbackPacket> {
override fun handle(packet: OpenPartyCallbackPacket, client: MinecraftClient) {
client.setScreen(
PartySelectGUI(
title = packet.title,
pokemon = packet.pokemon,
// usePortraits = packet.usePortraits,
// animate = packet.animate,
uuid = packet.uuid
)
)
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.callback.partymove
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.callback.PartySelectPokemonDTO
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.interact.moveselect.MoveSelectConfiguration
import com.cobblemon.mod.common.client.gui.interact.moveselect.MoveSelectGUI
import com.cobblemon.mod.common.client.gui.interact.partyselect.PartySelectConfiguration
import com.cobblemon.mod.common.client.gui.interact.partyselect.PartySelectGUI
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallbackPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyMoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyPokemonMoveSelectedPacket
import net.minecraft.client.MinecraftClient
object OpenPartyMoveCallbackHandler : ClientNetworkPacketHandler<OpenPartyMoveCallbackPacket> {
override fun handle(packet: OpenPartyMoveCallbackPacket, client: MinecraftClient) {
val pokemonToMoves = packet.pokemonList.toMap()
val cancel: (Any) -> Unit = {
CobblemonNetwork.sendToServer(PartyMoveSelectCancelledPacket(uuid = packet.uuid))
if (it is MoveSelectGUI) {
it.closeProperly()
} else if (it is PartySelectGUI) {
it.closeProperly()
}
}
lateinit var partySelectConfiguration: PartySelectConfiguration
fun makeMoveSelectConfiguration(pokemonSelectDTO: PartySelectPokemonDTO): MoveSelectConfiguration {
return MoveSelectConfiguration(
title = "".text(),
moves = pokemonToMoves[pokemonSelectDTO]!!,
onCancel = cancel,
onBack = { MinecraftClient.getInstance().setScreen(PartySelectGUI(partySelectConfiguration)) },
onSelect = { gui, moveSelectDTO ->
val pokemonIndex = packet.pokemonList.indexOfFirst { it.first == pokemonSelectDTO }
val moveIndex = pokemonToMoves[pokemonSelectDTO]!!.indexOf(moveSelectDTO)
CobblemonNetwork.sendToServer(PartyPokemonMoveSelectedPacket(packet.uuid, pokemonIndex, moveIndex))
gui.closeProperly()
}
)
}
partySelectConfiguration = PartySelectConfiguration(
title = "".text(),
pokemon = pokemonToMoves.keys.toList(),
onCancel = cancel,
onBack = cancel,
onSelect = { _, it -> MinecraftClient.getInstance().setScreen(MoveSelectGUI(makeMoveSelectConfiguration(it))) }
)
MinecraftClient.getInstance().setScreen(PartySelectGUI(partySelectConfiguration))
}
}
@@ -27,8 +27,8 @@ object ReloadShowdownCommand {
private fun execute(context: CommandContext<ServerCommandSource>): Int {
try {
ShowdownService.get().closeConnection()
ShowdownService.get().openConnection()
ShowdownService.service.closeConnection()
ShowdownService.service.openConnection()
context.source.sendMessage(Text.of("Reloaded showdown"))
} catch (e: Exception) {
e.printStackTrace()
@@ -45,7 +45,7 @@ object TestPartySlotCommand {
val player = context.player(PLAYER)
val slot = IntegerArgumentType.getInteger(context, SLOT)
val properties = PokemonPropertiesArgumentType.getPokemonProperties(context, PROPERTIES)
return if (player.party().get(slot - 1)?.createPokemonProperties(*PokemonPropertyExtractor.ALL)
return if (player.party().get(slot - 1)?.createPokemonProperties(PokemonPropertyExtractor.ALL)
?.let { properties.isSubSetOf(it) } == true) Command.SINGLE_SUCCESS else NO_SUCCESS
}
}
@@ -51,7 +51,7 @@ object TestPcSlotCommand {
val boxNumber = IntegerArgumentType.getInteger(context, BOX)
val slot = IntegerArgumentType.getInteger(context, SLOT)
val properties = PokemonPropertiesArgumentType.getPokemonProperties(context, PROPERTIES)
return if (player.pc().boxes[boxNumber - 1][slot - 1]?.createPokemonProperties(*PokemonPropertyExtractor.ALL)
return if (player.pc().boxes[boxNumber - 1][slot - 1]?.createPokemonProperties(PokemonPropertyExtractor.ALL)
?.let { properties.isSubSetOf(it) } == true) Command.SINGLE_SUCCESS else NO_SUCCESS
}
}
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatureAssignments
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.SpawnDetailPresets
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.net.messages.client.data.UnlockReloadPacket
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokemon.SpeciesAdditions
@@ -59,6 +60,7 @@ object CobblemonDataProvider : DataProvider {
this.register(PropertiesCompletionProvider)
this.register(SpawnDetailPresets)
this.register(CobblemonMechanics)
this.register(BagItems)
CobblemonSpawnPools.load()
@@ -29,12 +29,13 @@ import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.BattleCaptureAction
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleTypes
import com.cobblemon.mod.common.battles.ForcePassActionResponse
import com.cobblemon.mod.common.client.entity.EmptyPokeBallClientDelegate
import com.cobblemon.mod.common.entity.EntityProperty
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.Poseable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.battle.BattleApplyCaptureResponsePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleApplyPassResponsePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleCaptureStartPacket
import com.cobblemon.mod.common.net.messages.client.spawn.SpawnPokeballPacket
import com.cobblemon.mod.common.pokeball.PokeBall
@@ -192,29 +193,23 @@ class EmptyPokeBallEntity : ThrownItemEntity, Poseable, WaterDragModifier {
return drop()
}
val request = throwerActor.request?.takeIf { throwerActor.mustChoose } ?: run {
val canFitForcedAction = throwerActor.canFitForcedAction()
if (!canFitForcedAction) {
owner.sendMessage(lang("capture.not_your_turn").red())
return drop()
}
val countMovable = (request.active?.count() ?: 0) - request.forceSwitch.count { it }
if (countMovable > throwerActor.expectingCaptureActions) {
throwerActor.expectingCaptureActions++
battle.captureActions.add(BattleCaptureAction(battle, hitBattlePokemon, this).also { it.attach() })
battle.broadcastChatMessage(
lang(
"capture.attempted_capture",
throwerActor.getName(),
pokeBall.item().name,
pokemonEntity.pokemon.species.translatedName
).yellow()
)
battle.sendUpdate(BattleCaptureStartPacket(pokeBall.name, aspects.get(), hitBattlePokemon.getPNX()))
throwerActor.sendUpdate(BattleApplyCaptureResponsePacket())
} else {
owner.sendMessage(lang("capture.not_your_turn").red())
return drop()
}
battle.captureActions.add(BattleCaptureAction(battle, hitBattlePokemon, this).also { it.attach() })
battle.broadcastChatMessage(
lang(
"capture.attempted_capture",
throwerActor.getName(),
pokeBall.item().name,
pokemonEntity.pokemon.species.translatedName
).yellow()
)
battle.sendUpdate(BattleCaptureStartPacket(pokeBall.name, aspects.get(), hitBattlePokemon.getPNX()))
throwerActor.forceChoose(ForcePassActionResponse())
} else if (pokemonEntity.isBusy) {
owner?.sendMessage(lang("capture.busy", pokemonEntity.pokemon.species.translatedName).red())
return drop()
@@ -29,6 +29,7 @@ import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.scheduling.afterOnMain
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.api.types.ElementalTypes.FIRE
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.block.entity.PokemonPastureBlockEntity
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
@@ -620,36 +621,53 @@ class PokemonEntity(
override fun getMinAmbientSoundDelay() = Cobblemon.config.ambientPokemonCryTicks
private fun attemptItemInteraction(player: PlayerEntity, stack: ItemStack): Boolean {
if (stack.isEmpty) {
return false
}
if (player is ServerPlayerEntity && isBattling) {
val battle = battleId.get().orElse(null)?.let(BattleRegistry::getBattle) ?: return false
val bagItemConvertible = BagItems.getConvertibleForStack(stack) ?: return false
val battlePokemon = battle.actors.flatMap { it.pokemonList }.find { it.effectedPokemon.uuid == pokemon.uuid } ?: return false // Shouldn't be possible but anyway
if (battlePokemon.actor.getSide().actors.none { it.isForPlayer(player)}) {
return true
}
return bagItemConvertible.handleInteraction(player, battlePokemon, stack)
}
if (player !is ServerPlayerEntity || this.isBusy) {
return false
}
if (!stack.isEmpty) {
if (pokemon.getOwnerPlayer() == player) {
val context = ItemInteractionEvolution.ItemInteractionContext(stack.item, player.world)
pokemon.evolutions
.filterIsInstance<ItemInteractionEvolution>()
.forEach { evolution ->
if (evolution.attemptEvolution(pokemon, context)) {
if (!player.isCreative) {
stack.decrement(1)
}
this.world.playSoundServer(position = this.pos, sound = CobblemonSounds.ITEM_USE, volume = 1F, pitch = 1F)
return true
// Check evolution item interaction
if (pokemon.getOwnerPlayer() == player) {
val context = ItemInteractionEvolution.ItemInteractionContext(stack.item, player.world)
pokemon.evolutions
.filterIsInstance<ItemInteractionEvolution>()
.forEach { evolution ->
if (evolution.attemptEvolution(pokemon, context)) {
if (!player.isCreative) {
stack.decrement(1)
}
this.world.playSoundServer(position = this.pos, sound = CobblemonSounds.ITEM_USE, volume = 1F, pitch = 1F)
return true
}
}
(stack.item as? PokemonInteractiveItem)?.let {
if (it.onInteraction(player, this, stack)) {
it.getSound()?.let {
this.world.playSoundServer(
position = this.pos,
sound = it,
volume = 1F,
pitch = 1F
)
}
return true
}
}
(stack.item as? PokemonInteractiveItem)?.let {
if (it.onInteraction(player, this, stack)) {
it.getSound()?.let {
this.world.playSoundServer(
position = this.pos,
sound = it,
volume = 1F,
pitch = 1F
)
}
return true
}
}
return false
@@ -900,7 +918,7 @@ class PokemonEntity(
feature.enabled = true
this.pokemon.markFeatureDirty(feature)
this.pokemon.updateAspects()
val i = this.random.nextInt(3)
val i = this.random.nextInt(3) + 1
for (j in 0 until i) {
val itemEntity = this.dropItem(Items.WHITE_WOOL, 1) ?: return
itemEntity.velocity = itemEntity.velocity.add(
@@ -18,6 +18,7 @@ import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.activestate.ActivePokemonState
import com.cobblemon.mod.common.pokemon.activestate.SentOutState
import com.cobblemon.mod.common.util.playSoundServer
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import java.util.Optional
import net.minecraft.entity.Entity
import net.minecraft.entity.ai.pathing.PathNodeType
@@ -202,7 +203,9 @@ class PokemonServerDelegate : PokemonSideDelegate {
if (entity.deathTime == 60) {
if (entity.owner == null) {
entity.world.sendEntityStatus(entity, 60.toByte()) // Sends smoke effect
(entity.drops ?: entity.pokemon.form.drops).drop(entity, entity.world as ServerWorld, entity.pos, entity.killer)
if(entity.world.gameRules.getBoolean(CobblemonGameRules.DO_POKEMON_LOOT)) {
(entity.drops ?: entity.pokemon.form.drops).drop(entity, entity.world as ServerWorld, entity.pos, entity.killer)
}
}
entity.remove(Entity.RemovalReason.KILLED)
@@ -9,32 +9,34 @@
package com.cobblemon.mod.common.item
import com.cobblemon.mod.common.block.MedicinalLeekBlock
import net.minecraft.block.*
import net.minecraft.block.ComposterBlock
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.AliasedBlockItem
import net.minecraft.item.ItemStack
import net.minecraft.item.ItemUsageContext
import net.minecraft.item.PlaceableOnWaterItem
import net.minecraft.item.*
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.TypedActionResult
import net.minecraft.world.RaycastContext
import net.minecraft.world.World
class MedicinalLeekItem(block: MedicinalLeekBlock) : AliasedBlockItem(block, Settings()) {
class MedicinalLeekItem(block: MedicinalLeekBlock, settings: Settings) : AliasedBlockItem(block, settings) {
init {
ComposterBlock.ITEM_TO_LEVEL_INCREASE_CHANCE[this] = .65F
}
override fun use(world: World?, user: PlayerEntity, hand: Hand?): TypedActionResult<ItemStack?>? {
override fun useOnBlock(context: ItemUsageContext): ActionResult = ActionResult.PASS
override fun use(world: World, user: PlayerEntity, hand: Hand): TypedActionResult<ItemStack> {
val blockHitResult = PlaceableOnWaterItem.raycast(world, user, RaycastContext.FluidHandling.SOURCE_ONLY)
val blockHitResult2 = blockHitResult.withBlockPos(blockHitResult.blockPos.up())
val actionResult: ActionResult = super.useOnBlock(ItemUsageContext(user, hand, blockHitResult2))
return TypedActionResult(actionResult, user.getStackInHand(hand))
val placeResult = this.place(ItemPlacementContext(ItemUsageContext(user, hand, blockHitResult2)))
val stack = user.getStackInHand(hand)
// This will always be true but just in case we change it down the line.
if (!placeResult.isAccepted && this.isFood && user.canConsume(this.foodComponent?.isAlwaysEdible == true)) {
user.setCurrentHand(hand)
return TypedActionResult.consume(stack)
}
return TypedActionResult(placeResult, stack)
}
override fun useOnBlock(context: ItemUsageContext?): ActionResult? {
return ActionResult.PASS
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.item
import com.cobblemon.mod.common.api.mulch.Mulchable
import com.cobblemon.mod.common.api.mulch.MulchVariant
import net.minecraft.item.ItemStack
import net.minecraft.item.ItemUsageContext
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.ActionResult
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
import net.minecraft.world.WorldEvents
class MulchItem(val variant: MulchVariant) : CobblemonItem(Settings()) {
override fun useOnBlock(context: ItemUsageContext): ActionResult {
val world = context.world
val pos = context.blockPos
if (this.useOnMulchAble(context.stack, world, pos)) {
// Plays bone meal effect
if (!world.isClient) {
world.syncWorldEvent(WorldEvents.BONE_MEAL_USED, pos, 0)
}
return ActionResult.success(world.isClient)
}
return ActionResult.PASS
}
private fun useOnMulchAble(stack: ItemStack, world: World, pos: BlockPos): Boolean {
val state = world.getBlockState(pos)
if (state.block is Mulchable) {
val mulchAble = state.block as? Mulchable ?: return false
if (world is ServerWorld && mulchAble.canHaveMulchApplied(world, pos, state, this.variant)) {
mulchAble.applyMulch(world, world.random, pos, state, this.variant)
stack.decrement(1)
return true
}
}
return false
}
}
@@ -8,17 +8,76 @@
package com.cobblemon.mod.common.item
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.CobblemonMechanics
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.item.PokemonSelectingItem
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.block.RevivalHerbBlock
import com.cobblemon.mod.common.item.battle.BagItem
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.ceil
import net.minecraft.block.ComposterBlock
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.AliasedBlockItem
import net.minecraft.item.ItemStack
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.sound.SoundCategory
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.TypedActionResult
import net.minecraft.world.World
class RevivalHerbItem(block: RevivalHerbBlock) : AliasedBlockItem(block, Settings()) {
class RevivalHerbItem(block: RevivalHerbBlock) : AliasedBlockItem(block, Settings()), PokemonSelectingItem {
init {
// 65% to raise composter level
ComposterBlock.ITEM_TO_LEVEL_INCREASE_CHANCE[this] = .65F
}
private val runtime = MoLangRuntime()
override val bagItem = object : BagItem {
override val itemName = "item.cobblemon.revival_herb"
override fun canUse(battle: PokemonBattle, target: BattlePokemon) = target.health <= 0
override fun getShowdownInput(actor: BattleActor, battlePokemon: BattlePokemon, data: String?): String {
battlePokemon.effectedPokemon.decrementFriendship(CobblemonMechanics.remedies.getFriendshipDrop(runtime))
return "revive 0.25"
}
}
override fun use(world: World, user: PlayerEntity, hand: Hand): TypedActionResult<ItemStack> {
return super.use(world, user, hand)
if (user is ServerPlayerEntity) {
val result = use(user, user.getStackInHand(hand))
if (result.result != ActionResult.PASS) {
return result
}
}
return super<AliasedBlockItem>.use(world, user, hand)
}
override fun canUseOnPokemon(pokemon: Pokemon) = pokemon.currentHealth <= 0
override fun applyToPokemon(
player: ServerPlayerEntity,
stack: ItemStack,
pokemon: Pokemon
): TypedActionResult<ItemStack>? {
if (pokemon.currentHealth <= 0) {
player.playSound(CobblemonSounds.MEDICINE_HERB_USE, SoundCategory.PLAYERS, 1F, 1F)
pokemon.currentHealth = ceil(pokemon.hp / 4F).toInt()
if (!player.isCreative) {
stack.decrement(1)
}
return TypedActionResult.success(stack)
} else {
return TypedActionResult.pass(stack)
}
}
override fun applyToBattlePokemon(player: ServerPlayerEntity, stack: ItemStack, battlePokemon: BattlePokemon) {
super.applyToBattlePokemon(player, stack, battlePokemon)
player.playSound(CobblemonSounds.MEDICINE_HERB_USE, SoundCategory.PLAYERS, 1F, 1F)
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.item.battle
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import net.minecraft.item.ItemStack
import net.minecraft.server.network.ServerPlayerEntity
/**
* A bag item effect that links to a script in Showdown. Implementations must be added to
* [BagItems.bagItems].
*
* @author Hiroku
* @since June 26th, 2023
*/
interface BagItem {
companion object {
val EMPTY: BagItem = object : BagItem {
override val itemName = "name"
override fun canUse(battle: PokemonBattle, target: BattlePokemon) = true
override fun getShowdownInput(actor: BattleActor, battlePokemon: BattlePokemon, data: String?) = "none"
}
}
/** The name provided to Showdown so that battle messages include the name of the effect for lang. */
val itemName: String
/** Whether or not the item can probably be used right now, based on the mod-side version of battle state. */
fun canUse(battle: PokemonBattle, target: BattlePokemon): Boolean
/** Gets the itemId and data for inputting to Showdown. Hyper potion is `potion 200` for example. */
fun getShowdownInput(actor: BattleActor, battlePokemon: BattlePokemon, data: String?): String
/**
* Used for bag items that required prompts before selecting. It checks that the given stack is in the player's
* hands, is non-zero on size, that the battle is still tolerant of a forced action for bag use, and that this
* bag item can still be used.
*/
fun canStillUse(player: ServerPlayerEntity, battle: PokemonBattle, actor: BattleActor, target: BattlePokemon, stack: ItemStack): Boolean {
return stack in player.handItems && stack.count > 0 && canUse(battle, target) && actor.canFitForcedAction()
}
}
@@ -0,0 +1,56 @@
/*
* 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.battle
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.BagItemActionResponse
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.util.battleLang
import net.minecraft.item.ItemStack
import net.minecraft.server.network.ServerPlayerEntity
/**
* Something that can be a bag item. This needs to be registered in [BagItems]
*
* @author Hiroku
* @since June 26th, 2023
*/
interface BagItemConvertible {
/**
* Returns a [BagItem] if the given stack matches this convertible.
*
* If you're implementing this from an item subclass then you really
* need to remember to check that the supplied stack is of the correct
* item type because it doesn't necessarily have to be.
*
* [BagItemConvertible] can be implemented by non-Items so using it on
* items is actually a bit weird, just convenient.
*/
fun getBagItem(stack: ItemStack): BagItem?
fun handleInteraction(player: ServerPlayerEntity, battlePokemon: BattlePokemon, stack: ItemStack): Boolean {
val battle = battlePokemon.actor.battle
val bagItem = getBagItem(stack) ?: return false
if (!battlePokemon.actor.canFitForcedAction()) {
player.sendMessage(battleLang("bagitem.cannot").red())
return false
}
if (!bagItem.canUse(battle, battlePokemon)) {
player.sendMessage(battleLang("bagitem.invalid").red())
return false
}
battlePokemon.actor.forceChoose(BagItemActionResponse(bagItem, battlePokemon))
stack.decrement(1)
return true
}
}
@@ -0,0 +1,25 @@
/*
* 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.battle
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.item.CobblemonItem
class DireHitItem : CobblemonItem(Settings()), SimpleBagItemConvertible {
override val bagItem = object : BagItem {
override val itemName = "item.cobblemon.dire_hit"
override fun canUse(battle: PokemonBattle, target: BattlePokemon) = target.health > 0
override fun getShowdownInput(actor: BattleActor, battlePokemon: BattlePokemon, data: String?): String {
battlePokemon.effectedPokemon.incrementFriendship(1)
return "dire_hit"
}
}
}
@@ -0,0 +1,25 @@
/*
* 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.battle
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.item.CobblemonItem
class GuardSpecItem : CobblemonItem(Settings()), SimpleBagItemConvertible {
override val bagItem = object : BagItem {
override val itemName = "item.cobblemon.guard_spec"
override fun canUse(battle: PokemonBattle, target: BattlePokemon) = target.health > 0
override fun getShowdownInput(actor: BattleActor, battlePokemon: BattlePokemon, data: String?): String {
battlePokemon.effectedPokemon.incrementFriendship(1)
return "guard_spec"
}
}
}
@@ -0,0 +1,24 @@
/*
* 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.battle
import net.minecraft.item.ItemStack
/**
* A type of [BagItemConvertible] that just checks the item type of the stack.
*
* Used for internal Item subclasses.
*
* @author Hiroku
* @since July 1st, 2023
*/
interface SimpleBagItemConvertible : BagItemConvertible {
val bagItem: BagItem
override fun getBagItem(stack: ItemStack) = stack.item.takeIf { it == this }?.let { bagItem }
}

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