Craft Dynamic Upgrades in Minecraft

CardEngine is a developer-friendly library that implements an interactive card-selection upgrade system. Players level up and choose from randomly drawn cards to gain permanent stats, active skills, or a passive skill that can be upgraded from 1โ˜… to 5โ˜….

โœจ

Java-First Cards, JSON Tuning

Write skills in Java, then tune values, rarity, descriptions, and icons in auto-generated JSON files, with no rebuild needed.

๐ŸŽด

Active & Passive Slots

2 active skill slots (R / X) with cooldowns and CDR, plus 1 passive slot with a full equip, tick, upgrade, and unequip lifecycle.

๐Ÿงน

Automatic Resource Cleanup

Bind summoned entities, NBT tags, state collections, and timed tasks to a skill. CardEngine cleans them up on unequip, death, logout, dimension change, and server stop.

โญ

Star-Level Scaling

Use @CardParamUp / @CardParamDown so values scale automatically from 1โ˜… to 5โ˜….

๐Ÿ–ผ๏ธ

Custom Icons

Use any Minecraft item or your own PNG texture as a card icon, rendered in the draw screen, HUD, and stats screen.

Quick Facts

CardEngine at a glance

Default keybinds

KOpen card selection
LStats & Upgrades screen (2 tabs)
RCast active skill, slot 1
XCast active skill, slot 2

Star scaling multipliers

Star@CardParamUp@CardParamDown
1โ˜…1.001.00
2โ˜…1.150.85
3โ˜…1.350.65
4โ˜…1.550.45
5โ˜…1.700.30

Rarity tiers

common ยท uncommon ยท rare ยท epic ยท legendary ยท mythic ยท special

Developer Integration Guide

Follow this guide to integrate CardEngine and create your own cards, active skills, and passive skills.

1

Configure build.gradle

Add the CardEngine jar file to your project dependencies by configuring a flatDir repository.

repositories {
    flatDir {
        dir 'libs'
    }
}

dependencies {
    //fg.deobf is required to remap the jar correctly in your development environment
    implementation fg.deobf("card_engine:card_engine:1.0.0")
}
2

Register Cards & Mechanics

Register everything inside FMLCommonSetupEvent using event.enqueueWork(...). A good pattern is one class per skill with a static void register(), then call them all from your main mod class.

@Mod(MyMod.MODID)
public class MyMod {
    public static final String MODID = "my_mod";

    public MyMod() {
        var bus = FMLJavaModLoadingContext.get().getModEventBus();
        bus.addListener(this::commonSetup);
        MinecraftForge.EVENT_BUS.register(this);
    }

    private void commonSetup(FMLCommonSetupEvent event) {
        event.enqueueWork(() -> {
            MyDashSkill.register();
            MyPassiveSkill.register();
        });
    }
}

Option A: Quick Stat Card

Perfect for simple cards that link directly to vanilla Minecraft Attributes. CardEngine auto-generates JSON configurations and handles UI selection rendering automatically.

// Line 1: Register the mechanism (binds key to vanilla Attribute)
CardRegistry.registerCard("my_mod:attack_damage", Attributes.ATTACK_DAMAGE);

// Line 2: Register the card (CardEngine handles rendering & JSON generation!)
CardRegistry.register("my_mod:attack_damage_rare", "my_mod:attack_damage");

Option B: Custom Mechanism with @CardParam

For custom logic, create a class implementing CardEngine and declare fields with @CardParam. CardEngine automatically exports these parameters to the card's JSON file.

// 1. Write your custom class implementing CardEngine
public class VampirismUpgrade implements CardEngine {
    @CardParam(10.0) // Exports "heal_ratio" (default 10.0) to JSON config automatically!
    private double heal_ratio;

    @Override
    public void apply(ServerPlayer player) {
        // heal_ratio is automatically injected with the value configured in the JSON file
        player.heal((float) (player.getMaxHealth() * (heal_ratio / 100.0)));
    }
}

// 2. Register both the mechanism class and the card:
CardRegistry.registerCard("my_mod:vampirism", VampirismUpgrade.class);
CardRegistry.register("my_mod:vamp_card", "my_mod:vampirism");

Option C: Active Skills (With Cooldowns)

Register a triggerable active skill with a base cooldown (in ticks) and link it to a card. CardEngine manages hotkeys (R and X), slot assignment, HUD indicators, and cooldown overlays. Final cooldown is BaseCD ร— (1 โˆ’ CDR), with CDR capped at 90%.

// 1. Register the active skill logic & base cooldown (e.g. 100 ticks = 5s)
CardRegistry.registerActiveSkill("my_mod:dash", 100, (player) -> {
    Vec3 look = player.getLookAngle();
    player.setDeltaMovement(look.x * 1.5, 0.2, look.z * 1.5);
    player.hurtMarked = true; // Sync movement to client

    // Play sound
    player.level().playSound(null, player.getX(), player.getY(), player.getZ(),
        SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1.0F, 1.0F);
});

// 2. Link the card to the active skill
CardRegistry.registerASkillCard("my_mod:dash_card", "my_mod:dash");

If the skill needs equip/unequip hooks, implement IActiveSkill instead of a lambda: trigger(player), plus optional onEquip(player, slot) and onUnequip(player, slot).

Option D: Passive Skills (1 slot)

A player can equip at most one passive skill. Picking a new one while the slot is occupied shows a replace-confirmation dialog. Implement IPassiveSkill:

public class MyBuffPassiveSkill implements IPassiveSkill {
    public static final String CARD_ID = "my_mod:my_passive_card";

    @Override
    public void onEquip(ServerPlayer player, int starLevel) {
        // Attach attribute modifiers / start buffs
    }

    @Override
    public void onTick(ServerPlayer player, int starLevel) {
        // Runs every 10 ticks
    }

    @Override
    public void onUpgrade(ServerPlayer player, int newStarLevel) {
        // Refresh buff strength when the card is upgraded (1โ˜… to 5โ˜…)
    }

    @Override
    public void onUnequip(ServerPlayer player) {
        // Remove attribute modifiers.
        // CardEngine calls CardResourceManager.cleanupBySkill() for you afterwards.
    }

    public static void register() {
        CardRegistry.registerPassiveSkill(CARD_ID, new MyBuffPassiveSkill());
        CardRegistry.registerPSkillCard(CARD_ID, "my_mod:my_passive_mech");
    }
}
3

Automatic Resource Cleanup

Whenever a skill creates temporary resources, bind them to the skill with CardResourceManager right when they are created. CardEngine releases them automatically when the skill is unequipped, the player logs out or changes dimension, the target entity dies or despawns, the server stops, or the optional TTL expires.

public class MySummonSkill implements IActiveSkill {
    public static final String SKILL_ID = "my_mod:my_skill";
    private final List<CustomBombData> activeBombs = new CopyOnWriteArrayList<>();

    @Override
    public void trigger(ServerPlayer player) {
        // List / Set / Map: cleared automatically
        CardResourceManager.bindState(player, SKILL_ID, activeBombs);

        // Summoned entity / projectile: discarded automatically (optional TTL in ticks)
        Entity minion = spawnMinion(player);
        CardResourceManager.bindEntity(player, SKILL_ID, minion, 400);

        // NBT marker on a target: removed when the target dies or the skill is unequipped
        CardResourceManager.bindEntityNbt(targetMob, "my_custom_tag", SKILL_ID);

        // Custom cleanup callback with TTL (400 ticks = 20s)
        CardResourceManager.bindTask(player, SKILL_ID, () -> { /* custom cleanup */ }, 400);
    }

    @Override
    public void onUnequip(ServerPlayer player, int slot) {
        // cleanupBySkill() runs automatically afterwards.
        // Only clear your own local caches here.
        activeBombs.clear();
    }
}

Rules for addons:

  • Do not keep static Map<UUID, ...> without a cleanup mechanism.
  • Do not store Entity or ServerLevel references in static collections (chunk and world leaks). Use UUID or WeakReference.
  • Do not add your own @SubscribeEvent for PlayerLoggedOutEvent, PlayerChangedDimensionEvent, EntityLeaveLevelEvent, LivingDeathEvent, or ServerStoppedEvent. CardEngine already handles them.
  • Always call CardResourceManager.bind...() as soon as a temporary resource is created.

Skills that need per-tick logic (projectiles, zones, domains) can expose a static void tick(ServerLevel) and call it from a single TickEvent.ServerTickEvent (phase END) in your main mod class.

4

Star Scaling & Custom Icons

Star scaling

Annotate numeric parameters with @CardParamUp (values that grow with stars, such as damage or healing) or @CardParamDown (values that shrink with stars, such as cooldowns or delays) and CardEngine multiplies them by the star level automatically (see the multiplier table on the Introduction tab). You can still edit the base values in the card's JSON after building.

Card icons

The "icon" field in a card's JSON accepts two formats.

A. Vanilla item

"icon": "minecraft:nether_star"

B. Custom PNG texture: place the file under assets/<modid>/textures/gui/icons/ in any mod or resource pack, then reference it by resource path:

"icon": "cardengine:textures/gui/icons/my_icon.png"
"icon": "my_addon:textures/gui/icons/my_icon.png"

When the value ends in .png (or is not a valid item), CardEngine draws it directly with alpha blending. Custom icons render in the card draw screen, the skill HUD, and the stats screen.

5

Apply Active Upgrades in Forge Events

Retrieve player stats capability from the event listeners and apply custom gameplay scaling accordingly.

@SubscribeEvent
public static void onLivingHurt(LivingHurtEvent event) {
    if (event.getSource().getEntity() instanceof Player player) {
        player.getCapability(PlayerUpgradeProvider.PLAYER_UPGRADE).ifPresent(upgrade -> {
            // Read accumulated stats from the player
            double critChance = upgrade.getStats().getOrDefault("my_mod:crit_chance", 0.0);
            if (critChance > 0.0 && player.getRandom().nextFloat() < critChance) {
                event.setAmount(event.getAmount() * 2.0f); // Deal critical double damage!
            }
        });
    }
}

Agent System Prompt Context

Copy and paste this context directly into your AI coding assistant (Claude, ChatGPT, Cursor, GitHub Copilot...) to brief it on CardEngine's APIs, lifecycle, and modding rules in as few tokens as possible.

You are writing a Minecraft 1.20.1 Forge mod addon for the CardEngine library (package net.com.cardengine).
Skills are written in Java. JSON in config/card_engine/cards/ only tunes values, description, icon, rarity.

## ASK THE USER FIRST
Before writing a skill with numeric values (damage, heal, duration, radius, cooldown), ask once:
"Do you want @CardParamUp / @CardParamDown so values scale automatically by star level (1-5)? You can still edit them in JSON after building."
- @CardParamUp: grows with stars (damage, heal, radius). @CardParamDown: shrinks with stars (cooldown, delay).
- If the user declines: hardcode or read from JSON, no annotation.
Star multipliers: 1โ˜… 1.00/1.00, 2โ˜… 1.15/0.85, 3โ˜… 1.35/0.65, 4โ˜… 1.55/0.45, 5โ˜… 1.70/0.30 (Up/Down).

## Registration (api.registries.CardRegistry), call inside FMLCommonSetupEvent via event.enqueueWork
- registerActiveSkill(skillId, cdTicks, lambdaOrIActiveSkill); registerASkillCard(cardId, skillId)
- registerPassiveSkill(cardId, IPassiveSkill); registerPSkillCard(cardId, mechName)
- registerStatsCard(cardId, statKey); linkSkillAndCard(skillId, cardId)
- Custom mechanism: class implements CardEngine { apply(ServerPlayer) } with @CardParam fields.
Pattern: one class per skill with static register(); main mod class calls them all.
Per-tick logic: static tickXxx(ServerLevel) called from one TickEvent.ServerTickEvent (phase END).

## Lifecycle
- IActiveSkill: trigger(player); default onEquip(player, slot), onUnequip(player, slot).
- IPassiveSkill: onEquip(player, star), onUnequip(player); default onTick(player, star) every 10 ticks, onUpgrade(player, newStar).
- Slots: 2 active (R = slot 1, X = slot 2) + 1 passive. K = draw screen, L = stats screen.
- Cooldown = BaseCD * (1 - CDR), CDR max 90%.
- On replace/unequip the engine does: old.onUnequip -> CardResourceManager.cleanupBySkill -> save new -> new.onEquip. Do NOT call cleanup manually; in onUnequip only clear your own local caches.

## Temporary resources (api.cleanup.CardResourceManager), bind immediately when created
- bindEntity(player, skillId, entity[, ttlTicks])
- bindEntityNbt(targetEntity, nbtKey, skillId)
- bindState(player, skillId, collectionOrMap)
- bindTask(player, skillId, runnable[, ttlTicks])
Engine auto-cleans on unequip, logout, dimension change, entity death/despawn, server stop, and TTL expiry (swept every 100 ticks).

## Golden rules
1. No static Map<UUID,...> without a cleanup mechanism.
2. Never store Entity or ServerLevel in static collections; use UUID or WeakReference.
3. Do NOT write @SubscribeEvent for PlayerLoggedOutEvent, PlayerChangedDimensionEvent, EntityLeaveLevelEvent, LivingDeathEvent, ServerStoppedEvent (CardEngine handles them).
4. Always use CardResourceManager.bind...() for temporary resources.

## Card icon (JSON "icon")
- Item: "minecraft:nether_star"
- Custom PNG: "modid:textures/gui/icons/name.png" (file at assets/modid/textures/gui/icons/, any mod or resource pack). Rendered with blit in draw screen, HUD, and stats screen.

## Reading stats
player.getCapability(PlayerUpgradeProvider.PLAYER_UPGRADE).ifPresent(u -> u.getStats().getOrDefault("modid:stat_key", 0.0));
Client: ClientStatsHandler.getPassiveSkillSlot() returns the equipped passive card id.

## Caveats
- Mythic rarity is spelled "mythic" everywhere (the old typo "mysthic" was removed in v1.0.1).
- Sync player levels by listening to PlayerXpEvent.XpChange and respect event.isCanceled().

Frequently Asked Questions

Once registered programmatically, CardEngine automatically outputs card configurations as JSON files inside config/card_engine/cards/. Server admins can edit these files to modify icons, values, descriptions, and weights without rebuilding the code. Missing files are regenerated with defaults.

Yes. All stats and capability data are tracked on the logical server and saved per world in world/data/card_engine/[uuid].json. Packets sync stats, cooldowns, and slots to clients to render the HUD and screens.

Admin operators can execute /cardengine draw <player> to trigger a draft screen, or use the developer API upgrade.setPendingDraws(count) followed by upgrade.triggerPendingDraw(player) in code. Players can also press K.

Two active skills (slot 1 on R, slot 2 on X) and one passive skill. When a slot is full, the selection screen asks which one to replace. Replacing triggers the old skill's onUnequip and an automatic resource cleanup.

Yes. Put a .png in assets/<modid>/textures/gui/icons/ of any mod or resource pack and set "icon": "<modid>:textures/gui/icons/name.png" in the card JSON. Vanilla items such as minecraft:nether_star also work.

No. Bind them with CardResourceManager.bindEntity(...) (or bindState, bindEntityNbt, bindTask) and CardEngine cleans them up on unequip, death, logout, dimension change, server stop, or TTL expiry.

Have Questions or Custom Requests?

Fill out the form below, and we'll reply with integration tips!