hedgewars/uGearsUtils.pas
branchui-scaling
changeset 15283 c4fd2813b127
parent 15227 263b9850c16d
child 15280 b12f63054c94
--- a/hedgewars/uGearsUtils.pas	Wed May 16 18:22:28 2018 +0200
+++ b/hedgewars/uGearsUtils.pas	Wed Jul 31 23:14:27 2019 +0200
@@ -25,14 +25,16 @@
 procedure doMakeExplosion(X, Y, Radius: LongInt; AttackingHog: PHedgehog; Mask: Longword); inline;
 procedure doMakeExplosion(X, Y, Radius: LongInt; AttackingHog: PHedgehog; Mask: Longword; const Tint: LongWord);
 procedure AddSplashForGear(Gear: PGear; justSkipping: boolean);
+procedure AddBounceEffectForGear(Gear: PGear; imageScale: Single);
 procedure AddBounceEffectForGear(Gear: PGear);
 
 function  ModifyDamage(dmg: Longword; Gear: PGear): Longword;
 procedure ApplyDamage(Gear: PGear; AttackerHog: PHedgehog; Damage: Longword; Source: TDamageSource);
 procedure spawnHealthTagForHH(HHGear: PGear; dmg: Longword);
-procedure HHHurt(Hedgehog: PHedgehog; Source: TDamageSource);
-procedure HHHeal(Hedgehog: PHedgehog; healthBoost: Longword; showMessage: boolean; vgTint: Longword);
-procedure HHHeal(Hedgehog: PHedgehog; healthBoost: Longword; showMessage: boolean);
+procedure HHHurt(Hedgehog: PHedgehog; Source: TDamageSource; Damage: Longword);
+procedure HHHeal(Hedgehog: PHedgehog; healthBoost: LongInt; showMessage: boolean; vgTint: Longword);
+procedure HHHeal(Hedgehog: PHedgehog; healthBoost: LongInt; showMessage: boolean);
+function IncHogHealth(Hedgehog: PHedgehog; healthBoost: LongInt): LongInt;
 procedure CheckHHDamage(Gear: PGear);
 procedure CalcRotationDirAngle(Gear: PGear);
 procedure ResurrectHedgehog(var gear: PGear);
@@ -40,15 +42,18 @@
 procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt); inline;
 procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt; skipProximity: boolean);
 
+function  CheckGearNear(Kind: TGearType; X, Y: hwFloat; rX, rY: LongInt): PGear;
 function  CheckGearNear(Gear: PGear; Kind: TGearType; rX, rY: LongInt): PGear;
 function  CheckGearDrowning(var Gear: PGear): boolean;
 procedure CheckCollision(Gear: PGear); inline;
 procedure CheckCollisionWithLand(Gear: PGear); inline;
 
 procedure AmmoShove(Ammo: PGear; Damage, Power: LongInt);
+procedure AmmoShoveCache(Ammo: PGear; Damage, Power: LongInt);
 procedure AmmoShoveLine(Ammo: PGear; Damage, Power: LongInt; oX, oY, tX, tY: hwFloat);
 function  GearsNear(X, Y: hwFloat; Kind: TGearType; r: LongInt): PGearArrayS;
-procedure SpawnBoxOfSmth;
+function  SpawnBoxOfSmth: PGear;
+procedure PlayBoxSpawnTaunt(Gear: PGear);
 procedure ShotgunShot(Gear: PGear);
 function  CanUseTardis(HHGear: PGear): boolean;
 
@@ -60,7 +65,9 @@
 function  GetUtility(Hedgehog: PHedgehog): TAmmoType;
 
 function WorldWrap(var Gear: PGear): boolean;
+function HomingWrap(var Gear: PGear): boolean;
 
+function IsHogFacingLeft(Gear: PGear): boolean;
 function IsHogLocal(HH: PHedgehog): boolean;
 
 
@@ -113,11 +120,6 @@
     end;
 if (Mask and EXPLAutoSound) <> 0 then PlaySound(sndExplosion);
 
-(*if (Mask and EXPLAllDamageInRadius) = 0 then
-    dmgRadius:= Radius shl 1
-else
-    dmgRadius:= Radius;
-dmgBase:= dmgRadius + cHHRadius div 2;*)
 dmgBase:= Radius shl 1 + cHHRadius div 2;
 
 // we might have to run twice if weWrap is enabled
@@ -131,8 +133,6 @@
 while Gear <> nil do
     begin
     dmg:= 0;
-    //dmg:= dmgRadius  + cHHRadius div 2 - hwRound(Distance(Gear^.X - int2hwFloat(X), Gear^.Y - int2hwFloat(Y)));
-    //if (dmg > 1) and
     if (Gear^.State and gstNoDamage) = 0 then
         begin
         case Gear^.Kind of
@@ -142,15 +142,13 @@
                 gtMelonPiece,
                 gtGrenade,
                 gtClusterBomb,
-            //    gtCluster, too game breaking I think
                 gtSMine,
                 gtAirMine,
                 gtCase,
                 gtTarget,
                 gtFlame,
                 gtKnife,
-                gtExplosives: begin //,
-                //gtStructure: begin
+                gtExplosives: begin
 // Run the calcs only once we know we have a type that will need damage
                         tdX:= Gear^.X-fX;
                         tdY:= Gear^.Y-fY;
@@ -176,9 +174,18 @@
                                 Gear^.State:= (Gear^.State or gstMoving) and (not gstLoser);
                                 if Gear^.Kind = gtKnife then Gear^.State:= Gear^.State and (not gstCollision);
                                 if (Gear^.Kind = gtHedgehog) and (Gear^.Hedgehog^.Effects[heInvulnerable] = 0) then
-                                    Gear^.State:= (Gear^.State or gstMoving) and (not gstWinner);
+                                    begin
+                                    Gear^.State:= (Gear^.State or gstMoving) and (not (gstHHJumping or gstHHHJump));
+                                    if (not GameOver) then
+                                        Gear^.State:= (Gear^.State and (not gstWinner));
+                                    end;
                                 Gear^.Active:= true;
-                                if Gear^.Kind <> gtFlame then FollowGear:= Gear
+                                if Gear^.Kind <> gtFlame then FollowGear:= Gear;
+                                if Gear^.Kind = gtAirMine then
+                                    begin
+                                    Gear^.Tag:= 1;
+                                    Gear^.FlightTime:= 5000;
+                                    end
                                 end;
                             if ((Mask and EXPLPoisoned) <> 0) and (Gear^.Kind = gtHedgehog) and
                                 (Gear^.Hedgehog^.Effects[heInvulnerable] = 0) and (Gear^.Hedgehog^.Effects[heFrozen] = 0) and
@@ -187,7 +194,7 @@
                                     if Gear^.Hedgehog^.Effects[hePoisoned] = 0 then
                                         begin
                                         s:= ansistring(Gear^.Hedgehog^.Name);
-                                        AddCaption(FormatA(GetEventString(eidPoisoned), s), cWhiteColor, capgrpMessage);
+                                        AddCaption(FormatA(GetEventString(eidPoisoned), s), capcolDefault, capgrpMessage);
                                         uStats.HedgehogPoisoned(Gear, AttackingHog)
                                         end;
                                     Gear^.Hedgehog^.Effects[hePoisoned] := 5;
@@ -215,7 +222,7 @@
     end;
 
 if (Mask and EXPLDontDraw) = 0 then
-    if (GameFlags and gfSolidLand) = 0 then
+    if ((GameFlags and gfSolidLand) = 0) or ((Mask and EXPLForceDraw) <> 0) then
         begin
         cnt:= DrawExplosion(X, Y, Radius) div 1608; // approx 2 16x16 circles to erase per chunk
         if (cnt > 0) and (SpritesData[sprChunk].Texture <> nil) then
@@ -230,12 +237,12 @@
         break;
 
     // Radius + 5 because that's the actual radius the explosion changes graphically
-    if X + (Radius + 5) > LongInt(rightX) then
+    if X + (Radius + 5) > rightX then
         begin
         dec(X, playWidth);
         wrap:= true;
         end
-    else if X - (Radius + 5) < LongInt(leftX) then
+    else if X - (Radius + 5) < leftX then
         begin
         inc(X, playWidth);
         wrap:= true;
@@ -276,8 +283,16 @@
         Gear^.LastDamage := AttackerHog;
 
         Gear^.Hedgehog^.Team^.Clan^.Flawless:= false;
-        HHHurt(Gear^.Hedgehog, Source);
-        AddDamageTag(hwRound(Gear^.X), hwRound(Gear^.Y), Damage, Gear^.Hedgehog^.Team^.Clan^.Color);
+
+        if (Gear^.State and gstHHDeath) <> 0 then
+            // If hog took damage while dying, explode hog instantly (see doStepHedgehogDead)
+            Gear^.Timer:= 1
+        else
+            begin
+            HHHurt(Gear^.Hedgehog, Source, Damage);
+            AddDamageTag(hwRound(Gear^.X), hwRound(Gear^.Y), Damage, Gear^.Hedgehog^.Team^.Clan^.Color);
+            end;
+
         tmpDmg:= min(Damage, max(0,Gear^.Health-Gear^.Damage));
         if (Gear <> CurrentHedgehog^.Gear) and (CurrentHedgehog^.Gear <> nil) and (tmpDmg >= 1) then
             begin
@@ -288,7 +303,7 @@
                     begin
                     // was considering pulsing on attack, Tiy thinks it should be permanent while in play
                     //CurrentHedgehog^.Gear^.State:= CurrentHedgehog^.Gear^.State or gstVampiric;
-                    inc(CurrentHedgehog^.Gear^.Health,vampDmg);
+                    vampDmg:= IncHogHealth(CurrentHedgehog, vampDmg);
                     RenderHealth(CurrentHedgehog^);
                     RecountTeamHealth(CurrentHedgehog^.Team);
                     HHHeal(CurrentHedgehog, vampDmg, true, $FF0000FF);
@@ -305,28 +320,24 @@
 
         uStats.HedgehogDamaged(Gear, AttackerHog, Damage, false);
 
-	if AprilOne and (Gear^.Hedgehog^.Hat = 'fr_tomato') and (Damage > 2) then
-	    for i := 0 to random(min(Damage,20))+5 do
-		begin
-		vg:= AddVisualGear(hwRound(Gear^.X), hwRound(Gear^.Y), vgtStraightShot);
-		if vg <> nil then
-		    with vg^ do
-			begin
-			dx:= 0.001 * (random(100)+10);
-			dy:= 0.001 * (random(100)+10);
-			tdy:= -cGravityf;
-			if random(2) = 0 then
-			    dx := -dx;
-			//if random(2) = 0 then
-			//    dy := -dy;
-			FrameTicks:= random(500) + 1000;
-			State:= ord(sprBubbles);
-			//Tint:= $bd2f03ff
-			Tint:= $ff0000ff
-			end
-	end
+    if AprilOne and (Gear^.Hedgehog^.Hat = 'fr_tomato') and (Damage > 2) then
+        for i := 0 to random(min(Damage,20))+5 do
+        begin
+        vg:= AddVisualGear(hwRound(Gear^.X), hwRound(Gear^.Y), vgtStraightShot);
+        if vg <> nil then
+            with vg^ do
+            begin
+            dx:= 0.001 * (random(100)+10);
+            dy:= 0.001 * (random(100)+10);
+            tdy:= -cGravityf;
+            if random(2) = 0 then
+                dx := -dx;
+            FrameTicks:= random(500) + 1000;
+            State:= ord(sprBubbles);
+            Tint:= $ff0000ff
+            end
+    end
     end else
-    //else if Gear^.Kind <> gtStructure then // not gtHedgehog nor gtStructure
         Gear^.Hedgehog:= AttackerHog;
     inc(Gear^.Damage, Damage);
 
@@ -344,11 +355,17 @@
 end;
 
 // Play effects for hurt hedgehog
-procedure HHHurt(Hedgehog: PHedgehog; Source: TDamageSource);
+procedure HHHurt(Hedgehog: PHedgehog; Source: TDamageSource; Damage: Longword);
 begin
 if Hedgehog^.Effects[heFrozen] <> 0 then exit;
 
-if (Source = dsFall) or (Source = dsExplosion) then
+if (Damage >= ouchDmg) and (OuchTauntTimer = 0) and ((Source = dsFall) or (Source = dsBullet) or (Source = dsShove) or (Source = dsHammer)) then
+    begin
+    PlaySoundV(sndOuch, Hedgehog^.Team^.voicepack);
+    // Prevent sndOuch from being played too often in short time
+    OuchTauntTimer:= 1250;
+    end
+else if (Source = dsFall) or (Source = dsExplosion) then
     case random(3) of
         0: PlaySoundV(sndOoff1, Hedgehog^.Team^.voicepack);
         1: PlaySoundV(sndOoff2, Hedgehog^.Team^.voicepack);
@@ -373,9 +390,9 @@
 Hedgehog: Hedgehog which gets the health boost
 healthBoost: Amount of added health added
 showMessage: Whether to show announcer message
-vgTint: Tint of heal particle
+vgTint: Tint of heal particle (if 0, don't render particles)
 -}
-procedure HHHeal(Hedgehog: PHedgehog; healthBoost: Longword; showMessage: boolean; vgTint: Longword);
+procedure HHHeal(Hedgehog: PHedgehog; healthBoost: LongInt; showMessage: boolean; vgTint: Longword);
 var i: LongInt;
     vg: PVisualGear;
     s: ansistring;
@@ -391,25 +408,45 @@
 
     i:= 0;
     // One particle for every 5 HP. Max. 200 particles
-    while (i < healthBoost) and (i < 1000) do
-        begin
-        vg:= AddVisualGear(hwRound(Hedgehog^.Gear^.X), hwRound(Hedgehog^.Gear^.Y), vgtStraightShot);
-        if vg <> nil then
-            with vg^ do
-                begin
-                Tint:= vgTint;
-                State:= ord(sprHealth)
-                end;
-        inc(i, 5)
-        end;
+    if (vgTint <> 0) then
+        while (i < healthBoost) and (i < 1000) do
+            begin
+            vg:= AddVisualGear(hwRound(Hedgehog^.Gear^.X), hwRound(Hedgehog^.Gear^.Y), vgtStraightShot);
+            if vg <> nil then
+                with vg^ do
+                    begin
+                    Tint:= vgTint;
+                    State:= ord(sprHealth)
+                    end;
+            inc(i, 5)
+            end;
 end;
 
 // Shorthand for the same above, but with tint implied
-procedure HHHeal(Hedgehog: PHedgehog; healthBoost: Longword; showMessage: boolean);
+procedure HHHeal(Hedgehog: PHedgehog; healthBoost: LongInt; showMessage: boolean);
 begin
     HHHeal(Hedgehog, healthBoost, showMessage, $00FF00FF);
 end;
 
+// Increase hog health by healthBoost (at least 1).
+// Resulting health is capped at cMaxHogHealth.
+// Returns actual amount healed.
+function IncHogHealth(Hedgehog: PHedgehog; healthBoost: LongInt): LongInt;
+var oldHealth: LongInt;
+begin
+   if healthBoost < 1 then
+       begin
+       IncHogHealth:= 0;
+       exit;
+       end;
+   oldHealth:= Hedgehog^.Gear^.Health;
+   inc(Hedgehog^.Gear^.Health, healthBoost);
+   // Prevent overflow
+   if (Hedgehog^.Gear^.Health < 1) or (Hedgehog^.Gear^.Health > cMaxHogHealth) then
+       Hedgehog^.Gear^.Health:= cMaxHogHealth;
+   IncHogHealth:= Hedgehog^.Gear^.Health - oldHealth;
+end;
+
 procedure CheckHHDamage(Gear: PGear);
 var
     dmg: LongInt;
@@ -435,11 +472,6 @@
     if ((Gear^.Hedgehog^.Effects[heInvulnerable] <> 0)) then
         exit;
 
-    //if _0_6 < Gear^.dY then
-    //    PlaySound(sndOw4, Gear^.Hedgehog^.Team^.voicepack)
-    //else
-    //    PlaySound(sndOw1, Gear^.Hedgehog^.Team^.voicepack);
-
     if Gear^.LastDamage <> nil then
         ApplyDamage(Gear, Gear^.LastDamage, dmg, dsFall)
     else
@@ -453,7 +485,6 @@
     dAngle: real;
 begin
     // Frac/Round to be kind to JS as of 2012-08-27 where there is yet no int64/uint64
-    //dAngle := (Gear^.dX.QWordValue + Gear^.dY.QWordValue) / $80000000;
     dAngle := (Gear^.dX.Round + Gear^.dY.Round) / 2 + (Gear^.dX.Frac/$100000000+Gear^.dY.Frac/$100000000);
     if not Gear^.dX.isNegative then
         Gear^.DirAngle := Gear^.DirAngle + dAngle
@@ -555,6 +586,13 @@
     else Scale:= Scale + ((1-Scale) / 2);
     if Scale > 1 then Timer:= round(min(Scale*0.0005/cGravityf,4))
     else Timer:= 1;
+    if Scale > 1 then
+        if (not isImpactH) then
+            Y:= Y + 10
+        else if isImpactRight then
+            X:= X + 10
+        else
+            X:= X - 10;
     // Low Gravity
     FrameTicks:= FrameTicks*Timer;
     end;
@@ -631,7 +669,7 @@
     if WorldEdge = weSea then
         begin
         tmp:= dist2Water;
-        dist2Water:= min(dist2Water, min(X - Gear^.Radius - LongInt(leftX), LongInt(rightX) - (X + Gear^.Radius)));
+        dist2Water:= min(dist2Water, min(X - Gear^.Radius - leftX, rightX - (X + Gear^.Radius)));
         // if water on sides is closer than on bottom -> horizontal direction
         isDirH:= tmp <> dist2Water;
         end;
@@ -711,9 +749,9 @@
                             Gear^.State := Gear^.State and (not gstHHDriven);
                             s:= ansistring(Gear^.Hedgehog^.Name);
                             if Gear^.Hedgehog^.King then
-                                AddCaption(FormatA(GetEventString(eidKingDied), s), cWhiteColor, capgrpMessage)
+                                AddCaption(FormatA(GetEventString(eidKingDied), s), capcolDefault, capgrpMessage)
                             else
-                                AddCaption(FormatA(GetEventString(eidDrowned), s), cWhiteColor, capgrpMessage);
+                                AddCaption(FormatA(GetEventString(eidDrowned), s), capcolDefault, capgrpMessage);
                             end
                         end
                     else
@@ -723,7 +761,7 @@
                 end
             else // submersible
                 begin
-                // drown submersible grears if far below map
+                // drown submersible gears if far below map
                 if (Y > cWaterLine + cVisibleWater*4) then
                     begin
                     DrownGear(Gear);
@@ -762,7 +800,8 @@
 
         // splash sound animation and droplets
         if isImpact or isSkip then
-            addSplashForGear(Gear, isSkip);
+            if (not (((dist2Water + Gear^.Radius div 2) < 0) or (abs(dist2Water + Gear^.Radius) >= Gear^.Radius))) then
+                addSplashForGear(Gear, isSkip);
 
         if isSkip then
             ScriptCall('onGearWaterSkip', Gear^.uid);
@@ -774,13 +813,14 @@
 
 procedure ResurrectHedgehog(var gear: PGear);
 var tempTeam : PTeam;
-    sparkles: PVisualGear;
+    sparkles, expl: PVisualGear;
     gX, gY: LongInt;
 begin
     if (Gear^.LastDamage <> nil) then
         uStats.HedgehogDamaged(Gear, Gear^.LastDamage, 0, true)
     else
         uStats.HedgehogDamaged(Gear, CurrentHedgehog, 0, true);
+    // Reset gear state
     AttackBar:= 0;
     gear^.dX := _0;
     gear^.dY := _0;
@@ -799,20 +839,25 @@
     DeleteCI(gear);
     gX := hwRound(gear^.X);
     gY := hwRound(gear^.Y);
-    // might need more sparkles for a column
+    // Spawn a few sparkles at death position.
+    // Might need more sparkles for a column.
     sparkles:= AddVisualGear(gX, gY, vgtDust, 1);
     if sparkles <> nil then
         begin
         sparkles^.Tint:= tempTeam^.Clan^.Color shl 8 or $FF;
-        //sparkles^.Angle:= random(360);
         end;
+    // Set new position of gear (might fail)
     FindPlace(gear, false, 0, LAND_WIDTH, true);
     if gear <> nil then
         begin
-        AddVisualGear(hwRound(gear^.X), hwRound(gear^.Y), vgtExplosion);
+        // Visual effect at position of resurrection
+        expl:= AddVisualGear(hwRound(gear^.X), hwRound(gear^.Y), vgtExplosion);
         PlaySound(sndWarp);
         RenderHealth(gear^.Hedgehog^);
-        ScriptCall('onGearResurrect', gear^.uid);
+        if expl <> nil then
+            ScriptCall('onGearResurrect', gear^.uid, expl^.uid)
+        else
+            ScriptCall('onGearResurrect', gear^.uid);
         gear^.State := gstWait;
         end;
     RecountTeamHealth(tempTeam);
@@ -896,7 +941,7 @@
 tryAgain:= true;
 if WorldEdge <> weNone then
     begin
-    Left:= max(Left, LongInt(leftX) + Gear^.Radius);
+    Left:= max(Left, leftX + Gear^.Radius);
     Right:= min(Right,rightX-Gear^.Radius)
     end;
 while tryAgain do
@@ -1025,28 +1070,92 @@
     end
 end;
 
-function CheckGearNear(Gear: PGear; Kind: TGearType; rX, rY: LongInt): PGear;
+function CheckGearNearImpl(Kind: TGearType; X, Y: hwFloat; rX, rY: LongInt; exclude: PGear): PGear;
 var t: PGear;
+    width, bound, dX, dY: hwFloat;
+    isHit: Boolean;
+    i, j: LongWord;
 begin
-t:= GearsList;
-rX:= sqr(rX);
-rY:= sqr(rY);
+    bound:= _1_5 * int2hwFloat(max(rX, rY));
+    rX:= sqr(rX);
+    rY:= sqr(rY);
+    width:= int2hwFloat(RightX - LeftX);
+    if (Kind = gtHedgehog) then
+        begin
+        for j:= 0 to Pred(TeamsCount) do
+            if TeamsArray[j]^.TeamHealth > 0 then // it's impossible for a team to have hogs in game and zero health right?
+                with TeamsArray[j]^ do
+                    for i:= 0 to cMaxHHIndex do
+                        with Hedgehogs[i] do
+                            if (Gear <> nil) and (Gear <> exclude) then
+                                begin
+                                // code duplication - could throw into an inline function I guess
+                                dX := X - Gear^.X;
+                                dY := Y - Gear^.Y;
+                                isHit := (hwAbs(dX) + hwAbs(dY) < bound)
+                                    and (not ((hwSqr(dX) / rX + hwSqr(dY) / rY) > _1));
+
+                                if (not isHit) and (WorldEdge = weWrap) then
+                                    begin
+                                    if (hwAbs(dX - width) + hwAbs(dY) < bound)
+                                        and (not ((hwSqr(dX - width) / rX + hwSqr(dY) / rY) > _1)) then
+                                        isHit := true
+                                    else if (hwAbs(dX + width) + hwAbs(dY) < bound)
+                                        and (not ((hwSqr(dX + width) / rX + hwSqr(dY) / rY) > _1)) then
+                                        isHit := true
+                                    end;
 
-while t <> nil do
-    begin
-    if (t <> Gear) and (t^.Kind = Kind) then
-        if (not ((hwSqr(Gear^.X - t^.X) / rX + hwSqr(Gear^.Y - t^.Y) / rY) > _1)) or
-        ((WorldEdge = weWrap) and (
-        (not ((hwSqr(Gear^.X - int2hwFloat(RightX-LeftX) - t^.X) / rX + hwSqr(Gear^.Y - t^.Y) / rY) > _1)) or
-        (not ((hwSqr(Gear^.X + int2hwFloat(RightX-LeftX) - t^.X) / rX + hwSqr(Gear^.Y - t^.Y) / rY) > _1)))) then
+                                if isHit then
+                                    begin
+                                    CheckGearNearImpl:= Gear;
+                                    exit;
+                                    end
+                                end;
+        end
+    else
         begin
-            CheckGearNear:= t;
-            exit;
-        end;
-    t:= t^.NextGear
+        t:= GearsList;
+
+        while t <> nil do
+            begin
+            if (t <> exclude) and (t^.Kind = Kind) then
+                begin
+                dX := X - t^.X;
+                dY := Y - t^.Y;
+                isHit := (hwAbs(dX) + hwAbs(dY) < bound)
+                    and (not ((hwSqr(dX) / rX + hwSqr(dY) / rY) > _1));
+
+                if (not isHit) and (WorldEdge = weWrap) then
+                    begin
+                    if (hwAbs(dX - width) + hwAbs(dY) < bound)
+                        and (not ((hwSqr(dX - width) / rX + hwSqr(dY) / rY) > _1)) then
+                        isHit := true
+                    else if (hwAbs(dX + width) + hwAbs(dY) < bound)
+                        and (not ((hwSqr(dX + width) / rX + hwSqr(dY) / rY) > _1)) then
+                        isHit := true
+                    end;
+
+                if isHit then
+                    begin
+                    CheckGearNearImpl:= t;
+                    exit;
+                    end;
+                end;
+            t:= t^.NextGear
+            end
     end;
 
-CheckGearNear:= nil
+    CheckGearNearImpl:= nil
+end;
+
+function CheckGearNear(Kind: TGearType; X, Y: hwFloat; rX, rY: LongInt): PGear;
+begin
+    CheckGearNear := CheckGearNearImpl(Kind, X, Y, rX, rY, nil);
+end;
+
+function CheckGearNear(Gear: PGear; Kind: TGearType; rX, rY: LongInt): PGear;
+begin
+    CheckGearNear := CheckGearNearImpl(Kind, Gear^.X, Gear^.Y, rX, rY, Gear);
 end;
 
 procedure CheckCollision(Gear: PGear); inline;
@@ -1150,11 +1259,11 @@
         gtHedgehog,
             gtMine,
             gtSMine,
+            gtAirMine,
             gtKnife,
             gtCase,
             gtTarget,
-            gtExplosives: begin//,
-//            gtStructure: begin
+            gtExplosives: begin
 //addFileLog('ShotgunShot radius: ' + inttostr(Gear^.Radius) + ', t^.Radius = ' + inttostr(t^.Radius) + ', distance = ' + inttostr(dist) + ', dmg = ' + inttostr(dmg));
                     dmg:= 0;
                     r:= Gear^.Radius + t^.Radius;
@@ -1180,7 +1289,13 @@
                         t^.State:= t^.State or gstMoving;
                         if t^.Kind = gtKnife then t^.State:= t^.State and (not gstCollision);
                         t^.Active:= true;
-                        FollowGear:= t
+                        FollowGear:= t;
+
+                        if t^.Kind = gtAirmine then
+                        begin
+                            t^.Tag:= 1;
+                            t^.FlightTime:= 5000;
+                        end
                         end
                     end;
             gtGrave: begin
@@ -1262,8 +1377,10 @@
     begin
     dec(i);
     Gear:= t^.ar[i];
-    if (Ammo^.Data <> nil) and (Ammo^.Kind in [gtDEagleShot, gtSniperRifleShot, gtMinigunBullet]) and (PGear(Ammo^.Data) = Gear)
-    or ((Ammo^.Kind = gtMinigunBullet) and (not UpdateHitOrder(Gear, Ammo^.WDTimer))) then
+    if (Ammo^.Kind in [gtDEagleShot, gtSniperRifleShot, gtMinigunBullet,
+                       gtFirePunch, gtKamikaze, gtWhip, gtShover])
+        and (((Ammo^.Data <> nil) and (PGear(Ammo^.Data) = Gear))
+             or (not UpdateHitOrder(Gear, Ammo^.WDTimer))) then
         continue;
 
     if ((Ammo^.Kind = gtFlame) or (Ammo^.Kind = gtBlowTorch)) and
@@ -1273,30 +1390,28 @@
     if (Gear^.State and gstNoDamage) = 0 then
         begin
 
-        if (Gear^.Kind <> gtMinigun) and
-            ((Ammo^.Kind = gtDEagleShot)
-                or (Ammo^.Kind = gtSniperRifleShot)
-                or (Ammo^.Kind = gtMinigunBullet)) then
-            begin
-            VGear := AddVisualGear(t^.cX[i], t^.cY[i], vgtBulletHit);
-            if VGear <> nil then
-                VGear^.Angle := DxDy2Angle(-Ammo^.dX, Ammo^.dY);
-            end;
-
         if (Gear^.Kind = gtHedgehog) and (Ammo^.State and gsttmpFlag <> 0) and (Ammo^.Kind = gtShover) then
             Gear^.FlightTime:= 1;
 
-
         case Gear^.Kind of
             gtHedgehog,
             gtMine,
+            gtAirMine,
             gtSMine,
             gtKnife,
             gtTarget,
             gtCase,
-            gtExplosives: //,
-            //gtStructure:
+            gtExplosives:
             begin
+            if (Ammo^.Kind in [gtFirePunch, gtKamikaze]) and (Gear^.Kind <> gtSMine) then
+                PlaySound(sndFirePunchHit);
+
+            if Ammo^.Kind in [gtDEagleShot, gtSniperRifleShot, gtMinigunBullet] then
+                begin
+                VGear := AddVisualGear(t^.cX[i], t^.cY[i], vgtBulletHit);
+                if VGear <> nil then
+                    VGear^.Angle := DxDy2Angle(-Ammo^.dX, Ammo^.dY);
+                end;
             if (Ammo^.Kind = gtDrill) then
                 begin
                 Ammo^.Timer:= 0;
@@ -1326,7 +1441,13 @@
                                 State:= ord(sprStar)
                                 end
                         end;
-                ApplyDamage(Gear, Ammo^.Hedgehog, tmpDmg, dsShove)
+                ApplyDamage(Gear, Ammo^.Hedgehog, tmpDmg, dsShove);
+
+                if Gear^.Kind = gtAirmine then
+                    begin
+                        Gear^.Tag:= 1;
+                        Gear^.FlightTime:= 5000;
+                    end
                 end
             else
                 Gear^.State:= Gear^.State or gstWinner;
@@ -1334,18 +1455,18 @@
                 begin
                 if (Ammo^.Hedgehog^.Gear <> nil) then
                     Ammo^.Hedgehog^.Gear^.State:= Ammo^.Hedgehog^.Gear^.State and (not gstNotKickable);
-                ApplyDamage(Gear, Ammo^.Hedgehog, tmpDmg * 100, dsUnknown); // crank up damage for explosives + blowtorch
+                ApplyDamage(Gear, Ammo^.Hedgehog, tmpDmg * 100, dsExplosion); // crank up damage for explosives + blowtorch
                 end;
 
             if (Gear^.Kind = gtHedgehog) and (Gear^.Hedgehog^.King or (Gear^.Hedgehog^.Effects[heFrozen] > 0)) then
                 begin
-                Gear^.dX:= Gear^.dX + Ammo^.dX * Power * _0_005;
-                Gear^.dY:= Gear^.dY + Ammo^.dY * Power * _0_005
+                Gear^.dX:= Ammo^.dX * Power * _0_005;
+                Gear^.dY:= Ammo^.dY * Power * _0_005
                 end
             else if ((Ammo^.Kind <> gtFlame) or (Gear^.Kind = gtHedgehog)) and (Power <> 0) then
                 begin
-                Gear^.dX:= Gear^.dX + Ammo^.dX * Power * _0_01;
-                Gear^.dY:= Gear^.dY + Ammo^.dY * Power * _0_01
+                Gear^.dX:= Ammo^.dX * Power * _0_01;
+                Gear^.dY:= Ammo^.dY * Power * _0_01
                 end;
 
             if (not isZero(Gear^.dX)) or (not isZero(Gear^.dY)) then
@@ -1383,10 +1504,7 @@
 procedure AmmoShoveLine(Ammo: PGear; Damage, Power: LongInt; oX, oY, tX, tY: hwFloat);
 var t: PGearArray;
 begin
-    if Ammo^.Kind = gtMinigunBullet then
-        t:= CheckAllGearsLineCollision(Ammo, oX, oY, tX, tY)
-    else
-        t:= CheckGearsLineCollision(Ammo, oX, oY, tX, tY);
+    t:= CheckAllGearsLineCollision(Ammo, oX, oY, tX, tY);
     AmmoShoveImpl(Ammo, Damage, Power, t);
 end;
 
@@ -1396,6 +1514,11 @@
         CheckGearsCollision(Ammo));
 end;
 
+procedure AmmoShoveCache(Ammo: PGear; Damage, Power: LongInt);
+begin
+    AmmoShoveImpl(Ammo, Damage, Power,
+        CheckCacheCollision(Ammo));
+end;
 
 function CountGears(Kind: TGearType): Longword;
 var t: PGear;
@@ -1481,14 +1604,14 @@
     GearsNear.ar:= @GearsNearArray
 end;
 
-
-procedure SpawnBoxOfSmth;
+function SpawnBoxOfSmth: PGear;
 var t, aTot, uTot, a, h: LongInt;
     i: TAmmoType;
 begin
-if (PlacingHogs) or
+SpawnBoxOfSmth:= nil;
+if (PlacingHogs) or (PlacingKings) or
     (cCaseFactor = 0)
-    or (CountGears(gtCase) >= 5)
+    or (CountGears(gtCase) >= cMaxCaseDrops)
     or (GetRandom(cCaseFactor) <> 0) then
        exit;
 
@@ -1522,11 +1645,12 @@
 if t<h then
     begin
     FollowGear:= AddGear(0, 0, gtCase, 0, _0, _0, 0);
+    FollowGear^.RenderHealth:= true;
     FollowGear^.Health:= cHealthCaseAmount;
     FollowGear^.Pos:= posCaseHealth;
     // health crate is smaller than the other crates
     FollowGear^.Radius := cCaseHealthRadius;
-    AddCaption(GetEventString(eidNewHealthPack), cWhiteColor, capgrpAmmoInfo);
+    AddCaption(GetEventString(eidNewHealthPack), capcolDefault, capgrpAmmoInfo);
     end
 else if (t<a+h) then
     begin
@@ -1538,7 +1662,7 @@
         i:= Low(TAmmoType);
         FollowGear^.Pos:= posCaseAmmo;
         FollowGear^.AmmoType:= i;
-        AddCaption(GetEventString(eidNewAmmoPack), cWhiteColor, capgrpAmmoInfo);
+        AddCaption(GetEventString(eidNewAmmoPack), capcolDefault, capgrpAmmoInfo);
         end
     end
 else
@@ -1551,7 +1675,7 @@
         i:= Low(TAmmoType);
         FollowGear^.Pos:= posCaseUtility;
         FollowGear^.AmmoType:= i;
-        AddCaption(GetEventString(eidNewUtilityPack), cWhiteColor, capgrpAmmoInfo);
+        AddCaption(GetEventString(eidNewUtilityPack), capcolDefault, capgrpAmmoInfo);
         end
     end;
 
@@ -1559,10 +1683,51 @@
 if (FollowGear <> nil) then
     begin
     FindPlace(FollowGear, true, 0, LAND_WIDTH);
+    PlayBoxSpawnTaunt(FollowGear);
+    SpawnBoxOfSmth:= FollowGear;
+    end
+end;
 
-    if (FollowGear <> nil) then
-        AddVoice(sndReinforce, CurrentTeam^.voicepack)
-    end
+procedure PlayBoxSpawnTaunt(Gear: PGear);
+const
+    // Max. distance between hog and crate for sndThisOneIsMine taunt
+    ThisOneIsMineDistance : LongInt = 130;
+var d, minD: LongInt;
+    gi, closestHog: PGear;
+begin
+    // Taunt
+    if (Gear <> nil) then
+        begin
+        // Look for hog closest to the crate (on the X axis)
+        gi := GearsList;
+        minD := LAND_WIDTH + ThisOneIsMineDistance + 1;
+        closestHog:= nil;
+        while gi <> nil do
+            begin
+            if (gi^.Kind = gtHedgehog) then
+                begin
+                // Y axis is ignored to simplify calculations
+                d := hwRound(hwAbs(gi^.X - Gear^.X));
+                if d < minD then
+                    begin
+                    minD := d;
+                    closestHog:= gi;
+                    end;
+                end;
+            gi := gi^.NextGear;
+            end;
+
+        // Is closest hog close enough to the crate (on the X axis)?
+        if (closestHog <> nil) and (closestHog^.Hedgehog <> nil) and (minD <= ThisOneIsMineDistance) then
+            // If so, there's a chance for a special taunt
+            if random(3) > 0 then
+                AddVoice(sndThisOneIsMine, closestHog^.Hedgehog^.Team^.voicepack)
+            else
+                AddVoice(sndReinforce, CurrentTeam^.voicepack)
+        else
+        // Default crate drop taunt
+            AddVoice(sndReinforce, CurrentTeam^.voicepack);
+        end;
 end;
 
 
@@ -1629,74 +1794,117 @@
 Trying to make the checks a little broader than on first pass to catch things that don't move normally.
 *)
 function WorldWrap(var Gear: PGear): boolean;
-//var tdx: hwFloat;
+var bounced: boolean;
 begin
 WorldWrap:= false;
 if WorldEdge = weNone then exit(false);
-if (hwRound(Gear^.X) < LongInt(leftX)) or
-   (hwRound(Gear^.X) > LongInt(rightX)) then
+if (hwRound(Gear^.X) < leftX) or
+   (hwRound(Gear^.X) > rightX) then
     begin
     if WorldEdge = weWrap then
         begin
-        if (hwRound(Gear^.X) < LongInt(leftX)) then
+        if (hwRound(Gear^.X) < leftX) then
              Gear^.X:= Gear^.X + int2hwfloat(rightX - leftX)
         else Gear^.X:= Gear^.X - int2hwfloat(rightX - leftX);
         LeftImpactTimer:= 150;
-        RightImpactTimer:= 150
+        RightImpactTimer:= 150;
+        WorldWrap:= true;
         end
     else if WorldEdge = weBounce then
         begin
-        if (hwRound(Gear^.X) - Gear^.Radius < LongInt(leftX)) then
+        bounced:= false;
+        // Bounce left
+        if (hwRound(Gear^.X) - Gear^.Radius < leftX) and (((hwSign(Gear^.dX) = -1) and (not isZero(Gear^.dX))) or (Gear^.Kind = gtHedgehog)) then
             begin
             LeftImpactTimer:= 333;
+            // Set X coordinate to bounce edge, unless the gear spawned inside the bounce edge before
+            if (Gear^.State and gstInBounceEdge) = 0 then
+                Gear^.X:= int2hwfloat(leftX + Gear^.Radius);
+            // Invert horizontal speed
             Gear^.dX.isNegative:= false;
-            Gear^.X:= int2hwfloat(LongInt(leftX) + Gear^.Radius)
+            bounced:= true;
             end
-        else
+        // Bounce right
+        else if (hwRound(Gear^.X) + Gear^.Radius > rightX) and (((hwSign(Gear^.dX) = 1) and (not isZero(Gear^.dX))) or (Gear^.Kind = gtHedgehog)) then
             begin
             RightImpactTimer:= 333;
+            // Set X coordinate to bounce edge, unless the gear spawned inside the bounce edge before
+            if (Gear^.State and gstInBounceEdge) = 0 then
+                Gear^.X:= int2hwfloat(rightX - Gear^.Radius);
+            // Invert horizontal speed
             Gear^.dX.isNegative:= true;
-            Gear^.X:= int2hwfloat(rightX-Gear^.Radius)
+            bounced:= true;
             end;
-        if (Gear^.Radius > 2) and (Gear^.dX.QWordValue > _0_001.QWordValue) then
-            AddBounceEffectForGear(Gear);
-        end{
-    else if WorldEdge = weSea then
-        begin
-        if (hwRound(Gear^.Y) > cWaterLine) and (Gear^.State and gstSubmersible <> 0) then
-            Gear^.State:= Gear^.State and (not gstSubmersible)
-        else
+        // Clear gstInBounceEdge when gear is no longer inside a bounce edge area
+        if ((Gear^.State and gstInBounceEdge) <> 0) and (hwRound(Gear^.X) - Gear^.Radius >= leftX) and (hwRound(Gear^.X) + Gear^.Radius <= rightX) then
+            Gear^.State:= Gear^.State and (not gstInBounceEdge);
+        if (bounced) then
             begin
-            Gear^.State:= Gear^.State or gstSubmersible;
-            Gear^.X:= int2hwFloat(PlayWidth)*int2hwFloat(min(max(0,hwRound(Gear^.Y)),PlayHeight))/PlayHeight;
-            Gear^.Y:= int2hwFloat(cWaterLine+cVisibleWater+Gear^.Radius*2);
-            tdx:= Gear^.dX;
-            Gear^.dX:= -Gear^.dY;
-            Gear^.dY:= tdx;
-            Gear^.dY.isNegative:= true
-            end
-        end};
-(*
-* Window in the sky (Gear moved high into the sky, Y is used to determine X) [unfortunately, not a safe thing to do. shame, I thought aerial bombardment would be kinda neat
-This one would be really easy to freeze game unless it was flagged unfortunately.
-
+            WorldWrap:= true;
+            if (Gear^.dX.QWordValue > _0_001.QWordValue) then
+               AddBounceEffectForGear(Gear);
+            end;
+        end
     else
-        begin
-        Gear^.X:= int2hwFloat(PlayWidth)*int2hwFloat(min(max(0,hwRound(Gear^.Y)),PlayHeight))/PlayHeight;
-        Gear^.Y:= -_2048-_256-_256;
-        tdx:= Gear^.dX;
-        Gear^.dX:= Gear^.dY;
-        Gear^.dY:= tdx;
-        Gear^.dY.isNegative:= false
-        end
-*)
-    WorldWrap:= true
+        WorldWrap:= true;
     end;
 end;
 
+(*
+Applies wrap-around logic for the target of homing gears.
+
+In wrap-around world edge, the shortest way may to the target might
+be across the border, so the X value of the target would lead the
+gear to the wrong direction across the whole map. This procedure
+changes the target X in this case.
+This function must be called after the gear passed through
+the wrap-around world edge (WorldWrap returned true).
+
+No-op for other world edges.
+
+Returns true if target has been changed.
+*)
+function HomingWrap(var Gear: PGear): boolean;
+var dist_center, dist_right, dist_left: hwFloat;
+begin
+    if WorldEdge = weWrap then
+        begin
+        HomingWrap:= false;
+        // We just check the same target 3 times:
+        // 1) in current section (no change)
+        // 2) clone in the right section
+        // 3) clone in the left section
+        // The gear will go for the target with the shortest distance to the gear.
+        // For simplicity, we only check distance on the X axis.
+        dist_center:= hwAbs(Gear^.X - int2hwFloat(Gear^.Target.X));
+        dist_right:= hwAbs(Gear^.X - int2hwFloat(Gear^.Target.X + (RightX-LeftX)));
+        dist_left:= hwAbs(Gear^.X - int2hwFloat(Gear^.Target.X - (RightX-LeftX)));
+        if (dist_left < dist_right) and (dist_left < dist_center) then
+            begin
+            dec(Gear^.Target.X, RightX-LeftX);
+            HomingWrap:= true;
+            end
+        else if (dist_right < dist_left) and (dist_right < dist_center) then
+            begin
+            inc(Gear^.Target.X, RightX-LeftX);
+            HomingWrap:= true;
+            end;
+        end;
+end;
+
+// Add an audiovisual bounce effect for gear after it bounced from bouncy material.
+// Graphical effect is based on speed.
 procedure AddBounceEffectForGear(Gear: PGear);
+begin
+    AddBounceEffectForGear(Gear, hwFloat2Float(Gear^.Density * hwAbs(Gear^.dY) + hwAbs(Gear^.dX)) / 1.5);
+end;
+
+// Same as above, but can specify the size of bounce image with imageScale manually.
+procedure AddBounceEffectForGear(Gear: PGear; imageScale: Single);
 var boing: PVisualGear;
 begin
+    if (Gear^.Density < _0_01) or (Gear^.Radius < 2) then
+        exit;
     boing:= AddVisualGear(hwRound(Gear^.X), hwRound(Gear^.Y), vgtStraightShot, 0, false, 1);
     if boing <> nil then
         with boing^ do
@@ -1705,18 +1913,27 @@
             dx:= 0;
             dy:= 0;
             FrameTicks:= 200;
-            Scale:= hwFloat2Float(Gear^.Density * hwAbs(Gear^.dY) + hwAbs(Gear^.dX)) / 1.5;
+            Scale:= imageScale;
             State:= ord(sprBoing)
             end;
-    if Gear^.Kind = gtDuck then
-        PlaySound(sndDuckDrop, true)
+    PlaySound(sndMelonImpact, true)
+end;
+
+function IsHogFacingLeft(Gear: PGear): boolean;
+var sign: LongInt;
+begin
+    sign:= hwSign(Gear^.dX);
+    if (CurAmmoGear <> nil) and (CurAmmoGear^.Kind = gtParachute) then
+        IsHogFacingLeft:= CurAmmoGear^.Tag = -1
+    else if ((Gear^.State and gstHHHJump) <> 0) and (Gear^.Hedgehog^.Effects[heArtillery] = 0) then
+        IsHogFacingLeft:= sign > 0
     else
-        PlaySound(sndMelonImpact, true)
+        IsHogFacingLeft:= sign < 0;
 end;
 
 function IsHogLocal(HH: PHedgehog): boolean;
 begin
-    IsHogLocal:= (not (HH^.Team^.ExtDriven or (HH^.BotLevel > 0))) or (HH^.Team^.Clan^.ClanIndex = LocalClan) or (GameType = gmtDemo);
+    IsHogLocal:= (not (HH^.Team^.ExtDriven or (HH^.BotLevel > 0))) or (HH^.Team^.Clan^.LocalOrAlly) or (GameType = gmtDemo);
 end;
 
 end.