Gear
Register custom weapons, armor sets, hybrid subclasses, trim patterns, trim materials, enchantments, and equipment scanners.
Weapons
Any Item can be registered as a Craftics weapon. The registration gives it a damage type, base stats, and an optional on-hit ability. Call CrafticsAPI.registerWeapon(item, entry) during onCrafticsInit().
API Method
CrafticsAPI.registerWeapon(Item item, WeaponEntry entry);
WeaponEntry Builder
WeaponEntry entry = WeaponEntry.builder(myItem)
.damageType(DamageType.SLASHING) // damage category
.attackPower(8) // flat attack value
.apCost(1) // action points per attack
.range(1) // attack range in tiles
.ranged(false) // true for projectile weapons
.breakChance(0.0) // probability of breaking on use (0.0 to 1.0)
.ability(myAbilityHandler) // optional on-hit ability
.build();
| Builder Method | Type | Default | Description |
|---|---|---|---|
damageType(DamageType) | enum | PHYSICAL | Weapon's damage category |
attackPower(int) | int | 1 | Base attack power |
attackPower(IntSupplier) | supplier | Dynamic attack power computed each time the weapon attacks | |
apCost(int) | int | 1 | Action point cost per attack |
range(int) | int | 1 | Attack range in tiles |
ranged(boolean) | bool | false | Whether this is a projectile weapon |
breakChance(double) | double | 0.0 | Chance to break on use (0.0 to 1.0) |
ability(WeaponAbilityHandler) | handler | null | On-hit ability (see below) |
DamageType Values
Every weapon belongs to one of eight damage types. Armor sets, trims, and player affinity points grant bonuses to specific types.
| Value | Display Name | Typical Weapons |
|---|---|---|
SLASHING | Slashing | Swords |
CLEAVING | Cleaving | Axes |
BLUNT | Blunt | Maces, shovels |
WATER | Water | Tridents, corals |
SPECIAL | Special | Blaze rods, end rods, breeze rods |
PET | Pet | Pet and ally attacks |
RANGED | Ranged | Bows, crossbows |
PHYSICAL | Physical | Default / untyped weapons |
Built-in Ability Factories (Abilities class)
The Abilities class provides static factory methods that return WeaponAbilityHandler instances. Compose multiple abilities with .and().
WeaponAbilityHandler handler = Abilities.bleed()
.and(Abilities.sweepAdjacent(0.10, 0.05));
| Factory Method | Description |
|---|---|
bleed() | Applies bleed stacks equal to the weapon's Sharpness enchant level. |
sweepAdjacent(baseChance, bonusPerPoint) | Chance to hit one adjacent enemy for half damage. Scales with SLASHING affinity. Chance = baseChance + (affinity * bonusPerPoint) + (luck * 0.02). |
armorIgnore(baseChance, bonusPerPoint) | Chance to permanently destroy a portion of the target's defense and deal that amount as bonus damage. Scales with CLEAVING affinity. |
stun(baseChance, bonusPerPoint) | Chance to stun the target for one turn. Scales with BLUNT affinity. |
knockbackDirection(distance) | Pushes the target away from the player by up to N tiles, checking bounds and walkability each step. |
aoe(radius, damageMultiplier) | Hits all non-ally enemies within radius (Manhattan distance) of the target for (int)(baseDamage * damageMultiplier). |
applyEffect(type, turns, amplifier) | Applies a status effect to the target. Supported types: POISON, BURNING, SOAKED, SLOWNESS, CONFUSION. |
pierce() | Hits the first enemy directly behind the target in the attack direction for full base damage. |
fireDamage(bonusDmg) | Sets the target on fire and deals flat bonus fire damage. |
Custom Ability Handler
WeaponAbilityHandler is a @FunctionalInterface. Implement it directly for fully custom on-hit logic.
@FunctionalInterface
public interface WeaponAbilityHandler {
WeaponAbility.AttackResult apply(
ServerPlayerEntity player,
CombatEntity target,
GridArena arena,
int baseDamage,
PlayerProgression.PlayerStats stats,
int luckPoints
);
// Chain with another handler:
default WeaponAbilityHandler and(WeaponAbilityHandler next);
}
Return a WeaponAbility.AttackResult(totalDamage, messages, extraTargets). The messages list holds combat log strings and extraTargets lists any additional entities your ability hit.
JSON Datapack Schema
Place weapon JSON files at data/<namespace>/craftics/weapons/*.json. The ability array is an ordered list of built-in ability blocks chained together.
| Field | Type | Required | Description |
|---|---|---|---|
item | string | yes | Full item registry ID, e.g. minecraft:diamond_sword |
damage_type | string | no | DamageType name (case-insensitive). Defaults to PHYSICAL. |
attack_power | int | no | Base attack value. Defaults to 1. |
ap_cost | int | no | Action point cost per attack. Defaults to 1. |
range | int | no | Attack range in tiles. Defaults to 1. |
ranged | bool | no | Whether this is a projectile weapon. Defaults to false. |
break_chance | float | no | Probability of breaking on use (0.0 to 1.0). Defaults to 0.0. |
ability | array | no | Ordered list of ability objects (see ability kinds below). |
Ability object kind values and their fields:
| kind | Extra Fields |
|---|---|
bleed | (none) |
pierce | (none) |
sweep | base_chance (float, default 0.10), bonus_per_point (float, default 0.05) |
armor_ignore | base_chance (float, default 0.10), bonus_per_point (float, default 0.05) |
stun | base_chance (float, default 0.10), bonus_per_point (float, default 0.05) |
knockback | distance (int, default 1) |
aoe | radius (int, default 1), damage_multiplier (float, default 0.5) |
fire_damage | bonus_damage (int, default 2) |
apply_effect | effect (string, e.g. POISON), turns (int, default 3), amplifier (int, default 0) |
{
"item": "minecraft:diamond_sword",
"damage_type": "slashing",
"attack_power": 7,
"ap_cost": 1,
"range": 1,
"ranged": false,
"break_chance": 0.0,
"ability": [
{ "kind": "bleed" },
{ "kind": "sweep", "base_chance": 0.1, "bonus_per_point": 0.05 }
]
}
Code Example
import com.crackedgames.craftics.api.CrafticsAPI;
import com.crackedgames.craftics.api.Abilities;
import com.crackedgames.craftics.api.registry.WeaponEntry;
import com.crackedgames.craftics.combat.DamageType;
// Heavy Claymore: cleaving type, armor ignore + knockback
Item claymore = Registries.ITEM.get(Identifier.of("mymod", "heavy_claymore"));
CrafticsAPI.registerWeapon(claymore, WeaponEntry.builder(claymore)
.damageType(DamageType.CLEAVING)
.attackPower(7)
.apCost(2)
.range(1)
.ability(Abilities.armorIgnore(0.15, 0.03)
.and(Abilities.knockbackDirection(2)))
.build());
Armor Sets
Armor sets grant per-piece damage affinity bonuses and full-set stat bonuses when all four matching armor pieces are worn. The set ID must match the material key that PlayerCombatStats.getArmorSet() derives from the player's equipped armor. Call CrafticsAPI.registerArmorSet(entry) during onCrafticsInit().
Damage affinity is per-piece: each worn piece of a material contributes half the damageBonus value for that type. A full four-piece set contributes twice the value. The flat stat bonuses (speedBonus, defenseBonus, etc.) apply only when all four pieces are worn.
API Method
CrafticsAPI.registerArmorSet(ArmorSetEntry entry);
ArmorSetEntry Builder
ArmorSetEntry entry = ArmorSetEntry.builder("mymod:mythril")
.damageBonus(DamageType.SLASHING, 2) // affinity value per 2 pieces (per-piece = 1)
.damageBonus(DamageType.SPECIAL, 1)
.allDamageBonus(1) // applied first; per-type entries override it
.speedBonus(1)
.apBonus(0)
.defenseBonus(3)
.attackBonus(1)
.apCostReduction(0)
.description("Mythril Armor: light and sharp")
.build();
CrafticsAPI.registerArmorSet(entry);
| Builder Method | Type | Default | Description |
|---|---|---|---|
damageBonus(DamageType, int) | per-type | 0 | Affinity value per 2 pieces for this damage type. A single piece grants half this amount. |
allDamageBonus(int) | all types | Sets the same affinity value for every damage type. Per-type entries override it for that specific type. | |
speedBonus(int) | int | 0 | Movement speed bonus (full set only) |
apBonus(int) | int | 0 | Extra action points per turn (full set only) |
defenseBonus(int) | int | 0 | Defense stat bonus (full set only) |
attackBonus(int) | int | 0 | Base attack bonus (full set only) |
apCostReduction(int) | int | 0 | Reduction in AP cost per attack (full set only) |
description(String) | string | "" | Tooltip description shown in the combat HUD |
JSON Datapack Schema
Place armor set JSON files at data/<namespace>/craftics/armor_sets/*.json. The id must match the armor-set name your material produces.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Armor-set material key, e.g. mythril |
description | string | no | Tooltip description |
all_damage_bonus | int | no | Affinity value applied to every damage type first |
damage_bonuses | array | no | Per-type affinity overrides: each object has type (DamageType name) and amount (int) |
speed_bonus | int | no | Speed bonus (full set only) |
ap_bonus | int | no | Extra action points (full set only) |
defense_bonus | int | no | Defense bonus (full set only) |
attack_bonus | int | no | Attack bonus (full set only) |
ap_cost_reduction | int | no | AP cost reduction per attack (full set only) |
{
"id": "mythril",
"description": "Mythril Set: light and sharp",
"all_damage_bonus": 0,
"damage_bonuses": [
{ "type": "SLASHING", "amount": 2 },
{ "type": "SPECIAL", "amount": 1 }
],
"speed_bonus": 1,
"ap_bonus": 0,
"defense_bonus": 3,
"attack_bonus": 1,
"ap_cost_reduction": 0
}
Code Example
import com.crackedgames.craftics.api.CrafticsAPI;
import com.crackedgames.craftics.api.registry.ArmorSetEntry;
import com.crackedgames.craftics.combat.DamageType;
CrafticsAPI.registerArmorSet(ArmorSetEntry.builder("mythril")
.damageBonus(DamageType.SLASHING, 2)
.damageBonus(DamageType.SPECIAL, 1)
.speedBonus(1)
.defenseBonus(3)
.attackBonus(1)
.description("Mythril Armor: light and sharp")
.build());
Hybrid Sets
A hybrid set is a subclass bonus a player earns by wearing exactly two distinct armor materials at once. The pair is unordered: {iron, diamond} and {diamond, iron} resolve to the same hybrid. Call CrafticsAPI.registerHybridSet(entry) during onCrafticsInit().
API Method
CrafticsAPI.registerHybridSet(HybridSetEntry entry);
HybridSetEntry Builder
HybridSetEntry entry = HybridSetEntry.builder("iron", "diamond")
.className("Warlord")
.description("Build stacks on kill; spend them to empower your next attack.")
.effect(HybridEffect.WARLORD)
.build();
CrafticsAPI.registerHybridSet(entry);
| Builder Method / Field | Type | Required | Description |
|---|---|---|---|
builder(materialA, materialB) | yes | The two armor-set material keys. The builder normalizes the pair alphabetically so order does not matter. | |
className(String) | string | no | Subclass display name shown on the armor tooltip |
description(String) | string | no | One-line mechanic description shown on the armor tooltip |
effect(HybridEffect) | enum | yes | The combat mechanic applied during Phase 2 (see HybridEffect values below) |
HybridEffect Values
Pass one of these enum constants to .effect(). Each constant maps to a distinct combat mechanic implemented internally.
| HybridEffect | HybridEffect | HybridEffect |
|---|---|---|
SKIRMISHER |
COUNTERPUNCHER |
LUCKY_STREAK |
BREAKER |
RAMPAGE |
RUN_AND_GUN |
SENTINEL |
CUTPURSE |
DUELIST |
AMBUSH |
DEADEYE |
GILDED_GUARD |
WARLORD |
IMMOVABLE |
AEGIS |
GLADIATOR |
BERSERKER |
CONTAGION |
STONEWALL |
SIEGE |
STORMBRINGER |
JSON Datapack Schema
Place hybrid set JSON files at data/<namespace>/craftics/hybrid_sets/*.json. The three required fields are material_a, material_b, and effect.
| Field | Type | Required | Description |
|---|---|---|---|
material_a | string | yes | First armor material key (e.g. iron) |
material_b | string | yes | Second armor material key (e.g. diamond) |
effect | string | yes | HybridEffect name (case-insensitive) |
class_name | string | no | Subclass display name |
description | string | no | One-line mechanic description |
{
"material_a": "iron",
"material_b": "diamond",
"class_name": "Warlord",
"description": "Build stacks on kill; spend them to empower your next attack.",
"effect": "WARLORD"
}
Code Example
import com.crackedgames.craftics.api.CrafticsAPI;
import com.crackedgames.craftics.api.registry.HybridSetEntry;
import com.crackedgames.craftics.combat.HybridEffect;
CrafticsAPI.registerHybridSet(HybridSetEntry.builder("iron", "diamond")
.className("Warlord")
.description("Build stacks on kill; spend them to empower your next attack.")
.effect(HybridEffect.WARLORD)
.build());
Trim Patterns
Each trim pattern grants a per-piece stat bonus and a full-set bonus activated when all four armor pieces carry the same pattern. Register custom patterns for modded trim templates. Call CrafticsAPI.registerTrimPattern(entry) during onCrafticsInit().
The pattern id must match the registry path of the vanilla trim pattern, not the full namespaced ID. For example, minecraft:sentry uses "sentry".
API Method
CrafticsAPI.registerTrimPattern(TrimPatternEntry entry);
TrimPatternEntry Record
TrimPatternEntry is a record with no builder. Construct it directly.
CrafticsAPI.registerTrimPattern(new TrimPatternEntry(
"mymod:dragon", // patternId
TrimEffects.Bonus.MELEE_POWER, // perPieceStat
"+1 Melee Power per piece", // perPieceDescription
TrimEffects.SetBonus.FERAL, // setBonus
"Dragon's Fury", // setBonusName
"First attack each turn costs 0 AP" // setBonusDescription
));
| Field | Type | Description |
|---|---|---|
patternId | String | Registry path of the trim pattern (e.g. "mymod:dragon") |
perPieceStat | TrimEffects.Bonus | Stat bonus added once per armor piece carrying this pattern. Null means no per-piece bonus. |
perPieceDescription | String | Human-readable text for the per-piece bonus |
setBonus | TrimEffects.SetBonus | Bonus activated when all four pieces use this pattern. Use NONE for no set bonus. |
setBonusName | String | Display name for the set bonus |
setBonusDescription | String | Human-readable text for the set bonus |
JSON Datapack Schema
Place trim pattern JSON files at data/<namespace>/craftics/trim_patterns/*.json.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Registry path of the trim pattern |
per_piece_stat | string | no | TrimEffects.Bonus name (case-insensitive). Omit for no per-piece bonus. |
per_piece_description | string | no | Description of the per-piece bonus |
set_bonus | string | no | TrimEffects.SetBonus name (case-insensitive). Defaults to NONE. |
set_bonus_name | string | no | Display name for the set bonus |
set_bonus_description | string | no | Description of the set bonus |
{
"id": "sentry",
"per_piece_stat": "RANGED_POWER",
"per_piece_description": "+1 Ranged Power per trimmed piece",
"set_bonus": "OVERWATCH",
"set_bonus_name": "Overwatch",
"set_bonus_description": "Counter-attack ranged enemies that hit you"
}
TrimEffects.Bonus Values
| Bonus | Effect |
|---|---|
RANGED_POWER | Bonus ranged attack damage |
MELEE_POWER | Bonus melee damage (all melee types) |
SPEED | Movement speed (tiles per turn) |
AP | Extra action points per turn |
DEFENSE | Damage reduction |
LUCK | Affects loot rolls and ability proc chances |
ATTACK_RANGE | Extra attack range in tiles |
MAX_HP | Bonus maximum hit points |
ARMOR_PEN | Ignore a portion of enemy defense |
REGEN | HP regeneration per turn |
ALLY_DAMAGE | Bonus damage for pet and ally attacks |
STEALTH_RANGE | Reduced enemy detection range |
SWORD_POWER | Bonus SLASHING damage specifically |
CLEAVING_POWER | Bonus CLEAVING damage specifically |
BLUNT_POWER | Bonus BLUNT damage specifically |
WATER_POWER | Bonus WATER damage specifically |
SPECIAL_POWER | Bonus SPECIAL damage specifically |
TrimEffects.SetBonus Values
| SetBonus | Name | Effect |
|---|---|---|
NONE | No set bonus | |
OVERWATCH | Sentry | Counter-attack ranged enemies that hit you |
SANDSTORM | Dune | Enemies within 2 tiles lose 1 Speed |
TIDAL | Coast | Water tiles heal 1 HP per turn |
FERAL | Wild | Kill streak: 1.3x damage per streak level (resets if no kills on your turn) |
FORTRESS | Ward | 50% less damage when you did not move this turn |
ALL_SEEING | Eye | Ranged attacks have +30% crit chance |
ETHEREAL | Vex | 20% chance to dodge incoming attacks |
OCEAN_BLESSING | Tide | Full heal when dropping below 25% HP (once per combat) |
BRUTE_FORCE | Snout | Melee attacks splash to adjacent enemies |
INFERNAL | Rib | Fire attacks deal +3 bonus damage |
FORTUNE_PEAK | Spire | Double emerald rewards |
PATHFINDER | Wayfinder | Movement ignores obstacle tiles |
TERRAFORMER | Shaper | Moving 3 or more tiles deals 2 damage to all enemies adjacent to your destination |
PHANTOM | Silence | Invisible for the first 2 turns (enemies do not act) |
RALLY | Raiser | Tamed allies get +2 Speed and +1 Attack |
SYMBIOTE | Host | Heal 1 HP for each enemy killed |
CURRENT | Flow | Killing an enemy refunds 1 AP |
THUNDERSTRIKE | Bolt | Critical hits stun the target for 1 turn |
Trim Materials
Each trim material grants a per-piece stat bonus stacking once for each armor piece that carries a trim using that material. Register custom materials for modded ingots. Call CrafticsAPI.registerTrimMaterial(entry) during onCrafticsInit().
The material id must match the registry path of the trim material, not the full namespaced ID. For example, minecraft:iron uses "iron".
API Method
CrafticsAPI.registerTrimMaterial(TrimMaterialEntry entry);
TrimMaterialEntry Record
TrimMaterialEntry is a record with no builder. Construct it directly.
CrafticsAPI.registerTrimMaterial(new TrimMaterialEntry(
"mymod:orichalcum", // materialId
TrimEffects.Bonus.ARMOR_PEN, // stat
2, // valuePerPiece
"+2 Armor Penetration per piece"
));
| Field | Type | Description |
|---|---|---|
materialId | String | Registry path of the trim material |
stat | TrimEffects.Bonus | Stat bonus type |
valuePerPiece | int | Amount of the stat bonus added per trimmed armor piece |
description | String | Human-readable description shown in the armor tooltip |
JSON Datapack Schema
Place trim material JSON files at data/<namespace>/craftics/trim_materials/*.json.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Registry path of the trim material |
stat | string | yes | TrimEffects.Bonus name (case-insensitive). An unknown value causes the file to be skipped. |
value_per_piece | int | no | Bonus amount per trimmed piece. Defaults to 1. |
description | string | no | Human-readable description |
{
"id": "iron",
"stat": "DEFENSE",
"value_per_piece": 1,
"description": "+1 Defense per trimmed piece"
}
Code Example
import com.crackedgames.craftics.api.CrafticsAPI;
import com.crackedgames.craftics.api.registry.TrimMaterialEntry;
import com.crackedgames.craftics.combat.TrimEffects;
// Orichalcum trim: +2 armor pen per trimmed piece
CrafticsAPI.registerTrimMaterial(new TrimMaterialEntry(
"mymod:orichalcum",
TrimEffects.Bonus.ARMOR_PEN,
2,
"+2 Armor Penetration per trimmed piece"
));
Enchantments
Register enchantments that contribute passive stat bonuses during Craftics combat. The handler receives an EnchantmentContext holding the enchantment level, the player, and a StatModifiers accumulator. Call CrafticsAPI.registerEnchantment(enchantmentId, handler) during onCrafticsInit().
Enchantments that modify weapon abilities, such as Sharpness or Smite, are handled by weapon ability handlers rather than this registry.
API Method
CrafticsAPI.registerEnchantment(String enchantmentId, EnchantmentEffectHandler handler);
EnchantmentEffectHandler and EnchantmentContext
@FunctionalInterface
public interface EnchantmentEffectHandler {
void apply(EnchantmentContext ctx);
}
public class EnchantmentContext {
public int getLevel(); // enchantment level (1, 2, 3, ...)
public ServerPlayerEntity getPlayer(); // the player
public StatModifiers getModifiers(); // accumulator to add bonuses to
}
Add bonuses to the accumulator using ctx.getModifiers().add(TrimEffects.Bonus, amount). The full list of available TrimEffects.Bonus values is in the Trim Patterns section above.
JSON Datapack Schema
Place enchantment JSON files at data/<namespace>/craftics/enchantments/*.json. Each bonus entry grants (enchantLevel / per_levels) * amount of the specified stat.
| Field | Type | Required | Description |
|---|---|---|---|
enchantment | string | yes | Full enchantment registry ID, e.g. minecraft:protection |
bonuses | array | no | List of stat bonus objects |
bonuses[].stat | string | yes | TrimEffects.Bonus name (case-insensitive) |
bonuses[].amount | int | no | Bonus amount per qualifying level. Defaults to 1. |
bonuses[].per_levels | int | no | How many enchantment levels are needed per amount. Defaults to 1 (one bonus per level). |
{
"enchantment": "minecraft:protection",
"bonuses": [
{ "stat": "DEFENSE", "amount": 1, "per_levels": 2 }
]
}
With per_levels: 2 and amount: 1, Protection IV (level 4) grants (4 / 2) * 1 = 2 Defense.
Code Example
import com.crackedgames.craftics.api.CrafticsAPI;
import com.crackedgames.craftics.combat.TrimEffects;
// Holy Blessing: +1 Defense and +1 Regen per enchantment level
CrafticsAPI.registerEnchantment("mymod:holy_blessing", (ctx) -> {
ctx.getModifiers().add(TrimEffects.Bonus.DEFENSE, ctx.getLevel());
ctx.getModifiers().add(TrimEffects.Bonus.REGEN, ctx.getLevel());
});
// Swiftness: +1 Speed regardless of level
CrafticsAPI.registerEnchantment("mymod:swiftness", (ctx) -> {
ctx.getModifiers().add(TrimEffects.Bonus.SPEED, 1);
});
Equipment Scanners
Equipment scanners let addon mods contribute stat bonuses from non-standard inventory slots, such as trinkets, baubles, or curio slots, into Craftics combat stats. The scanner is called during combat stat calculation and its results are merged with trim and armor bonuses. Call CrafticsAPI.registerEquipmentScanner(id, scanner) during onCrafticsInit().
This registry is code-only; no JSON datapack loader exists for scanners. The scanner function must be provided in Java.
API Method
CrafticsAPI.registerEquipmentScanner(String id, EquipmentScanner scanner);
EquipmentScanner Interface
@FunctionalInterface
public interface EquipmentScanner {
StatModifiers scan(ServerPlayerEntity player);
}
StatModifiers Class
StatModifiers accumulates bonuses using the TrimEffects.Bonus enum as keys. It can also carry an optional set bonus and custom combat effect handlers.
StatModifiers mods = new StatModifiers();
mods.add(TrimEffects.Bonus.DEFENSE, 2); // +2 Defense
mods.add(TrimEffects.Bonus.SPEED, 1); // +1 Speed
mods.add(TrimEffects.Bonus.MAX_HP, 4); // +4 Max HP
mods.addSetBonus(TrimEffects.SetBonus.FERAL, "Feral Ring Set"); // optional set bonus
Call mods.addCombatEffect(name, handler) to register a dynamic CombatEffectHandler alongside the stat bonuses. Combat effect handlers are covered on the Items & Effects page.
Code Example
import com.crackedgames.craftics.api.CrafticsAPI;
import com.crackedgames.craftics.api.EquipmentScanner;
import com.crackedgames.craftics.api.StatModifiers;
import com.crackedgames.craftics.combat.TrimEffects;
import net.minecraft.item.ItemStack;
CrafticsAPI.registerEquipmentScanner("mymod", (player) -> {
StatModifiers mods = new StatModifiers();
// Read items from your addon's custom equipment slots
ItemStack belt = getCustomSlot(player, "belt");
if (!belt.isEmpty()) {
if (belt.isOf(MY_SWIFT_BELT)) {
mods.add(TrimEffects.Bonus.SPEED, 1);
} else if (belt.isOf(MY_IRON_BELT)) {
mods.add(TrimEffects.Bonus.DEFENSE, 2);
}
}
ItemStack amulet = getCustomSlot(player, "amulet");
if (!amulet.isEmpty() && amulet.isOf(MY_LUCKY_CHARM)) {
mods.add(TrimEffects.Bonus.LUCK, 3);
}
return mods;
});
Affinity Reskins
Craftics has eight affinities - Slashing, Cleaving, Blunt, Ranged, Water, Special, Pet, Physical. They are the axes a player spends level-up points on.
The count is fixed on purpose. The level-up and respec screens are laid out for exactly eight, and a mod that added ten more would not get a bigger screen, it would get an unreadable one. What a total-conversion mod usually needs is not more axes but different ones, so the eight slots can be renamed and re-iconed instead.
Reskinning
CrafticsAPI.reskinAffinity(PlayerProgression.Affinity.SLASHING,
AffinitySkin.of("Physical", "§c⚔", "+3 dmg to physical moves"));
CrafticsAPI.reskinAffinity(PlayerProgression.Affinity.SPECIAL,
AffinitySkin.of("Special", "§d✨", "+3 dmg to special moves"));
// Rename only, keeping Craftics' icon and description:
CrafticsAPI.reskinAffinity(PlayerProgression.Affinity.PET,
AffinitySkin.named("Ally"));
| Factory | Replaces |
|---|---|
AffinitySkin.of(name, icon, description) | All three. |
AffinitySkin.named(name) | The name only; icon and description keep Craftics' originals. |
new AffinitySkin(name, icon, description) | Any field passed null keeps the original. |
What a reskin reaches
One call renames the affinity everywhere a player can see it. That is the contract, not a best effort - every display site in Craftics reads through the skin registry rather than the enum field:
- The level-up screen's affinity buttons
- The respec screen
- The Infinite Mode class picker
- The damage-type panel, including its per-type letter and icon
- Weapon tooltips, including the compat tooltips for Simply Swords and Simply Bows
- The combat damage feedback line, including the Resisted and Weak! notes
- The chat message for gaining an affinity point
- The Infinite Mode class-pick confirmation message
The damage type is renamed with it. Each affinity has a one-to-one damage type, and a player sees both. Renaming the Slashing affinity to "Physical" while weapon tooltips still said "Slashing damage" would read as a bug rather than a theme, so the damage type follows its affinity automatically.
Register from a CrafticsAddon, not a plain initializer. Addon entrypoints run in common initialization, so the server and the client both learn the skin. A server-only registration renames the chat lines and leaves every screen showing the old name - the exact half-applied state that looks like a Craftics bug.
What does not change
Nothing mechanical. A reskinned Slashing affinity still boosts Slashing weapons, still grants its sweep chance, and still occupies the same save slot - so a reskin can be added to or removed from an existing world without touching player progress. Affinity points are saved by enum name, not by display name.
If what you want is a new axis of effectiveness rather than a renamed one, that is Attack Types - they are unlimited, and unlike affinities nobody levels them.
Combat Tools
Craftics already treats one item as a button rather than an item. The Move item is created when a fight starts, locked to a slot the player chose, restocked if it goes missing, prevented from being dropped, and destroyed when the fight ends. It is a control that happens to live in the hotbar.
A mod whose fights are commanded rather than swung needs more of those. Picking a move, switching a creature, checking a matchup - each is a control, and a control the player has to remember to carry is a control they will lose. A registered CombatTool gets Move's treatment exactly.
Registering a tool
CrafticsAPI.registerCombatTool(CombatTool.builder("mymod:lead", MyItems.LEAD)
.order(1) // one slot right of Move
.onUse((player, combat) -> {
MyMoveMenu.open(player); // your screen, your rules
return true; // handled; no default item use
})
.build());
| Builder Method | Default | Description |
|---|---|---|
builder(id, item) |
required | Unique id and the item to pin. Give it a distinct item - a tool sharing an item with ordinary gear would see that gear stripped along with the tool. |
order(int) |
1 | How far right of the Move slot it sits, wrapping the hotbar. 1 is immediately adjacent. |
stripOutsideCombat(boolean) |
true | Destroy every copy when no fight is running. True for a control - a button that survives into the hub is an item the player can hoard, trade or lose. |
onUse(handler) |
none | Fired server-side on a mid-fight right-click. Return true to suppress the item's ordinary use. |
Opening a menu
Craftics ships no menu framework here, deliberately. onUse fires and Craftics does nothing else with the click. From there you open whatever you like - a vanilla ScreenHandler, or your own payload to your own client screen. A move-selection screen is your design, and anything Craftics invented would fit it worse. This is the same split the bench uses: Craftics owns what an action means, the addon owns the screen the player picks from.
Behaviour worth knowing
- The handler only fires during an active fight. Outside combat the tool should not exist at all, so a click there means a stale copy - running the handler would open a menu for a fight that already ended.
- Tools yield rather than destroy. A tool's slot is
(moveSlot + order)wrapped around the hotbar, and whatever is sitting there is pushed to a free slot. If the hotbar is full the tool gives up and retries next tick. Move drops the player's item to claim its slot because without Move you cannot act at all; a tool is a convenience, and a fight that ate your sword to place a button would be a far worse bug than a button arriving a tick late. - Undroppable. Tools get Move's drop protection. A button the player can Q-drop mid-fight is a button they will drop mid-fight and then be unable to press.
- A throwing handler is swallowed and logged, and the click still counts as handled - a broken menu must not fall through to an ordinary item use the player never asked for.
Auto-Integration
Modded weapons and armor that nobody registered are given combat stats worked out from the item itself. You do not have to do anything for this, and it is why a weapon pack is playable the moment it is installed rather than after somebody writes a compat module for it.
An explicit registration always wins. Inference only ever fills a gap, so registering a weapon or an armor set - from code or from a datapack - is how you correct a guess, and you are never competing with it.
Weapons
Damage comes from the item's own vanilla attack rating, placed on Craftics' ladder for that weapon family by interpolation. It is not a formula: Craftics' numbers are hand-tuned and do not track vanilla, so a modded sword sitting between iron and diamond in vanilla terms simply gets a Craftics number between iron and diamond. The ladder is read from the live vanilla items at runtime, so it follows your config and your Minecraft version rather than a hardcoded table, and a weapon far above netherite is clamped rather than extrapolated.
Shape comes from the item's name, and decides damage type, AP cost, reach and signature trick. The families are Craftics' own, so a modded halberd behaves like a Craftics halberd:
| Reads as | From names like | Type | AP | Reach |
|---|---|---|---|---|
| Dagger | dagger, knife, sai, tanto, kris, dirk | Slashing | 1 | 1 |
| Light blade | sword, katana, rapier, cutlass, scimitar, falchion | Slashing | 1 | 1 |
| Warglaive | warglaive | Cleaving | 1 | 1 |
| Axe | axe, battleaxe, cleaver, hatchet | Cleaving | 2 | 1 |
| Greataxe | greataxe | Cleaving | 3 | 1 |
| Greatblade | greatsword, claymore, zweihander, flamberge | Cleaving | 3 | 1 |
| Spear | spear, pike, lance, javelin, trident, harpoon | Slashing | 2 | 2 |
| Polearm | halberd, glaive, naginata, bardiche, guisarme | Cleaving | 2 | 2 |
| Scythe | scythe | Cleaving | 2 | 2 |
| Hammer | mace, club, flail, morningstar, cudgel | Blunt | 2 | 1 |
| Greathammer | warhammer, maul, sledge | Blunt | 3 | 1 |
| Thrown | chakram, boomerang, shuriken, discus | Ranged | 1 | 3 |
| Bow | bow, crossbow, sling, musket, blowgun | Ranged | 1 | 4 |
Compound names win over the words inside them, so a greataxe is not an axe and a warglaive is not a glaive. Tools are excluded outright - a modded pickaxe holds "axe" and does not become a battleaxe. Anything whose name reads as no weapon at all, or that carries no attack modifier, is left alone rather than guessed at.
Armor
Armor Class comes from the piece's own armor rating and toughness, placed on the ladder for its own slot - slots are not interchangeable, so a chestplate is measured against chestplates. Toughness counts, because diamond and netherite carry identical armor points and differ only in it.
An inferred set also gets an affinity, chosen from the material's name, because every Craftics armor set grants one and a set granting none reads as broken next to the rest:
| Material names like | Affinity | Matching vanilla set |
|---|---|---|
| leather, hide, fur, cloth, wool | Physical | Leather |
| chainmail, chain, mail, scale, brigandine | Slashing | Chainmail |
| iron, steel, titanium, obsidian, plate, mithril | Cleaving | Iron |
| gold, brass, bronze, electrum, silver | Special | Gold |
| diamond, crystal, ruby, amethyst, quartz | Blunt | Diamond |
| turtle, prismarine, nautilus, coral, kelp | Water | Turtle |
| anything else | Physical | - |
A modded variant of a vanilla material lands on the affinity the player already associates with it - "reinforced iron" boosts Cleaving exactly like iron does. Inferred sets get the affinity and the Armor Class and nothing else: no flat AP, speed or attack bonuses, since those are what make a vanilla set feel special and handing them to every material would flatten the difference rather than honour it.
Food needed none of this. Any edible item has always had its heal value read from its own nutrition and saturation, so modded food has worked since long before this existed.
Auto-integration can be turned off entirely with autoIntegrateModdedGear in the config, which restores the old behaviour: unregistered weapons hit for a bare fist, unregistered armor is worth no Armor Class.
Combat Portraits
Craftics picks a combatant's head icon by entity type. That works when a type names a creature and fails completely when one entity type stands in for hundreds: every combatant in the fight shares one registry id, so any icon registered for it would be right for one creature and wrong for all the others. The HUD falls back to a coloured square with a letter in it.
It is the icon-side version of what AI keys already solve on the server. No texture can fix it, so the hook hands over the drawing instead.
// From your own ClientModInitializer - this half of the API is client-side.
CrafticsClientAPI.registerPortraitRenderer(
(ctx, entityId, typeId, x, y, size, damageTint) -> {
if (!typeId.equals("mymod:creature")) return false; // not ours
var world = MinecraftClient.getInstance().world;
if (world == null) return false;
if (!(world.getEntityById(entityId) instanceof MyCreature creature)) return false;
MyRenderer.drawPortrait(ctx, creature, x, y, size, damageTint);
return true;
});
Why the entity id
The rosters are keyed by entity id and the creature is standing on the grid in the client world, so world.getEntityById(entityId) gets you the live entity and everything its own mod knows about it. That is usually far better than a flat image - a mod with a real model renderer can draw the same portrait its own screens use.
typeId comes with any Craftics stat suffix already stripped, and is cheap to test before doing an entity lookup.
Rules
- Return false for anything you do not recognise. Renderers are asked in registration order and the first to claim a combatant wins, so a renderer that claims everything blanks out the rest of the fight.
- You get first refusal, you do not replace the fallback. Craftics still tries its own head texture, then its coloured square, for anything left unclaimed.
- Honour
damageTint. It runs 0 (untouched) to 1 (nearly dead), and Craftics reddens its own heads by exactly this - the enemy column reads health at a glance because of it. A portrait that ignores it drops information the roster currently carries. It is 0 at the panels that do not tint, so honouring it always matches what Craftics would have drawn. - One registration covers every panel. Both rosters, the turn-order strip and the hover inspect panel. A portrait in one place and a blank square in the next looks more broken than blank squares everywhere.
- Sizes differ. The panels do not agree on one - draw to the
sizeyou are given rather than assuming. - Stay inside your square. Nothing clips you to
x, y, size; drawing outside lands on neighbouring panels. - A renderer that throws is reported once and then never asked again for the rest of the session. It costs your portraits and keeps the fight playable - a render loop is not the place to die.
Hiding HUD Panels
If your addon draws its own version of what a Craftics panel shows, turn the Craftics one off. A party screen that already lists the player's creatures does not want the ally roster underneath it: that is two lists of the same thing competing for one corner of the screen, and the one the player is reading is not Craftics'.
import com.crackedgames.craftics.api.HudPanel;
@Override
public void onCrafticsInit() {
// Our party UI is the ally list.
CrafticsAPI.hideHudPanel(HudPanel.ALLY_ROSTER);
}
| Panel | What it draws |
|---|---|
ALLY_ROSTER | Top-left list of your allies: portrait, name and HP bar each. |
ENEMY_ROSTER | Top-right column of remaining enemies, and the boss HP bar above it. |
TURN_ORDER | Top-center strip showing whose turn it is and who acts next. |
PLAYER_STATUS | Top-left panel with the player's own HP, AP and movement. |
What suppression does and does not do
- Visual only. Nothing about the fight changes and Craftics keeps tracking and syncing everything the panel would have shown, so
showHudPanelmid-fight immediately shows the truth rather than a panel that has to catch up. - Hover inspection survives
ENEMY_ROSTER. Hiding the roster hides the column of heads; pointing at an enemy, an ally or a party member still opens its stat panel. Those are a different feature and replacing a list is not a reason to lose them. - Layout closes up. Panels stack against each other, so hiding one moves the ones below it up rather than leaving a gap where it used to be.
- The player's own config wins. A panel they turned off in the config stays off, and
showHudPanelwill not force it back on. Equally, a panel you hid is not restored by a player who never turned it off. Neither side overrules the other. - Declare it every start. Suppression is per-process and not persisted, so it belongs in your initializer alongside your other registrations.
- Think twice about
PLAYER_STATUS. Unlike the rosters it is the only place AP and movement appear, and a player with no AP readout cannot tell why an action is being refused.
Players have their own toggles for the two rosters under the config screen's Visual section, so a player who finds a panel redundant can turn it off without an addon.