From 59897febc449626cf3e8c82f2dd386f43b518574 Mon Sep 17 00:00:00 2001 From: rexim Date: Sun, 24 Mar 2024 00:25:57 +0700 Subject: game -> eepers --- .gitignore | 4 +- build-linux.sh | 4 +- build-mingw32-w64.sh | 2 +- eepers.adb | 1616 +++++++++++++++++++++++++++++++++++++++++++++++++ game.adb | 1617 -------------------------------------------------- 5 files changed, 1621 insertions(+), 1622 deletions(-) create mode 100644 eepers.adb delete mode 100644 game.adb diff --git a/.gitignore b/.gitignore index e4248fb..2ad8d9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ *.ali *.o -game -game.exe +eepers +eepers.exe test test.exe *.swp diff --git a/build-linux.sh b/build-linux.sh index c73a403..24e3683 100755 --- a/build-linux.sh +++ b/build-linux.sh @@ -2,8 +2,8 @@ set -xe -gnatmake -f -O3 -Wall -Wextra -gnat2022 game.adb -bargs -static -largs -L./raylib/raylib-5.0_linux_amd64/lib/ -l:libraylib.a -lm -./game +gnatmake -f -O3 -Wall -Wextra -gnat2022 eepers.adb -bargs -static -largs -L./raylib/raylib-5.0_linux_amd64/lib/ -l:libraylib.a -lm +./eepers # gnatmake -f -Wall -Wextra -gnat2022 test.adb -largs -L./raylib/raylib-5.0_linux_amd64/lib/ -l:libraylib.a -lm # ./test diff --git a/build-mingw32-w64.sh b/build-mingw32-w64.sh index 632098b..d4efe60 100755 --- a/build-mingw32-w64.sh +++ b/build-mingw32-w64.sh @@ -2,4 +2,4 @@ set -xe -x86_64-w64-mingw32-gnatmake-win32 -mwindows -O3 -f -Wall -Wextra -gnat2022 game.adb -bargs -static -largs -L./raylib/raylib-5.0_win64_mingw-w64/lib/ -l:libraylib.a -lwinmm -lgdi32 -static +x86_64-w64-mingw32-gnatmake-win32 -mwindows -O3 -f -Wall -Wextra -gnat2022 eepers.adb -bargs -static -largs -L./raylib/raylib-5.0_win64_mingw-w64/lib/ -l:libraylib.a -lwinmm -lgdi32 -static diff --git a/eepers.adb b/eepers.adb new file mode 100644 index 0000000..e15b749 --- /dev/null +++ b/eepers.adb @@ -0,0 +1,1616 @@ +with Ada.Text_IO; +with Text_IO; use Text_IO; +with Interfaces.C; use Interfaces.C; +with Raylib; use Raylib; +with Raymath; use Raymath; +with Ada.Strings.Unbounded; +use Ada.Strings.Unbounded; +with Ada.Containers.Vectors; +with Ada.Unchecked_Deallocation; +with Ada.Containers.Hashed_Maps; +use Ada.Containers; +with Ada.Strings.Fixed; use Ada.Strings.Fixed; +with Ada.Strings; +with Ada.Exceptions; use Ada.Exceptions; +with Ada.Numerics.Discrete_Random; +with Interfaces.C.Pointers; +with Ada.Unchecked_Conversion; +with Ada.Numerics; use Ada.Numerics; + +procedure Eepers is + package Random_Integer is + new Ada.Numerics.Discrete_Random(Result_Subtype => Integer); + Gen: Random_Integer.Generator; + + type Footsteps_Range is mod 4; + Footsteps_Sounds: array (Footsteps_Range) of Sound; + Footsteps_Pitches: constant array (Footsteps_Range) of C_Float := [1.7, 1.6, 1.5, 1.4]; + package Random_Footsteps is + new Ada.Numerics.Discrete_Random(Result_Subtype => Footsteps_Range); + Footsteps_Gen: Random_Footsteps.Generator; + Blast_Sound: Sound; + Key_Pickup_Sound: Sound; + Bomb_Pickup_Sound: Sound; + Open_Door_Sound: Sound; + Checkpoint_Sound: Sound; + Plant_Bomb_Sound: Sound; + Guard_Step_Sound: Sound; + Ambient_Music: Music; + + DEVELOPMENT : constant Boolean := False; + + type Palette is ( + COLOR_BACKGROUND, + COLOR_FLOOR, + COLOR_WALL, + COLOR_BARRICADE, + COLOR_PLAYER, + COLOR_DOORKEY, + COLOR_BOMB, + COLOR_LABEL, + COLOR_GUARD, + COLOR_MOTHER, + COLOR_CHECKPOINT, + COLOR_EXPLOSION, + COLOR_HEALTHBAR, + COLOR_EYES, + COLOR_FATHER); + + type Byte is mod 256; + type HSV_Comp is (Hue, Sat, Value); + type HSV is array (HSV_Comp) of Byte; + + function HSV_To_RGB(C: HSV) return Color is + H: constant C_Float := C_Float(C(Hue))/255.0*360.0; + S: constant C_Float := C_Float(C(Sat))/255.0; + V: constant C_Float := C_Float(C(Value))/255.0; + begin + return Color_From_HSV(H, S, V); + end; + + Palette_RGB: array (Palette) of Color := [others => (A => 255, others => 0)]; + Palette_HSV: array (Palette) of HSV := [others => [others => 0]]; + + package Double_IO is new Ada.Text_IO.Float_IO(Double); + + procedure Save_Colors(File_Name: String) is + F: File_Type; + begin + Create(F, Out_File, File_Name); + for C in Palette loop + Put(F, C'Image); + for Comp in HSV_Comp loop + Put(F, Palette_HSV(C)(Comp)'Image); + end loop; + Put_Line(F, ""); + end loop; + Close(F); + end; + + procedure Load_Colors(File_Name: String) is + F: File_Type; + Line_Number : Integer := 0; + begin + Open(F, In_File, File_Name); + while not End_Of_File(F) loop + Line_Number := Line_Number + 1; + declare + Line: Unbounded_String := To_Unbounded_String(Get_Line(F)); + + function Chop_By(Src: in out Unbounded_String; Pattern: String) return Unbounded_String is + Space_Index: constant Integer := Index(Src, Pattern); + Result: Unbounded_String; + begin + if Space_Index = 0 then + Result := Src; + Src := Null_Unbounded_String; + else + Result := Unbounded_Slice(Src, 1, Space_Index - 1); + Src := Unbounded_Slice(Src, Space_Index + 1, Length(Src)); + end if; + + return Result; + end; + function Find_Color_By_Key(Key: Unbounded_String; Co: out Palette) return Boolean is + begin + for C in Palette loop + if Key = C'Image then + Co := C; + return True; + end if; + end loop; + return False; + end; + C: Palette; + Key: constant Unbounded_String := Chop_By(Line, " "); + begin + Line := Trim(Line, Ada.Strings.Left); + if Find_Color_By_Key(Key, C) then + Line := Trim(Line, Ada.Strings.Left); + Palette_HSV(C)(Hue) := Byte'Value(To_String(Chop_By(Line, " "))); + Line := Trim(Line, Ada.Strings.Left); + Palette_HSV(C)(Sat) := Byte'Value(To_String(Chop_By(Line, " "))); + Line := Trim(Line, Ada.Strings.Left); + Palette_HSV(C)(Value) := Byte'Value(To_String(Chop_By(Line, " "))); + Palette_RGB(C) := Color_From_HSV(C_Float(Palette_HSV(C)(Hue))/255.0*360.0, C_Float(Palette_HSV(C)(Sat))/255.0, C_Float(Palette_HSV(C)(Value))/255.0); + else + Put_Line(File_Name & ":" & Line_Number'Image & "WARNING: Unknown Palette Color: """ & To_String(Key) & """"); + end if; + end; + end loop; + Close(F); + exception + when E: Name_Error => + Put_Line("WARNING: could not load colors from file " & File_Name & ": " & Exception_Message(E)); + end; + + TURN_DURATION_SECS : constant Float := 0.125; + GUARD_ATTACK_COOLDOWN : constant Integer := 10; + EEPER_EXPLOSION_DAMAGE : constant Float := 0.45; + GUARD_TURN_REGENERATION : constant Float := 0.01; + BOMB_GENERATOR_COOLDOWN : constant Integer := 10; + GUARD_STEPS_LIMIT : constant Integer := 4; + GUARD_STEP_LENGTH_LIMIT : constant Integer := 100; + EXPLOSION_LENGTH : constant Integer := 10; + EYES_ANGULAR_VELOCITY : constant Float := 10.0; + + type IVector2 is record + X, Y: Integer; + end record; + + type Cell is (Cell_None, Cell_Floor, Cell_Wall, Cell_Barricade, Cell_Door, Cell_Explosion); + Cell_Size : constant Vector2 := (x => 50.0, y => 50.0); + + function Cell_Colors(C: Cell) return Color is + begin + case C is + when Cell_None => return Palette_RGB(COLOR_BACKGROUND); + when Cell_Floor => return Palette_RGB(COLOR_FLOOR); + when Cell_Wall => return Palette_RGB(COLOR_WALL); + when Cell_Barricade => return Palette_RGB(COLOR_BARRICADE); + when Cell_Door => return Palette_RGB(COLOR_DOORKEY); + when Cell_Explosion => return Palette_RGB(COLOR_EXPLOSION); + end case; + end; + + type Path_Map is array (Positive range <>, Positive range <>) of Integer; + type Path_Map_Access is access Path_Map; + procedure Delete_Path_Map is new Ada.Unchecked_Deallocation(Path_Map, Path_Map_Access); + type Map is array (Positive range <>, Positive range <>) of Cell; + type Map_Access is access Map; + procedure Delete_Map is new Ada.Unchecked_Deallocation(Map, Map_Access); + + function "<="(A, B: IVector2) return Boolean is + begin + return A.X <= B.X and then A.Y <= B.Y; + end; + + function "<"(A, B: IVector2) return Boolean is + begin + return A.X < B.X and then A.Y < B.Y; + end; + + function "="(A, B: IVector2) return Boolean is + begin + return A.X = B.X and then A.Y = B.Y; + end; + + function "+"(A, B: IVector2) return IVector2 is + begin + return (A.X + B.X, A.Y + B.Y); + end; + + function "-"(A, B: IVector2) return IVector2 is + begin + return (A.X - B.X, A.Y - B.Y); + end; + + function "*"(A: IVector2; S: Integer) return IVector2 is + begin + return (A.X*S, A.Y*S); + end; + + function Equivalent_IVector2(Left, Right: IVector2) return Boolean is + begin + return Left.X = Right.X and then Left.Y = Right.Y; + end; + + function Hash_IVector2(V: IVector2) return Hash_Type is + M31: constant Hash_Type := 2**31-1; -- a nice Mersenne prime + begin + return Hash_Type(V.X) * M31 + Hash_Type(V.Y); + end; + + type Item_Kind is (Item_Key, Item_Bomb_Gen, Item_Checkpoint); + + type Item(Kind: Item_Kind := Item_Key) is record + case Kind is + when Item_Bomb_Gen => + Cooldown: Integer; + when others => null; + end case; + end record; + + package Hashed_Map_Items is new + Ada.Containers.Hashed_Maps( + Key_Type => IVector2, + Element_Type => Item, + Hash => Hash_IVector2, + Equivalent_Keys => Equivalent_IVector2); + + function To_Vector2(iv: IVector2) return Vector2 is + begin + return (X => C_float(iv.X), Y => C_float(iv.Y)); + end; + + type Eyes_Kind is (Eyes_Open, Eyes_Closed, Eyes_Angry, Eyes_Cringe, Eyes_Surprised); + type Eye_Mesh is new Vector2_Array(1..4); + type Eye is (Left_Eye, Right_Eye); + type Eyes_Mesh is array (Eye) of Eye_Mesh; + Eyes_Meshes: constant array (Eyes_Kind) of Eyes_Mesh := [ + Eyes_Open => [ + -- 1-3 + -- |/| + -- 2-4 + Left_Eye => [ (0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0) ], + -- 3-4 + -- |\| + -- 1-2 + Right_Eye => [ (0.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 0.0) ] + ], + Eyes_Closed => [ + Left_Eye => [ (0.0, 0.8), (0.0, 1.0), (1.0, 0.8), (1.0, 1.0) ], + Right_Eye => [ (0.0, 1.0), (1.0, 1.0), (0.0, 0.8), (1.0, 0.8) ] + ], + Eyes_Angry => [ + Left_Eye => [ (0.0, 0.0), (0.0, 1.0), (1.0, 0.3), (1.0, 1.0) ], + Right_Eye => [ (0.0, 1.0), (1.0, 1.0), (0.0, 0.3), (1.0, 0.0) ] + ], + Eyes_Cringe => [ + Left_Eye => [ (0.0, 0.5), (0.5, 0.75), (1.3, 0.75), (0.0, 1.0) ], + Right_Eye => [ (1.0, 1.0), (0.5, 0.75), (-0.3, 0.75), (1.0, 0.5) ] + ], + Eyes_Surprised => [ + Left_Eye => [ (0.0, 0.3), (0.0, 1.0), (1.0, 0.3), (1.0, 1.0) ], + Right_Eye => [ (0.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 0.0) ] + ] + ]; + + type Player_State is record + Prev_Position: IVector2; + Position: IVector2; + Prev_Eyes: Eyes_Kind; + Eyes: Eyes_Kind; + Eyes_Angle: Float; + Eyes_Target: IVector2; + Keys: Integer := 0; + Bombs: Integer := 0; + Bomb_Slots: Integer := 1; + Dead: Boolean := False; + end record; + + type Eeper_Kind is (Eeper_Guard, Eeper_Mother, Eeper_Gnome, Eeper_Father); + + type Eeper_State is record + Kind: Eeper_Kind; + Dead: Boolean := True; + Position, Prev_Position: IVector2; + Eyes_Angle: Float; + Eyes_Target: IVector2; + Prev_Eyes: Eyes_Kind; + Eyes: Eyes_Kind := Eyes_Closed; + Size: IVector2; + Path: Path_Map_Access; + + Background: Palette; + Health: Float := 1.0; + Attack_Cooldown: Integer := GUARD_ATTACK_COOLDOWN; + end record; + + type Bomb_State is record + Position: IVector2; + Countdown: Integer := 0; + end record; + + type Bomb_State_Array is array (1..10) of Bomb_State; + + type Eeper_Index is range 1..15; + type Eeper_Array is array (Eeper_Index) of Eeper_State; + + type Checkpoint_State is record + Map: Map_Access := Null; + Player_Position: IVector2; + Player_Keys: Integer; + Player_Bombs: Integer; + Player_Bomb_Slots: Integer; + Eepers: Eeper_Array; + Items: Hashed_Map_Items.Map; + Bombs: Bomb_State_Array; + end record; + + type Game_State is record + Map: Map_Access := Null; + Player: Player_State; + Eepers: Eeper_Array; + + Turn_Animation: Float := 0.0; + + Items: Hashed_Map_Items.Map; + Bombs: Bomb_State_Array; + Camera_Position: Vector2 := (x => 0.0, y => 0.0); + + Checkpoint: Checkpoint_State; + + Duration_Of_Last_Turn: Double; + end record; + + function Within_Map(Game: Game_State; Position: IVector2) return Boolean is + begin + return Position.Y in Game.Map'Range(1) and then Position.X in Game.Map'Range(2); + end; + + function Clone_Map(M0: Map_Access) return Map_Access is + M1: Map_Access; + begin + M1 := new Map(M0'Range(1), M0'Range(2)); + M1.all := M0.all; + return M1; + end; + + type Direction is (Left, Right, Up, Down); + + Direction_Vector: constant array (Direction) of IVector2 := [ + Left => (X => -1, Y => 0), + Right => (X => 1, Y => 0), + Up => (X => 0, Y => -1), + Down => (X => 0, Y => 1)]; + + function Inside_Of_Rect(Start, Size, Point: in IVector2) return Boolean is + begin + return Start <= Point and then Point < Start + Size; + end; + + function Eeper_Can_Stand_Here(Game: Game_State; Start: IVector2; Me: Eeper_Index) return Boolean is + Size: constant IVector2 := Game.Eepers(Me).Size; + begin + for X in Start.X..Start.X+Size.X-1 loop + for Y in Start.Y..Start.Y+Size.Y-1 loop + if not Within_Map(Game, (X, Y)) then + return False; + end if; + -- NOTE: it's fine to step into the explosions, because they don't live long enough. + -- They disappear on the next turn. + if Game.Map(Y, X) /= Cell_Floor and Game.Map(Y, X) /= Cell_Explosion then + return False; + end if; + for Index in Eeper_Index loop + if not Game.Eepers(Index).Dead and then Index /= Me then + declare + Eeper : constant Eeper_State := Game.Eepers(Index); + begin + if Inside_Of_Rect(Eeper.Position, Eeper.Size, (X, Y)) then + return False; + end if; + end; + end if; + end loop; + end loop; + end loop; + return True; + end; + + package Queue is new + Ada.Containers.Vectors(Index_Type => Natural, Element_Type => IVector2); + + procedure Recompute_Path_For_Eeper + (Game: in out Game_State; + Me: Eeper_Index; + Steps_Limit: Integer; + Step_Length_Limit: Integer; + Stop_At_Me: Boolean := True) + is + Q: Queue.Vector; + Eeper: Eeper_State renames Game.Eepers(Me); + begin + for Y in Eeper.Path'Range(1) loop + for X in Eeper.Path'Range(2) loop + Eeper.Path(Y, X) := -1; + end loop; + end loop; + + for Dy in 0..Eeper.Size.Y-1 loop + for Dx in 0..Eeper.Size.X-1 loop + declare + Position: constant IVector2 := Game.Player.Position - (Dx, Dy); + begin + if Eeper_Can_Stand_Here(Game, Position, Me) then + Eeper.Path(Position.Y, Position.X) := 0; + Q.Append(Position); + end if; + end; + end loop; + end loop; + + while not Q.Is_Empty loop + declare + Position: constant IVector2 := Q(0); + begin + Q.Delete_First; + + if Stop_At_Me and then Position = Eeper.Position then + exit; + end if; + + if Eeper.Path(Position.Y, Position.X) >= Steps_Limit then + exit; + end if; + + for Dir in Direction loop + declare + New_Position: IVector2 := Position + Direction_Vector(Dir); + begin + for Limit in 1..Step_Length_Limit loop + if not Eeper_Can_Stand_Here(Game, New_Position, Me) then + exit; + end if; + if Eeper.Path(New_Position.Y, New_Position.X) >= 0 then + exit; + end if; + Eeper.Path(New_Position.Y, New_Position.X) := Eeper.Path(Position.Y, Position.X) + 1; + Q.Append(New_Position); + New_Position := New_Position + Direction_Vector(Dir); + end loop; + end; + end loop; + end; + end loop; + end; + + procedure Game_Save_Checkpoint(Game: in out Game_State) is + begin + if Game.Checkpoint.Map /= null then + Delete_Map(Game.Checkpoint.Map); + end if; + Game.Checkpoint.Map := Clone_Map(Game.Map); + Game.Checkpoint.Player_Position := Game.Player.Position; + Game.Checkpoint.Player_Keys := Game.Player.Keys; + Game.Checkpoint.Player_Bombs := Game.Player.Bombs; + Game.Checkpoint.Player_Bomb_Slots := Game.Player.Bomb_Slots; + Game.Checkpoint.Eepers := Game.Eepers; + Game.Checkpoint.Items := Game.Items; + Game.Checkpoint.Bombs := Game.Bombs; + end; + + procedure Game_Restore_Checkpoint(Game: in out Game_State) is + begin + if Game.Map /= null then + Delete_Map(Game.Map); + end if; + Game.Map := Clone_Map(Game.Checkpoint.Map); + Game.Player.Position := Game.Checkpoint.Player_Position; + Game.Player.Keys := Game.Checkpoint.Player_Keys; + Game.Player.Bombs := Game.Checkpoint.Player_Bombs; + Game.Player.Bomb_Slots := Game.Checkpoint.Player_Bomb_Slots; + Game.Eepers := Game.Checkpoint.Eepers; + Game.Items := Game.Checkpoint.Items; + Game.Bombs := Game.Checkpoint.Bombs; + end; + + Too_Many_Entities: exception; + + function Allocate_Eeper(Game: in out Game_State) return Eeper_Index is + begin + for Eeper in Game.Eepers'Range loop + if Game.Eepers(Eeper).Dead then + Game.Eepers(Eeper).Dead := False; + return Eeper; + end if; + end loop; + raise Too_Many_Entities; + end; + + procedure Spawn_Gnome(Game: in out Game_State; Position: IVector2) is + Gnome: Eeper_State renames Game.Eepers(Allocate_Eeper(Game)); + Size: constant IVector2 := (1, 1); + begin + Gnome.Kind := Eeper_Gnome; + Gnome.Prev_Eyes := Eyes_Closed; + Gnome.Eyes := Eyes_Closed; + Gnome.Eyes_Target := Position + (Size.X/2, Size.Y); + Gnome.Background := COLOR_DOORKEY; + Gnome.Position := Position; + Gnome.Prev_Position := Position; + Gnome.Size := Size; + end; + + procedure Spawn_Father(Game: in out Game_State; Position: IVector2) is + Father: Eeper_State renames Game.Eepers(Allocate_Eeper(Game)); + Size: constant IVector2 := (7, 7); + begin + Father.Kind := Eeper_Father; + Father.Prev_Eyes := Eyes_Closed; + Father.Eyes := Eyes_Closed; + Father.Eyes_Angle := Pi*0.5; + Father.Eyes_Target := Position + (Size.X/2, Size.Y); + Father.Background := COLOR_FATHER; + Father.Position := Position; + Father.Prev_Position := Position; + Father.Size := Size; + end; + + procedure Spawn_Mother(Game: in out Game_State; Position: IVector2) is + Mother: Eeper_State renames Game.Eepers(Allocate_Eeper(Game)); + Size: constant IVector2 := (7, 7); + begin + Mother.Kind := Eeper_Mother; + Mother.Prev_Eyes := Eyes_Closed; + Mother.Eyes := Eyes_Closed; + Mother.Eyes_Target := Position + (Size.X/2, Size.Y); + Mother.Background := COLOR_MOTHER; + Mother.Position := Position; + Mother.Prev_Position := Position; + Mother.Health := 1.0; + Mother.Size := Size; + end; + + procedure Spawn_Guard(Game: in out Game_State; Position: IVector2) is + Guard: Eeper_State renames Game.Eepers(Allocate_Eeper(Game)); + Size: constant IVector2 := (3, 3); + begin + Guard.Kind := Eeper_Guard; + Guard.Prev_Eyes := Eyes_Closed; + Guard.Eyes := Eyes_Closed; + Guard.Eyes_Target := Position + (Size.X/2, Size.Y); + Guard.Background := COLOR_GUARD; + Guard.Position := Position; + Guard.Prev_Position := Position; + Guard.Health := 1.0; + Guard.Size := Size; + Guard.Attack_Cooldown := GUARD_ATTACK_COOLDOWN; + end; + + type Level_Cell is ( + Level_None, + Level_Gnome, + Level_Mother, + Level_Guard, + Level_Floor, + Level_Wall, + Level_Door, + Level_Checkpoint, + Level_Bomb_Gen, + Level_Barricade, + Level_Key, + Level_Player, + Level_Father); + Level_Cell_Color: constant array (Level_Cell) of Color := [ + Level_None => Get_Color(16#00000000#), + Level_Gnome => Get_Color(16#FF9600FF#), + Level_Mother => Get_Color(16#96FF00FF#), + Level_Guard => Get_Color(16#00FF00FF#), + Level_Floor => Get_Color(16#FFFFFFFF#), + Level_Wall => Get_Color(16#000000FF#), + Level_Door => Get_Color(16#00FFFFFF#), + Level_Checkpoint => Get_Color(16#FF00FFFF#), + Level_Bomb_Gen => Get_Color(16#FF0000FF#), + Level_Barricade => Get_Color(16#FF0096FF#), + Level_Key => Get_Color(16#FFFF00FF#), + Level_Player => Get_Color(16#0000FFFF#), + Level_Father => Get_Color(16#265FDAFF#)]; + + function Cell_By_Color(Col: Color; Out_Cel: out Level_Cell) return Boolean is + begin + for Cel in Level_Cell loop + if Level_Cell_Color(Cel) = Col then + Out_Cel := Cel; + return True; + end if; + end loop; + return False; + end; + + function Screen_Size return Vector2 is + begin + return To_Vector2((Integer(Get_Screen_Width), Integer(Get_Screen_Height))); + end; + + procedure Load_Game_From_Image(File_Name: in String; Game: in out Game_State; Update_Player: Boolean) is + type Color_Array is array (Natural range <>) of aliased Raylib.Color; + package Color_Pointer is new Interfaces.C.Pointers( + Index => Natural, + Element => Raylib.Color, + Element_Array => Color_Array, + Default_Terminator => (others => 0)); + function To_Color_Pointer is new Ada.Unchecked_Conversion (Raylib.Addr, Color_Pointer.Pointer); + use Color_Pointer; + + Img: constant Image := Raylib.Load_Image(To_C(File_Name)); + Pixels: constant Color_Pointer.Pointer := To_Color_Pointer(Img.Data); + begin + if Game.Map /= null then + Delete_Map(Game.Map); + end if; + Game.Map := new Map(1..Integer(Img.Height), 1..Integer(Img.Width)); + + for Eeper of Game.Eepers loop + Eeper.Dead := True; + if Eeper.Path /= null then + Delete_Path_Map(Eeper.Path); + end if; + Eeper.Path := new Path_Map(1..Integer(Img.Height), 1..Integer(Img.Width)); + for Y in Eeper.Path'Range(1) loop + for X in Eeper.Path'Range(2) loop + Eeper.Path(Y, X) := -1; + end loop; + end loop; + end loop; + + Game.Items.Clear; + for Bomb of Game.Bombs loop + Bomb.Countdown := 0; + end loop; + + for Row in Game.Map'Range(1) loop + for Column in Game.Map'Range(2) loop + declare + Index: constant Ptrdiff_T := Ptrdiff_T((Row - 1)*Integer(Img.Width) + (Column - 1)); + Pixel: constant Color_Pointer.Pointer := Pixels + Index; + Cel: Level_Cell; + begin + if Cell_By_Color(Pixel.all, Cel) then + case Cel is + when Level_None => + Game.Map(Row, Column) := Cell_None; + when Level_Gnome => + Spawn_Gnome(Game, (Column, Row)); + Game.Map(Row, Column) := Cell_Floor; + when Level_Mother => + Spawn_Mother(Game, (Column, Row)); + Game.Map(Row, Column) := Cell_Floor; + when Level_Guard => + Spawn_Guard(Game, (Column, Row)); + Game.Map(Row, Column) := Cell_Floor; + when Level_Father => + Game.Camera_Position := Screen_Size*0.5 - (To_Vector2((Column, Row))*Cell_Size + To_Vector2((7, 7))*Cell_Size*0.5); + Spawn_Father(Game, (Column, Row)); + Game.Map(Row, Column) := Cell_Floor; + when Level_Floor => Game.Map(Row, Column) := Cell_Floor; + when Level_Wall => Game.Map(Row, Column) := Cell_Wall; + when Level_Door => Game.Map(Row, Column) := Cell_Door; + when Level_Checkpoint => + Game.Map(Row, Column) := Cell_Floor; + Game.Items.Insert((Column, Row), (Kind => Item_Checkpoint)); + when Level_Bomb_Gen => + Game.Map(Row, Column) := Cell_Floor; + Game.Items.Insert((Column, Row), (Kind => Item_Bomb_Gen, Cooldown => 0)); + when Level_Barricade => + Game.Map(Row, Column) := Cell_Barricade; + when Level_Key => + Game.Map(Row, Column) := Cell_Floor; + Game.Items.Insert((Column, Row), (Kind => Item_Key)); + when Level_Player => + Game.Map(Row, Column) := Cell_Floor; + if Update_Player then + Game.Player.Position := (Column, Row); + Game.Player.Prev_Position := (Column, Row); + -- TODO: should we save the state of the eyes in the checkpoint? + -- Or maybe checkpoint should just save the entirety of the Player_State. + -- 'Cause that's what we do Eepers anyway. It works for them. + Game.Player.Prev_Eyes := Eyes_Closed; + Game.Player.Eyes := Eyes_Open; + Game.Player.Eyes_Angle := Pi*0.5; + Game.Player.Eyes_Target := Game.Player.Position + Direction_Vector(Up); + end if; + end case; + else + Game.Map(Row, Column) := Cell_None; + end if; + end; + end loop; + end loop; + + Stop_Music_Stream(Ambient_Music); + Play_Music_Stream(Ambient_Music); + end; + + procedure Draw_Bomb(Position: IVector2; C: Color) is + begin + Draw_Circle_V(To_Vector2(Position)*Cell_Size + Cell_Size*0.5, Cell_Size.X*0.5, C); + end; + + procedure Draw_Key(Position: IVector2) is + begin + Draw_Circle_V(To_Vector2(Position)*Cell_Size + Cell_Size*0.5, Cell_Size.X*0.25, Palette_RGB(COLOR_DOORKEY)); + end; + + procedure Draw_Number(Start, Size: Vector2; N: Integer; C: Color) is + Label: constant Char_Array := To_C(Trim(Integer'Image(N), Ada.Strings.Left)); + Label_Height: constant Integer := 32; + Label_Width: constant Integer := Integer(Measure_Text(Label, Int(Label_Height))); + Text_Size: constant Vector2 := To_Vector2((Label_Width, Label_Height)); + Position: constant Vector2 := Start + Size*0.5 - Text_Size*0.5; + begin + Draw_Text(Label, Int(Position.X), Int(Position.Y), Int(Label_Height), C); + end; + + procedure Draw_Number(Cell_Position: IVector2; N: Integer; C: Color) is + begin + Draw_Number(To_Vector2(Cell_Position)*Cell_Size, Cell_Size, N, C); + end; + + procedure Game_Cells(Game: in Game_State) is + begin + for Row in Game.Map'Range(1) loop + for Column in Game.Map'Range(2) loop + declare + Position: constant Vector2 := To_Vector2((Column, Row))*Cell_Size; + begin + Draw_Rectangle_V(position, cell_size, Cell_Colors(Game.Map(Row, Column))); + end; + end loop; + end loop; + end; + + procedure Game_Items(Game: in Game_State) is + use Hashed_Map_Items; + begin + for C in Game.Items.Iterate loop + case Element(C).Kind is + when Item_Key => Draw_Key(Key(C)); + when Item_Checkpoint => + declare + Checkpoint_Item_Size: constant Vector2 := Cell_Size*0.5; + begin + Draw_Rectangle_V(To_Vector2(Key(C))*Cell_Size + Cell_Size*0.5 - Checkpoint_Item_Size*0.5, Checkpoint_Item_Size, Palette_RGB(COLOR_CHECKPOINT)); + end; + when Item_Bomb_Gen => + if Element(C).Cooldown > 0 then + Draw_Bomb(Key(C), Color_Brightness(Palette_RGB(COLOR_BOMB), -0.5)); + Draw_Number(Key(C), Element(C).Cooldown, Palette_RGB(COLOR_LABEL)); + else + Draw_Bomb(Key(C), Palette_RGB(COLOR_BOMB)); + end if; + end case; + end loop; + end; + + procedure Flood_Fill(Game: in out Game_State; Start: IVector2; Fill: Cell) is + Q: Queue.Vector; + Background: Cell; + begin + if not Within_Map(Game, Start) then + return; + end if; + + Background := Game.Map(Start.Y, Start.X); + Game.Map(Start.Y, Start.X) := Fill; + Q.Append(Start); + + while not Q.Is_Empty loop + declare + Position: constant IVector2 := Q(0); + begin + Q.Delete_First; + + for Dir in Direction loop + declare + New_Position: constant IVector2 := Position + Direction_Vector(Dir); + begin + if Within_Map(Game, New_Position) and then Game.Map(New_Position.Y, New_Position.X) = Background then + Game.Map(New_Position.Y, New_Position.X) := Fill; + Q.Append(New_Position); + end if; + end; + end loop; + end; + end loop; + end; + + procedure Game_Player_Turn(Game: in out Game_State; Dir: Direction) is + New_Position: constant IVector2 := Game.Player.Position + Direction_Vector(Dir); + begin + Game.Player.Prev_Eyes := Game.Player.Eyes; + Game.Player.Prev_Position := Game.Player.Position; + Game.Player.Eyes_Target := New_Position + Direction_Vector(Dir); + + if not Within_Map(Game, New_Position) then + return; + end if; + + case Game.Map(New_Position.Y, New_Position.X) is + when Cell_Floor => + Game.Player.Position := New_Position; + declare + use Hashed_Map_Items; + C: Cursor := Game.Items.Find(New_Position); + begin + if Has_Element(C) then + case Element(C).Kind is + when Item_Key => + Game.Player.Keys := Game.Player.Keys + 1; + Game.Items.Delete(C); + Play_Sound(Key_Pickup_Sound); + when Item_Bomb_Gen => if + Game.Player.Bombs < Game.Player.Bomb_Slots + and then Element(C).Cooldown <= 0 + then + Game.Player.Bombs := Game.Player.Bombs + 1; + Game.Items.Replace_Element(C, (Kind => Item_Bomb_Gen, Cooldown => BOMB_GENERATOR_COOLDOWN)); + Play_Sound(Bomb_Pickup_Sound); + end if; + when Item_Checkpoint => + Game.Items.Delete(C); + Game.Player.Bombs := Game.Player.Bomb_Slots; + Game_Save_Checkpoint(Game); + Play_Sound(Checkpoint_Sound); + end case; + end if; + end; + when Cell_Door => + if Game.Player.Keys > 0 then + Game.Player.Keys := Game.Player.Keys - 1; + Flood_Fill(Game, New_Position, Cell_Floor); + Game.Player.Position := New_Position; + Play_Sound(Open_Door_Sound); + end if; + when others => null; + end case; + Play_Sound(Footsteps_Sounds(Random_Footsteps.Random(Footsteps_Gen))); + end; + + procedure Explode(Game: in out Game_State; Position: in IVector2) is + procedure Explode_Line(Dir: Direction) is + New_Position: IVector2 := Position; + begin + Line: for I in 1..EXPLOSION_LENGTH loop + if not Within_Map(Game, New_Position) then + return; + end if; + + case Game.Map(New_Position.Y, New_Position.X) is + when Cell_Floor | Cell_Explosion => + Game.Map(New_Position.Y, New_Position.X) := Cell_Explosion; + + if New_Position = Game.Player.Position then + Game.Player.Dead := True; + return; + end if; + + for Eeper of Game.Eepers loop + if not Eeper.Dead and then Inside_Of_Rect(Eeper.Position, Eeper.Size, New_Position) then + case Eeper.Kind is + when Eeper_Father => null; + when Eeper_Gnome => + Game.Items.Insert(Eeper.Position, (Kind => Item_Key)); + Eeper.Dead := True; + when Eeper_Guard => + Eeper.Eyes := Eyes_Cringe; + Eeper.Health := Eeper.Health - EEPER_EXPLOSION_DAMAGE; + if Eeper.Health <= 0.0 then + Eeper.Dead := True; + end if; + when Eeper_Mother => + declare + Position: constant IVector2 := Eeper.Position; + begin + Eeper.Dead := True; + Spawn_Guard(Game, Position + (0, 0)); + Spawn_Guard(Game, Position + (4, 0)); + Spawn_Guard(Game, Position + (0, 4)); + Spawn_Guard(Game, Position + (4, 4)); + end; + end case; + return; + end if; + end loop; + + New_Position := New_Position + Direction_Vector(Dir); + when Cell_Barricade => + Flood_Fill(Game, New_Position, Cell_Floor); + return; + when others => + return; + end case; + end loop Line; + end; + begin + for Dir in Direction loop + Explode_Line(Dir); + end loop; + end; + + Keys: constant array (Direction) of int := [ + Left => KEY_A, + Right => KEY_D, + Up => KEY_W, + Down => KEY_S + ]; + + procedure Game_Update_Camera(Game: in out Game_State) is + Camera_Target: constant Vector2 := + Screen_Size*0.5 - To_Vector2(Game.Player.Position)*Cell_Size - Cell_Size*0.5; + Camera_Velocity: constant Vector2 := (Camera_Target - Game.Camera_Position)*2.0; + begin + Game.Camera_Position := Game.Camera_Position + Camera_Velocity*Get_Frame_Time; + end; + + function Game_Camera(Game: in Game_State) return Camera2D is + begin + return ( + offset => Game.Camera_Position, + target => (x => 0.0, y => 0.0), + rotation => 0.0, + zoom => 1.0); + end; + + function Interpolate_Positions(IPrev_Position, IPosition: IVector2; T: Float) return Vector2 is + Prev_Position: constant Vector2 := To_Vector2(IPrev_Position)*Cell_Size; + Curr_Position: constant Vector2 := To_Vector2(IPosition)*Cell_Size; + begin + return Vector2_Lerp(Prev_Position, Curr_Position, C_Float(1.0 - T*T)); + end; + + Space_Pressed: Boolean := False; + Dir_Pressed: array (Direction) of Boolean := [others => False]; + Any_Key_Pressed: Boolean := False; + + procedure Swallow_Player_Input is + begin + Space_Pressed := False; + Any_Key_Pressed := False; + Dir_Pressed := [others => False]; + end; + + procedure Game_Bombs_Turn(Game: in out Game_State) is + begin + for Bomb of Game.Bombs loop + if Bomb.Countdown > 0 then + Bomb.Countdown := Bomb.Countdown - 1; + if Bomb.Countdown <= 0 then + Play_Sound(Blast_Sound); + Explode(Game, Bomb.Position); + end if; + end if; + end loop; + end; + + procedure Game_Explosions_Turn(Game: in out Game_State) is + begin + for Y in Game.Map'Range(1) loop + for X in Game.Map'Range(2) loop + if Game.Map(Y, X) = Cell_Explosion then + Game.Map(Y, X) := Cell_Floor; + end if; + end loop; + end loop; + end; + + function Look_At(Looker, Target: Vector2) return Float is + begin + return -Float(Vector2_Line_Angle(Looker, Target)); + end; + + procedure Game_Eepers_Turn(Game: in out Game_State) is + begin + for Me in Eeper_Index loop + declare + Eeper: Eeper_State renames Game.Eepers(Me); + begin + if not Eeper.Dead then + Eeper.Prev_Position := Eeper.Position; + Eeper.Prev_Eyes := Eeper.Eyes; + case Eeper.Kind is + when Eeper_Father => + declare + Wake_Up_Radius: constant IVector2 := (3, 3); + begin + if Inside_Of_Rect(Eeper.Position, Eeper.Size, Game.Player.Position) then + Load_Game_From_Image("assets/map.png", Game, True); + elsif Inside_Of_Rect(Eeper.Position - Wake_Up_Radius, Eeper.Size + Wake_Up_Radius*2, Game.Player.Position) then + Eeper.Eyes_Target := Game.Player.Position; + Eeper.Eyes := Eyes_Open; + else + Eeper.Eyes_Target := Eeper.Position + (Eeper.Size.X/2, Eeper.Size.Y); + Eeper.Eyes := Eyes_Closed; + end if; + end; + when Eeper_Guard | Eeper_Mother => + Recompute_Path_For_Eeper(Game, Me, GUARD_STEPS_LIMIT, GUARD_STEP_LENGTH_LIMIT); + if Eeper.Path(Eeper.Position.Y, Eeper.Position.X) = 0 then + Game.Player.Dead := True; + Eeper.Eyes := Eyes_Surprised; + elsif Eeper.Path(Eeper.Position.Y, Eeper.Position.X) > 0 then + if Eeper.Attack_Cooldown <= 0 then + declare + Current : constant Integer := Eeper.Path(Eeper.Position.Y, Eeper.Position.X); + Available_Positions: array (0..Direction_Vector'Length-1) of IVector2; + Count: Integer := 0; + begin + for Dir in Direction loop + declare + Position: IVector2 := Eeper.Position; + begin + while Eeper_Can_Stand_Here(Game, Position, Me) loop + Position := Position + Direction_Vector(Dir); + if Within_Map(Game, Position) and then Eeper.Path(Position.Y, Position.X) = Current - 1 then + Available_Positions(Count) := Position; + Count := Count + 1; + exit; + end if; + end loop; + end; + end loop; + if Count > 0 then + Eeper.Position := Available_Positions(Random_Integer.Random(Gen) mod Count); + Play_Sound(Guard_Step_Sound); + end if; + end; + Eeper.Attack_Cooldown := GUARD_ATTACK_COOLDOWN; + else + Eeper.Attack_Cooldown := Eeper.Attack_Cooldown - 1; + end if; + + if Eeper.Path(Eeper.Position.Y, Eeper.Position.X) = 1 then + Eeper.Eyes := Eyes_Angry; + else + Eeper.Eyes := Eyes_Open; + end if; + Eeper.Eyes_Target := Game.Player.Position; + + if Inside_Of_Rect(Eeper.Position, Eeper.Size, Game.Player.Position) then + Game.Player.Dead := True; + end if; + else + Eeper.Eyes := Eyes_Closed; + Eeper.Eyes_Target := Eeper.Position + (Eeper.Size.X/2, Eeper.Size.Y); + Eeper.Attack_Cooldown := GUARD_ATTACK_COOLDOWN + 1; + end if; + + if Eeper.Health < 1.0 then + Eeper.Health := Eeper.Health + GUARD_TURN_REGENERATION; + end if; + when Eeper_Gnome => + Recompute_Path_For_Eeper(Game, Me, 9, 1, Stop_At_Me => False); + declare + Position: constant IVector2 := Eeper.Position; + begin + if Eeper.Path(Position.Y, Position.X) >= 0 then + declare + Available_Positions: array (0..Direction_Vector'Length-1) of IVector2; + Count: Integer := 0; + begin + for Dir in Direction loop + declare + New_Position: constant IVector2 := Position + Direction_Vector(Dir); + begin + if Within_Map(Game, New_Position) + and then Game.Map(New_Position.Y, New_Position.X) = Cell_Floor + and then Eeper.Path(New_Position.Y, New_Position.X) > Eeper.Path(Position.Y, Position.X) + then + Available_Positions(Count) := New_Position; + Count := Count + 1; + end if; + end; + end loop; + + if Count > 0 then + Eeper.Position := Available_Positions(Random_Integer.Random(Gen) mod Count); + end if; + end; + Eeper.Eyes := Eyes_Open; + Eeper.Eyes_Target := Game.Player.Position; + else + Eeper.Eyes := Eyes_Closed; + Eeper.Eyes_Target := Eeper.Position + (Eeper.Size.X/2, Eeper.Size.Y); + end if; + end; + end case; + end if; + end; + end loop; + end; + + procedure Game_Items_Turn(Game: in out Game_State) is + use Hashed_Map_Items; + begin + for C in Game.Items.Iterate loop + if Element(C).Kind = Item_Bomb_Gen then + if Element(C).Cooldown > 0 then + Game.Items.Replace_Element(C, (Kind => Item_Bomb_Gen, Cooldown => Element(C).Cooldown - 1)); + end if; + end if; + end loop; + end; + + function Screen_Player_Position(Game: in Game_State) return Vector2 is + begin + if Game.Turn_Animation > 0.0 then + return Interpolate_Positions(Game.Player.Prev_Position, Game.Player.Position, Game.Turn_Animation); + else + return To_Vector2(Game.Player.Position)*Cell_Size; + end if; + end; + + function Clamp(X, Lo, Hi: Float) return Float is + begin + if X < Lo then + return Lo; + elsif X > Hi then + return Hi; + else + return X; + end if; + end; + + function Repeat(T, Length: Float) return Float is + function Floorf(A: C_Float) return C_Float + with + Import => True, + Convention => C, + External_Name => "floorf"; + begin + return Clamp(T - Float(Floorf(C_Float(T/Length)))*Length, 0.0, Length); + end; + + function Delta_Angle(A, B: Float) return Float is + Dlt: Float := Repeat(B - A, 2.0*Pi); + begin + if Dlt > Pi then + Dlt := Dlt - 2.0*Pi; + end if; + return Dlt; + end; + + procedure Draw_Eyes(Start, Size: Vector2; Angle: Float; Prev_Kind, Kind: Eyes_Kind; T: Float) is + Dir: constant Vector2 := Vector2_Rotate((1.0, 0.0), C_Float(Angle)); + Eyes_Ratio: constant Vector2 := (13.0/64.0, 23.0/64.0); + Eyes_Size: constant Vector2 := Eyes_Ratio*Size; + Center: constant Vector2 := Start + Size*0.5; + Position: constant Vector2 := Center + Dir*Eyes_Size.X*0.6; + Positions: constant array (Eye) of Vector2 := [ + Left_Eye => Position - Eyes_Size*(0.5, 0.0) - Eyes_Size*(1.0, 0.5), + Right_Eye => Position + Eyes_Size*(0.5, 0.0) - Eyes_Size*(0.0, 0.5) + ]; + Mesh: Eye_Mesh; + begin + for Eye_Index in Eye loop + for Vertex_Index in Eye_Mesh'Range loop + Mesh(Vertex_Index) := Positions(Eye_Index) + Eyes_Size*Vector2_Lerp(Eyes_Meshes(Prev_Kind)(Eye_Index)(Vertex_Index), Eyes_Meshes(Kind)(Eye_Index)(Vertex_Index), C_Float(1.0 - T*T)); + end loop; + Draw_Triangle_Strip(Mesh, Palette_RGB(COLOR_EYES)); + end loop; + end; + + procedure Game_Player(Game: in out Game_State) is + Eyes_Angular_Direction: constant Float := Delta_Angle(Game.Player.Eyes_Angle, Look_At(To_Vector2(Game.Player.Position)*Cell_Size + Cell_Size*0.5, To_Vector2(Game.Player.Eyes_Target)*Cell_Size + Cell_Size*0.5)); + begin + Game.Player.Eyes_Angle := Game.Player.Eyes_Angle + Eyes_Angular_Direction*EYES_ANGULAR_VELOCITY*Float(Get_Frame_Time); + if Game.Player.Dead then + if Game.Turn_Animation >= 0.0 then + Draw_Rectangle_V(Screen_Player_Position(Game), Cell_Size, Palette_RGB(COLOR_PLAYER)); + Draw_Eyes(Screen_Player_Position(Game), Cell_Size, Game.Player.Eyes_Angle, Game.Player.Prev_Eyes, Game.Player.Eyes, Game.Turn_Animation); + end if; + + if Any_Key_Pressed then + Game_Restore_Checkpoint(Game); + Game.Player.Dead := False; + end if; + + return; + end if; + + Draw_Rectangle_V(Screen_Player_Position(Game), Cell_Size, Palette_RGB(COLOR_PLAYER)); + Draw_Eyes(Screen_Player_Position(Game), Cell_Size, Game.Player.Eyes_Angle, Game.Player.Prev_Eyes, Game.Player.Eyes, Game.Turn_Animation); + + if Game.Turn_Animation > 0.0 then + return; + end if; + + if Space_Pressed and then Game.Player.Bombs > 0 then + declare + Start_Of_Turn: constant Double := Get_Time; + begin + Game.Turn_Animation := 1.0; + Game_Explosions_Turn(Game); + Game_Items_Turn(Game); + + -- Game_Player_Turn(Game, Dir); + Game.Player.Prev_Position := Game.Player.Position; + for Bomb of Game.Bombs loop + if Bomb.Countdown <= 0 then + Bomb.Countdown := 3; + Bomb.Position := Game.Player.Position; + exit; + end if; + end loop; + Game.Player.Bombs := Game.Player.Bombs - 1; + Play_Sound(Plant_Bomb_Sound); + + Game_Eepers_Turn(Game); + -- Game_Bombs_Turn(Game); + Game.Duration_Of_Last_Turn := Get_Time - Start_Of_Turn; + end; + else + for Dir in Direction loop + if Dir_Pressed(Dir) then + declare + Start_Of_Turn: constant Double := Get_Time; + begin + Game.Turn_Animation := 1.0; + Game_Explosions_Turn(Game); + Game_Items_Turn(Game); + Game_Player_Turn(Game, Dir); + Game_Eepers_Turn(Game); + Game_Bombs_Turn(Game); + Game.Duration_Of_Last_Turn := Get_Time - Start_Of_Turn; + end; + end if; + end loop; + end if; + end; + + procedure Game_Bombs(Game: Game_State) is + begin + for Bomb of Game.Bombs loop + if Bomb.Countdown > 0 then + Draw_Bomb(Bomb.Position, Palette_RGB(COLOR_BOMB)); + Draw_Number(Bomb.Position, Bomb.Countdown, Palette_RGB(COLOR_LABEL)); + end if; + end loop; + end; + + procedure Game_Hud(Game: in Game_State) is + begin + for Index in 1..Game.Player.Keys loop + declare + Position: constant Vector2 := (100.0 + C_float(Index - 1)*Cell_Size.X, 100.0); + begin + Draw_Circle_V(Position, Cell_Size.X*0.25, Palette_RGB(COLOR_DOORKEY)); + end; + end loop; + + for Index in 1..Game.Player.Bombs loop + declare + Position: constant Vector2 := (100.0 + C_float(Index - 1)*Cell_Size.X, 200.0); + begin + Draw_Circle_V(Position, Cell_Size.X*0.5, Palette_RGB(COLOR_BOMB)); + end; + end loop; + + if Game.Player.Dead then + declare + Label: constant Char_Array := To_C("You Died!"); + Label_Height: constant Integer := 48; + Label_Width: constant Integer := Integer(Measure_Text(Label, Int(Label_Height))); + Text_Size: constant Vector2 := To_Vector2((Label_Width, Label_Height)); + Position: constant Vector2 := Screen_Size*0.5 - Text_Size*0.5; + begin + Draw_Text(Label, Int(Position.X), Int(Position.Y), Int(Label_Height), Palette_RGB(COLOR_LABEL)); + end; + end if; + end; + + procedure Health_Bar(Boundary_Start, Boundary_Size: Vector2; Health: C_Float) is + Health_Padding: constant C_Float := 10.0; + Health_Height: constant C_Float := 10.0; + Health_Width: constant C_Float := Boundary_Size.X*Health; + begin + Draw_Rectangle_V( + Boundary_Start - (0.0, Health_Padding + Health_Height), + (Health_Width, Health_Height), + Palette_RGB(COLOR_HEALTHBAR)); + end; + + + procedure Draw_Cooldown_Timer_Bubble(Start, Size: Vector2; Cooldown: Integer; Background: Palette) is + Text_Color: constant Color := (A => 255, others => 0); + Bubble_Radius: constant C_Float := 30.0; + Bubble_Center: constant Vector2 := Start + Size*(0.5, 0.0) - (0.0, Bubble_Radius*2.0); + begin + Draw_Circle_V(Bubble_Center, Bubble_Radius, Palette_RGB(Background)); + Draw_Number(Bubble_Center - (Bubble_Radius, Bubble_Radius), (Bubble_Radius, Bubble_Radius)*2.0, Cooldown, Text_Color); + end; + + procedure Game_Eepers(Game: in out Game_State) is + begin + for Eeper of Game.Eepers loop + declare + Position: constant Vector2 := + (if Game.Turn_Animation > 0.0 + then Interpolate_Positions(Eeper.Prev_Position, Eeper.Position, Game.Turn_Animation) + else To_Vector2(Eeper.Position)*Cell_Size); + Size: constant Vector2 := To_Vector2(Eeper.Size)*Cell_Size; + Eyes_Angular_Direction: constant Float := Delta_Angle(Eeper.Eyes_Angle, Look_At(Position + Size*0.5, To_Vector2(Eeper.Eyes_Target)*Cell_Size + Cell_Size*0.5)); + begin + if not Eeper.Dead then + Eeper.Eyes_Angle := Eeper.Eyes_Angle + Eyes_Angular_Direction*EYES_ANGULAR_VELOCITY*Float(Get_Frame_Time); + case Eeper.Kind is + when Eeper_Father => + Draw_Rectangle_V(Position, Size, Palette_RGB(Eeper.Background)); + Draw_Eyes(Position, Size, Eeper.Eyes_Angle, Eeper.Prev_Eyes, Eeper.Eyes, Game.Turn_Animation); + when Eeper_Guard | Eeper_Mother => + Draw_Rectangle_V(Position, Size, Palette_RGB(Eeper.Background)); + Health_Bar(Position, Size, C_Float(Eeper.Health)); + if Eeper.Path(Eeper.Position.Y, Eeper.Position.X) = 1 then + Draw_Cooldown_Timer_Bubble(Position, Size, Eeper.Attack_Cooldown, Eeper.Background); + elsif Eeper.Path(Eeper.Position.Y, Eeper.Position.X) >= 0 then + Draw_Cooldown_Timer_Bubble(Position, Size, Eeper.Attack_Cooldown, Eeper.Background); + end if; + Draw_Eyes(Position, Size, Eeper.Eyes_Angle, Eeper.Prev_Eyes, Eeper.Eyes, Game.Turn_Animation); + when Eeper_Gnome => + declare + GNOME_RATIO: constant C_Float := 0.7; + GNOME_SIZE: constant Vector2 := Cell_Size*GNOME_RATIO; + GNOME_START: constant Vector2 := Position + Cell_Size*0.5 - GNOME_SIZE*0.5; + begin + Draw_Rectangle_V(GNOME_START, GNOME_SIZE, Palette_RGB(Eeper.Background)); + Draw_Eyes(GNOME_START, GNOME_SIZE, Eeper.Eyes_Angle, Eeper.Prev_Eyes, Eeper.Eyes, Game.Turn_Animation); + end; + end case; + end if; + end; + end loop; + end; + + Game: Game_State; + Title: constant Char_Array := To_C("Eepers"); + + Palette_Editor: Boolean := False; + Palette_Editor_Choice: Palette := Palette'First; + Palette_Editor_Selected: Boolean := False; + Palette_Editor_Component: HSV_Comp := Hue; + +begin + if not Change_Directory(Get_Application_Directory) then + Put_Line("WARNING: Could not change working directory to the application directory"); + end if; + + Set_Config_Flags(FLAG_WINDOW_RESIZABLE); + Init_Window(1600, 900, Title); + Set_Target_FPS(60); + Set_Exit_Key(KEY_NULL); + + Init_Audio_Device; + for Index in Footsteps_Range loop + Footsteps_Sounds(Index) := Load_Sound(To_C("assets/sounds/footsteps.mp3")); + Set_Sound_Pitch(Footsteps_Sounds(Index), Footsteps_Pitches(Index)); + end loop; + Blast_Sound := Load_Sound(To_C("assets/sounds/blast.ogg")); -- https://opengameart.org/content/magic-sfx-sample + Key_Pickup_Sound := Load_Sound(To_C("assets/sounds/key-pickup.wav")); -- https://opengameart.org/content/beep-tone-sound-sfx + Ambient_Music := Load_Music_Stream(To_C("assets/sounds/ambient.wav")); -- https://opengameart.org/content/ambient-soundtrack + Set_Music_Volume(Ambient_Music, 0.5); + Bomb_Pickup_Sound := Load_Sound(To_C("assets/sounds/bomb-pickup.ogg")); -- https://opengameart.org/content/pickupplastic-sound + Open_Door_Sound := Load_Sound(To_C("assets/sounds/open-door.wav")); -- https://opengameart.org/content/picked-coin-echo + Set_Sound_Volume(Open_Door_Sound, 0.5); + Checkpoint_Sound := Load_Sound(To_C("assets/sounds/checkpoint.ogg")); -- https://opengameart.org/content/level-up-power-up-coin-get-13-sounds + Set_Sound_Pitch(Checkpoint_Sound, 0.8); + Guard_Step_Sound := Load_Sound(To_C("assets/sounds/guard-step.ogg")); -- https://opengameart.org/content/fire-whip-hit-yo-frankie + Plant_Bomb_Sound := Load_Sound(To_C("assets/sounds/plant-bomb.wav")); -- https://opengameart.org/content/ui-soundpack-by-m1chiboi-bleeps-and-clicks + + Random_Integer.Reset(Gen); + Load_Colors("assets/colors.txt"); + Load_Game_From_Image("assets/map.png", Game, True); + Game_Save_Checkpoint(Game); + Play_Music_Stream(Ambient_Music); + + while not Window_Should_Close loop + if Is_Music_Stream_Playing(Ambient_Music) then + Update_Music_Stream(Ambient_Music); + end if; + Begin_Drawing; + Clear_Background(Palette_RGB(COLOR_BACKGROUND)); + + Swallow_Player_Input; + Polling: loop + declare + Key: constant int := Get_Key_Pressed; + begin + case Key is + when KEY_NULL => exit Polling; + when KEY_SPACE => Space_Pressed := True; + when KEY_A | KEY_LEFT => Dir_Pressed(Left) := True; + when KEY_D | KEY_RIGHT => Dir_Pressed(Right) := True; + when KEY_S | KEY_DOWN => Dir_Pressed(Down) := True; + when KEY_W | KEY_UP => Dir_Pressed(Up) := True; + when others => null; + end case; + Any_Key_Pressed := True; + end; + end loop Polling; + + if DEVELOPMENT then + if Is_Key_Pressed(KEY_R) then + Load_Game_From_Image("assets/map.png", Game, False); + end if; + + if Is_Key_Pressed(KEY_O) then + Palette_Editor := not Palette_Editor; + if not Palette_Editor then + Save_Colors("assets/colors.txt"); + end if; + end if; + + if Palette_Editor then + if Palette_Editor_Selected then + if Is_Key_Pressed(KEY_ESCAPE) then + Palette_Editor_Selected := False; + end if; + + if Is_Key_Pressed(Keys(Left)) then + if Palette_Editor_Component /= HSV_Comp'First then + Palette_Editor_Component := HSV_Comp'Pred(Palette_Editor_Component); + end if; + end if; + + if Is_Key_Pressed(Keys(Right)) then + if Palette_Editor_Component /= HSV_Comp'Last then + Palette_Editor_Component := HSV_Comp'Succ(Palette_Editor_Component); + end if; + end if; + + if Is_Key_Down(Keys(Up)) then + Palette_HSV(Palette_Editor_Choice)(Palette_Editor_Component) := Palette_HSV(Palette_Editor_Choice)(Palette_Editor_Component) + 1; + Palette_RGB(Palette_Editor_Choice) := HSV_To_RGB(Palette_HSV(Palette_Editor_Choice)); + end if; + + if Is_Key_Down(Keys(Down)) then + Palette_HSV(Palette_Editor_Choice)(Palette_Editor_Component) := Palette_HSV(Palette_Editor_Choice)(Palette_Editor_Component) - 1; + Palette_RGB(Palette_Editor_Choice) := HSV_To_RGB(Palette_HSV(Palette_Editor_Choice)); + end if; + else + if Is_Key_Pressed(Keys(Down)) then + if Palette_Editor_Choice /= Palette'Last then + Palette_Editor_Choice := Palette'Succ(Palette_Editor_Choice); + end if; + end if; + + if Is_Key_Pressed(Keys(Up)) then + if Palette_Editor_Choice /= Palette'First then + Palette_Editor_Choice := Palette'Pred(Palette_Editor_Choice); + end if; + end if; + + if Is_Key_Pressed(KEY_ESCAPE) then + Palette_Editor := False; + end if; + + if Is_Key_Pressed(KEY_ENTER) then + Palette_Editor_Selected := True; + end if; + end if; + + Swallow_Player_Input; + end if; + end if; + + if Game.Turn_Animation > 0.0 then + Game.Turn_Animation := (Game.Turn_Animation*TURN_DURATION_SECS - Float(Get_Frame_Time))/TURN_DURATION_SECS; + end if; + + Game_Update_Camera(Game); + Begin_Mode2D(Game_Camera(Game)); + Game_Cells(Game); + Game_Items(Game); + Game_Player(Game); + Game_Eepers(Game); + Game_Bombs(Game); + if DEVELOPMENT then + if Is_Key_Down(KEY_P) then + for Row in Game.Map'Range(1) loop + for Column in Game.Map'Range(2) loop + Draw_Number((Column, Row), Game.Eepers(1).Path(Row, Column), (A => 255, others => 0)); + end loop; + end loop; + end if; + end if; + End_Mode2D; + + Game_Hud(Game); + if DEVELOPMENT then + Draw_FPS(10, 10); + declare + S: String(1..20); + begin + Double_IO.Put(S, Game.Duration_Of_Last_Turn, Exp => 0); + Draw_Text(To_C(S), 100, 10, 32, (others => 255)); + end; + end if; + + if Palette_Editor then + for C in Palette loop + declare + Label: constant Char_Array := To_C(C'Image); + Label_Height: constant Integer := 32; + Position: constant Vector2 := (200.0, 200.0 + C_Float(Palette'Pos(C))*C_Float(Label_Height)); + begin + Draw_Text(Label, Int(Position.X), Int(Position.Y), Int(Label_Height), + (if not Palette_Editor_Selected and C = Palette_Editor_Choice + then (R => 255, A => 255, others => 0) + else (others => 255))); + + for Comp in HSV_Comp loop + declare + Label: constant Char_Array := To_C(Comp'Image & ": " & Palette_HSV(C)(Comp)'Image); + Label_Height: constant Integer := 32; + Position: constant Vector2 := ( + X => 600.0 + 200.0*C_Float(HSV_Comp'Pos(Comp)), + Y => 200.0 + C_Float(Palette'Pos(C))*C_Float(Label_Height) + ); + begin + Draw_Text(Label, Int(Position.X), Int(Position.Y), Int(Label_Height), + (if Palette_Editor_Selected and C = Palette_Editor_Choice and Comp = Palette_Editor_Component + then (R => 255, A => 255, others => 0) + else (others => 255))); + end; + end loop; + end; + end loop; + end if; + End_Drawing; + end loop; + Close_Window; +end; + +-- TODO: Icon on for Windows build +-- TODO: Window title icon +-- TODO: Loop the music +-- TODO: Sound on Finishing Round +-- TODO: Footstep variation for Mother/Guard bosses (depending on the distance traveled?) +-- TODO: Footsteps for mother should be lower +-- TODO: Eyes_Cringe as triangles +-- The current ones look out of style +-- TODO: Restarting the game does not reset bombs and keys of the Player +-- TODO: Can you escape boss rooms using Gnomes? +-- It's very hard because you need to somehow put them behind yourself +-- TODO: Restarting should be considered a turn +-- It's very useful to update Path Maps and stuff. +-- Or maybe we should just save Path Maps too? +-- TODO: Items in HUD may sometimes blend with the background +-- TODO: If you are standing on the refilled bomb gen and place a bomb you should refill your bomb in that turn. +-- TODO: Checkpoints should be circles (like all the items) +-- TODO: Custom font +-- TODO: Determenistically choose the paths again, but make them more interesting +-- - Gnomes are just being deterministic +-- - Mother and Guard always pick the longest path. Or generally the path that brings the Euclidean Distance closer +-- TODO: Mother should require several attacks before being "split" +-- TODO: Do not stack up damage for Eepers per the tiles of their body. +-- The denote last direction of the step. +-- TODO: Enemies should attack on zero just like a bomb. +-- TODO: Properly disablable DEV features +-- TODO: Fullscreen mode +-- TODO: Don't reset cooldown timer for eepers (might be duplicate) +-- Maybe just pause +-- TODO: Try MSAA (if too slow, don't) +-- TODO: Show Eeper Cooldown timer outside of the screen somehow +-- TODO: Visual Clue that the Eeper is about to kill the Player when Completely outside of the Screen +-- - Cooldown ball is shaking +-- TODO: Cool animation for New Game +-- TODO: Tutorial sign that says "WASD" to move when you start the game for the first time. And how to place the bomb on picking it up +-- TODO: Keep steping while you are holding a certain direction +-- Cause constantly tapping it feels like ass. +-- TODO: count the player's turns towards the final score of the game +-- We can even collect different stats, like bombs collected, bombs used, +-- times died etc. +-- TODO: Animate key when you pick it up +-- Smoothly move it into the HUD. +-- TODO: Different palettes depending on the area +-- Or maybe different palette for each NG+ +-- TODO: Particles +-- - Player Death animation +-- - Eeper Death animation +-- - Cool effects when you pick up items and checkpoints +-- TODO: Camera shaking when big bosses (Guard and Mother) make moves +-- TODO: Menu +-- Could be just a splash with the game name and logo. +-- TODO: WebAssembly build +-- https://blog.adacore.com/use-of-gnat-llvm-to-translate-ada-applications-to-webassembly +-- TODO: Explosions should trigger other primed bombs? +-- TODO: Path finding in a separate thread +-- TODO: Eeper slide attack animation is pretty boring @polish +-- TODO: Bake assets into executable +-- TODO: Background is too boring +-- Maybe some sort of repeating pattern would be better. +-- TODO: Indicate how many bomb slots we have in HUD +-- TODO: Eyes of Father changing as the Player gets closer: +-- - Happy (very important to indicate that he's not hostile) +-- TODO: Transition Player's eyes linearly in Euclidean space instead of angularly. diff --git a/game.adb b/game.adb deleted file mode 100644 index 90e1a3f..0000000 --- a/game.adb +++ /dev/null @@ -1,1617 +0,0 @@ -with Ada.Text_IO; -with Text_IO; use Text_IO; -with Interfaces.C; use Interfaces.C; -with Raylib; use Raylib; -with Raymath; use Raymath; -with Ada.Strings.Unbounded; -use Ada.Strings.Unbounded; -with Ada.Containers.Vectors; -with Ada.Unchecked_Deallocation; -with Ada.Containers.Hashed_Maps; -use Ada.Containers; -with Ada.Strings.Fixed; use Ada.Strings.Fixed; -with Ada.Strings; -with Ada.Exceptions; use Ada.Exceptions; -with Ada.Numerics.Discrete_Random; -with Interfaces.C.Pointers; -with Ada.Unchecked_Conversion; -with Ada.Numerics; use Ada.Numerics; - -procedure Game is - package Random_Integer is - new Ada.Numerics.Discrete_Random(Result_Subtype => Integer); - Gen: Random_Integer.Generator; - - type Footsteps_Range is mod 4; - Footsteps_Sounds: array (Footsteps_Range) of Sound; - Footsteps_Pitches: constant array (Footsteps_Range) of C_Float := [1.7, 1.6, 1.5, 1.4]; - package Random_Footsteps is - new Ada.Numerics.Discrete_Random(Result_Subtype => Footsteps_Range); - Footsteps_Gen: Random_Footsteps.Generator; - Blast_Sound: Sound; - Key_Pickup_Sound: Sound; - Bomb_Pickup_Sound: Sound; - Open_Door_Sound: Sound; - Checkpoint_Sound: Sound; - Plant_Bomb_Sound: Sound; - Guard_Step_Sound: Sound; - Ambient_Music: Music; - - DEVELOPMENT : constant Boolean := False; - - type Palette is ( - COLOR_BACKGROUND, - COLOR_FLOOR, - COLOR_WALL, - COLOR_BARRICADE, - COLOR_PLAYER, - COLOR_DOORKEY, - COLOR_BOMB, - COLOR_LABEL, - COLOR_GUARD, - COLOR_MOTHER, - COLOR_CHECKPOINT, - COLOR_EXPLOSION, - COLOR_HEALTHBAR, - COLOR_EYES, - COLOR_FATHER); - - type Byte is mod 256; - type HSV_Comp is (Hue, Sat, Value); - type HSV is array (HSV_Comp) of Byte; - - function HSV_To_RGB(C: HSV) return Color is - H: constant C_Float := C_Float(C(Hue))/255.0*360.0; - S: constant C_Float := C_Float(C(Sat))/255.0; - V: constant C_Float := C_Float(C(Value))/255.0; - begin - return Color_From_HSV(H, S, V); - end; - - Palette_RGB: array (Palette) of Color := [others => (A => 255, others => 0)]; - Palette_HSV: array (Palette) of HSV := [others => [others => 0]]; - - package Double_IO is new Ada.Text_IO.Float_IO(Double); - - procedure Save_Colors(File_Name: String) is - F: File_Type; - begin - Create(F, Out_File, File_Name); - for C in Palette loop - Put(F, C'Image); - for Comp in HSV_Comp loop - Put(F, Palette_HSV(C)(Comp)'Image); - end loop; - Put_Line(F, ""); - end loop; - Close(F); - end; - - procedure Load_Colors(File_Name: String) is - F: File_Type; - Line_Number : Integer := 0; - begin - Open(F, In_File, File_Name); - while not End_Of_File(F) loop - Line_Number := Line_Number + 1; - declare - Line: Unbounded_String := To_Unbounded_String(Get_Line(F)); - - function Chop_By(Src: in out Unbounded_String; Pattern: String) return Unbounded_String is - Space_Index: constant Integer := Index(Src, Pattern); - Result: Unbounded_String; - begin - if Space_Index = 0 then - Result := Src; - Src := Null_Unbounded_String; - else - Result := Unbounded_Slice(Src, 1, Space_Index - 1); - Src := Unbounded_Slice(Src, Space_Index + 1, Length(Src)); - end if; - - return Result; - end; - function Find_Color_By_Key(Key: Unbounded_String; Co: out Palette) return Boolean is - begin - for C in Palette loop - if Key = C'Image then - Co := C; - return True; - end if; - end loop; - return False; - end; - C: Palette; - Key: constant Unbounded_String := Chop_By(Line, " "); - begin - Line := Trim(Line, Ada.Strings.Left); - if Find_Color_By_Key(Key, C) then - Line := Trim(Line, Ada.Strings.Left); - Palette_HSV(C)(Hue) := Byte'Value(To_String(Chop_By(Line, " "))); - Line := Trim(Line, Ada.Strings.Left); - Palette_HSV(C)(Sat) := Byte'Value(To_String(Chop_By(Line, " "))); - Line := Trim(Line, Ada.Strings.Left); - Palette_HSV(C)(Value) := Byte'Value(To_String(Chop_By(Line, " "))); - Palette_RGB(C) := Color_From_HSV(C_Float(Palette_HSV(C)(Hue))/255.0*360.0, C_Float(Palette_HSV(C)(Sat))/255.0, C_Float(Palette_HSV(C)(Value))/255.0); - else - Put_Line(File_Name & ":" & Line_Number'Image & "WARNING: Unknown Palette Color: """ & To_String(Key) & """"); - end if; - end; - end loop; - Close(F); - exception - when E: Name_Error => - Put_Line("WARNING: could not load colors from file " & File_Name & ": " & Exception_Message(E)); - end; - - TURN_DURATION_SECS : constant Float := 0.125; - GUARD_ATTACK_COOLDOWN : constant Integer := 10; - EEPER_EXPLOSION_DAMAGE : constant Float := 0.45; - GUARD_TURN_REGENERATION : constant Float := 0.01; - BOMB_GENERATOR_COOLDOWN : constant Integer := 10; - GUARD_STEPS_LIMIT : constant Integer := 4; - GUARD_STEP_LENGTH_LIMIT : constant Integer := 100; - EXPLOSION_LENGTH : constant Integer := 10; - EYES_ANGULAR_VELOCITY : constant Float := 10.0; - - type IVector2 is record - X, Y: Integer; - end record; - - type Cell is (Cell_None, Cell_Floor, Cell_Wall, Cell_Barricade, Cell_Door, Cell_Explosion); - Cell_Size : constant Vector2 := (x => 50.0, y => 50.0); - - function Cell_Colors(C: Cell) return Color is - begin - case C is - when Cell_None => return Palette_RGB(COLOR_BACKGROUND); - when Cell_Floor => return Palette_RGB(COLOR_FLOOR); - when Cell_Wall => return Palette_RGB(COLOR_WALL); - when Cell_Barricade => return Palette_RGB(COLOR_BARRICADE); - when Cell_Door => return Palette_RGB(COLOR_DOORKEY); - when Cell_Explosion => return Palette_RGB(COLOR_EXPLOSION); - end case; - end; - - type Path_Map is array (Positive range <>, Positive range <>) of Integer; - type Path_Map_Access is access Path_Map; - procedure Delete_Path_Map is new Ada.Unchecked_Deallocation(Path_Map, Path_Map_Access); - type Map is array (Positive range <>, Positive range <>) of Cell; - type Map_Access is access Map; - procedure Delete_Map is new Ada.Unchecked_Deallocation(Map, Map_Access); - - function "<="(A, B: IVector2) return Boolean is - begin - return A.X <= B.X and then A.Y <= B.Y; - end; - - function "<"(A, B: IVector2) return Boolean is - begin - return A.X < B.X and then A.Y < B.Y; - end; - - function "="(A, B: IVector2) return Boolean is - begin - return A.X = B.X and then A.Y = B.Y; - end; - - function "+"(A, B: IVector2) return IVector2 is - begin - return (A.X + B.X, A.Y + B.Y); - end; - - function "-"(A, B: IVector2) return IVector2 is - begin - return (A.X - B.X, A.Y - B.Y); - end; - - function "*"(A: IVector2; S: Integer) return IVector2 is - begin - return (A.X*S, A.Y*S); - end; - - function Equivalent_IVector2(Left, Right: IVector2) return Boolean is - begin - return Left.X = Right.X and then Left.Y = Right.Y; - end; - - function Hash_IVector2(V: IVector2) return Hash_Type is - M31: constant Hash_Type := 2**31-1; -- a nice Mersenne prime - begin - return Hash_Type(V.X) * M31 + Hash_Type(V.Y); - end; - - type Item_Kind is (Item_Key, Item_Bomb_Gen, Item_Checkpoint); - - type Item(Kind: Item_Kind := Item_Key) is record - case Kind is - when Item_Bomb_Gen => - Cooldown: Integer; - when others => null; - end case; - end record; - - package Hashed_Map_Items is new - Ada.Containers.Hashed_Maps( - Key_Type => IVector2, - Element_Type => Item, - Hash => Hash_IVector2, - Equivalent_Keys => Equivalent_IVector2); - - function To_Vector2(iv: IVector2) return Vector2 is - begin - return (X => C_float(iv.X), Y => C_float(iv.Y)); - end; - - type Eyes_Kind is (Eyes_Open, Eyes_Closed, Eyes_Angry, Eyes_Cringe, Eyes_Surprised); - type Eye_Mesh is new Vector2_Array(1..4); - type Eye is (Left_Eye, Right_Eye); - type Eyes_Mesh is array (Eye) of Eye_Mesh; - Eyes_Meshes: constant array (Eyes_Kind) of Eyes_Mesh := [ - Eyes_Open => [ - -- 1-3 - -- |/| - -- 2-4 - Left_Eye => [ (0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0) ], - -- 3-4 - -- |\| - -- 1-2 - Right_Eye => [ (0.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 0.0) ] - ], - Eyes_Closed => [ - Left_Eye => [ (0.0, 0.8), (0.0, 1.0), (1.0, 0.8), (1.0, 1.0) ], - Right_Eye => [ (0.0, 1.0), (1.0, 1.0), (0.0, 0.8), (1.0, 0.8) ] - ], - Eyes_Angry => [ - Left_Eye => [ (0.0, 0.0), (0.0, 1.0), (1.0, 0.3), (1.0, 1.0) ], - Right_Eye => [ (0.0, 1.0), (1.0, 1.0), (0.0, 0.3), (1.0, 0.0) ] - ], - Eyes_Cringe => [ - Left_Eye => [ (0.0, 0.5), (0.5, 0.75), (1.3, 0.75), (0.0, 1.0) ], - Right_Eye => [ (1.0, 1.0), (0.5, 0.75), (-0.3, 0.75), (1.0, 0.5) ] - ], - Eyes_Surprised => [ - Left_Eye => [ (0.0, 0.3), (0.0, 1.0), (1.0, 0.3), (1.0, 1.0) ], - Right_Eye => [ (0.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 0.0) ] - ] - ]; - - type Player_State is record - Prev_Position: IVector2; - Position: IVector2; - Prev_Eyes: Eyes_Kind; - Eyes: Eyes_Kind; - Eyes_Angle: Float; - Eyes_Target: IVector2; - Keys: Integer := 0; - Bombs: Integer := 0; - Bomb_Slots: Integer := 1; - Dead: Boolean := False; - end record; - - type Eeper_Kind is (Eeper_Guard, Eeper_Mother, Eeper_Gnome, Eeper_Father); - - type Eeper_State is record - Kind: Eeper_Kind; - Dead: Boolean := True; - Position, Prev_Position: IVector2; - Eyes_Angle: Float; - Eyes_Target: IVector2; - Prev_Eyes: Eyes_Kind; - Eyes: Eyes_Kind := Eyes_Closed; - Size: IVector2; - Path: Path_Map_Access; - - Background: Palette; - Health: Float := 1.0; - Attack_Cooldown: Integer := GUARD_ATTACK_COOLDOWN; - end record; - - type Bomb_State is record - Position: IVector2; - Countdown: Integer := 0; - end record; - - type Bomb_State_Array is array (1..10) of Bomb_State; - - type Eeper_Index is range 1..15; - type Eeper_Array is array (Eeper_Index) of Eeper_State; - - type Checkpoint_State is record - Map: Map_Access := Null; - Player_Position: IVector2; - Player_Keys: Integer; - Player_Bombs: Integer; - Player_Bomb_Slots: Integer; - Eepers: Eeper_Array; - Items: Hashed_Map_Items.Map; - Bombs: Bomb_State_Array; - end record; - - type Game_State is record - Map: Map_Access := Null; - Player: Player_State; - Eepers: Eeper_Array; - - Turn_Animation: Float := 0.0; - - Items: Hashed_Map_Items.Map; - Bombs: Bomb_State_Array; - Camera_Position: Vector2 := (x => 0.0, y => 0.0); - - Checkpoint: Checkpoint_State; - - Duration_Of_Last_Turn: Double; - end record; - - function Within_Map(Game: Game_State; Position: IVector2) return Boolean is - begin - return Position.Y in Game.Map'Range(1) and then Position.X in Game.Map'Range(2); - end; - - function Clone_Map(M0: Map_Access) return Map_Access is - M1: Map_Access; - begin - M1 := new Map(M0'Range(1), M0'Range(2)); - M1.all := M0.all; - return M1; - end; - - type Direction is (Left, Right, Up, Down); - - Direction_Vector: constant array (Direction) of IVector2 := [ - Left => (X => -1, Y => 0), - Right => (X => 1, Y => 0), - Up => (X => 0, Y => -1), - Down => (X => 0, Y => 1)]; - - function Inside_Of_Rect(Start, Size, Point: in IVector2) return Boolean is - begin - return Start <= Point and then Point < Start + Size; - end; - - function Eeper_Can_Stand_Here(Game: Game_State; Start: IVector2; Me: Eeper_Index) return Boolean is - Size: constant IVector2 := Game.Eepers(Me).Size; - begin - for X in Start.X..Start.X+Size.X-1 loop - for Y in Start.Y..Start.Y+Size.Y-1 loop - if not Within_Map(Game, (X, Y)) then - return False; - end if; - -- NOTE: it's fine to step into the explosions, because they don't live long enough. - -- They disappear on the next turn. - if Game.Map(Y, X) /= Cell_Floor and Game.Map(Y, X) /= Cell_Explosion then - return False; - end if; - for Index in Eeper_Index loop - if not Game.Eepers(Index).Dead and then Index /= Me then - declare - Eeper : constant Eeper_State := Game.Eepers(Index); - begin - if Inside_Of_Rect(Eeper.Position, Eeper.Size, (X, Y)) then - return False; - end if; - end; - end if; - end loop; - end loop; - end loop; - return True; - end; - - package Queue is new - Ada.Containers.Vectors(Index_Type => Natural, Element_Type => IVector2); - - procedure Recompute_Path_For_Eeper - (Game: in out Game_State; - Me: Eeper_Index; - Steps_Limit: Integer; - Step_Length_Limit: Integer; - Stop_At_Me: Boolean := True) - is - Q: Queue.Vector; - Eeper: Eeper_State renames Game.Eepers(Me); - begin - for Y in Eeper.Path'Range(1) loop - for X in Eeper.Path'Range(2) loop - Eeper.Path(Y, X) := -1; - end loop; - end loop; - - for Dy in 0..Eeper.Size.Y-1 loop - for Dx in 0..Eeper.Size.X-1 loop - declare - Position: constant IVector2 := Game.Player.Position - (Dx, Dy); - begin - if Eeper_Can_Stand_Here(Game, Position, Me) then - Eeper.Path(Position.Y, Position.X) := 0; - Q.Append(Position); - end if; - end; - end loop; - end loop; - - while not Q.Is_Empty loop - declare - Position: constant IVector2 := Q(0); - begin - Q.Delete_First; - - if Stop_At_Me and then Position = Eeper.Position then - exit; - end if; - - if Eeper.Path(Position.Y, Position.X) >= Steps_Limit then - exit; - end if; - - for Dir in Direction loop - declare - New_Position: IVector2 := Position + Direction_Vector(Dir); - begin - for Limit in 1..Step_Length_Limit loop - if not Eeper_Can_Stand_Here(Game, New_Position, Me) then - exit; - end if; - if Eeper.Path(New_Position.Y, New_Position.X) >= 0 then - exit; - end if; - Eeper.Path(New_Position.Y, New_Position.X) := Eeper.Path(Position.Y, Position.X) + 1; - Q.Append(New_Position); - New_Position := New_Position + Direction_Vector(Dir); - end loop; - end; - end loop; - end; - end loop; - end; - - procedure Game_Save_Checkpoint(Game: in out Game_State) is - begin - if Game.Checkpoint.Map /= null then - Delete_Map(Game.Checkpoint.Map); - end if; - Game.Checkpoint.Map := Clone_Map(Game.Map); - Game.Checkpoint.Player_Position := Game.Player.Position; - Game.Checkpoint.Player_Keys := Game.Player.Keys; - Game.Checkpoint.Player_Bombs := Game.Player.Bombs; - Game.Checkpoint.Player_Bomb_Slots := Game.Player.Bomb_Slots; - Game.Checkpoint.Eepers := Game.Eepers; - Game.Checkpoint.Items := Game.Items; - Game.Checkpoint.Bombs := Game.Bombs; - end; - - procedure Game_Restore_Checkpoint(Game: in out Game_State) is - begin - if Game.Map /= null then - Delete_Map(Game.Map); - end if; - Game.Map := Clone_Map(Game.Checkpoint.Map); - Game.Player.Position := Game.Checkpoint.Player_Position; - Game.Player.Keys := Game.Checkpoint.Player_Keys; - Game.Player.Bombs := Game.Checkpoint.Player_Bombs; - Game.Player.Bomb_Slots := Game.Checkpoint.Player_Bomb_Slots; - Game.Eepers := Game.Checkpoint.Eepers; - Game.Items := Game.Checkpoint.Items; - Game.Bombs := Game.Checkpoint.Bombs; - end; - - Too_Many_Entities: exception; - - function Allocate_Eeper(Game: in out Game_State) return Eeper_Index is - begin - for Eeper in Game.Eepers'Range loop - if Game.Eepers(Eeper).Dead then - Game.Eepers(Eeper).Dead := False; - return Eeper; - end if; - end loop; - raise Too_Many_Entities; - end; - - procedure Spawn_Gnome(Game: in out Game_State; Position: IVector2) is - Gnome: Eeper_State renames Game.Eepers(Allocate_Eeper(Game)); - Size: constant IVector2 := (1, 1); - begin - Gnome.Kind := Eeper_Gnome; - Gnome.Prev_Eyes := Eyes_Closed; - Gnome.Eyes := Eyes_Closed; - Gnome.Eyes_Target := Position + (Size.X/2, Size.Y); - Gnome.Background := COLOR_DOORKEY; - Gnome.Position := Position; - Gnome.Prev_Position := Position; - Gnome.Size := Size; - end; - - procedure Spawn_Father(Game: in out Game_State; Position: IVector2) is - Father: Eeper_State renames Game.Eepers(Allocate_Eeper(Game)); - Size: constant IVector2 := (7, 7); - begin - Father.Kind := Eeper_Father; - Father.Prev_Eyes := Eyes_Closed; - Father.Eyes := Eyes_Closed; - Father.Eyes_Angle := Pi*0.5; - Father.Eyes_Target := Position + (Size.X/2, Size.Y); - Father.Background := COLOR_FATHER; - Father.Position := Position; - Father.Prev_Position := Position; - Father.Size := Size; - end; - - procedure Spawn_Mother(Game: in out Game_State; Position: IVector2) is - Mother: Eeper_State renames Game.Eepers(Allocate_Eeper(Game)); - Size: constant IVector2 := (7, 7); - begin - Mother.Kind := Eeper_Mother; - Mother.Prev_Eyes := Eyes_Closed; - Mother.Eyes := Eyes_Closed; - Mother.Eyes_Target := Position + (Size.X/2, Size.Y); - Mother.Background := COLOR_MOTHER; - Mother.Position := Position; - Mother.Prev_Position := Position; - Mother.Health := 1.0; - Mother.Size := Size; - end; - - procedure Spawn_Guard(Game: in out Game_State; Position: IVector2) is - Guard: Eeper_State renames Game.Eepers(Allocate_Eeper(Game)); - Size: constant IVector2 := (3, 3); - begin - Guard.Kind := Eeper_Guard; - Guard.Prev_Eyes := Eyes_Closed; - Guard.Eyes := Eyes_Closed; - Guard.Eyes_Target := Position + (Size.X/2, Size.Y); - Guard.Background := COLOR_GUARD; - Guard.Position := Position; - Guard.Prev_Position := Position; - Guard.Health := 1.0; - Guard.Size := Size; - Guard.Attack_Cooldown := GUARD_ATTACK_COOLDOWN; - end; - - type Level_Cell is ( - Level_None, - Level_Gnome, - Level_Mother, - Level_Guard, - Level_Floor, - Level_Wall, - Level_Door, - Level_Checkpoint, - Level_Bomb_Gen, - Level_Barricade, - Level_Key, - Level_Player, - Level_Father); - Level_Cell_Color: constant array (Level_Cell) of Color := [ - Level_None => Get_Color(16#00000000#), - Level_Gnome => Get_Color(16#FF9600FF#), - Level_Mother => Get_Color(16#96FF00FF#), - Level_Guard => Get_Color(16#00FF00FF#), - Level_Floor => Get_Color(16#FFFFFFFF#), - Level_Wall => Get_Color(16#000000FF#), - Level_Door => Get_Color(16#00FFFFFF#), - Level_Checkpoint => Get_Color(16#FF00FFFF#), - Level_Bomb_Gen => Get_Color(16#FF0000FF#), - Level_Barricade => Get_Color(16#FF0096FF#), - Level_Key => Get_Color(16#FFFF00FF#), - Level_Player => Get_Color(16#0000FFFF#), - Level_Father => Get_Color(16#265FDAFF#)]; - - function Cell_By_Color(Col: Color; Out_Cel: out Level_Cell) return Boolean is - begin - for Cel in Level_Cell loop - if Level_Cell_Color(Cel) = Col then - Out_Cel := Cel; - return True; - end if; - end loop; - return False; - end; - - function Screen_Size return Vector2 is - begin - return To_Vector2((Integer(Get_Screen_Width), Integer(Get_Screen_Height))); - end; - - procedure Load_Game_From_Image(File_Name: in String; Game: in out Game_State; Update_Player: Boolean) is - type Color_Array is array (Natural range <>) of aliased Raylib.Color; - package Color_Pointer is new Interfaces.C.Pointers( - Index => Natural, - Element => Raylib.Color, - Element_Array => Color_Array, - Default_Terminator => (others => 0)); - function To_Color_Pointer is new Ada.Unchecked_Conversion (Raylib.Addr, Color_Pointer.Pointer); - use Color_Pointer; - - Img: constant Image := Raylib.Load_Image(To_C(File_Name)); - Pixels: constant Color_Pointer.Pointer := To_Color_Pointer(Img.Data); - begin - if Game.Map /= null then - Delete_Map(Game.Map); - end if; - Game.Map := new Map(1..Integer(Img.Height), 1..Integer(Img.Width)); - - for Eeper of Game.Eepers loop - Eeper.Dead := True; - if Eeper.Path /= null then - Delete_Path_Map(Eeper.Path); - end if; - Eeper.Path := new Path_Map(1..Integer(Img.Height), 1..Integer(Img.Width)); - for Y in Eeper.Path'Range(1) loop - for X in Eeper.Path'Range(2) loop - Eeper.Path(Y, X) := -1; - end loop; - end loop; - end loop; - - Game.Items.Clear; - for Bomb of Game.Bombs loop - Bomb.Countdown := 0; - end loop; - - for Row in Game.Map'Range(1) loop - for Column in Game.Map'Range(2) loop - declare - Index: constant Ptrdiff_T := Ptrdiff_T((Row - 1)*Integer(Img.Width) + (Column - 1)); - Pixel: constant Color_Pointer.Pointer := Pixels + Index; - Cel: Level_Cell; - begin - if Cell_By_Color(Pixel.all, Cel) then - case Cel is - when Level_None => - Game.Map(Row, Column) := Cell_None; - when Level_Gnome => - Spawn_Gnome(Game, (Column, Row)); - Game.Map(Row, Column) := Cell_Floor; - when Level_Mother => - Spawn_Mother(Game, (Column, Row)); - Game.Map(Row, Column) := Cell_Floor; - when Level_Guard => - Spawn_Guard(Game, (Column, Row)); - Game.Map(Row, Column) := Cell_Floor; - when Level_Father => - Game.Camera_Position := Screen_Size*0.5 - (To_Vector2((Column, Row))*Cell_Size + To_Vector2((7, 7))*Cell_Size*0.5); - Spawn_Father(Game, (Column, Row)); - Game.Map(Row, Column) := Cell_Floor; - when Level_Floor => Game.Map(Row, Column) := Cell_Floor; - when Level_Wall => Game.Map(Row, Column) := Cell_Wall; - when Level_Door => Game.Map(Row, Column) := Cell_Door; - when Level_Checkpoint => - Game.Map(Row, Column) := Cell_Floor; - Game.Items.Insert((Column, Row), (Kind => Item_Checkpoint)); - when Level_Bomb_Gen => - Game.Map(Row, Column) := Cell_Floor; - Game.Items.Insert((Column, Row), (Kind => Item_Bomb_Gen, Cooldown => 0)); - when Level_Barricade => - Game.Map(Row, Column) := Cell_Barricade; - when Level_Key => - Game.Map(Row, Column) := Cell_Floor; - Game.Items.Insert((Column, Row), (Kind => Item_Key)); - when Level_Player => - Game.Map(Row, Column) := Cell_Floor; - if Update_Player then - Game.Player.Position := (Column, Row); - Game.Player.Prev_Position := (Column, Row); - -- TODO: should we save the state of the eyes in the checkpoint? - -- Or maybe checkpoint should just save the entirety of the Player_State. - -- 'Cause that's what we do Eepers anyway. It works for them. - Game.Player.Prev_Eyes := Eyes_Closed; - Game.Player.Eyes := Eyes_Open; - Game.Player.Eyes_Angle := Pi*0.5; - Game.Player.Eyes_Target := Game.Player.Position + Direction_Vector(Up); - end if; - end case; - else - Game.Map(Row, Column) := Cell_None; - end if; - end; - end loop; - end loop; - - Stop_Music_Stream(Ambient_Music); - Play_Music_Stream(Ambient_Music); - end; - - procedure Draw_Bomb(Position: IVector2; C: Color) is - begin - Draw_Circle_V(To_Vector2(Position)*Cell_Size + Cell_Size*0.5, Cell_Size.X*0.5, C); - end; - - procedure Draw_Key(Position: IVector2) is - begin - Draw_Circle_V(To_Vector2(Position)*Cell_Size + Cell_Size*0.5, Cell_Size.X*0.25, Palette_RGB(COLOR_DOORKEY)); - end; - - procedure Draw_Number(Start, Size: Vector2; N: Integer; C: Color) is - Label: constant Char_Array := To_C(Trim(Integer'Image(N), Ada.Strings.Left)); - Label_Height: constant Integer := 32; - Label_Width: constant Integer := Integer(Measure_Text(Label, Int(Label_Height))); - Text_Size: constant Vector2 := To_Vector2((Label_Width, Label_Height)); - Position: constant Vector2 := Start + Size*0.5 - Text_Size*0.5; - begin - Draw_Text(Label, Int(Position.X), Int(Position.Y), Int(Label_Height), C); - end; - - procedure Draw_Number(Cell_Position: IVector2; N: Integer; C: Color) is - begin - Draw_Number(To_Vector2(Cell_Position)*Cell_Size, Cell_Size, N, C); - end; - - procedure Game_Cells(Game: in Game_State) is - begin - for Row in Game.Map'Range(1) loop - for Column in Game.Map'Range(2) loop - declare - Position: constant Vector2 := To_Vector2((Column, Row))*Cell_Size; - begin - Draw_Rectangle_V(position, cell_size, Cell_Colors(Game.Map(Row, Column))); - end; - end loop; - end loop; - end; - - procedure Game_Items(Game: in Game_State) is - use Hashed_Map_Items; - begin - for C in Game.Items.Iterate loop - case Element(C).Kind is - when Item_Key => Draw_Key(Key(C)); - when Item_Checkpoint => - declare - Checkpoint_Item_Size: constant Vector2 := Cell_Size*0.5; - begin - Draw_Rectangle_V(To_Vector2(Key(C))*Cell_Size + Cell_Size*0.5 - Checkpoint_Item_Size*0.5, Checkpoint_Item_Size, Palette_RGB(COLOR_CHECKPOINT)); - end; - when Item_Bomb_Gen => - if Element(C).Cooldown > 0 then - Draw_Bomb(Key(C), Color_Brightness(Palette_RGB(COLOR_BOMB), -0.5)); - Draw_Number(Key(C), Element(C).Cooldown, Palette_RGB(COLOR_LABEL)); - else - Draw_Bomb(Key(C), Palette_RGB(COLOR_BOMB)); - end if; - end case; - end loop; - end; - - procedure Flood_Fill(Game: in out Game_State; Start: IVector2; Fill: Cell) is - Q: Queue.Vector; - Background: Cell; - begin - if not Within_Map(Game, Start) then - return; - end if; - - Background := Game.Map(Start.Y, Start.X); - Game.Map(Start.Y, Start.X) := Fill; - Q.Append(Start); - - while not Q.Is_Empty loop - declare - Position: constant IVector2 := Q(0); - begin - Q.Delete_First; - - for Dir in Direction loop - declare - New_Position: constant IVector2 := Position + Direction_Vector(Dir); - begin - if Within_Map(Game, New_Position) and then Game.Map(New_Position.Y, New_Position.X) = Background then - Game.Map(New_Position.Y, New_Position.X) := Fill; - Q.Append(New_Position); - end if; - end; - end loop; - end; - end loop; - end; - - procedure Game_Player_Turn(Game: in out Game_State; Dir: Direction) is - New_Position: constant IVector2 := Game.Player.Position + Direction_Vector(Dir); - begin - Game.Player.Prev_Eyes := Game.Player.Eyes; - Game.Player.Prev_Position := Game.Player.Position; - Game.Player.Eyes_Target := New_Position + Direction_Vector(Dir); - - if not Within_Map(Game, New_Position) then - return; - end if; - - case Game.Map(New_Position.Y, New_Position.X) is - when Cell_Floor => - Game.Player.Position := New_Position; - declare - use Hashed_Map_Items; - C: Cursor := Game.Items.Find(New_Position); - begin - if Has_Element(C) then - case Element(C).Kind is - when Item_Key => - Game.Player.Keys := Game.Player.Keys + 1; - Game.Items.Delete(C); - Play_Sound(Key_Pickup_Sound); - when Item_Bomb_Gen => if - Game.Player.Bombs < Game.Player.Bomb_Slots - and then Element(C).Cooldown <= 0 - then - Game.Player.Bombs := Game.Player.Bombs + 1; - Game.Items.Replace_Element(C, (Kind => Item_Bomb_Gen, Cooldown => BOMB_GENERATOR_COOLDOWN)); - Play_Sound(Bomb_Pickup_Sound); - end if; - when Item_Checkpoint => - Game.Items.Delete(C); - Game.Player.Bombs := Game.Player.Bomb_Slots; - Game_Save_Checkpoint(Game); - Play_Sound(Checkpoint_Sound); - end case; - end if; - end; - when Cell_Door => - if Game.Player.Keys > 0 then - Game.Player.Keys := Game.Player.Keys - 1; - Flood_Fill(Game, New_Position, Cell_Floor); - Game.Player.Position := New_Position; - Play_Sound(Open_Door_Sound); - end if; - when others => null; - end case; - Play_Sound(Footsteps_Sounds(Random_Footsteps.Random(Footsteps_Gen))); - end; - - procedure Explode(Game: in out Game_State; Position: in IVector2) is - procedure Explode_Line(Dir: Direction) is - New_Position: IVector2 := Position; - begin - Line: for I in 1..EXPLOSION_LENGTH loop - if not Within_Map(Game, New_Position) then - return; - end if; - - case Game.Map(New_Position.Y, New_Position.X) is - when Cell_Floor | Cell_Explosion => - Game.Map(New_Position.Y, New_Position.X) := Cell_Explosion; - - if New_Position = Game.Player.Position then - Game.Player.Dead := True; - return; - end if; - - for Eeper of Game.Eepers loop - if not Eeper.Dead and then Inside_Of_Rect(Eeper.Position, Eeper.Size, New_Position) then - case Eeper.Kind is - when Eeper_Father => null; - when Eeper_Gnome => - Game.Items.Insert(Eeper.Position, (Kind => Item_Key)); - Eeper.Dead := True; - when Eeper_Guard => - Eeper.Eyes := Eyes_Cringe; - Eeper.Health := Eeper.Health - EEPER_EXPLOSION_DAMAGE; - if Eeper.Health <= 0.0 then - Eeper.Dead := True; - end if; - when Eeper_Mother => - declare - Position: constant IVector2 := Eeper.Position; - begin - Eeper.Dead := True; - Spawn_Guard(Game, Position + (0, 0)); - Spawn_Guard(Game, Position + (4, 0)); - Spawn_Guard(Game, Position + (0, 4)); - Spawn_Guard(Game, Position + (4, 4)); - end; - end case; - return; - end if; - end loop; - - New_Position := New_Position + Direction_Vector(Dir); - when Cell_Barricade => - Flood_Fill(Game, New_Position, Cell_Floor); - return; - when others => - return; - end case; - end loop Line; - end; - begin - for Dir in Direction loop - Explode_Line(Dir); - end loop; - end; - - Keys: constant array (Direction) of int := [ - Left => KEY_A, - Right => KEY_D, - Up => KEY_W, - Down => KEY_S - ]; - - procedure Game_Update_Camera(Game: in out Game_State) is - Camera_Target: constant Vector2 := - Screen_Size*0.5 - To_Vector2(Game.Player.Position)*Cell_Size - Cell_Size*0.5; - Camera_Velocity: constant Vector2 := (Camera_Target - Game.Camera_Position)*2.0; - begin - Game.Camera_Position := Game.Camera_Position + Camera_Velocity*Get_Frame_Time; - end; - - function Game_Camera(Game: in Game_State) return Camera2D is - begin - return ( - offset => Game.Camera_Position, - target => (x => 0.0, y => 0.0), - rotation => 0.0, - zoom => 1.0); - end; - - function Interpolate_Positions(IPrev_Position, IPosition: IVector2; T: Float) return Vector2 is - Prev_Position: constant Vector2 := To_Vector2(IPrev_Position)*Cell_Size; - Curr_Position: constant Vector2 := To_Vector2(IPosition)*Cell_Size; - begin - return Vector2_Lerp(Prev_Position, Curr_Position, C_Float(1.0 - T*T)); - end; - - Space_Pressed: Boolean := False; - Dir_Pressed: array (Direction) of Boolean := [others => False]; - Any_Key_Pressed: Boolean := False; - - procedure Swallow_Player_Input is - begin - Space_Pressed := False; - Any_Key_Pressed := False; - Dir_Pressed := [others => False]; - end; - - procedure Game_Bombs_Turn(Game: in out Game_State) is - begin - for Bomb of Game.Bombs loop - if Bomb.Countdown > 0 then - Bomb.Countdown := Bomb.Countdown - 1; - if Bomb.Countdown <= 0 then - Play_Sound(Blast_Sound); - Explode(Game, Bomb.Position); - end if; - end if; - end loop; - end; - - procedure Game_Explosions_Turn(Game: in out Game_State) is - begin - for Y in Game.Map'Range(1) loop - for X in Game.Map'Range(2) loop - if Game.Map(Y, X) = Cell_Explosion then - Game.Map(Y, X) := Cell_Floor; - end if; - end loop; - end loop; - end; - - function Look_At(Looker, Target: Vector2) return Float is - begin - return -Float(Vector2_Line_Angle(Looker, Target)); - end; - - procedure Game_Eepers_Turn(Game: in out Game_State) is - begin - for Me in Eeper_Index loop - declare - Eeper: Eeper_State renames Game.Eepers(Me); - begin - if not Eeper.Dead then - Eeper.Prev_Position := Eeper.Position; - Eeper.Prev_Eyes := Eeper.Eyes; - case Eeper.Kind is - when Eeper_Father => - declare - Wake_Up_Radius: constant IVector2 := (3, 3); - begin - if Inside_Of_Rect(Eeper.Position, Eeper.Size, Game.Player.Position) then - Load_Game_From_Image("assets/map.png", Game, True); - elsif Inside_Of_Rect(Eeper.Position - Wake_Up_Radius, Eeper.Size + Wake_Up_Radius*2, Game.Player.Position) then - Eeper.Eyes_Target := Game.Player.Position; - Eeper.Eyes := Eyes_Open; - else - Eeper.Eyes_Target := Eeper.Position + (Eeper.Size.X/2, Eeper.Size.Y); - Eeper.Eyes := Eyes_Closed; - end if; - end; - when Eeper_Guard | Eeper_Mother => - Recompute_Path_For_Eeper(Game, Me, GUARD_STEPS_LIMIT, GUARD_STEP_LENGTH_LIMIT); - if Eeper.Path(Eeper.Position.Y, Eeper.Position.X) = 0 then - Game.Player.Dead := True; - Eeper.Eyes := Eyes_Surprised; - elsif Eeper.Path(Eeper.Position.Y, Eeper.Position.X) > 0 then - if Eeper.Attack_Cooldown <= 0 then - declare - Current : constant Integer := Eeper.Path(Eeper.Position.Y, Eeper.Position.X); - Available_Positions: array (0..Direction_Vector'Length-1) of IVector2; - Count: Integer := 0; - begin - for Dir in Direction loop - declare - Position: IVector2 := Eeper.Position; - begin - while Eeper_Can_Stand_Here(Game, Position, Me) loop - Position := Position + Direction_Vector(Dir); - if Within_Map(Game, Position) and then Eeper.Path(Position.Y, Position.X) = Current - 1 then - Available_Positions(Count) := Position; - Count := Count + 1; - exit; - end if; - end loop; - end; - end loop; - if Count > 0 then - Eeper.Position := Available_Positions(Random_Integer.Random(Gen) mod Count); - Play_Sound(Guard_Step_Sound); - end if; - end; - Eeper.Attack_Cooldown := GUARD_ATTACK_COOLDOWN; - else - Eeper.Attack_Cooldown := Eeper.Attack_Cooldown - 1; - end if; - - if Eeper.Path(Eeper.Position.Y, Eeper.Position.X) = 1 then - Eeper.Eyes := Eyes_Angry; - else - Eeper.Eyes := Eyes_Open; - end if; - Eeper.Eyes_Target := Game.Player.Position; - - if Inside_Of_Rect(Eeper.Position, Eeper.Size, Game.Player.Position) then - Game.Player.Dead := True; - end if; - else - Eeper.Eyes := Eyes_Closed; - Eeper.Eyes_Target := Eeper.Position + (Eeper.Size.X/2, Eeper.Size.Y); - Eeper.Attack_Cooldown := GUARD_ATTACK_COOLDOWN + 1; - end if; - - if Eeper.Health < 1.0 then - Eeper.Health := Eeper.Health + GUARD_TURN_REGENERATION; - end if; - when Eeper_Gnome => - Recompute_Path_For_Eeper(Game, Me, 9, 1, Stop_At_Me => False); - declare - Position: constant IVector2 := Eeper.Position; - begin - if Eeper.Path(Position.Y, Position.X) >= 0 then - declare - Available_Positions: array (0..Direction_Vector'Length-1) of IVector2; - Count: Integer := 0; - begin - for Dir in Direction loop - declare - New_Position: constant IVector2 := Position + Direction_Vector(Dir); - begin - if Within_Map(Game, New_Position) - and then Game.Map(New_Position.Y, New_Position.X) = Cell_Floor - and then Eeper.Path(New_Position.Y, New_Position.X) > Eeper.Path(Position.Y, Position.X) - then - Available_Positions(Count) := New_Position; - Count := Count + 1; - end if; - end; - end loop; - - if Count > 0 then - Eeper.Position := Available_Positions(Random_Integer.Random(Gen) mod Count); - end if; - end; - Eeper.Eyes := Eyes_Open; - Eeper.Eyes_Target := Game.Player.Position; - else - Eeper.Eyes := Eyes_Closed; - Eeper.Eyes_Target := Eeper.Position + (Eeper.Size.X/2, Eeper.Size.Y); - end if; - end; - end case; - end if; - end; - end loop; - end; - - procedure Game_Items_Turn(Game: in out Game_State) is - use Hashed_Map_Items; - begin - for C in Game.Items.Iterate loop - if Element(C).Kind = Item_Bomb_Gen then - if Element(C).Cooldown > 0 then - Game.Items.Replace_Element(C, (Kind => Item_Bomb_Gen, Cooldown => Element(C).Cooldown - 1)); - end if; - end if; - end loop; - end; - - function Screen_Player_Position(Game: in Game_State) return Vector2 is - begin - if Game.Turn_Animation > 0.0 then - return Interpolate_Positions(Game.Player.Prev_Position, Game.Player.Position, Game.Turn_Animation); - else - return To_Vector2(Game.Player.Position)*Cell_Size; - end if; - end; - - function Clamp(X, Lo, Hi: Float) return Float is - begin - if X < Lo then - return Lo; - elsif X > Hi then - return Hi; - else - return X; - end if; - end; - - function Repeat(T, Length: Float) return Float is - function Floorf(A: C_Float) return C_Float - with - Import => True, - Convention => C, - External_Name => "floorf"; - begin - return Clamp(T - Float(Floorf(C_Float(T/Length)))*Length, 0.0, Length); - end; - - function Delta_Angle(A, B: Float) return Float is - Dlt: Float := Repeat(B - A, 2.0*Pi); - begin - if Dlt > Pi then - Dlt := Dlt - 2.0*Pi; - end if; - return Dlt; - end; - - procedure Draw_Eyes(Start, Size: Vector2; Angle: Float; Prev_Kind, Kind: Eyes_Kind; T: Float) is - Dir: constant Vector2 := Vector2_Rotate((1.0, 0.0), C_Float(Angle)); - Eyes_Ratio: constant Vector2 := (13.0/64.0, 23.0/64.0); - Eyes_Size: constant Vector2 := Eyes_Ratio*Size; - Center: constant Vector2 := Start + Size*0.5; - Position: constant Vector2 := Center + Dir*Eyes_Size.X*0.6; - Positions: constant array (Eye) of Vector2 := [ - Left_Eye => Position - Eyes_Size*(0.5, 0.0) - Eyes_Size*(1.0, 0.5), - Right_Eye => Position + Eyes_Size*(0.5, 0.0) - Eyes_Size*(0.0, 0.5) - ]; - Mesh: Eye_Mesh; - begin - for Eye_Index in Eye loop - for Vertex_Index in Eye_Mesh'Range loop - Mesh(Vertex_Index) := Positions(Eye_Index) + Eyes_Size*Vector2_Lerp(Eyes_Meshes(Prev_Kind)(Eye_Index)(Vertex_Index), Eyes_Meshes(Kind)(Eye_Index)(Vertex_Index), C_Float(1.0 - T*T)); - end loop; - Draw_Triangle_Strip(Mesh, Palette_RGB(COLOR_EYES)); - end loop; - end; - - procedure Game_Player(Game: in out Game_State) is - Eyes_Angular_Direction: constant Float := Delta_Angle(Game.Player.Eyes_Angle, Look_At(To_Vector2(Game.Player.Position)*Cell_Size + Cell_Size*0.5, To_Vector2(Game.Player.Eyes_Target)*Cell_Size + Cell_Size*0.5)); - begin - Game.Player.Eyes_Angle := Game.Player.Eyes_Angle + Eyes_Angular_Direction*EYES_ANGULAR_VELOCITY*Float(Get_Frame_Time); - if Game.Player.Dead then - if Game.Turn_Animation >= 0.0 then - Draw_Rectangle_V(Screen_Player_Position(Game), Cell_Size, Palette_RGB(COLOR_PLAYER)); - Draw_Eyes(Screen_Player_Position(Game), Cell_Size, Game.Player.Eyes_Angle, Game.Player.Prev_Eyes, Game.Player.Eyes, Game.Turn_Animation); - end if; - - if Any_Key_Pressed then - Game_Restore_Checkpoint(Game); - Game.Player.Dead := False; - end if; - - return; - end if; - - Draw_Rectangle_V(Screen_Player_Position(Game), Cell_Size, Palette_RGB(COLOR_PLAYER)); - Draw_Eyes(Screen_Player_Position(Game), Cell_Size, Game.Player.Eyes_Angle, Game.Player.Prev_Eyes, Game.Player.Eyes, Game.Turn_Animation); - - if Game.Turn_Animation > 0.0 then - return; - end if; - - if Space_Pressed and then Game.Player.Bombs > 0 then - declare - Start_Of_Turn: constant Double := Get_Time; - begin - Game.Turn_Animation := 1.0; - Game_Explosions_Turn(Game); - Game_Items_Turn(Game); - - -- Game_Player_Turn(Game, Dir); - Game.Player.Prev_Position := Game.Player.Position; - for Bomb of Game.Bombs loop - if Bomb.Countdown <= 0 then - Bomb.Countdown := 3; - Bomb.Position := Game.Player.Position; - exit; - end if; - end loop; - Game.Player.Bombs := Game.Player.Bombs - 1; - Play_Sound(Plant_Bomb_Sound); - - Game_Eepers_Turn(Game); - -- Game_Bombs_Turn(Game); - Game.Duration_Of_Last_Turn := Get_Time - Start_Of_Turn; - end; - else - for Dir in Direction loop - if Dir_Pressed(Dir) then - declare - Start_Of_Turn: constant Double := Get_Time; - begin - Game.Turn_Animation := 1.0; - Game_Explosions_Turn(Game); - Game_Items_Turn(Game); - Game_Player_Turn(Game, Dir); - Game_Eepers_Turn(Game); - Game_Bombs_Turn(Game); - Game.Duration_Of_Last_Turn := Get_Time - Start_Of_Turn; - end; - end if; - end loop; - end if; - end; - - procedure Game_Bombs(Game: Game_State) is - begin - for Bomb of Game.Bombs loop - if Bomb.Countdown > 0 then - Draw_Bomb(Bomb.Position, Palette_RGB(COLOR_BOMB)); - Draw_Number(Bomb.Position, Bomb.Countdown, Palette_RGB(COLOR_LABEL)); - end if; - end loop; - end; - - procedure Game_Hud(Game: in Game_State) is - begin - for Index in 1..Game.Player.Keys loop - declare - Position: constant Vector2 := (100.0 + C_float(Index - 1)*Cell_Size.X, 100.0); - begin - Draw_Circle_V(Position, Cell_Size.X*0.25, Palette_RGB(COLOR_DOORKEY)); - end; - end loop; - - for Index in 1..Game.Player.Bombs loop - declare - Position: constant Vector2 := (100.0 + C_float(Index - 1)*Cell_Size.X, 200.0); - begin - Draw_Circle_V(Position, Cell_Size.X*0.5, Palette_RGB(COLOR_BOMB)); - end; - end loop; - - if Game.Player.Dead then - declare - Label: constant Char_Array := To_C("You Died!"); - Label_Height: constant Integer := 48; - Label_Width: constant Integer := Integer(Measure_Text(Label, Int(Label_Height))); - Text_Size: constant Vector2 := To_Vector2((Label_Width, Label_Height)); - Position: constant Vector2 := Screen_Size*0.5 - Text_Size*0.5; - begin - Draw_Text(Label, Int(Position.X), Int(Position.Y), Int(Label_Height), Palette_RGB(COLOR_LABEL)); - end; - end if; - end; - - procedure Health_Bar(Boundary_Start, Boundary_Size: Vector2; Health: C_Float) is - Health_Padding: constant C_Float := 10.0; - Health_Height: constant C_Float := 10.0; - Health_Width: constant C_Float := Boundary_Size.X*Health; - begin - Draw_Rectangle_V( - Boundary_Start - (0.0, Health_Padding + Health_Height), - (Health_Width, Health_Height), - Palette_RGB(COLOR_HEALTHBAR)); - end; - - - procedure Draw_Cooldown_Timer_Bubble(Start, Size: Vector2; Cooldown: Integer; Background: Palette) is - Text_Color: constant Color := (A => 255, others => 0); - Bubble_Radius: constant C_Float := 30.0; - Bubble_Center: constant Vector2 := Start + Size*(0.5, 0.0) - (0.0, Bubble_Radius*2.0); - begin - Draw_Circle_V(Bubble_Center, Bubble_Radius, Palette_RGB(Background)); - Draw_Number(Bubble_Center - (Bubble_Radius, Bubble_Radius), (Bubble_Radius, Bubble_Radius)*2.0, Cooldown, Text_Color); - end; - - procedure Game_Eepers(Game: in out Game_State) is - begin - for Eeper of Game.Eepers loop - declare - Position: constant Vector2 := - (if Game.Turn_Animation > 0.0 - then Interpolate_Positions(Eeper.Prev_Position, Eeper.Position, Game.Turn_Animation) - else To_Vector2(Eeper.Position)*Cell_Size); - Size: constant Vector2 := To_Vector2(Eeper.Size)*Cell_Size; - Eyes_Angular_Direction: constant Float := Delta_Angle(Eeper.Eyes_Angle, Look_At(Position + Size*0.5, To_Vector2(Eeper.Eyes_Target)*Cell_Size + Cell_Size*0.5)); - begin - if not Eeper.Dead then - Eeper.Eyes_Angle := Eeper.Eyes_Angle + Eyes_Angular_Direction*EYES_ANGULAR_VELOCITY*Float(Get_Frame_Time); - case Eeper.Kind is - when Eeper_Father => - Draw_Rectangle_V(Position, Size, Palette_RGB(Eeper.Background)); - Draw_Eyes(Position, Size, Eeper.Eyes_Angle, Eeper.Prev_Eyes, Eeper.Eyes, Game.Turn_Animation); - when Eeper_Guard | Eeper_Mother => - Draw_Rectangle_V(Position, Size, Palette_RGB(Eeper.Background)); - Health_Bar(Position, Size, C_Float(Eeper.Health)); - if Eeper.Path(Eeper.Position.Y, Eeper.Position.X) = 1 then - Draw_Cooldown_Timer_Bubble(Position, Size, Eeper.Attack_Cooldown, Eeper.Background); - elsif Eeper.Path(Eeper.Position.Y, Eeper.Position.X) >= 0 then - Draw_Cooldown_Timer_Bubble(Position, Size, Eeper.Attack_Cooldown, Eeper.Background); - end if; - Draw_Eyes(Position, Size, Eeper.Eyes_Angle, Eeper.Prev_Eyes, Eeper.Eyes, Game.Turn_Animation); - when Eeper_Gnome => - declare - GNOME_RATIO: constant C_Float := 0.7; - GNOME_SIZE: constant Vector2 := Cell_Size*GNOME_RATIO; - GNOME_START: constant Vector2 := Position + Cell_Size*0.5 - GNOME_SIZE*0.5; - begin - Draw_Rectangle_V(GNOME_START, GNOME_SIZE, Palette_RGB(Eeper.Background)); - Draw_Eyes(GNOME_START, GNOME_SIZE, Eeper.Eyes_Angle, Eeper.Prev_Eyes, Eeper.Eyes, Game.Turn_Animation); - end; - end case; - end if; - end; - end loop; - end; - - Game: Game_State; - Title: constant Char_Array := To_C("Eepers"); - - Palette_Editor: Boolean := False; - Palette_Editor_Choice: Palette := Palette'First; - Palette_Editor_Selected: Boolean := False; - Palette_Editor_Component: HSV_Comp := Hue; - -begin - if not Change_Directory(Get_Application_Directory) then - Put_Line("WARNING: Could not change working directory to the application directory"); - end if; - - Set_Config_Flags(FLAG_WINDOW_RESIZABLE); - Init_Window(1600, 900, Title); - Set_Target_FPS(60); - Set_Exit_Key(KEY_NULL); - - Init_Audio_Device; - for Index in Footsteps_Range loop - Footsteps_Sounds(Index) := Load_Sound(To_C("assets/sounds/footsteps.mp3")); - Set_Sound_Pitch(Footsteps_Sounds(Index), Footsteps_Pitches(Index)); - end loop; - Blast_Sound := Load_Sound(To_C("assets/sounds/blast.ogg")); -- https://opengameart.org/content/magic-sfx-sample - Key_Pickup_Sound := Load_Sound(To_C("assets/sounds/key-pickup.wav")); -- https://opengameart.org/content/beep-tone-sound-sfx - Ambient_Music := Load_Music_Stream(To_C("assets/sounds/ambient.wav")); -- https://opengameart.org/content/ambient-soundtrack - Set_Music_Volume(Ambient_Music, 0.5); - Bomb_Pickup_Sound := Load_Sound(To_C("assets/sounds/bomb-pickup.ogg")); -- https://opengameart.org/content/pickupplastic-sound - Open_Door_Sound := Load_Sound(To_C("assets/sounds/open-door.wav")); -- https://opengameart.org/content/picked-coin-echo - Set_Sound_Volume(Open_Door_Sound, 0.5); - Checkpoint_Sound := Load_Sound(To_C("assets/sounds/checkpoint.ogg")); -- https://opengameart.org/content/level-up-power-up-coin-get-13-sounds - Set_Sound_Pitch(Checkpoint_Sound, 0.8); - Guard_Step_Sound := Load_Sound(To_C("assets/sounds/guard-step.ogg")); -- https://opengameart.org/content/fire-whip-hit-yo-frankie - Plant_Bomb_Sound := Load_Sound(To_C("assets/sounds/plant-bomb.wav")); -- https://opengameart.org/content/ui-soundpack-by-m1chiboi-bleeps-and-clicks - - Random_Integer.Reset(Gen); - Load_Colors("assets/colors.txt"); - Load_Game_From_Image("assets/map.png", Game, True); - Game_Save_Checkpoint(Game); - Play_Music_Stream(Ambient_Music); - - while not Window_Should_Close loop - if Is_Music_Stream_Playing(Ambient_Music) then - Update_Music_Stream(Ambient_Music); - end if; - Begin_Drawing; - Clear_Background(Palette_RGB(COLOR_BACKGROUND)); - - Swallow_Player_Input; - Polling: loop - declare - Key: constant int := Get_Key_Pressed; - begin - case Key is - when KEY_NULL => exit Polling; - when KEY_SPACE => Space_Pressed := True; - when KEY_A | KEY_LEFT => Dir_Pressed(Left) := True; - when KEY_D | KEY_RIGHT => Dir_Pressed(Right) := True; - when KEY_S | KEY_DOWN => Dir_Pressed(Down) := True; - when KEY_W | KEY_UP => Dir_Pressed(Up) := True; - when others => null; - end case; - Any_Key_Pressed := True; - end; - end loop Polling; - - if DEVELOPMENT then - if Is_Key_Pressed(KEY_R) then - Load_Game_From_Image("assets/map.png", Game, False); - end if; - - if Is_Key_Pressed(KEY_O) then - Palette_Editor := not Palette_Editor; - if not Palette_Editor then - Save_Colors("assets/colors.txt"); - end if; - end if; - - if Palette_Editor then - if Palette_Editor_Selected then - if Is_Key_Pressed(KEY_ESCAPE) then - Palette_Editor_Selected := False; - end if; - - if Is_Key_Pressed(Keys(Left)) then - if Palette_Editor_Component /= HSV_Comp'First then - Palette_Editor_Component := HSV_Comp'Pred(Palette_Editor_Component); - end if; - end if; - - if Is_Key_Pressed(Keys(Right)) then - if Palette_Editor_Component /= HSV_Comp'Last then - Palette_Editor_Component := HSV_Comp'Succ(Palette_Editor_Component); - end if; - end if; - - if Is_Key_Down(Keys(Up)) then - Palette_HSV(Palette_Editor_Choice)(Palette_Editor_Component) := Palette_HSV(Palette_Editor_Choice)(Palette_Editor_Component) + 1; - Palette_RGB(Palette_Editor_Choice) := HSV_To_RGB(Palette_HSV(Palette_Editor_Choice)); - end if; - - if Is_Key_Down(Keys(Down)) then - Palette_HSV(Palette_Editor_Choice)(Palette_Editor_Component) := Palette_HSV(Palette_Editor_Choice)(Palette_Editor_Component) - 1; - Palette_RGB(Palette_Editor_Choice) := HSV_To_RGB(Palette_HSV(Palette_Editor_Choice)); - end if; - else - if Is_Key_Pressed(Keys(Down)) then - if Palette_Editor_Choice /= Palette'Last then - Palette_Editor_Choice := Palette'Succ(Palette_Editor_Choice); - end if; - end if; - - if Is_Key_Pressed(Keys(Up)) then - if Palette_Editor_Choice /= Palette'First then - Palette_Editor_Choice := Palette'Pred(Palette_Editor_Choice); - end if; - end if; - - if Is_Key_Pressed(KEY_ESCAPE) then - Palette_Editor := False; - end if; - - if Is_Key_Pressed(KEY_ENTER) then - Palette_Editor_Selected := True; - end if; - end if; - - Swallow_Player_Input; - end if; - end if; - - if Game.Turn_Animation > 0.0 then - Game.Turn_Animation := (Game.Turn_Animation*TURN_DURATION_SECS - Float(Get_Frame_Time))/TURN_DURATION_SECS; - end if; - - Game_Update_Camera(Game); - Begin_Mode2D(Game_Camera(Game)); - Game_Cells(Game); - Game_Items(Game); - Game_Player(Game); - Game_Eepers(Game); - Game_Bombs(Game); - if DEVELOPMENT then - if Is_Key_Down(KEY_P) then - for Row in Game.Map'Range(1) loop - for Column in Game.Map'Range(2) loop - Draw_Number((Column, Row), Game.Eepers(1).Path(Row, Column), (A => 255, others => 0)); - end loop; - end loop; - end if; - end if; - End_Mode2D; - - Game_Hud(Game); - if DEVELOPMENT then - Draw_FPS(10, 10); - declare - S: String(1..20); - begin - Double_IO.Put(S, Game.Duration_Of_Last_Turn, Exp => 0); - Draw_Text(To_C(S), 100, 10, 32, (others => 255)); - end; - end if; - - if Palette_Editor then - for C in Palette loop - declare - Label: constant Char_Array := To_C(C'Image); - Label_Height: constant Integer := 32; - Position: constant Vector2 := (200.0, 200.0 + C_Float(Palette'Pos(C))*C_Float(Label_Height)); - begin - Draw_Text(Label, Int(Position.X), Int(Position.Y), Int(Label_Height), - (if not Palette_Editor_Selected and C = Palette_Editor_Choice - then (R => 255, A => 255, others => 0) - else (others => 255))); - - for Comp in HSV_Comp loop - declare - Label: constant Char_Array := To_C(Comp'Image & ": " & Palette_HSV(C)(Comp)'Image); - Label_Height: constant Integer := 32; - Position: constant Vector2 := ( - X => 600.0 + 200.0*C_Float(HSV_Comp'Pos(Comp)), - Y => 200.0 + C_Float(Palette'Pos(C))*C_Float(Label_Height) - ); - begin - Draw_Text(Label, Int(Position.X), Int(Position.Y), Int(Label_Height), - (if Palette_Editor_Selected and C = Palette_Editor_Choice and Comp = Palette_Editor_Component - then (R => 255, A => 255, others => 0) - else (others => 255))); - end; - end loop; - end; - end loop; - end if; - End_Drawing; - end loop; - Close_Window; -end; - --- TODO: Loop the music --- TODO: Sound on Finishing Round --- TODO: Footstep variation for Mother/Guard bosses (depending on the distance traveled?) --- TODO: Footsteps for mother should be lower --- TODO: Eyes_Cringe as triangles --- The current ones look out of style --- TODO: Restarting the game does not reset bombs and keys of the Player --- TODO: Can you escape boss rooms using Gnomes? --- It's very hard because you need to somehow put them behind yourself --- TODO: Restarting should be considered a turn --- It's very useful to update Path Maps and stuff. --- Or maybe we should just save Path Maps too? --- TODO: Items in HUD may sometimes blend with the background --- TODO: If you are standing on the refilled bomb gen and place a bomb you should refill your bomb in that turn. --- TODO: Checkpoints should be circles (like all the items) --- TODO: Custom font --- TODO: Determenistically choose the paths again, but make them more interesting --- - Gnomes are just being deterministic --- - Mother and Guard always pick the longest path. Or generally the path that brings the Euclidean Distance closer --- TODO: Mother should require several attacks before being "split" --- TODO: Do not stack up damage for Eepers per the tiles of their body. --- The denote last direction of the step. --- TODO: Enemies should attack on zero just like a bomb. --- TODO: Properly disablable DEV features --- TODO: Fullscreen mode --- TODO: Don't reset cooldown timer for eepers (might be duplicate) --- Maybe just pause --- TODO: Try MSAA (if too slow, don't) --- TODO: Rename executable to "eepers" --- TODO: Icon on for Windows build --- TODO: Window title icon --- TODO: Show Eeper Cooldown timer outside of the screen somehow --- TODO: Visual Clue that the Eeper is about to kill the Player when Completely outside of the Screen --- - Cooldown ball is shaking --- TODO: Cool animation for New Game --- TODO: Tutorial sign that says "WASD" to move when you start the game for the first time. And how to place the bomb on picking it up --- TODO: Keep steping while you are holding a certain direction --- Cause constantly tapping it feels like ass. --- TODO: count the player's turns towards the final score of the game --- We can even collect different stats, like bombs collected, bombs used, --- times died etc. --- TODO: Animate key when you pick it up --- Smoothly move it into the HUD. --- TODO: Different palettes depending on the area --- Or maybe different palette for each NG+ --- TODO: Particles --- - Player Death animation --- - Eeper Death animation --- - Cool effects when you pick up items and checkpoints --- TODO: Camera shaking when big bosses (Guard and Mother) make moves --- TODO: Menu --- Could be just a splash with the game name and logo. --- TODO: WebAssembly build --- https://blog.adacore.com/use-of-gnat-llvm-to-translate-ada-applications-to-webassembly --- TODO: Explosions should trigger other primed bombs? --- TODO: Path finding in a separate thread --- TODO: Eeper slide attack animation is pretty boring @polish --- TODO: Bake assets into executable --- TODO: Background is too boring --- Maybe some sort of repeating pattern would be better. --- TODO: Indicate how many bomb slots we have in HUD --- TODO: Eyes of Father changing as the Player gets closer: --- - Happy (very important to indicate that he's not hostile) --- TODO: Transition Player's eyes linearly in Euclidean space instead of angularly. -- cgit v1.2.3