package mindustry.input; import arc.*; import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.input.*; import arc.input.GestureDetector.*; import arc.math.*; import arc.math.geom.*; import arc.scene.*; import arc.scene.event.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.struct.Queue; import arc.util.*; import mindustry.*; import mindustry.ai.*; import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.async.*; import mindustry.content.*; import mindustry.core.*; import mindustry.entities.*; import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.input.Placement.*; import mindustry.net.Administration.*; import mindustry.net.*; import mindustry.type.*; import mindustry.ui.fragments.*; import mindustry.world.*; import mindustry.world.blocks.ConstructBlock.*; import mindustry.world.blocks.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.meta.*; import java.util.*; import static arc.Core.*; import static mindustry.Vars.*; public abstract class InputHandler implements InputProcessor, GestureListener{ //not sure where else to put this - maps unique commands based on position to a list of units that will be turned into a unit group static ObjectMap> queuedCommands = new ObjectMap<>(); /** Used for dropping items. */ final static float playerSelectRange = mobile ? 17f : 11f; final static float unitSelectRadScl = 1f; final static IntSeq removed = new IntSeq(); final static IntSet intSet = new IntSet(); /** Maximum line length. */ final static int maxLength = 100; final static Rect r1 = new Rect(), r2 = new Rect(); final static Seq tmpUnits = new Seq<>(false); final static Binding[] controlGroupBindings = { Binding.block_select_01, Binding.block_select_02, Binding.block_select_03, Binding.block_select_04, Binding.block_select_05, Binding.block_select_06, Binding.block_select_07, Binding.block_select_08, Binding.block_select_09, Binding.block_select_10 }; /** If true, there is a cutscene currently occurring in logic. */ public boolean logicCutscene; public Vec2 logicCamPan = new Vec2(); public float logicCamSpeed = 0.1f; public float logicCutsceneZoom = -1f; /** If any of these functions return true, input is locked. */ public Seq inputLocks = Seq.with(() -> renderer.isCutscene(), () -> logicCutscene); public Interval controlInterval = new Interval(); public @Nullable Block block; public boolean overrideLineRotation; public int rotation; public boolean droppingItem; public float itemDepositCooldown; public Group uiGroup; public boolean isBuilding = true, buildWasAutoPaused = false, wasShooting = false; public @Nullable UnitType controlledType; public float recentRespawnTimer; public @Nullable Schematic lastSchematic; public GestureDetector detector; public PlaceLine line = new PlaceLine(); public BuildPlan resultplan; public BuildPlan bplan = new BuildPlan(); public Seq linePlans = new Seq<>(); public Seq selectPlans = new Seq<>(BuildPlan.class); public Queue lastPlans = new Queue<>(); public @Nullable Unit lastUnit; public @Nullable Unit spectating; //for RTS controls public Seq selectedUnits = new Seq<>(); public Seq commandBuildings = new Seq<>(false); public boolean commandMode = false; public boolean commandRect = false; public boolean tappedOne = false; public float commandRectX, commandRectY; /** Groups of units saved to different hotkeys */ public IntSeq[] controlGroups = new IntSeq[controlGroupBindings.length]; private Seq plansOut = new Seq<>(BuildPlan.class); private QuadTree playerPlanTree = new QuadTree<>(new Rect()); public final BlockInventoryFragment inv; public final BlockConfigFragment config; public final PlanConfigFragment planConfig; private WidgetGroup group = new WidgetGroup(); private final Eachable allPlans = cons -> { if(!player.dead()){ player.unit().plans().each(cons); } selectPlans.each(cons); linePlans.each(cons); }; private final Eachable allSelectLines = cons -> { selectPlans.each(cons); linePlans.each(cons); }; public InputHandler(){ group.touchable = Touchable.childrenOnly; inv = new BlockInventoryFragment(); config = new BlockConfigFragment(); planConfig = new PlanConfigFragment(); Events.on(UnitDestroyEvent.class, e -> { if(e.unit != null && e.unit.isPlayer() && e.unit.getPlayer().isLocal() && e.unit.type.weapons.contains(w -> w.bullet.killShooter)){ player.shooting = false; } }); Events.on(WorldLoadEvent.class, e -> { playerPlanTree = new QuadTree<>(new Rect(0f, 0f, world.unitWidth(), world.unitHeight())); }); Events.on(ResetEvent.class, e -> { logicCutscene = false; itemDepositCooldown = 0f; Arrays.fill(controlGroups, null); lastUnit = null; lastPlans.clear(); }); } //methods to override @Remote(called = Loc.server, unreliable = true) public static void transferItemEffect(Item item, float x, float y, Itemsc to){ if(to == null) return; createItemTransfer(item, 1, x, y, to, null); } @Remote(called = Loc.server, unreliable = true) public static void takeItems(Building build, Item item, int amount, Unit to){ if(to == null || build == null) return; int removed = build.removeStack(item, Math.min(to.maxAccepted(item), amount)); if(removed == 0) return; to.addItem(item, removed); for(int j = 0; j < Mathf.clamp(removed / 3, 1, 8); j++){ Time.run(j * 3f, () -> transferItemEffect(item, build.x, build.y, to)); } } @Remote(called = Loc.server, unreliable = true) public static void transferItemToUnit(Item item, float x, float y, Itemsc to){ if(to == null) return; createItemTransfer(item, 1, x, y, to, () -> to.addItem(item)); } @Remote(called = Loc.server, unreliable = true) public static void setItem(Building build, Item item, int amount){ if(build == null || build.items == null) return; build.items.set(item, amount); } @Remote(called = Loc.server, unreliable = true) public static void clearItems(Building build){ if(build == null || build.items == null) return; build.items.clear(); } @Remote(called = Loc.server, unreliable = true) public static void transferItemTo(@Nullable Unit unit, Item item, int amount, float x, float y, Building build){ if(build == null || build.items == null || item == null) return; if(unit != null && unit.item() == item) unit.stack.amount = Math.max(unit.stack.amount - amount, 0); for(int i = 0; i < Mathf.clamp(amount / 3, 1, 8); i++){ Time.run(i * 3, () -> createItemTransfer(item, amount, x, y, build, () -> {})); } if(amount > 0){ build.handleStack(item, amount, unit); } } @Remote(called = Loc.both, targets = Loc.both, forward = true, unreliable = true) public static void deletePlans(Player player, int[] positions){ if(net.server() && !netServer.admins.allowAction(player, ActionType.removePlanned, a -> a.plans = positions)){ throw new ValidateException(player, "Player cannot remove plans."); } if(player == null) return; var it = player.team().data().plans.iterator(); //O(n^2) search here; no way around it outer: while(it.hasNext()){ var plan = it.next(); for(int pos : positions){ if(plan.x == Point2.x(pos) && plan.y == Point2.y(pos)){ plan.removed = true; it.remove(); continue outer; } } } } public static void createItemTransfer(Item item, int amount, float x, float y, Position to, Runnable done){ Fx.itemTransfer.at(x, y, amount, item.color, to); if(done != null){ Time.run(Fx.itemTransfer.lifetime, done); } } @Remote(called = Loc.server, targets = Loc.both, forward = true) public static void commandUnits(Player player, int[] unitIds, @Nullable Building buildTarget, @Nullable Unit unitTarget, @Nullable Vec2 posTarget, boolean queueCommand, boolean finalBatch){ if(player == null || unitIds == null) return; if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> { event.unitIDs = unitIds; })){ throw new ValidateException(player, "Player cannot command units."); } Teamc teamTarget = buildTarget == null ? unitTarget : buildTarget; Vec2 targetAsVec = new Vec2().set(teamTarget != null ? teamTarget : posTarget); Seq toAdd = queuedCommands.get(targetAsVec, Seq::new); boolean anyCommandedTarget = false; for(int id : unitIds){ Unit unit = Groups.unit.getByID(id); if(unit != null && unit.team == player.team() && unit.controller() instanceof CommandAI ai){ //implicitly order it to move if(ai.command == null || ai.command.switchToMove){ ai.command(UnitCommand.moveCommand); } if(teamTarget != null && teamTarget.team() != player.team() && !(teamTarget instanceof Unit u && !unit.canTarget(u)) && !(teamTarget instanceof Building && !unit.type.targetGround)){ anyCommandedTarget = true; if(queueCommand){ ai.commandQueue(teamTarget); }else{ ai.commandQueue.clear(); ai.commandTarget(teamTarget); } }else if(posTarget != null){ if(queueCommand){ ai.commandQueue(posTarget); }else{ ai.commandQueue.clear(); ai.commandPosition(posTarget); } } unit.lastCommanded = player.coloredName(); if(ai.commandQueue.size <= 0){ ai.group = null; } //remove when other player command if(!headless && player != Vars.player){ control.input.selectedUnits.remove(unit); } toAdd.add(unit); } } //in the "final batch" of commands, assign formations based on EVERYTHING that was commanded. if(finalBatch){ //each physics layer has its own group UnitGroup[] groups = new UnitGroup[PhysicsProcess.layers]; var units = queuedCommands.remove(targetAsVec); for(Unit unit : units){ if(unit.controller() instanceof CommandAI ai){ //only assign a group when this is not a queued command if(ai.commandQueue.size == 0 && unitIds.length > 1){ int layer = unit.collisionLayer(); if(layer == -1) layer = 0; if(groups[layer] == null){ groups[layer] = new UnitGroup(); } groups[layer].units.add(unit); ai.group = groups[layer]; } } } for(int i = 0; i < groups.length; i ++){ var group = groups[i]; if(group != null && group.units.size > 0){ group.calculateFormation(targetAsVec, i); } } } if(unitIds.length > 0 && player == Vars.player && !state.isPaused()){ if(anyCommandedTarget){ Fx.attackCommand.at(teamTarget); }else{ Fx.moveCommand.at(posTarget); } } } @Remote(called = Loc.server, targets = Loc.both, forward = true) public static void setUnitCommand(Player player, int[] unitIds, UnitCommand command){ if(player == null || unitIds == null || command == null) return; if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> { event.unitIDs = unitIds; })){ throw new ValidateException(player, "Player cannot command units."); } for(int id : unitIds){ Unit unit = Groups.unit.getByID(id); if(unit != null && unit.team == player.team() && unit.controller() instanceof CommandAI ai){ boolean reset = command.resetTarget || ai.currentCommand().resetTarget; ai.command(command); if(reset){ ai.targetPos = null; ai.attackTarget = null; } unit.lastCommanded = player.coloredName(); } } } @Remote(called = Loc.server, targets = Loc.both, forward = true) public static void setUnitStance(Player player, int[] unitIds, UnitStance stance){ if(player == null || unitIds == null || stance == null) return; if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> { event.unitIDs = unitIds; })){ throw new ValidateException(player, "Player cannot command units."); } for(int id : unitIds){ Unit unit = Groups.unit.getByID(id); if(unit != null && unit.team == player.team() && unit.controller() instanceof CommandAI ai){ if(stance == UnitStance.stop){ //not a real stance, just cancels orders ai.clearCommands(); }else{ ai.stance = stance; } unit.lastCommanded = player.coloredName(); } } } @Remote(called = Loc.server, targets = Loc.both, forward = true) public static void commandBuilding(Player player, int[] buildings, Vec2 target){ if(player == null || target == null) return; if(net.server() && !netServer.admins.allowAction(player, ActionType.commandBuilding, event -> { event.buildingPositions = buildings; })){ throw new ValidateException(player, "Player cannot command buildings."); } for(int pos : buildings){ var build = world.build(pos); if(build == null || build.team() != player.team() || !build.block.commandable) continue; build.onCommand(target); build.updateLastAccess(player); if(!state.isPaused() && player == Vars.player){ Fx.moveCommand.at(target); } Events.fire(new BuildingCommandEvent(player, build, target)); } } @Remote(called = Loc.server, targets = Loc.both, forward = true) public static void requestItem(Player player, Building build, Item item, int amount){ if(player == null || build == null || !build.interactable(player.team()) || !player.within(build, itemTransferRange) || player.dead() || amount <= 0) return; if(net.server() && (!Units.canInteract(player, build) || !netServer.admins.allowAction(player, ActionType.withdrawItem, build.tile, action -> { action.item = item; action.itemAmount = amount; }))){ throw new ValidateException(player, "Player cannot request items."); } Call.takeItems(build, item, Math.min(player.unit().maxAccepted(item), amount), player.unit()); Events.fire(new WithdrawEvent(build, player, item, amount)); } @Remote(targets = Loc.both, forward = true, called = Loc.server) public static void transferInventory(Player player, Building build){ if(player == null || build == null || !player.within(build, itemTransferRange) || build.items == null || player.dead() || (state.rules.onlyDepositCore && !(build instanceof CoreBuild))) return; if(net.server() && (player.unit().stack.amount <= 0 || !Units.canInteract(player, build) || //to avoid rejecting deposit packets that happen to overlap due to packet speed differences, the actual cap is double the cooldown with 2 deposits. (!player.isLocal() && !player.itemDepositRate.allow((long)(state.rules.itemDepositCooldown * 1000 * 2), 2)) || !netServer.admins.allowAction(player, ActionType.depositItem, build.tile, action -> { action.itemAmount = player.unit().stack.amount; action.item = player.unit().item(); }))){ throw new ValidateException(player, "Player cannot transfer an item."); } var unit = player.unit(); Item item = unit.item(); int accepted = build.acceptStack(item, unit.stack.amount, unit); Call.transferItemTo(unit, item, accepted, unit.x, unit.y, build); Events.fire(new DepositEvent(build, player, item, accepted)); } @Remote(variants = Variant.one) public static void removeQueueBlock(int x, int y, boolean breaking){ player.unit().removeBuild(x, y, breaking); } @Remote(targets = Loc.both, called = Loc.server) public static void requestUnitPayload(Player player, Unit target){ if(player == null || !(player.unit() instanceof Payloadc pay)) return; Unit unit = player.unit(); if(target.isAI() && target.isGrounded() && pay.canPickup(target) && target.within(unit, unit.type.hitSize * 2f + target.type.hitSize * 2f)){ Call.pickedUnitPayload(unit, target); } } @Remote(targets = Loc.both, called = Loc.server) public static void requestBuildPayload(Player player, Building build){ if(player == null || !(player.unit() instanceof Payloadc pay) || build == null) return; Unit unit = player.unit(); if(!unit.within(build, tilesize * build.block.size * 1.2f + tilesize * 5f)) return; if(net.server() && !netServer.admins.allowAction(player, ActionType.pickupBlock, build.tile, action -> { action.unit = unit; })){ throw new ValidateException(player, "Player cannot pick up a block."); } if(state.teams.canInteract(unit.team, build.team)){ //pick up block's payload Payload current = build.getPayload(); if(current != null && pay.canPickupPayload(current)){ Call.pickedBuildPayload(unit, build, false); //pick up whole building directly }else if(build.block.buildVisibility != BuildVisibility.hidden && build.canPickup() && pay.canPickup(build)){ Call.pickedBuildPayload(unit, build, true); } } } @Remote(targets = Loc.server, called = Loc.server) public static void pickedUnitPayload(Unit unit, Unit target){ if(target != null && unit instanceof Payloadc pay){ pay.pickup(target); }else if(target != null){ target.remove(); } } @Remote(targets = Loc.server, called = Loc.server) public static void pickedBuildPayload(Unit unit, Building build, boolean onGround){ if(build != null && unit instanceof Payloadc pay){ if(onGround){ if(build.block.buildVisibility != BuildVisibility.hidden && build.canPickup() && pay.canPickup(build)){ pay.pickup(build); }else{ Fx.unitPickup.at(build); build.tile.remove(); } }else{ Payload current = build.getPayload(); if(current != null && pay.canPickupPayload(current)){ Payload taken = build.takePayload(); if(taken != null){ pay.addPayload(taken); Fx.unitPickup.at(build); } } } }else if(build != null && onGround){ Fx.unitPickup.at(build); build.tile.remove(); } } @Remote(targets = Loc.both, called = Loc.server) public static void requestDropPayload(Player player, float x, float y){ if(player == null || net.client() || player.dead()) return; Payloadc pay = (Payloadc)player.unit(); if(pay.payloads().isEmpty()) return; if(net.server() && !netServer.admins.allowAction(player, ActionType.dropPayload, player.unit().tileOn(), action -> { action.payload = pay.payloads().peek(); })){ throw new ValidateException(player, "Player cannot drop a payload."); } //apply margin of error Tmp.v1.set(x, y).sub(pay).limit(tilesize * 4f).add(pay); float cx = Tmp.v1.x, cy = Tmp.v1.y; Call.payloadDropped(player.unit(), cx, cy); } @Remote(called = Loc.server, targets = Loc.server) public static void payloadDropped(Unit unit, float x, float y){ if(unit instanceof Payloadc pay){ float prevx = pay.x(), prevy = pay.y(); pay.set(x, y); pay.dropLastPayload(); pay.set(prevx, prevy); } } @Remote(called = Loc.server) public static void unitEnteredPayload(Unit unit, Building build){ if(unit == null || build == null || unit.team != build.team) return; unit.remove(); //reset the enter command if(unit.controller() instanceof CommandAI ai && ai.command == UnitCommand.enterPayloadCommand){ ai.clearCommands(); ai.command = UnitCommand.moveCommand; } //clear removed state of unit so it can be synced if(Vars.net.client()){ Vars.netClient.clearRemovedEntity(unit.id); } UnitPayload unitPay = new UnitPayload(unit); if(build.acceptPayload(build, unitPay)){ Fx.unitDrop.at(build); build.handlePayload(build, unitPay); } } @Remote(targets = Loc.client, called = Loc.server) public static void dropItem(Player player, float angle){ if(player == null) return; if(net.server() && player.unit().stack.amount <= 0){ throw new ValidateException(player, "Player cannot drop an item."); } var unit = player.unit(); Fx.dropItem.at(unit.x, unit.y, angle, Color.white, unit.item()); unit.clearItem(); } @Remote(targets = Loc.both, called = Loc.server, forward = true, unreliable = true) public static void rotateBlock(@Nullable Player player, Building build, boolean direction){ if(build == null) return; if(net.server() && (!Units.canInteract(player, build) || !netServer.admins.allowAction(player, ActionType.rotate, build.tile, action -> action.rotation = Mathf.mod(build.rotation + Mathf.sign(direction), 4)))){ throw new ValidateException(player, "Player cannot rotate a block."); } if(player != null) build.updateLastAccess(player); int previous = build.rotation; build.rotation = Mathf.mod(build.rotation + Mathf.sign(direction), 4); build.updateProximity(); build.noSleep(); Fx.rotateBlock.at(build.x, build.y, build.block.size); Events.fire(new BuildRotateEvent(build, player == null ? null : player.unit(), previous)); } @Remote(targets = Loc.both, called = Loc.both, forward = true) public static void tileConfig(@Nullable Player player, Building build, @Nullable Object value){ if(build == null && net.server()) throw new ValidateException(player, "building is null"); if(build == null) return; if(net.server() && (!Units.canInteract(player, build) || !netServer.admins.allowAction(player, ActionType.configure, build.tile, action -> action.config = value))){ if(player.con != null){ var packet = new TileConfigCallPacket(); //undo the config on the client packet.player = player; packet.build = build; packet.value = build.config(); player.con.send(packet, true); } throw new ValidateException(player, "Player cannot configure a tile."); } build.configured(player == null || player.dead() ? null : player.unit(), value); Events.fire(new ConfigEvent(build, player, value)); } //only useful for servers or local mods, and is not replicated across clients //uses unreliable packets due to high frequency @Remote(targets = Loc.both, called = Loc.both, unreliable = true) public static void tileTap(@Nullable Player player, Tile tile){ if(tile == null) return; Events.fire(new TapEvent(player, tile)); } @Remote(targets = Loc.both, called = Loc.server, forward = true) public static void buildingControlSelect(Player player, Building build){ if(player == null || build == null || player.dead()) return; //make sure player is allowed to control the building if(net.server() && !netServer.admins.allowAction(player, ActionType.buildSelect, action -> action.tile = build.tile)){ throw new ValidateException(player, "Player cannot control a building."); } if(player.team() == build.team && build.canControlSelect(player.unit())){ var before = player.unit(); build.onControlSelect(player.unit()); if(!before.dead && before.spawnedByCore && !before.isPlayer()){ Call.unitDespawn(before); } } } @Remote(called = Loc.server) public static void unitBuildingControlSelect(Unit unit, Building build){ if(unit == null || unit.dead()) return; //client skips checks to prevent ghost units if(unit.team() == build.team && (net.client() || build.canControlSelect(unit))){ build.onControlSelect(unit); } } @Remote(targets = Loc.both, called = Loc.both, forward = true) public static void unitControl(Player player, @Nullable Unit unit){ if(player == null) return; //make sure player is allowed to control the unit if(net.server() && (!state.rules.possessionAllowed || !netServer.admins.allowAction(player, ActionType.control, action -> action.unit = unit))){ throw new ValidateException(player, "Player cannot control a unit."); } //clear player unit when they possess a core if(unit == null){ //just clear the unit (is this used?) player.clearUnit(); //make sure it's AI controlled, so players can't overwrite each other }else if(unit.isAI() && unit.team == player.team() && !unit.dead && unit.playerControllable()){ if(net.client() && player.isLocal()){ player.justSwitchFrom = player.unit(); player.justSwitchTo = unit; } //TODO range check for docking? var before = player.unit(); player.unit(unit); if(before != null){ if(before.spawnedByCore){ unit.dockedType = before.type; }else if(before.dockedType != null && before.dockedType.coreUnitDock){ //direct dock transfer??? unit.dockedType = before.dockedType; } if(before.spawnedByCore && !before.isPlayer()){ Call.unitDespawn(before); } } Time.run(Fx.unitSpirit.lifetime, () -> Fx.unitControl.at(unit.x, unit.y, 0f, unit)); if(!player.dead()){ Fx.unitSpirit.at(player.x, player.y, 0f, unit); } }else if(net.server()){ //reject forwarding the packet if the unit was dead, AI or team throw new ValidateException(player, "Player attempted to control invalid unit."); } Events.fire(new UnitControlEvent(player, unit)); } @Remote(targets = Loc.both, called = Loc.server, forward = true) public static void unitClear(Player player){ if(player == null) return; //make sure player is allowed to control the building if(net.server() && !netServer.admins.allowAction(player, ActionType.respawn, action -> {})){ throw new ValidateException(player, "Player cannot respawn."); } if(!player.dead() && !player.unit().spawnedByCore){ var docked = player.unit().dockedType; //get best core unit type as approximation if(docked == null){ var closest = player.bestCore(); if(closest != null){ docked = ((CoreBlock)closest.block).unitType; } } //respawn if necessary if(docked != null && docked.coreUnitDock){ //TODO animation, etc Fx.spawn.at(player); if(!net.client()){ Unit unit = docked.create(player.team()); unit.set(player.unit()); //translate backwards so it doesn't spawn stuck in the unit if(player.unit().isFlying() && unit.type.flying){ Tmp.v1.trns(player.unit().rotation + 180f, player.unit().hitSize / 2f + unit.hitSize / 2f); unit.x += Tmp.v1.x; unit.y += Tmp.v1.y; } unit.rotation(player.unit().rotation); //unit.impulse(0f, -3f); //TODO should there be an impulse? unit.controller(player); unit.spawnedByCore(true); unit.add(); } //skip standard respawn code return; } } //should only get to this code if docking failed or this isn't a docking unit //problem: this gets called on both ends. it shouldn't be. Fx.spawn.at(player); player.clearUnit(); player.checkSpawn(); player.deathTimer = Player.deathDelay + 1f; //for instant respawn } /** Adds an input lock; if this function returns true, input is locked. Used for mod 'cutscenes' or custom camera panning. */ public void addLock(Boolp lock){ inputLocks.add(lock); } /** @return whether most input is locked, for 'cutscenes' */ public boolean locked(){ return inputLocks.contains(Boolp::get); } public Eachable allPlans(){ return allPlans; } public boolean isUsingSchematic(){ return !selectPlans.isEmpty(); } public void spectate(Unit unit){ spectating = unit; camera.position.set(unit); } public void update(){ if(spectating != null && (!spectating.isValid() || spectating.team != player.team())){ spectating = null; } if(logicCutscene && !renderer.isCutscene()){ Core.camera.position.lerpDelta(logicCamPan, logicCamSpeed); }else{ logicCutsceneZoom = -1f; } itemDepositCooldown -= Time.delta / 60f; commandBuildings.removeAll(b -> !b.isValid()); if(!commandMode){ commandRect = false; } if(player.isBuilder()){ var playerPlans = player.unit().plans; if(player.unit() != lastUnit && playerPlans.size <= 1){ playerPlans.ensureCapacity(lastPlans.size); for(var plan : lastPlans){ playerPlans.addLast(plan); } } if(lastPlans.size != playerPlans.size || (lastPlans.size > 0 && playerPlans.size > 0 && lastPlans.first() != playerPlans.first())){ lastPlans.clear(); for(var plan : playerPlans){ lastPlans.addLast(plan); } } } lastUnit = player.unit(); playerPlanTree.clear(); if(!player.dead()){ player.unit().plans.each(playerPlanTree::insert); } player.typing = ui.chatfrag.shown(); if(player.dead()){ droppingItem = false; } if(player.isBuilder()){ player.unit().updateBuilding(isBuilding); } if(!player.dead() && player.shooting && !wasShooting && player.unit().hasWeapons() && state.rules.unitAmmo && !player.team().rules().infiniteAmmo && player.unit().ammo <= 0){ player.unit().type.weapons.first().noAmmoSound.at(player.unit()); } //you don't want selected blocks while locked, looks weird if(locked()){ block = null; } wasShooting = player.shooting; //only reset the controlled type and control a unit after the timer runs out //essentially, this means the client waits for ~1 second after controlling something before trying to control something else automatically if(!player.dead() && (recentRespawnTimer -= Time.delta / 70f) <= 0f && player.justSwitchFrom != player.unit()){ controlledType = player.unit().type; } if(controlledType != null && player.dead() && controlledType.playerControllable){ Unit unit = Units.closest(player.team(), player.x, player.y, u -> !u.isPlayer() && u.type == controlledType && u.playerControllable() && !u.dead); if(unit != null){ //only trying controlling once a second to prevent packet spam if(!net.client() || controlInterval.get(0, 70f)){ recentRespawnTimer = 1f; Call.unitControl(player, unit); } } } } public void checkUnit(){ if(controlledType != null && controlledType.playerControllable){ Unit unit = Units.closest(player.team(), player.x, player.y, u -> !u.isPlayer() && u.type == controlledType && !u.dead); if(unit == null && controlledType == UnitTypes.block){ unit = world.buildWorld(player.x, player.y) instanceof ControlBlock cont && cont.canControl() ? cont.unit() : null; } if(unit != null){ if(net.client()){ Call.unitControl(player, unit); }else{ unit.controller(player); } } } } public void tryPickupPayload(){ Unit unit = player.unit(); if(!(unit instanceof Payloadc pay)) return; Unit target = Units.closest(player.team(), pay.x(), pay.y(), unit.type.hitSize * 2f, u -> u.isAI() && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize)); if(target != null){ Call.requestUnitPayload(player, target); }else{ Building build = world.buildWorld(pay.x(), pay.y()); if(build != null && state.teams.canInteract(unit.team, build.team)){ Call.requestBuildPayload(player, build); } } } public void tryDropPayload(){ Unit unit = player.unit(); if(!(unit instanceof Payloadc)) return; Call.requestDropPayload(player, player.x, player.y); } public float getMouseX(){ return Core.input.mouseX(); } public float getMouseY(){ return Core.input.mouseY(); } public void buildPlacementUI(Table table){ } public void buildUI(Group group){ } public void updateState(){ if(state.isMenu()){ controlledType = null; logicCutscene = false; config.forceHide(); commandMode = commandRect = false; } } //TODO when shift is held? ctrl? public boolean multiUnitSelect(){ return false; } public void selectUnitsRect(){ if(commandMode && commandRect){ if(!tappedOne){ var units = selectedCommandUnits(commandRectX, commandRectY, input.mouseWorldX() - commandRectX, input.mouseWorldY() - commandRectY); if(multiUnitSelect()){ //tiny brain method of unique addition selectedUnits.removeAll(units); }else{ //nothing selected, clear units selectedUnits.clear(); } selectedUnits.addAll(units); Events.fire(Trigger.unitCommandChange); commandBuildings.clear(); } commandRect = false; } } public void selectTypedUnits(){ if(commandMode){ Unit unit = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY()); if(unit != null){ selectedUnits.clear(); camera.bounds(Tmp.r1); selectedUnits.addAll(selectedCommandUnits(Tmp.r1.x, Tmp.r1.y, Tmp.r1.width, Tmp.r1.height, u -> u.type == unit.type)); Events.fire(Trigger.unitCommandChange); } } } public void tapCommandUnit(){ if(commandMode){ Unit unit = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY()); Building build = world.buildWorld(input.mouseWorldX(), input.mouseWorldY()); if(unit != null){ if(!selectedUnits.contains(unit)){ selectedUnits.add(unit); }else{ selectedUnits.remove(unit); } commandBuildings.clear(); }else{ //deselect selectedUnits.clear(); if(build != null && build.team == player.team() && build.block.commandable){ if(commandBuildings.contains(build)){ commandBuildings.remove(build); }else{ commandBuildings.add(build); } }else{ commandBuildings.clear(); } } Events.fire(Trigger.unitCommandChange); } } public void commandTap(float screenX, float screenY){ commandTap(screenX, screenY, false); } public void commandTap(float screenX, float screenY, boolean queue){ if(commandMode){ //right click: move to position //move to location - TODO right click instead? Vec2 target = input.mouseWorld(screenX, screenY).cpy(); if(selectedUnits.size > 0){ Teamc attack = world.buildWorld(target.x, target.y); if(attack == null || attack.team() == player.team()){ attack = selectedEnemyUnit(target.x, target.y); } int[] ids = new int[selectedUnits.size]; for(int i = 0; i < ids.length; i++){ ids[i] = selectedUnits.get(i).id; } if(attack != null){ Events.fire(Trigger.unitCommandAttack); }else{ Events.fire(Trigger.unitCommandPosition); } int maxChunkSize = 200; if(ids.length > maxChunkSize){ for(int i = 0; i < ids.length; i += maxChunkSize){ int[] data = Arrays.copyOfRange(ids, i, Math.min(i + maxChunkSize, ids.length)); Call.commandUnits(player, data, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue, i + maxChunkSize >= ids.length); } }else{ Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue, true); } } if(commandBuildings.size > 0){ Call.commandBuilding(player, commandBuildings.mapInt(b -> b.pos()).toArray(), target); } } } public void drawCommand(Unit sel){ Drawf.poly(sel.x, sel.y, 6, sel.hitSize / unitSelectRadScl + Mathf.absin(4f, 1f), 0f, selectedUnits.contains(sel) ? Pal.remove : Pal.accent); } public void drawCommanded(){ Draw.draw(Layer.plans, () -> { drawCommanded(true); }); Draw.draw(Layer.groundUnit - 1, () -> { drawCommanded(false); }); } public void drawCommanded(boolean flying){ float lineLimit = 6.5f; Color color = Pal.accent; int sides = 6; float alpha = 0.5f; if(commandMode){ //happens sometimes selectedUnits.removeAll(u -> !u.isCommandable()); //draw command overlay UI for(Unit unit : selectedUnits){ CommandAI ai = unit.command(); Position lastPos = ai.attackTarget != null ? ai.attackTarget : ai.targetPos; if(flying && ai.attackTarget != null && ai.currentCommand().drawTarget){ Drawf.target(ai.attackTarget.getX(), ai.attackTarget.getY(), 6f, Pal.remove); } if(unit.isFlying() != flying) continue; //draw target line if(ai.targetPos != null && ai.currentCommand().drawTarget){ Position lineDest = ai.attackTarget != null ? ai.attackTarget : ai.targetPos; Drawf.limitLine(unit, lineDest, unit.hitSize / unitSelectRadScl + 1f, lineLimit, color.write(Tmp.c1).a(alpha)); if(ai.attackTarget == null){ Drawf.square(lineDest.getX(), lineDest.getY(), 3.5f, color.write(Tmp.c1).a(alpha)); if(ai.currentCommand() == UnitCommand.enterPayloadCommand){ var build = world.buildWorld(lineDest.getX(), lineDest.getY()); if(build != null && build.block.acceptsUnitPayloads && build.team == unit.team){ Drawf.selected(build, color); } } } } float rad = unit.hitSize / unitSelectRadScl + 1f; Fill.lightInner(unit.x, unit.y, sides, Math.max(0f, rad * 0.8f), rad, 0f, Tmp.c3.set(color).a(0f), Tmp.c2.set(color).a(0.7f) ); Lines.stroke(1f); Draw.color(color); Lines.poly(unit.x, unit.y, sides, rad + 0.5f); //uncomment for a dark border //Draw.color(Pal.gray); //Lines.poly(unit.x, unit.y, sides, rad + 1.5f); Draw.reset(); if(lastPos == null){ lastPos = unit; } //draw command queue if(ai.currentCommand().drawTarget && ai.commandQueue.size > 0){ for(var next : ai.commandQueue){ Drawf.limitLine(lastPos, next, lineLimit, lineLimit, color.write(Tmp.c1).a(alpha)); lastPos = next; if(next instanceof Vec2 vec){ Drawf.square(vec.x, vec.y, 3.5f, color.write(Tmp.c1).a(alpha)); }else{ Drawf.target(next.getX(), next.getY(), 6f, Pal.remove); } } } if(ai.targetPos != null && ai.currentCommand() == UnitCommand.loopPayloadCommand && unit instanceof Payloadc pay){ Draw.color(color, 0.4f + Mathf.absin(5f, 0.5f)); TextureRegion region = pay.hasPayload() ? Icon.download.getRegion() : Icon.upload.getRegion(); float offset = 11f; float size = 8f; Draw.rect(region, ai.targetPos.x, ai.targetPos.y + offset, size, size / region.ratio()); if(ai.commandQueue.size > 0){ region = !pay.hasPayload() ? Icon.download.getRegion() : Icon.upload.getRegion(); Draw.rect(region, ai.commandQueue.first().getX(), ai.commandQueue.first().getY() + offset, size, size / region.ratio()); } Draw.color(); } } if(flying){ for(var commandBuild : commandBuildings){ if(commandBuild != null){ Drawf.square(commandBuild.x, commandBuild.y, commandBuild.hitSize() / 1.4f + 1f); var cpos = commandBuild.getCommandPosition(); if(cpos != null){ Drawf.limitLine(commandBuild, cpos, commandBuild.hitSize() / 2f, lineLimit, color.write(Tmp.c1).a(alpha)); Drawf.square(cpos.x, cpos.y, 3.5f, color.write(Tmp.c1).a(alpha)); } } } } } Draw.reset(); } public void drawUnitSelection(){ if(commandRect && commandMode){ float x2 = input.mouseWorldX(), y2 = input.mouseWorldY(); var units = selectedCommandUnits(commandRectX, commandRectY, x2 - commandRectX, y2 - commandRectY); for(var unit : units){ drawCommand(unit); } Draw.color(Pal.accent, 0.3f); Fill.crect(commandRectX, commandRectY, x2 - commandRectX, y2 - commandRectY); } if(commandMode && !commandRect){ Unit sel = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY()); if(sel != null && !(!multiUnitSelect() && selectedUnits.size == 1 && selectedUnits.contains(sel))){ drawCommand(sel); } } } public void drawBottom(){ } public void drawTop(){ } public void drawOverSelect(){ } public void drawSelected(int x, int y, Block block, Color color){ Drawf.selected(x, y, block, color); } public void drawBreaking(BuildPlan plan){ if(plan.breaking){ drawBreaking(plan.x, plan.y); }else{ drawSelected(plan.x, plan.y, plan.block, Pal.remove); } } public void drawOverlapCheck(Block block, int cursorX, int cursorY, boolean valid){ if(!valid && state.rules.placeRangeCheck){ var blocker = Build.getEnemyOverlap(block, player.team(), cursorX, cursorY); if(blocker != null && blocker.wasVisible){ Drawf.selected(blocker, Pal.remove); Tmp.v1.set(cursorX, cursorY).scl(tilesize).add(block.offset, block.offset).sub(blocker).scl(-1f).nor(); Drawf.dashLineDst(Pal.remove, cursorX * tilesize + block.offset + Tmp.v1.x * block.size * tilesize/2f, cursorY * tilesize + block.offset + Tmp.v1.y * block.size * tilesize/2f, blocker.x + Tmp.v1.x * -blocker.block.size * tilesize/2f, blocker.y + Tmp.v1.y * -blocker.block.size * tilesize/2f ); } } } public boolean planMatches(BuildPlan plan){ Tile tile = world.tile(plan.x, plan.y); return tile != null && tile.build instanceof ConstructBuild cons && cons.current == plan.block; } public void drawBreaking(int x, int y){ Tile tile = world.tile(x, y); if(tile == null) return; Block block = tile.block(); drawSelected(x, y, block, Pal.remove); } public void useSchematic(Schematic schem){ selectPlans.addAll(schematics.toPlans(schem, player.tileX(), player.tileY())); } protected void showSchematicSave(){ if(lastSchematic == null) return; var last = lastSchematic; ui.showTextInput("@schematic.add", "@name", 1000, "", text -> { Schematic replacement = schematics.all().find(s -> s.name().equals(text)); if(replacement != null){ ui.showConfirm("@confirm", "@schematic.replace", () -> { schematics.overwrite(replacement, last); ui.showInfoFade("@schematic.saved"); ui.schematics.showInfo(replacement); }); }else{ last.tags.put("name", text); last.tags.put("description", ""); schematics.add(last); ui.showInfoFade("@schematic.saved"); ui.schematics.showInfo(last); Events.fire(new SchematicCreateEvent(last)); } }); } public void rotatePlans(Seq plans, int direction){ int ox = schemOriginX(), oy = schemOriginY(); plans.each(plan -> { if(plan.breaking) return; plan.pointConfig(p -> { int cx = p.x, cy = p.y; int lx = cx; if(direction >= 0){ cx = -cy; cy = lx; }else{ cx = cy; cy = -lx; } p.set(cx, cy); }); //rotate actual plan, centered on its multiblock position float wx = (plan.x - ox) * tilesize + plan.block.offset, wy = (plan.y - oy) * tilesize + plan.block.offset; float x = wx; if(direction >= 0){ wx = -wy; wy = x; }else{ wx = wy; wy = -x; } plan.x = World.toTile(wx - plan.block.offset) + ox; plan.y = World.toTile(wy - plan.block.offset) + oy; plan.rotation = plan.block.planRotation(Mathf.mod(plan.rotation + direction, 4)); }); } public void flipPlans(Seq plans, boolean x){ int origin = (x ? schemOriginX() : schemOriginY()) * tilesize; plans.each(plan -> { if(plan.breaking) return; float value = -((x ? plan.x : plan.y) * tilesize - origin + plan.block.offset) + origin; if(x){ plan.x = (int)((value - plan.block.offset) / tilesize); }else{ plan.y = (int)((value - plan.block.offset) / tilesize); } plan.pointConfig(p -> { int corigin = x ? plan.originalWidth/2 : plan.originalHeight/2; int nvalue = -(x ? p.x : p.y); if(x){ plan.originalX = -(plan.originalX - corigin) + corigin; p.x = nvalue; }else{ plan.originalY = -(plan.originalY - corigin) + corigin; p.y = nvalue; } }); //flip rotation plan.block.flipRotation(plan, x); }); } protected int schemOriginX(){ return rawTileX(); } protected int schemOriginY(){ return rawTileY(); } /** @return the selection plan that overlaps this position, or null. */ protected @Nullable BuildPlan getPlan(int x, int y){ return getPlan(x, y, 1, null); } /** Returns the selection plan that overlaps this position, or null. */ protected @Nullable BuildPlan getPlan(int x, int y, int size, BuildPlan skip){ float offset = ((size + 1) % 2) * tilesize / 2f; r2.setSize(tilesize * size); r2.setCenter(x * tilesize + offset, y * tilesize + offset); resultplan = null; Boolf test = plan -> { if(plan == skip) return false; Tile other = plan.tile(); if(other == null) return false; if(!plan.breaking){ r1.setSize(plan.block.size * tilesize); r1.setCenter(other.worldx() + plan.block.offset, other.worldy() + plan.block.offset); }else{ r1.setSize(other.block().size * tilesize); r1.setCenter(other.worldx() + other.block().offset, other.worldy() + other.block().offset); } return r2.overlaps(r1); }; for(var plan : player.unit().plans()){ if(test.get(plan)) return plan; } return selectPlans.find(test); } protected void drawBreakSelection(int x1, int y1, int x2, int y2, int maxLength){ drawBreakSelection(x1, y1, x2, y2, maxLength, true); } protected void drawBreakSelection(int x1, int y1, int x2, int y2, int maxLength, boolean useSelectPlans){ NormalizeDrawResult result = Placement.normalizeDrawArea(Blocks.air, x1, y1, x2, y2, false, maxLength, 1f); NormalizeResult dresult = Placement.normalizeArea(x1, y1, x2, y2, rotation, false, maxLength); for(int x = dresult.x; x <= dresult.x2; x++){ for(int y = dresult.y; y <= dresult.y2; y++){ Tile tile = world.tileBuilding(x, y); if(tile == null || !validBreak(tile.x, tile.y)) continue; drawBreaking(tile.x, tile.y); } } Tmp.r1.set(result.x, result.y, result.x2 - result.x, result.y2 - result.y); Draw.color(Pal.remove); Lines.stroke(1f); for(var plan : player.unit().plans()){ if(!plan.breaking && plan.bounds(Tmp.r2).overlaps(Tmp.r1)){ drawBreaking(plan); } } if(useSelectPlans){ for(var plan : selectPlans){ if(!plan.breaking && plan.bounds(Tmp.r2).overlaps(Tmp.r1)){ drawBreaking(plan); } } } for(BlockPlan plan : player.team().data().plans){ Block block = plan.block; if(block.bounds(plan.x, plan.y, Tmp.r2).overlaps(Tmp.r1)){ drawSelected(plan.x, plan.y, plan.block, Pal.remove); } } Lines.stroke(2f); Draw.color(Pal.removeBack); Lines.rect(result.x, result.y - 1, result.x2 - result.x, result.y2 - result.y); Draw.color(Pal.remove); Lines.rect(result.x, result.y, result.x2 - result.x, result.y2 - result.y); } protected void drawRebuildSelection(int x1, int y1, int x2, int y2){ drawSelection(x1, y1, x2, y2, 0, Pal.sapBulletBack, Pal.sapBullet); NormalizeDrawResult result = Placement.normalizeDrawArea(Blocks.air, x1, y1, x2, y2, false, 0, 1f); Tmp.r1.set(result.x, result.y, result.x2 - result.x, result.y2 - result.y); for(BlockPlan plan : player.team().data().plans){ Block block = plan.block; if(block.bounds(plan.x, plan.y, Tmp.r2).overlaps(Tmp.r1)){ drawSelected(plan.x, plan.y, plan.block, Pal.sapBullet); } } NormalizeResult dresult = Placement.normalizeArea(x1, y1, x2, y2, rotation, false, 999999999); intSet.clear(); for(int x = dresult.x; x <= dresult.x2; x++){ for(int y = dresult.y; y <= dresult.y2; y++){ Tile tile = world.tileBuilding(x, y); if(tile != null && intSet.add(tile.pos()) && canRepairDerelict(tile)){ drawSelected(tile.x, tile.y, tile.block(), Pal.sapBullet); } } } } protected void drawBreakSelection(int x1, int y1, int x2, int y2){ drawBreakSelection(x1, y1, x2, y2, maxLength); } protected void drawSelection(int x1, int y1, int x2, int y2, int maxLength){ drawSelection(x1, y1, x2, y2, maxLength, Pal.accentBack, Pal.accent); } protected void drawSelection(int x1, int y1, int x2, int y2, int maxLength, Color col1, Color col2){ NormalizeDrawResult result = Placement.normalizeDrawArea(Blocks.air, x1, y1, x2, y2, false, maxLength, 1f); Lines.stroke(2f); Draw.color(col1); Lines.rect(result.x, result.y - 1, result.x2 - result.x, result.y2 - result.y); Draw.color(col2); Lines.rect(result.x, result.y, result.x2 - result.x, result.y2 - result.y); } protected void flushSelectPlans(Seq plans){ for(BuildPlan plan : plans){ if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation, null, true)){ BuildPlan other = getPlan(plan.x, plan.y, plan.block.size, null); if(other == null){ selectPlans.add(plan.copy()); }else if(!other.breaking && other.x == plan.x && other.y == plan.y && other.block.size == plan.block.size){ selectPlans.remove(other); selectPlans.add(plan.copy()); } } } } protected void flushPlansReverse(Seq plans){ //reversed iteration. for(int i = plans.size - 1; i >= 0; i--){ var plan = plans.get(i); if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation, null, true)){ BuildPlan copy = plan.copy(); plan.block.onNewPlan(copy); player.unit().addBuild(copy, false); } } } protected void flushPlans(Seq plans){ for(var plan : plans){ if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation, null, true)){ BuildPlan copy = plan.copy(); plan.block.onNewPlan(copy); player.unit().addBuild(copy); } } } protected void drawOverPlan(BuildPlan plan){ drawOverPlan(plan, validPlace(plan.x, plan.y, plan.block, plan.rotation)); } protected void drawOverPlan(BuildPlan plan, boolean valid){ Draw.reset(); Draw.mixcol(!valid ? Pal.breakInvalid : Color.white, (!valid ? 0.4f : 0.24f) + Mathf.absin(Time.globalTime, 6f, 0.28f)); Draw.alpha(1f); plan.block.drawPlanConfigTop(plan, allSelectLines); Draw.reset(); } protected void drawPlan(BuildPlan plan){ drawPlan(plan, plan.cachedValid = validPlace(plan.x, plan.y, plan.block, plan.rotation)); } protected void drawPlan(BuildPlan plan, boolean valid){ plan.block.drawPlan(plan, allPlans(), valid); } /** Draws a placement icon for a specific block. */ protected void drawPlan(int x, int y, Block block, int rotation){ bplan.set(x, y, rotation, block); bplan.animScale = 1f; block.drawPlan(bplan, allPlans(), validPlace(x, y, block, rotation)); } /** Remove everything from the queue in a selection. */ protected void removeSelection(int x1, int y1, int x2, int y2){ removeSelection(x1, y1, x2, y2, false); } /** Remove everything from the queue in a selection. */ protected void removeSelection(int x1, int y1, int x2, int y2, int maxLength){ removeSelection(x1, y1, x2, y2, false, maxLength); } /** Remove everything from the queue in a selection. */ protected void removeSelection(int x1, int y1, int x2, int y2, boolean flush){ removeSelection(x1, y1, x2, y2, flush, maxLength); } /** Remove everything from the queue in a selection. */ protected void removeSelection(int x1, int y1, int x2, int y2, boolean flush, int maxLength){ NormalizeResult result = Placement.normalizeArea(x1, y1, x2, y2, rotation, false, maxLength); for(int x = 0; x <= Math.abs(result.x2 - result.x); x++){ for(int y = 0; y <= Math.abs(result.y2 - result.y); y++){ int wx = x1 + x * Mathf.sign(x2 - x1); int wy = y1 + y * Mathf.sign(y2 - y1); Tile tile = world.tileBuilding(wx, wy); if(tile == null) continue; if(!flush){ tryBreakBlock(wx, wy); }else if(validBreak(tile.x, tile.y) && !selectPlans.contains(r -> r.tile() != null && r.tile() == tile)){ selectPlans.add(new BuildPlan(tile.x, tile.y)); } } } //remove build plans Tmp.r1.set(result.x * tilesize, result.y * tilesize, (result.x2 - result.x) * tilesize, (result.y2 - result.y) * tilesize); Iterator it = player.unit().plans().iterator(); while(it.hasNext()){ var plan = it.next(); if(!plan.breaking && plan.bounds(Tmp.r2).overlaps(Tmp.r1)){ it.remove(); } } //don't remove plans on desktop, where flushing is false if(flush){ it = selectPlans.iterator(); while(it.hasNext()){ var plan = it.next(); if(!plan.breaking && plan.bounds(Tmp.r2).overlaps(Tmp.r1)){ it.remove(); } } } removed.clear(); //remove blocks to rebuild Iterator broken = player.team().data().plans.iterator(); while(broken.hasNext()){ BlockPlan plan = broken.next(); Block block = plan.block; if(block.bounds(plan.x, plan.y, Tmp.r2).overlaps(Tmp.r1)){ removed.add(Point2.pack(plan.x, plan.y)); plan.removed = true; broken.remove(); } } //TODO array may be too large? if(removed.size > 0 && net.active()){ Call.deletePlans(player, removed.toArray()); } } protected void updateLine(int x1, int y1, int x2, int y2){ linePlans.clear(); iterateLine(x1, y1, x2, y2, l -> { rotation = l.rotation; var plan = new BuildPlan(l.x, l.y, l.rotation, block, block.nextConfig()); plan.animScale = 1f; linePlans.add(plan); }); if(Core.settings.getBool("blockreplace")){ linePlans.each(plan -> { Block replace = plan.block.getReplacement(plan, linePlans); if(replace.unlockedNow()){ plan.block = replace; } }); block.handlePlacementLine(linePlans); } } protected void updateLine(int x1, int y1){ updateLine(x1, y1, tileX(getMouseX()), tileY(getMouseY())); } boolean checkConfigTap(){ return config.isShown() && config.getSelected().onConfigureTapped(input.mouseWorldX(), input.mouseWorldY()); } /** Handles tile tap events that are not platform specific. */ boolean tileTapped(@Nullable Building build){ planConfig.hide(); if(build == null){ inv.hide(); config.hideConfig(); commandBuildings.clear(); return false; } boolean consumed = false, showedInventory = false; //select building for commanding if(build.block.commandable && commandMode){ //TODO handled in tap. consumed = true; }else if(build.block.configurable && build.interactable(player.team())){ //check if tapped block is configurable consumed = true; if((!config.isShown() && build.shouldShowConfigure(player)) //if the config fragment is hidden, show //alternatively, the current selected block can 'agree' to switch config tiles || (config.isShown() && config.getSelected().onConfigureBuildTapped(build) && build.shouldShowConfigure(player))){ Sounds.click.at(build); config.showConfig(build); } //otherwise... }else if(!config.hasConfigMouse()){ //make sure a configuration fragment isn't on the cursor //then, if it's shown and the current block 'agrees' to hide, hide it. if(config.isShown() && config.getSelected().onConfigureBuildTapped(build)){ consumed = true; config.hideConfig(); } if(config.isShown()){ consumed = true; } } //call tapped event if(!consumed && build.interactable(player.team())){ build.tapped(); } //consume tap event if necessary if(build.interactable(player.team()) && build.block.consumesTap){ consumed = true; }else if(build.interactable(player.team()) && build.block.synthetic() && (!consumed || build.block.allowConfigInventory)){ if(build.block.hasItems && build.items.total() > 0){ inv.showFor(build); consumed = true; showedInventory = true; } } if(!showedInventory){ inv.hide(); } return consumed; } /** Tries to select the player to drop off items, returns true if successful. */ boolean tryTapPlayer(float x, float y){ if(canTapPlayer(x, y)){ droppingItem = true; return true; } return false; } boolean canTapPlayer(float x, float y){ return player.within(x, y, playerSelectRange) && player.unit().stack.amount > 0; } /** Tries to begin mining a tile, returns true if successful. */ boolean tryBeginMine(Tile tile){ if(!player.dead() && canMine(tile)){ player.unit().mineTile = tile; return true; } return false; } /** Tries to stop mining, returns true if mining was stopped. */ boolean tryStopMine(){ if(!player.dead() && player.unit().mining()){ player.unit().mineTile = null; return true; } return false; } boolean tryStopMine(Tile tile){ if(!player.dead() && player.unit().mineTile == tile){ player.unit().mineTile = null; return true; } return false; } boolean tryRepairDerelict(Tile selected){ if(selected != null && !state.rules.editor && player.team() != Team.derelict && selected.build != null && selected.build.block.unlockedNow() && selected.build.team == Team.derelict && Build.validPlace(selected.block(), player.team(), selected.build.tileX(), selected.build.tileY(), selected.build.rotation)){ player.unit().addBuild(new BuildPlan(selected.build.tileX(), selected.build.tileY(), selected.build.rotation, selected.block(), selected.build.config())); return true; } return false; } boolean canRepairDerelict(Tile tile){ return tile != null && tile.build != null && !state.rules.editor && player.team() != Team.derelict && tile.build.team == Team.derelict && tile.build.block.unlockedNowHost() && Build.validPlace(tile.block(), player.team(), tile.build.tileX(), tile.build.tileY(), tile.build.rotation); } boolean canMine(Tile tile){ return !Core.scene.hasMouse() && player.unit().validMine(tile) && player.unit().acceptsItem(player.unit().getMineResult(tile)) && !((!Core.settings.getBool("doubletapmine") && tile.floor().playerUnmineable) && tile.overlay().itemDrop == null); } /** Returns the tile at the specified MOUSE coordinates. */ Tile tileAt(float x, float y){ return world.tile(tileX(x), tileY(y)); } int rawTileX(){ return World.toTile(Core.input.mouseWorld().x); } int rawTileY(){ return World.toTile(Core.input.mouseWorld().y); } int tileX(float cursorX){ Vec2 vec = Core.input.mouseWorld(cursorX, 0); if(selectedBlock()){ vec.sub(block.offset, block.offset); } return World.toTile(vec.x); } int tileY(float cursorY){ Vec2 vec = Core.input.mouseWorld(0, cursorY); if(selectedBlock()){ vec.sub(block.offset, block.offset); } return World.toTile(vec.y); } /** Forces the camera to a position and enables panning on desktop. */ public void panCamera(Vec2 position){ if(!locked()){ camera.position.set(position); } } public boolean selectedBlock(){ return isPlacing(); } public boolean isPlacing(){ return block != null; } public boolean isBreaking(){ return false; } public boolean isRebuildSelecting(){ return input.keyDown(Binding.rebuild_select); } public float mouseAngle(float x, float y){ return Core.input.mouseWorld(getMouseX(), getMouseY()).sub(x, y).angle(); } public @Nullable Unit selectedUnit(){ Unit unit = Units.closest(player.team(), Core.input.mouseWorld().x, Core.input.mouseWorld().y, 40f, u -> u.isAI() && u.playerControllable()); if(unit != null){ unit.hitbox(Tmp.r1); Tmp.r1.grow(6f); if(Tmp.r1.contains(Core.input.mouseWorld())){ return unit; } } Building build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y); if(build instanceof ControlBlock cont && cont.canControl() && build.team == player.team() && cont.unit() != player.unit() && cont.unit().isAI()){ return cont.unit(); } return null; } public @Nullable Building selectedControlBuild(){ Building build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y); if(build != null && !player.dead() && build.canControlSelect(player.unit()) && build.team == player.team()){ return build; } return null; } public @Nullable Unit selectedCommandUnit(float x, float y){ var tree = player.team().data().tree(); tmpUnits.clear(); float rad = 4f; tree.intersect(x - rad/2f, y - rad/2f, rad, rad, tmpUnits); return tmpUnits.min(u -> u.isCommandable(), u -> u.dst(x, y) - u.hitSize/2f); } public @Nullable Unit selectedEnemyUnit(float x, float y){ tmpUnits.clear(); float rad = 4f; Seq data = state.teams.present; for(int i = 0; i < data.size; i++){ if(data.items[i].team != player.team()){ data.items[i].tree().intersect(x - rad / 2f, y - rad / 2f, rad, rad, tmpUnits); } } return tmpUnits.min(u -> !u.inFogTo(player.team()), u -> u.dst(x, y) - u.hitSize/2f); } public Seq selectedCommandUnits(float x, float y, float w, float h, Boolf predicate){ var tree = player.team().data().tree(); tmpUnits.clear(); float rad = 4f; tree.intersect(Tmp.r1.set(x - rad/2f, y - rad/2f, rad*2f + w, rad*2f + h).normalize(), tmpUnits); tmpUnits.removeAll(u -> !u.isCommandable() || !predicate.get(u)); return tmpUnits; } public Seq selectedCommandUnits(float x, float y, float w, float h){ return selectedCommandUnits(x, y, w, h, u -> true); } public void remove(){ Core.input.removeProcessor(this); group.remove(); if(Core.scene != null){ Table table = (Table)Core.scene.find("inputTable"); if(table != null){ table.clear(); } } if(detector != null){ Core.input.removeProcessor(detector); } if(uiGroup != null){ uiGroup.remove(); uiGroup = null; } } public void add(){ Core.input.getInputProcessors().remove(i -> i instanceof InputHandler || (i instanceof GestureDetector && ((GestureDetector)i).getListener() instanceof InputHandler)); Core.input.addProcessor(detector = new GestureDetector(20, 0.5f, 0.3f, 0.15f, this)); Core.input.addProcessor(this); if(Core.scene != null){ Table table = (Table)Core.scene.find("inputTable"); if(table != null){ table.clear(); buildPlacementUI(table); } uiGroup = new WidgetGroup(); uiGroup.touchable = Touchable.childrenOnly; uiGroup.setFillParent(true); ui.hudGroup.addChild(uiGroup); uiGroup.toBack(); buildUI(uiGroup); group.setFillParent(true); Vars.ui.hudGroup.addChildBefore(Core.scene.find("overlaymarker"), group); inv.build(group); config.build(group); planConfig.build(group); } } public boolean canShoot(){ return block == null && !onConfigurable() && !isDroppingItem() && !player.unit().activelyBuilding() && !(player.unit() instanceof Mechc && player.unit().isFlying()) && !player.unit().mining() && !commandMode; } public boolean onConfigurable(){ return false; } public boolean isDroppingItem(){ return droppingItem; } public boolean canDropItem(){ return droppingItem && !canTapPlayer(Core.input.mouseWorldX(), Core.input.mouseWorldY()); } public void tryDropItems(@Nullable Building build, float x, float y){ if(!droppingItem || player.unit().stack.amount <= 0 || canTapPlayer(x, y) || state.isPaused() ){ droppingItem = false; return; } droppingItem = false; ItemStack stack = player.unit().stack; if(build != null && build.acceptStack(stack.item, stack.amount, player.unit()) > 0 && build.interactable(player.team()) && build.block.hasItems && player.unit().stack().amount > 0 && build.interactable(player.team())){ if(!(state.rules.onlyDepositCore && !(build instanceof CoreBuild)) && itemDepositCooldown <= 0f){ Call.transferInventory(player, build); itemDepositCooldown = state.rules.itemDepositCooldown; } }else{ Call.dropItem(player.angleTo(x, y)); } } public void rebuildArea(int x1, int y1, int x2, int y2){ NormalizeResult result = Placement.normalizeArea(x1, y1, x2, y2, rotation, false, 999999999); Tmp.r1.set(result.x * tilesize, result.y * tilesize, (result.x2 - result.x) * tilesize, (result.y2 - result.y) * tilesize); Iterator broken = player.team().data().plans.iterator(); while(broken.hasNext()){ BlockPlan plan = broken.next(); Block block = plan.block; if(block.bounds(plan.x, plan.y, Tmp.r2).overlaps(Tmp.r1)){ player.unit().addBuild(new BuildPlan(plan.x, plan.y, plan.rotation, plan.block, plan.config)); } } intSet.clear(); for(int x = result.x; x <= result.x2; x++){ for(int y = result.y; y <= result.y2; y++){ Tile tile = world.tileBuilding(x, y); if(tile != null && tile.build != null && intSet.add(tile.pos())){ tryRepairDerelict(tile); } } } } public void tryBreakBlock(int x, int y){ if(validBreak(x, y)){ breakBlock(x, y); } } public boolean validPlace(int x, int y, Block type, int rotation){ return validPlace(x, y, type, rotation, null); } public boolean validPlace(int x, int y, Block type, int rotation, @Nullable BuildPlan ignore){ return validPlace(x, y, type, rotation, ignore, false); } public boolean validPlace(int x, int y, Block type, int rotation, @Nullable BuildPlan ignore, boolean ignoreUnits){ if(player.isBuilder() && player.unit().plans.size > 0){ Tmp.r1.setCentered(x * tilesize + type.offset, y * tilesize + type.offset, type.size * tilesize); plansOut.clear(); playerPlanTree.intersect(Tmp.r1, plansOut); for(int i = 0; i < plansOut.size; i++){ var plan = plansOut.items[i]; if(plan != ignore && !plan.breaking && plan.block.bounds(plan.x, plan.y, Tmp.r1).overlaps(type.bounds(x, y, Tmp.r2)) && !(type.canReplace(plan.block) && Tmp.r1.equals(Tmp.r2))){ return false; } } } return ignoreUnits ? Build.validPlaceIgnoreUnits(type, player.team(), x, y, rotation, true, true) : Build.validPlace(type, player.team(), x, y, rotation); } public boolean validBreak(int x, int y){ return Build.validBreak(player.team(), x, y); } public void breakBlock(int x, int y){ if(!player.isBuilder()) return; Tile tile = world.tile(x, y); if(tile != null && tile.build != null) tile = tile.build.tile; player.unit().addBuild(new BuildPlan(tile.x, tile.y)); } public void drawArrow(Block block, int x, int y, int rotation){ drawArrow(block, x, y, rotation, validPlace(x, y, block, rotation)); } public void drawArrow(Block block, int x, int y, int rotation, boolean valid){ float trns = (block.size / 2) * tilesize; int dx = Geometry.d4(rotation).x, dy = Geometry.d4(rotation).y; float offsetx = x * tilesize + block.offset + dx*trns; float offsety = y * tilesize + block.offset + dy*trns; Draw.color(!valid ? Pal.removeBack : Pal.accentBack); TextureRegion regionArrow = Core.atlas.find("place-arrow"); Draw.rect(regionArrow, offsetx, offsety - 1, regionArrow.width * regionArrow.scl(), regionArrow.height * regionArrow.scl(), rotation * 90 - 90); Draw.color(!valid ? Pal.remove : Pal.accent); Draw.rect(regionArrow, offsetx, offsety, regionArrow.width * regionArrow.scl(), regionArrow.height * regionArrow.scl(), rotation * 90 - 90); } void iterateLine(int startX, int startY, int endX, int endY, Cons cons){ Seq points; boolean diagonal = Core.input.keyDown(Binding.diagonal_placement); if(Core.settings.getBool("swapdiagonal") && mobile){ diagonal = !diagonal; } if(block != null && block.swapDiagonalPlacement){ diagonal = !diagonal; } int endRotation = -1; var start = world.build(startX, startY); var end = world.build(endX, endY); if(diagonal && (block == null || block.allowDiagonal)){ if(block != null && start instanceof ChainedBuilding && end instanceof ChainedBuilding && block.canReplace(end.block) && block.canReplace(start.block)){ points = Placement.upgradeLine(startX, startY, endX, endY); }else{ points = Placement.pathfindLine(block != null && block.conveyorPlacement, startX, startY, endX, endY); } }else if(block != null && block.allowRectanglePlacement){ points = Placement.normalizeRectangle(startX, startY, endX, endY, block.size); }else{ points = Placement.normalizeLine(startX, startY, endX, endY); } if(points.size > 1 && end instanceof ChainedBuilding){ Point2 secondToLast = points.get(points.size - 2); if(!(world.build(secondToLast.x, secondToLast.y) instanceof ChainedBuilding)){ endRotation = end.rotation; } } if(block != null){ block.changePlacementPath(points, rotation, diagonal); } float angle = Angles.angle(startX, startY, endX, endY); int baseRotation = rotation; if(!overrideLineRotation || diagonal){ baseRotation = (startX == endX && startY == endY) ? rotation : ((int)((angle + 45) / 90f)) % 4; } Tmp.r3.set(-1, -1, 0, 0); for(int i = 0; i < points.size; i++){ Point2 point = points.get(i); if(block != null && Tmp.r2.setSize(block.size * tilesize).setCenter(point.x * tilesize + block.offset, point.y * tilesize + block.offset).overlaps(Tmp.r3)){ continue; } Point2 next = i == points.size - 1 ? null : points.get(i + 1); line.x = point.x; line.y = point.y; if(!overrideLineRotation || diagonal){ int result = baseRotation; if(next != null){ result = Tile.relativeTo(point.x, point.y, next.x, next.y); }else if(endRotation != -1){ result = endRotation; }else if(block.conveyorPlacement && i > 0){ Point2 prev = points.get(i - 1); result = Tile.relativeTo(prev.x, prev.y, point.x, point.y); } if(result != -1){ line.rotation = result; } }else{ line.rotation = rotation; } line.last = next == null; cons.get(line); Tmp.r3.setSize(block.size * tilesize).setCenter(point.x * tilesize + block.offset, point.y * tilesize + block.offset); } } static class PlaceLine{ public int x, y, rotation; public boolean last; } }