625 lines
18 KiB
ObjectPascal

unit bgragame;
{$mode objfpc}{$H+}
interface
uses
Classes, Graphics, LCLType, BGRABitmap, BGRABitmapTypes,
SysUtils, Forms, Controls;
const
L1 = VK_LEFT;
R1 = VK_RIGHT;
U1 = VK_UP;
D1 = VK_DOWN;
L2 = VK_A;
R2 = VK_D;
U2 = VK_W;
D2 = VK_S;
type
TGameKey = (gkLeft, gkUp, gkRight, gkDown);
PGameMap = ^TGameMap;
TGameMap = record
mapGraphics: string;
mapProperty: string;
mapSolve: string;
mapPrevPos: string;
mapKeyChar: char;
mapSolidChar: char;
mapWidth: integer;
mapHeight: integer;
blockWidth: integer;
blockHeight: integer;
background: TBGRABitmap;
ballSphereMap: TBGRABitmap;
ballPosition,ballSpeed: TPointF;
ballPositionDefined: boolean;
mouseDown: boolean;
mousePos: TPointF;
mapKeysDown: set of TGameKey;
end;
procedure DrawMap(var map: TGameMap; bitmap: TBGRABitmap; redrawAll: boolean);
function ClickMapCell(var map: TGameMap; pos: TPoint): boolean;
procedure MouseDownMap(var map: TGameMap; pos: TPoint);
procedure MouseUpMap(var map: TGameMap; pos: TPoint);
function GetGameKey(var Key: word; invertLR, invertUD: boolean): TGameKey;
procedure KeyDownMap(var map: TGameMap; var Key: word);
procedure KeyUpMap(var map: TGameMap; var Key: word);
function KeyPressMapCell(var map: TGameMap; Key: TGameKey): boolean;
function KeyPressMapCell(var map: TGameMap; var Key: word): boolean;
function IsMapSolved(var map: TGameMap): boolean;
procedure ToggleFullScreen(Form: TForm; ARect: TRect);
procedure CenterControl(Control: TControl);
function CalculateAspectRatioH(const W1, H1, W2: integer): integer; //result H2
function CalculateAspectRatioW(const W1, H1, H2: integer): integer; //result W2
function CalculateDestRect(ImageW, ImageH, DestW, DestH: integer;
Stretch, Proportional, Center: boolean): TRect;
procedure HighDPI(FromDPI: integer);
procedure ScaleDPI(Control: TControl; FromDPI: integer);
procedure ScaleAspectRatio(Control: TControl; OriginalParentW, OriginalParentH: integer);
procedure ScaleAspectRatio(Control: TControl; DestW, DestH: integer;
Stretch, Proportional, Center: boolean);
implementation
uses BGRAGradients;
procedure DrawMap(var map: TGameMap; bitmap: TBGRABitmap; minx,miny,maxx,maxy: integer; fullbackground: boolean); forward;
procedure DrawBall(var map: TGameMap; bitmap: TBGRABitmap); forward;
procedure InvalidateMap(var map: TGameMap; ARect: TRect);
var minx,miny,maxx,maxy,xb,yb: integer;
begin
if map.blockWidth <> 0 then
minx := ARect.Left div map.blockWidth;
if map.blockWidth <> 0 then
maxx := (ARect.Right div map.blockWidth) +1;
if map.blockHeight <> 0 then
miny := ARect.top div map.blockHeight;
if map.blockHeight <> 0 then
maxy := (ARect.bottom div map.blockHeight) +1;
if miny < 0 then miny :=0;
if minx < 0 then minx := 0;
if maxx > map.mapWidth-1 then maxx := map.mapWidth-1;
if maxy > map.mapHeight-1 then maxy := map.mapHeight-1;
for yb := miny to maxy do
for xb := minx to maxx do
map.mapPrevPos[xb+yb*map.mapWidth+1] := '1';
end;
const acceleration = 0.01;
maxSpeed = 0.1;
procedure MoveBall(var map: TGameMap);
var speed: single;
ix,iy,nix,niy: integer;
dir: TPointF;
dirlen: single;
begin
if map.ballPositionDefined then
begin
if map.mouseDown then
begin
dir := map.mousePos-map.ballPosition;
dirlen := sqrt(dir*dir);
if dirlen > acceleration then dir *= acceleration/dirlen;
map.ballSpeed += dir;
end else
begin
if gkLeft in map.mapKeysDown then map.ballSpeed.x -= acceleration;
if gkRight in map.mapKeysDown then map.ballSpeed.x += acceleration;
if gkUp in map.mapKeysDown then map.ballSpeed.y -= acceleration;
if gkDown in map.mapKeysDown then map.ballSpeed.y += acceleration;
end;
speed := sqrt(map.ballSpeed*map.ballSpeed);
if speed > maxSpeed then map.ballSpeed *= maxSpeed/speed;
ix := round(map.ballPosition.x);
iy := round(map.ballPosition.y);
map.ballPosition += map.ballSpeed;
nix := round(map.ballPosition.x);
niy := round(map.ballPosition.y);
if nix > ix then
begin
if not KeyPressMapCell(map, gkRight) then
begin
map.ballPosition.X := ix+0.499;
map.ballSpeed.X := -abs(map.ballSpeed.X);
end;
end;
if nix < ix then
begin
if not KeyPressMapCell(map, gkLeft) then
begin
map.ballPosition.X := ix-0.499;
map.ballSpeed.X := abs(map.ballSpeed.X);
end;
end;
if niy > iy then
begin
if not KeyPressMapCell(map, gkDown) then
begin
map.ballPosition.y := iy+0.499;
map.ballSpeed.y := -abs(map.ballSpeed.y);
end;
end;
if niy < iy then
begin
if not KeyPressMapCell(map, gkUp) then
begin
map.ballPosition.y := iy-0.499;
map.ballSpeed.y := abs(map.ballSpeed.y);
end;
end;
end;
end;
procedure DrawMap(var map: TGameMap; bitmap: TBGRABitmap; redrawAll: boolean);
begin
MoveBall(map);
if redrawAll then
DrawMap(map,bitmap,0,0,map.mapWidth-1,map.mapHeight-1,true)
else
DrawMap(map,bitmap,0,0,map.mapWidth-1,map.mapHeight-1,false);
DrawBall(map, bitmap);
end;
procedure DrawBall(var map: TGameMap; bitmap: TBGRABitmap);
var phong: TPhongShading;
radius,xi,yi: integer;
r: TRect;
begin
if map.ballPositionDefined then
begin
phong := TPhongShading.Create;
phong.LightPosition := point(bitmap.Width div 2, bitmap.Height div 2);
radius := map.blockWidth;
if map.blockHeight < radius then radius := map.blockHeight;
radius := round(radius*0.3);
xi := round((map.ballPosition.X+0.5)*map.blockWidth);
yi := round((map.ballPosition.Y+0.5)*map.blockHeight);
r := rect(xi-radius,yi-radius,xi+radius+1,yi+radius+1);
if (map.ballSphereMap <> nil) and ((map.ballSphereMap.Width <> r.right-r.left) or (map.ballSphereMap.Height <> r.bottom-r.top)) then
FreeAndNil(map.ballSphereMap);
if map.ballSphereMap = nil then map.ballSphereMap := CreateSpherePreciseMap(r.right-r.left,r.bottom-r.top);
phong.Draw(bitmap,map.ballSphereMap,radius,r.left,r.top,CSSLightGray);
phong.Free;
InvalidateMap(map, rect(xi-radius,yi-radius,xi+radius+1,yi+radius+1));
end;
end;
procedure DrawMap(var map: TGameMap; bitmap: TBGRABitmap; minx,miny,maxx,maxy: integer; fullbackground: boolean);
var
n, nx, ny: integer;
r: TRect;
colorOscillation: single;
oldClip: TRect;
shouldRedraw: boolean;
begin
colorOscillation:= sin(frac(Now)*24*60*60 * 2*Pi)*0.5+0.5;
if minx < 0 then minx := 0;
if miny < 0 then miny := 0;
if maxx > map.mapWidth-1 then maxx := map.mapWidth-1;
if maxy > map.mapHeight-1 then maxy := map.mapHeight-1;
oldClip := bitmap.ClipRect;
bitmap.ClipRect := rect(map.blockWidth * minx, map.blockHeight * miny,
map.blockWidth * (maxx+1), map.blockHeight * (maxy+1));
if fullbackground then bitmap.PutImage(0, 0, map.background, dmSet);
if map.mapPrevPos = '' then
begin
setlength(map.mapPrevPos, map.mapWidth*map.mapHeight);
for n := 1 to length(map.mapPrevPos) do
map.mapPrevPos[n] := '0';
end;
r.Top := map.blockHeight * miny;
r.Bottom := r.Top + map.blockHeight;
for ny := miny to maxy do
begin
r.Left := map.blockWidth * minx;
r.Right := r.Left + map.blockWidth;
n := ny*map.mapWidth+minx+1;
for nx := minx to maxx do
begin
shouldRedraw:= (map.mapProperty[n] = map.mapKeyChar) or (map.mapGraphics[n] = 'X');
if fullbackground or shouldRedraw or (map.mapPrevPos[n] = '1') then
begin
if shouldRedraw then map.mapPrevPos[n] := '1' else map.mapPrevPos[n] := '0';
// begin to draw here
if not fullbackground then bitmap.PutImagePart(r.left, r.top, map.background, r, dmSet);
if map.mapProperty[n] = map.mapKeyChar then
begin
bitmap.Rectangle(r, BGRAPixelTransparent, BGRA(0, 255, 0, round(200*colorOscillation)),
dmDrawWithTransparency);
if not map.ballPositionDefined then
begin
map.ballPosition := pointF(nx,ny);
map.ballPositionDefined := true;
end;
end
else if map.mapProperty[n] = map.mapSolidChar then
bitmap.Rectangle(r, BGRAPixelTransparent, BGRA(0, 0, 0, 100),
dmDrawWithTransparency)
else
bitmap.Rectangle(r, BGRAPixelTransparent, BGRA(255, 255, 255, 200),
dmDrawWithTransparency);
if map.blockWidth > map.blockHeight then
bitmap.FontHeight := map.blockHeight
else
bitmap.FontHeight := map.blockWidth;
if map.mapGraphics[n] = 'X' then
bitmap.Rectangle(r, BGRAPixelTransparent, BGRA(255, 0, 0, round(200*colorOscillation)),
dmDrawWithTransparency);
// end to draw here
end;
Inc(n);
inc(r.Left,map.blockWidth);
inc(r.right,map.blockWidth);
end;
inc(r.top,map.blockHeight);
inc(r.bottom,map.blockHeight);
end;
bitmap.ClipRect := oldClip;
end;
function ClickMapCell(var map: TGameMap; pos: TPoint): boolean;
var
n, nx, ny: integer;
r: TRect;
begin
Result := False;
n := 1;
for ny := 0 to map.mapHeight - 1 do
begin
for nx := 0 to map.mapWidth - 1 do
begin
r.Left := map.blockWidth * nx;
r.Top := map.blockHeight * ny;
r.Right := r.Left + map.blockWidth;
r.Bottom := r.Top + map.blockHeight;
if (pos.x >= r.Left) and (pos.x <= r.Right) and (pos.y >= r.Top) and
(pos.y <= r.Bottom) then
begin
// begin here
// mapKeyChar is n
if (map.mapProperty[n] = map.mapKeyChar) then
exit;
// mapSolidChar is n
if (map.mapProperty[n] = map.mapSolidChar) then
exit;
// mapKeyChar is on the left, move to n
if (nx <> 0) then
if (map.mapProperty[n - 1] = map.mapKeyChar) then
begin
map.mapProperty[n - 1] := map.mapProperty[n];
map.mapProperty[n] := map.mapKeyChar;
end;
// mapKeyChar is on the right, move to n
if (nx <> map.mapWidth - 1) then
if (map.mapProperty[n + 1] = map.mapKeyChar) then
begin
map.mapProperty[n + 1] := map.mapProperty[n];
map.mapProperty[n] := map.mapKeyChar;
end;
// mapKeyChar is on top, move to n
if (map.mapProperty[n - map.mapWidth] = map.mapKeyChar) then
begin
map.mapProperty[n - map.mapWidth] := map.mapProperty[n];
map.mapProperty[n] := map.mapKeyChar;
end;
// mapKeyChar is on bottom, move to n
if (map.mapProperty[n + map.mapWidth] = map.mapKeyChar) then
begin
map.mapProperty[n + map.mapWidth] := map.mapProperty[n];
map.mapProperty[n] := map.mapKeyChar;
end;
// end here
Result := True;
exit;
end;
Inc(n);
end;
end;
end;
procedure MouseDownMap(var map: TGameMap; pos: TPoint);
begin
map.mouseDown := true;
map.mousePos := pointf(pos.x/map.blockWidth-0.5,pos.y/map.blockHeight-0.5);
end;
procedure MouseUpMap(var map: TGameMap; pos: TPoint);
begin
map.mouseDown := false;
map.mousePos := pointf(pos.x/map.blockWidth-0.5,pos.y/map.blockHeight-0.5);
end;
function GetGameKey(var Key: word; invertLR, invertUD: boolean): TGameKey;
begin
if (Key = L1) or (Key = L2) then
Result := gkLeft;
if (Key = R1) or (Key = R2) then
Result := gkRight;
if (Key = U1) or (Key = U2) then
Result := gkUp;
if (Key = D1) or (Key = D2) then
Result := gkDown;
if invertLR then
case Result of
gkLeft: Result := gkRight;
gkRight: Result := gkLeft;
end;
if invertUD then
case Result of
gkUp: Result := gkDown;
gkDown: Result := gkUp;
end;
end;
procedure KeyDownMap(var map: TGameMap; var Key: word);
begin
map.mapKeysDown := map.mapKeysDown + [GetGameKey(Key,false,false)];
end;
procedure KeyUpMap(var map: TGameMap; var Key: word);
begin
map.mapKeysDown := map.mapKeysDown - [GetGameKey(Key,false,false)];
end;
function KeyPressMapCell(var map: TGameMap; Key: TGameKey): boolean;
var
n, nx, ny: integer;
begin
Result := False;
n := 1;
for ny := 0 to map.mapHeight - 1 do
begin
for nx := 0 to map.mapWidth - 1 do
begin
if map.mapProperty[n] = map.mapKeyChar then
begin
// begin here
case Key of
gkLeft:
if (nx <> 0) and (map.mapProperty[n - 1] <> map.mapSolidChar) then
begin
map.mapProperty[n] := map.mapProperty[n - 1];
map.mapProperty[n - 1] := map.mapKeyChar;
Result := True;
end;
gkRight:
if (nx <> map.mapWidth - 1) and (map.mapProperty[n + 1] <>
map.mapSolidChar) then
begin
map.mapProperty[n] := map.mapProperty[n + 1];
map.mapProperty[n + 1] := map.mapKeyChar;
Result := True;
end;
gkUp:
if (ny <> 0) and (map.mapProperty[n - map.mapWidth] <> map.mapSolidChar) then
begin
map.mapProperty[n] := map.mapProperty[n - map.mapWidth];
map.mapProperty[n - map.mapWidth] := map.mapKeyChar;
Result := True;
end;
gkDown:
if (ny <> map.mapHeight - 1) and
(map.mapProperty[n + map.mapHeight] <> map.mapSolidChar) then
begin
map.mapProperty[n] := map.mapProperty[n + map.mapWidth];
map.mapProperty[n + map.mapWidth] := map.mapKeyChar;
Result := True;
end;
end;
// end here
if Result then
exit;
end;
Inc(n);
end;
end;
end;
function KeyPressMapCell(var map: TGameMap; var Key: word): boolean;
begin
Result := KeyPressMapCell(map, GetGameKey(Key, False, False));
if result then Key := 0;
end;
function IsMapSolved(var map: TGameMap): boolean;
begin
Result := map.mapProperty = map.mapSolve;
end;
procedure ToggleFullScreen(Form: TForm; ARect: TRect);
begin
Form.SetBounds(ARect.Left, ARect.Top, ARect.Right, ARect.Bottom);
if Form.BorderStyle <> bsNone then
begin
Form.BorderStyle := bsNone;
Form.WindowState := wsMaximized;
end
else
begin
Form.BorderStyle := bsSizeable;
Form.WindowState := wsMaximized;
end;
end;
procedure CenterControl(Control: TControl);
begin
if not Control.HasParent then
Exit;
Control.SetBounds(
Round((Control.Parent.Width - Control.Width) div 2),
Round((Control.Parent.Height - Control.Height) div 2),
Control.Width, Control.Height);
end;
function CalculateAspectRatioH(const W1, H1, W2: integer): integer;
begin
Result := Round(H1 / W1 * W2);
end;
function CalculateAspectRatioW(const W1, H1, H2: integer): integer;
begin
Result := Round(W1 / H1 * H2);
end;
function CalculateDestRect(ImageW, ImageH, DestW, DestH: integer;
Stretch, Proportional, Center: boolean): TRect;
var
w: integer;
h: integer;
begin
// Stretch or Proportional when Image (Width or Height) is bigger than Destination
if Stretch or (Proportional and ((ImageW > DestW) or (ImageH > DestH))) then
begin
// Proportional when Image (Width or Height) is bigger than 0
if Proportional and (ImageW > 0) and (ImageH > 0) then
begin
w := DestW;
h := CalculateAspectRatioH(ImageW, ImageH, DestW);
if h > DestH then
begin
h := DestH;
w := CalculateAspectRatioW(ImageW, ImageH, DestH);
end;
ImageW := w;
ImageH := h;
end
// Stretch not Proportional or when Image (Width or Height) is 0
else
begin
ImageW := DestW;
ImageH := DestH;
end;
end;
Result := Rect(0, 0, ImageW, ImageH);
// Center: Destination (Width or Height) - Image divided by 2
if Center then
begin
Result.Left := Round((DestW - ImageW) div 2);
Result.Top := Round((DestH - ImageH) div 2);
end;
end;
procedure HighDPI(FromDPI: integer);
var
i: integer;
begin
if Screen.PixelsPerInch = FromDPI then
exit;
for i := 0 to Screen.FormCount - 1 do
ScaleDPI(Screen.Forms[i], FromDPI);
end;
procedure ScaleDPI(Control: TControl; FromDPI: integer);
var
i: integer;
WinControl: TWinControl;
begin
if Screen.PixelsPerInch = FromDPI then
exit;
with Control do
begin
Left := ScaleX(Left, FromDPI);
Top := ScaleY(Top, FromDPI);
Width := ScaleX(Width, FromDPI);
Height := ScaleY(Height, FromDPI);
end;
if Control is TWinControl then
begin
WinControl := TWinControl(Control);
if WinControl.ControlCount = 0 then
exit;
with WinControl.ChildSizing do
begin
HorizontalSpacing := ScaleX(HorizontalSpacing, FromDPI);
LeftRightSpacing := ScaleX(LeftRightSpacing, FromDPI);
TopBottomSpacing := ScaleY(TopBottomSpacing, FromDPI);
VerticalSpacing := ScaleY(VerticalSpacing, FromDPI);
end;
for i := 0 to WinControl.ControlCount - 1 do
ScaleDPI(WinControl.Controls[i], FromDPI);
end;
end;
procedure ScaleAspectRatio(Control: TControl; OriginalParentW, OriginalParentH: integer);
var
l, t, w, h: integer;
begin
l := MulDiv(Control.Left, Control.Parent.Width, OriginalParentW);
t := MulDiv(Control.Top, Control.Parent.Height, OriginalParentH);
w := MulDiv(Control.Width, Control.Parent.Width, OriginalParentW);
h := MulDiv(Control.Height, Control.Parent.Height, OriginalParentH);
Control.SetBounds(l, t, w, h);
end;
procedure ScaleAspectRatio(Control: TControl; DestW, DestH: integer;
Stretch, Proportional, Center: boolean);
var
i: integer;
r: TRect;
WinControl: TWinControl;
w, h: integer;
begin
if Control is TWinControl then
begin
WinControl := TWinControl(Control);
w := WinControl.Width;
h := WinControl.Height;
r := CalculateDestRect(WinControl.Width, WinControl.Height, DestW,
DestH, Stretch, Proportional, Center);
WinControl.SetBounds(r.Left, r.Top, r.Right, r.Bottom);
if WinControl.ControlCount = 0 then
exit;
for i := 0 to WinControl.ControlCount - 1 do
ScaleAspectRatio(WinControl.Controls[i], w, h);
end;
end;
end.