Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 44 additions & 33 deletions .claude/commands/new-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ Before writing any code, determine from the user's description:

1. **Block name** (snake_case ID, e.g., `lava_generator`)
2. **Display name** (e.g., "Lava Generator")
3. **System type**: `power` or `fluid` — determines base class (`PowerBlock` or `FluidBlock`) and which registry/factory/dialog to integrate with
3. **System type**: `power`, `fluid`, `transport`, or `utility` — determines base class (`PowerBlock`, `FluidBlock`, `TransportBlock`) and which registry/factory/dialog to integrate with. Utility blocks use the power system (extend `PowerBlock`) but live in the `utility` package.
4. **Placement type**: `SIMPLE` (no direction), `DIRECTIONAL` (faces toward placement direction), or `DIRECTIONAL_OPPOSITE` (faces away)
5. **Visual states**: What variants does the block have? (e.g., inactive/active, charge levels, facing directions)
6. **Key properties**: maxStorage, updateIntervalTicks, canReceivePower, etc.
7. **Behavior**: What happens during powerUpdate()/fluidUpdate()?
7. **Behavior**: What happens during powerUpdate()/fluidUpdate()/transportUpdate()?
8. **Recipe ingredients**: What materials craft it?
9. **Gradient colors** for the item name (hex pair, e.g., `#FF4500:#FF8C00`)

Expand Down Expand Up @@ -51,15 +51,15 @@ class {ClassName}(location: Location) : {BaseClass}(location, maxStorage = {N})
// Add variant IDs as needed, e.g.:
// const val BLOCK_ID_ACTIVE = "{block_id}_active"

val descriptor = BlockDescriptor(
baseBlockId = BLOCK_ID,
displayName = "{Display Name}",
description = "{short description}",
placementType = PlacementType.SIMPLE,
directionalVariants = emptyMap(),
allRegistrableIds = listOf(BLOCK_ID), // include all variant IDs
constructor = { loc, _ -> {ClassName}(loc) }
)
val descriptor =
BlockDescriptor(
baseBlockId = BLOCK_ID,
displayName = "{Display Name}",
description = "{short description}",
placementType = PlacementType.SIMPLE,
additionalBlockIds = listOf(), // include any variant IDs beyond the base
constructor = { loc, _ -> {ClassName}(loc) },
)
}

override val baseBlockId: String = BLOCK_ID
Expand All @@ -72,17 +72,21 @@ class {ClassName}(location: Location) : {BaseClass}(location, maxStorage = {N})
}
```

**DIRECTIONAL block** (like PowerCable, FluidPipe): Include `facing` property, `DIRECTIONAL_IDS` map for all 6 faces (NORTH, SOUTH, EAST, WEST, UP, DOWN), and use `ID_TO_FACING` reverse lookup. Constructor takes `(Location, BlockFace)`.
**DIRECTIONAL block** (like PowerCable, FluidPipe): Include `facing` property via `override val facing: BlockFace` in the constructor. Constructor takes `(Location, BlockFace)`. Use `ADJACENT_FACES` (inherited from `AtlasBlock`) when iterating over all 6 block faces — never hardcode the face list.

**DIRECTIONAL_OPPOSITE block** (like SmallDrill): Same directional pattern but placement type is `DIRECTIONAL_OPPOSITE`.

### Important code patterns

- **Fluid transfer ordering**: When transferring fluid between blocks, always `removeFluid()` first to capture the value, then call `target.storeFluid(fluid)`. If `storeFluid` fails, restore with `storeFluid(fluid)` on self. Never pass `storedFluid` directly to another block's `storeFluid()` — the mutable field could become stale.
- **Face iteration**: Use the inherited `ADJACENT_FACES` constant from `AtlasBlock` instead of creating a new `listOf(BlockFace.NORTH, ...)`. Filter it as needed (e.g., `ADJACENT_FACES.filter { it != facing.oppositeFace }`).
- **BlockDescriptor fields**: The descriptor uses `additionalBlockIds` (not `allRegistrableIds`) for variant IDs beyond the base. The total registered count = 1 (base) + additionalBlockIds.size.

### Step 3: Add CraftEngine Block Configuration

Create `src/main/resources/atlas/configuration/{block_id}.yml`.

Follow the CraftEngine configuration format. Each variant gets its own `items` section (use `items#1`, `items#2`, etc. for additional variants). The base variant uses `loot: template: default:loot_table/self`, while other variants use explicit loot pools that drop the base item.

Reference existing configuration files (e.g., `small_solar_panel.yml`, `lava_generator.yml`) for the exact format. Here's the general structure:
Follow the CraftEngine configuration format. Reference existing configuration files for the exact format. Here's the general structure for a **non-directional** block:

```yaml
items:
Expand Down Expand Up @@ -121,13 +125,12 @@ items:
side: minecraft:block/custom/{block_id}_side
```

For additional variant entries (active states, directional, etc.), add `items#1`, `items#2`, etc. sections with explicit loot that drops the base item:
```yaml
items#1:
atlas:{block_id}_{variant}:
# ... same structure but with:
# loot pools that drop atlas:{block_id} (the base item)
```
For **directional** blocks, use `states` with `properties`, `appearances`, and `variants` sections. Reference `power_splitter.yml` (facing + boolean property), `fluid_merger.yml` (facing + string property), or `fluid_pipe.yml` for the exact format. Key pattern:
- Define properties (facing: direction, plus any state property like powered/fluid)
- Define appearances for each direction (north uses full generation block, south/east/west use y-rotation, up/down use separate models with remapped textures)
- Map variants to appearances

For additional variant entries (active states, etc.), add `items#1`, `items#2`, etc. sections with explicit loot that drops the base item.

### Step 4: Add Recipe

Expand All @@ -146,16 +149,20 @@ recipes:
count: 1
```

Ensure the recipe ingredients are unique — no two blocks should have identical shapeless recipes.

### Step 5: Register in Atlas.kt

Add the descriptor to `src/main/kotlin/com/coderjoe/atlas/Atlas.kt`:

- **Power blocks**: Add `com.coderjoe.atlas.power.block.{ClassName}.descriptor` to the list in `powerDescriptors()`
- **Fluid blocks**: Add `com.coderjoe.atlas.fluid.block.{ClassName}.descriptor` to the list in `fluidDescriptors()`
- **Power blocks**: Add to the list in `powerDescriptors()`
- **Fluid blocks**: Add to the list in `fluidDescriptors()`
- **Transport blocks**: Add to the list in `transportDescriptors()`
- **Utility blocks**: Add to `powerDescriptors()` (utility blocks use the power system)

### Step 6: Add Dialog Integration

For **power blocks**, edit `src/main/kotlin/com/coderjoe/atlas/power/PowerBlockDialog.kt`:
For **power blocks** (including utility), edit `src/main/kotlin/com/coderjoe/atlas/power/PowerBlockDialog.kt`:
1. Add import for the new class
2. Add `is {ClassName} -> "{Display Name}"` case in `getBlockDisplayName()`
3. Add description case in `getBlockDescription()`:
Expand All @@ -166,21 +173,23 @@ For **power blocks**, edit `src/main/kotlin/com/coderjoe/atlas/power/PowerBlockD

For **fluid blocks**, edit `src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockDialog.kt` following the same pattern.

For **transport blocks**, edit `src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockDialog.kt` following the same pattern.

### Step 7: Update Tests

1. **Update `TestHelper.initPowerFactory()`** (or `initFluidFactory()`) in `src/test/kotlin/com/coderjoe/atlas/TestHelper.kt` — add the new descriptor to the registration list.
1. **Update `TestHelper.initPowerFactory()`** (or `initFluidFactory()` / `initTransportFactory()`) in `src/test/kotlin/com/coderjoe/atlas/TestHelper.kt` — add the new descriptor to the registration list.

2. **Update block count assertions** in:
- `src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt` — update the count in `power system initializes with N block types` (or fluid equivalent)
- `src/test/kotlin/com/coderjoe/atlas/power/PowerBlockInitializerTest.kt` (or fluid equivalent) — update the count and comment
- `src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt` — update the count in the relevant `{system} system initializes with N block types` test
- `src/test/kotlin/com/coderjoe/atlas/power/PowerBlockInitializerTest.kt` (or fluid/transport equivalent) — update the count and comment

The count = total number of IDs across all `allRegistrableIds` lists. Add the number of new variant IDs to the existing count.
The count = total number of registered IDs (1 base + additionalBlockIds per block). Add the number of new IDs to the existing count.

3. **Create block test file** at `src/test/kotlin/com/coderjoe/atlas/{system}/{ClassName}Test.kt` with tests for:
- Key properties (maxStorage, canReceivePower, etc.)
- Visual state transitions
- Core behavior (powerUpdate/fluidUpdate logic)
- Descriptor properties (baseBlockId, displayName, allRegistrableIds count)
- Core behavior (powerUpdate/fluidUpdate/transportUpdate logic)
- Descriptor properties (baseBlockId, displayName)
- Edge cases (full storage, no adjacent blocks, wrong fluid type, etc.)

### Step 8: Generate Placeholder Textures
Expand All @@ -200,9 +209,11 @@ Name textures following the convention: `{block_id}_{face}.png`, `{block_id}_{fa

Before finishing, verify:
- [ ] Block class created with correct descriptor
- [ ] All variant IDs listed in `allRegistrableIds`
- [ ] `additionalBlockIds` includes all variant IDs beyond the base
- [ ] Uses `ADJACENT_FACES` constant (not hardcoded face lists) when iterating faces
- [ ] Fluid transfers use remove-first-then-store pattern
- [ ] CraftEngine configuration YAML created with all variants
- [ ] Recipe added in the configuration file
- [ ] Recipe added in the configuration file (with unique ingredients)
- [ ] Descriptor registered in Atlas.kt
- [ ] Dialog cases added
- [ ] TestHelper updated with new descriptor
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/com/coderjoe/atlas/Atlas.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.coderjoe.atlas.fluid.block.FluidContainer
import com.coderjoe.atlas.fluid.block.FluidMerger
import com.coderjoe.atlas.fluid.block.FluidPipe
import com.coderjoe.atlas.fluid.block.FluidPump
import com.coderjoe.atlas.fluid.block.FluidSplitter
import com.coderjoe.atlas.guide.GuideBook
import com.coderjoe.atlas.guide.GuideBookListener
import com.coderjoe.atlas.power.PowerBlock
Expand Down Expand Up @@ -218,6 +219,7 @@ class Atlas : JavaPlugin() {
FluidPipe.descriptor,
FluidContainer.descriptor,
FluidMerger.descriptor,
FluidSplitter.descriptor,
).associateBy { it.baseBlockId }
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.coderjoe.atlas.fluid.block.FluidContainer
import com.coderjoe.atlas.fluid.block.FluidMerger
import com.coderjoe.atlas.fluid.block.FluidPipe
import com.coderjoe.atlas.fluid.block.FluidPump
import com.coderjoe.atlas.fluid.block.FluidSplitter
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import net.kyori.adventure.text.format.TextDecoration
Expand Down Expand Up @@ -49,6 +50,7 @@ object FluidBlockDialog {
is FluidPipe -> "Fluid Pipe (${fluidBlock.facing.displayName()})"
is FluidContainer -> "Fluid Container (${fluidBlock.facing.displayName()})"
is FluidMerger -> "Fluid Merger (${fluidBlock.facing.displayName()})"
is FluidSplitter -> "Fluid Splitter (${fluidBlock.facing.displayName()})"
else -> "Fluid Block"
}

Expand Down Expand Up @@ -95,6 +97,9 @@ object FluidBlockDialog {
is FluidMerger ->
Component.text("Merger - merges fluid from all sides, outputs in facing direction")
.color(NamedTextColor.GRAY)
is FluidSplitter ->
Component.text("Splitter - distributes fluid to all adjacent faces")
.color(NamedTextColor.GRAY)
else ->
Component.text("Fluid block")
.color(NamedTextColor.GRAY)
Expand Down
82 changes: 82 additions & 0 deletions src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidSplitter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.coderjoe.atlas.fluid.block

import com.coderjoe.atlas.atlasInfo
import com.coderjoe.atlas.core.BlockDescriptor
import com.coderjoe.atlas.core.CraftEngineHelper
import com.coderjoe.atlas.core.PlacementType
import com.coderjoe.atlas.fluid.FluidBlock
import com.coderjoe.atlas.fluid.FluidBlockRegistry
import com.coderjoe.atlas.fluid.FluidType
import org.bukkit.Location
import org.bukkit.block.BlockFace

class FluidSplitter(location: Location, override val facing: BlockFace) : FluidBlock(location) {
override val updateIntervalTicks: Long = 20L

companion object {
const val BLOCK_ID = "atlas:fluid_splitter"

val descriptor =
BlockDescriptor(
baseBlockId = BLOCK_ID,
displayName = "Fluid Splitter",
description = "Splitter - distributes fluid to all adjacent faces",
placementType = PlacementType.DIRECTIONAL,
constructor = { loc, facing -> FluidSplitter(loc, facing) },
)
}

override val baseBlockId: String = BLOCK_ID

override fun getVisualStateBlockId(): String = BLOCK_ID

private fun updateFluidState() {
val fluidValue =
when (storedFluid) {
FluidType.WATER -> "water"
FluidType.LAVA -> "lava"
FluidType.NONE -> "none"
}
CraftEngineHelper.setStringProperty(location, "fluid", fluidValue)
}

override fun fluidUpdate() {
val registry = FluidBlockRegistry.instance ?: return

if (!hasFluid()) {
val source = registry.getAdjacentFluidBlock(location, facing.oppositeFace)

if (source != null && source.canProvideFluid(facing)) {
val fluid = source.removeFluid()
storeFluid(fluid)
plugin.logger.atlasInfo(
"FluidSplitter at ${location.blockX},${location.blockY},${location.blockZ} " +
"pulled ${fluid.name} from ${source::class.simpleName}",
)
}
}

if (hasFluid()) {
val outputFaces = ADJACENT_FACES.filter { it != facing.oppositeFace }

for (face in outputFaces) {
if (!hasFluid()) break
val target = registry.getAdjacentFluidBlock(location, face) ?: continue
if (!target.hasFluid()) {
val fluid = removeFluid()
if (target.storeFluid(fluid)) {
plugin.logger.atlasInfo(
"FluidSplitter at ${location.blockX},${location.blockY},${location.blockZ} " +
"pushed ${fluid.name} to ${target::class.simpleName} at ${face.name}",
)
} else {
storeFluid(fluid)
}
break
}
}
}

updateFluidState()
}
}
Loading
Loading