diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index 5a329a668a..6fbef58289 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -2005,7 +2005,7 @@ public class Blocks implements ContentList{ requirements(Category.units, with(Items.silicon, 70, Items.thorium, 60, Items.plastanium, 60)); size = 2; length = 6f; - repairSpeed = 5f; + repairSpeed = 4f; repairRadius = 140f; powerUse = 5f; beamWidth = 1.1f; diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index d54f7cf46f..d87e78b781 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -1936,7 +1936,6 @@ public class UnitTypes implements ContentList{ ejectEffect = Fx.none; bullet = new FlakBulletType(2.5f, 25){{ sprite = "missile-large"; - collides = false; //for targeting collidesGround = collidesAir = true; explodeRange = 40f; @@ -1950,9 +1949,9 @@ public class UnitTypes implements ContentList{ lightColor = Pal.heal; splashDamageRadius = 30f; - splashDamage = 28f; + splashDamage = 25f; - lifetime = 90f; + lifetime = 80f; backColor = Pal.heal; frontColor = Color.white; @@ -1982,11 +1981,11 @@ public class UnitTypes implements ContentList{ fragBullets = 7; fragVelocityMin = 0.3f; - fragBullet = new MissileBulletType(3.9f, 12){{ + fragBullet = new MissileBulletType(3.9f, 11){{ homingPower = 0.2f; weaveMag = 4; weaveScale = 4; - lifetime = 70f; + lifetime = 60f; shootEffect = Fx.shootHeal; smokeEffect = Fx.hitLaser; splashDamage = 13f; @@ -2112,8 +2111,8 @@ public class UnitTypes implements ContentList{ bullet = new ContinuousLaserBulletType(){{ maxRange = 90f; - damage = 25f; - length = 90f; + damage = 26f; + length = 95f; hitEffect = Fx.hitMeltHeal; drawSize = 200f; lifetime = 155f; @@ -2160,7 +2159,7 @@ public class UnitTypes implements ContentList{ timeIncrease = 3f; timeDuration = 60f * 20f; powerDamageScl = 3f; - damage = 40; + damage = 50; hitColor = lightColor = Pal.heal; lightRadius = 70f; clipSize = 250f; @@ -2176,7 +2175,7 @@ public class UnitTypes implements ContentList{ trailWidth = 6f; trailColor = Pal.heal; trailInterval = 3f; - splashDamage = 40f; + splashDamage = 60f; splashDamageRadius = rad; hitShake = 4f; trailRotation = true; diff --git a/core/src/mindustry/entities/comp/PlayerComp.java b/core/src/mindustry/entities/comp/PlayerComp.java index 0323906021..63c3f5c6a2 100644 --- a/core/src/mindustry/entities/comp/PlayerComp.java +++ b/core/src/mindustry/entities/comp/PlayerComp.java @@ -45,6 +45,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra transient String lastText = ""; transient float textFadeTime; transient private Unit lastReadUnit = Nulls.unit; + transient @Nullable Unit justSwitchFrom, justSwitchTo; public boolean isBuilder(){ return unit.canBuild(); @@ -100,6 +101,16 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra @Override public void afterSync(){ + //fix rubberbanding: + //when the player recs a unit that they JUST transitioned away from, use the new unit instead + //reason: we know the server is lying here, essentially skip the unit snapshot because we know the client's information is more recent + if(isLocal() && unit == justSwitchFrom && justSwitchFrom != null && justSwitchTo != null){ + unit = justSwitchTo; + }else{ + justSwitchFrom = null; + justSwitchTo = null; + } + //simulate a unit change after sync Unit set = unit; unit = lastReadUnit; @@ -149,6 +160,13 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra } + public void checkSpawn(){ + CoreBuild core = bestCore(); + if(core != null){ + core.requestSpawn(self()); + } + } + @Override public void remove(){ //clear unit upon removal @@ -171,6 +189,11 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra } public void unit(Unit unit){ + //refuse to switch when the unit was just transitioned from + if(isLocal() && unit == justSwitchFrom && justSwitchFrom != null && justSwitchTo != null){ + return; + } + if(unit == null) throw new IllegalArgumentException("Unit cannot be null. Use clearUnit() instead."); if(this.unit == unit) return; diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 8b5346fce0..6506db2d34 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -229,8 +229,10 @@ public class DesktopInput extends InputHandler{ if(on != null){ Call.unitControl(player, on); shouldShoot = false; + recentRespawnTimer = 1f; }else if(build != null){ Call.buildingControlSelect(player, build); + recentRespawnTimer = 1f; } } } @@ -239,8 +241,9 @@ public class DesktopInput extends InputHandler{ updateMovement(player.unit()); if(Core.input.keyTap(Binding.respawn)){ - Call.unitClear(player); controlledType = null; + recentRespawnTimer = 1f; + Call.unitClear(player); } } diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index ebabd2523f..991f48124b 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -58,6 +58,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ 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; @@ -375,15 +376,27 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ throw new ValidateException(player, "Player cannot control a unit."); } + //TODO problem: + //1. server send snapshot + //2. client requests to control unit, becomes unit locally + //3. snapshot arrives, client now thinks they're in the old unit (!!!) + //4. server gets packet that player is in the right unit + //5. server sends snapshot + //6. client gets snapshot, realizes that they are actually in the unit they selected + //7. client gets switched to new unit -> rubberbanding (!!!) + //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){ - if(!net.client()){ - player.unit(unit); + if(net.client()){ + player.justSwitchFrom = player.unit(); + player.justSwitchTo = unit; } + player.unit(unit); + 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); @@ -393,12 +406,14 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ Events.fire(new UnitControlEvent(player, unit)); } - @Remote(targets = Loc.both, called = Loc.both, forward = true) + @Remote(targets = Loc.both, called = Loc.server, forward = true) public static void unitClear(Player player){ if(player == null) return; + //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 } @@ -419,7 +434,6 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ player.unit().commandNearby(new CircleFormation()); Fx.commandSend.at(player, player.unit().type.commandRadius); } - } public Eachable allRequests(){ @@ -451,7 +465,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ wasShooting = player.shooting; - if(!player.dead()){ + //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 / 60f) <= 0f){ controlledType = player.unit().type; } @@ -461,6 +477,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ 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); } } diff --git a/core/src/mindustry/input/MobileInput.java b/core/src/mindustry/input/MobileInput.java index 792d242851..55851e5c1c 100644 --- a/core/src/mindustry/input/MobileInput.java +++ b/core/src/mindustry/input/MobileInput.java @@ -614,8 +614,10 @@ public class MobileInput extends InputHandler implements GestureListener{ //control a unit/block detected on first tap of double-tap if(unitTapped != null){ Call.unitControl(player, unitTapped); + recentRespawnTimer = 1f; }else if(buildingTapped != null){ Call.buildingControlSelect(player, buildingTapped); + recentRespawnTimer = 1f; }else if(!tryBeginMine(cursor)){ tileTapped(linked.build); } diff --git a/core/src/mindustry/ui/fragments/HudFragment.java b/core/src/mindustry/ui/fragments/HudFragment.java index 704cf0ec61..2f4061c10a 100644 --- a/core/src/mindustry/ui/fragments/HudFragment.java +++ b/core/src/mindustry/ui/fragments/HudFragment.java @@ -721,6 +721,7 @@ public class HudFragment extends Fragment{ t.clicked(() -> { if(!player.dead() && mobile){ Call.unitClear(player); + control.input.recentRespawnTimer = 1f; control.input.controlledType = null; } });