source: trunk/Engine.pas

Last change on this file was 80, checked in by chronos, 7 weeks ago
  • Modified: Windows installer for version 1.0.0.
File size: 51.6 KB
Line 
1unit Engine;
2
3interface
4
5uses
6 Dialogs, Classes, SysUtils, Graphics, SpecializedMatrix, RegistryEx, Common,
7 IntfGraphics, FPImage, LCLType, SpecializedBitmap, GraphType, Math, Rectangle,
8 Syncobjs, Threading, Forms, DateUtils, AudioSystem, Generics.Collections,
9 World, Matter, Sound;
10
11const
12 MaxBulletCount = 10;
13 EnergyDecreaseDig = 0;
14 EnergyDecreaseMove = 0.00025; // ~2.5 minutes of movement
15 EnergyDecreaseShoot = 0.005; // ~20 seconds of shooting
16 EnergyDecreaseOutside = 0.00003; // ~5 minutes of live
17 EnergyIncreaseHome = 0.001;
18 ShieldIncreaseHome = 0.001;
19 ShieldDecreaseHit = 0.15;
20 ExplosionBulletCount = 100;
21 ExplosionRange = 20;
22 ExplosionBulletMaxSpeed = 0.5;
23 ExplosionBulletMinSpeed = 0.2;
24 BulletExplosionRange = 4;
25 ShootDelay = 0.1; // seconds
26 DigDelay = 0.2; // seconds
27 ShootDigDelay = 0.05; // seconds
28 MoveDelay = 0.05; // seconds
29 PlayerFrameWidth = 80;
30 PlayerFrameHeight = 80;
31 PlayerHouseSize = 29;
32 PlayerHouseDoorSize = 7;
33 ExplosionDelay = 2;
34 NewRoundDelay = 2;
35 clTuna = $5555ff;
36 clPurple = $aa00aa;
37 clOrange = $0055aa;
38 clDarkOrange = $0000aa;
39 clDarkGreen = $00aa00;
40 clCyan = $ffff00;
41
42type
43 TEngine = class;
44 TPlayer = class;
45
46 TRealPoint = record
47 X, Y: Real;
48 end;
49
50 TPlayerKeys = record
51 Left: Word;
52 Right: Word;
53 Up: Word;
54 Down: Word;
55 Shoot: Word;
56 end;
57
58 { TBullet }
59
60 TBullet = class
61 Player: TPlayer;
62 Position: TRealPoint;
63 PositionTail: TRealPoint;
64 Direction: TRealPoint;
65 MaxDistance: Integer;
66 Distance: Real;
67 StopByDirt: Boolean;
68 CanKill: Boolean;
69 constructor Create;
70 end;
71
72 { TBullets }
73
74 TBullets = class(TObjectList<TBullet>)
75 procedure HideAll;
76 end;
77
78 { TTank }
79
80 TTank = class
81 Image: TMatrixByte;
82 Mask: TMatrixByte;
83 procedure Assign(Source: TTank);
84 constructor Create;
85 destructor Destroy; override;
86 end;
87
88 { TTanks }
89
90 TTanks = class(TObjectList<TTank>)
91 procedure Assign(Source: TTanks);
92 end;
93
94 TColisionState = record
95 Blocking: Boolean;
96 Diggable: Boolean;
97 end;
98
99 { TPlayer }
100
101 TPlayer = class
102 private
103 FExploded: Boolean;
104 NewDirection: Integer;
105 NewPosition: TPoint;
106 Dig: Boolean;
107 LastPos: TPoint;
108 ExplosionPending: Boolean;
109 ExplosionTime: TDateTime;
110 procedure SetExploded(const AValue: Boolean);
111 function ShowTankProc(Item1, Item2: Byte): Byte;
112 function HideTankProc(Item1, Item2: Byte): Byte;
113 function DigProc(Item1, Item2: Byte): Byte;
114 public
115 Name: string;
116 Color1: TColor;
117 Color2: TColor;
118 Id: Integer;
119 Enabled: Boolean;
120 Engine: TEngine;
121 Position: TPoint;
122 StartPosition: TPoint;
123 Score: Integer;
124 Direction: Integer;
125 ScreenFrame: TRectangle;
126 Keys: TPlayerKeys;
127 Tanks: TTanks;
128 Bullets: TBullets;
129 LastShootTime: TDateTime;
130 LastDigTime: TDateTime;
131 LastMoveTime: TDateTime;
132 Energy: Real;
133 LastEnergy: Real;
134 Shield: Real;
135 LastShield: Real;
136 House: TRectangle;
137 ShotsCount: Integer;
138 ShotsHit: Integer;
139 MetersDug: Integer;
140 MetersTravelled: Integer;
141 procedure ResetTank;
142 procedure Init;
143 procedure Explosion(Position: TPoint; Distance: Integer);
144 procedure Control;
145 function CheckNewDestination: Boolean;
146 procedure Tick;
147 procedure Paint;
148 procedure PlaceHouse;
149 function CheckColision: TColisionState;
150 procedure ShowTank;
151 procedure HideTank;
152 procedure InitTanks;
153 constructor Create;
154 destructor Destroy; override;
155 procedure Assign(Source: TPlayer);
156 procedure LoadFromRegistry(Context: TRegistryContext);
157 procedure SaveToRegistry(Context: TRegistryContext);
158 property Exploded: Boolean read FExploded write SetExploded;
159 end;
160
161 { TPlayers }
162
163 TPlayers = class(TObjectList<TPlayer>)
164 Engine: TEngine;
165 function AddNew: TPlayer;
166 procedure Assign(Players: TPlayers);
167 procedure LoadFromRegistry(Context: TRegistryContext);
168 procedure SaveToRegistry(Context: TRegistryContext);
169 function GetAliveCount: Integer;
170 function GetWinner: TPlayer;
171 end;
172
173 { TKeyboard }
174
175 TKeyboard = class
176 KeyState: array[0..High(Word)] of Boolean;
177 procedure ClearAll;
178 end;
179
180 { TSystemThread }
181
182 TSystemThread = class(TListedThread)
183 Engine: TEngine;
184 procedure Execute; override;
185 end;
186
187 { TDrawThread }
188
189 TDrawThread = class(TListedThread)
190 private
191 procedure DrawSync;
192 public
193 Engine: TEngine;
194 procedure Execute; override;
195 end;
196
197 TCanvasMethod = procedure (Canvas: TCanvas) of object;
198
199 TGameState = (gsMenu, gsGame, gsNewRound, gsMap, gsInformation,
200 gsInstructions);
201
202 { TEngine }
203
204 TEngine = class(TComponent)
205 private
206 FActive: Boolean;
207 FBitmap: TBitmap;
208 FBitmapLock: TCriticalSection;
209 FOnGameEnd: TNotifyEvent;
210 FRedrawPending: Boolean;
211 FBitmapLower: TBitmapTColor;
212 FDrawThread: TDrawThread;
213 FState: TGameState;
214 FSystemThread: TSystemThread;
215 FClearBackground: Boolean;
216 FStateTime: TDateTime;
217 FShowMenuStats: Boolean;
218 procedure InitDigMasks;
219 procedure SetActive(const AValue: Boolean);
220 procedure SetBitmap(const AValue: TBitmap);
221 function IsInsideHouses(Pos: TPoint): Boolean;
222 procedure InitPlayers;
223 procedure CheckGameEnd;
224 procedure ClearBackground;
225 procedure DrawMenu;
226 procedure DrawStats;
227 procedure DrawGamePrepare(Thread: TVirtualThread);
228 procedure DrawGame;
229 procedure DrawInformation;
230 procedure DrawInstructions;
231 procedure DrawNewRound;
232 procedure DrawMap;
233 procedure SetState(AValue: TGameState);
234 function TextOutWordWrap(Canvas: TCanvas; X, Y: Integer; Text: string; Width: Integer): Integer;
235 public
236 ThreadErrorMessage: string;
237 Keyboard: TKeyboard;
238 World: TWorld;
239 PlayerPool: TPlayers;
240 Players: TPlayers;
241 DigMasks: TObjectList<TMatrixByte>;
242 Lock: TCriticalSection;
243 CurrentRound: Integer;
244 MaxRound: Integer;
245 AudioShot: TSound;
246 AudioExplode: TSound;
247 DrawDuration: TDatetime;
248 constructor Create(AOwner: TComponent); override;
249 destructor Destroy; override;
250 procedure ResizePlayerFrames;
251 procedure Tick;
252 procedure Redraw;
253 procedure Draw;
254 procedure DrawThread(Thread: TVirtualThread);
255 procedure NewGame;
256 procedure NewRound;
257 procedure KeyUp(Key: Word);
258 procedure KeyDown(Key: Word);
259 procedure LoadFromRegistry(Context: TRegistryContext);
260 procedure SaveToRegistry(Context: TRegistryContext);
261 procedure InitPlayerPool;
262 property Bitmap: TBitmap read FBitmap write SetBitmap;
263 property Active: Boolean read FActive write SetActive;
264 property State: TGameState read FState write SetState;
265 property OnGameEnd: TNotifyEvent read FOnGameEnd write FOnGameEnd;
266 end;
267
268const
269 DirectionToDelta: array[0..7] of TPoint =
270 ((X: 0; Y: -1), (X: 1; Y: -1), (X: 1; Y: 0), (X: 1; Y: 1),
271 (X: 0; Y: 1), (X: -1; Y: 1), (X: -1; Y: 0), (X: -1; Y: -1));
272
273
274implementation
275
276uses
277 Platform, FastPixel;
278
279resourcestring
280 SRound = 'Round';
281 SPressEsc = 'Press ESC to continue';
282 SStartGame = 'Start game';
283 SInstructions = 'Instructions';
284 SInstructionsDetails = 'Tunneler is a two player game. The objective of the game is to be the first to win three rounds. ' +
285 'A round continues until one tank blows up (from being shot or simply running out of energy).';
286 SInstructionsDetails2 = 'The display: Each player has a view screen and an instrument panel with two meters. ' +
287 'The view screen shows your surroundings from above (about 1% of the entire "worlds"). ' +
288 'The two meters indicate the player''s energy and shield condition.';
289 SInstructionsDetails3 = 'The Game: Players can move in one of 8 directions. ' +
290 'Movement in tunnels is three times as fast as normal digging. ' +
291 'Fast digging can be accomplished by firing the tank''s cannon while moving. ' +
292 'Various actions use up different amounts of energy : moving costs some energy, digging costs more, and shotting costs the most. ' +
293 'Shields are damaged when hit by the other player''s cannon. ' +
294 'Players can refuel at either base but can repair their shields only at their own bases.';
295 SInstructionsDetails4 = 'Controls: \n ' +
296 'Blue: W - up, A - left, S - down, D - right, E - shoot \n ' +
297 'Green: arrows key, CTRL - shoot';
298 SInformation = 'Information';
299 SInformationDetails = 'Tunneler is written from scratch in Lazarus/FPC. ' +
300 'It is designed to be as similar as the original MS-DOS game (https://tunneler.org/) but also with some modern extensions. ' +
301 'The program uses standard canvas drawing with combination of low resolution fast pixel matrix. ' +
302 'It supports High DPI scaling. It supports up to eight players.';
303 SInformationDetails2 = 'This is a public domain open source program: feel free to copy it for friends and study the source code. \n ' +
304 'Homepage: https://app.zdechov.net/Tunneler';
305 SWorldReady = 'World ready';
306 SExit = 'Exit';
307 SStatistics = 'Statistics';
308 SShotsFired = 'Shots fired';
309 SShotsHit = 'Shots hit';
310 SPercentHit = 'Percent hit';
311 SMetersDug = 'Meters dug';
312 SMetersTravelled = 'Meters travelled';
313 SWinnerIs = 'The winner is';
314 SGreen = 'Green';
315 SBlue = 'Blue';
316 SRed = 'Red';
317 SPink = 'Pink';
318 SCyan = 'Cyan';
319 SYellow = 'Yellow';
320 SOrange = 'Orange';
321 SGray = 'Gray';
322
323{ TBullets }
324
325procedure TBullets.HideAll;
326var
327 I: Integer;
328begin
329 for I := 0 to Count - 1 do
330 with Items[I] do begin
331 Player.Engine.World.Surface.ItemsXY[Trunc(Position.X), Trunc(Position.Y)] := Byte(miSpace);
332 Player.Engine.World.Surface.ItemsXY[Trunc(PositionTail.X), Trunc(PositionTail.Y)] := Byte(miSpace);
333 end;
334end;
335
336{ TTanks }
337
338procedure TTanks.Assign(Source: TTanks);
339var
340 I: Integer;
341begin
342 Clear;
343 Count := Source.Count;
344 for I := 0 to Count - 1 do begin
345 Items[I] := TTank.Create;
346 Items[I].Assign(Source.Items[I]);
347 end;
348end;
349
350{ TPlayers }
351
352function TPlayers.AddNew: TPlayer;
353begin
354 Result := TPlayer.Create;
355 Add(Result);
356end;
357
358procedure TPlayers.Assign(Players: TPlayers);
359var
360 I: Integer;
361begin
362 while Count < Players.Count do Add(TPlayer.Create);
363 while Count > Players.Count do Delete(Count - 1);
364 for I := 0 to Count - 1 do
365 Items[I].Assign(Players[I]);
366end;
367
368procedure TPlayers.LoadFromRegistry(Context: TRegistryContext);
369var
370 Player: TPlayer;
371begin
372 for Player in Self do begin
373 Player.LoadFromRegistry(TRegistryContext.Create(Context.RootKey, Context.Key + '\' + IntToStr(Player.Id)));
374 end;
375end;
376
377procedure TPlayers.SaveToRegistry(Context: TRegistryContext);
378var
379 Player: TPlayer;
380begin
381 for Player in Self do begin
382 Player.SaveToRegistry(TRegistryContext.Create(Context.RootKey, Context.Key + '\' + IntToStr(Player.Id)));
383 end;
384end;
385
386function TPlayers.GetAliveCount: Integer;
387var
388 I: Integer;
389begin
390 Result := 0;
391 for I := 0 to Count - 1 do
392 with Items[I] do
393 if not Exploded then Inc(Result);
394end;
395
396function TPlayers.GetWinner: TPlayer;
397var
398 I: Integer;
399 TopScore: Integer;
400begin
401 Result := nil;
402 TopScore := 0;
403 for I := 0 to Count - 1 do
404 with Items[I] do
405 if Enabled and (Score > TopScore) then begin
406 TopScore := Score;
407 Result := Items[I];
408 end;
409end;
410
411{ TSystemThread }
412
413procedure TSystemThread.Execute;
414begin
415 try
416 repeat
417 Engine.Tick;
418 Sleep(10);
419 until Terminated;
420 except
421 on E: Exception do
422 Engine.ThreadErrorMessage := E.Message;
423 end;
424end;
425
426{ TDrawThread }
427
428procedure TDrawThread.DrawSync;
429begin
430 with Engine do
431 if Assigned(Bitmap) then begin
432 Lock.Acquire;
433 Bitmap.BeginUpdate(True);
434 try
435 Draw;
436 finally
437 Bitmap.EndUpdate;
438 Lock.Release;
439 end;
440 end;
441end;
442
443procedure TDrawThread.Execute;
444begin
445 try
446 repeat
447 Engine.DrawThread(Self);
448 if not Terminated then Synchronize(DrawSync);
449 Sleep(50);
450 until Terminated;
451 except
452 on E: Exception do
453 Engine.ThreadErrorMessage := E.Message;
454 end;
455end;
456
457{ TKeyboard }
458
459procedure TKeyboard.ClearAll;
460var
461 I: Integer;
462begin
463 for I := 0 to High(KeyState) do
464 KeyState[I] := False;
465end;
466
467{ TBullet }
468
469constructor TBullet.Create;
470begin
471 MaxDistance := -1;
472 Distance := 0;
473 CanKill := True;
474end;
475
476{ TTank }
477
478procedure TTank.Assign(Source: TTank);
479begin
480 Image.Assign(Source.Image);
481 Mask.Assign(Source.Mask);
482end;
483
484constructor TTank.Create;
485begin
486 Mask := TMatrixByte.Create;
487 Image := TMatrixByte.Create;
488end;
489
490destructor TTank.Destroy;
491begin
492 FreeAndNil(Mask);
493 FreeAndNil(Image);
494 inherited;
495end;
496
497{ TPlayer }
498
499procedure TPlayer.Control;
500var
501 Delta: TPoint;
502 NewBullet: TBullet;
503begin
504 if Exploded then Exit;
505
506 Delta.X := 0;
507 Delta.Y := 0;
508 if Engine.KeyBoard.KeyState[Ord(Keys.Down)] then Delta.Y := Delta.Y + 1;
509 if Engine.KeyBoard.KeyState[Ord(Keys.Up)] then Delta.Y := Delta.Y - 1;
510 if Engine.KeyBoard.KeyState[Ord(Keys.Right)] then Delta.X := Delta.X + 1;
511 if Engine.KeyBoard.KeyState[Ord(Keys.Left)] then Delta.X := Delta.X - 1;
512
513 NewDirection := Direction;
514 if (Delta.X <> 0) or (Delta.Y <> 0) then begin
515 if (Delta.X = 0) and (Delta.Y = -1) then NewDirection := 0
516 else if (Delta.X = 1) and (Delta.Y = -1) then NewDirection := 1
517 else if (Delta.X = 1) and (Delta.Y = 0) then NewDirection := 2
518 else if (Delta.X = 1) and (Delta.Y = 1) then NewDirection := 3
519 else if (Delta.X = 0) and (Delta.Y = 1) then NewDirection := 4
520 else if (Delta.X = -1) and (Delta.Y = 1) then NewDirection := 5
521 else if (Delta.X = -1) and (Delta.Y = 0) then NewDirection := 6
522 else if (Delta.X = -1) and (Delta.Y = -1) then NewDirection := 7;
523
524 if NewDirection = Direction then begin
525 NewPosition := Point(Position.X + Delta.X, Position.Y + Delta.Y);
526 CheckNewDestination;
527 end else begin
528 NewPosition := Position;
529 if not CheckNewDestination then begin
530 // if direction is changed then try to check movement plus one step
531 // to prevent tank block
532 NewPosition := Point(Position.X + Delta.X, Position.Y + Delta.Y);
533 CheckNewDestination;
534 end;
535 end;
536 end;
537
538 if Engine.KeyBoard.KeyState[Ord(Keys.Shoot)] then
539 if (Bullets.Count < MaxBulletCount) and
540 ((Now - LastShootTime) > ShootDelay * OneSecond) then begin
541 Inc(ShotsCount);
542 NewBullet := TBullet.Create;
543 NewBullet.Player := Self;
544 NewBullet.Position.X := Position.X + DirectionToDelta[Direction].X * 3;
545 NewBullet.Position.Y := Position.Y + DirectionToDelta[Direction].Y * 3;
546 NewBullet.Direction.X := DirectionToDelta[Direction].X;
547 NewBullet.Direction.Y := DirectionToDelta[Direction].Y;
548 NewBullet.StopByDirt := True;
549 Bullets.Add(NewBullet);
550 LastShootTime := Now;
551
552 Energy := Energy - EnergyDecreaseShoot;
553 if Energy < 0 then Energy := 0;
554
555 // Do not imedietelly clear dirt by bullet in front of tank
556 //with Engine, World do
557 //if TMatter(Matter[Surface.Items[Position]]).Diggable ;
558 Engine.AudioShot.Play;
559 end;
560end;
561
562function TPlayer.CheckNewDestination: Boolean;
563var
564 ColisionState: TColisionState;
565begin
566 Result := False;
567 HideTank;
568 ColisionState := CheckColision;
569 if not ColisionState.Blocking then begin
570 if ColisionState.Diggable then begin
571 if (Engine.KeyBoard.KeyState[Ord(Keys.Shoot)] and
572 ((Now - LastDigTime) > ShootDigDelay * OneSecond)) or
573 (not Engine.KeyBoard.KeyState[Ord(Keys.Shoot)] and
574 ((Now - LastDigTime) > DigDelay * OneSecond)) then begin
575 Dig := not Dig;
576 with Engine, World do
577 Surface.Merge(Surface.CreateIndex(
578 Position.X - TMatrixByte(DigMasks[Direction]).Count.X div 2,
579 Position.Y - TMatrixByte(DigMasks[Direction]).Count.Y div 2),
580 TMatrixByte(DigMasks[Direction]), DigProc);
581 Energy := Energy - EnergyDecreaseDig;
582 if Energy < 0 then Energy := 0;
583 Engine.Redraw;
584 LastDigTime := Now;
585 Direction := NewDirection;
586 Result := True;
587 Inc(MetersDug);
588 end;
589 end else begin
590 if ((Now - LastMoveTime) > MoveDelay * OneSecond) then begin
591 Position := NewPosition;
592 Direction := NewDirection;
593 Result := True;
594 Engine.Redraw;
595 LastMoveTime := Now;
596 Energy := Energy - EnergyDecreaseMove;
597 Inc(MetersTravelled);
598 end;
599 end;
600 end;
601 ShowTank;
602end;
603
604procedure TPlayer.Tick;
605var
606 I: Integer;
607 P: Integer;
608 Pos: TPoint;
609begin
610 // Check energy
611 if not ExplosionPending then begin
612 if not Engine.IsInsideHouses(Position) then begin
613 Energy := Energy - EnergyDecreaseOutside;
614 end else begin
615 if not Exploded then
616 Energy := Energy + EnergyIncreaseHome;
617 if Energy > 1 then Energy := 1;
618 end;
619 if Energy <= 0 then begin
620 Energy := 0;
621 Explosion(Position, ExplosionRange);
622 Exploded := True;
623 ExplosionPending := True;
624 ExplosionTime := Now;
625 Engine.AudioExplode.Play;
626 end;
627 end;
628 if ExplosionPending and (SecondOf(Now - ExplosionTime) > ExplosionDelay) then begin
629 ExplosionPending := False;
630 Engine.CheckGameEnd;
631 end;
632 if LastEnergy <> Energy then begin
633 LastEnergy := Energy;
634 Engine.Redraw;
635 end;
636
637 // Check shield
638 if House.IsInside(Position) then begin
639 if not Exploded then
640 Shield := Shield + ShieldIncreaseHome;
641 if Shield > 1 then Shield := 1;
642 end;
643 if LastShield <> Shield then begin
644 LastShield := Shield;
645 Engine.Redraw;
646 end;
647 if (Shield <= 0) and not Exploded then begin
648 Shield := 0;
649 Explosion(Position, ExplosionRange);
650 Exploded := True;
651 Engine.AudioExplode.Play;
652 end;
653
654 // Bullet movement
655 for I := Bullets.Count - 1 downto 0 do
656 with Bullets[I], Engine.World.Surface do begin
657 Pos := Point(Trunc(Position.X), Trunc(Position.Y));
658 if (ItemsXY[LastPos.X, LastPos.Y] = Byte(miBullet1)) or
659 (ItemsXY[LastPos.X, LastPos.Y] = Byte(miBullet2)) then
660 ItemsXY[LastPos.X, LastPos.Y] := Byte(miSpace);
661 LastPos := Pos;
662 P := Trunc(Direction.Y);
663
664 PositionTail := Position;
665 Position.X := Position.X + Direction.X;
666 Position.Y := Position.Y + Direction.Y;
667 Distance := Distance + Sqrt(Sqr(Direction.X) + Sqr(Direction.Y));
668 //ShowMessage(FloatToStr(Distance));
669 if (Distance > MaxDistance) and (MaxDistance >= 0) then begin
670 Bullets.Delete(I);
671 Engine.Redraw;
672 Continue;
673 end;
674
675 Pos := Point(Trunc(Position.X), Trunc(Position.Y));
676
677 if (ItemsXY[Pos.X, Pos.Y] <> Byte(miSpace)) and
678 (ItemsXY[Pos.X, Pos.Y] <> Byte(miBullet1)) and
679 (ItemsXY[Pos.X, Pos.Y] <> Byte(miBullet2)) then begin
680 if (ItemsXY[Pos.X, Pos.Y] = Byte(miDirt1)) or
681 (ItemsXY[Pos.X, Pos.Y] = Byte(miDirt2)) then begin
682 //ItemsXY[Pos.X, Pos.Y] := Byte(miSpace);
683 if StopByDirt then begin
684 Explosion(LastPos, BulletExplosionRange);
685 Bullets.Delete(I);
686 Engine.Redraw;
687 Continue;
688 end;
689 end else begin
690 for P := 0 to Engine.Players.Count - 1 do
691 with Engine.Players[P] do
692 if (Self.Id <> P) and
693 (Engine.World.Matters[ItemsXY[Pos.X, Pos.Y]].Kind = mkTankBody) and
694 (Engine.World.Matters[ItemsXY[Pos.X, Pos.Y]].Player = P) and CanKill then begin
695 Shield := Shield - ShieldDecreaseHit;
696 Inc(Self.ShotsHit);
697 end;
698 if StopByDirt then Explosion(LastPos, BulletExplosionRange);
699 Bullets.Delete(I);
700 Engine.Redraw;
701 Continue;
702 end;
703 end;
704
705 // Max position limit checking
706 with Engine.World.Surface do
707 if (Pos.X >= Count.X) or (Pos.X < 0) or
708 (Pos.Y >= Count.Y) or (Pos.Y < 0) then begin
709 Bullets.Delete(I);
710 Engine.Redraw;
711 Continue;
712 end;
713 ItemsXY[Pos.X, Pos.Y] := Byte(miBullet1);
714 ItemsXY[LastPos.X, LastPos.Y] := Byte(miBullet2);
715 Engine.Redraw;
716 end;
717
718 if (Engine.State = gsGame) and not Exploded then ShowTank;
719end;
720
721procedure TPlayer.Paint;
722var
723 X, Y: Integer;
724 XX, YY: Integer;
725 I: Integer;
726 B: TColor;
727begin
728 with Engine.FBitmapLower do begin
729 Fill(CreateIndex(ScreenFrame.Left, ScreenFrame.Top),
730 CreateIndex(ScreenFrame.Width, ScreenFrame.Height),
731 Engine.World.Matters[Integer(miRock)].Color);
732
733 with Engine.World do
734 for Y := ScreenFrame.Top to ScreenFrame.Bottom - 1 do
735 for X := ScreenFrame.Left to ScreenFrame.Right - 1 do begin
736 XX := X - ScreenFrame.Left - ((ScreenFrame.Right - ScreenFrame.Left) div 2) + Position.X;
737 YY := Y - ScreenFrame.Top - ((ScreenFrame.Bottom - ScreenFrame.Top) div 2) + Position.Y;
738 if (YY >= 0) and (YY < Surface.Count.Y) and
739 (XX >= 0) and (XX < Surface.Count.X) then begin
740 B := Engine.World.Matters[Surface.ItemsXY[XX, YY]].Color;
741 ItemsXY[X, Y] := B;
742 end;
743 end;
744
745 // Energy bar
746 for I := 1 to ScreenFrame.Width - 2 do
747 if Energy < I / (ScreenFrame.Width - 2) then
748 ItemsXY[ScreenFrame.Left + I, ScreenFrame.Bottom - 2] := clBlack
749 else ItemsXY[ScreenFrame.Left + I, ScreenFrame.Bottom - 2] := clYellow;
750
751 // Shield bar
752 for I := 1 to ScreenFrame.Width - 2 do
753 if Shield < I / (ScreenFrame.Width - 2) then
754 ItemsXY[ScreenFrame.Left + I, ScreenFrame.Bottom - 1] := clBlack
755 else ItemsXY[ScreenFrame.Left + I, ScreenFrame.Bottom - 1] := clAqua;
756 end;
757end;
758
759procedure TPlayer.PlaceHouse;
760var
761 X, Y: Integer;
762 Matter: Byte;
763begin
764 House.AsTRect := Rect(Position.X - PlayerHouseSize div 2, Position.Y - PlayerHouseSize div 2,
765 Position.X + PlayerHouseSize div 2, Position.Y + PlayerHouseSize div 2);
766 for Y := 0 to PlayerHouseSize - 1 do
767 for X := 0 to PlayerHouseSize - 1 do begin
768 if ((Y = 0) or (Y = (PlayerHouseSize - 1)) or (X = 0) or (X = (PlayerHouseSize - 1))) and
769 not (((Y = 0) or (Y = (PlayerHouseSize - 1))) and
770 (X >= ((PlayerHouseSize - PlayerHouseDoorSize) div 2)) and
771 (X <= ((PlayerHouseSize - 1 + PlayerHouseDoorSize) div 2)))
772 then Matter := Byte(miPlayer1Home) + Id * 4
773 else Matter := Byte(miSpace);
774 Engine.World.Surface.ItemsXY[House.Left + X,
775 House.Top + Y] := Matter;
776 end;
777end;
778
779function TPlayer.CheckColision: TColisionState;
780var
781 X, Y: Integer;
782 XX, YY: Integer;
783begin
784 Result.Diggable := False;
785 Result.Blocking := False;
786 with Engine, World, TTank(Tanks[NewDirection]) do
787 for Y := 0 to Image.Count.Y - 1 do
788 for X := 0 to Image.Count.X - 1 do begin
789 XX := X + NewPosition.X - Image.Count.X div 2;
790 YY := Y + NewPosition.Y - Image.Count.Y div 2;
791 if Image.ItemsXY[X, Y] > 0 then begin
792 if TMatter(Matters[Surface.ItemsXY[XX, YY]]).Blocking then
793 Result.Blocking := True;
794 if TMatter(Matters[Surface.ItemsXY[XX, YY]]).Diggable then
795 Result.Diggable := True;
796 end;
797 end;
798end;
799
800function TPlayer.ShowTankProc(Item1, Item2: Byte): Byte;
801begin
802 if Item2 > 0 then Result := Item2
803 else Result := Item1;
804end;
805
806procedure TPlayer.SetExploded(const AValue: Boolean);
807begin
808 if FExploded = AValue then Exit;
809 FExploded := AValue;
810 if FExploded then begin
811 HideTank;
812 Energy := 0;
813 Shield := 0;
814 end else ShowTank;
815end;
816
817procedure TPlayer.ShowTank;
818begin
819 with Engine.World do begin
820 Surface.Merge(Surface.CreateIndex(
821 Position.X - Tanks[Direction].Image.Count.X div 2,
822 Position.Y - Tanks[Direction].Image.Count.Y div 2),
823 Tanks[Direction].Image, ShowTankProc);
824 end;
825end;
826
827function TPlayer.HideTankProc(Item1, Item2: Byte): Byte;
828begin
829 if Item2 > 0 then Result := 0 else Result := Item1;
830end;
831
832function TPlayer.DigProc(Item1, Item2: Byte): Byte;
833begin
834 if ((Item1 = Integer(miDirt1)) or (Item1 = Integer(miDirt2))) and (Item2 = 1) then
835 Result := Integer(miSpace) else Result := Item1;
836end;
837
838procedure TPlayer.ResetTank;
839begin
840 HideTank;
841 Position := StartPosition;
842 ExplosionPending := False;
843 Bullets.HideAll;
844 Bullets.Clear;
845 Energy := 1;
846 Shield := 1;
847 Direction := 0;
848 ShowTank;
849 Exploded := False;
850end;
851
852procedure TPlayer.Init;
853begin
854 with Engine do
855 StartPosition := Point(Round(World.Surface.Count.X * 0.2) + Random(Round(World.Surface.Count.X * 0.6)),
856 Round(World.Surface.Count.Y * 0.2) + Random(Round(World.Surface.Count.Y * 0.6)));
857 Position := StartPosition;
858 PlaceHouse;
859 ShotsCount := 0;
860 ShotsHit := 0;
861 MetersDug := 0;
862 MetersTravelled := 0;
863end;
864
865procedure TPlayer.Explosion(Position: TPoint; Distance: Integer);
866var
867 NewBullet: TBullet;
868 I: Integer;
869 Speed: Real;
870 Angle: Real;
871begin
872 if not Exploded then begin
873 for I := 0 to Distance * 2 - 1 do begin
874 NewBullet := TBullet.Create;
875 NewBullet.Player := Self;
876 Speed := ExplosionBulletMinSpeed + (ExplosionBulletMaxSpeed - ExplosionBulletMinSpeed) * Random;
877 Angle := Random * 2 * Pi;
878 NewBullet.Direction.X := Sin(Angle) * Speed;
879 NewBullet.Direction.Y := Cos(Angle) * Speed;
880 NewBullet.Position.X := Position.X; // + NewBullet.Direction.X * 3;
881 NewBullet.Position.Y := Position.Y; // + NewBullet.Direction.Y * 3;
882 NewBullet.MaxDistance := Random(Distance);
883 NewBullet.CanKill := False;
884 Bullets.Add(NewBullet);
885 end;
886 end;
887end;
888
889procedure TPlayer.HideTank;
890begin
891 with Engine.World do begin
892 Surface.Merge(Surface.CreateIndex(
893 Position.X - Tanks[Direction].Image.Count.X div 2,
894 Position.Y - Tanks[Direction].Image.Count.Y div 2),
895 Tanks[Direction].Image, HideTankProc);
896 end;
897end;
898
899procedure TPlayer.InitTanks;
900var
901 NewTank: TTank;
902 I: Integer;
903 X, Y: Integer;
904begin
905 Tanks.Clear;
906
907 NewTank := TTank.Create;
908 with NewTank do begin
909 Image.Count := Image.CreateIndex(7, 7);
910 for I := 0 to 3 do
911 Image[3, I] := Byte(miPlayer1Cannon) + Id * 4;
912 for I := 1 to 6 do begin
913 Image[1, I] := Byte(miPlayer1TankBody) + Id * 4;
914 Image[5, I] := Byte(miPlayer1TankBody) + Id * 4;
915 end;
916 for I := 2 to 5 do begin
917 Image[2, I] := Byte(miPlayer1TankBody2) + Id * 4;
918 Image[4, I] := Byte(miPlayer1TankBody2) + Id * 4;
919 end;
920 Image[3, 4] := Byte(miPlayer1TankBody2) + Id * 4;
921 Image[3, 5] := Byte(miPlayer1TankBody2) + Id * 4;
922 end;
923 Tanks.Add(NewTank);
924
925 NewTank := TTank.Create;
926 with NewTank do begin
927 Image.Count := Image.CreateIndex(7, 7);
928 for I := 0 to 2 do
929 Image[3 + I, 3 - I] := Byte(miPlayer1Cannon) + Id * 4;
930 for I := 0 to 3 do begin
931 Image[I, 3 - I] := Byte(miPlayer1TankBody) + Id * 4;
932 Image[3 + I, 6 - I] := Byte(miPlayer1TankBody) + Id * 4;
933 end;
934 for I := 0 to 2 do begin
935 Image[1 + I, 3 - I] := Byte(miPlayer1TankBody2) + Id * 4;
936 Image[3 + I, 5 - I] := Byte(miPlayer1TankBody2) + Id * 4;
937 end;
938 Image[3, 2] := Byte(miPlayer1TankBody2) + Id * 4;
939 Image[2, 3] := Byte(miPlayer1TankBody2) + Id * 4;
940 Image[2, 4] := Byte(miPlayer1TankBody2) + Id * 4;
941 Image[3, 4] := Byte(miPlayer1TankBody2) + Id * 4;
942 Image[4, 3] := Byte(miPlayer1TankBody2) + Id * 4;
943 end;
944 Tanks.Add(NewTank);
945
946 NewTank := TTank.Create;
947 NewTank.Image.Assign(Tanks[0].Image);
948 NewTank.Image.Reverse;
949 NewTank.Image.ReverseHorizontal;
950 Tanks.Add(NewTank);
951
952 NewTank := TTank.Create;
953 NewTank.Image.Assign(Tanks[1].Image);
954 NewTank.Image.ReverseVertical;
955 Tanks.Add(NewTank);
956
957 NewTank := TTank.Create;
958 NewTank.Image.Assign(Tanks[0].Image);
959 NewTank.Image.ReverseVertical;
960 Tanks.Add(NewTank);
961
962 NewTank := TTank.Create;
963 NewTank.Image.Assign(Tanks[1].Image);
964 NewTank.Image.ReverseVertical;
965 NewTank.Image.ReverseHorizontal;
966 Tanks.Add(NewTank);
967
968 NewTank := TTank.Create;
969 NewTank.Image.Assign(Tanks[0].Image);
970 NewTank.Image.Reverse;
971 Tanks.Add(NewTank);
972
973 NewTank := TTank.Create;
974 NewTank.Image.Assign(Tanks[1].Image);
975 NewTank.Image.ReverseHorizontal;
976 Tanks.Add(NewTank);
977
978 for I := 0 to Tanks.Count - 1 do
979 with Tanks[I] do begin
980 Mask.Assign(Image);
981 for Y := 0 to Mask.Count.Y - 1 do
982 for X := 0 to Mask.Count.X - 1 do
983 if Mask.ItemsXY[X, Y] > 0 then Mask.ItemsXY[X, Y] := 1;
984 end;
985end;
986
987constructor TPlayer.Create;
988begin
989 Tanks := TTanks.Create;
990 Bullets := TBullets.Create;
991 House := TRectangle.Create;
992 ScreenFrame := TRectangle.Create;
993end;
994
995destructor TPlayer.Destroy;
996begin
997 FreeAndNil(ScreenFrame);
998 FreeAndNil(House);
999 FreeAndNil(Bullets);
1000 FreeAndNil(Tanks);
1001 inherited;
1002end;
1003
1004procedure TPlayer.Assign(Source: TPlayer);
1005begin
1006 Engine := Source.Engine;
1007 Id := Source.Id;
1008 Keys := Source.Keys;
1009 Color1 := Source.Color1;
1010 Color2 := Source.Color2;
1011 Energy := Source.Energy;
1012 Shield := Source.Shield;
1013 Name := Source.Name;
1014 Enabled := Source.Enabled;
1015 Position := Source.Position;
1016 StartPosition := Source.StartPosition;
1017 Score := Source.Score;
1018 ShotsCount := Source.ShotsCount;
1019 ShotsHit := Source.ShotsHit;
1020 MetersDug := Source.MetersDug;
1021 MetersTravelled := Source.MetersTravelled;
1022 FExploded := Source.FExploded;
1023 Tanks.Assign(Source.Tanks);
1024end;
1025
1026procedure TPlayer.LoadFromRegistry(Context: TRegistryContext);
1027begin
1028 with TRegistryEx.Create do
1029 try
1030 CurrentContext := Context;
1031 Name := ReadStringWithDefault('Name', Name);
1032 Color1 := ReadIntegerWithDefault('Color1', Color1);
1033 Color2 := ReadIntegerWithDefault('Color2', Color2);
1034 Enabled := ReadBoolWithDefault('Enabled', Enabled);
1035 Keys.Left := ReadIntegerWithDefault('KeysLeft', Keys.Left);
1036 Keys.Right := ReadIntegerWithDefault('KeyRight', Keys.Right);
1037 Keys.Down := ReadIntegerWithDefault('KeyDown', Keys.Down);
1038 Keys.Up := ReadIntegerWithDefault('KeyUp', Keys.Up);
1039 Keys.Shoot := ReadIntegerWithDefault('KeyShoot', Keys.Shoot);
1040 finally
1041 Free;
1042 end;
1043end;
1044
1045procedure TPlayer.SaveToRegistry(Context: TRegistryContext);
1046begin
1047 with TRegistryEx.Create do
1048 try
1049 CurrentContext := Context;
1050 WriteString('Name', Name);
1051 WriteInteger('Color1', Color1);
1052 WriteInteger('Color2', Color2);
1053 WriteBool('Enabled', Enabled);
1054 WriteInteger('KeysLeft', Keys.Left);
1055 WriteInteger('KeyRight', Keys.Right);
1056 WriteInteger('KeyDown', Keys.Down);
1057 WriteInteger('KeyUp', Keys.Up);
1058 WriteInteger('KeyShoot', Keys.Shoot);
1059 finally
1060 Free;
1061 end;
1062end;
1063
1064{ TEngine }
1065
1066procedure TEngine.SetActive(const AValue: Boolean);
1067begin
1068 if FActive = AValue then Exit;
1069 FActive := AValue;
1070 if AValue then begin
1071 FDrawThread := TDrawThread.Create(True);
1072 FDrawThread.Engine := Self;
1073 FDrawThread.FreeOnTerminate := False;
1074 FDrawThread.Name := 'Draw';
1075 FDrawThread.Start;
1076 FSystemThread := TSystemThread.Create(True);
1077 FSystemThread.Engine := Self;
1078 FSystemThread.FreeOnTerminate := False;
1079 FSystemThread.Name := 'Engine';
1080 FSystemThread.Start;
1081 end else begin
1082 FDrawThread.Terminate;
1083 Application.ProcessMessages;
1084 FreeAndNil(FDrawThread);
1085 FreeAndNil(FSystemThread);
1086 end;
1087end;
1088
1089procedure TEngine.SetBitmap(const AValue: TBitmap);
1090begin
1091 FBitmap := AValue;
1092 ResizePlayerFrames;
1093end;
1094
1095procedure TEngine.Redraw;
1096begin
1097 FRedrawPending := True;
1098end;
1099
1100function TEngine.IsInsideHouses(Pos: TPoint): Boolean;
1101var
1102 I: Integer;
1103begin
1104 Result := False;
1105 for I := 0 to Players.Count - 1 do
1106 if Players[I].House.IsInside(Pos) then begin
1107 Result := True;
1108 end;
1109end;
1110
1111procedure TEngine.DrawGame;
1112var
1113 X, Y: Integer;
1114 PixelX, PixelY: Integer;
1115 SubPixelPtr: PInteger;
1116 SubPixelRowPtr: PInteger;
1117 SubPixelSizeX: Integer;
1118 SubPixelSizeY: Integer;
1119 PixelPtr: PInteger;
1120 PixelRowPtr: PInteger;
1121 BytePerPixel: Integer;
1122 BytePerRow: Integer;
1123 RawImage: TRawImage;
1124 Color: Int64;
1125 Shift: TPoint;
1126 XDiv, XMod, XAcc: Integer;
1127 YDiv, YMod, YAcc: Integer;
1128 Ratio: Real;
1129 TargetHeight: Integer;
1130 TargetWidth: Integer;
1131 BgColor: Cardinal;
1132begin
1133 // TODO: To be able to draw into Bitmap not just through Canvas
1134 Bitmap.EndUpdate;
1135 Bitmap.BeginUpdate;
1136
1137 {$IFDEF WINDOWS}
1138 Bitmap.PixelFormat := pf32bit;
1139 {$ENDIF}
1140 RawImage := Bitmap.RawImage;
1141 BytePerPixel := RawImage.Description.BitsPerPixel div 8;
1142 BytePerRow := RawImage.Description.BytesPerLine;
1143 if FClearBackground then begin
1144 BgColor := World.Matters[Integer(miBorder)].Color;
1145 BgColor := SwapBRComponent(BgColor);
1146 FillDWord(RawImage.Data^, Bitmap.Height * BytePerRow div 4, BgColor);
1147 FClearBackground := False;
1148 end;
1149
1150 if (FBitmap.Width / FBitmapLower.Width) < (FBitmap.Height / FBitmapLower.Height) then
1151 Ratio := FBitmap.Width / FBitmapLower.Width
1152 else Ratio := FBitmap.Height / FBitmapLower.Height;
1153
1154 // Preserve aspect ratio
1155 TargetWidth := Trunc(FBitmapLower.Width * Ratio);
1156 TargetHeight := Trunc(FBitmapLower.Height * Ratio);
1157
1158 Shift.X := Trunc((Bitmap.Width - TargetWidth) / 2);
1159 Shift.Y := Trunc((Bitmap.Height - TargetHeight) / 2);
1160
1161 XDiv := TargetWidth div FBitmapLower.Width;
1162 XMod := TargetWidth mod FBitmapLower.Width;
1163 YDiv := TargetHeight div FBitmapLower.Height;
1164 YMod := TargetHeight mod FBitmapLower.Height;
1165
1166 PixelRowPtr := PInteger(RawImage.Data + BytePerRow * Shift.Y);
1167 YAcc := FBitmapLower.Height div 2;
1168 for Y := 0 to FBitmapLower.Height - 1 do begin
1169 SubPixelSizeY := YDiv;
1170 Inc(YAcc, YMod);
1171 if YAcc >= FBitmapLower.Height then begin
1172 Dec(YAcc, FBitmapLower.Height);
1173 Inc(SubPixelSizeY);
1174 end;
1175
1176 PixelPtr := PixelRowPtr + Shift.X;
1177 XAcc := FBitmapLower.Width div 2;
1178 for X := 0 to FBitmapLower.Width - 1 do begin
1179 SubPixelSizeX := XDiv;
1180 Inc(XAcc, XMod);
1181 if XAcc >= FBitmapLower.Width then begin
1182 Dec(XAcc, FBitmapLower.Width);
1183 Inc(SubPixelSizeX);
1184 end;
1185 Color := FBitmapLower.Pixels[X, Y] and $ffffff;
1186
1187 Color := SwapBRComponent(Color);
1188
1189 // Draw large pixel
1190 SubPixelRowPtr := PixelPtr;
1191 for PixelY := 0 to SubPixelSizeY - 1 do begin
1192 SubPixelPtr := SubPixelRowPtr;
1193 for PixelX := 0 to SubPixelSizeX - 1 do begin
1194 SubPixelPtr^ := Color;
1195 Inc(PByte(SubPixelPtr), BytePerPixel);
1196 end;
1197 Inc(PByte(SubPixelRowPtr), BytePerRow);
1198 end;
1199 Inc(PByte(PixelPtr), BytePerPixel * SubPixelSizeX);
1200 end;
1201 Inc(PByte(PixelRowPtr), BytePerRow * SubPixelSizeY);
1202 end;
1203end;
1204
1205procedure TEngine.DrawInformation;
1206var
1207 Text: string;
1208 X: Integer;
1209 Y: Integer;
1210const
1211 LineHeight = 60;
1212begin
1213 with Bitmap.Canvas do begin
1214 ClearBackground;
1215
1216 X := Bitmap.Width div 2;
1217 Y := Bitmap.Height div 20;
1218
1219 Brush.Style := bsClear;
1220 Pen.Style := psSolid;
1221 Pen.Color := clWhite;
1222 Font.Color := clGreen;
1223 Font.Size := 30;
1224 Text := SInformation;
1225 TextOut(X - TextWidth(Text) div 2, Y, Text);
1226 Inc(Y, 2 * LineHeight);
1227
1228 X := 30;
1229
1230 Font.Color := clYellow;
1231 Font.Size := 20;
1232 Text := SInformationDetails;
1233 Inc(Y, LineHeight * TextOutWordWrap(Bitmap.Canvas, X, Y, Text, Bitmap.Width - 60));
1234 Inc(Y, LineHeight);
1235
1236 Text := SInformationDetails2;
1237 Inc(Y, LineHeight * TextOutWordWrap(Bitmap.Canvas, X, Y, Text, Bitmap.Width - 60));
1238 Inc(Y, LineHeight);
1239
1240 X := Bitmap.Width div 2;
1241
1242 Font.Color := clGreen;
1243 Font.Size := 30;
1244 Text := SPressEsc;
1245 TextOut(X - TextWidth(Text) div 2, Bitmap.Height div 10 * 9, Text);
1246 end;
1247end;
1248
1249procedure TEngine.DrawInstructions;
1250var
1251 Text: string;
1252 X: Integer;
1253 Y: Integer;
1254const
1255 LineHeight = 60;
1256begin
1257 with Bitmap.Canvas do begin
1258 ClearBackground;
1259
1260 X := Bitmap.Width div 2;
1261 Y := Bitmap.Height div 20;
1262
1263 Brush.Style := bsClear;
1264 Pen.Style := psSolid;
1265 Pen.Color := clWhite;
1266 Font.Color := clTuna;
1267 Font.Size := 30;
1268 Text := SInstructions;
1269 TextOut(X - TextWidth(Text) div 2, Y, Text);
1270 Inc(Y, 2 * LineHeight);
1271
1272 X := 30;
1273
1274 Font.Color := clTeal;
1275 Font.Size := 20;
1276 Text := SInstructionsDetails;
1277 Inc(Y, LineHeight * TextOutWordWrap(Bitmap.Canvas, X, Y, Text, Bitmap.Width - 60));
1278 Inc(Y, LineHeight);
1279
1280 Text := SInstructionsDetails2;
1281 Inc(Y, LineHeight * TextOutWordWrap(Bitmap.Canvas, X, Y, Text, Bitmap.Width - 60));
1282 Inc(Y, LineHeight);
1283
1284 Text := SInstructionsDetails3;
1285 Inc(Y, LineHeight * TextOutWordWrap(Bitmap.Canvas, X, Y, Text, Bitmap.Width - 60));
1286 Inc(Y, LineHeight);
1287
1288 Text := SInstructionsDetails4;
1289 Inc(Y, LineHeight * TextOutWordWrap(Bitmap.Canvas, X, Y, Text, Bitmap.Width - 60));
1290 Inc(Y, LineHeight);
1291
1292 X := Bitmap.Width div 2;
1293 Font.Color := clGreen;
1294 Font.Size := 30;
1295 Text := SPressEsc;
1296 TextOut(X - TextWidth(Text) div 2, Bitmap.Height div 10 * 9, Text);
1297 end;
1298end;
1299
1300procedure TEngine.DrawNewRound;
1301var
1302 Text: string;
1303 I: Integer;
1304 Y: Integer;
1305begin
1306 with Bitmap.Canvas do begin
1307 ClearBackground;
1308
1309 Brush.Style := bsClear;
1310 Pen.Style := psSolid;
1311 Pen.Color := clWhite;
1312 Font.Color := clTuna;
1313 Font.Size := 30;
1314 Text := SRound + ' ' + IntToStr(CurrentRound);
1315 TextOut((Bitmap.Width - TextWidth(Text)) div 2, Bitmap.Height div 5, Text);
1316
1317 Y := 0;
1318 for I := 0 to Players.Count - 1 do
1319 with TPlayer(Players[I]) do begin
1320 if Enabled then begin
1321 Font.Color := Color1;
1322 Text := Name + ': ' + IntToStr(Score);
1323 TextOut((Bitmap.Width - TextWidth(Text)) div 2, Bitmap.Height div 5 * 2 + Y, Text);
1324 Inc(Y, 60);
1325 end;
1326 end;
1327 end;
1328end;
1329
1330procedure TEngine.DrawMap;
1331var
1332 Text: string;
1333begin
1334 Bitmap.EndUpdate;
1335 Bitmap.BeginUpdate;
1336
1337 World.DrawToBitmap(Bitmap);
1338
1339 Bitmap.EndUpdate;
1340 Bitmap.BeginUpdate(True);
1341 with Bitmap.Canvas do begin
1342 Brush.Style := bsClear;
1343 Pen.Style := psSolid;
1344 Font.Color := clGreen;
1345 Font.Size := 30;
1346 Text := SPressEsc;
1347 TextOut((Bitmap.Width - TextWidth(Text)) div 2, Bitmap.Height div 10 * 9, Text);
1348 end;
1349end;
1350
1351procedure TEngine.SetState(AValue: TGameState);
1352begin
1353 if FState = AValue then Exit;
1354 FState := AValue;
1355 FRedrawPending := True;
1356 FStateTime := Now;
1357end;
1358
1359function TEngine.TextOutWordWrap(Canvas: TCanvas; X, Y: Integer; Text: string; Width: Integer): Integer;
1360var
1361 Parts: TStringArray;
1362 I: Integer;
1363 XX: Integer;
1364begin
1365 Result := 1;
1366 XX := 0;
1367 Parts := Explode(' ', Text);
1368 for I := 0 to Length(Parts) - 1 do begin
1369 if (X + XX + Canvas.TextWidth(Parts[I]) > Width) or (Parts[I] = '\n') then begin
1370 Y := Y + Canvas.TextHeight(Parts[I]);
1371 XX := 0;
1372 Inc(Result);
1373 end;
1374 if Parts[I] = '\n' then Continue;
1375 Canvas.TextOut(X + XX, Y, Parts[I]);
1376 XX := XX + Canvas.TextWidth(Parts[I]) + Canvas.TextWidth(' ');
1377 end;
1378end;
1379
1380procedure TEngine.InitDigMasks;
1381var
1382 NewMask: TMatrixByte;
1383 I: Integer;
1384begin
1385 DigMasks.Clear;
1386
1387 // 001111100
1388 // 0111A1110
1389 // 00z1A1z00
1390 // 00zxAxz00
1391 // 00zxAxz00
1392 // 00zxxxz00
1393 // 00z000z00
1394 // 000000000
1395 // 000000000
1396
1397 NewMask := TMatrixByte.Create;
1398 with NewMask do begin
1399 Count := CreateIndex(9, 9);
1400 for I := 0 to 4 do ItemsXY[2 + I, 0] := 1;
1401 for I := 0 to 2 do begin
1402 ItemsXY[1 + I, 1] := 1;
1403 ItemsXY[5 + I, 1] := 1;
1404 end;
1405 ItemsXY[3, 2] := 1;
1406 ItemsXY[5, 2] := 1;
1407 end;
1408 DigMasks.Add(NewMask);
1409
1410 // 000011110
1411 // 0000z1111
1412 // 000zx1A11
1413 // 00zxxA111
1414 // 0zxxAxxz1
1415 // 000xxxz00
1416 // 0000xz000
1417 // 0000z0000
1418 // 000000000
1419
1420 NewMask := TMatrixByte.Create;
1421 with NewMask do begin
1422 Count := CreateIndex(9, 9);
1423 for I := 0 to 3 do begin
1424 ItemsXY[4 + I, 0] := 1;
1425 ItemsXY[5 + I, 1] := 1;
1426 end;
1427 ItemsXY[5, 2] := 1;
1428 ItemsXY[7, 2] := 1;
1429 ItemsXY[8, 2] := 1;
1430 for I := 0 to 2 do
1431 ItemsXY[6 + I, 3] := 1;
1432 ItemsXY[8, 4] := 1;
1433 end;
1434 DigMasks.Add(NewMask);
1435
1436 NewMask := TMatrixByte.Create;
1437 NewMask.Assign(DigMasks[0]);
1438 NewMask.Reverse;
1439 NewMask.ReverseHorizontal;
1440 DigMasks.Add(NewMask);
1441
1442 NewMask := TMatrixByte.Create;
1443 NewMask.Assign(DigMasks[1]);
1444 NewMask.ReverseVertical;
1445 DigMasks.Add(NewMask);
1446
1447 NewMask := TMatrixByte.Create;
1448 NewMask.Assign(DigMasks[0]);
1449 NewMask.ReverseVertical;
1450 DigMasks.Add(NewMask);
1451
1452 NewMask := TMatrixByte.Create;
1453 NewMask.Assign(DigMasks[1]);
1454 NewMask.ReverseVertical;
1455 NewMask.ReverseHorizontal;
1456 DigMasks.Add(NewMask);
1457
1458 NewMask := TMatrixByte.Create;
1459 NewMask.Assign(DigMasks[0]);
1460 NewMask.Reverse;
1461 DigMasks.Add(NewMask);
1462
1463 NewMask := TMatrixByte.Create;
1464 NewMask.Assign(DigMasks[1]);
1465 NewMask.ReverseHorizontal;
1466 DigMasks.Add(NewMask);
1467end;
1468
1469procedure TEngine.InitPlayerPool;
1470var
1471 I: Integer;
1472begin
1473 PlayerPool.Clear;
1474 with PlayerPool.AddNew do begin
1475 Name := SGreen;
1476 Keys.Left := 65;
1477 Keys.Down := 83;
1478 Keys.Right := 68;
1479 Keys.Up := 87;
1480 Keys.Shoot := 69;
1481 Color1 := $00ff00;
1482 Color2 := $00a000;
1483 Enabled := True;
1484 end;
1485 with PlayerPool.AddNew do begin
1486 Name := SBlue;
1487 Keys.Left := 37;
1488 Keys.Down := 40;
1489 Keys.Right := 39;
1490 Keys.Up := 38;
1491 Keys.Shoot := 17;
1492 Color1 := $ff2c2c;
1493 Color2 := $b60000;
1494 Enabled := True;
1495 end;
1496 with PlayerPool.AddNew do begin
1497 Name := SRed;
1498 Keys.Left := 76;
1499 Keys.Down := 147;
1500 Keys.Right := 222;
1501 Keys.Up := 80;
1502 Keys.Shoot := 191;
1503 Color1 := $0000ff;
1504 Color2 := $0000a0;
1505 end;
1506 with PlayerPool.AddNew do begin
1507 Name := SPink;
1508 Keys.Left := 100;
1509 Keys.Down := 98;
1510 Keys.Right := 102;
1511 Keys.Up := 104;
1512 Keys.Shoot := 105;
1513 Color1 := $ff2cff;
1514 Color2 := $b600b6;
1515 end;
1516 with PlayerPool.AddNew do begin
1517 Name := SCyan;
1518 Keys.Left := 0;
1519 Keys.Down := 0;
1520 Keys.Right := 0;
1521 Keys.Up := 0;
1522 Keys.Shoot := 0;
1523 Color1 := $ffff2c;
1524 Color2 := $b6b600;
1525 end;
1526 with PlayerPool.AddNew do begin
1527 Name := SYellow;
1528 Keys.Left := 0;
1529 Keys.Down := 0;
1530 Keys.Right := 0;
1531 Keys.Up := 0;
1532 Keys.Shoot := 0;
1533 Color1 := $2cffff;
1534 Color2 := $00b6b6;
1535 end;
1536 with PlayerPool.AddNew do begin
1537 Name := SOrange;
1538 Keys.Left := 0;
1539 Keys.Down := 0;
1540 Keys.Right := 0;
1541 Keys.Up := 0;
1542 Keys.Shoot := 0;
1543 Color1 := $008cff;
1544 Color2 := $002da0;
1545 end;
1546 with PlayerPool.AddNew do begin
1547 Name := SGray;
1548 Keys.Left := 0;
1549 Keys.Down := 0;
1550 Keys.Right := 0;
1551 Keys.Up := 0;
1552 Keys.Shoot := 0;
1553 Color1 := $d0d0d0;
1554 Color2 := $707070;
1555 end;
1556 for I := 0 to PlayerPool.Count - 1 do
1557 with PlayerPool[I] do begin
1558 Engine := Self;
1559 Id := I;
1560 InitTanks;
1561 if I < 2 then Enabled := True;
1562 end;
1563end;
1564
1565procedure TEngine.InitPlayers;
1566var
1567 I: Integer;
1568 NewPlayer: TPlayer;
1569begin
1570 Players.Clear;
1571 for I := 0 to PlayerPool.Count - 1 do
1572 with PlayerPool[I] do
1573 if Enabled then begin
1574 NewPlayer := TPlayer.Create;
1575 NewPlayer.Assign(PlayerPool[I]);
1576 Players.Add(NewPlayer);
1577 Score := 0;
1578 World.Matters[Integer(miPlayer1Cannon) + I * 4].Color := clYellow;
1579 World.Matters[Integer(miPlayer1Home) + I * 4].Color := Color1;
1580 World.Matters[Integer(miPlayer1TankBody) + I * 4].Color := Color1;
1581 World.Matters[Integer(miPlayer1TankBody2) + I * 4].Color := Color2;
1582 end;
1583end;
1584
1585procedure TEngine.CheckGameEnd;
1586var
1587 I: Integer;
1588begin
1589 if Players.GetAliveCount <= 1 then begin
1590 for I := 0 to Players.Count - 1 do
1591 with Players[I] do
1592 if not Exploded then Inc(Score);
1593 if CurrentRound < MaxRound then begin
1594 Inc(CurrentRound);
1595 NewRound;
1596 State := gsNewRound;
1597 end else
1598 State := gsMap;
1599 if Assigned(FOnGameEnd) then
1600 FOnGameEnd(Self);
1601 end;
1602end;
1603
1604procedure TEngine.ClearBackground;
1605begin
1606 with Bitmap.Canvas do begin
1607 Brush.Style := bsSolid;
1608 Brush.Color := clBlack;
1609 FillRect(0, 0, Bitmap.Width, Bitmap.Height);
1610 end;
1611end;
1612
1613procedure TEngine.DrawMenu;
1614var
1615 Text: string;
1616 MenuWidth: Integer;
1617begin
1618 with Bitmap.Canvas do begin
1619 ClearBackground;
1620
1621 if FShowMenuStats then begin
1622 MenuWidth := Bitmap.Width div 2;
1623 DrawStats;
1624 end else MenuWidth := Bitmap.Width;
1625
1626 Brush.Style := bsClear;
1627 Pen.Style := psSolid;
1628 Pen.Color := clWhite;
1629 Font.Color := clTuna;
1630 Font.Size := 30;
1631 Text := 'TUNNELER';
1632 TextOut((MenuWidth - TextWidth(Text)) div 2, Bitmap.Height div 10, Text);
1633
1634 Font.Color := clDarkOrange;
1635 Font.Size := 20;
1636 Text := 'by Chronosoft';
1637 TextOut((MenuWidth - TextWidth(Text)) div 2, Bitmap.Height div 10 + 70, Text);
1638
1639 Pen.Color := clPurple;
1640 Pen.Width := 6;
1641 Frame((MenuWidth - 400) div 2, Bitmap.Height div 10 * 4 - 40,
1642 (MenuWidth + 400) div 2, Bitmap.Height div 10 * 4 + 200);
1643
1644 Font.Color := clPurple;
1645 Font.Size := 20;
1646 Text := '<F1> ' + SStartGame;
1647 TextOut((MenuWidth - TextWidth(Text)) div 2, Bitmap.Height div 10 * 4, Text);
1648 Text := '<F2> ' + SInstructions;
1649 TextOut((MenuWidth - TextWidth(Text)) div 2, Bitmap.Height div 10 * 4 + 40, Text);
1650 Text := '<F3> ' + SInformation;
1651 TextOut((MenuWidth - TextWidth(Text)) div 2, Bitmap.Height div 10 * 4 + 80, Text);
1652 Text := '<Alt+F4> ' + SExit;
1653 TextOut((MenuWidth - TextWidth(Text)) div 2, Bitmap.Height div 10 * 4 + 120, Text);
1654
1655 Font.Color := clDarkGreen;
1656 Font.Size := 20;
1657 Text := '(' + SWorldReady + ')';
1658 TextOut((MenuWidth - TextWidth(Text)) div 2, Bitmap.Height div 10 * 9, Text);
1659 end;
1660end;
1661
1662procedure TEngine.DrawStats;
1663var
1664 X: Integer;
1665 Y: Integer;
1666 Text: string;
1667 Winner: TPlayer;
1668 I: Integer;
1669 ShotsPercent: Integer;
1670const
1671 LineHeight = 40;
1672begin
1673 with Bitmap.Canvas do begin
1674 X := Bitmap.Width div 4 * 3;
1675
1676 Brush.Style := bsClear;
1677 Pen.Style := psSolid;
1678
1679 Pen.Color := clWhite;
1680 MoveTo(Bitmap.Width div 2, 0);
1681 LineTo(Bitmap.Width div 2, Bitmap.Height);
1682
1683 Font.Color := clCyan;
1684 Font.Size := 20;
1685 Text := SStatistics;
1686 TextOut(X - TextWidth(Text) div 2, Bitmap.Height div 10, Text);
1687
1688 Font.Color := clOrange;
1689 Font.Size := 20;
1690
1691 Y := Bitmap.Height div 10 + 3 * LineHeight;
1692 X := Bitmap.Width div 2 + 50;
1693 Inc(Y, LineHeight);
1694 Text := SShotsFired;
1695 TextOut(X, Y, Text);
1696 Inc(Y, LineHeight);
1697 Text := SShotsHit;
1698 TextOut(X, Y, Text);
1699 Inc(Y, LineHeight);
1700 Text := SPercentHit;
1701 TextOut(X, Y, Text);
1702 Inc(Y, LineHeight);
1703 Inc(Y, LineHeight);
1704 Text := SMetersDug;
1705 TextOut(X, Y, Text);
1706 Inc(Y, LineHeight);
1707 Text := SMetersTravelled;
1708 TextOut(X, Y, Text);
1709 Inc(Y, LineHeight);
1710
1711 for I := 0 to Players.Count - 1 do
1712 with Players[I] do begin
1713 Y := Bitmap.Height div 10 + 3 * LineHeight;
1714 X := Bitmap.Width div 2 + 50 + 500 + 200 * I;
1715 Font.Color := Color1;
1716 Text := Name;
1717 TextOut(X - TextWidth(Text) , Y, Text);
1718 Inc(Y, LineHeight);
1719 Text := IntToStr(ShotsCount);
1720 TextOut(X - TextWidth(Text), Y, Text);
1721 Inc(Y, LineHeight);
1722 Text := IntToStr(ShotsHit);
1723 TextOut(X - TextWidth(Text), Y, Text);
1724 Inc(Y, LineHeight);
1725 if ShotsCount > 0 then
1726 ShotsPercent := Round(ShotsHit / ShotsCount * 100)
1727 else ShotsPercent := 0;
1728 Text := IntToStr(ShotsPercent) + '%';
1729 TextOut(X - TextWidth(Text), Y, Text);
1730 Inc(Y, LineHeight);
1731 Inc(Y, LineHeight);
1732 Text := IntToStr(MetersDug);
1733 TextOut(X - TextWidth(Text), Y, Text);
1734 Inc(Y, LineHeight);
1735 Text := IntToStr(MetersTravelled);
1736 TextOut(X - TextWidth(Text), Y, Text);
1737 Inc(Y, LineHeight);
1738 end;
1739
1740 Inc(Y, 3 * LineHeight);
1741
1742 X := Bitmap.Width div 2 + 50;
1743 Font.Color := clOrange;
1744 Winner := Players.GetWinner;
1745 if Assigned(Winner) then begin
1746 Text := SWinnerIs;
1747 TextOut(X, Y, Text);
1748 X := X + TextWidth(Text) + 20;
1749 Font.Color := Winner.Color1;
1750 Text := Winner.Name;
1751 TextOut(X, Y, Text);
1752 end;
1753 end;
1754end;
1755
1756procedure TEngine.DrawGamePrepare(Thread: TVirtualThread);
1757var
1758 I: Integer;
1759 OldCount: TBitmapTColorIndex;
1760begin
1761 Lock.Acquire;
1762 try
1763 // TODO: Without this (re)initialization we get range error
1764 OldCount := FBitmapLower.Count;
1765 FBitmapLower.Count := FBitmapLower.CreateIndex(0, 0);
1766 FBitmapLower.Count := OldCount;
1767
1768 if FClearBackground then FBitmapLower.FillAll(World.Matters[Integer(miBorder)].Color);
1769 for I := 0 to Players.Count - 1 do
1770 if Players[I].Enabled then begin
1771 Players[I].Paint;
1772 end;
1773 finally
1774 Lock.Release;
1775 end;
1776end;
1777
1778procedure TEngine.ResizePlayerFrames;
1779var
1780 HorizFrameCount: Integer;
1781 VertFrameCount: Integer;
1782 I: Integer;
1783begin
1784 if Assigned(FBitmapLower) then begin
1785 if Players.Count > 1 then begin
1786 if Players.Count > 2 then VertFrameCount := 2
1787 else VertFrameCount := 1;
1788 HorizFrameCount := Round(Players.Count / VertFrameCount);
1789 end else begin
1790 VertFrameCount := 1;
1791 HorizFrameCount := 1;
1792 end;
1793 FBitmapLower.Count := FBitmapLower.CreateIndex(PlayerFrameWidth * HorizFrameCount,
1794 PlayerFrameHeight * VertFrameCount);
1795 for I := 0 to Players.Count - 1 do begin
1796 Players[I].ScreenFrame.AsTRect := Rect(
1797 (I mod HorizFrameCount) * (FBitmapLower.Count.X div HorizFrameCount) + 1,
1798 (I div HorizFrameCount) * (FBitmapLower.Count.Y div VertFrameCount) + 1,
1799 ((I mod HorizFrameCount) + 1) * (FBitmapLower.Width div HorizFrameCount),
1800 ((I div HorizFrameCount) + 1) * (FBitmapLower.Height div VertFrameCount));
1801 end;
1802 end;
1803 FClearBackground := True;
1804 Redraw;
1805end;
1806
1807constructor TEngine.Create(AOwner: TComponent);
1808begin
1809 inherited;
1810 Lock := TCriticalSection.Create;
1811 FBitmapLower := TBitmapTColor.Create;
1812 FBitmapLock := TCriticalSection.Create;
1813 PlayerPool := TPlayers.Create;
1814 PlayerPool.Engine := Self;
1815 Players := TPlayers.Create;
1816 Players.Engine := Self;
1817 Keyboard := TKeyboard.Create;
1818 World := TWorld.Create;
1819 //DefaultAudioSystem := TAudioSystemMPlayer.Create(nil);
1820 AudioShot := TSound.Create(nil);
1821 AudioShot.FileName := GetCurrentDir + '/Audio/GE_KF7_Soviet.wav';
1822 AudioExplode := TSound.Create(nil);
1823 AudioExplode.FileName := GetCurrentDir + '/Audio/bomb.wav';
1824 InitPlayerPool;
1825 DigMasks := TObjectList<TMatrixByte>.Create;
1826 InitDigMasks;
1827 Redraw;
1828 MaxRound := 5;
1829end;
1830
1831destructor TEngine.Destroy;
1832begin
1833 Active := False;
1834 FreeAndNil(DigMasks);
1835 FreeAndNil(FBitmapLower);
1836 FreeAndNil(FBitmapLock);
1837 FreeAndNil(PlayerPool);
1838 FreeAndNil(Players);
1839 FreeAndNil(Keyboard);
1840 FreeAndNil(World);
1841 FreeAndNil(Lock);
1842 FreeAndNil(AudioShot);
1843 FreeAndNil(AudioExplode);
1844 inherited;
1845end;
1846
1847procedure TEngine.Tick;
1848var
1849 I: Integer;
1850begin
1851 if State = gsGame then begin
1852 Lock.Acquire;
1853 try
1854 for I := 0 to Players.Count - 1 do begin
1855 Players[I].Control;
1856 Players[I].Tick;
1857 end;
1858 finally
1859 Lock.Release;
1860 end;
1861 end else
1862 if State = gsNewRound then begin
1863 if SecondOf(Now - FStateTime) > NewRoundDelay then begin
1864 State := gsGame;
1865 end;
1866 end;
1867end;
1868
1869procedure TEngine.Draw;
1870var
1871 DrawStart: TDateTime;
1872begin
1873 if FRedrawPending then begin
1874 DrawStart := NowPrecise;
1875 FRedrawPending := False;
1876 case State of
1877 gsGame: DrawGame;
1878 gsMenu: DrawMenu;
1879 gsInformation: DrawInformation;
1880 gsInstructions: DrawInstructions;
1881 gsMap: DrawMap;
1882 gsNewRound: DrawNewRound;
1883 end;
1884
1885 DrawDuration := NowPrecise - DrawStart;
1886 end;
1887end;
1888
1889procedure TEngine.DrawThread(Thread: TVirtualThread);
1890begin
1891 if State = gsGame then DrawGamePrepare(Thread);
1892end;
1893
1894procedure TEngine.NewGame;
1895var
1896 I: Integer;
1897begin
1898 Active := False;
1899 InitPlayers;
1900 ResizePlayerFrames;
1901 CurrentRound := 1;
1902 World.Generate;
1903 for I := 0 to Players.Count - 1 do
1904 Players[I].Init;
1905 NewRound;
1906
1907 Active := True;
1908end;
1909
1910procedure TEngine.NewRound;
1911var
1912 I: Integer;
1913begin
1914 for I := 0 to Players.Count - 1 do
1915 Players[I].ResetTank;
1916 FClearBackground := True;
1917 Redraw;
1918end;
1919
1920procedure TEngine.LoadFromRegistry(Context: TRegistryContext);
1921begin
1922 with TRegistryEx.Create do
1923 try
1924 CurrentContext := Context;
1925 PlayerPool.LoadFromRegistry(TRegistryContext.Create(Context.RootKey, Context.Key + '\Players'));
1926 finally
1927 Free;
1928 end;
1929end;
1930
1931procedure TEngine.SaveToRegistry(Context: TRegistryContext);
1932begin
1933 with TRegistryEx.Create do
1934 try
1935 CurrentContext := Context;
1936
1937 PlayerPool.SaveToRegistry(TRegistryContext.Create(Context.RootKey, Context.Key + '\Players'));
1938 finally
1939 Free;
1940 end;
1941end;
1942
1943procedure TEngine.KeyUp(Key: Word);
1944const
1945 KeyF1 = 112;
1946 KeyF2 = 113;
1947 KeyF3 = 114;
1948 KeyF4 = 115;
1949 KeyEsc = 27;
1950var
1951 I: Integer;
1952begin
1953 KeyBoard.KeyState[Key] := False;
1954
1955 if State = gsMenu then begin
1956 if Key = KeyF1 then begin
1957 State := gsNewRound;
1958 NewGame;
1959 end else
1960 if Key = KeyF2 then begin
1961 State := gsInstructions;
1962 end else
1963 if Key = KeyF3 then begin
1964 State := gsInformation;
1965 end;
1966 end else
1967 if State = gsMap then begin
1968 if Key = KeyEsc then begin
1969 State := gsMenu;
1970 FShowMenuStats := True;
1971 end;
1972 end else
1973 if State in [gsInformation, gsInstructions] then begin
1974 if Key = KeyEsc then begin
1975 State := gsMenu;
1976 end;
1977 end else
1978 if State = gsGame then begin
1979 if Key = KeyEsc then begin
1980 State := gsMap;
1981 end;
1982 end;
1983
1984 {$IFDEF DEBUG}
1985 if (State = gsGame) and (Key = KeyF4) then begin
1986 // Destroy first alive player
1987 for I := 0 to Players.Count - 1 do
1988 with Players[I] do begin
1989 if not Exploded then begin
1990 Energy := -100;
1991 Break;
1992 end;
1993 end;
1994 end;
1995 {$ENDIF}
1996end;
1997
1998procedure TEngine.KeyDown(Key: Word);
1999begin
2000 KeyBoard.KeyState[Key] := True;
2001end;
2002
2003end.
2004
Note: See TracBrowser for help on using the repository browser.