hedgewars/uVisualGearsHandlers.pas
author Wuzzy <Wuzzy2@mail.ru>
Mon, 16 Sep 2019 17:33:49 +0200
changeset 15410 8504fee3b601
parent 15239 de53425ebc39
child 15583 e1078c72ad60
permissions -rw-r--r--
Racer: Fix weird water splashes after waypoint placement Does not affect official racer, as only waypoint placement is touched. The reason was that the air attack gear sometimes was not deleted fast enough so it might occassionally drop some air bombs (these are deleted now). Also, the airplane position was set to water level, which caused another water splash.

(*
 * Hedgewars, a free turn based strategy game
 * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *)

(*
 * This file contains the step handlers for visual gears.
 *
 * Since the effects of visual gears do not affect the course of the game,
 * no "synchronization" between players is required.
 * => The usage of safe functions or data types (e.g. GetRandom() or hwFloat)
 * is usually not necessary and therefore undesirable.
 *)

{$INCLUDE "options.inc"}

unit uVisualGearsHandlers;

interface
uses uTypes, uGears;

var doStepVGHandlers: array[TVisualGearType] of TVGearStepProcedure;

procedure doStepFlake(Gear: PVisualGear; Steps: Longword);
procedure doStepBeeTrace(Gear: PVisualGear; Steps: Longword);
procedure doStepCloud(Gear: PVisualGear; Steps: Longword);
procedure doStepExpl(Gear: PVisualGear; Steps: Longword);
procedure doStepNote(Gear: PVisualGear; Steps: Longword);
procedure doStepLineTrail(Gear: PVisualGear; Steps: Longword);
procedure doStepEgg(Gear: PVisualGear; Steps: Longword);
procedure doStepFire(Gear: PVisualGear; Steps: Longword);
procedure doStepShell(Gear: PVisualGear; Steps: Longword);
procedure doStepSmallDamage(Gear: PVisualGear; Steps: Longword);
procedure doStepBubble(Gear: PVisualGear; Steps: Longword);
procedure doStepSteam(Gear: PVisualGear; Steps: Longword);
procedure doStepAmmo(Gear: PVisualGear; Steps: Longword);
procedure doStepSmoke(Gear: PVisualGear; Steps: Longword);
procedure doStepDust(Gear: PVisualGear; Steps: Longword);
procedure doStepSplash(Gear: PVisualGear; Steps: Longword);
procedure doStepDroplet(Gear: PVisualGear; Steps: Longword);
procedure doStepSmokeRing(Gear: PVisualGear; Steps: Longword);
procedure doStepFeather(Gear: PVisualGear; Steps: Longword);
procedure doStepTeamHealthSorterWork(Gear: PVisualGear; Steps: Longword);
procedure doStepTeamHealthSorter(Gear: PVisualGear; Steps: Longword);
procedure doStepSpeechBubbleWork(Gear: PVisualGear; Steps: Longword);
procedure doStepSpeechBubble(Gear: PVisualGear; Steps: Longword);
procedure doStepHealthTagWork(Gear: PVisualGear; Steps: Longword);
procedure doStepHealthTagWorkUnderWater(Gear: PVisualGear; Steps: Longword);
procedure doStepHealthTag(Gear: PVisualGear; Steps: Longword);
procedure doStepSmokeTrace(Gear: PVisualGear; Steps: Longword);
procedure doStepExplosionWork(Gear: PVisualGear; Steps: Longword);
procedure doStepExplosion(Gear: PVisualGear; Steps: Longword);
procedure doStepBigExplosionWork(Gear: PVisualGear; Steps: Longword);
procedure doStepBigExplosion(Gear: PVisualGear; Steps: Longword);
procedure doStepChunk(Gear: PVisualGear; Steps: Longword);
procedure doStepBulletHit(Gear: PVisualGear; Steps: Longword);
procedure doStepCircle(Gear: PVisualGear; Steps: Longword);
procedure doStepSmoothWindBar(Gear: PVisualGear; Steps: Longword);
procedure doStepStraightShot(Gear: PVisualGear; Steps: Longword);

function isSorterActive: boolean; inline;
procedure initModule;

implementation
uses uCollisions, uVariables, Math, uConsts, uVisualGearsList, uFloat, uSound, uRenderUtils, uWorld, uUtils;

procedure doStepFlake(Gear: PVisualGear; Steps: Longword);
var sign: real;
    moved, rising, outside: boolean;
    vfc, vft: LongWord;
    spawnMargin: LongInt;
const
    randMargin = 50;
begin
if SuddenDeathDmg then
    begin
    if (vobSDCount = 0) then exit;
    end
else
    if (vobCount = 0) then exit;

sign:= 1;
with Gear^ do
    begin

    X:= X + (cWindSpeedf * 400 + dX + tdX) * Steps * Gear^.Scale;

    if SuddenDeathDmg then
        begin
        Y:= Y + (dY + tdY + cGravityf * vobSDFallSpeed) * Steps * Gear^.Scale;
        vfc:= vobSDFramesCount;
        vft:= vobSDFrameTicks;
        end
    else
        begin
        Y:= Y + (dY + tdY + cGravityf * vobFallSpeed) * Steps * Gear^.Scale;
        vfc:= vobFramesCount;
        vft:= vobFrameTicks;
        end;

    if vft > 0 then
        begin
        inc(FrameTicks, Steps);
        if FrameTicks > vft then
            begin
            dec(FrameTicks, vft);
            inc(Frame);
            if Frame = vfc then
                Frame:= 0
            end;
        end;

    Angle:= Angle + dAngle * Steps;
    if Angle > 360 then
        Angle:= Angle - 360
    else
        if Angle < - 360 then
            Angle:= Angle + 360;


    if (round(X) >= cLeftScreenBorder)
    and (round(X) <= cRightScreenBorder)
    and (round(Y) - 250 <= LAND_HEIGHT)
    and (Timer > 0) and (Timer-Steps > 0) then
        begin
        if tdX > 0 then
            sign := 1
        else
            sign:= -1;
        tdX:= tdX - 0.005*Steps*sign;
        if ((sign < 0) and (tdX > 0)) or ((sign > 0) and (tdX < 0)) then
            tdX:= 0;
        if tdX > 0 then
            sign := 1
        else
            sign:= -1;
        tdY:= tdY - 0.005*Steps*sign;
        if ((sign < 0) and (tdY > 0)) or ((sign > 0) and (tdY < 0)) then
            tdY:= 0;
        dec(Timer, Steps)
        end
    else
        begin
        moved:= false;
        if round(X) < cLeftScreenBorder then
            begin
            X:= X + cScreenSpace;
            moved:= true
            end
        else if round(X) > cRightScreenBorder then
            begin
            X:= X - cScreenSpace;
            moved:= true
            end;

        // it's possible for flakes to move upwards
        if SuddenDeathDmg then
            rising:= (cGravityf * vobSDFallSpeed) < 0
        else
            rising:= (cGravityf * vobFallSpeed) < 0;

        if gear^.layer = 2 then
            spawnMargin:= 400
        else
            spawnMargin:= 200;

        // flake fell far below map?
        outside:= (not rising) and (round(Y) - spawnMargin + randMargin > LAND_HEIGHT);
        // if not, did it rise far above map?
        outside:= outside or (rising and (round(Y) < LAND_HEIGHT - 1024 - spawnMargin - randMargin));

        // if flake left acceptable vertical area, respawn it opposite side
        if outside then
            begin
            X:= cLeftScreenBorder + random(cScreenSpace);
            if rising then
                Y:= Y + (1024 + spawnMargin + random(50))
            else
                Y:= Y - (1024 + spawnMargin + random(50));
            moved:= true;
            end;

        if moved then
            begin
            Angle:= random(360);
            dx:= 0.0000038654705 * random(10000);
            dy:= 0.000003506096 * random(7000);
            if random(2) = 0 then dx := -dx
            end;
        Timer:= 0;
        tdX:= 0;
        tdY:= 0
        end;
    end;

end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepBeeTrace(Gear: PVisualGear; Steps: Longword);
begin
if Gear^.FrameTicks > Steps then
    dec(Gear^.FrameTicks, Steps)
else
    DeleteVisualGear(Gear);
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepCloud(Gear: PVisualGear; Steps: Longword);
var s: Longword;
    t: real;
begin
Gear^.X:= Gear^.X + (cWindSpeedf * 750 * Gear^.dX * Gear^.Scale) * Steps;

// up-and-down-bounce magic
s := (GameTicks + Gear^.Timer) mod 4096;
t := 8 * Gear^.Scale * hwFloat2Float(AngleSin(s mod 2048));
if (s < 2048) then t := -t;

Gear^.Y := LAND_HEIGHT - 1184 + LongInt(Gear^.Timer mod 8) + t;

if round(Gear^.X) < cLeftScreenBorder then
    Gear^.X:= Gear^.X + cScreenSpace
else
    if round(Gear^.X) > cRightScreenBorder then
        Gear^.X:= Gear^.X - cScreenSpace
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepExpl(Gear: PVisualGear; Steps: Longword);
var s: LongInt;
begin
s:= min(Steps, cExplFrameTicks);

Gear^.X:= Gear^.X + Gear^.dX * s;
Gear^.Y:= Gear^.Y + Gear^.dY * s;

if Gear^.FrameTicks <= Steps then
    if Gear^.Frame = 0 then
        DeleteVisualGear(Gear)
    else
        begin
        dec(Gear^.Frame);
        Gear^.FrameTicks:= cExplFrameTicks
        end
    else dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepNote(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;

Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
Gear^.dY:= Gear^.dY + cGravityf * Steps / 2;

Gear^.Angle:= Gear^.Angle + (Gear^.Frame + 1) * Steps / 10;
while Gear^.Angle > cMaxAngle do
    Gear^.Angle:= Gear^.Angle - cMaxAngle;

if Gear^.FrameTicks <= Steps then
    DeleteVisualGear(Gear)
else
    dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepLineTrail(Gear: PVisualGear; Steps: Longword);
begin
{$IFNDEF PAS2C}
Steps := Steps;
{$ENDIF}
if Gear^.Timer <= Steps then
    DeleteVisualGear(Gear)
else
    dec(Gear^.Timer, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepEgg(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;

Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
Gear^.dY:= Gear^.dY + cGravityf * Steps;

Gear^.Angle:= round(Gear^.Angle + Steps) mod cMaxAngle;

if Gear^.FrameTicks <= Steps then
    begin
    DeleteVisualGear(Gear);
    exit
    end
else
    dec(Gear^.FrameTicks, Steps);

if Gear^.FrameTicks < $FF then
   Gear^.Tint:= (Gear^.Tint and $FFFFFF00) or Gear^.FrameTicks
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepFire(Gear: PVisualGear; Steps: Longword);
var vgt: PVisualGear;
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;

Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
if (Gear^.State and gstTmpFlag) = 0 then
    begin
    Gear^.dY:= Gear^.dY + cGravityf * Steps;
    if ((GameTicks mod 200) < Steps + 1) then
        begin
        vgt:= AddVisualGear(round(Gear^.X), round(Gear^.Y), vgtFire);
        if vgt <> nil then
            begin
            vgt^.dx:= 0;
            vgt^.dy:= 0;
            vgt^.State:= gstTmpFlag;
            end;
        end
    end
else
    inc(Steps, Steps);

if Gear^.FrameTicks <= Steps then
       DeleteVisualGear(Gear)
else
    dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepShell(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;

Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
Gear^.dY:= Gear^.dY + cGravityf * Steps;

Gear^.Angle:= round(Gear^.Angle + Steps) mod cMaxAngle;

if Gear^.FrameTicks <= Steps then
    DeleteVisualGear(Gear)
else
    dec(Gear^.FrameTicks, Steps)
end;

procedure doStepSmallDamage(Gear: PVisualGear; Steps: Longword);
begin
Gear^.Y:= Gear^.Y - 0.02 * Steps;

if Gear^.FrameTicks <= Steps then
    DeleteVisualGear(Gear)
else
    dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepBubble(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;
Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
Gear^.Y:= Gear^.Y - cDrownSpeedf * Steps;
Gear^.dX := Gear^.dX / (1.001 * Steps);
Gear^.dY := Gear^.dY / (1.001 * Steps);

if (Gear^.FrameTicks <= Steps) or (not CheckCoordInWater(round(Gear^.X), round(Gear^.Y))) then
    DeleteVisualGear(Gear)
else
    dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepSteam(Gear: PVisualGear; Steps: Longword);
begin
if ((cWindSpeedf > 0) and ( leftX > Gear^.X))
or ((cWindSpeedf < 0) and (rightX < Gear^.X)) then
    Gear^.X:= Gear^.X + (cWindSpeedf * 100 + Gear^.dX) * Steps;
Gear^.Y:= Gear^.Y - cDrownSpeedf * Steps;

if Gear^.FrameTicks <= Steps then
    if Gear^.Frame = 0 then
        DeleteVisualGear(Gear)
    else
        begin
        if Random(2) = 0 then
            dec(Gear^.Frame);
        Gear^.FrameTicks:= cExplFrameTicks
        end
else dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepAmmo(Gear: PVisualGear; Steps: Longword);
begin
Gear^.Y:= Gear^.Y - cDrownSpeedf * Steps;

Gear^.scale:= Gear^.scale + 0.0025 * Steps;
Gear^.alpha:= Gear^.alpha - 0.0015 * Steps;

if Gear^.alpha < 0 then
    DeleteVisualGear(Gear)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepSmoke(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + (cWindSpeedf + Gear^.dX) * Steps;
Gear^.Y:= Gear^.Y - (cDrownSpeedf + Gear^.dY) * Steps;

Gear^.dX := Gear^.dX + (cWindSpeedf * 0.3 * Steps);

if Gear^.FrameTicks <= Steps then
    if Gear^.Frame = 0 then
        DeleteVisualGear(Gear)
    else
        begin
        if Random(2) = 0 then
            dec(Gear^.Frame);
        Gear^.FrameTicks:= cExplFrameTicks
        end
    else dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepDust(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + (cWindSpeedf + (cWindSpeedf * 0.03 * Steps) + Gear^.dX) * Steps;
Gear^.Y:= Gear^.Y - (Gear^.dY) * Steps;

Gear^.dX := Gear^.dX - (Gear^.dX * 0.005 * Steps);
Gear^.dY := Gear^.dY - (cDrownSpeedf * 0.001 * Steps);

if Gear^.FrameTicks <= Steps then
    if Gear^.Frame = 0 then
            DeleteVisualGear(Gear)
    else
        begin
        dec(Gear^.Frame);
        Gear^.FrameTicks:= cExplFrameTicks
        end
    else dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepSplash(Gear: PVisualGear; Steps: Longword);
begin
if Gear^.FrameTicks <= Steps then
    DeleteVisualGear(Gear)
else
    dec(Gear^.FrameTicks, Steps);
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepDroplet(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;

Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
Gear^.dY:= Gear^.dY + cGravityf * Steps;

if CheckCoordInWater(round(Gear^.X), round(Gear^.Y)) then
    begin
    DeleteVisualGear(Gear);
    PlaySound(TSound(ord(sndDroplet1) + Random(3)));
    end;
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepSmokeRing(Gear: PVisualGear; Steps: Longword);
begin
inc(Gear^.Timer, Steps);
if Gear^.Timer >= Gear^.FrameTicks then
    DeleteVisualGear(Gear)
else
    begin
    Gear^.scale := 1.25 * (-power(2, -10 * Int(Gear^.Timer)/Gear^.FrameTicks) + 1) + 0.4;
    Gear^.alpha := 1 - power(Gear^.Timer / 350, 4);
    if Gear^.alpha < 0 then
        Gear^.alpha:= 0;
    end;
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepFeather(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;

Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
Gear^.dY:= Gear^.dY + cGravityf * Steps;

Gear^.Angle:= round(Gear^.Angle + Steps) mod cMaxAngle;

if Gear^.FrameTicks <= Steps then
    DeleteVisualGear(Gear)
else
    dec(Gear^.FrameTicks, Steps)
end;

////////////////////////////////////////////////////////////////////////////////
const cSorterWorkTime = 640;
var thexchar: array[0..cMaxTeams] of
            record
            dy, ny, dw: LongInt;
            team: PTeam;
            SortFactor: QWord;
            hdw: array[0..cMaxHHIndex] of LongInt;
            end;
    currsorter: PVisualGear = nil;

function isSorterActive: boolean; inline;
begin
    isSorterActive:= currsorter <> nil
end;

procedure doStepTeamHealthSorterWork(Gear: PVisualGear; Steps: Longword);
var i, t, h: LongInt;
begin
if currsorter = Gear then
  for t:= 1 to min(Steps, Gear^.Timer) do
    begin
    dec(Gear^.Timer);
    if (Gear^.Timer and 15) = 0 then
        for i:= 0 to Pred(TeamsCount) do
            with thexchar[i] do
                begin
                {$WARNINGS OFF}
                team^.DrawHealthY:= ny + dy * LongInt(Gear^.Timer) div cSorterWorkTime;
                team^.TeamHealthBarHealth:= team^.TeamHealth + dw * LongInt(Gear^.Timer) div cSorterWorkTime;

                for h:= 0 to cMaxHHIndex do
                    if (team^.Hedgehogs[h].Gear <> nil) then
                        team^.Hedgehogs[h].HealthBarHealth:= team^.Hedgehogs[h].Gear^.Health + hdw[h] * LongInt(Gear^.Timer) div cSorterWorkTime
                    else
                        team^.Hedgehogs[h].HealthBarHealth:= hdw[h] * LongInt(Gear^.Timer) div cSorterWorkTime;
                {$WARNINGS ON}
                end;
    end;

if (Gear^.Timer = 0) or (currsorter <> Gear) then
    begin
    if currsorter = Gear then
        currsorter:= nil;
    DeleteVisualGear(Gear);
    exit
    end
end;

procedure doStepTeamHealthSorter(Gear: PVisualGear; Steps: Longword);
var i: Longword;
    b, noHogs: boolean;
    t, h: LongInt;
begin
{$IFNDEF PAS2C}
Steps:= Steps; // avoid compiler hint
{$ENDIF}

for t:= 0 to Pred(TeamsCount) do
    with thexchar[t] do
        begin
        team:= TeamsArray[t];
        dy:= team^.DrawHealthY;
        dw:= team^.TeamHealthBarHealth - team^.TeamHealth;
        if team^.TeamHealth > 0 then
            begin
            SortFactor:= team^.Clan^.ClanHealth;
            SortFactor:= (SortFactor shl  3) + team^.Clan^.ClanIndex;
            SortFactor:= (SortFactor shl 30) + team^.TeamHealth;
            end
        else
            SortFactor:= 0;

        for h:= 0 to cMaxHHIndex do
            if (team^.Hedgehogs[h].Gear <> nil) then
                hdw[h]:= team^.Hedgehogs[h].HealthBarHealth - team^.Hedgehogs[h].Gear^.Health
            else
                hdw[h]:= team^.Hedgehogs[h].HealthBarHealth;
        end;

if TeamsCount > 1 then
    repeat
    b:= true;
    for t:= 0 to TeamsCount - 2 do
        if (thexchar[t].SortFactor > thexchar[Succ(t)].SortFactor) then
            begin
            thexchar[cMaxTeams]:= thexchar[t];
            thexchar[t]:= thexchar[Succ(t)];
            thexchar[Succ(t)]:= thexchar[cMaxTeams];
            b:= false
            end
    until b;

t:= - 4;
for i:= 0 to Pred(TeamsCount) do
        with thexchar[i] do
          begin
          noHogs:= true;
          for h:= 0 to cMaxHHIndex do
              // Check if all hogs are hidden
              if team^.Hedgehogs[h].Gear <> nil then
                  noHogs:= false;
          // Skip team bar if all hogs are dead or hidden
          if (team^.TeamHealth > 0) and (noHogs = false) then
            begin
            dec(t, team^.Clan^.HealthTex^.h + 2);
            ny:= t;
            dy:= dy - ny
            end;
          end;

Gear^.Timer:= cSorterWorkTime;
Gear^.doStep:= @doStepTeamHealthSorterWork;
currsorter:= Gear;
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepSpeechBubbleWork(Gear: PVisualGear; Steps: Longword);
var realgear: PGear;
begin
if Gear^.Timer > Steps then dec(Gear^.Timer, Steps) else Gear^.Timer:= 0;
realgear:= nil;
if Gear^.Frame <> 0 then  // use a non-hedgehog gear - a lua trick that hopefully won't be overused
    begin
    realgear:= GearByUID(Gear^.Frame);
    if realgear <> nil then
        begin
        Gear^.X:= hwFloat2Float(realgear^.X) + (Gear^.Tex^.w div 2  - Gear^.Tag);
        Gear^.Y:= hwFloat2Float(realgear^.Y) - (realgear^.Radius + Gear^.Tex^.h);
        Gear^.Angle:= 1; // Mark speech bubble as ready for rendering
        end
    end
else if Gear^.Hedgehog^.Gear <> nil then
    begin
    Gear^.X:= hwFloat2Float(Gear^.Hedgehog^.Gear^.X) + (Gear^.Tex^.w div 2  - Gear^.Tag);
    Gear^.Y:= hwFloat2Float(Gear^.Hedgehog^.Gear^.Y) - (cHHRadius + Gear^.Tex^.h);
    Gear^.Angle:= 1; // Mark speech bubble as ready for rendering
    end;

if (Gear^.Timer = 0) or ((realgear = nil) and (Gear^.Frame <> 0))  then
    begin
    if (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.SpeechGear = Gear) then
        Gear^.Hedgehog^.SpeechGear:= nil;
    DeleteVisualGear(Gear)
    end;
end;

procedure doStepSpeechBubble(Gear: PVisualGear; Steps: Longword);
var realgear: PGear;
begin

{$IFNDEF PAS2C}
Steps:= Steps; // avoid compiler hint
{$ENDIF}
if Gear^.Frame <> 0 then
    realgear:= GearByUID(Gear^.FrameTicks)
else
    begin
    with Gear^.Hedgehog^ do
        if SpeechGear <> nil then
            SpeechGear^.Timer:= 0;
    realgear:= Gear^.Hedgehog^.Gear;
    Gear^.Hedgehog^.SpeechGear:= Gear;
    end;

if realgear <> nil then
    case Gear^.FrameTicks of
        1: Gear^.Tag:= SpritesData[sprSpeechTail].Width-37+realgear^.Radius;
        2: Gear^.Tag:= SpritesData[sprThoughtTail].Width-29+realgear^.Radius;
        3: Gear^.Tag:= SpritesData[sprShoutTail].Width-19+realgear^.Radius;
        end;

Gear^.Timer:= max(LongInt(Length(Gear^.Text)) * 150, 3000);
Gear^.Tex:= RenderSpeechBubbleTex(ansistring(Gear^.Text), Gear^.FrameTicks, fnt16);

Gear^.doStep:= @doStepSpeechBubbleWork;

Gear^.Y:= Gear^.Y - Gear^.Tex^.h
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepHealthTagWork(Gear: PVisualGear; Steps: Longword);
begin
if Steps > Gear^.Timer then
    DeleteVisualGear(Gear)
else
    begin
    dec(Gear^.Timer, Steps);
    Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
    Gear^.X:= Gear^.X + Gear^.dX * Steps
    end;
end;

procedure doStepHealthTagWorkUnderWater(Gear: PVisualGear; Steps: Longword);
begin
if round(Gear^.Y) - 10 < cWaterLine then
    DeleteVisualGear(Gear)
else
    begin
    Gear^.X:= Gear^.X + Gear^.dX * Steps;
    Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
    end;
end;

procedure doStepHealthTag(Gear: PVisualGear; Steps: Longword);
var s: shortstring;
begin
s:= IntToStr(Gear^.State);

if Gear^.Hedgehog <> nil then
    Gear^.Tex:= RenderStringTex(ansistring(s), Gear^.Hedgehog^.Team^.Clan^.Color, fnt16)
else
    Gear^.Tex:= RenderStringTex(ansistring(s), cWhiteColor, fnt16);

Gear^.doStep:= @doStepHealthTagWork;

if (round(Gear^.Y) > cWaterLine) and (Gear^.Frame = 0) and (Gear^.FrameTicks = 0) then
    Gear^.doStep:= @doStepHealthTagWorkUnderWater;

Gear^.Y:= Gear^.Y - Gear^.Tex^.h;

if Steps > 1 then
    Gear^.doStep(Gear, Steps-1);
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepSmokeTrace(Gear: PVisualGear; Steps: Longword);
begin
inc(Gear^.Timer, Steps );
if Gear^.Timer > 64 then
    begin
    if Gear^.State = 0 then
        begin
        DeleteVisualGear(Gear);
        exit;
        end;
    dec(Gear^.State, Gear^.Timer div 65);
    Gear^.Timer:= Gear^.Timer mod 65;
    end;
Gear^.dX:= Gear^.dX + cWindSpeedf * Steps;
Gear^.X:= Gear^.X + Gear^.dX;
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepExplosionWork(Gear: PVisualGear; Steps: Longword);
begin
inc(Gear^.Timer, Steps);
if Gear^.Timer > 75 then
    begin
    inc(Gear^.State, Gear^.Timer div 76);
    Gear^.Timer:= Gear^.Timer mod 76;
    if Gear^.State > 5 then
        DeleteVisualGear(Gear);
    end;
end;

procedure doStepExplosion(Gear: PVisualGear; Steps: Longword);
var i: LongWord;
    gX,gY: LongInt;
    vg: PVisualGear;
begin
gX:= round(Gear^.X);
gY:= round(Gear^.Y);
for i:= 0 to 31 do
    begin
    vg:= AddVisualGear(gX, gY, vgtFire);
    if vg <> nil then
        begin
        vg^.State:= gstTmpFlag;
        inc(vg^.FrameTicks, vg^.FrameTicks)
        end
    end;
for i:= 0 to  8 do AddVisualGear(gX, gY, vgtExplPart);
for i:= 0 to  8 do AddVisualGear(gX, gY, vgtExplPart2);
Gear^.doStep:= @doStepExplosionWork;
if Steps > 1 then
    Gear^.doStep(Gear, Steps-1);
end;


////////////////////////////////////////////////////////////////////////////////
procedure doStepBigExplosionWork(Gear: PVisualGear; Steps: Longword);
var maxMovement: LongInt;
begin

inc(Gear^.Timer, Steps);
if (Gear^.Timer and 5) = 0 then
    begin
    maxMovement := max(1, 13 - ((Gear^.Timer * 15) div 250));
    ShakeCamera(maxMovement);
    end;

if Gear^.Timer > 250 then
    DeleteVisualGear(Gear);
end;

procedure doStepBigExplosion(Gear: PVisualGear; Steps: Longword);
var i: LongWord;
    gX,gY: LongInt;
    vg: PVisualGear;
begin
//ScreenFade:= sfFromWhite;
//ScreenFadeValue:= round(60 * zoom * zoom);
//ScreenFadeSpeed:= 5;
gX:= round(Gear^.X);
gY:= round(Gear^.Y);
AddVisualGear(gX, gY, vgtSmokeRing);
for i:= 0 to 46 do
    begin
    vg:= AddVisualGear(gX, gY, vgtFire);
    if vg <> nil then
        begin
        vg^.State:= gstTmpFlag;
        inc(vg^.FrameTicks, vg^.FrameTicks)
        end
    end;
for i:= 0 to 15 do
    AddVisualGear(gX, gY, vgtExplPart);
for i:= 0 to 15 do
    AddVisualGear(gX, gY, vgtExplPart2);
Gear^.doStep:= @doStepBigExplosionWork;
if Steps > 1 then
    Gear^.doStep(Gear, Steps-1);

{$IFNDEF PAS2C}
with mobileRecord do
    if (performRumble <> nil) and (not fastUntilLag) then
        performRumble(kSystemSoundID_Vibrate);
{$ENDIF}
end;

procedure doStepChunk(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;

Gear^.Y:= Gear^.Y + Gear^.dY * Steps;
Gear^.dY:= Gear^.dY + cGravityf * Steps;

Gear^.Angle:= round(Gear^.Angle + Steps) mod cMaxAngle;

if CheckCoordInWater(round(Gear^.X), round(Gear^.Y)) then
    begin
    if ((cReducedQuality and rqPlainSplash) = 0) then
        AddVisualGear(round(Gear^.X), round(Gear^.Y), vgtDroplet);
    DeleteVisualGear(Gear);
    end
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepBulletHit(Gear: PVisualGear; Steps: Longword);
begin
if Gear^.FrameTicks <= Steps then
    DeleteVisualGear(Gear)
else
    dec(Gear^.FrameTicks, Steps);
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepCircle(Gear: PVisualGear; Steps: Longword);
var tmp: LongInt;
    i: LongWord;
begin
with Gear^ do
    if Frame <> 0 then
        for i:= 1 to Steps do
            begin
            inc(FrameTicks);
            if (FrameTicks mod Frame) = 0 then
                begin
                tmp:= Gear^.Tint and $FF;
                if tdY >= 0 then
                    inc(tmp)
                else
                    dec(tmp);
                if tmp < round(dX) then
                    tdY:= 1;
                if tmp > round(dY) then
                    tdY:= -1;
                if tmp > 255 then
                    tmp := 255;
                if tmp < 0 then
                    tmp := 0;
                Gear^.Tint:= (Gear^.Tint and $FFFFFF00) or Longword(tmp)
                end
            end
end;

////////////////////////////////////////////////////////////////////////////////
var
    currwindbar: PVisualGear = nil;

procedure doStepSmoothWindBarWork(Gear: PVisualGear; Steps: Longword);
const maxWindBarWidth = 73;
begin
    if currwindbar = Gear then
    begin
    inc(Gear^.Timer, Steps);

    while Gear^.Timer >= 10 do
        begin
        dec(Gear^.Timer, 10);
        if WindBarWidth < Gear^.Tag then
            inc(WindBarWidth)
        else if WindBarWidth > Gear^.Tag then
            dec(WindBarWidth);
        // Prevent wind bar from overflowing
        if WindBarWidth > maxWindBarWidth then
            WindBarWidth:= maxWindBarWidth;
        if WindBarWidth < - maxWindBarWidth then
            WindBarWidth:= - maxWindBarWidth;
        end;
    if cWindspeedf > Gear^.dAngle then
        begin
        cWindspeedf := cWindspeedf - Gear^.Angle*Steps;
        if cWindspeedf < Gear^.dAngle then cWindspeedf:= Gear^.dAngle;
        end
    else if cWindspeedf < Gear^.dAngle then
        begin
        cWindspeedf := cWindspeedf + Gear^.Angle*Steps;
        if cWindspeedf > Gear^.dAngle then cWindspeedf:= Gear^.dAngle;
        end;
    end;

    if (((WindBarWidth = Gear^.Tag) or (Abs(WindBarWidth) >= maxWindBarWidth)) and (cWindspeedf = Gear^.dAngle)) or (currwindbar <> Gear) then
    begin
        if currwindbar = Gear then currwindbar:= nil;
        DeleteVisualGear(Gear)
    end
end;

procedure doStepSmoothWindBar(Gear: PVisualGear; Steps: Longword);
begin
    currwindbar:= Gear;
    Gear^.doStep:= @doStepSmoothWindBarWork;
    doStepSmoothWindBarWork(Gear, Steps)
end;
////////////////////////////////////////////////////////////////////////////////
procedure doStepStraightShot(Gear: PVisualGear; Steps: Longword);
begin
Gear^.X:= Gear^.X + Gear^.dX * Steps;
Gear^.Y:= Gear^.Y - Gear^.dY * Steps;

Gear^.dY:= Gear^.dY + Gear^.tdY * Steps;
Gear^.dX:= Gear^.dX + Gear^.tdX * Steps;

if Gear^.FrameTicks <= Steps then
    DeleteVisualGear(Gear)
else
    begin
    dec(Gear^.FrameTicks, Steps);
    if (Gear^.FrameTicks < 501) and (Gear^.FrameTicks mod 5 = 0) then
        Gear^.Tint:= (Gear^.Tint and $FFFFFF00) or (((Gear^.Tint and $000000FF) * Gear^.FrameTicks) div 500)
    end
end;

////////////////////////////////////////////////////////////////////////////////
procedure doStepNoPlaceWarn(Gear: PVisualGear; Steps: Longword);
begin

if Gear^.FrameTicks <= Steps then
    DeleteVisualGear(Gear)
else
    begin
    // age
    dec(Gear^.FrameTicks, Steps);
    // toggle between orange and red every few ticks
    if (Gear^.FrameTicks div 256) mod 2 = 0 then
        Gear^.Tint:= $FF400000
    else
        Gear^.Tint:= $FF000000;
    // fade out alpha
    Gear^.Tint:= (Gear^.Tint and (not $FF)) or (255 * Gear^.FrameTicks div 3000);
    end
end;

const handlers: array[TVisualGearType] of TVGearStepProcedure =
        (
            @doStepFlake,
            @doStepCloud,
            @doStepExpl,
            @doStepExpl,
            @doStepFire,
            @doStepSmallDamage,
            @doStepTeamHealthSorter,
            @doStepSpeechBubble,
            @doStepBubble,
            @doStepSteam,
            @doStepAmmo,
            @doStepSmoke,
            @doStepSmoke,
            @doStepShell,
            @doStepDust,
            @doStepSplash,
            @doStepDroplet,
            @doStepSmokeRing,
            @doStepBeeTrace,
            @doStepEgg,
            @doStepFeather,
            @doStepHealthTag,
            @doStepSmokeTrace,
            @doStepSmokeTrace,
            @doStepExplosion,
            @doStepBigExplosion,
            @doStepChunk,
            @doStepNote,
            @doStepLineTrail,
            @doStepBulletHit,
            @doStepCircle,
            @doStepSmoothWindBar,
            @doStepStraightShot,
            @doStepNoPlaceWarn
        );

procedure initModule;
begin
    doStepVGHandlers:= handlers
end;

end.