package com.tiedup.remake.commands; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.tiedup.remake.entities.*; import com.tiedup.remake.entities.skins.EliteKidnapperSkinManager; import com.tiedup.remake.items.ModItems; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import net.minecraft.resources.ResourceLocation; import com.tiedup.remake.v2.BodyRegionV2; import java.util.List; import java.util.Optional; import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.MobSpawnType; import net.minecraft.world.item.ItemStack; import net.minecraft.world.phys.AABB; /** * NPC management commands. * * Commands: * /npc spawn kidnapper [player] - Spawn a kidnapper at location * /npc spawn elite [player] - Spawn an elite kidnapper * /npc spawn archer [player] - Spawn an archer kidnapper * /npc spawn damsel [player] - Spawn a damsel NPC * /npc kill [radius] - Kill all mod NPCs in radius * /npc tie - Tie the nearest NPC * /npc gag - Gag the nearest NPC * /npc blindfold - Blindfold the nearest NPC * /npc collar - Collar the nearest NPC * /npc untie - Untie the nearest NPC (remove all) * /npc state - Show state of nearest NPC * /npc full - Apply full bondage to nearest NPC * * All commands require OP level 2. */ public class NPCCommand { public static void register( CommandDispatcher dispatcher ) { dispatcher.register(createNPCCommand()); } /** * Create the NPC command builder (for use as subcommand of /tiedup). * @return The command builder */ public static com.mojang.brigadier.builder.LiteralArgumentBuilder< CommandSourceStack > createNPCCommand() { return Commands.literal("npc") .requires(CommandHelper.REQUIRES_OP) .then( Commands.literal("spawn") // /npc spawn kidnapper [player] .then( Commands.literal("kidnapper") .executes(ctx -> spawnKidnapper(ctx, null)) .then( Commands.argument( "player", EntityArgument.player() ).executes(ctx -> spawnKidnapper( ctx, EntityArgument.getPlayer(ctx, "player") ) ) ) ) // /npc spawn elite [player] .then( Commands.literal("elite").then( Commands.argument("name", StringArgumentType.word()) .suggests((ctx, builder) -> { for (KidnapperVariant v : EliteKidnapperSkinManager.CORE.getAllVariants()) { builder.suggest(v.id()); } return builder.buildFuture(); }) .executes(ctx -> spawnElite(ctx, null)) .then( Commands.argument( "player", EntityArgument.player() ).executes(ctx -> spawnElite( ctx, EntityArgument.getPlayer( ctx, "player" ) ) ) ) ) ) // /npc spawn archer [player] .then( Commands.literal("archer") .executes(ctx -> spawnArcher(ctx, null)) .then( Commands.argument( "player", EntityArgument.player() ).executes(ctx -> spawnArcher( ctx, EntityArgument.getPlayer(ctx, "player") ) ) ) ) // /npc spawn damsel [player] .then( Commands.literal("damsel") .executes(ctx -> spawnDamsel(ctx, null)) .then( Commands.argument( "player", EntityArgument.player() ).executes(ctx -> spawnDamsel( ctx, EntityArgument.getPlayer(ctx, "player") ) ) ) ) ) // /npc kill - Kill all mod NPCs in radius .then( Commands.literal("kill") .executes(ctx -> killNPCs(ctx, 10)) .then( Commands.argument( "radius", com.mojang.brigadier.arguments.IntegerArgumentType.integer( 1, 100 ) ).executes(ctx -> killNPCs( ctx, com.mojang.brigadier.arguments.IntegerArgumentType.getInteger( ctx, "radius" ) ) ) ) ) // /npc tie - Tie nearest NPC .then(Commands.literal("tie").executes(NPCCommand::tieNPC)) // /npc gag - Gag nearest NPC .then(Commands.literal("gag").executes(NPCCommand::gagNPC)) // /npc blindfold - Blindfold nearest NPC .then( Commands.literal("blindfold").executes(NPCCommand::blindfoldNPC) ) // /npc collar - Collar nearest NPC .then(Commands.literal("collar").executes(NPCCommand::collarNPC)) // /npc untie - Untie nearest NPC .then(Commands.literal("untie").executes(NPCCommand::untieNPC)) // /npc state - Show NPC state .then(Commands.literal("state").executes(NPCCommand::showNPCState)) // /npc full - Full bondage on nearest NPC .then( Commands.literal("full").executes(NPCCommand::fullBondageNPC) ); } private static int spawnKidnapper( CommandContext context, ServerPlayer targetPlayer ) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); ServerLevel level = source.getLevel(); // Get spawn location double x, y, z; if (targetPlayer != null) { x = targetPlayer.getX(); y = targetPlayer.getY(); z = targetPlayer.getZ(); } else if (source.getEntity() instanceof ServerPlayer player) { x = player.getX(); y = player.getY(); z = player.getZ(); } else { source.sendFailure( Component.translatable("command.tiedup.error.must_be_player") ); return 0; } // Spawn the kidnapper EntityKidnapper kidnapper = ModEntities.KIDNAPPER.get().create(level); if (kidnapper != null) { kidnapper.moveTo(x, y, z, level.random.nextFloat() * 360F, 0.0F); kidnapper.finalizeSpawn( level, level.getCurrentDifficultyAt(kidnapper.blockPosition()), MobSpawnType.COMMAND, null, null ); level.addFreshEntity(kidnapper); source.sendSuccess( () -> Component.translatable( "command.tiedup.npc.spawned_kidnapper", formatPos(x, y, z) ).withStyle(ChatFormatting.GREEN), true ); return 1; } source.sendFailure(Component.translatable("command.tiedup.npc.spawn_failed_kidnapper")); return 0; } private static int spawnElite( CommandContext context, ServerPlayer targetPlayer ) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); ServerLevel level = source.getLevel(); String name = StringArgumentType.getString(context, "name"); // Parse variant KidnapperVariant variant = EliteKidnapperSkinManager.CORE.getVariant( name ); if (variant == null) { source.sendFailure( Component.translatable("command.tiedup.npc.unknown_variant", name) ); return 0; } // Get spawn location double x, y, z; if (targetPlayer != null) { x = targetPlayer.getX(); y = targetPlayer.getY(); z = targetPlayer.getZ(); } else if (source.getEntity() instanceof ServerPlayer player) { x = player.getX(); y = player.getY(); z = player.getZ(); } else { source.sendFailure( Component.translatable("command.tiedup.error.must_be_player") ); return 0; } // Spawn the elite kidnapper EntityKidnapperElite elite = ModEntities.KIDNAPPER_ELITE.get().create( level ); if (elite != null) { elite.moveTo(x, y, z, level.random.nextFloat() * 360F, 0.0F); elite.setKidnapperVariant(variant); elite.finalizeSpawn( level, level.getCurrentDifficultyAt(elite.blockPosition()), MobSpawnType.COMMAND, null, null ); level.addFreshEntity(elite); source.sendSuccess( () -> Component.translatable( "command.tiedup.npc.spawned_elite", variant.defaultName(), formatPos(x, y, z) ).withStyle(ChatFormatting.GREEN), true ); return 1; } source.sendFailure( Component.translatable("command.tiedup.npc.spawn_failed_elite") ); return 0; } private static int spawnArcher( CommandContext context, ServerPlayer targetPlayer ) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); ServerLevel level = source.getLevel(); // Get spawn location double x, y, z; if (targetPlayer != null) { x = targetPlayer.getX(); y = targetPlayer.getY(); z = targetPlayer.getZ(); } else if (source.getEntity() instanceof ServerPlayer player) { x = player.getX(); y = player.getY(); z = player.getZ(); } else { source.sendFailure( Component.translatable("command.tiedup.error.must_be_player") ); return 0; } // Spawn the archer EntityKidnapperArcher archer = ModEntities.KIDNAPPER_ARCHER.get().create(level); if (archer != null) { archer.moveTo(x, y, z, level.random.nextFloat() * 360F, 0.0F); archer.finalizeSpawn( level, level.getCurrentDifficultyAt(archer.blockPosition()), MobSpawnType.COMMAND, null, null ); level.addFreshEntity(archer); source.sendSuccess( () -> Component.translatable( "command.tiedup.npc.spawned_archer", formatPos(x, y, z) ).withStyle(ChatFormatting.GREEN), true ); return 1; } source.sendFailure( Component.translatable("command.tiedup.npc.spawn_failed_archer") ); return 0; } private static int spawnDamsel( CommandContext context, ServerPlayer targetPlayer ) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); ServerLevel level = source.getLevel(); // Get spawn location double x, y, z; if (targetPlayer != null) { x = targetPlayer.getX(); y = targetPlayer.getY(); z = targetPlayer.getZ(); } else if (source.getEntity() instanceof ServerPlayer player) { x = player.getX(); y = player.getY(); z = player.getZ(); } else { source.sendFailure( Component.translatable("command.tiedup.error.must_be_player") ); return 0; } // Spawn the damsel EntityDamsel damsel = ModEntities.DAMSEL.get().create(level); if (damsel != null) { damsel.moveTo(x, y, z, level.random.nextFloat() * 360F, 0.0F); damsel.finalizeSpawn( level, level.getCurrentDifficultyAt(damsel.blockPosition()), MobSpawnType.COMMAND, null, null ); level.addFreshEntity(damsel); source.sendSuccess( () -> Component.translatable( "command.tiedup.npc.spawned_damsel", formatPos(x, y, z) ).withStyle(ChatFormatting.GREEN), true ); return 1; } source.sendFailure(Component.translatable("command.tiedup.npc.spawn_failed_damsel")); return 0; } private static int killNPCs( CommandContext context, int radius ) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional playerOpt = CommandHelper.getPlayerOrFail( source ); if (playerOpt.isEmpty()) return 0; ServerPlayer player = playerOpt.get(); ServerLevel level = player.serverLevel(); int killed = 0; // Find and kill all mod NPCs in radius var entities = level.getEntitiesOfClass( net.minecraft.world.entity.LivingEntity.class, player.getBoundingBox().inflate(radius), e -> e instanceof com.tiedup.remake.entities.AbstractTiedUpNpc ); for (var entity : entities) { entity.discard(); killed++; } int finalKilled = killed; source.sendSuccess( () -> Component.translatable( "command.tiedup.npc.killed", finalKilled, radius ).withStyle(ChatFormatting.GREEN), true ); return killed; } private static String formatPos(double x, double y, double z) { return String.format("(%.1f, %.1f, %.1f)", x, y, z); } // NPC Bondage Commands (from DamselTestCommand) /** * Find the nearest mod NPC (Damsel or Kidnapper) within 10 blocks. */ private static IBondageState findNearestNPC( CommandContext context ) { Entity source = context.getSource().getEntity(); if (source == null) return null; AABB searchBox = source.getBoundingBox().inflate(10.0); // Search for any IBondageState entity (EntityDamsel implements this) List npcs = source .level() .getEntitiesOfClass(EntityDamsel.class, searchBox); if (npcs.isEmpty()) { return null; } // Return closest NPC return npcs .stream() .min((a, b) -> Double.compare(a.distanceToSqr(source), b.distanceToSqr(source)) ) .orElse(null); } private static int tieNPC(CommandContext context) { IBondageState npc = findNearestNPC(context); if (npc == null) { context .getSource() .sendFailure( Component.translatable("command.tiedup.npc.no_npc_nearby") ); return 0; } if (npc.isTiedUp()) { context .getSource() .sendFailure(Component.translatable("command.tiedup.npc.already_tied")); return 0; } npc.equip( BodyRegionV2.ARMS, DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes")) ); context .getSource() .sendSuccess( () -> Component.translatable("command.tiedup.npc.tied", npc.getKidnappedName()).withStyle(ChatFormatting.GREEN), true ); return 1; } private static int gagNPC(CommandContext context) { IBondageState npc = findNearestNPC(context); if (npc == null) { context .getSource() .sendFailure( Component.translatable("command.tiedup.npc.no_npc_nearby") ); return 0; } if (npc.isGagged()) { context .getSource() .sendFailure(Component.translatable("command.tiedup.npc.already_gagged")); return 0; } npc.equip( BodyRegionV2.MOUTH, DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag")) ); context .getSource() .sendSuccess( () -> Component.translatable("command.tiedup.npc.gagged", npc.getKidnappedName()).withStyle(ChatFormatting.GREEN), true ); return 1; } private static int blindfoldNPC( CommandContext context ) { IBondageState npc = findNearestNPC(context); if (npc == null) { context .getSource() .sendFailure( Component.translatable("command.tiedup.npc.no_npc_nearby") ); return 0; } if (npc.isBlindfolded()) { context .getSource() .sendFailure(Component.translatable("command.tiedup.npc.already_blindfolded")); return 0; } npc.equip( BodyRegionV2.EYES, DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold")) ); context .getSource() .sendSuccess( () -> Component.translatable("command.tiedup.npc.blindfolded", npc.getKidnappedName() ).withStyle(ChatFormatting.GREEN), true ); return 1; } private static int collarNPC(CommandContext context) { IBondageState npc = findNearestNPC(context); if (npc == null) { context .getSource() .sendFailure( Component.translatable("command.tiedup.npc.no_npc_nearby") ); return 0; } if (npc.hasCollar()) { context .getSource() .sendFailure(Component.translatable("command.tiedup.npc.already_collared")); return 0; } npc.equip( BodyRegionV2.NECK, DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_collar")) ); context .getSource() .sendSuccess( () -> Component.translatable("command.tiedup.npc.collared", npc.getKidnappedName()).withStyle(ChatFormatting.GREEN), true ); return 1; } private static int untieNPC(CommandContext context) { IBondageState npc = findNearestNPC(context); if (npc == null) { context .getSource() .sendFailure( Component.translatable("command.tiedup.npc.no_npc_nearby") ); return 0; } npc.untie(true); // Drop items context .getSource() .sendSuccess( () -> Component.translatable("command.tiedup.npc.untied", npc.getKidnappedName()).withStyle(ChatFormatting.GREEN), true ); return 1; } private static int fullBondageNPC( CommandContext context ) { IBondageState npc = findNearestNPC(context); if (npc == null) { context .getSource() .sendFailure( Component.translatable("command.tiedup.npc.no_npc_nearby") ); return 0; } // Apply full bondage using AbstractTiedUpNpc method if ( npc instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npcEntity ) { npcEntity.applyBondage( DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes")), DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag")), DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold")), DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs")), DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_collar")), ItemStack.EMPTY // No clothes ); } context .getSource() .sendSuccess( () -> Component.translatable( "command.tiedup.npc.fully_restrained", npc.getKidnappedName() ).withStyle(ChatFormatting.GREEN), true ); return 1; } private static int showNPCState( CommandContext context ) { IBondageState npc = findNearestNPC(context); if (npc == null) { context .getSource() .sendFailure( Component.translatable("command.tiedup.npc.no_npc_nearby") ); return 0; } CommandSourceStack source = context.getSource(); source.sendSuccess( () -> Component.translatable( "command.tiedup.npc.state_header", npc.getKidnappedName() ).withStyle(ChatFormatting.GOLD), false ); if (npc instanceof EntityDamsel damsel) { source.sendSuccess( () -> Component.translatable("command.tiedup.npc.state_variant", damsel.getVariantId()).withStyle(ChatFormatting.YELLOW), false ); source.sendSuccess( () -> Component.translatable("command.tiedup.npc.state_slim_arms", Component.translatable(damsel.hasSlimArms() ? "command.tiedup.yes" : "command.tiedup.no") ).withStyle(ChatFormatting.YELLOW), false ); } source.sendSuccess( () -> Component.translatable("command.tiedup.npc.state_tied", Component.translatable(npc.isTiedUp() ? "command.tiedup.yes" : "command.tiedup.no") ).withStyle(ChatFormatting.YELLOW), false ); source.sendSuccess( () -> Component.translatable("command.tiedup.npc.state_gagged", Component.translatable(npc.isGagged() ? "command.tiedup.yes" : "command.tiedup.no") ).withStyle(ChatFormatting.YELLOW), false ); source.sendSuccess( () -> Component.translatable("command.tiedup.npc.state_blindfolded", Component.translatable(npc.isBlindfolded() ? "command.tiedup.yes" : "command.tiedup.no") ).withStyle(ChatFormatting.YELLOW), false ); source.sendSuccess( () -> Component.translatable("command.tiedup.npc.state_collar", Component.translatable(npc.hasCollar() ? "command.tiedup.yes" : "command.tiedup.no") ).withStyle(ChatFormatting.YELLOW), false ); source.sendSuccess( () -> Component.translatable("command.tiedup.npc.state_earplugs", Component.translatable(npc.hasEarplugs() ? "command.tiedup.yes" : "command.tiedup.no") ).withStyle(ChatFormatting.YELLOW), false ); source.sendSuccess( () -> Component.translatable("command.tiedup.npc.state_captive", Component.translatable(npc.isCaptive() ? "command.tiedup.yes" : "command.tiedup.no") ).withStyle(ChatFormatting.YELLOW), false ); return 1; } }