hedgewars/uDrawing.pas
author S.D.
Sun, 16 Oct 2022 13:14:16 +0300
changeset 15908 014f4edd0421
permissions -rw-r--r--
Add Drawing mode, which allows drawing some graphics private to the members of a clan

(*
 * 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.