Mindustry/core/src/mindustry/input/InputHandler.java
2025-04-04 11:47:35 -04:00

2186 lines
80 KiB
Java

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<Vec2, Seq<Unit>> 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<Unit> 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<Boolp> 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<BuildPlan> linePlans = new Seq<>();
public Seq<BuildPlan> selectPlans = new Seq<>(BuildPlan.class);
public Queue<BuildPlan> lastPlans = new Queue<>();
public @Nullable Unit lastUnit;
public @Nullable Unit spectating;
//for RTS controls
public Seq<Unit> selectedUnits = new Seq<>();
public Seq<Building> 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<BuildPlan> plansOut = new Seq<>(BuildPlan.class);
private QuadTree<BuildPlan> 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<BuildPlan> allPlans = cons -> {
if(!player.dead()){
player.unit().plans().each(cons);
}
selectPlans.each(cons);
linePlans.each(cons);
};
private final Eachable<BuildPlan> 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<Unit> 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<BuildPlan> 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<BuildPlan> 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<BuildPlan> 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<BuildPlan> 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<BuildPlan> 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<BuildPlan> 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<BuildPlan> 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<BuildPlan> 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<BlockPlan> 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<TeamData> 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<Unit> selectedCommandUnits(float x, float y, float w, float h, Boolf<Unit> 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<Unit> 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<BlockPlan> 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<PlaceLine> cons){
Seq<Point2> 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;
}
}