From 026db920b37e697b1cf1bb16c489b51843eb3cae Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 13 Mar 2026 20:27:38 +0100 Subject: [PATCH] Can now pick up items Signed-off-by: Hans Kokx --- lib/features/entities/collectible.dart | 47 ++++++++ lib/features/entities/decorative.dart | 10 +- lib/features/entities/enemies/dog.dart | 125 +++++++++++++++++++++ lib/features/entities/entity_registry.dart | 8 +- lib/features/player/player.dart | 108 ++++++++++++++++++ lib/features/renderer/raycast_painter.dart | 16 ++- lib/features/renderer/renderer.dart | 84 +++++++++----- 7 files changed, 358 insertions(+), 40 deletions(-) create mode 100644 lib/features/entities/collectible.dart create mode 100644 lib/features/entities/enemies/dog.dart create mode 100644 lib/features/player/player.dart diff --git a/lib/features/entities/collectible.dart b/lib/features/entities/collectible.dart new file mode 100644 index 0000000..e956863 --- /dev/null +++ b/lib/features/entities/collectible.dart @@ -0,0 +1,47 @@ +import 'package:wolf_dart/features/entities/entity.dart'; + +enum CollectibleType { ammo, health, treasure, weapon, key } + +class Collectible extends Entity { + final CollectibleType type; + + Collectible({ + required super.x, + required super.y, + required super.spriteIndex, + required super.mapId, + required this.type, + }) : super(state: EntityState.staticObj); + + // Define which Map IDs are actually items you can pick up + static bool isCollectible(int objId) { + return (objId >= 43 && objId <= 44) || // Keys + (objId >= 47 && objId <= 56); // Health, Ammo, Weapons, Treasure, 1-Up + } + + static CollectibleType _getType(int objId) { + if (objId == 43 || objId == 44) return CollectibleType.key; + if (objId == 47 || objId == 48) return CollectibleType.health; + if (objId == 49) return CollectibleType.ammo; + if (objId == 50 || objId == 51) return CollectibleType.weapon; + return CollectibleType.treasure; // 52-56 + } + + static Collectible? trySpawn( + int objId, + double x, + double y, + int difficultyLevel, + ) { + if (isCollectible(objId)) { + return Collectible( + x: x, + y: y, + spriteIndex: objId - 21, // Same VSWAP math as decorations! + mapId: objId, + type: _getType(objId), + ); + } + return null; + } +} diff --git a/lib/features/entities/decorative.dart b/lib/features/entities/decorative.dart index 6101cba..0ffd4c1 100644 --- a/lib/features/entities/decorative.dart +++ b/lib/features/entities/decorative.dart @@ -11,7 +11,15 @@ class Decorative extends Entity { // Checks if the Map ID belongs to a standard decoration static bool isDecoration(int objId) { - return (objId >= 23 && objId <= 70) || objId == 124; + if (objId == 124) return true; // Dead guard + if (objId >= 23 && objId <= 70) { + // Exclude the collectibles! + if ((objId >= 43 && objId <= 44) || (objId >= 47 && objId <= 56)) { + return false; + } + return true; + } + return false; } // Safely calculates the VSWAP sprite index for standard decorations diff --git a/lib/features/entities/enemies/dog.dart b/lib/features/entities/enemies/dog.dart new file mode 100644 index 0000000..ac9044e --- /dev/null +++ b/lib/features/entities/enemies/dog.dart @@ -0,0 +1,125 @@ +import 'dart:math' as math; + +import 'package:wolf_dart/classes/linear_coordinates.dart'; +import 'package:wolf_dart/features/entities/enemies/enemy.dart'; +import 'package:wolf_dart/features/entities/entity.dart'; + +class Dog extends Enemy { + static const double speed = 0.05; // Dogs are much faster than guards! + bool _hasBittenThisCycle = false; + + Dog({ + required super.x, + required super.y, + required super.angle, + required super.mapId, + }) : super( + spriteIndex: 99, // Dogs start at index 99 in VSWAP + state: EntityState.idle, + ); + + static Dog? trySpawn(int objId, double x, double y, int difficultyLevel) { + bool canSpawn = false; + switch (difficultyLevel) { + case 0: + canSpawn = objId >= 116 && objId <= 119; + break; + case 1: + canSpawn = objId >= 152 && objId <= 155; + break; + case 2: + canSpawn = objId >= 188 && objId <= 191; + break; + case 3: + canSpawn = objId >= 224 && objId <= 227; + break; + } + + if (canSpawn) { + return Dog( + x: x, + y: y, + angle: Enemy.getInitialAngle(objId), + mapId: objId, + ); + } + return null; + } + + @override + void update({ + required int elapsedMs, + required LinearCoordinates player, + required bool Function(int x, int y) isWalkable, + required void Function(int damage) onDamagePlayer, + }) { + if (state == EntityState.idle) { + if (hasLineOfSight(player, isWalkable)) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + } + + if (state == EntityState.idle || + state == EntityState.patrolling || + state == EntityState.shooting) { + // "Shooting" here means biting + + double dx = player.x - x; + double dy = player.y - y; + double distance = math.sqrt(dx * dx + dy * dy); + double angleToPlayer = math.atan2(dy, dx); + + if (state == EntityState.patrolling || state == EntityState.shooting) { + angle = angleToPlayer; + } + + double diff = angle - angleToPlayer; + while (diff <= -math.pi) { + diff += 2 * math.pi; + } + while (diff > math.pi) { + diff -= 2 * math.pi; + } + + int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; + if (octant < 0) octant += 8; + + if (state == EntityState.idle) { + spriteIndex = 99 + octant; // Base dog standing sprite + } else if (state == EntityState.patrolling) { + if (distance > 0.8) { + double moveX = x + math.cos(angle) * speed; + double moveY = y + math.sin(angle) * speed; + + if (isWalkable(moveX.toInt(), y.toInt())) x = moveX; + if (isWalkable(x.toInt(), moveY.toInt())) y = moveY; + } + + // Dogs only have 4 walking angles, alternating legs + int walkFrame = (elapsedMs ~/ 100) % 4; + spriteIndex = 107 + (walkFrame * 8) + octant; + + // Dog Bite Attack (Must be practically touching the player) + if (distance < 1.0 && elapsedMs - lastActionTime > 1000) { + state = EntityState.shooting; + lastActionTime = elapsedMs; + _hasBittenThisCycle = false; + } + } else if (state == EntityState.shooting) { + int timeAttacking = elapsedMs - lastActionTime; + + if (timeAttacking < 200) { + spriteIndex = 139; // Jumping/Biting frame + if (!_hasBittenThisCycle) { + onDamagePlayer(5); // Dogs do less damage than guards + _hasBittenThisCycle = true; + } + } else { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + } + } + } +} diff --git a/lib/features/entities/entity_registry.dart b/lib/features/entities/entity_registry.dart index 422564c..329662a 100644 --- a/lib/features/entities/entity_registry.dart +++ b/lib/features/entities/entity_registry.dart @@ -1,5 +1,7 @@ +import 'package:wolf_dart/features/entities/collectible.dart'; import 'package:wolf_dart/features/entities/decorative.dart'; import 'package:wolf_dart/features/entities/enemies/brown_guard.dart'; +import 'package:wolf_dart/features/entities/enemies/dog.dart'; import 'package:wolf_dart/features/entities/entity.dart'; typedef EntitySpawner = @@ -8,8 +10,10 @@ typedef EntitySpawner = abstract class EntityRegistry { // Add future enemies (SSGuard, Dog, etc.) to this list! static final List _spawners = [ - Decorative.trySpawn, - BrownGuard.trySpawn, + Collectible.trySpawn, // Check collectibles + Decorative.trySpawn, // Then check decorations + BrownGuard.trySpawn, // Then check guards + Dog.trySpawn, // Then check dogs ]; static Entity? spawn( diff --git a/lib/features/player/player.dart b/lib/features/player/player.dart new file mode 100644 index 0000000..47797eb --- /dev/null +++ b/lib/features/player/player.dart @@ -0,0 +1,108 @@ +import 'dart:math' as math; + +import 'package:wolf_dart/classes/linear_coordinates.dart'; +import 'package:wolf_dart/features/entities/collectible.dart'; + +class Player { + // Spatial + double x; + double y; + double angle; + + // Stats + int health = 100; + int ammo = 8; + int score = 0; + + // Inventory + bool hasGoldKey = false; + bool hasSilverKey = false; + bool hasMachineGun = false; + bool hasChainGun = false; + + Player({ + required this.x, + required this.y, + required this.angle, + }); + + // Helper getter to interface with the RaycasterPainter + LinearCoordinates get position => (x: x, y: y); + + // Helper methods to keep state manipulation safe + void takeDamage(int damage) { + health = math.max(0, health - damage); + + if (health <= 0) { + print("YOU DIED!"); + } else { + print("Ouch! ($health)"); + } + } + + void heal(int amount) { + final int newHealth = math.min(100, health + amount); + + if (health < 100) { + print("Feelin' better. ($newHealth)"); + } + + health = newHealth; + } + + void addAmmo(int amount) { + final int newAmmo = math.min(99, ammo + amount); + + if (ammo < 99) { + print("Hell yeah. ($newAmmo)"); + } + + ammo = newAmmo; + } + + bool tryPickup(Collectible item) { + bool pickedUp = false; + + switch (item.type) { + case CollectibleType.health: + if (health >= 100) return false; + // Map IDs 47 (Dog Food) and 48 (Medkit) + heal(item.mapId == 47 ? 4 : 25); + pickedUp = true; + break; + + case CollectibleType.ammo: + if (ammo >= 99) return false; + addAmmo(8); + pickedUp = true; + break; + + case CollectibleType.treasure: + // Score values for Cross (52), Chalice (53), Chest (54), Crown (55) + if (item.mapId == 52) score += 100; + if (item.mapId == 53) score += 500; + if (item.mapId == 54) score += 1000; + if (item.mapId == 55) score += 5000; + if (item.mapId == 56) { + // 1-Up + heal(100); + addAmmo(25); + } + pickedUp = true; + break; + + case CollectibleType.weapon: + if (item.mapId == 50) hasMachineGun = true; + if (item.mapId == 51) hasChainGun = true; + pickedUp = true; + break; + + case CollectibleType.key: + if (item.mapId == 43) hasGoldKey = true; + if (item.mapId == 44) hasSilverKey = true; + pickedUp = true; + break; + } + return pickedUp; + } +} diff --git a/lib/features/renderer/raycast_painter.dart b/lib/features/renderer/raycast_painter.dart index 78b5de3..24d6907 100644 --- a/lib/features/renderer/raycast_painter.dart +++ b/lib/features/renderer/raycast_painter.dart @@ -1,16 +1,15 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/classes/matrix.dart'; import 'package:wolf_dart/features/entities/entity.dart'; +import 'package:wolf_dart/features/player/player.dart'; import 'package:wolf_dart/features/renderer/color_palette.dart'; class RaycasterPainter extends CustomPainter { final Matrix map; final List> textures; - final LinearCoordinates player; - final double playerAngle; + final Player player; final double fov; final Map doorOffsets; final List> sprites; @@ -20,7 +19,6 @@ class RaycasterPainter extends CustomPainter { required this.map, required this.textures, required this.player, - required this.playerAngle, required this.fov, required this.doorOffsets, required this.sprites, @@ -44,8 +42,8 @@ class RaycasterPainter extends CustomPainter { // The 1D Z-Buffer List zBuffer = List.filled(screenWidth, 0.0); - double dirX = math.cos(playerAngle); - double dirY = math.sin(playerAngle); + double dirX = math.cos(player.angle); + double dirY = math.sin(player.angle); double planeX = -dirY * math.tan(fov / 2); double planeY = dirX * math.tan(fov / 2); @@ -275,8 +273,8 @@ class RaycasterPainter extends CustomPainter { if (side == 1) texNum += 1; } - if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX; - if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX; + if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX; + if (side == 1 && math.sin(player.angle) < 0) texX = 63 - texX; double startY = drawStart.toDouble(); double stepY = wallHeight / 64.0; @@ -307,6 +305,6 @@ class RaycasterPainter extends CustomPainter { @override bool shouldRepaint(covariant RaycasterPainter oldDelegate) { return oldDelegate.player != player || - oldDelegate.playerAngle != playerAngle; + oldDelegate.player.angle != player.angle; } } diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index d6df03b..658aec0 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -6,10 +6,12 @@ import 'package:flutter/services.dart'; import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/classes/matrix.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart'; +import 'package:wolf_dart/features/entities/collectible.dart'; import 'package:wolf_dart/features/entities/enemies/enemy.dart'; import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/entity_registry.dart'; import 'package:wolf_dart/features/map/wolf_map.dart'; +import 'package:wolf_dart/features/player/player.dart'; import 'package:wolf_dart/features/renderer/raycast_painter.dart'; import 'package:wolf_dart/sprite_gallery.dart'; @@ -38,13 +40,11 @@ class _WolfRendererState extends State final double fov = math.pi / 3; - late LinearCoordinates player; - late double playerAngle; + late Player player; bool _isLoading = true; bool _spaceWasPressed = false; - int playerHealth = 100; double damageFlashOpacity = 0.0; List entities = []; @@ -76,17 +76,26 @@ class _WolfRendererState extends State // Player Spawn if (objId >= 19 && objId <= 22) { - player = (x: x + 0.5, y: y + 0.5); + double spawnAngle = 0.0; switch (objId) { case 19: - playerAngle = 3 * math.pi / 2; + spawnAngle = 3 * math.pi / 2; + break; case 20: - playerAngle = 0.0; + spawnAngle = 0.0; + break; case 21: - playerAngle = math.pi / 2; + spawnAngle = math.pi / 2; + break; case 22: - playerAngle = math.pi; + spawnAngle = math.pi; + break; } + player = Player( + x: x + 0.5, + y: y + 0.5, + angle: spawnAngle, + ); } else { Entity? newEntity = EntityRegistry.spawn( objId, @@ -160,7 +169,8 @@ class _WolfRendererState extends State } } } - player = nearestSafeSpot; + player.x = nearestSafeSpot.x; + player.y = nearestSafeSpot.y; } } @@ -197,21 +207,21 @@ class _WolfRendererState extends State final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed; if (pressedKeys.contains(LogicalKeyboardKey.keyW)) { - moveStepX += math.cos(playerAngle) * moveSpeed; - moveStepY += math.sin(playerAngle) * moveSpeed; + moveStepX += math.cos(player.angle) * moveSpeed; + moveStepY += math.sin(player.angle) * moveSpeed; } if (pressedKeys.contains(LogicalKeyboardKey.keyS)) { - moveStepX -= math.cos(playerAngle) * moveSpeed; - moveStepY -= math.sin(playerAngle) * moveSpeed; + moveStepX -= math.cos(player.angle) * moveSpeed; + moveStepY -= math.sin(player.angle) * moveSpeed; } if (pressedKeys.contains(LogicalKeyboardKey.keyA)) { - playerAngle -= turnSpeed; + player.angle -= turnSpeed; } if (pressedKeys.contains(LogicalKeyboardKey.keyD)) { - playerAngle += turnSpeed; + player.angle += turnSpeed; } - if (playerAngle < 0) playerAngle += 2 * math.pi; - if (playerAngle > 2 * math.pi) playerAngle -= 2 * math.pi; + if (player.angle < 0) player.angle += 2 * math.pi; + if (player.angle > 2 * math.pi) player.angle -= 2 * math.pi; // 2. UPDATED WALL COLLISION const double margin = 0.3; @@ -221,7 +231,7 @@ class _WolfRendererState extends State : (newX - margin).toInt(); if (_isWalkable(checkX, player.y.toInt())) { - player = (x: newX, y: player.y); + player.x = newX; } double newY = player.y + moveStepY; @@ -230,15 +240,15 @@ class _WolfRendererState extends State : (newY - margin).toInt(); if (_isWalkable(player.x.toInt(), checkY)) { - player = (x: player.x, y: newY); + player.y = newY; } // 3. UPDATED DOOR INTERACTION bool isSpacePressed = pressedKeys.contains(LogicalKeyboardKey.space); if (isSpacePressed && !_spaceWasPressed) { - int targetX = (player.x + math.cos(playerAngle)).toInt(); - int targetY = (player.y + math.sin(playerAngle)).toInt(); + int targetX = (player.x + math.cos(player.angle)).toInt(); + int targetY = (player.y + math.sin(player.angle)).toInt(); if (targetY > 0 && targetY < currentLevel.length && @@ -256,15 +266,37 @@ class _WolfRendererState extends State _spaceWasPressed = isSpacePressed; // --- 4. UPDATE ENTITY LOGIC --- + List itemsToRemove = []; // Collect items to delete after the loop + for (Entity entity in entities) { if (entity is Enemy) { entity.update( elapsedMs: elapsed.inMilliseconds, - player: player, + player: player.position, isWalkable: _isWalkable, onDamagePlayer: _takeDamage, ); } + // NEW: Add Collectible Interaction Logic + else if (entity is Collectible) { + double dx = player.x - entity.x; + double dy = player.y - entity.y; + double dist = math.sqrt(dx * dx + dy * dy); + + // If player is close enough to the item + if (dist < 0.5) { + if (player.tryPickup(entity)) { + itemsToRemove.add(entity); + } + } + } + } + + // Remove the items that were successfully picked up + if (itemsToRemove.isNotEmpty) { + setState(() { + entities.removeWhere((e) => itemsToRemove.contains(e)); + }); } // Fade out the damage flash smoothly @@ -277,11 +309,8 @@ class _WolfRendererState extends State // A helper method to handle getting shot void _takeDamage(int damage) { - playerHealth -= damage; - damageFlashOpacity = 0.5; // Trigger the red flash - if (playerHealth <= 0) { - print("YOU DIED! (We should add a game over screen later)"); - } + player.takeDamage(damage); + damageFlashOpacity = 0.5; } @override @@ -310,7 +339,6 @@ class _WolfRendererState extends State map: currentLevel, textures: gameMap.textures, player: player, - playerAngle: playerAngle, fov: fov, doorOffsets: doorOffsets, entities: entities,