hedgewars/uDrawing.pas
changeset 15908 014f4edd0421
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hedgewars/uDrawing.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -0,0 +1,525 @@
+(*
+ * 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
+ *)
+
+{$INCLUDE "options.inc"}
+
+unit uDrawing;
+(*
+ * This unit defines the Drawing mode, which allows drawing some graphics
+ * private to the members of a clan.
+ *)
+interface
+
+procedure initModule;
+procedure freeModule;
+
+function isDrawingModeActive(): boolean;
+procedure onModeButtonPressed();
+procedure onModeButtonReleased();
+procedure onFocusStateChanged();
+procedure onCursorMoved();
+procedure onLeftMouseButtonPressed();
+procedure onLeftMouseButtonReleased();
+procedure onRightMouseButtonPressed();
+procedure onMiddleMouseButtonPressed();
+procedure handleIPCInput(cmd : shortstring);
+
+implementation
+uses uTypes, uConsts, uVariables, uVisualGearsList, uUtils, uDebug, uIO, SDLh, Math;
+
+const
+   cColorsCount = 9;
+   cColors : array [0..cColorsCount - 1] of LongWord = (
+                                $ff020400,
+                                $4980c100,
+                                $1de6ba00,
+                                $b541ef00,
+                                $e55bb000,
+                                $20bf0000,
+                                $fe8b0e00,
+                                $8f590200,
+                                $ffff0100
+                                );
+   // Reserve one color for the local user
+   cKnownUsersMax = cColorsCount - 1;
+   cPointRadius = 25;
+   cBeaconDuration = 125;
+   cEffectDuration = 500;
+   cMaxDrawingRadius = 150;
+   cEffectGearsCountMax = 4;
+
+type
+   TDrawingState = (drwDisabled, drwStart, drwPoint, drwArrow);
+
+   TVisualEffect = record
+                      vGears     : array [0..cEffectGearsCountMax - 1] of PVisualGear;
+                      gearsCount : integer;
+                   end;
+   TDrawingContext = record
+                        state            : TDrawingState;
+                        currVEffect      : TVisualEffect;
+                        prevAutoCameraOn : boolean;
+                        startCursorX     : LongInt;
+                        startCursorY     : LongInt;
+                        knownUsers       : array [0..cKnownUsersMax - 1] of shortstring;
+                        knownUsersCount  : integer;
+                        lastReplacedUserIdx : integer;
+                     end;
+
+var drawingCtx : TDrawingContext;
+
+procedure AddKnownUser(user : shortstring);
+var i : integer;
+begin
+   with drawingCtx do
+   begin
+      for i:= 0 to knownUsersCount - 1 do
+      begin
+         if knownUsers[i] = user then
+            exit;
+      end;
+      if knownUsersCount < cKnownUsersMax then
+      begin
+         knownUsers[knownUsersCount]:= user;
+         Inc(knownUsersCount);
+      end
+      else
+      begin
+         lastReplacedUserIdx:= (lastReplacedUserIdx + 1) mod cKnownUsersMax;
+         knownUsers[lastReplacedUserIdx]:= user;
+      end;
+   end;
+end;
+
+function GetUserColor(user : shortstring) : LongWord;
+var i : integer;
+begin
+   if user = '' then  // local user
+      exit(cColors[0]);
+   with drawingCtx do
+   begin
+      for i:= 0 to knownUsersCount - 1 do
+      begin
+         if knownUsers[i] = user then
+         begin
+            exit(cColors[i + 1]);
+         end;
+      end;
+      exit(cColors[0]);
+   end;
+end;
+
+procedure recalcArrowParams(var arrow: TVisualEffect; X1, Y1, X2, Y2 : real);
+var tmp, tmpSin, tmpCos : real;
+begin
+   with arrow.vGears[0]^ do
+   begin
+      X:= X1;
+      Y:= Y1;
+      dX:= X2;
+      dY:= Y2;
+   end;
+   // Compute arrow pointer coordinates
+   if X2 = X1 then
+      if Y2 > Y1 then
+         tmp:= PI / 2
+      else
+         tmp:= -PI / 2
+   else
+      tmp:= arctan2(Y2 - Y1, X2 - X1);
+   tmpSin:= sin(tmp - PI / 4);
+   tmpCos:= cos(tmp - PI / 4);
+   with arrow.vGears[1]^ do
+   begin
+      X:= X2;
+      Y:= Y2;
+      dX:= X2 - 50 * tmpCos;
+      dY:= Y2 - 50 * tmpSin;
+   end;
+   tmpSin:= sin(tmp + PI / 4);
+   tmpCos:= cos(tmp + PI / 4);
+   with arrow.vGears[2]^ do
+   begin
+      X:= X2;
+      Y:= Y2;
+      dX:= X2 - 50 * tmpCos;
+      dY:= Y2 - 50 * tmpSin;
+   end;
+   // Compute circle center
+   with arrow.vGears[3]^ do
+   begin
+      X:= (X1 + X2) / 2;
+      Y:= (Y1 + Y2) / 2;
+   end;
+end;
+
+procedure doStepPoint(Gear: PVisualGear; Steps: Longword);
+var tmp: LongInt;
+begin
+if Gear^.FrameTicks <= Steps then
+   DeleteVisualGear(Gear)
+else
+begin
+   dec(Gear^.FrameTicks, Steps);
+   if Gear^.Tag = 0 then
+   begin
+      tmp:= round(Gear^.FrameTicks * $FF / cEffectDuration);
+      if tmp > $FF then
+         tmp:= $FF;
+      if tmp >= 0 then
+         Gear^.Tint:= (Gear^.Tint and $FFFFFF00) or Longword(tmp);
+   end
+   else if Gear^.Tag = 1 then
+   begin
+      Gear^.State:= round(Gear^.FrameTicks * 2048 / cBeaconDuration);
+   end;
+end;
+end;
+
+procedure doStepArrow(Gear: PVisualGear; Steps: Longword);
+var tmp: LongInt;
+begin
+if Gear^.Tag = 100 then
+   exit;
+if Gear^.FrameTicks <= Steps then
+   DeleteVisualGear(Gear)
+else
+begin
+   dec(Gear^.FrameTicks, Steps);
+   if Gear^.Tag < 3 then
+   begin
+      tmp:= round(Gear^.FrameTicks * $FF / cEffectDuration);
+      if tmp > $FF then
+         tmp:= $FF;
+      if tmp >= 0 then
+         Gear^.Tint:= (Gear^.Tint and $FFFFFF00) or Longword(tmp);
+   end
+   else if Gear^.Tag = 3 then
+   begin
+      Gear^.State:= round(Gear^.FrameTicks * 2048 / cBeaconDuration);
+   end;
+end;
+end;
+
+function isEffectEmpty(var vEffect : TVisualEffect) : boolean;
+begin
+   isEffectEmpty:= vEffect.gearsCount = 0;
+end;
+
+function AddVEffectCircle(user : shortstring; X, Y : LongInt) : TVisualEffect;
+var vGear  : PVisualGear;
+   vEffect : TVisualEffect;
+   color   : LongWord;
+   i       : integer;
+begin
+   color:= GetUserColor(user);
+   for i:= 0 to 1 do
+   begin
+      vGear := AddVisualGear(X, Y, vgtCircle, cPointRadius, true, 1);
+      if vGear = nil then
+      begin
+         OutError('uDrawing: AddVisualGear returned nil', false);
+         vEffect.gearsCount:= 0;
+         exit(vEffect);
+      end;
+      vGear^.Tint:= color or $FF;
+      vGear^.Angle:= 0;
+      vGear^.Timer:= 10;
+      vGear^.Tag:= i;
+      vGear^.doStep:= @doStepPoint;
+      vEffect.vGears[i]:= vGear;
+   end;
+   vEffect.vGears[0]^.Tint:= color or $FF;
+   vEffect.vGears[0]^.FrameTicks:= cEffectDuration;
+   vEffect.vGears[1]^.Tint:= color or $3F;
+   vEffect.vGears[1]^.FrameTicks:= cBeaconDuration;
+
+   vEffect.gearsCount:= 2;
+   AddVEffectCircle:= vEffect;
+end;
+
+function AddVEffectArrow(user : shortstring; X1, Y1, X2, Y2 : LongInt) : TVisualEffect;
+var vGear  : PVisualGear;
+   vEffect : TVisualEffect;
+   color   : LongWord;
+   i       : integer;
+begin
+   color:= GetUserColor(user);
+   for i:= 0 to 2 do
+   begin
+      vGear := AddVisualGear(0, 0, vgtLine, 10, true, 1);
+      if vGear = nil then
+      begin
+         OutError('uDrawing: AddVisualGear returned nil', false);
+         vEffect.gearsCount:= 0;
+         exit(vEffect);
+      end;
+      vGear^.Tint:= color or $FF;
+      vGear^.FrameTicks:= cEffectDuration;
+      vGear^.Tag:= 100;
+      vGear^.doStep:= @doStepArrow;
+      vEffect.vGears[i]:= vGear;
+   end;
+
+   vGear := AddVisualGear(0, 0, vgtCircle, 2048, true, 1);
+   if vGear = nil then
+   begin
+      OutError('uDrawing: AddVisualGear returned nil', false);
+      vEffect.gearsCount:= 0;
+      exit(vEffect);
+   end;
+   vGear^.Tint:= color;
+   vGear^.Angle:= 0;
+   vGear^.FrameTicks:= cBeaconDuration;
+   vGear^.Timer:= 10;
+   vGear^.Tag:= 100;
+   vGear^.doStep:= @doStepArrow;
+   vEffect.vGears[3]:= vGear;
+
+   vEffect.gearsCount:= 4;
+   recalcArrowParams(vEffect, X1, Y1, X2, Y2);
+
+   AddVEffectArrow:= vEffect;
+end;
+
+procedure VEffectArrowStart(var vEffect : TVisualEffect);
+var i : integer;
+begin
+   for i:= 0 to vEffect.gearsCount - 1 do
+      vEffect.vGears[i]^.Tag:= i;
+   vEffect.vGears[3]^.Tint:= (vEffect.vGears[3]^.Tint and $FFFFFF00) or $3F;
+end;
+
+procedure DeleteVEffect(var vEffect : TVisualEffect);
+var i : integer;
+begin
+   for i:= 0 to vEffect.gearsCount - 1 do
+      DeleteVisualGear(vEffect.vGears[i]);
+   vEffect.gearsCount:= 0;
+end;
+
+function isDrawingModeActive() : boolean;
+begin
+   isDrawingModeActive:= drawingCtx.state <> drwDisabled;
+end;
+
+procedure SendIPCArrow(X1, Y1, X2, Y2: LongInt);
+var s: shortstring;
+begin
+s[0]:= #18;
+s[1]:= 'O';
+s[2]:= 'a';
+SDLNet_Write32(X1, @s[3]);
+SDLNet_Write32(Y1, @s[7]);
+SDLNet_Write32(X2, @s[11]);
+SDLNet_Write32(Y2, @s[15]);
+SendIPC(s)
+end;
+
+procedure SendIPCCircle(X1, Y1: LongInt);
+var s: shortstring;
+begin
+s[0]:= #10;
+s[1]:= 'O';
+s[2]:= 'c';
+SDLNet_Write32(X1, @s[3]);
+SDLNet_Write32(Y1, @s[7]);
+SendIPC(s)
+end;
+
+procedure handleIPCInput(cmd: shortstring);
+var i, drwCmdOffset : integer;
+   userNameLen      : Byte;
+   user             : shortstring;
+   X1, Y1, X2, Y2   : LongInt;
+   VEffect          : TVisualEffect;
+begin
+case cmd[1] of
+  'u' : begin
+     userNameLen:= Byte(cmd[2]);
+     for i:= 0 to userNameLen do
+        user[i]:= cmd[2 + i];
+     drwCmdOffset:= 2 + userNameLen + 1;
+     if Length(cmd) < drwCmdOffset then
+        exit;
+     AddKnownUser(user);
+     case cmd[drwCmdOffset] of
+       'a' : begin
+          if Length(cmd) < drwCmdOffset + 4 * 4 then
+             exit;
+          X1:= SDLNet_Read32(@cmd[drwCmdOffset + 1]);
+          Y1:= SDLNet_Read32(@cmd[drwCmdOffset + 1 + 4]);
+          X2:= SDLNet_Read32(@cmd[drwCmdOffset + 1 + 8]);
+          Y2:= SDLNet_Read32(@cmd[drwCmdOffset + 1 + 12]);
+          VEffect:= AddVEffectArrow(user, X1, Y1, X2, Y2);
+          if not isEffectEmpty(VEffect) then
+             VEffectArrowStart(VEffect);
+       end;
+       'c' : begin
+          if Length(cmd) < drwCmdOffset + 4 * 2 then
+             exit;
+          X1:= SDLNet_Read32(@cmd[drwCmdOffset + 1]);
+          Y1:= SDLNet_Read32(@cmd[drwCmdOffset + 1 + 4]);
+          VEffect:= AddVEffectCircle(user, X1, Y1);
+       end;
+     end;
+  end;
+end;
+end;
+
+procedure onModeButtonPressed();
+begin
+   drawingCtx.state:= drwStart;
+   drawingCtx.prevAutoCameraOn:= autoCameraOn;
+   autoCameraOn:= false;
+end;
+
+procedure onModeButtonReleased();
+begin
+   DeleteVEffect(drawingCtx.currVEffect);
+   if drawingCtx.state <> drwDisabled then
+   begin
+      drawingCtx.state:= drwDisabled;
+      autoCameraOn:= drawingCtx.prevAutoCameraOn;
+   end;
+end;
+
+procedure onFocusStateChanged();
+begin
+   if not cHasFocus then
+      onModeButtonReleased();
+end;
+
+procedure onLeftMouseButtonPressed();
+begin
+if not isDrawingModeActive() then
+   exit;
+case drawingCtx.state of
+    drwStart: begin
+                 drawingCtx.startCursorX:= CursorPoint.X;
+                 drawingCtx.startCursorY:= CursorPoint.Y;
+                 drawingCtx.state:= drwPoint;
+              end;
+end;
+end;
+
+procedure onLeftMouseButtonReleased();
+var tmpX, tmpY, tmpX2, tmpY2 : LongInt;
+   vEffect     : TVisualEffect;
+begin
+if not isDrawingModeActive() then
+   exit;
+case drawingCtx.state of
+    drwPoint: begin
+                 tmpX:= drawingCtx.startCursorX - WorldDx;
+                 tmpY:= cScreenHeight - drawingCtx.startCursorY - WorldDy;
+                 vEffect:= AddVEffectCircle('', tmpX, tmpY);
+                 if not isEffectEmpty(vEffect) then
+                    SendIPCCircle(tmpX, tmpY);
+                 drawingCtx.state:= drwStart;
+              end;
+    drwArrow: begin
+                  tmpX2:= CursorPoint.X - WorldDx;
+                  tmpY2:= cScreenHeight - CursorPoint.Y - WorldDy;
+                  with drawingCtx do
+                  begin
+                     tmpX:= startCursorX - WorldDx;
+                     tmpY:= cScreenHeight - startCursorY - WorldDy;
+                     recalcArrowParams(currVEffect, tmpX, tmpY, tmpX2, tmpY2);
+                     VEffectArrowStart(currVEffect);
+                     SendIPCArrow(tmpX, tmpY, tmpX2, tmpY2);
+                     currVEffect.gearsCount:= 0;
+                  end;
+                  drawingCtx.state:= drwStart;
+               end;
+end;
+end;
+
+procedure onRightMouseButtonPressed();
+begin
+   if not isDrawingModeActive() then
+      exit;
+   DeleteVEffect(drawingCtx.currVEffect);
+   drawingCtx.state:= drwStart;
+end;
+
+procedure onMiddleMouseButtonPressed();
+begin
+end;
+
+procedure onCursorMoved();
+var tmpX, tmpY, tmpX2, tmpY2, dX, dY : LongInt;
+   h                                 : real;
+begin
+if not isDrawingModeActive() then
+   exit;
+autoCameraOn:= false;
+dX:= CursorPoint.X - drawingCtx.startCursorX;
+dY:= CursorPoint.Y - drawingCtx.startCursorY;
+h:= sqrt(dX * dX + dY * dY);
+if (drawingCtx.state <> drwStart) and (h > cMaxDrawingRadius) then
+begin
+   CursorPoint.X:= drawingCtx.startCursorX + round(dX * cMaxDrawingRadius / h);
+   CursorPoint.Y:= drawingCtx.startCursorY + round(dY * cMaxDrawingRadius / h);
+end;
+case drawingCtx.state of
+    drwPoint : begin
+                 if h > cPointRadius then
+                 begin
+                    tmpX:= drawingCtx.startCursorX - WorldDx;
+                    tmpY:= cScreenHeight - drawingCtx.startCursorY - WorldDy;
+                    tmpX2:= CursorPoint.X - WorldDx;
+                    tmpY2:= cScreenHeight - CursorPoint.Y - WorldDy;
+                    drawingCtx.currVEffect:= AddVEffectArrow('', tmpX, tmpY, tmpX2, tmpY2);
+                    if not isEffectEmpty(drawingCtx.currVEffect) then
+                       drawingCtx.state:= drwArrow
+                    else
+                       drawingCtx.state:= drwStart;
+                 end;
+              end;
+    drwArrow : begin
+                  tmpX:= drawingCtx.startCursorX - WorldDx;
+                  tmpY:= cScreenHeight - drawingCtx.startCursorY - WorldDy;
+                  tmpX2:= CursorPoint.X - WorldDx;
+                  tmpY2:= cScreenHeight - CursorPoint.Y - WorldDy;
+                  with drawingCtx do
+                  begin
+                     recalcArrowParams(currVEffect, tmpX, tmpY, tmpX2, tmpY2);
+                  end;
+               end;
+end;
+end;
+
+procedure initModule;
+begin
+   with drawingCtx do
+   begin
+      state:= drwDisabled;
+      currVEffect.gearsCount:= 0;
+      startCursorX:= 0;
+      startCursorY:= 0;
+      knownUsersCount:= 0;
+      lastReplacedUserIdx:= 0;
+   end;
+end;
+
+procedure freeModule;
+begin
+end;
+
+end.