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.