const rl = @import("raylib"); const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const friction = 0.99; const walkForce = 4000; const maxSpeed = 200; const handDistance = 50; const Enemy = struct { position: rl.Vector2, }; const Player = struct { position: rl.Vector2, velocity: rl.Vector2 = rl.Vector2.zero(), acceleration: rl.Vector2 = rl.Vector2.zero(), handDirection: rl.Vector2 = rl.Vector2{ .x = 1, .y = 0 } }; const Rope = struct { const maxNodes = 64; const NodeArray = std.BoundedArray(rl.Vector2, maxNodes); nodePositions: NodeArray, nodeVelocities: NodeArray, segmentLength: f32, springForce: f32 = 10, leftoverTime: f32 = 0, friction: f32 = 0.0005, fn init(from: rl.Vector2, to: rl.Vector2, nodeCount: u32) Rope { assert(nodeCount <= maxNodes); var nodePositions = NodeArray.init(nodeCount) catch unreachable; const diff = to.sub(from); const segmentLength = diff.length() / @as(f32, @floatFromInt(nodeCount)); const segmentStep = diff.normalize().scale(segmentLength); nodePositions.set(0, from); for (1..nodeCount) |i| { const nextNode = nodePositions.get(i-1).add(segmentStep); nodePositions.set(i, nextNode); } var nodeVelocities = NodeArray.init(nodeCount) catch unreachable; for (nodeVelocities.slice()) |*vel| { vel.* = rl.Vector2.zero(); } return Rope{ .nodePositions = nodePositions, .nodeVelocities = nodeVelocities, .segmentLength = segmentLength }; } fn update(self: *Rope, dt: f32) void { const timestep = 1.0/(60.0 * 100); self.leftoverTime += dt; while (self.leftoverTime >= timestep) : (self.leftoverTime -= timestep) { self.update_step(timestep); } } fn update_step(self: *Rope, dt: f32) void { var offsets = NodeArray.init(self.nodePositions.len) catch unreachable; for (offsets.slice()) |*offset| { offset.* = rl.Vector2.zero(); } var accelerations = NodeArray.init(self.nodePositions.len) catch unreachable; for (accelerations.slice()) |*acc| { acc.* = rl.Vector2.zero(); } const node_count = self.nodePositions.len; for (1..node_count) |i| { const node1: rl.Vector2 = self.nodePositions.get(i-1); const node2: rl.Vector2 = self.nodePositions.get(i); const node_diff = node2.sub(node1); const node_dir = node_diff.normalize(); const rope_force = node_diff.length() - self.segmentLength; const d = node_dir.scale(rope_force); offsets.set(i-1, offsets.get(i-1).add(d.scale(0.5))); offsets.set(i , offsets.get(i ).add(d.scale(-0.5))); accelerations.set(i-1, accelerations.get(i-1).add(d)); accelerations.set(i , accelerations.get(i ).add(d.scale(-1))); } for (1..node_count) |i| { const pos = &self.nodePositions.slice()[i]; const vel = &self.nodeVelocities.slice()[i]; const acc = accelerations.get(i); const offset = offsets.get(i); vel.* = vel.add(acc.scale(dt)); vel.* = vel.scale(1 - self.friction); pos.* = pos.add(vel.scale(dt*10000)); pos.* = pos.add(offset.scale(0.01)); } } }; allocator: Allocator, prng: std.rand.DefaultPrng, enemyTimer: f32 = 0, spawnedEnemiesCount: u32 = 0, killCount: u32 = 0, enemies: std.ArrayList(Enemy), player: Player, rope: Rope, const Self = @This(); pub fn init(allocator: Allocator) Self { var playerPosition = rl.Vector2{ .x = 400, .y = 400 }; var ropeSize: f32 = 200; var handPosition = playerPosition.add(rl.Vector2{ .x = handDistance, .y = 0}); return Self { .allocator = allocator, .prng = std.rand.DefaultPrng.init(@bitCast(std.time.timestamp())), .enemies = std.ArrayList(Enemy).init(allocator), .player = .{ .position = playerPosition }, .rope = Rope.init(handPosition, handPosition.add(.{ .x = ropeSize, .y = 0.002 }), 10) }; } pub fn reset(self: *Self) void { self.deinit(); self.* = Self.init(self.allocator); } pub fn deinit(self: Self) void { self.enemies.deinit(); } pub fn tick(self: *Self) !void { rl.ClearBackground(rl.BLACK); const dt = rl.GetFrameTime(); var rng = self.prng.random(); _ = rng; var player = &self.player; var enemies = &self.enemies; var rope = &self.rope; var allocator = self.allocator; self.enemyTimer -= dt; // if (self.enemyTimer <= 0) { // self.enemyTimer = 0.5 + rng.float(f32) * 2; // self.spawnedEnemiesCount += 1; // try enemies.append(Enemy{ // .position = rl.Vector2.randomOnUnitCircle(rng).scale(100) // }); // } var inputDx: f32 = 0; var inputDy: f32 = 0; if (rl.IsKeyDown(rl.KeyboardKey.KEY_W)) { inputDy -= 1; } if (rl.IsKeyDown(rl.KeyboardKey.KEY_S)) { inputDy += 1; } if (rl.IsKeyDown(rl.KeyboardKey.KEY_A)) { inputDx -= 1; } if (rl.IsKeyDown(rl.KeyboardKey.KEY_D)) { inputDx += 1; } var input_dir = rl.Vector2{ .x = inputDx, .y = inputDy }; input_dir = rl.Vector2Normalize(input_dir); if (input_dir.x != 0 or input_dir.y != 0) { player.handDirection = input_dir; } player.acceleration = input_dir; player.acceleration = rl.Vector2Scale(player.acceleration, walkForce); player.velocity = rl.Vector2Add(player.velocity, rl.Vector2Scale(player.acceleration, dt)); player.velocity = rl.Vector2ClampValue(player.velocity, 0, maxSpeed); player.velocity = rl.Vector2Scale(player.velocity, std.math.pow(f32, (1 - friction), dt)); player.position = rl.Vector2Add(player.position, rl.Vector2Scale(player.velocity, dt)); const enemySize: f32 = 20; const deathBallSize: f32 = 10; const playerSize = 20; const enemySpeed = 100; for (enemies.items) |*enemy| { const toPlayer = player.position.sub(enemy.position); if (toPlayer.length() <= playerSize + enemySize) { self.reset(); return; } const directionToPlayer = toPlayer.normalize(); enemy.position = enemy.position.add(directionToPlayer.scale(enemySpeed * dt)); rl.DrawCircle( @intFromFloat(enemy.position.x), @intFromFloat(enemy.position.y), enemySize, rl.RED ); } const handPosition = player.position.add(player.handDirection.scale(handDistance)); const enemyCountText = try std.fmt.allocPrintZ(allocator, "{d}", .{self.spawnedEnemiesCount}); defer allocator.free(enemyCountText); rl.DrawText(enemyCountText, 10, 10, 24, rl.RED); const killCountText = try std.fmt.allocPrintZ(allocator, "{d}", .{self.killCount}); defer allocator.free(killCountText); rl.DrawText(killCountText, 10, 30, 24, rl.GREEN); rl.DrawCircle( @intFromFloat(player.position.x), @intFromFloat(player.position.y), playerSize, rl.RAYWHITE ); rl.DrawCircle( @intFromFloat(handPosition.x), @intFromFloat(handPosition.y), 5, rl.RAYWHITE ); rl.DrawLine( @intFromFloat(player.position.x), @intFromFloat(player.position.y), @intFromFloat(player.position.x + player.velocity.x), @intFromFloat(player.position.y + player.velocity.y), rl.GREEN ); rl.DrawCircle( @intFromFloat(player.position.x + input_dir.x * maxSpeed), @intFromFloat(player.position.y + input_dir.y * maxSpeed), 5, rl.GREEN ); rope.nodePositions.set(0, handPosition); rope.update(dt); const rope_color = rl.PURPLE; for (0..(rope.nodePositions.len-1)) |i| { var node1: rl.Vector2 = rope.nodePositions.get(i); var node2: rl.Vector2 = rope.nodePositions.get(i+1); rl.DrawLine( @intFromFloat(node1.x), @intFromFloat(node1.y), @intFromFloat(node2.x), @intFromFloat(node2.y), rope_color ); } for (rope.nodePositions.slice()) |node| { rl.DrawCircle( @intFromFloat(node.x), @intFromFloat(node.y), 5, rope_color ); } const deathBallPosition = rope.nodePositions.get(rope.nodePositions.len-1); rl.DrawCircle( @intFromFloat(deathBallPosition.x), @intFromFloat(deathBallPosition.y), deathBallSize, rl.RED ); { var i: usize = 0; while (i < enemies.items.len) { const enemy = &enemies.items[i]; const distanceToDeathBall = enemy.position.sub(deathBallPosition).length(); if (distanceToDeathBall < enemySize + deathBallSize) { self.killCount += 1; _ = enemies.swapRemove(i); continue; } i += 1; } } }