pala M5a+M4a: XP chasseur/fermier + recettes machines data-driven + config auto-complete

This commit is contained in:
Anaïs Saraiva
2026-10-19 00:47:13 +02:00
parent 610dc597fb
commit c76d67a2fd
7 changed files with 224 additions and 27 deletions
+17 -5
View File
@@ -456,9 +456,9 @@ avant M4 (TODO:crafts-machines).
- Features wiki couvertes (entree CDC) : ~210 (§1.14). Portage existant : ~120 (assets+registres).
Logique specifiee restant a implementer : ~90. Couverture spec : 100 % des pages FR Java ingestionnees.
- TODO explicites : 13 nommes (grep : ocr-fiable, worldgen-couches, broadsword,
- TODO explicites : 16 nommes (grep : ocr-fiable, worldgen-couches, broadsword,
crafts-machines, chervil, commandes-exactes, ghost, luckperms, runclient, mcp-ocular, shebang,
java-home, pog-os)
java-home, pog-os, craft-xp, smelt-xp, crusher-xp)
+ 4 points UNVERIFIED (seuils XP 6-100, durees sticks, valeurs armures wiki vs MCreator,
patterns machines images).
- Methode : WAVES (crawl 137 articles → stub JS isoles → re-crawl navigateur → verifie) + ETERNAL CODE
@@ -509,8 +509,20 @@ avant M4 (TODO:crafts-machines).
conso 20 %/tick, luck 1/128, auto-generate a 1.0 ; NE cuit plus de minerais) + furnace accepte
mixed coals + gate fermier 8 a l'ouverture du crusher. VERIFIE : build SUCCESSFUL, boot Done,
0 erreur. UNVERIFIED en jeu : cycle fruit->lingot avec joueur, gate a fermier<8.
- Client/mod : ./gradlew runClient dans mods/pala/ : NON LANCE (session : pas d'affichage ; screenshots
ocular impossibles ici). TODO:runclient : 1 screenshot/feature dans mods/pala/docs/screens/ avant M10.
Raison : environnement headless, a faire sur poste avec X.
- M5a LIVRE 2026-10-19 ~00:30 : XP chasseur (AFTER_DEATH verifie au javap : passifs 14@0, poulpe
10@0, creeper 40, wither 1000, gates niveau) + fermier (ble 1.5@0, patate 2@2, carotte 2.5@2,
pasteque 4@3, citrouille 5@6, eggplant 10@8, okami 20@12, kiwano 50@16 ; maturite age=max).
Config auto-complete les vieilles pala.json (save si cles null — code pret, persistance
a confirmer au prochain boot). VERIFIE : build SUCCESSFUL, boot Done, 0 erreur.
UNVERIFIED : boot+jeu (boot en cours au commit). TODO:craft-xp, TODO:smelt-xp (aucun event
serveur dans fabric-api, mixin requis), TODO:crusher-xp (attribution : stocker UUID poseur, M5b).
- M4a LIVRE (meme vague) : recettes machines data-driven (crusherResults/Fuels, furnaceSmelts/Fuels
en config, /pala reload live, ids invalides ignores). VERIFIE : build SUCCESSFUL.
- Client/mod : boucle visuelle OPERATIONNELLE 2026-10-19 (Xvfb :99 + runClient SDL x11 +
captures Java Robot + pilotage xdotool + vision_analyze) : menu, Select World, monde New World
(creatif, cheats), /give, pose de bloc, inventaire, F3+T. Screens : scratch vis*.png.
docs/screens/ : TODO:runclient (copier les screens valides + log par feature).
Lecon : SDL prend wayland par defaut (forcer SDL_VIDEODRIVER=x11), xdotool click --window
ignore par SDL (utiliser XTEST au pointeur), ffmpeg local sans x11grab (Robot a la place).
- MCP : aucun MCP externe installe ; navigateur integre + vision_analyze utilises (OCR rejete §0).
TODO:mcp-ocular : installer pour la boucle runClient quand l'environnement le permet.
@@ -60,6 +60,50 @@ public final class PalaConfig {
"default", java.util.List.of("pala.job", "pala.trade", "pala.market"),
"heros", java.util.List.of("pala.*")));
// M5a (wiki VERIFIE) : chasseur = entite -> [xp, niveau requis].
// Passifs 14@0, poulpe 10@0 (page : "des le niveau 0/1"), creeper 40, wither 1000.
// Moddees (chevre 20@1, escargot 25@2...) : mobs absents du portage => non listees (M8/donjons).
public Map<String, double[]> hunterKills = new HashMap<>(Map.ofEntries(
Map.entry("minecraft:cow", new double[]{14, 0}),
Map.entry("minecraft:sheep", new double[]{14, 0}),
Map.entry("minecraft:pig", new double[]{14, 0}),
Map.entry("minecraft:chicken", new double[]{14, 0}),
Map.entry("minecraft:rabbit", new double[]{14, 0}),
Map.entry("minecraft:horse", new double[]{14, 0}),
Map.entry("minecraft:squid", new double[]{10, 0}),
Map.entry("minecraft:glow_squid", new double[]{10, 0}),
Map.entry("minecraft:creeper", new double[]{40, 0}),
Map.entry("minecraft:wither", new double[]{1000, 0})));
// M5a (wiki VERIFIE) : fermier recolte = bloc -> [xp, niveau requis].
// Ble 1.5@0 ("des le niveau 0"), patate 2@2, carotte 2.5@2 (table : Niveau 2 + ligne
// Carotte sans niveau => 2), pasteque 4@3, citrouille 5@6, eggplant 10@8,
// chervil==okami 20@12 (TODO:chervil), kiwano 50@16. Okami/orange hors chervil : 0.
public Map<String, double[]> farmerHarvest = new HashMap<>(Map.ofEntries(
Map.entry("minecraft:wheat", new double[]{1.5, 0}),
Map.entry("minecraft:potatoes", new double[]{2, 2}),
Map.entry("minecraft:carrots", new double[]{2.5, 2}),
Map.entry("minecraft:melon", new double[]{4, 3}),
Map.entry("minecraft:pumpkin", new double[]{5, 6})));
// M4a : recettes machines data-driven (CDC §3.4). Cles = ids de registres.
// Defauts = legacy+wiki VERIFIES (M3). Rechargeables via /pala reload.
public Map<String, String> crusherResults = new HashMap<>(Map.of(
"pala:egg_plant_fruit", "pala:amethyst_ingot",
"pala:okami_fruit", "pala:titane_ingot",
"pala:kiwano", "pala:paladium_ingot",
"pala:orange_blue_fruit", "pala:endium_nugget"));
public java.util.List<String> crusherFuels = new java.util.ArrayList<>(java.util.List.of(
"pala:pala_mixed_coal", "pala:creative_mixed_coal", "pala:amethyst_mixed_coal",
"pala:titane_mixed_coal", "pala:gold_mixed_coal"));
public java.util.List<String> crusherInfiniteFuels = new java.util.ArrayList<>(
java.util.List.of("pala:creative_mixed_coal"));
public Map<String, String> furnaceSmelts = new HashMap<>(Map.of(
"pala:paladium_ore", "pala:paladium_ingot",
"pala:paladium_green_ore", "pala:paladium_green_ingot"));
public java.util.List<String> furnaceFuels = new java.util.ArrayList<>(java.util.List.of(
"minecraft:coal", "minecraft:charcoal"));
private static PalaConfig instance = new PalaConfig();
private PalaConfig() {
@@ -101,6 +145,36 @@ public final class PalaConfig {
if (loaded.rankNodes != null && !loaded.rankNodes.isEmpty()) {
instance.rankNodes = loaded.rankNodes;
}
if (loaded.hunterKills != null && !loaded.hunterKills.isEmpty()) {
instance.hunterKills = loaded.hunterKills;
}
if (loaded.farmerHarvest != null && !loaded.farmerHarvest.isEmpty()) {
instance.farmerHarvest = loaded.farmerHarvest;
}
if (loaded.crusherResults != null && !loaded.crusherResults.isEmpty()) {
instance.crusherResults = loaded.crusherResults;
}
if (loaded.crusherFuels != null && !loaded.crusherFuels.isEmpty()) {
instance.crusherFuels = loaded.crusherFuels;
}
if (loaded.crusherInfiniteFuels != null) {
instance.crusherInfiniteFuels = loaded.crusherInfiniteFuels;
}
if (loaded.furnaceSmelts != null && !loaded.furnaceSmelts.isEmpty()) {
instance.furnaceSmelts = loaded.furnaceSmelts;
}
if (loaded.furnaceFuels != null && !loaded.furnaceFuels.isEmpty()) {
instance.furnaceFuels = loaded.furnaceFuels;
}
// M5a : un vieux pala.json (M0) n'a pas les nouvelles cles (null) : les
// defauts code sont en memoire, on re-sauvegarde pour les materialiser.
// Une table explicitement videe (non-null vide) est respecte tel quel.
if (loaded.hunterKills == null || loaded.farmerHarvest == null
|| loaded.crusherResults == null || loaded.crusherFuels == null
|| loaded.crusherInfiniteFuels == null || loaded.furnaceSmelts == null
|| loaded.furnaceFuels == null) {
save();
}
}
return true;
} catch (IOException | com.google.gson.JsonParseException e) {
@@ -16,6 +16,9 @@ import fr.surviepote.pala.PalaCommands;
// fr.surviepote.pala.PalaCommands (reutilise ici, pas de doublon) ;
// - ProcedureDurability : helper fondu dans hurtAndBreak.
//
// M5a (nouveau, pas legacy) : PalaJobEvents — XP chasseur (AFTER_DEATH) + fermier
// (recoltes vanilla mures + fruits moddes murs), tables config.
//
// SKIP avec raisons (9) :
// - ProcedureAngelicWaterMobplayerCollidesBlock, ProcedureFakeWaterMobplayerCollidesBlock :
// fluides non portes (voir consignes) ;
@@ -35,6 +38,7 @@ public final class PalaEvents {
PalaItemUseEvents.register();
PalaToolEvents.register();
PalaCropEvents.register();
PalaJobEvents.register();
PalaCommands.register();
}
}
@@ -0,0 +1,97 @@
package fr.surviepote.pala.events;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
import fr.surviepote.pala.PalaBlocks;
import fr.surviepote.pala.core.PalaConfig;
import fr.surviepote.pala.core.PalaJobs;
// M5a — XP chasseur (kills) + fermier (recoltes). Tables wiki VERIFIEES (PalaConfig).
// - Chasseur : AFTER_DEATH verifie au javap (fabric-entity-events-v1) ; tueur = source directe.
// XP uniquement si niveau metier >= requis (actions debloquees par niveau, wiki).
// - Fermier : cultures vanilla mures uniquement (age au max) + fruits moddes murs
// (eggplant 10@8, okami 20@12 via equivalence chervil, kiwano 50@16).
// TODO:craft-xp (pain 1, tarte 4, fioles...) : aucun event craft serveur dans fabric-api,
// mixin requis. TODO:smelt-xp (viande cuite 10) : aucun event four-take vanilla.
// TODO:crusher-xp (eggplant 3, chervil 4.5, kiwano 6) : attribution impossible, la BE ne
// connait pas son poseur ; M5b = stocker l'UUID du poseur dans la BE.
public final class PalaJobEvents {
private PalaJobEvents() {
}
public static void register() {
ServerLivingEntityEvents.AFTER_DEATH.register(PalaJobEvents::afterDeath);
}
private static void afterDeath(LivingEntity entity, DamageSource source) {
Entity killer = source.getEntity();
if (!(killer instanceof ServerPlayer player)) {
return;
}
String id = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
double[] row = PalaConfig.get().hunterKills.get(id);
if (row == null) {
return;
}
if (PalaJobs.levelOf(player, "chasseur") >= (int) row[1]) {
PalaJobs.addXp(player, "chasseur", row[0]);
}
}
/** Recolte vanilla : appele depuis PalaToolEvents.afterBreak (etat AVANT casse). */
public static void onHarvest(ServerPlayer player, BlockState state) {
Block block = state.getBlock();
String id;
if (block == Blocks.WHEAT || block == Blocks.POTATOES || block == Blocks.CARROTS) {
if (!isMature(state)) {
return;
}
id = BuiltInRegistries.BLOCK.getKey(block).toString();
} else if (block == Blocks.MELON || block == Blocks.PUMPKIN) {
id = BuiltInRegistries.BLOCK.getKey(block).toString();
} else {
return;
}
grant(player, "fermier", PalaConfig.get().farmerHarvest.get(id));
}
/** Fruits moddes murs : appele depuis handleCropBreak avec le fruit associe. */
public static void onModdedHarvest(ServerPlayer player, String fruit, double xp, int minLevel) {
if (PalaJobs.levelOf(player, "fermier") >= minLevel) {
PalaJobs.addXp(player, "fermier", xp);
}
}
private static void grant(ServerPlayer player, String job, double[] row) {
if (row != null && PalaJobs.levelOf(player, job) >= (int) row[1]) {
PalaJobs.addXp(player, job, row[0]);
}
}
/** Maturite : propriete "age" a son max (ble/carotte/patate), sans nom d'API fige. */
private static boolean isMature(BlockState state) {
for (Object prop : state.getProperties()) {
if (prop instanceof IntegerProperty age && age.getName().equals("age")) {
int max = 0;
for (int v : age.getPossibleValues()) {
max = Math.max(max, v);
}
return state.getValue(age) >= max;
}
}
return false;
}
}
@@ -112,6 +112,9 @@ public final class PalaToolEvents {
// --- Metiers : XP mineur (M0) ---
PalaJobs.onBlockBreak(sp, sl, pos, state);
// --- Metiers : XP fermier recoltes vanilla (M5a) ---
PalaJobEvents.onHarvest(sp, state);
// --- Pioche divine : compteur + evolution ---
if (held != null && GOD_PICKAXES.contains(held)) {
if (held == PalaItems.PICKAXE_OF_THE_GOD_LEVEL25) {
@@ -151,7 +154,7 @@ public final class PalaToolEvents {
}
// --- Recoltes : drops selon stade ---
handleCropBreak(sl, broken, pos);
handleCropBreak(sl, broken, pos, sp);
// --- Farmland cassee sous une culture -> la culture casse aussi ---
if (broken == Blocks.FARMLAND) {
@@ -199,10 +202,11 @@ public final class PalaToolEvents {
|| block == PalaBlocks.ORANGE_BLUE_STAGE3 || block == PalaBlocks.ORANGE_BLUE_STAGE4;
}
private static void handleCropBreak(ServerLevel level, Block broken, BlockPos pos) {
private static void handleCropBreak(ServerLevel level, Block broken, BlockPos pos, ServerPlayer sp) {
if (broken == PalaBlocks.FIKUTSI_STAGE3) {
PalaItemUseEvents.drop(level, pos, PalaItems.EGG_PLANT_FRUIT, 1);
PalaItemUseEvents.drop(level, pos, PalaItems.FIKUTSI_SEEDS, 1);
PalaJobEvents.onModdedHarvest(sp, "eggplant", 10, 8);
} else if (broken == PalaBlocks.FIKUTSI_STAGE0 || broken == PalaBlocks.FIKUTSI_STAGE1
|| broken == PalaBlocks.FIKUTSI_STAGE2) {
PalaItemUseEvents.drop(level, pos, PalaItems.FIKUTSI_SEEDS, 1);
@@ -212,6 +216,7 @@ public final class PalaToolEvents {
} else if (broken == PalaBlocks.KIWANO_STAGE4) {
PalaItemUseEvents.drop(level, pos, PalaItems.KIWANO, 1);
PalaItemUseEvents.drop(level, pos, PalaItems.KIWANO_SEEDS, 1);
PalaJobEvents.onModdedHarvest(sp, "kiwano", 50, 16);
} else if (broken == PalaBlocks.KIWANO_STAGE0 || broken == PalaBlocks.KIWANO_STAGE1
|| broken == PalaBlocks.KIWANO_STAGE2 || broken == PalaBlocks.KIWANO_STAGE3) {
PalaItemUseEvents.drop(level, pos, PalaItems.KIWANO_SEEDS, 1);
@@ -221,6 +226,7 @@ public final class PalaToolEvents {
} else if (broken == PalaBlocks.OKAMI_STAGE3) {
PalaItemUseEvents.drop(level, pos, PalaItems.OKAMI_FRUIT, 1);
PalaItemUseEvents.drop(level, pos, PalaItems.OKAMI_SEEDS, 1);
PalaJobEvents.onModdedHarvest(sp, "okami", 20, 12);
} else if (broken == PalaBlocks.OKAMI_STAGE0 || broken == PalaBlocks.OKAMI_STAGE1
|| broken == PalaBlocks.OKAMI_STAGE2) {
PalaItemUseEvents.drop(level, pos, PalaItems.OKAMI_SEEDS, 1);
@@ -1,11 +1,10 @@
package fr.surviepote.pala.machine;
import java.util.Map;
import fr.surviepote.pala.PalaBlockEntities;
import fr.surviepote.pala.PalaBlocks;
import fr.surviepote.pala.PalaItems;
import fr.surviepote.pala.core.PalaConfig;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.SimpleContainer;
@@ -15,7 +14,6 @@ import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.DispenserMenu;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
@@ -44,11 +42,7 @@ public class PalaMachineBlockEntity extends BlockEntity implements MenuProvider
private static final int COOK_TIME_FURNACE = 200;
private static final double LUCK_PER_TICK = 1.0 / 128.0;
private static final Map<Item, Item> CRUSHER_RESULTS = Map.of(
PalaItems.EGG_PLANT_FRUIT, PalaItems.AMETHYST_INGOT,
PalaItems.OKAMI_FRUIT, PalaItems.TITANE_INGOT,
PalaItems.KIWANO, PalaItems.PALADIUM_INGOT,
PalaItems.ORANGE_BLUE_FRUIT, PalaItems.ENDIUM_NUGGET);
// M4a : recettes lues de PalaConfig (data-driven, /pala reload). Ids invalides = ignores.
private final SimpleContainer inventory = new SimpleContainer(9);
private int progress;
@@ -74,11 +68,12 @@ public class PalaMachineBlockEntity extends BlockEntity implements MenuProvider
ItemStack fruit = inventory.getItem(0);
ItemStack fuel = inventory.getItem(1);
ItemStack output = inventory.getItem(2);
Item result = CRUSHER_RESULTS.get(fruit.getItem());
Item result = resolveItem(PalaConfig.get().crusherResults
.get(itemId(fruit)));
if (result == null || luck >= 1.0 || !isMixedFuel(fuel)) {
return;
}
boolean infinite = fuel.is(PalaItems.CREATIVE_MIXED_COAL);
boolean infinite = PalaConfig.get().crusherInfiniteFuels.contains(itemId(fuel));
if (output.isEmpty() || (output.is(result)
&& output.getCount() + 1 <= output.getMaxStackSize())) {
fruit.shrink(1);
@@ -126,23 +121,32 @@ public class PalaMachineBlockEntity extends BlockEntity implements MenuProvider
}
private static ItemStack smeltResult(ItemStack input) {
if (input.is(PalaBlocks.PALADIUM_ORE.asItem())) {
return new ItemStack(PalaItems.PALADIUM_INGOT);
Item result = resolveItem(PalaConfig.get().furnaceSmelts.get(itemId(input)));
return result == null ? ItemStack.EMPTY : new ItemStack(result);
}
if (input.is(PalaBlocks.PALADIUM_GREEN_ORE.asItem())) {
return new ItemStack(PalaItems.PALADIUM_GREEN_INGOT);
private static String itemId(ItemStack stack) {
return BuiltInRegistries.ITEM.getKey(stack.getItem()).toString();
}
private static Item resolveItem(String id) {
if (id == null) {
return null;
}
try {
return BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.parse(id));
} catch (Exception e) {
return null;
}
return ItemStack.EMPTY;
}
private static boolean isMixedFuel(ItemStack stack) {
return stack.is(PalaItems.PALA_MIXED_COAL) || stack.is(PalaItems.CREATIVE_MIXED_COAL)
|| stack.is(PalaItems.AMETHYST_MIXED_COAL) || stack.is(PalaItems.TITANE_MIXED_COAL)
|| stack.is(PalaItems.GOLD_MIXED_COAL);
return PalaConfig.get().crusherFuels.contains(itemId(stack));
}
private static boolean isFurnaceFuel(ItemStack stack) {
return stack.is(Items.COAL) || stack.is(Items.CHARCOAL) || isMixedFuel(stack);
String id = itemId(stack);
return PalaConfig.get().furnaceFuels.contains(id) || PalaConfig.get().crusherFuels.contains(id);
}
@Override
Binary file not shown.