Add map loading logic and spawn rate control

This commit is contained in:
2025-09-30 02:32:03 -07:00
parent 70d1871985
commit 6f582fcca1
43 changed files with 470 additions and 118 deletions

View File

@@ -1,12 +0,0 @@
using Godot;
namespace Zennysoft.Game.Ma;
public partial class EnemyDatabase : Node
{
[Export]
public PackedScene[] EnemyList;
[Export]
public float[] SpawnRate;
}

View File

@@ -0,0 +1,21 @@
namespace Zennysoft.Game.Ma;
public enum EnemyType
{
Sproingy,
Michael,
FilthEater,
Sara,
Ballos,
Chariot,
Chinthe,
AmbassadorGreen,
AmbassadorRed,
AmbassadorSteel,
AgniDemon,
AqueousDemon,
EdenPillar,
Palan,
ShieldOfHeaven,
GoldSproingy
}

View File

@@ -0,0 +1 @@
uid://bwqjmol68w6pr

View File

@@ -0,0 +1,56 @@
using Godot;
using System;
namespace Zennysoft.Game.Ma;
public static class EnemyTypeToEnemyConverter
{
private static readonly string _folderPath = "res://src/enemy/enemy_types";
public static Enemy Convert(EnemyType enemyType)
{
switch (enemyType)
{
case EnemyType.Sproingy:
return InstantiateFromPath(@$"{_folderPath}/01. sproingy/Sproingy.tscn");
case EnemyType.Michael:
return InstantiateFromPath(@$"{_folderPath}/02. michael/Michael.tscn");
case EnemyType.FilthEater:
return InstantiateFromPath(@$"{_folderPath}/03. filth_eater/FilthEater.tscn");
case EnemyType.Sara:
return InstantiateFromPath(@$"{_folderPath}/04. sara/Sara.tscn");
case EnemyType.Ballos:
return InstantiateFromPath(@$"{_folderPath}/05. ballos/Ballos.tscn");
case EnemyType.Chariot:
return InstantiateFromPath(@$"{_folderPath}/06. chariot/Chariot.tscn");
case EnemyType.Chinthe:
return InstantiateFromPath(@$"{_folderPath}/07. chinthe/Chinthe.tscn");
case EnemyType.AmbassadorGreen:
return InstantiateFromPath(@$"{_folderPath}/08a. Ambassador/Ambassador.tscn");
case EnemyType.AmbassadorRed:
return InstantiateFromPath(@$"{_folderPath}/08b. Ambassador (red)/AmbassadorRed.tscn");
case EnemyType.AmbassadorSteel:
return InstantiateFromPath(@$"{_folderPath}/08c. Ambassador (steel)/AmbassadorSteel.tscn");
case EnemyType.AgniDemon:
return InstantiateFromPath(@$"{_folderPath}/09. Agni/AgniDemon.tscn");
case EnemyType.AqueousDemon:
return InstantiateFromPath(@$"{_folderPath}/9b. Aqueos Demon/AqueosDemon.tscn");
case EnemyType.EdenPillar:
return InstantiateFromPath(@$"{_folderPath}/10. Eden Pillar/Eden Pillar.tscn");
case EnemyType.Palan:
return InstantiateFromPath(@$"{_folderPath}/11. Palan/Palan.tscn");
case EnemyType.ShieldOfHeaven:
return InstantiateFromPath(@$"{_folderPath}/12. Shield of Heaven/ShieldModelView.tscn");
case EnemyType.GoldSproingy:
return InstantiateFromPath(@$"{_folderPath}/13. gold sproingy/GoldSproingy.tscn");
default:
throw new NotImplementedException("Not supported");
}
}
private static Enemy InstantiateFromPath(string scenePath)
{
var enemyScene = GD.Load<PackedScene>(scenePath);
return enemyScene.Instantiate<Enemy>();
}
}

View File

@@ -0,0 +1 @@
uid://ctspbxmdle4vt

View File

@@ -17,6 +17,8 @@ public partial class Game : Node3D, IGame
{
public override void _Notification(int what) => this.Notify(what);
IGame IProvide<IGame>.Value() => this;
IGameRepo IProvide<IGameRepo>.Value() => GameRepo;
IPlayer IProvide<IPlayer>.Value() => _player;
@@ -97,7 +99,6 @@ public partial class Game : Node3D, IGame
},
MapData = new MapData()
{
FloorScenes = []
},
RescuedItems = new RescuedItemDatabase()
{

View File

@@ -8,7 +8,7 @@ using Godot;
using System.Threading.Tasks;
using Zennysoft.Ma.Adapter;
public interface IGame : IProvide<IGameRepo>, IProvide<IPlayer>, IProvide<IMap>, IProvide<ISaveChunk<GameData>>, INode3D
public interface IGame : IProvide<IGame>, IProvide<IGameRepo>, IProvide<IPlayer>, IProvide<IMap>, IProvide<ISaveChunk<GameData>>, INode3D
{
void LoadExistingGame();

View File

@@ -15,8 +15,6 @@ public interface IMap : INode3D, IProvide<ISaveChunk<MapInfo>>
Task LoadFloor(string sceneName);
ImmutableDictionary<Floor, string> FloorScenes { get; }
IDungeonFloor CurrentFloor { get; }
Transform3D GetPlayerSpawnPosition();

View File

@@ -0,0 +1,49 @@
using Godot;
using System;
using System.Linq;
using static SpecialFloorLayout;
namespace Zennysoft.Game.Ma;
public static class LayoutToScenePathConverter
{
private static readonly string _folderPath = "res://src/map/dungeon/floors";
public static string Convert(LayoutType layoutType)
{
if (layoutType is SpecialFloorLayout specialFloor)
{
var path = $"{_folderPath}/Special Floors/";
var files = DirAccess.GetFilesAt(path);
switch (specialFloor.FloorName)
{
case SpecialFloorType.Overworld:
return path + files.Single(x => x.Contains("Overworld.tscn"));
case SpecialFloorType.Altar:
return path + files.Single(x => x.Contains("Altar.tscn"));
case SpecialFloorType.BossFloorA:
return path + files.Single(x => x.Contains("Boss Floor A.tscn"));
case SpecialFloorType.BossFloorB:
return path + files.Single(x => x.Contains("Boss Floor B.tscn"));
case SpecialFloorType.GoddessOfGuidanceFloor:
return path + files.Single(x => x.Contains("Goddess of Guidance's Room.tscn"));
case SpecialFloorType.TrueGoddessOfGuidanceFloor:
return path + files.Single(x => x.Contains("Goddess of Guidance's Room - True Form.tscn"));
case SpecialFloorType.FinalFloor:
return path + files.Single(x => x.Contains("Final Floor.tscn"));
default:
throw new NotImplementedException();
}
}
else if (layoutType is DungeonFloorLayout dungeonFloor)
{
var rng = new RandomNumberGenerator();
rng.Randomize();
var index = (int)rng.RandWeighted([.. dungeonFloor.LayoutWithSpawnRate.Values]);
var result = dungeonFloor.LayoutWithSpawnRate.ElementAt(index);
return _folderPath + "/" + result.Key;
}
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1 @@
uid://d0yn4yu7264c8

View File

@@ -0,0 +1,7 @@
using Godot;
namespace Zennysoft.Game.Ma;
public abstract partial class LayoutType : Node
{
}

View File

@@ -0,0 +1 @@
uid://csb6g5a657010

View File

@@ -36,9 +36,7 @@ public partial class Map : Node3D, IMap
#endregion
[Export]
private Array<Floor> Floors { get; set; } = default!;
public ImmutableDictionary<Floor, string> FloorScenes { get; private set; }
private Array<LayoutType> Floors { get; set; } = default!;
public IDungeonFloor CurrentFloor { get; private set; }
@@ -57,14 +55,6 @@ public partial class Map : Node3D, IMap
public void InitializeMapData()
{
FloorScenes = ImmutableDictionary<Floor, string>.Empty
.Add(Floor.Overworld, _floorFilePath + "Special Floors/Overworld.tscn")
.Add(Floor.Altar, _floorFilePath + "Special Floors/Altar.tscn")
.Add(Floor.BossFloorA, _floorFilePath + "Special Floors/15. Boss Floor A.tscn")
.Add(Floor.BossFloorB, _floorFilePath + "Special Floors/34. Boss Floor B.tscn")
.Add(Floor.GoddessOfGuidanceFloor, _floorFilePath + "Special Floors/35. Goddess of Guidance's Room.tscn")
.Add(Floor.VoidRoom, _floorFilePath + "Set B/30. Void Room.tscn")
.Add(Floor.FinalFloor, _floorFilePath + "Special Floors/36. Final Floor.tscn");
CurrentFloorNumber.OnNext(0);
}
@@ -79,9 +69,11 @@ public partial class Map : Node3D, IMap
public async Task LoadFloor()
{
GetMapFiles();
FloorScenes.TryGetValue(Floors.First(), out var sceneToLoad);
var floor = GetChildren().OfType<LayoutType>().ElementAt(CurrentFloorNumber.Value);
var sceneToLoad = LayoutToScenePathConverter.Convert(floor);
await LoadFloor(sceneToLoad);
if (CurrentFloor is DungeonFloor dungeonFloor && floor is DungeonFloorLayout dungeonFloorLayout)
dungeonFloor.SpawnEnemies(dungeonFloorLayout.EnemySpawnRates);
}
public async Task LoadFloor(string sceneName)
@@ -125,10 +117,4 @@ public partial class Map : Node3D, IMap
var transform = GetPlayerSpawnPosition();
Player.TeleportPlayer(transform);
}
private string[] GetMapFiles()
{
var folderLocation = _floorFilePath + "Floor" + CurrentFloorNumber.Value.ToString("D2");
return DirAccess.GetFilesAt(folderLocation);
}
}

View File

@@ -1,6 +1,8 @@
[gd_scene load_steps=6 format=3 uid="uid://by67pn7fdsg1m"]
[gd_scene load_steps=8 format=3 uid="uid://by67pn7fdsg1m"]
[ext_resource type="Script" uid="uid://14e8mu48ed4" path="res://src/map/Map.cs" id="1_bw70o"]
[ext_resource type="Script" uid="uid://cabvj6s31iucg" path="res://addons/special_floor_layout_node/SpecialFloorLayout.cs" id="2_00xd7"]
[ext_resource type="Script" uid="uid://ci7o3nn4mdo8o" path="res://addons/dungeon_floor_layout/DungeonFloorLayout.cs" id="3_v14r0"]
[sub_resource type="Animation" id="Animation_00xd7"]
length = 0.001
@@ -54,9 +56,9 @@ _data = {
&"fade_out": SubResource("Animation_v14r0")
}
[node name="Map" type="Node3D"]
[node name="Map" type="Node3D" node_paths=PackedStringArray("Floors")]
script = ExtResource("1_bw70o")
Floors = Array[int]([3, 2, 0, 4, 1, 5, 6])
Floors = [3, 2, 0, 4, 1, 5, 6]
[node name="ColorRect" type="ColorRect" parent="."]
anchors_preset = 15
@@ -71,3 +73,28 @@ unique_name_in_owner = true
libraries = {
&"": SubResource("AnimationLibrary_00xd7")
}
[node name="Overworld (Add SpecialFloorLayout node for special floors and pick the FloorName from the list)" type="Node" parent="."]
script = ExtResource("2_00xd7")
metadata/_custom_type_script = "uid://cabvj6s31iucg"
[node name="Altar (Arrange order of nodes to change default load order)" type="Node" parent="."]
script = ExtResource("2_00xd7")
FloorName = 1
metadata/_custom_type_script = "uid://cabvj6s31iucg"
[node name="Floor01 (Press Populate Map Data Button to load floor from SetAFloors folder)" type="Node" parent="."]
script = ExtResource("3_v14r0")
LayoutWithSpawnRate = Dictionary[String, float]({})
EnemySpawnRates = {
0: 1.0
}
metadata/_custom_type_script = "uid://ci7o3nn4mdo8o"
[node name="Floor02 (Add DungeonFloorLayout node for regular dungeon floors)" type="Node" parent="."]
script = ExtResource("3_v14r0")
LayoutWithSpawnRate = Dictionary[String, float]({})
EnemySpawnRates = {
0: 1.0
}
metadata/_custom_type_script = "uid://ci7o3nn4mdo8o"

View File

@@ -12,8 +12,6 @@ public partial class DungeonFloor : Node3D, IDungeonFloor
{
public override void _Notification(int what) => this.Notify(what);
[Node] public EnemyDatabase EnemyDatabase { get; set; } = default!;
private Transform3D _playerSpawnPoint;
public ImmutableList<IDungeonRoom> Rooms { get; private set; }
@@ -25,12 +23,16 @@ public partial class DungeonFloor : Node3D, IDungeonFloor
Rooms = [];
Rooms = FindAllDungeonRooms([.. GetChildren()], Rooms);
_playerSpawnPoint = RandomizePlayerSpawnPoint();
var monsterRooms = Rooms.OfType<MonsterRoom>();
foreach (var room in monsterRooms)
room.SpawnEnemies(EnemyDatabase);
}
public Transform3D GetPlayerSpawnPoint() => new Transform3D(_playerSpawnPoint.Basis, new Vector3(_playerSpawnPoint.Origin.X, -1.75f, _playerSpawnPoint.Origin.Z));
public void SpawnEnemies(Godot.Collections.Dictionary<EnemyType, float> enemyInfo)
{
var monsterRooms = Rooms.OfType<MonsterRoom>();
foreach (var room in monsterRooms)
room.SpawnEnemies(enemyInfo);
}
public Transform3D GetPlayerSpawnPoint() => new Transform3D(_playerSpawnPoint.Basis, new Vector3(_playerSpawnPoint.Origin.X, 0f, _playerSpawnPoint.Origin.Z));
private Transform3D RandomizePlayerSpawnPoint()
{

View File

@@ -11,8 +11,6 @@ public abstract partial class DungeonRoom : Node3D, IDungeonRoom
{
public override void _Notification(int what) => this.Notify(what);
[Node] public Marker3D PlayerSpawn { get; set; } = default!;
[Node] private MeshInstance3D _minimap { get; set; } = default!;
public bool IsPlayerInRoom => _isPlayerInRoom;
@@ -30,8 +28,8 @@ public abstract partial class DungeonRoom : Node3D, IDungeonRoom
public void Setup()
{
_enemiesInRoom = [];
_room.BodyEntered += Room_BodyEntered;
_room.BodyExited += Room_BodyExited;
//_room.BodyEntered += Room_BodyEntered;
//_room.BodyExited += Room_BodyExited;
}
private void Room_BodyExited(Node3D body)

View File

@@ -14,17 +14,19 @@ public partial class ExitRoom : DungeonRoom
[Node] private Area3D _exit { get; set; } = default!;
[Node] public Marker3D PlayerSpawn { get; set; } = default!;
public override void _Ready()
{
_exit.AreaEntered += Exit_AreaEntered;
_exit.AreaEntered += Exit_AreaEntered;
}
public void ExitReached()
=> Game.FloorExitReached();
=> Game.FloorExitReached();
private void Exit_AreaEntered(Area3D area)
{
if (area.GetOwner() is IPlayer)
ExitReached();
if (area.GetOwner() is IPlayer)
ExitReached();
}
}

View File

@@ -5,8 +5,6 @@ using System.Collections.Immutable;
namespace Zennysoft.Game.Ma;
public interface IDungeonRoom : INode3D
{
Marker3D PlayerSpawn { get; }
bool IsPlayerInRoom { get; }
bool PlayerDiscoveredRoom { get; }

View File

@@ -15,51 +15,57 @@ public partial class MonsterRoom : DungeonRoom
[Node] public Node3D EnemySpawnPoints { get; set; } = default!;
[Node] public Marker3D PlayerSpawn { get; set; } = default!;
[Node] public ItemDatabase ItemDatabase { get; set; } = default!;
public override void _Ready()
{
SpawnItems();
SpawnItems();
}
public void SpawnEnemies(EnemyDatabase enemyDatabase)
public void SpawnEnemies(Godot.Collections.Dictionary<EnemyType, float> enemyInfo)
{
var rng = new RandomNumberGenerator();
rng.Randomize();
var enemySpawnPoints = EnemySpawnPoints.GetChildren();
var numberOfEnemiesToSpawn = rng.RandiRange(1, enemySpawnPoints.Count);
var rng = new RandomNumberGenerator();
rng.Randomize();
var enemySpawnPoints = EnemySpawnPoints.GetChildren();
var numberOfEnemiesToSpawn = rng.RandiRange(1, enemySpawnPoints.Count);
foreach (var spawnPoint in enemySpawnPoints.Cast<Marker3D>())
{
if (numberOfEnemiesToSpawn <= 0)
break;
numberOfEnemiesToSpawn--;
foreach (var spawnPoint in enemySpawnPoints.Cast<Marker3D>())
{
if (numberOfEnemiesToSpawn <= 0)
break;
numberOfEnemiesToSpawn--;
var enemy = enemyDatabase.EnemyList[rng.RandWeighted(enemyDatabase.SpawnRate)];
var instantiatedEnemy = enemy.Instantiate<Enemy>();
instantiatedEnemy.Position = new Vector3(spawnPoint.Position.X, 0, spawnPoint.Position.Z);
AddChild(instantiatedEnemy);
}
var index = rng.RandWeighted([.. enemyInfo.Values]);
var selectedEnemy = enemyInfo.ElementAt((int)index);
var instantiatedEnemy = EnemyTypeToEnemyConverter.Convert(selectedEnemy.Key);
instantiatedEnemy.Position = new Vector3(spawnPoint.Position.X, 1.75f, spawnPoint.Position.Z);
AddChild(instantiatedEnemy);
}
}
private void SpawnItems()
{
var itemSpawnPoints = ItemSpawnPoints.GetChildren();
var rng = new RandomNumberGenerator();
rng.Randomize();
var numberOfItemsToSpawn = rng.RandiRange(1, itemSpawnPoints.Count);
itemSpawnPoints.Shuffle();
var database = new ItemDatabase();
foreach (var spawnPoint in itemSpawnPoints.Cast<Marker3D>())
{
if (numberOfItemsToSpawn <= 0)
break;
numberOfItemsToSpawn--;
if (ItemSpawnPoints == null)
return;
var selectedItem = database.PickItem<InventoryItem>();
var duplicated = selectedItem.Duplicate((int)DuplicateFlags.UseInstantiation) as Node3D;
duplicated.Position = new Vector3(spawnPoint.Position.X, -1.5f, spawnPoint.Position.Z);
AddChild(duplicated);
}
var itemSpawnPoints = ItemSpawnPoints.GetChildren();
var rng = new RandomNumberGenerator();
rng.Randomize();
var numberOfItemsToSpawn = rng.RandiRange(1, itemSpawnPoints.Count);
itemSpawnPoints.Shuffle();
var database = new ItemDatabase();
foreach (var spawnPoint in itemSpawnPoints.Cast<Marker3D>())
{
if (numberOfItemsToSpawn <= 0)
break;
numberOfItemsToSpawn--;
var selectedItem = database.PickItem<InventoryItem>();
var duplicated = selectedItem.Duplicate((int)DuplicateFlags.UseInstantiation) as Node3D;
duplicated.Position = new Vector3(spawnPoint.Position.X, -1.5f, spawnPoint.Position.Z);
AddChild(duplicated);
}
}
}

View File

@@ -1,8 +0,0 @@
[gd_resource type="Resource" script_class="MapInfo" load_steps=2 format=3 uid="uid://c0764jocyuon4"]
[ext_resource type="Script" uid="uid://bb4a0ijn0q2ou" path="res://src/map/MapInfo.cs" id="1_hrh8w"]
[resource]
script = ExtResource("1_hrh8w")
MapNameAndOdds = Dictionary[String, float]({})
metadata/_custom_type_script = "uid://bb4a0ijn0q2ou"

View File

@@ -1,10 +1,9 @@
[gd_scene load_steps=16 format=3 uid="uid://dpec2lbt83dhe"]
[gd_scene load_steps=15 format=3 uid="uid://dpec2lbt83dhe"]
[ext_resource type="Script" uid="uid://dhollu4j3pynq" path="res://src/map/dungeon/code/MonsterRoom.cs" id="1_312b8"]
[ext_resource type="PackedScene" uid="uid://bvpwkx6ag1yf4" path="res://src/map/dungeon/models/Area 1/Antechamber/A1-Antechamber.glb" id="2_kycn2"]
[ext_resource type="Texture2D" uid="uid://ncu0fsnqyede" path="res://src/minimap/textures/Room Maps/mi_antechamber.png" id="6_0ndak"]
[ext_resource type="PackedScene" uid="uid://twrj4wixcbu7" path="res://src/items/ItemDatabase.tscn" id="17_25wvm"]
[ext_resource type="Texture2D" uid="uid://bkvegamuqdsdd" path="res://src/map/dungeon/corridors/Corridor A/CORRIDOR test_FLOOR1.jpg" id="17_jig7d"]
[ext_resource type="Texture2D" uid="uid://del2dfj3etokd" path="res://src/map/assets/Blocked Door A1 .png" id="20_le1vp"]
[sub_resource type="BoxShape3D" id="BoxShape3D_phhs1"]
@@ -21,10 +20,8 @@ albedo_texture = ExtResource("20_le1vp")
texture_filter = 0
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_b605t"]
albedo_texture = ExtResource("17_jig7d")
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_cnaww"]
albedo_texture = ExtResource("17_jig7d")
[sub_resource type="BoxShape3D" id="BoxShape3D_e81mq"]
size = Vector3(20, 8, 16)
@@ -169,7 +166,8 @@ shape = SubResource("BoxShape3D_e81mq")
[node name="Minimap" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.61193, 0.859072, 0)
[node name="mi_antechamber" type="MeshInstance3D" parent="Minimap"]
[node name="Minimap" type="MeshInstance3D" parent="Minimap"]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.983959, 0)
layers = 2
mesh = SubResource("PlaneMesh_7ppra")

View File

@@ -111,3 +111,8 @@ popup/item_13/text = "Shield of Heaven"
popup/item_13/id = 13
popup/item_14/text = "Gold Sproingy"
popup/item_14/id = 14
[node name="LoadNextFloorButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer/VFlowContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Load Next Floor"

View File

@@ -23,6 +23,8 @@ public partial class PauseDebugMenu : Control, IDebugMenu
[Node] public OptionButton SpawnEnemyDropDown { get; set; } = default!;
[Node] public Button LoadNextFloorButton { get; set; } = default!;
private readonly string _floorFilePath = @"res://src/map/dungeon/floors/";
private readonly string _enemyFilePath = @"res://src/enemy/enemy_types";
@@ -33,6 +35,8 @@ public partial class PauseDebugMenu : Control, IDebugMenu
public override void _Ready()
{
LoadNextFloorButton.Pressed += LoadNextFloorButton_Pressed;
_itemDatabase = new ItemDatabase();
_spawnableItems = _itemDatabase.Items;