Skip to content

Commit 18596cb

Browse files
committed
feat: add idea chip feature to research mechanics
- Introduced an optional `idea_chip` field in `ResearchDefinition` to gate research. - Implemented `IdeaChipMatcher` for partial matching of idea chips based on item components. - Updated `ResearchTableBlockEntity` to include an idea chip inventory slot and validate its presence when starting research. - Modified `ResearchTableMenu` to register the new idea chip slot. - Enhanced `ResearchTableScreen` to render the idea chip slot with contextual feedback and tooltips. - Added command `/researchcube giveChip` to distribute idea chips to players. - Updated example research JSON to include an idea chip requirement. - Documented the idea chip feature in `DATAPACK.md` for clarity on usage and implementation.
1 parent f7ac768 commit 18596cb

13 files changed

Lines changed: 469 additions & 21 deletions

File tree

.github/copilot-instructions.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@
4747
| Package | Purpose |
4848
|---|---|
4949
| `com.researchcube` | `ResearchCubeMod` — entry point, `rl()` helper, DeferredRegister wiring, `IFluidHandler` capability registration |
50-
| `block` | `ResearchTableBlock` (opens menu on right-click), `ResearchTableBlockEntity` (GeoBlockEntity, ticking, 10-slot inventory + FluidTank, `getUpdateTag`/`getUpdatePacket` for client sync), `DriveCraftingTableBlock`, `DriveCraftingTableBlockEntity`, `ProcessingStationBlock`, `ProcessingStationBlockEntity` |
50+
| `block` | `ResearchTableBlock` (opens menu on right-click), `ResearchTableBlockEntity` (GeoBlockEntity, ticking, 11-slot inventory + FluidTank, `getUpdateTag`/`getUpdatePacket` for client sync), `DriveCraftingTableBlock`, `DriveCraftingTableBlockEntity`, `ProcessingStationBlock`, `ProcessingStationBlockEntity` |
5151
| `item` | `DriveItem` (tiered, stores recipe IDs in CustomData, `isFull()` capacity check, foil effect, opens `DriveInspectorScreen` on right-click), `CubeItem` (tiered, validation only), `ResearchBookItem` (opens research book screen via packet), `ResearchChipItem`, `ResearchFluidBucketItem` (custom bucket for research fluids) |
52-
| `menu` | `ResearchTableMenu`10 BE slots + player inventory, 4-value `SimpleContainerData`, `completedResearch` set from buf; `DriveCraftingTableMenu` — drive crafting container; `ProcessingStationMenu` — processing station container |
52+
| `menu` | `ResearchTableMenu`11 BE slots + player inventory, 4-value `SimpleContainerData`, `completedResearch` set from buf; `DriveCraftingTableMenu` — drive crafting container; `ProcessingStationMenu` — processing station container |
5353
| `client` | `ModClientEvents` — registers screens + GeckoLib renderer + sound (Dist.CLIENT, MOD bus); `ClientSoundHandler` — starts/stops `ResearchStationSoundInstance`; `ClientResearchData` — client-side cache of completed research for JEI/EMI integration; `ResearchHudOverlay` — on-screen HUD showing active research progress |
5454
| `client/screen` | `ResearchTableScreen` — scrollable research list with tier colors + lock icons, prereq tooltip, fluid gauge, gradient progress bar, Start/Stop buttons; `DriveCraftingTableScreen`; `ProcessingStationScreen`; `ResearchBookScreen` — read-only research encyclopedia; `ResearchTreeScreen` — tree visualization; `DriveInspectorScreen` — shows recipes stored on a drive; `ScreenRenderHelper` — shared rendering utilities |
5555
| `client/renderer` | `ResearchStationModel`, `ResearchStationRenderer` — GeckoLib geo/animation/texture wiring |
@@ -59,11 +59,11 @@
5959
| `compat/jade` | `ResearchCubeJadePlugin`, `ResearchStationProvider`, `ProcessingStationProvider` — Jade block overlays |
6060
| `network` | `StartResearchPacket`, `CancelResearchPacket`, `WipeTankPacket`, `OpenResearchBookPacket`, `StartProcessingPacket` (all client→server); `SyncResearchProgressPacket` (server→client); `ModNetworking` (PayloadRegistrar) |
6161
| `recipe` | `DriveCraftingRecipe`, `DriveCraftingRecipeSerializer`, `ProcessingRecipe`, `ProcessingRecipeSerializer`, `ProcessingFluidStack` |
62-
| `research` | `ResearchDefinition` (id, tier, duration, prerequisites, itemCosts, fluidCost, recipePool, name, description, category), `ResearchRegistry`, `ResearchManager`, `ResearchTier` (with `maxRecipes` and `getColor()`), `ItemCost`, `FluidCost`, `WeightedRecipe`, `ResearchSavedData` |
62+
| `research` | `ResearchDefinition` (id, tier, duration, prerequisites, itemCosts, fluidCost, recipePool, name, description, category, ideaChip), `ResearchRegistry`, `ResearchManager`, `ResearchTier` (with `maxRecipes` and `getColor()`), `ItemCost`, `FluidCost`, `WeightedRecipe`, `ResearchSavedData` |
6363
| `research/prerequisite` | `Prerequisite` interface (with `describe()`), `AndPrerequisite`, `OrPrerequisite`, `SinglePrerequisite`, `NonePrerequisite`, `PrerequisiteParser` |
6464
| `research/criterion` | `CompleteResearchTrigger` — advancement criterion fired on research completion |
6565
| `registry` | `ModItems`, `ModBlocks`, `ModBlockEntities`, `ModMenus`, `ModCreativeTabs`, `ModRecipeTypes`, `ModRecipeSerializers`, `ModFluids`, `ModConfig`, `ModCriterionTriggers` |
66-
| `util` | `NbtUtil` (CustomData read/write), `TierUtil` (canResearch validation), `RecipeOutputResolver` |
66+
| `util` | `NbtUtil` (CustomData read/write), `TierUtil` (canResearch validation), `RecipeOutputResolver`, `IdeaChipMatcher` (partial ItemStack matching for idea chip validation) |
6767
| `event` | `ModServerEvents` (AddReloadListenerEvent) |
6868

6969
## Critical workflows
@@ -88,7 +88,7 @@
8888
- Register new game objects using `DeferredRegister` in `registry/Mod*` classes, then ensure they are registered in `ResearchCubeMod` constructor.
8989
- **Server authority**: never start/complete/cancel research from client code. Client screens send packets; server validates.
9090
- Preserve slot semantics in `ResearchTableBlockEntity` / `ResearchTableMenu`:
91-
- slot 0 = drive, slot 1 = cube, slots 2–7 = item costs, slot 8 = bucket_in, slot 9 = bucket_out (`SLOT_DRIVE=0`, `SLOT_CUBE=1`, `COST_SLOT_START=2`, `SLOT_BUCKET_IN=8`, `SLOT_BUCKET_OUT=9`, `TOTAL_SLOTS=10`).
91+
- slot 0 = drive, slot 1 = cube, slots 2–7 = item costs, slot 8 = bucket_in, slot 9 = bucket_out, slot 10 = idea_chip (`SLOT_DRIVE=0`, `SLOT_CUBE=1`, `COST_SLOT_START=2`, `SLOT_BUCKET_IN=8`, `SLOT_BUCKET_OUT=9`, `SLOT_IDEA_CHIP=10`, `TOTAL_SLOTS=11`).
9292
- When iterating cost slots, always loop `COST_SLOT_START` to `SLOT_BUCKET_IN` (exclusive), i.e. slots 2–7 only. Never include bucket slots in item-cost validation or consumption.
9393
- The block entity also holds a `FluidTank` (capacity `TANK_CAPACITY = 8000` mB). Fluid cost is validated and drained separately from item costs.
9494
- Enforce tier rules through `TierUtil.canResearch(cubeTier, driveTier, researchTier)` — cube tier ≥ research tier AND drive tier == research tier.

DATAPACK.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# ResearchCube Datapack Guide
2+
3+
## Research Definitions
4+
5+
Research definitions are JSON files placed in `data/{namespace}/research/{name}.json`. The file path determines the research ID (e.g., `data/example/research/crystal_analysis.json``example:crystal_analysis`).
6+
7+
### JSON Schema
8+
9+
```json
10+
{
11+
"name": "Display Name",
12+
"description": "Short description shown in tooltips.",
13+
"category": "grouping_category",
14+
"tier": "BASIC",
15+
"duration": 1200,
16+
"item_costs": [
17+
{ "item": "minecraft:redstone", "count": 16 }
18+
],
19+
"fluid_cost": {
20+
"fluid": "researchcube:thinking_fluid",
21+
"amount": 1000
22+
},
23+
"idea_chip": {
24+
"id": "researchcube:metadata_irrecoverable",
25+
"components": {
26+
"minecraft:custom_name": "\"Idea: My Research\"",
27+
"minecraft:custom_data": { "my_chip_id": "my_research" }
28+
}
29+
},
30+
"prerequisites": {
31+
"type": "ALL",
32+
"values": ["namespace:other_research"]
33+
},
34+
"recipe_pool": [
35+
"namespace:recipe_id",
36+
{ "id": "namespace:other_recipe", "weight": 3 }
37+
]
38+
}
39+
```
40+
41+
### Fields
42+
43+
| Field | Required | Description |
44+
|---|---|---|
45+
| `tier` | Yes | Research tier: `UNSTABLE`, `BASIC`, `ADVANCED`, `PRECISE`, `FLAWLESS`, `SELF_AWARE` |
46+
| `duration` | Yes | Duration in game ticks (20 ticks = 1 second) |
47+
| `name` | No | Human-readable display name (defaults to file name) |
48+
| `description` | No | Short description shown in tooltips |
49+
| `category` | No | Grouping category for the research list UI |
50+
| `item_costs` | No | Array of `{ "item": "<item_id>", "count": <n> }` |
51+
| `fluid_cost` | No | `{ "fluid": "<fluid_id>", "amount": <mB> }` |
52+
| `idea_chip` | No | ItemStack gating this research (see Idea Chips section) |
53+
| `prerequisites` | No | Prerequisite structure (`ALL`, `ANY`, or single ID) |
54+
| `recipe_pool` | No | Array of recipe IDs (string or `{ "id": ..., "weight": ... }`) |
55+
56+
### Research Tiers
57+
58+
| Tier | Required Fluid | Required Cube |
59+
|---|---|---|
60+
| UNSTABLE | Thinking Fluid | cube_unstable+ |
61+
| BASIC | Thinking Fluid | cube_basic+ |
62+
| ADVANCED | Pondering Fluid | cube_advanced+ |
63+
| PRECISE | Reasoning Fluid | cube_precise+ |
64+
| FLAWLESS | Reasoning Fluid | cube_flawless+ |
65+
| SELF_AWARE | Imagination Fluid | cube_self_aware |
66+
67+
The drive must exactly match the research tier. The cube must be equal or higher tier.
68+
69+
---
70+
71+
## Idea Chips
72+
73+
Idea chips allow pack developers to gate specific research behind a custom item that
74+
is placed in a dedicated slot in the Research Station. The chip is **consumed when
75+
research begins** and is **NOT refunded on cancel**.
76+
77+
### How It Works
78+
79+
1. Add an `"idea_chip"` field to your research definition JSON.
80+
2. The field uses standard `ItemStack` codec format (`"id"` + optional `"components"`).
81+
3. When a player selects that research, the Research Station UI shows the idea chip
82+
slot with a red border and tooltip indicating what chip is required.
83+
4. The player must place a matching item in the idea chip slot to enable the Start button.
84+
5. On research start, the chip is consumed (stack shrinks by 1).
85+
6. On cancel, item costs and fluid are refunded, but the idea chip is **not** refunded.
86+
87+
### Partial-Match Semantics
88+
89+
The idea chip uses **partial matching**: only the components explicitly declared in
90+
your JSON are checked. Components present on the player's item but not declared in
91+
the JSON are ignored.
92+
93+
This means you can declare just a `custom_name` to match by name, or just
94+
`custom_data` to match by a stable tag, or both. Default item components (like
95+
`max_stack_size`) do not need to be listed.
96+
97+
### Creating Chips for Players
98+
99+
Pack devs distribute idea chips to players via quests, loot tables, or give commands.
100+
101+
**Give command example:**
102+
```
103+
/give @p researchcube:metadata_irrecoverable[custom_name='"Idea: Forbidden Crystal"',custom_data={researchcube_chip_id:"forbidden_crystal"}]
104+
```
105+
106+
**Item modifier (loot function):**
107+
```json
108+
{
109+
"function": "minecraft:set_components",
110+
"components": {
111+
"minecraft:custom_name": "\"Idea: Forbidden Crystal\"",
112+
"minecraft:custom_data": { "researchcube_chip_id": "forbidden_crystal" }
113+
}
114+
}
115+
```
116+
117+
### Recommended Practice
118+
119+
- Use a unique `minecraft:custom_data` tag as a **stable ID** for matching
120+
(e.g., `{ "researchcube_chip_id": "my_research" }`).
121+
- Set a human-readable `minecraft:custom_name` for **player-facing display**
122+
(e.g., `"Idea: Advanced Alloys"`).
123+
- Using both ensures the chip is identifiable by players and reliably matched by the system.
124+
125+
### Example Research Definition with Idea Chip
126+
127+
```json
128+
{
129+
"name": "Forbidden Crystal Synthesis",
130+
"category": "materials",
131+
"tier": "ADVANCED",
132+
"duration": 4800,
133+
"item_costs": [
134+
{ "item": "minecraft:amethyst_shard", "count": 16 },
135+
{ "item": "minecraft:diamond", "count": 4 }
136+
],
137+
"fluid_cost": {
138+
"fluid": "researchcube:pondering_fluid",
139+
"amount": 2000
140+
},
141+
"idea_chip": {
142+
"id": "researchcube:metadata_irrecoverable",
143+
"components": {
144+
"minecraft:custom_name": "\"Idea: Forbidden Crystal\"",
145+
"minecraft:custom_data": { "researchcube_chip_id": "forbidden_crystal" }
146+
}
147+
},
148+
"prerequisites": {
149+
"type": "ALL",
150+
"values": ["example:crystal_resonance"]
151+
},
152+
"recipe_pool": [
153+
"example:resonant_crystal_recipe"
154+
]
155+
}
156+
```
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "Forbidden Crystal Synthesis",
3+
"description": "A dangerous experiment requiring a special authorization chip to begin.",
4+
"category": "materials",
5+
"tier": "ADVANCED",
6+
"duration": 4800,
7+
"item_costs": [
8+
{ "item": "minecraft:amethyst_shard", "count": 16 },
9+
{ "item": "minecraft:diamond", "count": 4 }
10+
],
11+
"fluid_cost": {
12+
"fluid": "researchcube:pondering_fluid",
13+
"amount": 2000
14+
},
15+
"idea_chip": {
16+
"id": "researchcube:metadata_irrecoverable",
17+
"components": {
18+
"minecraft:custom_name": "\"Idea: Forbidden Crystal\"",
19+
"minecraft:custom_data": { "researchcube_chip_id": "forbidden_crystal" }
20+
}
21+
},
22+
"prerequisites": {
23+
"type": "ALL",
24+
"values": ["example:crystal_resonance"]
25+
},
26+
"recipe_pool": [
27+
"example:resonant_crystal_recipe"
28+
]
29+
}

src/main/java/com/researchcube/block/ResearchTableBlockEntity.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.researchcube.research.criterion.CompleteResearchTrigger;
1212
import com.researchcube.util.NbtUtil;
1313
import com.researchcube.util.TierUtil;
14+
import com.researchcube.util.IdeaChipMatcher;
1415
import com.researchcube.network.SyncResearchProgressPacket;
1516
import net.minecraft.core.BlockPos;
1617
import net.minecraft.core.HolderLookup;
@@ -58,7 +59,10 @@
5859
* Slots:
5960
* 0 = Drive
6061
* 1 = Cube
61-
* 2+ = Item cost inputs (expandable)
62+
* 2-7 = Item cost inputs
63+
* 8 = Bucket input
64+
* 9 = Bucket output
65+
* 10 = Idea chip
6266
*
6367
* NBT:
6468
* ActiveResearch: ResourceLocation string of current research
@@ -72,7 +76,8 @@ public class ResearchTableBlockEntity extends BlockEntity implements GeoBlockEnt
7276
public static final int COST_SLOT_START = 2;
7377
public static final int SLOT_BUCKET_IN = 8;
7478
public static final int SLOT_BUCKET_OUT = 9;
75-
public static final int TOTAL_SLOTS = 10; // 2 fixed + 6 cost + 2 bucket slots
79+
public static final int SLOT_IDEA_CHIP = 10;
80+
public static final int TOTAL_SLOTS = 11; // 2 fixed + 6 cost + 2 bucket + 1 idea chip
7681
public static final int TANK_CAPACITY = 8000; // 8 buckets (in mB)
7782

7883
// GeckoLib animation
@@ -297,6 +302,17 @@ public boolean tryStartResearch(String researchId, Set<String> completedResearch
297302
}
298303
}
299304

305+
// Validate idea chip (if defined)
306+
if (definition.getIdeaChip().isPresent()) {
307+
ItemStack required = definition.getIdeaChip().get();
308+
ItemStack candidate = inventory.getStackInSlot(SLOT_IDEA_CHIP);
309+
if (!IdeaChipMatcher.matches(required, candidate)) {
310+
ResearchCubeMod.LOGGER.warn("[ResearchCube] Cannot start '{}': missing idea chip: {}",
311+
researchId, required.getHoverName().getString());
312+
return false;
313+
}
314+
}
315+
300316
// All checks passed — snapshot costs for potential refund, then consume
301317
this.consumedCosts = definition.getItemCosts();
302318
this.consumedFluidCost = fluidCost;
@@ -308,6 +324,11 @@ public boolean tryStartResearch(String researchId, Set<String> completedResearch
308324
fluidTank.drain(toDrain, IFluidHandler.FluidAction.EXECUTE);
309325
}
310326

327+
// Consume idea chip (not refunded on cancel — it is the entry price)
328+
if (definition.getIdeaChip().isPresent()) {
329+
inventory.getStackInSlot(SLOT_IDEA_CHIP).shrink(1);
330+
}
331+
311332
// Start research
312333
this.activeResearchId = researchId;
313334
this.startTime = level.getGameTime();
@@ -563,6 +584,7 @@ private void sendClearPacket(ServerLevel serverLevel, String key) {
563584
/**
564585
* Cancel active research and refund item costs back into cost slots.
565586
* Called from CancelResearchPacket handler.
587+
* Note: the idea chip is NOT refunded — it was consumed as the entry price.
566588
*/
567589
public void cancelResearchWithRefund() {
568590
if (!isResearching()) return;

src/main/java/com/researchcube/client/screen/ResearchBookScreen.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,13 @@ private void renderEntryTooltip(GuiGraphics graphics, int mouseX, int mouseY, in
294294
}
295295
}
296296

297+
// Idea chip requirement
298+
if (def.getIdeaChip().isPresent()) {
299+
ItemStack chip = def.getIdeaChip().get();
300+
tooltip.add(Component.literal("Idea Chip: " + chip.getHoverName().getString())
301+
.withStyle(s -> s.withColor(0xFFAA55)));
302+
}
303+
297304
// Prerequisites
298305
if (!(def.getPrerequisites() instanceof NonePrerequisite)) {
299306
boolean met = def.getPrerequisites().isSatisfied(completedResearch);

0 commit comments

Comments
 (0)