diff --git a/Hook Climber.csproj b/Hook Climber.csproj
new file mode 100644
index 0000000..3467498
--- /dev/null
+++ b/Hook Climber.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ net6.0
+ Hook_Climber
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/Hook Climber.sln b/Hook Climber.sln
new file mode 100644
index 0000000..185dee4
--- /dev/null
+++ b/Hook Climber.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.7.34221.43
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hook Climber", "Hook Climber.csproj", "{47ACEAFD-7819-4DAB-BAA2-7B98C7132276}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {47ACEAFD-7819-4DAB-BAA2-7B98C7132276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {47ACEAFD-7819-4DAB-BAA2-7B98C7132276}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {47ACEAFD-7819-4DAB-BAA2-7B98C7132276}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {47ACEAFD-7819-4DAB-BAA2-7B98C7132276}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {B80AB0B0-39BA-44FF-A87E-F21DF586CC1C}
+ EndGlobalSection
+EndGlobal
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..f0c9dec
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,463 @@
+using static Raylib_cs.Raylib;
+using Raylib_cs;
+using static Raylib_cs.Raymath;
+using System.Numerics;
+using TiledCS;
+
+namespace Hook_Climber;
+
+static class Utils
+{
+ public static Rectangle centeredRect(Vector2 center, Vector2 size)
+ {
+ return new Rectangle(center - size / 2, size);
+ }
+}
+
+class Game {
+ public static float playerMaxSpeed = 650.0f;
+ public static float playerWalkForce = 4500.0f;
+ public static float playerFriction = 0.999f;
+ public static float playerGravity = 2000.0f;
+
+ public static float hookDistance = 75f;
+ public static float hookPower = 300f;
+ public static float hookGravity = 600.0f;
+ public static float hookOffset = 50f;
+ public static float hookRopeStrength = 30f;
+
+
+ public static Vector2 hookSize = new Vector2(10, 10);
+ public static Vector2 playerSize = new Vector2(50, 50);
+
+ public Player player;
+ public List platforms;
+
+ public static Rectangle GetSweptBroadphaseBox(Rectangle b, Vector2 v)
+ {
+ Rectangle broadphasebox = b;
+
+ broadphasebox.X = v.X > 0 ? b.X : b.X + v.X;
+ broadphasebox.Y = v.Y > 0 ? b.Y : b.Y + v.Y;
+ broadphasebox.Width = v.X > 0 ? v.X + b.Width : b.Width - v.X;
+ broadphasebox.Height = v.Y > 0 ? v.Y + b.Height : b.Height - v.Y;
+
+ return broadphasebox;
+ }
+
+
+ public IEnumerable<(Rectangle, float, Vector2)> IterCollisions(Rectangle collider, Vector2 velocity, float dt)
+ {
+ foreach (var platform in platforms)
+ {
+ Vector2 normal;
+ float collisiontime = CheckCollision(
+ collider,
+ velocity * dt,
+ platform,
+ out normal
+ );
+ if (normal.X == 0 && normal.Y == 0) continue;
+
+ yield return (platform, collisiontime * dt, normal);
+ }
+ }
+
+ public static float CheckCollision(Rectangle b1, Vector2 v1, Rectangle b2, out Vector2 normal)
+ {
+ normal.X = 0;
+ normal.Y = 0;
+ if (!CheckCollisionRecs(GetSweptBroadphaseBox(b1, v1), b2))
+ {
+ return 1;
+ }
+
+ float entryX, exitX;
+ if (v1.X > 0)
+ {
+ entryX = b2.X - (b1.X + b1.Width);
+ exitX = (b2.X + b2.Width) - b1.X;
+ }
+ else
+ {
+ entryX = (b2.X + b2.Width) - b1.X;
+ exitX = b2.X - (b1.X + b1.Width);
+ }
+
+ float entryY, exitY;
+ if (v1.Y > 0)
+ {
+ entryY = b2.Y - (b1.Y + b1.Height);
+ exitY = (b2.Y + b2.Height) - b1.Y;
+ }
+ else
+ {
+ entryY = (b2.Y + b2.Height) - b1.Y;
+ exitY = b2.Y - (b1.Y + b1.Height);
+ }
+
+ float entryXTime, exitXTime;
+ if (v1.X == 0)
+ {
+ entryXTime = float.MinValue;
+ exitXTime = float.MaxValue;
+ }
+ else
+ {
+ entryXTime = entryX / v1.X;
+ exitXTime = exitX / v1.X;
+ }
+
+ float entryYTime, exitYTime;
+ if (v1.Y == 0)
+ {
+ entryYTime = float.MinValue;
+ exitYTime = float.MaxValue;
+ }
+ else
+ {
+ entryYTime = entryY / v1.Y;
+ exitYTime = exitY / v1.Y;
+ }
+
+ float entryTime = Math.Max(entryXTime, entryYTime);
+ float exitTime = Math.Min(exitXTime, exitYTime);
+
+ if (entryTime > exitTime || entryXTime < 0 && entryYTime < 0 || entryXTime > 1.0f || entryYTime > 1.0f)
+ {
+ return 1;
+ }
+
+ if (entryXTime > entryYTime)
+ {
+ normal.X = entryX < 0.0 ? 1 : -1;
+ }
+ else
+ {
+ normal.Y = entryY < 0.0 ? 1 : -1;
+ }
+
+ return entryTime;
+ }
+}
+
+class Hook
+{
+ public Game game;
+
+ public Vector2 offset;
+ public Vector2 dir;
+
+ public Vector2 primedAt;
+ public bool shown;
+ public bool primed;
+
+ public bool headReleased;
+ public Vector2 headPosition;
+ public Vector2 headVelocity;
+ public bool headGrounded;
+ public float reelDir = 0;
+ public float idealRopeLength = 200f;
+
+ public GamepadAxis moveX;
+ public GamepadAxis moveY;
+ public GamepadButton use;
+ public GamepadButton prime;
+
+ public void resetHead()
+ {
+ headGrounded = false;
+ headReleased = false;
+ }
+
+ public List headProjection(uint steps, float dt)
+ {
+ var points = new List();
+
+ var position = headPosition;
+ var velocity = headVelocity;
+ for (int i = 0; i < steps; i++)
+ {
+ if (updatePhysics(ref position, ref velocity, dt)) break;
+ points.Add(position);
+ }
+
+ return points;
+ }
+
+ public bool updatePhysics(ref Vector2 position, ref Vector2 velocity, float dt)
+ {
+ velocity.Y += Game.hookGravity * dt;
+ bool collisionOccured = checkAndResolveCollisions(Utils.centeredRect(position, Game.hookSize), ref velocity, dt);
+ position += velocity * dt;
+
+ return collisionOccured;
+ }
+
+ public bool checkAndResolveCollisions(Rectangle collider, ref Vector2 velocity, float dt)
+ {
+ bool collision = false;
+
+ foreach (var (_, collisiontime, normal) in game.IterCollisions(collider, velocity, dt))
+ {
+ velocity.X *= 0;
+ velocity.Y *= 0;
+
+ //velocity.X = velocity.X * collisiontime / dt;
+ //velocity.Y = velocity.Y * collisiontime / dt;
+ collision = true;
+ }
+
+ return collision;
+ }
+
+};
+
+class Player
+{
+ public Vector2 position = new Vector2(0, 0);
+ public Vector2 velocity = new Vector2(0, 0);
+
+ public Rectangle collisionRect()
+ {
+ return new Rectangle(position - Game.playerSize / 2, Game.playerSize.X, Game.playerSize.Y);
+ }
+};
+
+internal class Program
+{
+ public static void Main()
+ {
+ InitWindow(2000, 1200, "Hook Climber");
+ SetWindowState(ConfigFlags.ResizableWindow);
+
+ var map = new TiledMap("./main.tmx");
+ var tilesets = map.GetTiledTilesets("./");
+
+ var game = new Game();
+ game.player = new Player();
+
+ var leftHook = new Hook
+ {
+ game = game,
+ offset = new Vector2(-Game.hookOffset, 0),
+ moveX = GamepadAxis.LeftX,
+ moveY = GamepadAxis.LeftY,
+ use = GamepadButton.LeftTrigger2,
+ prime = GamepadButton.LeftTrigger1,
+ idealRopeLength = 200
+ };
+
+ var rightHook = new Hook
+ {
+ game = game,
+ offset = new Vector2(Game.hookOffset, 0),
+ moveX = GamepadAxis.RightX,
+ moveY = GamepadAxis.RightY,
+ use = GamepadButton.RightTrigger2,
+ prime = GamepadButton.RightTrigger1,
+ idealRopeLength = 200
+ };
+
+ var hooks = new Hook[]{ leftHook, rightHook };
+
+ var gamepadId = 0;
+
+ game.platforms = new List {
+ new Rectangle(-450, game.player.position.Y + Game.playerSize.Y/2, 1000, 20),
+ new Rectangle(0, -200, 100, 10),
+ new Rectangle(-100, -400, 100, 10),
+ new Rectangle(100, -600, 100, 10),
+ new Rectangle(-200, -800, 100, 10),
+ new Rectangle(-300, -1000, 100, 10),
+ };
+
+ var player = game.player;
+
+ var camera = new Camera2D();
+ camera.Target = player.position;
+
+ while (!WindowShouldClose())
+ {
+ var dt = GetFrameTime();
+ var windowWidth = GetScreenWidth();
+ var windowHeight = GetScreenHeight();
+
+ camera.Offset = new Vector2(windowWidth / 2, windowHeight / 2);
+ camera.Target = Vector2.Lerp(camera.Target, player.position, 20 * dt);
+ camera.Zoom = 1;
+
+ BeginDrawing();
+ ClearBackground(Color.White);
+ BeginMode2D(camera);
+
+ var dx = 0f;
+ if (IsKeyDown(KeyboardKey.D))
+ {
+ dx += 1;
+ }
+ if (IsKeyDown(KeyboardKey.A))
+ {
+ dx -= 1;
+ }
+
+ if (IsGamepadAvailable(gamepadId))
+ {
+ foreach (var hook in hooks)
+ {
+ if (IsGamepadButtonPressed(gamepadId, hook.prime))
+ {
+ hook.primedAt = hook.dir;
+ }
+ if (IsGamepadButtonDown(gamepadId, hook.prime))
+ {
+ var trajectory = hook.primedAt - hook.dir;
+
+ hook.headPosition = player.position + hook.primedAt * Game.hookDistance + hook.offset;
+ hook.headVelocity = trajectory * Game.hookPower + player.velocity;
+ }
+
+ if (IsGamepadButtonReleased(gamepadId, hook.prime))
+ {
+ if (hook.headReleased && (hook.primedAt - hook.dir).Length() < 0.01)
+ {
+ hook.resetHead();
+ }
+ else if (hook.primed && hook.shown)
+ {
+ hook.headReleased = true;
+ }
+ }
+
+ hook.shown = IsGamepadButtonDown(gamepadId, hook.use);
+ hook.primed = IsGamepadButtonDown(gamepadId, hook.prime);
+
+ if (!hook.headReleased)
+ {
+ hook.dir.X = GetGamepadAxisMovement(gamepadId, hook.moveX);
+ hook.dir.Y = GetGamepadAxisMovement(gamepadId, hook.moveY);
+ } else
+ {
+ hook.dir = new Vector2();
+
+ if (IsGamepadButtonDown(gamepadId, hook.use))
+ {
+ hook.reelDir = GetGamepadAxisMovement(gamepadId, hook.moveY);
+ } else
+ {
+ hook.reelDir = 0;
+ }
+ }
+ }
+
+ dx += GetGamepadAxisMovement(gamepadId, GamepadAxis.LeftX);
+ }
+
+ dx = Clamp(dx, -1, 1);
+
+ if (leftHook.shown)
+ {
+ dx = 0;
+ }
+
+ player.velocity.X += dx * Game.playerWalkForce * dt;
+ player.velocity.Y += Game.playerGravity * dt;
+ player.velocity = Vector2ClampValue(player.velocity, 0, Game.playerMaxSpeed);
+ player.velocity *= (float)Math.Pow(1 - Game.playerFriction, dt);
+
+ foreach (var (_, collisiontime, normal) in game.IterCollisions(player.collisionRect(), player.velocity, dt))
+ {
+ float remainingtime = (1.0f - collisiontime);
+ float dotprod = (player.velocity.X * normal.Y + player.velocity.Y * normal.X) * remainingtime;
+ // player.velocity.X = dotprod * normal.Y;
+ player.velocity.Y = dotprod * normal.X;
+ }
+
+ player.position += player.velocity * dt;
+
+ DrawCircle(0, 0, 5, Color.Red);
+ DrawRectangleRec(player.collisionRect(), Color.Black);
+
+ foreach (var hook in hooks)
+ {
+
+ var handPosition = player.position + hook.dir * Game.hookDistance + hook.offset;
+
+ if (hook.headReleased)
+ {
+
+ if (!hook.headGrounded)
+ {
+ hook.headGrounded = hook.updatePhysics(ref hook.headPosition, ref hook.headVelocity, dt);
+
+ if (hook.headGrounded)
+ {
+ hook.idealRopeLength = (handPosition - hook.headPosition).Length();
+ }
+ }
+
+ if (hook.headGrounded)
+ {
+ hook.idealRopeLength += hook.reelDir * dt * 150;
+ hook.idealRopeLength = Math.Max(hook.idealRopeLength, 0f);
+
+ var handToHead = hook.headPosition - handPosition;
+ player.velocity += (handToHead.Length() - hook.idealRopeLength) * Vector2Normalize(handToHead) * Game.hookRopeStrength * dt;
+ }
+
+ DrawRectangleRec(Utils.centeredRect(hook.headPosition, Game.hookSize), Color.DarkBlue);
+ DrawLineV(handPosition, hook.headPosition, Color.Blue);
+ }
+
+ if (!hook.shown && !hook.headReleased) continue;
+
+ if (hook.primed)
+ {
+ var primePosition = player.position + hook.primedAt * Game.hookDistance + hook.offset;
+ DrawRectangleRec(Utils.centeredRect(primePosition, Game.hookSize), Color.DarkGray);
+ DrawLineV(handPosition, primePosition, Color.Gray);
+
+ var projection = hook.headProjection(100, 1f / 60f);
+ if (projection.Count > 0)
+ {
+ var projectionArray = projection.ToArray();
+ unsafe
+ {
+ fixed (Vector2* pointerToFirst = &projectionArray[0])
+ {
+ DrawLineStrip(pointerToFirst, projectionArray.Length, Color.Blue);
+ }
+ }
+ }
+ }
+
+ DrawRectangleRec(Utils.centeredRect(handPosition, Game.hookSize), Color.Blue);
+ }
+
+ foreach (var platform in game.platforms)
+ {
+ DrawRectangleRec(platform, Color.Gray);
+ }
+
+ EndMode2D();
+
+ { // Speed bar
+ var speedWidth = 300.0f;
+ DrawText("Speed", 10, 10, 20, Color.Black);
+ DrawRectangleRec(new Rectangle(75, 10, speedWidth, 20), Color.Gray);
+ DrawRectangleRec(new Rectangle(75, 10, speedWidth * (player.velocity.Length() / Game.playerMaxSpeed), 20), Color.Red);
+ DrawText($"{player.velocity.X:N3}", 400, 10, 20, Color.Black);
+ DrawText($"{player.velocity.Y:N3}", 500, 10, 20, Color.Black);
+ }
+
+ { // Rope lengths
+ DrawText($"Left ideal rope length: {leftHook.idealRopeLength}", 10, 30, 20, Color.Black);
+ DrawText($"Right ideal rope length: {rightHook.idealRopeLength}", 10, 50, 20, Color.Black);
+ }
+
+ EndDrawing();
+ }
+
+ CloseWindow();
+ }
+}
\ No newline at end of file