Instruction file imported from DiamondDagger590/McRPG (
.cursor/rules/core.mdc). Copyright stays with the author.
McRPG Core Rules
McRPG is a Java 21 Minecraft RPG plugin (Paper 1.21) that adds skills and abilities. It depends on McCore — an owned shared framework — and uses ContentExpansion to register content.
McCore Ownership
McCore (com.diamonddagger590:McCore) is also an owned project. Place reusable logic there, not in McRPG.
McCore provides: CorePlugin, CoreBootstrap, Registry, Manager, RegistryAccess, RegistryKey, DelayableCoreTask, Parser (equation evaluator), CustomItemWrapper.
Build Commands
./gradlew verifiedShadowJar— clean + test + build (recommended)./gradlew fastShadowJar— clean + build (no tests)./gradlew test— tests only
The entire test suite must pass before a task is considered complete. Run ./gradlew verifiedShadowJar and verify zero failures across all test classes — not just tests related to the current change. Regressions in unrelated tests still block completion.
Entity Hierarchy
AbilityHolder → LoadoutHolder → SkillHolder → McRPGPlayer
Players always implement all three. Non-player entities use AbilityHolder directly.
Domain Terminology
Ability & Skill System
| Term | Meaning |
|---|---|
| Ability | An action or passive effect an entity can use (active or passive) |
| Skill | A leveling system (e.g., Swords, Mining) that unlocks and scales abilities |
| AbilityHolder | McRPG wrapper around any entity that can hold/use abilities |
| LoadoutHolder | AbilityHolder restricted to only abilities in their active loadout |
| SkillHolder | AbilityHolder that also has levelable skills |
| McRPGPlayer | Concrete player — implements SkillHolder, LoadoutHolder, and CorePlayer |
| Tier | Enhancement level of an ability; higher tiers change mechanics, not just stats |
| Mana | Per-player resource pool consumed on active ability activation. Tracked via PlayerStatInstance keyed by McRPGPlayerStat.MANA. Regenerates passively at a flat rate |
| Combo Activation | Click-combo sequences (RRR, RRL, RLR) that trigger active abilities. All active abilities activate exclusively via combos gated by mana. Managed by ComboManager |
| ComboActivatable | Interface marking an active ability as combo-eligible. Extends ManaAbility. Provides comboActivate(AbilityHolder) returning boolean |
| ManaAbility | Interface declaring getManaCost(AbilityHolder). Implemented by ComboActivatable and ConfigurableActiveAbility |
| PlayerStat | Abstract base for per-player tracked stats (mana, health). Registered in PlayerStatRegistry. Instance state in PlayerStatData/PlayerStatInstance |
| PlayerStatModifier | Extensible class keyed by NamespacedKey for flat/percent bonuses to a stat's effective max |
| PlayerStatConsumeEvent | Cancellable event fired before every PlayerStatInstance.consume() call. Allows third-party cost modification or cancellation |
| Cooldown | Time lock applied to an ability after it activates |
| Component | Modular activation/cancel logic registered on an ability (priority-ordered) |
| Attribute | AbilityAttribute<T> in AbilityData — per-holder ability state, no reflection |
| ContentExpansion | Module bundling skills, abilities, statistics, settings, and localization for registration |
| StatisticContent | Wrapper pairing a McCore Statistic with an expansion key for content-pack registration |
| StatisticContentPack | Content pack collecting StatisticContent entries — one per expansion |
| McRPGStatistic | Utility class: global statistic constants + factory methods for per-skill/per-ability statistics |
| DAO | Static JDBC methods for reading/writing data (SkillDAO, LoadoutAbilityDAO) |
Quest System
| Term | Meaning |
|---|---|
| QuestDefinition | Immutable blueprint for a quest (phases, scope type, rewards, repeat mode). Shared across all runtime instances. |
| QuestPhaseDefinition | Ordered group of stages in a definition; ALL or ANY completion mode. Not persisted — state computed from child stages. |
| QuestStageDefinition | Group of objectives in a definition; all objectives must complete for the stage to finish. |
| QuestObjectiveDefinition | Single trackable objective (e.g., "break 50 stone"). Carries QuestObjectiveType key and config. |
| QuestInstance | Mutable runtime object created from a QuestDefinition. Tracks state, scope, timestamps, and child stage/objective instances. |
| QuestState | Enum: NOT_STARTED, IN_PROGRESS, COMPLETED, CANCELLED |
| QuestScope | The set of players participating in a QuestInstance (single player, permission group, land, etc.) |
| QuestScopeProvider | Abstract factory that creates/loads QuestScope instances for a specific scope type; registered by NamespacedKey |
| QuestSource | How a quest was obtained (board, ability upgrade, manual, etc.); controls abandonability |
| QuestObjectiveType | Extensible interface defining behavior and progress tracking for an objective category |
| QuestRewardType | Extensible interface defining how a specific reward is granted |
| PendingReward | Serialized reward queued for an offline player; granted at next login, expires after configurable duration |
| BoardOffering | A single quest slot on the board. State: AVAILABLE → ACCEPTED → COMPLETED/EXPIRED |
| QuestRarity | Rarity tier assigned to board offerings; affects template scaling and visual display |
| QuestTemplate | Declarative YAML blueprint for procedurally generating QuestDefinition instances |
Registry Access Pattern
// Always access via registryAccess() — never instantiate managers directly
EntityManager em = mcRPG.registryAccess().registry(RegistryKey.MANAGER).manager(McRPGManagerKey.ENTITY);
AbilityRegistry ar = mcRPG.registryAccess().registry(McRPGRegistryKey.ABILITY);
YamlDocument config = mcRPG.registryAccess().registry(RegistryKey.MANAGER).manager(McRPGManagerKey.FILE).getFile(FileType.SWORDS_CONFIG);
Localization & player-facing numbers
All player-facing text goes through McRPGLocalizationManager (McRPGManagerKey.LOCALIZATION) so the locale chain applies when resolving translation keys. That is separate from numbers: for decimals in localized strings, GUIs, ability lore, PAPI, and similar surfaces, obtain the formatter via localizationManager.getDisplayDecimalFormatter(), then call formatDisplayDecimal(McRPGPlayer, …), formatDisplayDecimal(Audience, …), or formatDisplayDecimal(Locale, …) (float overloads exist). Do not use Float.toString / Double.toString — no locale rules and full precision leak into UI. NumberFormat uses the chain head only for McRPGPlayer overloads (the full chain is still used for message lookup elsewhere). Audience overloads use a loaded McRPGPlayer’s chain head when the audience is that Player, otherwise the server default chain’s head (e.g. console). Explicit Locale overloads use that locale. Locale resolution is internal to the formatter; it reads the locale chain head via getLocaleChain(McRPGPlayer) and falls back to the server default for non-player audiences. McRPGDisplayDecimalFormatter keeps one NumberFormat per Locale; each call mutates min/max fraction digits on that instance and formats inside synchronized on it (not thread-safe otherwise; no per-digit-tuple cache). Omit the two int parameters for default 1–2 fraction digits; pass them to customize (non-negative, min ≤ max).
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Abstract base | Base prefix |
BaseAbility, BaseSkill |
| McRPG native impl | McRPG prefix |
McRPGAbility, McRPGPlayer |
| DTOs | Data suffix |
AbilityData, SkillHolderData |
| DAOs | DAO suffix |
SkillDAO, LoadoutAbilityDAO |
| Registries | Registry suffix |
AbilityRegistry, SkillRegistry |
| Custom events | Event suffix |
BleedActivateEvent |
| Bukkit listeners | On + action |
OnAttackAbilityListener |
| Components | Component suffix |
BleedEligibleForTargetComponent |
| Attributes | Attribute suffix |
AbilityTierAttribute |
| Config wrappers | ConfigFile suffix |
SwordsConfigFile |
Ability NamespacedKey constants are static final on the ability class: new NamespacedKey(McRPGMethods.getMcRPGNamespace(), "bleed").
Required Annotations
@NotNull(IntelliJ v12) on all non-null return types and parameters@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)on listeners@Overrideon all overridden methods@ParserConfigKeyson all concreteConfigurableTierableAbilityimplementations — declares the Parser-backed tier-config YAML keys the ability reads at runtime (use@ParserConfigKeys({})if none beyond genericunlock-level/upgrade-point-cost)
Code Style
- 4-space indentation, K&R brace style (standard Java)
- Prefer
varfor local variables when the declared type is long/nested and would be more distracting than helpful; otherwise prefer explicit types - Javadoc on all methods (public and private) with
@paramand@returnsemantics — this applies to every method regardless of visibility - Prefer object collaborators with instance methods for domain behavior; avoid introducing static helper classes when behavior naturally belongs to a service/slot/manager object. Demote private helpers to instance scope when they logically operate on or near the enclosing class's instance state — declaring them
staticmisrepresents their relationship to that state and makes future dependency additions harder. If a helper is genuinely shared cross-class plumbing (e.g. template placeholder substitution), push it onto the appropriate manager or service rather than duplicating it. - No section-divider comments (
// --- Section name ---or// ===== Section =====) — if a class needs labeled sections, it is too large or has too many concerns. Extract a collaborator or use natural method ordering instead - Third-party developer mindset: McRPG is extensible by external plugins. Changes to public APIs, events, and registries should be made as if you were a third-party plugin hooking in — prefer additive non-breaking changes, fire Bukkit events where external plugins would want to intercept, document extension points.
Builder Pattern
Use a builder when a class meets any of these criteria:
- Constructor has 6+ total parameters
- Constructor has 3+ optional/nullable parameters that lead to overloaded constructors or disambiguator hacks (e.g.
boolean ignored) - The class is immutable and constructed in multiple callsites with varying subsets of optional fields
Required structure:
public static final class Builderas an inner class on the target type- Required fields are parameters of the Builder constructor — no zero-arg builder constructors
- Optional fields have sensible defaults and are set via fluent setters returning
this build()validates invariants and returns the constructed object- The target class's constructor is
private— the builder is the only public construction path (deprecated constructors may remain temporarily during migration)
Do not use a builder when:
- Class has ≤5 parameters that are all required — a constructor is clear enough
- Class is mutable with setters
- Only one or two callsites construct the object — the ceremony isn't justified
- Records with simple field lists — the canonical constructor is already self-documenting
Anti-Patterns
- No reflection — use attribute factory pattern instead
- No hard-coded values — all tunable values must come from YAML config via
Route - No deep inheritance — compose via interfaces (
PassiveAbility,CooldownableAbility, etc.) - No mutable global static — use registries; only
McRPG.getInstance()is acceptable as static access - No
getInstance()singletons for domain state — per-player state belongs on the player object (McRPGPlayer); per-system state should be aManagerregistered in the registry and accessed viaregistryAccess().getInstance()singletons hide coupling, prevent constructor injection, and force tests to set up global state instead of passing parameters - No static utility classes for domain logic — a method that calls any global accessor (
SomeClass.getInstance(),McRPG.getInstance(), or accesses a singleton) has a dependency even if it does not appear in the parameter list. If a static method would break when the global state it reaches for is null, it has a hidden dependency. Model it as an object collaborator with injected state - No ability state on ability objects — abilities are shared singletons; state lives in
AbilityData/AbilityAttributeper holder - No direct entity casts — use
instanceofpattern matching:if (entity instanceof Player p) { ... } - No McRPG-specific logic in McCore — McCore changes affect all downstream plugins
- No fully-qualified type references in method bodies — always use a top-level
importstatement; inline package paths (e.g.org.bukkit.Location loc) are forbidden - Plugin logger only — every logger call must come through
McRPG.getInstance().getLogger(). Do not useplayer.getServer().getLogger(),Bukkit.getLogger(), orLogger.getLogger(<Class>.class.getName())— those bypass the plugin logger prefix and any plugin-scoped log handlers - No
e.printStackTrace()— always useMcRPG.getInstance().getLogger().log(Level.SEVERE, "context message", e)so stack traces route through the plugin logger and are preserved in log aggregators - No
Optional.get()without a guard — useorElse,orElseGet,orElseThrow, or anisPresent()check first; bare.get()crashes on the empty path - No Bukkit API calls from async threads — world, entity, and inventory mutations must be scheduled on the main thread via
Bukkit.getScheduler().runTask(plugin, () -> { ... }) - No blocking
.get()/.join()on aCompletableFuturefrom the main thread — deadlocks when the future's completion path needs the main-thread scheduler - No entity or player object references in long-lived collections — store
UUIDinstead; holdingEntity/Playerobjects prevents GC of unloaded entities - No unbounded
MaporSetfields without a documented eviction strategy — insert-only caches are memory leaks; document the cleanup lifecycle event in a Javadoc comment - No
putHolderOnCooldown()insidecomboActivate()— the combo listener (OnComboCompleteListener) manages cooldown application for combo-activated abilities. Calling it inside the ability causes double-cooldown - No void-return
comboActivate()oractivateAbility()— both returnboolean(true= executed,false= internally cancelled). The boolean enables mana refund and conditional cooldown in callers - No
ConfigurableTierableAbilitywithout@ParserConfigKeys— every concrete implementation must declare its Parser-backed YAML keys via the annotation. TheParserConfigKeysPresenceTestenforces this at CI time - No raw
Bukkit.getScheduler()for repeating or delayed tasks — use McCore'sCoreTaskhierarchy (RepeatableCoreTask,DelayableCoreTask,ExpireableCoreTask, etc.) which provides state tracking, pause/resume, and consistent second-based timing. Direct scheduler calls are acceptable only for one-shot main-thread rescheduling from async code - No roadmap/LLD phase references in Javadoc or comments — labels like "Phase 1", "Phase 2 LLD", or "Future phases" rot immediately: they are meaningless to engineers who weren't present when the work was planned. Describe the what and why of the code instead. For extension points, name the type or mechanism (e.g.
ContentExpansion,QuestTemplate). For deprecated code, explain what changed architecturally, not when in a delivery roadmap it changed. The only acceptable "phase" language is within inline comments describing the sequential steps of a single async operation (e.g.// Phase 1 (DB executor): load,// Phase 2 (main thread): generate).
GUI Color Palette
All player-facing text uses the Warm Fantasy RPG palette defined in PALETTE.md. Colors are runtime-resolvable placeholders — locale YAML files use semantic names like <primary>, and McRPGLocalizationManager replaces them with configured MiniMessage values before parsing. Server owners customize colors in config.yml's palette section. Never introduce a new color without adding it to the palette first.
The palette is dynamic: McRPGLocalizationManager.buildPaletteReplacements() iterates every key in the palette: YAML section at startup (and on reload). Server owners can add arbitrary custom keys alongside the built-in roles — any key they define becomes a usable placeholder in locale files (e.g., adding my-guild-color: "<color:#FF00FF>" enables <my-guild-color> everywhere). This is driven by MainConfigFile.PALETTE_SECTION, the single route that triggers palette reloads.
| Role | Placeholder | Default Value | When to Use |
|---|---|---|---|
| gui-title | <gui-title> |
<color:#8B6914> |
GUI inventory titles (high contrast against the white title bar) |
| primary | <primary> |
<color:#D4A76A> |
Nav item names, stat value highlights, section headers |
| hint | <hint> |
<color:#E8C97A> |
Click hints, calls-to-action, interactive prompts |
| mana | <mana> |
<color:#5EA8FF> |
Mana cost values, mana-related lore |
| ability-active | <ability-active> |
<color:#FF7B5E> |
Active (ComboActivatable) ability names and type tags |
| ability-passive | <ability-passive> |
<color:#7FB87F> |
Passive ability names and type tags |
| ability-innate | <ability-innate> |
<color:#A78BCA> |
Innate ability names (always-on, no unlock required) |
| body | <body> |
<gray> |
Descriptive lore text, labels before values |
| positive | <positive> |
<green> |
Enabled, success, accepted |
| negative | <negative> |
<red> |
Disabled, error, deny, abandon |
| warning | <warning> |
<yellow> |
Caution, expiration, approaching limits |
| skill-swords | <skill-swords> |
<color:#C75050> |
Swords skill name in any player-facing surface |
| skill-mining | <skill-mining> |
<color:#7AAFC9> |
Mining skill name |
| skill-herbalism | <skill-herbalism> |
<color:#6DB86D> |
Herbalism skill name |
| skill-woodcutting | <skill-woodcutting> |
<color:#B8874B> |
Woodcutting skill name |
Forbidden in new code: <gold> (replaced by <primary>), <red> for item names (use <primary> or type placeholder), <black> for titles (use <gui-title>), <primary> for titles (use <gui-title>), raw hex codes in locale YAML (use palette placeholders).
Click-hint format rule
All click instructions in GUI lore use the verb-only <hint> format: color only the click-type verb with <hint>, leave the rest of the sentence in <body>:
# Correct — verb-only highlight
- '<hint>Left-click <body>to edit this ability.'
- '<hint>Right-click <body>to configure.'
- '<hint>Click <body>to view details.'
# Wrong — whole line colored
- '<hint>Click to configure' # entire hint is <hint>
- '<body>Click to edit' # click verb colored same as body text
- '<body><hint>Click</hint> to ...' # inline tag wrapping the verb only
Semantic overrides replace <hint> on the verb for clearly destructive or strongly positive actions:
- Destructive (abandon, disable, delete):
<negative>Right-click <body>to abandon/<negative>Click <body>to disable - Positive/acceptance (enable, accept, confirm add):
<positive>Click <body>to accept/<positive>Click <body>to enable
Hyphenation: compound click types are always hyphenated — Left-click, Right-click, Shift-click.
Ability name color rule
Every ability's name: field in its locale YAML must use the correct type placeholder — never <red>, <gold>, or a raw hex code:
| Ability type | Required placeholder |
|---|---|
ComboActivatable |
<ability-active> |
PassiveAbility + ABILITY_UNLOCKED_ATTRIBUTE in applicable attributes |
<ability-passive> |
| All others (always-on innate passives) | <ability-innate> |
The AbilityNameColorConsistencyTest enforces this for all bundled abilities at CI time. Third-party expansions should include an equivalent test for their own locale YAML files.
Skill name color rule
Every skill's name: field in its locale YAML must use the per-skill palette placeholder — never <gold>, <primary>, or a raw hex code:
| Skill | Required placeholder |
|---|---|
| Swords | <skill-swords> |
| Mining | <skill-mining> |
| Herbalism | <skill-herbalism> |
| Woodcutting | <skill-woodcutting> |
Third-party skills should define their own skill-{key} entry in config.yml and use it in their locale's name: field.
The SkillNameColorConsistencyTest enforces this for all bundled skills at CI time.
Skill.getColoredName() API
Skill.getColoredName(McRPGPlayer) returns the fully resolved, palette-colored skill name string for display. ConfigurableSkill resolves it from the locale name: field (which carries the <skill-{key}> tag) so the palette substitution happens in place. All callsites that show a skill name in player-facing text — GUI titles, ability lore, action bar, commands — must use getColoredName() instead of getName(). The only exception is when the name will be used as a command argument (e.g., player.performCommand), where plain text is required and getName() is correct.
Test Naming Conventions
@DisplayNameformat: Descriptive label that clearly communicates the test's intent. Examples:"Given a registered key, when getting by key, then returns the statistic","throws when the manager is already registered","DISABLED cycles to ENABLED"- Method naming:
action_outcome_whenCondition(the_whenConditionsuffix is optional when obvious). Examples:getNextSetting_disabled_cyclesToEnabled,getBaseValue_returnsDefault,fromString_unknownValue_returnsEmpty @Nestedclasses: Group tests by class-under-test or logical section with@Nested+@DisplayName- Parameterized tests: Prefer
@ParameterizedTest+@EnumSourceover manual loops for enum variant coverage
Mana Balance Framework
Active abilities are balanced using a "Slow Regen, High Stakes" philosophy. Key parameters: 100 max pool, 2/sec passive regen (50s full recovery), three cost buckets:
| Bucket | T1 Cost | T5 Cost | When to Use |
|---|---|---|---|
| Light | 28-32 | 12-16 | Combat, mobility, quick single-use effects |
| Medium | 42-50 | 25-33 | Buffs, sustain, moderate utility |
| Heavy | 70-80 | 55-60 | Powerful utility, resource generation, AoE gathering |
Mana is the primary gate — cooldowns are anti-spam only for Light abilities and buffer-overlap prevention for Medium/Heavy. Do not balance abilities using cooldowns as the main gate — this incentivizes loadout swapping.
When designing or rebalancing abilities, load .cursor/rules/mana-balance-philosophy.mdc for the full cookie-cutter process, formula patterns, validation constraints, and agent workflow instructions.
Maintenance: If you introduce a new pattern, naming convention, or anti-pattern not described here, update this file and
CLAUDE.mdin the same PR.