Prvni verze
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package com.titanicmc.titanic;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
/**
|
||||
* Common (server-authoritative) config for the Titanic mod.
|
||||
*/
|
||||
public class Config {
|
||||
|
||||
public static final ForgeConfigSpec SPEC;
|
||||
|
||||
public static final ForgeConfigSpec.IntValue WRECK_RARITY;
|
||||
public static final ForgeConfigSpec.IntValue DRY_DOCK_IRON_REQUIRED;
|
||||
public static final ForgeConfigSpec.IntValue DRY_DOCK_GLASS_REQUIRED;
|
||||
public static final ForgeConfigSpec.IntValue DRY_DOCK_GLASS_PANES_REQUIRED;
|
||||
public static final ForgeConfigSpec.IntValue DRY_DOCK_REPEATERS_REQUIRED;
|
||||
public static final ForgeConfigSpec.IntValue DRY_DOCK_COMPARATORS_REQUIRED;
|
||||
public static final ForgeConfigSpec.IntValue DRY_DOCK_CHESTS_REQUIRED;
|
||||
public static final ForgeConfigSpec.IntValue DRY_DOCK_PLANKS_REQUIRED;
|
||||
public static final ForgeConfigSpec.IntValue DRY_DOCK_REDSTONE_BLOCKS_REQUIRED;
|
||||
public static final ForgeConfigSpec.IntValue MAX_SHIP_BLOCKS;
|
||||
public static final ForgeConfigSpec.DoubleValue RAISE_SPEED;
|
||||
public static final ForgeConfigSpec.DoubleValue BOILER_HEAT_PER_FUEL;
|
||||
public static final ForgeConfigSpec.DoubleValue ENGINE_POWER_PER_HEAT;
|
||||
|
||||
static {
|
||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
||||
|
||||
builder.push("world_generation");
|
||||
WRECK_RARITY = builder
|
||||
.comment("1 in N ocean chunks (approximately) will attempt to place a Titanic wreck.")
|
||||
.defineInRange("wreckRarity", 800, 1, 100000);
|
||||
builder.pop();
|
||||
|
||||
builder.push("ships");
|
||||
MAX_SHIP_BLOCKS = builder
|
||||
.comment("Safety limit on how many connected blocks may be assembled into one ship.")
|
||||
.defineInRange("maxShipBlocks", 20000, 100, 200000);
|
||||
RAISE_SPEED = builder
|
||||
.comment("Upward speed (blocks/tick) applied while a wreck is being raised to the surface.")
|
||||
.defineInRange("raiseSpeed", 0.08, 0.001, 1.0);
|
||||
BOILER_HEAT_PER_FUEL = builder
|
||||
.comment("Heat generated per fuel-tick burned in a Boiler.")
|
||||
.defineInRange("boilerHeatPerFuel", 0.02, 0.0001, 10.0);
|
||||
ENGINE_POWER_PER_HEAT = builder
|
||||
.comment("Engine power applied to the ship per unit of accumulated heat, when a Crankshaft is adjacent.")
|
||||
.defineInRange("enginePowerPerHeat", 4000.0, 1.0, 1000000.0);
|
||||
builder.pop();
|
||||
|
||||
builder.push("dry_dock_repair");
|
||||
DRY_DOCK_IRON_REQUIRED = builder.comment("Iron ingots required to repair the hull (3 * 64 per the design doc).")
|
||||
.defineInRange("ironIngots", 192, 1, 100000);
|
||||
DRY_DOCK_GLASS_REQUIRED = builder.comment("Glass blocks required for portholes/windows.")
|
||||
.defineInRange("glassBlocks", 64, 1, 100000);
|
||||
DRY_DOCK_GLASS_PANES_REQUIRED = builder.comment("Glass panes required.")
|
||||
.defineInRange("glassPanes", 64, 1, 100000);
|
||||
DRY_DOCK_REPEATERS_REQUIRED = builder.comment("Redstone repeaters required (engine control linkages).")
|
||||
.defineInRange("repeaters", 16, 1, 100000);
|
||||
DRY_DOCK_COMPARATORS_REQUIRED = builder.comment("Redstone comparators required (gauges/telegraph).")
|
||||
.defineInRange("comparators", 20, 1, 100000);
|
||||
DRY_DOCK_CHESTS_REQUIRED = builder.comment("Chests required (cargo holds / boiler feed bunkers).")
|
||||
.defineInRange("chests", 64, 1, 100000);
|
||||
DRY_DOCK_PLANKS_REQUIRED = builder.comment("Planks required (deck planking).")
|
||||
.defineInRange("planks", 50, 1, 100000);
|
||||
DRY_DOCK_REDSTONE_BLOCKS_REQUIRED = builder.comment("Redstone blocks required (power for the engine telegraph).")
|
||||
.defineInRange("redstoneBlocks", 10, 1, 100000);
|
||||
builder.pop();
|
||||
|
||||
SPEC = builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.titanicmc.titanic;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import com.titanicmc.titanic.event.ForgeEventHandler;
|
||||
import com.titanicmc.titanic.registry.ModBlockEntities;
|
||||
import com.titanicmc.titanic.registry.ModBlocks;
|
||||
import com.titanicmc.titanic.registry.ModCreativeTabs;
|
||||
import com.titanicmc.titanic.registry.ModEntities;
|
||||
import com.titanicmc.titanic.registry.ModFeatures;
|
||||
import com.titanicmc.titanic.registry.ModItems;
|
||||
import com.titanicmc.titanic.ship.TitanicShipControl;
|
||||
import kotlin.Unit;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
||||
|
||||
/**
|
||||
* Titanic - raise the wreck, repair her in dry dock, and sail her again.
|
||||
* <p>
|
||||
* Ship physics (buoyancy, steering, collisions) are provided by the Valkyrien Skies 2
|
||||
* library mod. This mod never manually shuffles blocks around to fake movement; once a
|
||||
* structure is "assembled" via {@code org.valkyrienskies.mod.common.assembly.ShipAssembler}
|
||||
* it becomes a real physics object that VS2 simulates every tick.
|
||||
*/
|
||||
@Mod(TitanicMod.MODID)
|
||||
public class TitanicMod {
|
||||
|
||||
public static final String MODID = "titanic";
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
public TitanicMod(FMLJavaModLoadingContext context) {
|
||||
IEventBus modEventBus = context.getModEventBus();
|
||||
|
||||
ModItems.ITEMS.register(modEventBus);
|
||||
ModBlocks.BLOCKS.register(modEventBus);
|
||||
ModBlockEntities.BLOCK_ENTITIES.register(modEventBus);
|
||||
ModEntities.ENTITIES.register(modEventBus);
|
||||
ModFeatures.FEATURES.register(modEventBus);
|
||||
ModCreativeTabs.TABS.register(modEventBus);
|
||||
|
||||
context.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
|
||||
|
||||
MinecraftForge.EVENT_BUS.register(new ForgeEventHandler());
|
||||
|
||||
// Required by Valkyrien Skies 2: any class attached to a ship via
|
||||
// LoadedServerShip#setAttachment must first be registered with its attachment
|
||||
// registry, or setAttachment throws IllegalArgumentException. This mod's mods.toml
|
||||
// declares an AFTER ordering dependency on valkyrienskies, so its core (and this
|
||||
// registry) is guaranteed to already be initialized here. TitanicShipControl carries
|
||||
// only in-memory runtime state (raise/engine state, a live ship reference) that should
|
||||
// never be persisted, hence the transient serializer.
|
||||
VSGameUtilsKt.getVsCore().registerAttachment(TitanicShipControl.class, builder -> {
|
||||
builder.useTransientSerializer();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.titanicmc.titanic.block;
|
||||
|
||||
import com.titanicmc.titanic.blockentity.BoilerBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/** A huge coal-fired boiler. See {@link BoilerBlockEntity} for behaviour. */
|
||||
public class BoilerBlock extends BaseEntityBlock {
|
||||
|
||||
public BoilerBlock(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand,
|
||||
BlockHitResult hit) {
|
||||
if (level.isClientSide) return InteractionResult.SUCCESS;
|
||||
if (!(level.getBlockEntity(pos) instanceof BoilerBlockEntity boiler)) return InteractionResult.PASS;
|
||||
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
if (!held.isEmpty() && boiler.tryFeed(held)) {
|
||||
player.displayClientMessage(Component.translatable("message.titanic.boiler_fed"), true);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
player.displayClientMessage(Component.translatable(
|
||||
boiler.isBurning() ? "message.titanic.boiler_burning" : "message.titanic.boiler_needs_fuel"), true);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new BoilerBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
|
||||
if (level.isClientSide) return null;
|
||||
return (lvl, pos, st, be) -> {
|
||||
if (be instanceof BoilerBlockEntity boiler) {
|
||||
boiler.serverTick((ServerLevel) lvl);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.titanicmc.titanic.block;
|
||||
|
||||
import com.titanicmc.titanic.entity.SeatEntity;
|
||||
import com.titanicmc.titanic.registry.ModEntities;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A simple dining-room / cabin chair. Right-click to sit down; sneak to get back up.
|
||||
* Works fine on an assembled ship since VS2 handles entities riding blocks-on-ships.
|
||||
*/
|
||||
public class ChairBlock extends Block {
|
||||
|
||||
public ChairBlock(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(net.minecraft.world.level.block.state.BlockState state, Level level, BlockPos pos,
|
||||
Player player, InteractionHand hand, BlockHitResult hit) {
|
||||
if (level.isClientSide) {
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
if (player.isPassenger() || player.isShiftKeyDown()) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
AABB searchBox = new AABB(pos).inflate(0.2);
|
||||
List<SeatEntity> existing = level.getEntitiesOfClass(SeatEntity.class, searchBox);
|
||||
SeatEntity seat = existing.isEmpty() ? null : existing.get(0);
|
||||
|
||||
if (seat == null) {
|
||||
seat = ModEntities.SEAT.get().create(level);
|
||||
if (seat == null) return InteractionResult.PASS;
|
||||
seat.setPos(pos.getX() + 0.5, pos.getY() + 0.2, pos.getZ() + 0.5);
|
||||
level.addFreshEntity(seat);
|
||||
}
|
||||
|
||||
player.startRiding(seat, true);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.titanicmc.titanic.block;
|
||||
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
/**
|
||||
* Purely a structural marker: a {@link BoilerBlock} only converts its heat into ship
|
||||
* propulsion while at least one Crankshaft is directly adjacent to it. This forces the
|
||||
* player to actually build out an "engine room" rather than dropping a single block.
|
||||
*/
|
||||
public class CrankshaftBlock extends Block {
|
||||
public CrankshaftBlock(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.titanicmc.titanic.block;
|
||||
|
||||
import com.titanicmc.titanic.blockentity.HelmBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/** The real ship's wheel, installed automatically once dry dock repairs are finished. */
|
||||
public class HelmBlock extends BaseEntityBlock {
|
||||
|
||||
public HelmBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(this.stateDefinition.any().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(BlockStateProperties.HORIZONTAL_FACING);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
return defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, context.getHorizontalDirection().getOpposite());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand,
|
||||
BlockHitResult hit) {
|
||||
if (level.isClientSide) return InteractionResult.SUCCESS;
|
||||
if (!(level.getBlockEntity(pos) instanceof HelmBlockEntity helm)) return InteractionResult.PASS;
|
||||
|
||||
if (helm.sit(player)) {
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
player.displayClientMessage(Component.translatable("message.titanic.helm_not_seaworthy"), true);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new HelmBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
|
||||
if (level.isClientSide) return null;
|
||||
return (lvl, pos, st, be) -> {
|
||||
if (be instanceof HelmBlockEntity helm) {
|
||||
helm.serverTick();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.titanicmc.titanic.block;
|
||||
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
|
||||
/**
|
||||
* Generated on the wreck's bridge during world generation, marking where the real helm will
|
||||
* be installed once dry dock repairs are finished. Never placeable by the player.
|
||||
*/
|
||||
public class HelmSocketBlock extends Block {
|
||||
|
||||
public HelmSocketBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(this.stateDefinition.any().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(BlockStateProperties.HORIZONTAL_FACING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.titanicmc.titanic.block;
|
||||
|
||||
import com.titanicmc.titanic.blockentity.LiftingFrameBlockEntity;
|
||||
import com.titanicmc.titanic.registry.ModBlockEntities;
|
||||
import com.titanicmc.titanic.registry.ModItems;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.IntegerProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* The recovery "box" a player builds and lowers over the sunken wreck. It has four eyes;
|
||||
* right-clicking it with a Steel Cable threads one eye at a time. Once all four are threaded
|
||||
* the frame is "rigged" and can be linked to a {@link WinchBlock} on a surface vessel to
|
||||
* start the raise sequence.
|
||||
*/
|
||||
public class LiftingFrameBlock extends BaseEntityBlock {
|
||||
|
||||
public static final IntegerProperty CABLES = IntegerProperty.create("cables", 0, 4);
|
||||
|
||||
public LiftingFrameBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(this.stateDefinition.any().setValue(CABLES, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(CABLES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand,
|
||||
BlockHitResult hit) {
|
||||
if (level.isClientSide) {
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
BlockEntity be = level.getBlockEntity(pos);
|
||||
if (!(be instanceof LiftingFrameBlockEntity frame)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
int cables = state.getValue(CABLES);
|
||||
|
||||
if (cables < 4 && held.is(ModItems.STEEL_CABLE.get())) {
|
||||
held.shrink(1);
|
||||
level.setBlock(pos, state.setValue(CABLES, cables + 1), 3);
|
||||
player.displayClientMessage(
|
||||
Component.translatable("message.titanic.frame_threaded", cables + 1, 4), true);
|
||||
if (cables + 1 == 4) {
|
||||
player.displayClientMessage(Component.translatable("message.titanic.frame_rigged"), true);
|
||||
}
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
if (cables >= 4) {
|
||||
player.displayClientMessage(
|
||||
Component.translatable(frame.isLinkedToWinch()
|
||||
? "message.titanic.frame_already_linked"
|
||||
: "message.titanic.frame_needs_winch"), true);
|
||||
} else {
|
||||
player.displayClientMessage(
|
||||
Component.translatable("message.titanic.frame_needs_cable", 4 - cables), true);
|
||||
}
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new LiftingFrameBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
|
||||
if (level.isClientSide) return null;
|
||||
return (lvl, pos, st, blockEntity) -> {
|
||||
if (blockEntity instanceof LiftingFrameBlockEntity frame) {
|
||||
frame.serverTick((ServerLevel) lvl);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.titanicmc.titanic.block;
|
||||
|
||||
import com.titanicmc.titanic.blockentity.RepairPanelBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Generated inside the wreck's bridge. Right-click while holding a repair material to hand
|
||||
* it in. See {@link RepairPanelBlockEntity} for the full repair logic.
|
||||
*/
|
||||
public class RepairPanelBlock extends BaseEntityBlock {
|
||||
|
||||
public RepairPanelBlock(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand,
|
||||
BlockHitResult hit) {
|
||||
if (level.isClientSide) return InteractionResult.SUCCESS;
|
||||
if (!(level.getBlockEntity(pos) instanceof RepairPanelBlockEntity panel)) return InteractionResult.PASS;
|
||||
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
panel.handleDeliver((ServerLevel) level, player, held);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new RepairPanelBlockEntity(pos, state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.titanicmc.titanic.block;
|
||||
|
||||
import com.titanicmc.titanic.blockentity.LiftingFrameBlockEntity;
|
||||
import com.titanicmc.titanic.registry.ModItems;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/**
|
||||
* Placed on the player's recovery vessel above the water. Right-clicking with a Steel Cable
|
||||
* searches straight down for a fully rigged {@link LiftingFrameBlock} and, if found, links the
|
||||
* two together, starting the automatic raise sequence.
|
||||
*/
|
||||
public class WinchBlock extends Block {
|
||||
|
||||
private static final int MAX_SEARCH_DEPTH = 256;
|
||||
|
||||
public WinchBlock(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand,
|
||||
BlockHitResult hit) {
|
||||
if (level.isClientSide) {
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
if (!held.is(ModItems.STEEL_CABLE.get())) {
|
||||
player.displayClientMessage(Component.translatable("message.titanic.winch_needs_cable"), true);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
BlockPos.MutableBlockPos search = pos.mutable();
|
||||
for (int i = 0; i < MAX_SEARCH_DEPTH; i++) {
|
||||
search.move(0, -1, 0);
|
||||
BlockEntity be = level.getBlockEntity(search);
|
||||
if (be instanceof LiftingFrameBlockEntity frame) {
|
||||
if (!frame.isRigged(level.getBlockState(search))) {
|
||||
player.displayClientMessage(Component.translatable("message.titanic.frame_not_rigged"), true);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
held.shrink(1);
|
||||
frame.linkToWinch(pos);
|
||||
player.displayClientMessage(Component.translatable("message.titanic.raise_started"), true);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
}
|
||||
|
||||
player.displayClientMessage(Component.translatable("message.titanic.no_frame_found"), true);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.titanicmc.titanic.blockentity;
|
||||
|
||||
import com.titanicmc.titanic.Config;
|
||||
import com.titanicmc.titanic.block.CrankshaftBlock;
|
||||
import com.titanicmc.titanic.registry.ModBlockEntities;
|
||||
import com.titanicmc.titanic.ship.ShipUtil;
|
||||
import com.titanicmc.titanic.ship.TitanicShipControl;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraftforge.common.ForgeHooks;
|
||||
import org.valkyrienskies.core.api.ships.LoadedServerShip;
|
||||
|
||||
/**
|
||||
* Burns fuel to build up heat. Heat only turns into ship propulsion while a
|
||||
* {@link CrankshaftBlock} is directly adjacent - this represents the engine room
|
||||
* requiring both giant boilers and their crankshafts to actually drive the propellers.
|
||||
* <p>
|
||||
* Fuel can be fed directly by right-clicking with a burnable item, or the boiler will
|
||||
* automatically pull one burnable item per cycle from any directly adjacent chest.
|
||||
*/
|
||||
public class BoilerBlockEntity extends BlockEntity {
|
||||
|
||||
private int burnTimeRemaining = 0;
|
||||
|
||||
public BoilerBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(ModBlockEntities.BOILER.get(), pos, state);
|
||||
}
|
||||
|
||||
/** Attempts to feed the boiler with the given item. Returns true if it was consumed. */
|
||||
public boolean tryFeed(ItemStack stack) {
|
||||
int burnTime = ForgeHooks.getBurnTime(stack, null);
|
||||
if (burnTime <= 0) {
|
||||
return false;
|
||||
}
|
||||
stack.shrink(1);
|
||||
burnTimeRemaining += burnTime;
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void serverTick(ServerLevel level) {
|
||||
if (burnTimeRemaining <= 0) {
|
||||
tryAutoFeedFromChest(level);
|
||||
}
|
||||
|
||||
if (burnTimeRemaining <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
burnTimeRemaining--;
|
||||
|
||||
if (!hasAdjacentCrankshaft(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoadedServerShip ship = ShipUtil.getLoadedShipManagingPos(level, getBlockPos());
|
||||
if (ship == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TitanicShipControl control = ship.getAttachment(TitanicShipControl.class);
|
||||
if (control == null || !control.isRepaired()) {
|
||||
// Un-repaired hulls can't safely run their engines yet.
|
||||
return;
|
||||
}
|
||||
|
||||
double power = Config.BOILER_HEAT_PER_FUEL.get() * Config.ENGINE_POWER_PER_HEAT.get();
|
||||
control.addEnginePower(power);
|
||||
setChanged();
|
||||
}
|
||||
|
||||
private boolean hasAdjacentCrankshaft(ServerLevel level) {
|
||||
for (Direction direction : Direction.values()) {
|
||||
if (level.getBlockState(getBlockPos().relative(direction)).getBlock() instanceof CrankshaftBlock) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void tryAutoFeedFromChest(ServerLevel level) {
|
||||
for (Direction direction : Direction.values()) {
|
||||
BlockEntity neighbor = level.getBlockEntity(getBlockPos().relative(direction));
|
||||
if (!(neighbor instanceof Container container)) {
|
||||
continue;
|
||||
}
|
||||
for (int slot = 0; slot < container.getContainerSize(); slot++) {
|
||||
ItemStack stack = container.getItem(slot);
|
||||
if (stack.isEmpty()) continue;
|
||||
int burnTime = ForgeHooks.getBurnTime(stack, null);
|
||||
if (burnTime > 0) {
|
||||
container.removeItem(slot, 1);
|
||||
burnTimeRemaining += burnTime;
|
||||
setChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBurning() {
|
||||
return burnTimeRemaining > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
super.saveAdditional(tag);
|
||||
tag.putInt("BurnTime", burnTimeRemaining);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(CompoundTag tag) {
|
||||
super.load(tag);
|
||||
burnTimeRemaining = tag.getInt("BurnTime");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.titanicmc.titanic.blockentity;
|
||||
|
||||
import com.titanicmc.titanic.registry.ModBlockEntities;
|
||||
import com.titanicmc.titanic.ship.ShipUtil;
|
||||
import com.titanicmc.titanic.ship.TitanicShipControl;
|
||||
import net.minecraft.commands.arguments.EntityAnchorArgument;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.valkyrienskies.core.api.ships.LoadedServerShip;
|
||||
import org.valkyrienskies.mod.common.ValkyrienSkiesMod;
|
||||
import org.valkyrienskies.mod.common.entity.ShipMountingEntity;
|
||||
|
||||
/**
|
||||
* The real, working ship's wheel. Right-click it to sit down and steer, just like the
|
||||
* wheelhouse of the real Titanic: forward/back on the wheel throttles the engines, left/right
|
||||
* turns the rudder.
|
||||
*/
|
||||
public class HelmBlockEntity extends BlockEntity {
|
||||
|
||||
private ShipMountingEntity seat;
|
||||
|
||||
public HelmBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(ModBlockEntities.HELM.get(), pos, state);
|
||||
}
|
||||
|
||||
public void serverTick() {
|
||||
LoadedServerShip ship = getShip();
|
||||
if (ship == null) return;
|
||||
|
||||
TitanicShipControl control = ship.getAttachment(TitanicShipControl.class);
|
||||
if (control == null) {
|
||||
// Normally a Helm only exists after the Repair Panel finishes repairs (which
|
||||
// already marks the control as repaired). This is just a safety net for ships
|
||||
// that got a Helm some other way (e.g. placed directly in creative mode).
|
||||
control = new TitanicShipControl();
|
||||
control.markRepaired();
|
||||
ship.setAttachment(TitanicShipControl.class, control);
|
||||
}
|
||||
control.ship = ship;
|
||||
}
|
||||
|
||||
private LoadedServerShip getShip() {
|
||||
return ShipUtil.getLoadedShipManagingPos((net.minecraft.server.level.ServerLevel) level, getBlockPos());
|
||||
}
|
||||
|
||||
public boolean sit(Player player) {
|
||||
LoadedServerShip ship = getShip();
|
||||
if (ship == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seat == null || !seat.isAlive()) {
|
||||
seat = ValkyrienSkiesMod.SHIP_MOUNTING_ENTITY_TYPE.create(level);
|
||||
if (seat == null) return false;
|
||||
|
||||
var facing = getBlockState().getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||
Vec3 offset = Vec3.atLowerCornerOf(getBlockPos()).add(0.5, 1.1, 0.5).add(facing.getOpposite().getStepX() * 0.6, 0, facing.getOpposite().getStepZ() * 0.6);
|
||||
seat.moveTo(offset.x, offset.y, offset.z);
|
||||
seat.lookAt(EntityAnchorArgument.Anchor.EYES, offset.add(facing.getStepX(), 0, facing.getStepZ()));
|
||||
seat.setController(true);
|
||||
((net.minecraft.server.level.ServerLevel) level).addFreshEntityWithPassengers(seat);
|
||||
}
|
||||
|
||||
return player.startRiding(seat, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.titanicmc.titanic.blockentity;
|
||||
|
||||
import com.titanicmc.titanic.registry.ModBlockEntities;
|
||||
import com.titanicmc.titanic.ship.ShipUtil;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* Tracks the rigging state of a {@link com.titanicmc.titanic.block.LiftingFrameBlock}.
|
||||
* Once linked to a winch, {@link #serverTick(ServerLevel)} drives the raise sequence by
|
||||
* asking {@link ShipUtil} to assemble the wreck into a Valkyrien Skies ship and apply a
|
||||
* temporary lifting force until it breaches the surface.
|
||||
*/
|
||||
public class LiftingFrameBlockEntity extends BlockEntity {
|
||||
|
||||
private boolean linkedToWinch = false;
|
||||
private BlockPos winchPos = null;
|
||||
private boolean raiseStarted = false;
|
||||
|
||||
public LiftingFrameBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(ModBlockEntities.LIFTING_FRAME.get(), pos, state);
|
||||
}
|
||||
|
||||
public boolean isRigged(BlockState state) {
|
||||
return state.getValue(com.titanicmc.titanic.block.LiftingFrameBlock.CABLES) >= 4;
|
||||
}
|
||||
|
||||
public boolean isLinkedToWinch() {
|
||||
return linkedToWinch;
|
||||
}
|
||||
|
||||
public void linkToWinch(BlockPos winch) {
|
||||
this.linkedToWinch = true;
|
||||
this.winchPos = winch.immutable();
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public void serverTick(ServerLevel level) {
|
||||
if (raiseStarted || !linkedToWinch || winchPos == null) {
|
||||
return;
|
||||
}
|
||||
if (!isRigged(getBlockState())) {
|
||||
return;
|
||||
}
|
||||
raiseStarted = true;
|
||||
ShipUtil.beginWreckRaise(level, getBlockPos());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
super.saveAdditional(tag);
|
||||
tag.putBoolean("LinkedToWinch", linkedToWinch);
|
||||
tag.putBoolean("RaiseStarted", raiseStarted);
|
||||
if (winchPos != null) {
|
||||
tag.putLong("WinchPos", winchPos.asLong());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(CompoundTag tag) {
|
||||
super.load(tag);
|
||||
linkedToWinch = tag.getBoolean("LinkedToWinch");
|
||||
raiseStarted = tag.getBoolean("RaiseStarted");
|
||||
if (tag.contains("WinchPos")) {
|
||||
winchPos = BlockPos.of(tag.getLong("WinchPos"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package com.titanicmc.titanic.blockentity;
|
||||
|
||||
import com.titanicmc.titanic.Config;
|
||||
import com.titanicmc.titanic.registry.ModBlockEntities;
|
||||
import com.titanicmc.titanic.registry.ModBlocks;
|
||||
import com.titanicmc.titanic.registry.ModItems;
|
||||
import com.titanicmc.titanic.ship.ShipUtil;
|
||||
import com.titanicmc.titanic.ship.TitanicShipControl;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.ItemTags;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import org.valkyrienskies.core.api.ships.LoadedServerShip;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Generated as part of the wreck's bridge structure, so it automatically becomes part of the
|
||||
* ship at assembly time. Players hand in dry dock repair materials here; every delivery
|
||||
* proportionally patches the hull's damaged blocks and broken portholes. The final part,
|
||||
* the ship's wheel, finishes the job by installing a real, working {@link com.titanicmc.titanic.block.HelmBlock}.
|
||||
*/
|
||||
public class RepairPanelBlockEntity extends BlockEntity {
|
||||
|
||||
private enum Material {
|
||||
IRON, GLASS, PANES, REPEATERS, COMPARATORS, CHESTS, PLANKS, REDSTONE_BLOCKS
|
||||
}
|
||||
|
||||
private final int[] delivered = new int[Material.values().length];
|
||||
private boolean wheelDelivered = false;
|
||||
|
||||
private int hullTotal = -1;
|
||||
private int portholeTotal = -1;
|
||||
|
||||
public RepairPanelBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(ModBlockEntities.REPAIR_PANEL.get(), pos, state);
|
||||
}
|
||||
|
||||
public void handleDeliver(ServerLevel level, Player player, ItemStack stack) {
|
||||
if (stack.is(ModItems.SHIPS_WHEEL_PART.get())) {
|
||||
deliverWheel(level, player);
|
||||
return;
|
||||
}
|
||||
|
||||
Material material = classify(stack);
|
||||
if (material == null) {
|
||||
player.displayClientMessage(Component.translatable("message.titanic.repair_wrong_item"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
int required = requiredFor(material);
|
||||
int have = delivered[material.ordinal()];
|
||||
if (have >= required) {
|
||||
player.displayClientMessage(Component.translatable("message.titanic.repair_material_done"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
int amount = Math.min(stack.getCount(), required - have);
|
||||
stack.shrink(amount);
|
||||
delivered[material.ordinal()] += amount;
|
||||
setChanged();
|
||||
|
||||
player.displayClientMessage(Component.translatable("message.titanic.repair_delivered",
|
||||
delivered[material.ordinal()], required), true);
|
||||
|
||||
applyRepairProgress(level);
|
||||
}
|
||||
|
||||
private void deliverWheel(ServerLevel level, Player player) {
|
||||
if (!isMaterialsComplete()) {
|
||||
player.displayClientMessage(Component.translatable("message.titanic.repair_needs_materials_first"), true);
|
||||
return;
|
||||
}
|
||||
if (wheelDelivered) {
|
||||
player.displayClientMessage(Component.translatable("message.titanic.repair_already_done"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
wheelDelivered = true;
|
||||
setChanged();
|
||||
finishRepair(level, player);
|
||||
}
|
||||
|
||||
private boolean isMaterialsComplete() {
|
||||
for (Material material : Material.values()) {
|
||||
if (delivered[material.ordinal()] < requiredFor(material)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void applyRepairProgress(ServerLevel level) {
|
||||
int totalRequired = 0;
|
||||
int totalDelivered = 0;
|
||||
for (Material material : Material.values()) {
|
||||
totalRequired += requiredFor(material);
|
||||
totalDelivered += delivered[material.ordinal()];
|
||||
}
|
||||
double progress = totalRequired == 0 ? 1.0 : (double) totalDelivered / (double) totalRequired;
|
||||
|
||||
Set<BlockPos> shipBlocks = ShipUtil.collectConnectedBlocks(level, getBlockPos(), Config.MAX_SHIP_BLOCKS.get());
|
||||
|
||||
if (hullTotal < 0) {
|
||||
hullTotal = ShipUtil.findBlocksOfType(level, shipBlocks, ModBlocks.DAMAGED_HULL.get()).size();
|
||||
}
|
||||
if (portholeTotal < 0) {
|
||||
portholeTotal = ShipUtil.findBlocksOfType(level, shipBlocks, ModBlocks.BROKEN_PORTHOLE.get()).size();
|
||||
}
|
||||
|
||||
convertRemaining(level, shipBlocks, ModBlocks.DAMAGED_HULL.get(), ModBlocks.SHIP_HULL.get().defaultBlockState(),
|
||||
hullTotal, progress);
|
||||
convertRemaining(level, shipBlocks, ModBlocks.BROKEN_PORTHOLE.get(), Blocks.GLASS_PANE.defaultBlockState(),
|
||||
portholeTotal, progress);
|
||||
}
|
||||
|
||||
private void convertRemaining(ServerLevel level, Set<BlockPos> shipBlocks, net.minecraft.world.level.block.Block from,
|
||||
BlockState toState, int total, double progress) {
|
||||
if (total <= 0) return;
|
||||
List<BlockPos> remaining = ShipUtil.findBlocksOfType(level, shipBlocks, from);
|
||||
int alreadyConverted = total - remaining.size();
|
||||
int shouldBeConverted = (int) Math.floor(progress * total);
|
||||
int toConvertNow = Math.max(0, shouldBeConverted - alreadyConverted);
|
||||
|
||||
for (int i = 0; i < toConvertNow && i < remaining.size(); i++) {
|
||||
level.setBlock(remaining.get(i), toState, 3);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishRepair(ServerLevel level, Player player) {
|
||||
Set<BlockPos> shipBlocks = ShipUtil.collectConnectedBlocks(level, getBlockPos(), Config.MAX_SHIP_BLOCKS.get());
|
||||
List<BlockPos> sockets = ShipUtil.findBlocksOfType(level, shipBlocks, ModBlocks.HELM_SOCKET.get());
|
||||
|
||||
for (BlockPos socketPos : sockets) {
|
||||
Direction facing = level.getBlockState(socketPos).hasProperty(BlockStateProperties.HORIZONTAL_FACING)
|
||||
? level.getBlockState(socketPos).getValue(BlockStateProperties.HORIZONTAL_FACING)
|
||||
: Direction.NORTH;
|
||||
level.setBlock(socketPos, ModBlocks.HELM.get().defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing), 3);
|
||||
}
|
||||
|
||||
LoadedServerShip ship = ShipUtil.getLoadedShipManagingPos(level, getBlockPos());
|
||||
if (ship != null) {
|
||||
TitanicShipControl control = ship.getAttachment(TitanicShipControl.class);
|
||||
if (control == null) {
|
||||
control = new TitanicShipControl();
|
||||
ship.setAttachment(TitanicShipControl.class, control);
|
||||
}
|
||||
control.markRepaired();
|
||||
}
|
||||
|
||||
level.players().forEach(p -> p.displayClientMessage(Component.translatable("message.titanic.repair_complete"), false));
|
||||
}
|
||||
|
||||
private static Material classify(ItemStack stack) {
|
||||
if (stack.is(Items.IRON_INGOT)) return Material.IRON;
|
||||
if (stack.is(Items.GLASS)) return Material.GLASS;
|
||||
if (stack.is(Items.GLASS_PANE)) return Material.PANES;
|
||||
if (stack.is(Items.REPEATER)) return Material.REPEATERS;
|
||||
if (stack.is(Items.COMPARATOR)) return Material.COMPARATORS;
|
||||
if (stack.getItem() instanceof net.minecraft.world.item.BlockItem blockItem
|
||||
&& blockItem.getBlock() instanceof net.minecraft.world.level.block.ChestBlock) {
|
||||
return Material.CHESTS;
|
||||
}
|
||||
if (stack.is(ItemTags.PLANKS)) return Material.PLANKS;
|
||||
if (stack.is(Items.REDSTONE_BLOCK)) return Material.REDSTONE_BLOCKS;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int requiredFor(Material material) {
|
||||
return switch (material) {
|
||||
case IRON -> Config.DRY_DOCK_IRON_REQUIRED.get();
|
||||
case GLASS -> Config.DRY_DOCK_GLASS_REQUIRED.get();
|
||||
case PANES -> Config.DRY_DOCK_GLASS_PANES_REQUIRED.get();
|
||||
case REPEATERS -> Config.DRY_DOCK_REPEATERS_REQUIRED.get();
|
||||
case COMPARATORS -> Config.DRY_DOCK_COMPARATORS_REQUIRED.get();
|
||||
case CHESTS -> Config.DRY_DOCK_CHESTS_REQUIRED.get();
|
||||
case PLANKS -> Config.DRY_DOCK_PLANKS_REQUIRED.get();
|
||||
case REDSTONE_BLOCKS -> Config.DRY_DOCK_REDSTONE_BLOCKS_REQUIRED.get();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
super.saveAdditional(tag);
|
||||
tag.putIntArray("Delivered", delivered);
|
||||
tag.putBoolean("WheelDelivered", wheelDelivered);
|
||||
tag.putInt("HullTotal", hullTotal);
|
||||
tag.putInt("PortholeTotal", portholeTotal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(CompoundTag tag) {
|
||||
super.load(tag);
|
||||
int[] loaded = tag.getIntArray("Delivered");
|
||||
System.arraycopy(loaded, 0, delivered, 0, Math.min(loaded.length, delivered.length));
|
||||
wheelDelivered = tag.getBoolean("WheelDelivered");
|
||||
hullTotal = tag.contains("HullTotal") ? tag.getInt("HullTotal") : -1;
|
||||
portholeTotal = tag.contains("PortholeTotal") ? tag.getInt("PortholeTotal") : -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.titanicmc.titanic.entity;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.syncher.SynchedEntityData;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Invisible, non-solid mount used so a player can "sit" on a chair. Spawned on demand by
|
||||
* {@link com.titanicmc.titanic.block.ChairBlock} and removed automatically once nobody is
|
||||
* riding it.
|
||||
*/
|
||||
public class SeatEntity extends Entity {
|
||||
|
||||
public SeatEntity(EntityType<?> type, Level level) {
|
||||
super(type, level);
|
||||
this.noPhysics = true;
|
||||
this.blocksBuilding = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
if (!level().isClientSide && this.getPassengers().isEmpty() && this.tickCount > 0) {
|
||||
this.discard();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void defineSynchedData() {
|
||||
// No synced data needed.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readAdditionalSaveData(CompoundTag tag) {
|
||||
// Nothing to persist.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAdditionalSaveData(CompoundTag tag) {
|
||||
// Nothing to persist.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPickable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPushable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBeSaved() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.titanicmc.titanic.event;
|
||||
|
||||
import com.titanicmc.titanic.ship.ShipRaiseScheduler;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
|
||||
/**
|
||||
* Cross-cutting game event handling. Registered on the Forge event bus in
|
||||
* {@link com.titanicmc.titanic.TitanicMod}.
|
||||
*/
|
||||
public class ForgeEventHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase == TickEvent.Phase.END) {
|
||||
ShipRaiseScheduler.tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.titanicmc.titanic.registry;
|
||||
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import com.titanicmc.titanic.blockentity.BoilerBlockEntity;
|
||||
import com.titanicmc.titanic.blockentity.HelmBlockEntity;
|
||||
import com.titanicmc.titanic.blockentity.LiftingFrameBlockEntity;
|
||||
import com.titanicmc.titanic.blockentity.RepairPanelBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
|
||||
public class ModBlockEntities {
|
||||
|
||||
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
|
||||
DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, TitanicMod.MODID);
|
||||
|
||||
public static final RegistryObject<BlockEntityType<LiftingFrameBlockEntity>> LIFTING_FRAME =
|
||||
BLOCK_ENTITIES.register("lifting_frame", () -> BlockEntityType.Builder.of(
|
||||
LiftingFrameBlockEntity::new, ModBlocks.LIFTING_FRAME.get()).build(null));
|
||||
|
||||
public static final RegistryObject<BlockEntityType<RepairPanelBlockEntity>> REPAIR_PANEL =
|
||||
BLOCK_ENTITIES.register("repair_panel", () -> BlockEntityType.Builder.of(
|
||||
RepairPanelBlockEntity::new, ModBlocks.REPAIR_PANEL.get()).build(null));
|
||||
|
||||
public static final RegistryObject<BlockEntityType<HelmBlockEntity>> HELM =
|
||||
BLOCK_ENTITIES.register("helm", () -> BlockEntityType.Builder.of(
|
||||
HelmBlockEntity::new, ModBlocks.HELM.get()).build(null));
|
||||
|
||||
public static final RegistryObject<BlockEntityType<BoilerBlockEntity>> BOILER =
|
||||
BLOCK_ENTITIES.register("boiler", () -> BlockEntityType.Builder.of(
|
||||
BoilerBlockEntity::new, ModBlocks.BOILER.get()).build(null));
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.titanicmc.titanic.registry;
|
||||
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
public class ModBlockTags {
|
||||
|
||||
private ModBlockTags() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Every block that can legitimately be part of a generated Titanic wreck/ship: the hull
|
||||
* plating itself plus all the furniture/fixture blocks {@link com.titanicmc.titanic.worldgen.WreckFeature}
|
||||
* places inside it. Used to bound {@link com.titanicmc.titanic.ship.ShipUtil#collectConnectedBlocks}
|
||||
* so a flood fill starting on the wreck can never spill out into the surrounding seafloor
|
||||
* terrain the wreck happens to be resting on/against.
|
||||
*/
|
||||
public static final TagKey<Block> SHIP_BLOCKS = TagKey.create(Registries.BLOCK,
|
||||
new ResourceLocation(TitanicMod.MODID, "ship_blocks"));
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.titanicmc.titanic.registry;
|
||||
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import com.titanicmc.titanic.block.BoilerBlock;
|
||||
import com.titanicmc.titanic.block.ChairBlock;
|
||||
import com.titanicmc.titanic.block.CrankshaftBlock;
|
||||
import com.titanicmc.titanic.block.HelmBlock;
|
||||
import com.titanicmc.titanic.block.HelmSocketBlock;
|
||||
import com.titanicmc.titanic.block.LiftingFrameBlock;
|
||||
import com.titanicmc.titanic.block.RepairPanelBlock;
|
||||
import com.titanicmc.titanic.block.WinchBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.SoundType;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
|
||||
public class ModBlocks {
|
||||
|
||||
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, TitanicMod.MODID);
|
||||
|
||||
/** Rusted, holed hull plating - generated by the wreck, converted to {@link #SHIP_HULL} on repair. */
|
||||
public static final RegistryObject<Block> DAMAGED_HULL = BLOCKS.register("damaged_hull",
|
||||
() -> new Block(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.COLOR_BROWN)
|
||||
.strength(4.0f, 20.0f)
|
||||
.sound(SoundType.NETHERITE_BLOCK)
|
||||
.requiresCorrectToolForDrops()));
|
||||
|
||||
/** Freshly patched hull plating - what {@link #DAMAGED_HULL} becomes once repaired. */
|
||||
public static final RegistryObject<Block> SHIP_HULL = BLOCKS.register("ship_hull",
|
||||
() -> new Block(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.METAL)
|
||||
.strength(5.0f, 20.0f)
|
||||
.sound(SoundType.METAL)
|
||||
.requiresCorrectToolForDrops()));
|
||||
|
||||
/** A shattered porthole - generated by the wreck, converted to a glass pane on repair. */
|
||||
public static final RegistryObject<Block> BROKEN_PORTHOLE = BLOCKS.register("broken_porthole",
|
||||
() -> new Block(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.COLOR_LIGHT_GRAY)
|
||||
.strength(1.0f)
|
||||
.sound(SoundType.GLASS)
|
||||
.noOcclusion()));
|
||||
|
||||
/** Marks where the real helm will be installed once repairs finish. World-gen only. */
|
||||
public static final RegistryObject<Block> HELM_SOCKET = BLOCKS.register("helm_socket",
|
||||
() -> new HelmSocketBlock(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.WOOD)
|
||||
.strength(3.0f)
|
||||
.sound(SoundType.WOOD)
|
||||
.noOcclusion()));
|
||||
|
||||
/** The real, working ship's wheel. */
|
||||
public static final RegistryObject<Block> HELM = BLOCKS.register("helm",
|
||||
() -> new HelmBlock(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.WOOD)
|
||||
.strength(3.5f)
|
||||
.sound(SoundType.WOOD)
|
||||
.noOcclusion()));
|
||||
|
||||
public static final RegistryObject<Block> LIFTING_FRAME = BLOCKS.register("lifting_frame",
|
||||
() -> new LiftingFrameBlock(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.METAL)
|
||||
.strength(5.0f)
|
||||
.sound(SoundType.METAL)
|
||||
.noOcclusion()));
|
||||
|
||||
public static final RegistryObject<Block> WINCH = BLOCKS.register("winch",
|
||||
() -> new WinchBlock(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.METAL)
|
||||
.strength(5.0f)
|
||||
.sound(SoundType.METAL)));
|
||||
|
||||
public static final RegistryObject<Block> REPAIR_PANEL = BLOCKS.register("repair_panel",
|
||||
() -> new RepairPanelBlock(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.COLOR_LIGHT_BLUE)
|
||||
.strength(4.0f)
|
||||
.sound(SoundType.METAL)
|
||||
.lightLevel(state -> 6)));
|
||||
|
||||
public static final RegistryObject<Block> BOILER = BLOCKS.register("boiler",
|
||||
() -> new BoilerBlock(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.COLOR_BLACK)
|
||||
.strength(6.0f, 20.0f)
|
||||
.sound(SoundType.METAL)
|
||||
.lightLevel(state -> 3)
|
||||
.requiresCorrectToolForDrops()));
|
||||
|
||||
public static final RegistryObject<Block> CRANKSHAFT = BLOCKS.register("crankshaft",
|
||||
() -> new CrankshaftBlock(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.METAL)
|
||||
.strength(6.0f, 20.0f)
|
||||
.sound(SoundType.METAL)
|
||||
.requiresCorrectToolForDrops()));
|
||||
|
||||
public static final RegistryObject<Block> CHAIR = BLOCKS.register("chair",
|
||||
() -> new ChairBlock(BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.WOOD)
|
||||
.strength(2.5f)
|
||||
.sound(SoundType.WOOD)
|
||||
.noOcclusion()));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.titanicmc.titanic.registry;
|
||||
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
|
||||
public class ModCreativeTabs {
|
||||
|
||||
public static final DeferredRegister<CreativeModeTab> TABS =
|
||||
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, TitanicMod.MODID);
|
||||
|
||||
public static final RegistryObject<CreativeModeTab> TITANIC_TAB = TABS.register("titanic",
|
||||
() -> CreativeModeTab.builder()
|
||||
.title(Component.translatable("itemGroup.titanic"))
|
||||
.icon(() -> ModItems.HELM.get().getDefaultInstance())
|
||||
.displayItems((parameters, output) -> {
|
||||
output.accept(ModItems.STEEL_INGOT.get());
|
||||
output.accept(ModItems.STEEL_CABLE.get());
|
||||
output.accept(ModItems.SHIPS_WHEEL_PART.get());
|
||||
output.accept(ModItems.SHIP_HULL.get());
|
||||
output.accept(ModItems.LIFTING_FRAME.get());
|
||||
output.accept(ModItems.WINCH.get());
|
||||
output.accept(ModItems.REPAIR_PANEL.get());
|
||||
output.accept(ModItems.BOILER.get());
|
||||
output.accept(ModItems.CRANKSHAFT.get());
|
||||
output.accept(ModItems.CHAIR.get());
|
||||
output.accept(ModItems.HELM.get());
|
||||
})
|
||||
.build());
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.titanicmc.titanic.registry;
|
||||
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import com.titanicmc.titanic.entity.SeatEntity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
|
||||
public class ModEntities {
|
||||
|
||||
public static final DeferredRegister<EntityType<?>> ENTITIES =
|
||||
DeferredRegister.create(ForgeRegistries.ENTITY_TYPES, TitanicMod.MODID);
|
||||
|
||||
public static final RegistryObject<EntityType<SeatEntity>> SEAT = ENTITIES.register("seat",
|
||||
() -> EntityType.Builder.<SeatEntity>of(SeatEntity::new, MobCategory.MISC)
|
||||
.sized(0.1f, 0.1f)
|
||||
.noSummon()
|
||||
.noSave()
|
||||
.clientTrackingRange(6)
|
||||
.build("seat"));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.titanicmc.titanic.registry;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import com.titanicmc.titanic.worldgen.WreckFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
|
||||
public class ModFeatures {
|
||||
|
||||
public static final DeferredRegister<Feature<?>> FEATURES =
|
||||
DeferredRegister.create(ForgeRegistries.FEATURES, TitanicMod.MODID);
|
||||
|
||||
public static final RegistryObject<Feature<NoneFeatureConfiguration>> WRECK =
|
||||
FEATURES.register("wreck", () -> new WreckFeature(NoneFeatureConfiguration.CODEC));
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.titanicmc.titanic.registry;
|
||||
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
|
||||
public class ModItems {
|
||||
|
||||
public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, TitanicMod.MODID);
|
||||
|
||||
/** Smelted from iron; used to craft steel cables and hull-repair adjacent recipes. */
|
||||
public static final RegistryObject<Item> STEEL_INGOT = ITEMS.register("steel_ingot",
|
||||
() -> new Item(new Item.Properties()));
|
||||
|
||||
/**
|
||||
* Threaded through a Lifting Frame's four eyes and then joined to a Winch to raise the wreck.
|
||||
* Consumed on use.
|
||||
*/
|
||||
public static final RegistryObject<Item> STEEL_CABLE = ITEMS.register("steel_cable",
|
||||
() -> new Item(new Item.Properties()));
|
||||
|
||||
/**
|
||||
* The final part delivered to the Repair Panel. Consuming it slots a real,
|
||||
* controllable helm into the wreck's bridge and finishes the repair.
|
||||
*/
|
||||
public static final RegistryObject<Item> SHIPS_WHEEL_PART = ITEMS.register("ships_wheel",
|
||||
() -> new Item(new Item.Properties()));
|
||||
|
||||
public static final RegistryObject<Item> SHIP_HULL = ITEMS.register("ship_hull",
|
||||
() -> new BlockItem(ModBlocks.SHIP_HULL.get(), new Item.Properties()));
|
||||
public static final RegistryObject<Item> LIFTING_FRAME = ITEMS.register("lifting_frame",
|
||||
() -> new BlockItem(ModBlocks.LIFTING_FRAME.get(), new Item.Properties()));
|
||||
public static final RegistryObject<Item> WINCH = ITEMS.register("winch",
|
||||
() -> new BlockItem(ModBlocks.WINCH.get(), new Item.Properties()));
|
||||
public static final RegistryObject<Item> REPAIR_PANEL = ITEMS.register("repair_panel",
|
||||
() -> new BlockItem(ModBlocks.REPAIR_PANEL.get(), new Item.Properties()));
|
||||
public static final RegistryObject<Item> BOILER = ITEMS.register("boiler",
|
||||
() -> new BlockItem(ModBlocks.BOILER.get(), new Item.Properties()));
|
||||
public static final RegistryObject<Item> CRANKSHAFT = ITEMS.register("crankshaft",
|
||||
() -> new BlockItem(ModBlocks.CRANKSHAFT.get(), new Item.Properties()));
|
||||
public static final RegistryObject<Item> CHAIR = ITEMS.register("chair",
|
||||
() -> new BlockItem(ModBlocks.CHAIR.get(), new Item.Properties()));
|
||||
public static final RegistryObject<Item> HELM = ITEMS.register("helm",
|
||||
() -> new BlockItem(ModBlocks.HELM.get(), new Item.Properties()));
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.titanicmc.titanic.ship;
|
||||
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.valkyrienskies.core.api.ships.LoadedServerShip;
|
||||
import org.valkyrienskies.core.api.world.ServerShipWorld;
|
||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Freshly {@link ShipUtil#assembleWreck assembled} ships are not always a
|
||||
* {@link LoadedServerShip} on the very tick they're created - Valkyrien Skies relocates the
|
||||
* collected blocks into a brand new ship storage area, and that area only becomes "loaded"
|
||||
* once VS2 finishes registering it, which can take a tick or more.
|
||||
* <p>
|
||||
* Rather than giving up immediately when that happens, pending raises are tracked here by the
|
||||
* ship's stable {@code long} id (not by {@link net.minecraft.core.BlockPos}, since the seed
|
||||
* position's block may itself have just been physically relocated as part of assembly) and
|
||||
* retried every server tick until the ship reports itself loaded, or a generous timeout is hit.
|
||||
*/
|
||||
public final class ShipRaiseScheduler {
|
||||
|
||||
/** ~30 seconds at 20 TPS. VS2 normally finishes loading within a tick or two. */
|
||||
private static final int MAX_WAIT_TICKS = 600;
|
||||
|
||||
private static final List<Pending> PENDING = new ArrayList<>();
|
||||
|
||||
private ShipRaiseScheduler() {
|
||||
}
|
||||
|
||||
/** Queues an already-assembled-but-not-yet-loaded ship to begin raising once it loads. */
|
||||
public static void schedule(ServerLevel level, long shipId, double targetWorldY) {
|
||||
PENDING.add(new Pending(level, shipId, targetWorldY));
|
||||
}
|
||||
|
||||
/** Call once per server tick (see {@link com.titanicmc.titanic.event.ForgeEventHandler}). */
|
||||
public static void tick() {
|
||||
if (PENDING.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Iterator<Pending> it = PENDING.iterator();
|
||||
while (it.hasNext()) {
|
||||
Pending pending = it.next();
|
||||
ServerShipWorld shipWorld = VSGameUtilsKt.getShipObjectWorld(pending.level.getServer());
|
||||
LoadedServerShip ship = shipWorld.getLoadedShips().getById(pending.shipId);
|
||||
if (ship != null) {
|
||||
TitanicMod.LOGGER.info(
|
||||
"Ship {} finished loading after {} ticks - beginning raise to y={}",
|
||||
pending.shipId, pending.ticksWaited, pending.targetWorldY);
|
||||
ShipUtil.beginRaiseSafely(pending.level, ship, pending.targetWorldY);
|
||||
it.remove();
|
||||
continue;
|
||||
}
|
||||
pending.ticksWaited++;
|
||||
if (pending.ticksWaited > MAX_WAIT_TICKS) {
|
||||
TitanicMod.LOGGER.warn(
|
||||
"Gave up waiting for assembled ship {} to finish loading for raising after {} ticks",
|
||||
pending.shipId, pending.ticksWaited);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Pending {
|
||||
final ServerLevel level;
|
||||
final long shipId;
|
||||
final double targetWorldY;
|
||||
int ticksWaited = 0;
|
||||
|
||||
Pending(ServerLevel level, long shipId, double targetWorldY) {
|
||||
this.level = level;
|
||||
this.shipId = shipId;
|
||||
this.targetWorldY = targetWorldY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package com.titanicmc.titanic.ship;
|
||||
|
||||
import com.titanicmc.titanic.Config;
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import com.titanicmc.titanic.registry.ModBlockTags;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.joml.primitives.AABBdc;
|
||||
import org.valkyrienskies.core.api.ships.LoadedServerShip;
|
||||
import org.valkyrienskies.core.api.ships.ServerShip;
|
||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
||||
import org.valkyrienskies.mod.common.assembly.ShipAssembler;
|
||||
|
||||
/**
|
||||
* All direct interop with the Valkyrien Skies 2 API lives here, so any future API changes
|
||||
* only need to be fixed in one place.
|
||||
* <p>
|
||||
* Important VS2 quirk this class relies on: once a structure is assembled into a ship, every
|
||||
* one of its blocks keeps living at its original ("storage") {@link BlockPos} for as long as
|
||||
* the ship exists, even though the ship visually moves elsewhere in the world. Any BlockPos
|
||||
* handed to us through normal block/interaction callbacks (place, use, block entity ticking)
|
||||
* is always already in that stable storage space - Valkyrien Skies transparently remaps
|
||||
* rendering, physics and click-raycasts for us. That means code elsewhere in this mod never
|
||||
* needs to special-case "is this block on a ship" positions; it can use them exactly like any
|
||||
* other {@link BlockPos}.
|
||||
*/
|
||||
public final class ShipUtil {
|
||||
|
||||
private ShipUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ship that manages storage of the block at {@code pos}, or {@code null} if
|
||||
* that position isn't part of any assembled ship.
|
||||
*/
|
||||
public static ServerShip getShipManagingPos(ServerLevel level, BlockPos pos) {
|
||||
return VSGameUtilsKt.getShipManagingPos(level, pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #getShipManagingPos}, but only returns a result if the ship is currently
|
||||
* loaded (required to read/write ship attachments like {@link TitanicShipControl}).
|
||||
*/
|
||||
public static LoadedServerShip getLoadedShipManagingPos(ServerLevel level, BlockPos pos) {
|
||||
ServerShip ship = getShipManagingPos(level, pos);
|
||||
return ship instanceof LoadedServerShip loaded ? loaded : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flood-fills every block tagged {@link ModBlockTags#SHIP_BLOCKS} connected to {@code seed}
|
||||
* (up to the configured safety limit). Used both to gather the initial wreck for assembly
|
||||
* and to re-scan an already assembled ship's blocks (e.g. to find remaining damaged blocks).
|
||||
* <p>
|
||||
* Only blocks that are actually part of a Titanic wreck/ship qualify - anything else (air,
|
||||
* fluids, and, importantly, the natural seafloor terrain a wreck rests on/against) acts as a
|
||||
* boundary the fill does not cross. Without this restriction a seed touching bare stone,
|
||||
* sand, or gravel would keep flooding through the entire connected landmass until it hit
|
||||
* {@code maxBlocks}, producing a huge, malformed "ship" instead of just the wreck.
|
||||
* <p>
|
||||
* {@code seed} itself is always accepted and expanded from regardless of that tag, even if
|
||||
* it isn't a wreck block - callers seed this from player-placed attachment points like the
|
||||
* {@link com.titanicmc.titanic.block.LiftingFrameBlock} lowered over the wreck, which need
|
||||
* to bridge into the tagged wreck blocks touching them rather than being rejected outright.
|
||||
*/
|
||||
public static Set<BlockPos> collectConnectedBlocks(ServerLevel level, BlockPos seed, int maxBlocks) {
|
||||
Set<BlockPos> collected = new HashSet<>();
|
||||
Set<Long> visited = new HashSet<>();
|
||||
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
|
||||
|
||||
BlockPos immutableSeed = seed.immutable();
|
||||
queue.add(immutableSeed);
|
||||
visited.add(immutableSeed.asLong());
|
||||
|
||||
while (!queue.isEmpty() && collected.size() < maxBlocks) {
|
||||
BlockPos pos = queue.poll();
|
||||
BlockState state = level.getBlockState(pos);
|
||||
boolean isSeed = pos.equals(immutableSeed);
|
||||
if (!isSeed && !state.is(ModBlockTags.SHIP_BLOCKS)) {
|
||||
continue;
|
||||
}
|
||||
if (state.isAir() || !state.getFluidState().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
collected.add(pos);
|
||||
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
if (dx == 0 && dy == 0 && dz == 0) continue;
|
||||
BlockPos next = pos.offset(dx, dy, dz);
|
||||
long key = next.asLong();
|
||||
if (visited.add(key)) {
|
||||
queue.add(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
/** Filters a previously collected block set down to positions currently holding {@code block}. */
|
||||
public static List<BlockPos> findBlocksOfType(ServerLevel level, Set<BlockPos> allBlocks, Block block) {
|
||||
List<BlockPos> found = new ArrayList<>();
|
||||
for (BlockPos pos : allBlocks) {
|
||||
if (level.getBlockState(pos).is(block)) {
|
||||
found.add(pos);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flood-fills every {@link ModBlockTags#SHIP_BLOCKS}-tagged block connected to {@code seed}
|
||||
* and hands the collected region to Valkyrien Skies so it becomes one real, physically
|
||||
* simulated ship.
|
||||
*/
|
||||
public static ServerShip assembleWreck(ServerLevel level, BlockPos seed) {
|
||||
Set<BlockPos> collected = collectConnectedBlocks(level, seed, Config.MAX_SHIP_BLOCKS.get());
|
||||
if (collected.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TitanicMod.LOGGER.info("Assembling Titanic hull with {} blocks", collected.size());
|
||||
return ShipAssembler.assembleToShip(level, collected, 1.0);
|
||||
}
|
||||
|
||||
/** Kicks off the raise sequence: assembles the wreck and applies lifting force until it surfaces. */
|
||||
public static void beginWreckRaise(ServerLevel level, BlockPos seed) {
|
||||
ServerShip assembled = getShipManagingPos(level, seed);
|
||||
if (assembled == null) {
|
||||
assembled = assembleWreck(level, seed);
|
||||
}
|
||||
if (assembled == null) {
|
||||
TitanicMod.LOGGER.warn("Failed to assemble wreck at {} for raising", seed);
|
||||
return;
|
||||
}
|
||||
|
||||
double targetWorldY = level.getSeaLevel() + 1;
|
||||
TitanicMod.LOGGER.info("Wreck assembled as ship id={} (already loaded={}), raise target y={}",
|
||||
assembled.getId(), assembled instanceof LoadedServerShip, targetWorldY);
|
||||
if (assembled instanceof LoadedServerShip ship) {
|
||||
beginRaiseSafely(level, ship, targetWorldY);
|
||||
} else {
|
||||
// A freshly assembled ship isn't always immediately Loaded - VS2 relocates the
|
||||
// collected blocks into a new ship storage area that can take a tick or more to
|
||||
// finish registering. Retry by stable ship id until it comes online instead of
|
||||
// failing outright (see ShipRaiseScheduler for why id, not BlockPos, is used).
|
||||
ShipRaiseScheduler.schedule(level, assembled.getId(), targetWorldY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes any player standing on/near the ship safely clear before starting the raise, then
|
||||
* begins it.
|
||||
* <p>
|
||||
* The raise repeatedly re-asserts the ship's position directly (see
|
||||
* {@link TitanicShipControl}), which was observed to violently launch a player standing on
|
||||
* the hull ~100 blocks away over the course of the raise - each of those position
|
||||
* corrections appears to trigger Minecraft's own "entity is stuck inside a block" pushback
|
||||
* against the player, and dozens of small pushes over a few seconds add up fast. Moving
|
||||
* players safely above the ship first avoids that class of collision entirely.
|
||||
*/
|
||||
public static void beginRaiseSafely(ServerLevel level, LoadedServerShip ship, double targetWorldY) {
|
||||
pushPlayersClear(level, ship);
|
||||
getOrCreateControl(ship).beginRaise(targetWorldY);
|
||||
}
|
||||
|
||||
/** Teleports any player inside/near the ship's world AABB to a safe spot directly above it. */
|
||||
private static void pushPlayersClear(ServerLevel level, LoadedServerShip ship) {
|
||||
AABBdc shipAabb = ship.getWorldAABB();
|
||||
AABB searchArea = new AABB(
|
||||
shipAabb.minX(), shipAabb.minY(), shipAabb.minZ(),
|
||||
shipAabb.maxX(), shipAabb.maxY(), shipAabb.maxZ())
|
||||
.inflate(4.0);
|
||||
double safeY = shipAabb.maxY() + 10.0;
|
||||
|
||||
for (ServerPlayer player : level.getEntitiesOfClass(ServerPlayer.class, searchArea)) {
|
||||
if (player.getY() < safeY) {
|
||||
player.teleportTo(player.getX(), safeY, player.getZ());
|
||||
player.setDeltaMovement(0.0, 0.0, 0.0);
|
||||
player.fallDistance = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets or creates the {@link TitanicShipControl} attachment for a loaded ship. */
|
||||
public static TitanicShipControl getOrCreateControl(LoadedServerShip ship) {
|
||||
TitanicShipControl control = ship.getAttachment(TitanicShipControl.class);
|
||||
if (control == null) {
|
||||
control = new TitanicShipControl();
|
||||
ship.setAttachment(TitanicShipControl.class, control);
|
||||
}
|
||||
// Keep this in sync unconditionally (cheap) so a valid LoadedServerShip reference is
|
||||
// always available for direct transform manipulation (see TitanicShipControl.onServerTick),
|
||||
// not just once a Helm block entity happens to refresh it.
|
||||
control.ship = ship;
|
||||
return control;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.titanicmc.titanic.ship;
|
||||
|
||||
import com.titanicmc.titanic.TitanicMod;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
import org.valkyrienskies.core.api.ships.LoadedServerShip;
|
||||
import org.valkyrienskies.core.api.ships.PhysShip;
|
||||
import org.valkyrienskies.core.api.ships.ServerTickListener;
|
||||
import org.valkyrienskies.core.api.ships.ShipPhysicsListener;
|
||||
import org.valkyrienskies.core.api.ships.properties.ShipTransform;
|
||||
import org.valkyrienskies.core.api.world.PhysLevel;
|
||||
import org.valkyrienskies.mod.api.SeatedControllingPlayer;
|
||||
|
||||
/**
|
||||
* Per-ship "brain" attached to every assembled Titanic hull. Handles three responsibilities:
|
||||
* <ul>
|
||||
* <li>Raising: steps the hull's position directly upward each tick until it breaches the
|
||||
* surface (see {@link #onServerTick}).</li>
|
||||
* <li>Buoyancy assist: while not yet repaired, the wreck is full of holes and shouldn't
|
||||
* realistically float - we cancel out net gravity so it doesn't sink while under tow.</li>
|
||||
* <li>Propulsion: Boilers (with an adjacent Crankshaft) feed {@link #addEnginePower(double)}
|
||||
* every tick; that power is turned into forward thrust in the direction the helm is
|
||||
* facing, and steering torque from the helm's turn input.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class TitanicShipControl implements ShipPhysicsListener, ServerTickListener {
|
||||
|
||||
private static final double GRAVITY = 10.0;
|
||||
/**
|
||||
* Linear damping applied to vertical velocity every physics tick (see {@link #physTick}).
|
||||
* Keeps any velocity the hull picks up (from collisions, residual momentum, etc.) from
|
||||
* building up and coasting the hull upward/downward forever once gravity is cancelled below.
|
||||
*/
|
||||
private static final double VERTICAL_DAMPING = 2.0;
|
||||
/**
|
||||
* Blocks climbed per server tick while raising (20 ticks/sec, so ~3 blocks/sec).
|
||||
* <p>
|
||||
* Raising is driven by directly stepping the ship's transform via
|
||||
* {@link org.valkyrienskies.core.api.ships.ServerShip#unsafeSetTransform} rather than by
|
||||
* applying physics forces. Force-based raising (mass * constant acceleration, every physics
|
||||
* tick) proved wildly unreliable for hulls this large: observed overshoot ranged from a few
|
||||
* blocks to 600+ blocks past the target in under a second, wildly out of proportion to the
|
||||
* force actually intended - almost certainly some multiplier/timestep mismatch specific to
|
||||
* hulls with this many blocks/this much mass. Direct, small, per-tick position steps sidestep
|
||||
* that whole class of problem entirely.
|
||||
*/
|
||||
private static final double RAISE_SPEED_PER_TICK = 0.15;
|
||||
|
||||
private volatile boolean raising = false;
|
||||
private volatile double raiseTargetWorldY = 62.0;
|
||||
private volatile boolean repaired = false;
|
||||
|
||||
private double pendingEnginePower = 0.0;
|
||||
private double enginePower = 0.0;
|
||||
|
||||
private int raiseTickCounter = 0;
|
||||
private boolean loggedInvalidMass = false;
|
||||
|
||||
/**
|
||||
* Our own authoritative X/Y/Z for the ship while raising, forced onto it every tick via
|
||||
* {@link org.valkyrienskies.core.api.ships.ServerShip#unsafeSetTransform} regardless of
|
||||
* whatever position VS2's physics/collision engine may have produced in between our ticks.
|
||||
* <p>
|
||||
* The read-back position ({@code getTransform()}) was observed to become wildly corrupted
|
||||
* mid-raise (Y jumping to values in the tens of thousands, even decreasing between ticks,
|
||||
* which our own upward-only stepping could never do) - a bug in VS2 itself when a
|
||||
* freshly-assembled hull's collision is resolved against the terrain/water it was just cut
|
||||
* out of. Trusting that read-back to decide "where to step from next" let the corruption
|
||||
* compound. Tracking our own value here and always re-asserting it sidesteps that entirely:
|
||||
* whatever VS2 does to the ship between our ticks is simply overwritten back to where we
|
||||
* expect it to be on the very next tick.
|
||||
*/
|
||||
private double raiseX = Double.NaN;
|
||||
private double raiseY = Double.NaN;
|
||||
private double raiseZ = Double.NaN;
|
||||
|
||||
/** Refreshed every game tick by the Helm block entity so physTick can find the seated player. */
|
||||
public volatile LoadedServerShip ship;
|
||||
|
||||
public void beginRaise(double targetWorldY) {
|
||||
this.raising = true;
|
||||
this.raiseTargetWorldY = targetWorldY;
|
||||
// NaN sentinel: onServerTick seeds these from the ship's real position on its first
|
||||
// raising tick, then owns them exclusively from there on (see field javadoc above).
|
||||
this.raiseX = Double.NaN;
|
||||
this.raiseY = Double.NaN;
|
||||
this.raiseZ = Double.NaN;
|
||||
TitanicMod.LOGGER.info("TitanicShipControl.beginRaise: target y={}", targetWorldY);
|
||||
}
|
||||
|
||||
public boolean isRaising() {
|
||||
return raising;
|
||||
}
|
||||
|
||||
public void markRepaired() {
|
||||
this.repaired = true;
|
||||
}
|
||||
|
||||
public boolean isRepaired() {
|
||||
return repaired;
|
||||
}
|
||||
|
||||
/** Called by a Boiler/Crankshaft pair every game tick to contribute propulsion power. */
|
||||
public void addEnginePower(double power) {
|
||||
pendingEnginePower += power;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick() {
|
||||
enginePower = pendingEnginePower;
|
||||
pendingEnginePower = 0.0;
|
||||
|
||||
if (!raising) {
|
||||
return;
|
||||
}
|
||||
LoadedServerShip currentShip = this.ship;
|
||||
if (currentShip == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShipTransform transform = currentShip.getTransform();
|
||||
if (Double.isNaN(raiseY)) {
|
||||
// First tick of this raise - seed our own tracked position from the real one once.
|
||||
Vector3dc startPos = transform.getPosition();
|
||||
raiseX = startPos.x();
|
||||
raiseY = startPos.y();
|
||||
raiseZ = startPos.z();
|
||||
}
|
||||
|
||||
if (raiseY >= raiseTargetWorldY) {
|
||||
raising = false;
|
||||
TitanicMod.LOGGER.info("Raising complete at y={}", raiseY);
|
||||
return;
|
||||
}
|
||||
|
||||
raiseY = Math.min(raiseY + RAISE_SPEED_PER_TICK, raiseTargetWorldY);
|
||||
Vector3d newPos = new Vector3d(raiseX, raiseY, raiseZ);
|
||||
ShipTransform newTransform =
|
||||
transform.copy(newPos, transform.getPositionInModel(), transform.getRotation(), transform.getScaling());
|
||||
currentShip.unsafeSetTransform(newTransform);
|
||||
|
||||
if (raiseTickCounter++ % 20 == 0) {
|
||||
TitanicMod.LOGGER.info("Raising: y={} target={} (actual ship-reported y={})",
|
||||
raiseY, raiseTargetWorldY, transform.getPosition().y());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void physTick(PhysShip physShip, PhysLevel physLevel) {
|
||||
double mass = physShip.getMass();
|
||||
|
||||
if (!(mass > 0.0) || !Double.isFinite(mass)) {
|
||||
// A ship's inertia data can momentarily be unset/invalid right as it finishes
|
||||
// loading. Applying forces computed from a bogus mass (0, NaN, Infinity) would
|
||||
// inject garbage into the physics engine, which can corrupt or silently delete the
|
||||
// ship - skip this tick entirely rather than risk that.
|
||||
if (!loggedInvalidMass) {
|
||||
TitanicMod.LOGGER.warn("TitanicShipControl.physTick: skipping tick(s), invalid mass={}", mass);
|
||||
loggedInvalidMass = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Valkyrien Skies has its own built-in fluid buoyancy simulation, which was fighting
|
||||
// our own hand-tuned gravity-cancel/raise force below - both were pushing the hull
|
||||
// upward at once, and depending on how much of the hull happened to be submerged that
|
||||
// combination produced wildly inconsistent, sometimes huge, overshoot (observed
|
||||
// anywhere from a few blocks to 150+ blocks past the raise target). We want full manual
|
||||
// control over vertical motion here, so disable VS2's automatic buoyancy/fluid drag
|
||||
// entirely and let our own forces be the only thing acting on it.
|
||||
physShip.setBuoyantFactor(0.0);
|
||||
physShip.setDoFluidDrag(false);
|
||||
|
||||
// NOTE: marking the ship static while raising was tried here to stop VS2's own
|
||||
// physics/collision engine from fighting our unsafeSetTransform calls. It backfired in a
|
||||
// different way than expected: our position updates DID appear to take effect visually
|
||||
// for the player (the hull was seen rising for a few seconds), but the physics engine's
|
||||
// own internal position never actually moved (confirmed via logging - it stayed pinned
|
||||
// at the pre-raise value for the entire raise) and un-staticing at the end snapped
|
||||
// everything back to that stale position, which is what looked like the ship
|
||||
// "disappearing" right after the visible rise. VS2's own reference usage of
|
||||
// unsafeSetTransform (its ShipCreatorItem) never touches setStatic either, so we don't
|
||||
// either - we stay dynamic and simply re-assert our own tracked position every tick in
|
||||
// onServerTick (see the raiseX/Y/Z field javadoc), unconditionally overwriting whatever
|
||||
// the physics engine did to it in between. Now that beginRaiseSafely moves nearby
|
||||
// players out of the way first, the engine's own uncontrolled collision response during
|
||||
// that tug-of-war can no longer launch anyone even if it still happens.
|
||||
|
||||
Vector3dc centerOfMass = physShip.getCenterOfMass();
|
||||
|
||||
// While actively raising, position is driven directly in onServerTick via
|
||||
// unsafeSetTransform - don't also fight it with gravity-cancel force here.
|
||||
if (!raising && !repaired) {
|
||||
// Cancel gravity so the patched-up hull doesn't sink under tow.
|
||||
physShip.applyWorldForce(new Vector3d(0.0, mass * GRAVITY, 0.0), centerOfMass);
|
||||
}
|
||||
|
||||
// Always bleed off vertical velocity a little so any residual momentum (from
|
||||
// collisions, buoyancy remnants, etc.) doesn't coast the hull upward/downward forever
|
||||
// once other vertical forces are balanced out.
|
||||
double verticalVelocity = physShip.getVelocity().y();
|
||||
physShip.applyWorldForce(new Vector3d(0.0, -mass * verticalVelocity * VERTICAL_DAMPING, 0.0), centerOfMass);
|
||||
|
||||
LoadedServerShip currentShip = this.ship;
|
||||
SeatedControllingPlayer controller =
|
||||
currentShip != null ? currentShip.getAttachment(SeatedControllingPlayer.class) : null;
|
||||
|
||||
if (enginePower > 0.0001 && controller != null) {
|
||||
Vector3d forwardDirection = new Vector3d(
|
||||
controller.getSeatInDirection().getStepX(),
|
||||
0.0,
|
||||
controller.getSeatInDirection().getStepZ());
|
||||
physShip.getTransform().getShipToWorldRotation().transform(forwardDirection);
|
||||
forwardDirection.normalize();
|
||||
|
||||
Vector3d thrust = forwardDirection.mul(enginePower * controller.getForwardImpulse());
|
||||
physShip.applyWorldForce(thrust, centerOfMass);
|
||||
|
||||
float turn = controller.getLeftImpulse();
|
||||
if (Math.abs(turn) > 0.0001) {
|
||||
double turnStrength = enginePower * 0.02 * turn;
|
||||
physShip.applyWorldTorque(new Vector3d(0.0, turnStrength, 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
// Always damp angular velocity a little so the hull doesn't spin forever.
|
||||
Vector3dc omega = physShip.getAngularVelocity();
|
||||
physShip.applyWorldTorque(new Vector3d(omega).mul(-mass * 0.02));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.titanicmc.titanic.worldgen;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.titanicmc.titanic.registry.ModBlocks;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext;
|
||||
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
|
||||
import net.minecraftforge.fml.ModList;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
|
||||
/**
|
||||
* Procedurally builds a stylised, single-piece Titanic wreck lying on the seafloor. This is
|
||||
* deliberately a simplified silhouette (a tapered hull with a bow, a few interior rooms, and
|
||||
* marked bridge/engine areas) rather than a 1:1 scale replica - it's generated entirely in
|
||||
* code, with no schematic/NBT structure file and no player-visible "assembly" of separate
|
||||
* pieces.
|
||||
*/
|
||||
public class WreckFeature extends Feature<NoneFeatureConfiguration> {
|
||||
|
||||
private static final int LENGTH = 48;
|
||||
private static final int WIDTH = 9;
|
||||
private static final int HEIGHT = 12;
|
||||
private static final int BOW_TAPER = LENGTH / 4;
|
||||
|
||||
public WreckFeature(Codec<NoneFeatureConfiguration> codec) {
|
||||
super(codec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(FeaturePlaceContext<NoneFeatureConfiguration> context) {
|
||||
WorldGenLevel level = context.level();
|
||||
RandomSource random = context.random();
|
||||
BlockPos origin = anchorToChunk(context.origin());
|
||||
|
||||
int centerZ = WIDTH / 2;
|
||||
|
||||
for (int x = 0; x < LENGTH; x++) {
|
||||
int halfWidth = halfWidthAt(x);
|
||||
boolean bowTip = x == 0;
|
||||
boolean sternWall = x == LENGTH - 1;
|
||||
|
||||
for (int y = 0; y < HEIGHT; y++) {
|
||||
boolean keel = y == 0;
|
||||
boolean deck = y == HEIGHT - 1;
|
||||
|
||||
for (int z = centerZ - halfWidth; z <= centerZ + halfWidth; z++) {
|
||||
boolean sideWall = z == centerZ - halfWidth || z == centerZ + halfWidth;
|
||||
boolean shell = bowTip || sternWall || keel || deck || sideWall;
|
||||
|
||||
BlockPos pos = origin.offset(x, y, z - centerZ);
|
||||
|
||||
if (!shell) {
|
||||
// Hollow interior.
|
||||
level.setBlock(pos, Blocks.WATER.defaultBlockState(), 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deck && random.nextFloat() < 0.06f) {
|
||||
// A few holes punched through the deck.
|
||||
level.setBlock(pos, Blocks.WATER.defaultBlockState(), 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sideWall && !bowTip && !sternWall && y == HEIGHT / 2 && z % 3 == 0) {
|
||||
level.setBlock(pos, ModBlocks.BROKEN_PORTHOLE.get().defaultBlockState(), 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shell && random.nextFloat() < 0.1f && !keel) {
|
||||
// Damage: leave a gap in the hull plating.
|
||||
continue;
|
||||
}
|
||||
|
||||
level.setBlock(pos, ModBlocks.DAMAGED_HULL.get().defaultBlockState(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildEngineRoom(level, origin, centerZ, random);
|
||||
buildGalleyAndCabins(level, origin, centerZ, random);
|
||||
buildBridge(level, origin, centerZ);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The placement modifiers ({@code in_square} + {@code heightmap}) hand us an origin whose
|
||||
* X/Z can land anywhere within the chunk currently being decorated. Since the hull is
|
||||
* {@link #LENGTH} (48) blocks long in X - three chunks' worth - building it forward from
|
||||
* that arbitrary in-chunk X would routinely write into chunks more than one chunk away from
|
||||
* the decorating chunk, which is exactly what trips vanilla's "Detected setBlock in a far
|
||||
* chunk" sanity check (a plain {@link Feature}'s {@link WorldGenLevel} is only guaranteed
|
||||
* safe for writes within one chunk of the one being decorated).
|
||||
* <p>
|
||||
* To stay inside that safe window regardless of where in its chunk the origin happens to
|
||||
* fall, the hull is anchored to the decorating chunk's own boundary instead of to the
|
||||
* origin's exact X: it always starts one full chunk before that chunk and runs for exactly
|
||||
* three chunks (48 blocks), i.e. spanning the decorating chunk minus-one through plus-one -
|
||||
* precisely the safe range - every time. Only X is adjusted; Y (from the heightmap sample)
|
||||
* and Z (the hull is only {@link #WIDTH} blocks wide, well under one chunk) are left as-is.
|
||||
*/
|
||||
private static BlockPos anchorToChunk(BlockPos origin) {
|
||||
int chunkStartX = origin.getX() - Math.floorMod(origin.getX(), 16);
|
||||
return new BlockPos(chunkStartX - 16, origin.getY(), origin.getZ());
|
||||
}
|
||||
|
||||
private static int halfWidthAt(int x) {
|
||||
int half = WIDTH / 2;
|
||||
if (x < BOW_TAPER) {
|
||||
int taperedHalf = Math.round(half * ((x + 1f) / BOW_TAPER));
|
||||
return Math.max(1, taperedHalf);
|
||||
}
|
||||
return half;
|
||||
}
|
||||
|
||||
private void buildEngineRoom(WorldGenLevel level, BlockPos origin, int centerZ, RandomSource random) {
|
||||
int startX = LENGTH / 4 + 2;
|
||||
int floorY = 1;
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int x = startX + i * 2;
|
||||
BlockPos boilerPos = origin.offset(x, floorY, 0);
|
||||
level.setBlock(boilerPos, ModBlocks.BOILER.get().defaultBlockState(), 2);
|
||||
level.setBlock(boilerPos.relative(Direction.SOUTH), ModBlocks.CRANKSHAFT.get().defaultBlockState(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildGalleyAndCabins(WorldGenLevel level, BlockPos origin, int centerZ, RandomSource random) {
|
||||
int x = (int) (LENGTH * 0.62);
|
||||
int floorY = 1;
|
||||
|
||||
BlockState cookingBlock = getFarmersDelightCookingBlock();
|
||||
level.setBlock(origin.offset(x, floorY, -2), cookingBlock, 2);
|
||||
level.setBlock(origin.offset(x, floorY, 2), ModBlocks.CHAIR.get().defaultBlockState(), 2);
|
||||
level.setBlock(origin.offset(x + 1, floorY, 2), ModBlocks.CHAIR.get().defaultBlockState(), 2);
|
||||
|
||||
int cabinX = (int) (LENGTH * 0.72);
|
||||
level.setBlock(origin.offset(cabinX, floorY, -1), Blocks.RED_BED.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST)
|
||||
.setValue(BlockStateProperties.BED_PART, net.minecraft.world.level.block.state.properties.BedPart.FOOT), 2);
|
||||
level.setBlock(origin.offset(cabinX + 1, floorY, -1), Blocks.RED_BED.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST)
|
||||
.setValue(BlockStateProperties.BED_PART, net.minecraft.world.level.block.state.properties.BedPart.HEAD), 2);
|
||||
}
|
||||
|
||||
private void buildBridge(WorldGenLevel level, BlockPos origin, int centerZ) {
|
||||
int x = LENGTH - 6;
|
||||
int floorY = HEIGHT - 3;
|
||||
|
||||
level.setBlock(origin.offset(x, floorY, 0), ModBlocks.HELM_SOCKET.get().defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST), 2);
|
||||
level.setBlock(origin.offset(x - 1, floorY, 0), ModBlocks.REPAIR_PANEL.get().defaultBlockState(), 2);
|
||||
}
|
||||
|
||||
private static BlockState getFarmersDelightCookingBlock() {
|
||||
if (ModList.get().isLoaded("farmersdelight")) {
|
||||
var block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation("farmersdelight", "cooking_pot"));
|
||||
if (block != null) {
|
||||
return block.defaultBlockState();
|
||||
}
|
||||
}
|
||||
return Blocks.FURNACE.defaultBlockState();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user