Skip to content

Moon Villages Overhaul & Gameplay Expansion - #565

Draft
gregorixG wants to merge 17 commits into
TeamGalacticraft:mainfrom
gregorixG:feature/moon-villages
Draft

gregorixG wants to merge 17 commits into
TeamGalacticraft:mainfrom
gregorixG:feature/moon-villages

Conversation

@gregorixG

@gregorixG gregorixG commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR evolves Moon highlands villages from simple structure generation into a fully integrated gameplay system.

It introduces custom Moon villagers, a dedicated village ecosystem (population, protection, and behavior), new professions and trades, supporting POIs/workstations, and a comprehensive refresh of village world generation. The result is a more complete, self-contained settlement experience with progression and interaction systems.

Key Features

Moon Villager System

  • Introduces MoonVillagerEntity with Moon-specific AI and persistence.
  • Adds job-site and home-anchor logic, allowing villagers to claim and return to Moon POIs.
  • Implements custom restocking behavior centered around claimed workstations rather than relying solely on vanilla village mechanics.
  • Adds a dynamic Lunar Home Anchor rendering system:
    • The anchor blends into generated houses instead of appearing as a fixed marker block.
    • Scans a 3×3 area around itself and selects the most common solid neighboring block.
    • Renders using the selected block state to match the local structure palette.
    • Updates when placed or when nearby blocks change, ensuring it stays visually consistent.
    • Continues to function as the hidden home/protection marker used by villagers and village defense systems.

Professions & Economy

  • Adds six Moon-specific professions:
    • Lunar Cartographer
    • Lunar Engineer
    • Lunar Mechanic
    • Lunar Cheese Maker
    • Lunar Provisioner
    • Lunar Botanist
  • Integrates corresponding POIs and workstations.
  • Expands the trade economy using lunar sapphires and introduces late-tier rewards (schematics, utilities, workstation-related items).

Village Progression

  • Introduces the Village Access Key as a progression mechanic:
    • Sold by all Moon professions at Tier 5.
    • Must be equipped in the Galacticraft accessory slot.
    • Unlocks meteoric iron doors in village structures.
  • Access to private village buildings is now progression-based rather than granted by default.

Village Systems

Population & Spawning

  • Adds Moon-specific spawning and population systems for:
    • Villagers
    • Golems
    • Passive animals
  • Introduces Moon variants of cows, sheep, and chickens with full support (renderers, loot tables, attributes, spawn rules, oxygen visuals).

Protection Mechanics

  • Villages react to player behavior:
    • Breaking blocks near a Lunar Home Anchor without a Village Access Key flags the player as hostile.
    • Nearby Moon golems will target the player.
    • If no defender is present, a persistent Moon golem is spawned behind the intruder as an immediate response.

Golem Behavior

  • Moon villages maintain a persistent defender:
    • At least one Moon golem is seeded per village if none are nearby.
    • Golems are customized for Moon conditions:
      • Can survive in space
      • Increased movement speed, step height, and follow range
      • Persistent aggro targets across saves
      • Roof-chasing jump logic to prevent trivial evasion

Content & Assets

  • Adds new Moon village-related blocks, items, and villager workbenches:

    • Lunar cheese press
    • Lunar herbalist table
    • Lunar smithing table
    • Lunar workbench
    • Lunar home anchor
    • Meteoric iron door
    • Moon glass / olivine glass variants
    • Village Access Key (assets and data)
  • Also includes:

    • Client-side rendering and block entity rendering support
    • Updated loot tables, tags, translations, and textures

World Generation

  • Registers and updates Moon village worldgen:
    • Structure definitions and structure sets
    • Highlands start pools
    • House pools (including large variants)
    • Street, decor, sapling, and villager pools
  • Refreshes structure templates for improved cohesion and completeness.
  • Fixes outdated/broken jigsaw references to align with current structure data.

Notes

  • Village access and safety are now tied to progression rather than assumed player trust.
  • The system is designed to make Moon villages feel alive, reactive, and worth engaging with beyond initial discovery.

Credit to SpatialX for all buildings and a few really good ideas

@gregorixG
gregorixG force-pushed the feature/moon-villages branch from 012a7b3 to 8ef38ae Compare April 20, 2026 11:02

@Roelymole Roelymole left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are a few thoughts to get you started. A big theme is avoiding using mixins where we don't need them.

Comment thread src/main/java/dev/galacticraft/mod/structure/GCStructureTemplatePools.java Outdated
Comment thread src/main/java/dev/galacticraft/mod/content/GCBlocks.java Outdated
Comment thread src/main/java/dev/galacticraft/mod/mixin/VillagerMixin.java Outdated
Comment thread src/main/java/dev/galacticraft/mod/mixin/ServerLevelMixin.java Outdated
Comment thread src/main/java/dev/galacticraft/mod/content/entity/MoonVillagerEntity.java Outdated
Comment thread src/main/java/dev/galacticraft/mod/mixin/BeardifierMixin.java Outdated
@gregorixG
gregorixG force-pushed the feature/moon-villages branch 4 times, most recently from b9ce6f2 to aaa11f4 Compare May 27, 2026 19:12

@Roelymole Roelymole left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are a few more thoughts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the lines in this file are indented too much, but like I said before, the Moon versions of vanilla animals don't really fit.

Again, they should probably be removed for now and we can come up with something that makes more later.

Comment on lines +107 to +116
public static boolean hasVillageAccessKey(Player player) {
Container accessories = player.galacticraft$getAccessories();
for (int slot = 0; slot < accessories.getContainerSize(); ++slot) {
ItemStack stack = accessories.getItem(slot);
if (stack.is(GCItemTags.VILLAGE_ACCESS_KEYS)) {
return true;
}
}
return false;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should maybe also check whether the player has the key in one of their hands, in case they don't have enough room in their accessory slots.

Comment on lines 535 to +538
this.tag(ConventionalItemTags.VILLAGER_JOB_SITES)
.add(GCBlocks.LUNAR_CARTOGRAPHY_TABLE.asItem());
.add(GCBlocks.LUNAR_CARTOGRAPHY_TABLE.asItem())
.add(GCBlocks.LUNAR_WORKBENCH.asItem())
.add(GCBlocks.LUNAR_SMITHING_TABLE.asItem());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have added the Lunar Cheese Press to the #c:villager_job_sites block tag, but not the item tag. I'm guessing this was just an oversight.

// COMPACT MINERAL BLOCKS
this.block(GCBlocks.SILICON_BLOCK, "Block of Silicon");
this.block(GCBlocks.METEORIC_IRON_BLOCK, "Block of Meteoric Iron");
this.block(GCBlocks.METEORIC_IRON_DOOR, "Meteoric Iron Door");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This translation doesn't really belong in the middle of all the compact mineral block translations e.g. "Block of ..."

It might be better to find somewhere else for it to go, such as the misc decor section.

"NoiseBasedChunkGeneratorMixin",
"PlayerMixin",
"ServerLevelMixin",
"WalkNodeEvaluatorMixin",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These mixins are listed in alphabetical order, so "WalkNodeEvaluatorMixin" should be last.

Comment on lines +617 to +641
private void createTallDoorTop(BlockModelGenerators generator, Block topBlock, Block baseDoor) {
generator.blockStateOutput.accept(MultiVariantGenerator.multiVariant(topBlock).with(PropertyDispatch.properties(MeteoricIronDoorTopBlock.FACING, MeteoricIronDoorTopBlock.HINGE, MeteoricIronDoorTopBlock.OPEN)
.select(Direction.EAST, net.minecraft.world.level.block.state.properties.DoorHingeSide.LEFT, false, doorTopVariant(baseDoor, "_top_cap_left", VariantProperties.Rotation.R0))
.select(Direction.EAST, net.minecraft.world.level.block.state.properties.DoorHingeSide.LEFT, true, doorTopVariant(baseDoor, "_top_cap_left_open", VariantProperties.Rotation.R90))
.select(Direction.EAST, net.minecraft.world.level.block.state.properties.DoorHingeSide.RIGHT, false, doorTopVariant(baseDoor, "_top_cap_right", VariantProperties.Rotation.R0))
.select(Direction.EAST, net.minecraft.world.level.block.state.properties.DoorHingeSide.RIGHT, true, doorTopVariant(baseDoor, "_top_cap_right_open", VariantProperties.Rotation.R270))
.select(Direction.NORTH, net.minecraft.world.level.block.state.properties.DoorHingeSide.LEFT, false, doorTopVariant(baseDoor, "_top_cap_left", VariantProperties.Rotation.R270))
.select(Direction.NORTH, net.minecraft.world.level.block.state.properties.DoorHingeSide.LEFT, true, doorTopVariant(baseDoor, "_top_cap_left_open", VariantProperties.Rotation.R0))
.select(Direction.NORTH, net.minecraft.world.level.block.state.properties.DoorHingeSide.RIGHT, false, doorTopVariant(baseDoor, "_top_cap_right", VariantProperties.Rotation.R270))
.select(Direction.NORTH, net.minecraft.world.level.block.state.properties.DoorHingeSide.RIGHT, true, doorTopVariant(baseDoor, "_top_cap_right_open", VariantProperties.Rotation.R180))
.select(Direction.SOUTH, net.minecraft.world.level.block.state.properties.DoorHingeSide.LEFT, false, doorTopVariant(baseDoor, "_top_cap_left", VariantProperties.Rotation.R90))
.select(Direction.SOUTH, net.minecraft.world.level.block.state.properties.DoorHingeSide.LEFT, true, doorTopVariant(baseDoor, "_top_cap_left_open", VariantProperties.Rotation.R180))
.select(Direction.SOUTH, net.minecraft.world.level.block.state.properties.DoorHingeSide.RIGHT, false, doorTopVariant(baseDoor, "_top_cap_right", VariantProperties.Rotation.R90))
.select(Direction.SOUTH, net.minecraft.world.level.block.state.properties.DoorHingeSide.RIGHT, true, doorTopVariant(baseDoor, "_top_cap_right_open", VariantProperties.Rotation.R0))
.select(Direction.WEST, net.minecraft.world.level.block.state.properties.DoorHingeSide.LEFT, false, doorTopVariant(baseDoor, "_top_cap_left", VariantProperties.Rotation.R180))
.select(Direction.WEST, net.minecraft.world.level.block.state.properties.DoorHingeSide.LEFT, true, doorTopVariant(baseDoor, "_top_cap_left_open", VariantProperties.Rotation.R270))
.select(Direction.WEST, net.minecraft.world.level.block.state.properties.DoorHingeSide.RIGHT, false, doorTopVariant(baseDoor, "_top_cap_right", VariantProperties.Rotation.R180))
.select(Direction.WEST, net.minecraft.world.level.block.state.properties.DoorHingeSide.RIGHT, true, doorTopVariant(baseDoor, "_top_cap_right_open", VariantProperties.Rotation.R90))
));
}

private static Variant doorTopVariant(Block baseDoor, String suffix, VariantProperties.Rotation rotation) {
Variant variant = Variant.variant().with(VariantProperties.MODEL, ModelLocationUtils.getModelLocation(baseDoor, suffix));
return rotation == VariantProperties.Rotation.R0 ? variant : variant.with(VariantProperties.Y_ROT, rotation);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add the following import at the top.

import net.minecraft.world.level.block.state.properties.DoorHingeSide;

Also, the files meteoric_iron_door_top_cap_left.json, meteoric_iron_door_top_cap_left_open.json, meteoric_iron_door_top_cap_right.json and meteoric_iron_door_top_cap_right_open.json can be data generated.

    private void createTallDoorTop(BlockModelGenerators generator, Block topBlock, Block baseDoor) {
        ResourceLocation resourceLocation = ModelLocationUtils.getModelLocation(baseDoor, "_top_cap");
        TextureMapping textureMapping = TextureMapping.door(resourceLocation, resourceLocation);

        ResourceLocation topLeft = ModelTemplates.DOOR_TOP_LEFT.create(resourceLocation.withSuffix("_left"), textureMapping, generator.modelOutput);
        ResourceLocation topLeftOpen = ModelTemplates.DOOR_TOP_LEFT_OPEN.create(resourceLocation.withSuffix("_left_open"), textureMapping, generator.modelOutput);
        ResourceLocation topRight = ModelTemplates.DOOR_TOP_RIGHT.create(resourceLocation.withSuffix("_right"), textureMapping, generator.modelOutput);
        ResourceLocation topRightOpen = ModelTemplates.DOOR_TOP_RIGHT_OPEN.create(resourceLocation.withSuffix("_right_open"), textureMapping, generator.modelOutput);

        generator.blockStateOutput.accept(MultiVariantGenerator.multiVariant(topBlock).with(PropertyDispatch.properties(MeteoricIronDoorTopBlock.FACING, MeteoricIronDoorTopBlock.HINGE, MeteoricIronDoorTopBlock.OPEN)
                .select(Direction.EAST, DoorHingeSide.LEFT, false, doorTopVariant(baseDoor, topLeft, VariantProperties.Rotation.R0))
                .select(Direction.EAST, DoorHingeSide.LEFT, true, doorTopVariant(baseDoor, topLeftOpen, VariantProperties.Rotation.R90))
                .select(Direction.EAST, DoorHingeSide.RIGHT, false, doorTopVariant(baseDoor, topRight, VariantProperties.Rotation.R0))
                .select(Direction.EAST, DoorHingeSide.RIGHT, true, doorTopVariant(baseDoor, topRightOpen, VariantProperties.Rotation.R270))
                .select(Direction.NORTH, DoorHingeSide.LEFT, false, doorTopVariant(baseDoor, topLeft, VariantProperties.Rotation.R270))
                .select(Direction.NORTH, DoorHingeSide.LEFT, true, doorTopVariant(baseDoor, topLeftOpen, VariantProperties.Rotation.R0))
                .select(Direction.NORTH, DoorHingeSide.RIGHT, false, doorTopVariant(baseDoor, topRight, VariantProperties.Rotation.R270))
                .select(Direction.NORTH, DoorHingeSide.RIGHT, true, doorTopVariant(baseDoor, topRightOpen, VariantProperties.Rotation.R180))
                .select(Direction.SOUTH, DoorHingeSide.LEFT, false, doorTopVariant(baseDoor, topLeft, VariantProperties.Rotation.R90))
                .select(Direction.SOUTH, DoorHingeSide.LEFT, true, doorTopVariant(baseDoor, topLeftOpen, VariantProperties.Rotation.R180))
                .select(Direction.SOUTH, DoorHingeSide.RIGHT, false, doorTopVariant(baseDoor, topRight, VariantProperties.Rotation.R90))
                .select(Direction.SOUTH, DoorHingeSide.RIGHT, true, doorTopVariant(baseDoor, topRightOpen, VariantProperties.Rotation.R0))
                .select(Direction.WEST, DoorHingeSide.LEFT, false, doorTopVariant(baseDoor, topLeft, VariantProperties.Rotation.R180))
                .select(Direction.WEST, DoorHingeSide.LEFT, true, doorTopVariant(baseDoor, topLeftOpen, VariantProperties.Rotation.R270))
                .select(Direction.WEST, DoorHingeSide.RIGHT, false, doorTopVariant(baseDoor, topRight, VariantProperties.Rotation.R180))
                .select(Direction.WEST, DoorHingeSide.RIGHT, true, doorTopVariant(baseDoor, topRightOpen, VariantProperties.Rotation.R90))
        ));
    }

    private static Variant doorTopVariant(Block baseDoor, ResourceLocation resourceLocation, VariantProperties.Rotation rotation) {
        Variant variant = Variant.variant().with(VariantProperties.MODEL, resourceLocation);
        return rotation == VariantProperties.Rotation.R0 ? variant : variant.with(VariantProperties.Y_ROT, rotation);
    }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The files src/main/resources/assets/galacticraft/blockstates/lunar_cheese_press.json, src/main/resources/assets/galacticraft/blockstates/lunar_cheese_press_part.json and src/main/resources/assets/galacticraft/models/item/lunar_cheese_press.json can be data generated fairly easily.

I would rename the createRotatedDelegate method in GCModelProviders to createRandomlyRotatedDelegate (to avoid confusion) and then add a similar method called createHorizontallyRotatedDelegate that you should call twice, once for GCBlocks.LUNAR_CHEESE_PRESS and once for GCBlocks.LUNAR_CHEESE_PRESS_PART.

private static void createHorizontallyRotatedDelegate(BlockModelGenerators generator, Block block) {
    generator.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block, Variant.variant().with(VariantProperties.MODEL, ModelLocationUtils.getModelLocation(block))).with(generator.createHorizontalFacingDispatch()));
}

Comment on lines +162 to +164
protected BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, net.minecraft.world.level.LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
if (direction == Direction.UP && !neighborState.is(GCBlocks.LUNAR_CHEESE_PRESS_PART)) {
return net.minecraft.world.level.block.Blocks.AIR.defaultBlockState();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please import net.minecraft.world.level.LevelAccessor and net.minecraft.world.level.block.Blocks

Comment on lines +98 to +100
protected BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, net.minecraft.world.level.LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
if (direction == Direction.DOWN && !neighborState.is(GCBlocks.LUNAR_CHEESE_PRESS)) {
return net.minecraft.world.level.block.Blocks.AIR.defaultBlockState();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please import net.minecraft.world.level.LevelAccessor and net.minecraft.world.level.block.Blocks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The item only seems to drop when breaking the top part of the door, or when breaking the block underneath.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants