diff --git a/FUTURE_FEATURES.md b/FUTURE_FEATURES.md
new file mode 100644
index 0000000..9e2ae7e
--- /dev/null
+++ b/FUTURE_FEATURES.md
@@ -0,0 +1,33 @@
+# Future Features - WebFun-Style Development Tools
+
+Based on the WebFun implementation (https://codeberg.org/cyco/WebFun), these features would be valuable additions:
+
+## Save Game Inspector
+- View and edit saved game state
+- Inspect inventory, position, zone state
+- Debug mission progress
+
+## Zone Editor
+- Visual zone editing with tile placement
+- Edit zone actions/scripts (IACT)
+- Configure zone objects (NPCs, items, doors)
+- WebFun uses "a lisp-like language" for scripting
+
+## Asset Viewer/Editor
+- Tile browser with flags visualization
+- Character list with animation preview
+- Sound browser
+- Puzzle data viewer
+
+## Debug Menu
+- Toggle debug overlays (collision, zone info, NPC paths)
+- Script debugger for zone actions
+- Code coverage for in-game scripts
+- Zone teleportation
+- Item spawning
+- God mode / invincibility
+
+## Implementation Notes
+- Could be a separate ImGui overlay or SDL-based UI
+- Toggle with a debug key (F12 or similar)
+- Save/load debug state between sessions
diff --git a/src/YodaStoriesNG.Engine/Bot/MissionBot.cs b/src/YodaStoriesNG.Engine/Bot/MissionBot.cs
index 0c01091..8ae714f 100644
--- a/src/YodaStoriesNG.Engine/Bot/MissionBot.cs
+++ b/src/YodaStoriesNG.Engine/Bot/MissionBot.cs
@@ -274,12 +274,40 @@ public class MissionBot
if (_actions.IsCompleted)
{
_actions.Reset();
+
+ // Check if we have a pending zone exit direction - if so, try to walk off the edge
+ if (_pendingZoneExitDirection.HasValue && _state.CurrentZone != null)
+ {
+ var dir = _pendingZoneExitDirection.Value;
+ // Use same threshold as MoveToZoneEdge (<=2 from edge)
+ bool atEdge = dir switch
+ {
+ Direction.Up => _state.PlayerY <= 2,
+ Direction.Down => _state.PlayerY >= _state.CurrentZone.Height - 3,
+ Direction.Left => _state.PlayerX <= 2,
+ Direction.Right => _state.PlayerX >= _state.CurrentZone.Width - 3,
+ _ => false
+ };
+
+ if (atEdge)
+ {
+ // Check if there's a connected zone in this direction
+ var connected = _worldGenerator.GetConnectedZone(_state.CurrentZoneId, dir);
+ Console.WriteLine($"[BOT] At edge, walking {dir}. Connected zone: {connected?.ToString() ?? "none"}");
+ OnActionRequested?.Invoke(BotActionType.Move, 0, 0, dir);
+ _pendingZoneExitDirection = null;
+ return; // Stay in executing state for zone transition
+ }
+ }
+
+ _pendingZoneExitDirection = null;
_currentState = BotState.ThinkingAboutObjective;
_explorationAttempts = 0;
}
else if (!_actions.IsBusy)
{
// Action finished without completing - go back to thinking
+ _pendingZoneExitDirection = null;
_currentState = BotState.ThinkingAboutObjective;
}
}
@@ -467,34 +495,69 @@ public class MissionBot
TryChangeZone();
}
+ // Track the direction we're trying to exit
+ private Direction? _pendingZoneExitDirection;
+
private void MoveToZoneEdge(Direction dir)
{
if (_state.CurrentZone == null) return;
int targetX = _state.PlayerX;
int targetY = _state.PlayerY;
+ int edgeY = 0, edgeX = 0;
switch (dir)
{
case Direction.Up:
- targetY = 0;
+ targetY = 1; // Move to Y=1 first (Y=0 might be blocked)
+ edgeY = 0;
+ edgeX = _state.PlayerX;
break;
case Direction.Down:
- targetY = _state.CurrentZone.Height - 1;
+ targetY = _state.CurrentZone.Height - 2; // Y=height-2
+ edgeY = _state.CurrentZone.Height - 1;
+ edgeX = _state.PlayerX;
break;
case Direction.Left:
- targetX = 0;
+ targetX = 1; // X=1
+ edgeX = 0;
+ edgeY = _state.PlayerY;
break;
case Direction.Right:
- targetX = _state.CurrentZone.Width - 1;
+ targetX = _state.CurrentZone.Width - 2; // X=width-2
+ edgeX = _state.CurrentZone.Width - 1;
+ edgeY = _state.PlayerY;
break;
}
- // Find nearest walkable position at edge
- var nearest = _pathfinder.FindNearestWalkable(_state.CurrentZone, targetX, targetY, _state.ZoneNPCs);
- if (nearest.HasValue)
+ // Store the direction for when we reach the edge
+ _pendingZoneExitDirection = dir;
+
+ // Check if we're already near the edge
+ bool nearEdge = dir switch
{
- _actions.MoveTo(nearest.Value.X, nearest.Value.Y);
+ Direction.Up => _state.PlayerY <= 2,
+ Direction.Down => _state.PlayerY >= _state.CurrentZone.Height - 3,
+ Direction.Left => _state.PlayerX <= 2,
+ Direction.Right => _state.PlayerX >= _state.CurrentZone.Width - 3,
+ _ => false
+ };
+
+ if (nearEdge)
+ {
+ // Already near edge - just walk in that direction to exit
+ var connected = _worldGenerator.GetConnectedZone(_state.CurrentZoneId, dir);
+ Console.WriteLine($"[BOT] At edge, walking {dir} to exit zone {_state.CurrentZoneId}. Connected zone: {connected?.ToString() ?? "none"}");
+ OnActionRequested?.Invoke(BotActionType.Move, 0, 0, dir);
+ }
+ else
+ {
+ // Move toward edge first
+ var nearest = _pathfinder.FindNearestWalkable(_state.CurrentZone, targetX, targetY, _state.ZoneNPCs);
+ if (nearest.HasValue)
+ {
+ _actions.MoveTo(nearest.Value.X, nearest.Value.Y);
+ }
}
}
diff --git a/src/YodaStoriesNG.Engine/Bot/MissionSolver.cs b/src/YodaStoriesNG.Engine/Bot/MissionSolver.cs
index 1495d22..315fba3 100644
--- a/src/YodaStoriesNG.Engine/Bot/MissionSolver.cs
+++ b/src/YodaStoriesNG.Engine/Bot/MissionSolver.cs
@@ -161,6 +161,34 @@ public class MissionSolver
};
}
+ // Check if we need to change zones to reach the target
+ if (currentStep.ZoneId.HasValue && currentStep.ZoneId.Value != _state.CurrentZoneId)
+ {
+ // Need to navigate to target zone
+ var dir = GetDirectionToAdjacentZone(currentStep.ZoneId.Value);
+ if (dir.HasValue)
+ {
+ return new BotObjective
+ {
+ Type = ObjectiveType.ChangeZone,
+ Description = $"Go to zone {currentStep.ZoneId.Value}",
+ TargetZoneId = currentStep.ZoneId.Value,
+ Direction = dir.Value
+ };
+ }
+ else
+ {
+ // Target zone not adjacent - explore toward it
+ return new BotObjective
+ {
+ Type = ObjectiveType.Explore,
+ Description = $"Navigate toward zone {currentStep.ZoneId.Value}"
+ };
+ }
+ }
+
+ // We're in the target zone (or no specific zone required)
+
// Check if we have the required item for this step
if (currentStep.RequiredItemId > 0)
{
@@ -171,16 +199,107 @@ public class MissionSolver
}
else
{
- // Need to find the item
+ // Need to find the item - check at specific location first
+ if (currentStep.TargetX > 0 || currentStep.TargetY > 0)
+ {
+ return new BotObjective
+ {
+ Type = ObjectiveType.PickupItem,
+ Description = $"Pick up item at ({currentStep.TargetX},{currentStep.TargetY})",
+ TargetX = currentStep.TargetX,
+ TargetY = currentStep.TargetY,
+ RequiredItemId = currentStep.RequiredItemId
+ };
+ }
return CreateFindItemObjective(currentStep.RequiredItemId);
}
}
- // No required item - explore
+ // No specific required item - go to target location if specified
+ if (currentStep.TargetX > 0 || currentStep.TargetY > 0)
+ {
+ // Check for items at target location
+ var itemAtTarget = FindAnyItemInCurrentZone();
+ if (itemAtTarget != null)
+ {
+ return new BotObjective
+ {
+ Type = ObjectiveType.PickupItem,
+ Description = $"Pick up item at ({itemAtTarget.X},{itemAtTarget.Y})",
+ TargetX = itemAtTarget.X,
+ TargetY = itemAtTarget.Y
+ };
+ }
+ }
+
+ // Priority 1: If we have ANY items, try to use them on friendly NPCs
+ if (_state.Inventory.Count > 0)
+ {
+ var friendlyNpc = FindNearestFriendlyNpc();
+ if (friendlyNpc != null)
+ {
+ var itemToUse = _state.Inventory.FirstOrDefault();
+ if (itemToUse > 0)
+ {
+ return new BotObjective
+ {
+ Type = ObjectiveType.UseItemOnNpc,
+ Description = $"Try using item on NPC",
+ TargetNpc = friendlyNpc,
+ TargetX = friendlyNpc.X,
+ TargetY = friendlyNpc.Y,
+ RequiredItemId = itemToUse
+ };
+ }
+ }
+ }
+
+ // Priority 2: Talk to any friendly NPC (might get items or advance quest)
+ var talkableNpc = FindNearestFriendlyNpc();
+ if (talkableNpc != null)
+ {
+ return new BotObjective
+ {
+ Type = ObjectiveType.TalkToNpc,
+ Description = "Talk to NPC",
+ TargetNpc = talkableNpc,
+ TargetX = talkableNpc.X,
+ TargetY = talkableNpc.Y
+ };
+ }
+
+ // Priority 3: Pick up any items in the zone
+ var anyItem = FindAnyItemInCurrentZone();
+ if (anyItem != null)
+ {
+ return new BotObjective
+ {
+ Type = ObjectiveType.PickupItem,
+ Description = $"Pick up item at ({anyItem.X},{anyItem.Y})",
+ TargetX = anyItem.X,
+ TargetY = anyItem.Y
+ };
+ }
+
+ // Priority 4: Kill any enemies blocking progress
+ var enemy = FindNearestEnemy();
+ if (enemy != null)
+ {
+ return new BotObjective
+ {
+ Type = ObjectiveType.KillEnemy,
+ Description = "Defeat enemy",
+ TargetNpc = enemy,
+ TargetX = enemy.X,
+ TargetY = enemy.Y
+ };
+ }
+
+ // Priority 5: Explore to find more stuff
return new BotObjective
{
Type = ObjectiveType.Explore,
- Description = "Explore to find next objective"
+ Description = "Explore to find objectives"
};
}
@@ -297,11 +416,13 @@ public class MissionSolver
///
/// Finds any collectable item in the current zone.
+ /// Checks both zone objects (crates) and items placed in the tile grid.
///
private ZoneObject? FindAnyItemInCurrentZone()
{
if (_state.CurrentZone == null) return null;
+ // First check zone objects (crates, locators)
foreach (var obj in _state.CurrentZone.Objects)
{
if ((obj.Type == ZoneObjectType.CrateItem ||
@@ -312,6 +433,39 @@ public class MissionSolver
return obj;
}
}
+
+ // Also scan the tile grid for items (e.g., mushrooms on rocks)
+ var zone = _state.CurrentZone;
+ if (zone.TileGrid != null)
+ {
+ for (int y = 0; y < zone.Height; y++)
+ {
+ for (int x = 0; x < zone.Width; x++)
+ {
+ // Check all layers for item tiles
+ for (int layer = 0; layer < 3; layer++)
+ {
+ var tileId = zone.TileGrid[y, x, layer];
+ if (tileId != 0xFFFF && tileId < _gameData.Tiles.Count)
+ {
+ var tile = _gameData.Tiles[tileId];
+ if (tile.IsItem && !_state.IsObjectCollected(_state.CurrentZoneId, x, y))
+ {
+ // Found an item tile - return as a synthetic object
+ return new ZoneObject
+ {
+ Type = ZoneObjectType.LocatorItem,
+ X = x,
+ Y = y,
+ Argument = tileId
+ };
+ }
+ }
+ }
+ }
+ }
+ }
+
return null;
}
diff --git a/src/YodaStoriesNG.Engine/Game/GameEngine.cs b/src/YodaStoriesNG.Engine/Game/GameEngine.cs
index a4d5042..65c7a20 100644
--- a/src/YodaStoriesNG.Engine/Game/GameEngine.cs
+++ b/src/YodaStoriesNG.Engine/Game/GameEngine.cs
@@ -232,12 +232,12 @@ public unsafe class GameEngine : IDisposable
switch (type)
{
case BotActionType.Move:
- // Calculate dx/dy from current position to target
- int dx = Math.Sign(x - _state.PlayerX);
- int dy = Math.Sign(y - _state.PlayerY);
- if (dx == 0 && dy == 0)
+ int dx, dy;
+ // When x=0 and y=0, use direction for movement (bot convention for directional moves)
+ if (x == 0 && y == 0)
{
- // Use direction to determine movement
+ dx = 0;
+ dy = 0;
switch (dir)
{
case Direction.Up: dy = -1; break;
@@ -246,6 +246,12 @@ public unsafe class GameEngine : IDisposable
case Direction.Right: dx = 1; break;
}
}
+ else
+ {
+ // Calculate dx/dy from current position to target
+ dx = Math.Sign(x - _state.PlayerX);
+ dy = Math.Sign(y - _state.PlayerY);
+ }
TryMovePlayer(dx, dy, dir, false);
break;
diff --git a/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs b/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs
index 3272e7c..d6a2dcd 100644
--- a/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs
+++ b/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs
@@ -281,7 +281,7 @@ public class WorldGenerator
///
public WorldMap GenerateWorld()
{
- // Pick a random mission (goal puzzle)
+ // Pick a random mission (goal puzzle) - but don't build puzzle chain yet
CurrentMission = SelectRandomMission();
Console.WriteLine($"Selected mission: {CurrentMission.Name} on {CurrentMission.Planet}");
@@ -298,12 +298,41 @@ public class WorldGenerator
// Generate the main planet grid
GeneratePlanetGrid();
+ // NOW build the puzzle chain - after grid is generated so we know which zones exist
+ if (CurrentMission.GoalPuzzle != null)
+ {
+ BuildPuzzleChain(CurrentMission, CurrentMission.GoalPuzzle);
+ LogMissionDetails(CurrentMission);
+ }
+
// Set up item chain
SetupItemChain();
return CurrentWorld;
}
+ ///
+ /// Logs mission details to console.
+ ///
+ private void LogMissionDetails(Mission mission)
+ {
+ Console.WriteLine($"\n=== MISSION: {mission.Name} ===");
+ Console.WriteLine($"Planet: {mission.Planet}");
+ Console.WriteLine($"Goal: {mission.Description}");
+ Console.WriteLine($"Puzzle chain ({mission.PuzzleChain.Count} steps):");
+ for (int i = 0; i < mission.PuzzleChain.Count; i++)
+ {
+ var step = mission.PuzzleChain[i];
+ var reqItem = GetItemName(step.RequiredItemId);
+ var rewItem = GetItemName(step.RewardItemId);
+ var zoneInfo = step.ZoneId.HasValue ? $" [Zone {step.ZoneId}]" : "";
+ Console.WriteLine($" {i + 1}. [{step.Puzzle.Type}] Need: {reqItem} -> Get: {rewItem}{zoneInfo}");
+ if (!string.IsNullOrEmpty(step.Hint))
+ Console.WriteLine($" Hint: {step.Hint}");
+ }
+ Console.WriteLine();
+ }
+
///
/// Selects a random mission from available goal puzzles and builds the puzzle chain.
///
@@ -335,24 +364,7 @@ public class WorldGenerator
GoalPuzzle = goalPuzzle
};
- // Build the puzzle chain leading to the goal
- BuildPuzzleChain(mission, goalPuzzle);
-
- // Log mission details
- Console.WriteLine($"\n=== MISSION: {mission.Name} ===");
- Console.WriteLine($"Planet: {mission.Planet}");
- Console.WriteLine($"Goal: {mission.Description}");
- Console.WriteLine($"Puzzle chain ({mission.PuzzleChain.Count} steps):");
- for (int i = 0; i < mission.PuzzleChain.Count; i++)
- {
- var step = mission.PuzzleChain[i];
- var reqItem = GetItemName(step.RequiredItemId);
- var rewItem = GetItemName(step.RewardItemId);
- Console.WriteLine($" {i + 1}. [{step.Puzzle.Type}] Need: {reqItem} -> Get: {rewItem}");
- if (!string.IsNullOrEmpty(step.Hint))
- Console.WriteLine($" Hint: {step.Hint}");
- }
- Console.WriteLine();
+ // NOTE: Puzzle chain will be built later in GenerateWorld() after grid is generated
return mission;
}
@@ -407,80 +419,136 @@ public class WorldGenerator
}
///
- /// Builds the puzzle chain for a mission based on the goal puzzle.
+ /// Builds the puzzle chain for a mission based on zone objects.
+ /// Since puzzle data parsing is unreliable, we scan zones for items and NPCs.
+ /// Each step includes the specific zone where the item/NPC can be found.
+ /// Only uses zones that are in the generated world grid.
///
private void BuildPuzzleChain(Mission mission, Puzzle goalPuzzle)
{
- // Get all non-goal puzzles that can be used in the chain
- var tradePuzzles = _gameData.Puzzles.Where(p => p.Type == PuzzleType.Trade).ToList();
- var usePuzzles = _gameData.Puzzles.Where(p => p.Type == PuzzleType.Use).ToList();
- var questPuzzles = _gameData.Puzzles.Where(p => p.Type == PuzzleType.Quest).ToList();
-
- // The goal puzzle defines what item is needed to complete the mission
- var goalItemId = goalPuzzle.Item1;
-
- // Work backwards from the goal to build the chain
- var currentNeededItem = goalItemId;
- var usedPuzzles = new HashSet();
var chainSteps = new List();
- // Add the final goal step
- chainSteps.Add(new PuzzleStep
+ // Get the set of zone IDs that are actually in our generated world
+ var worldZoneIds = new HashSet();
+ if (CurrentWorld?.Grid != null)
{
- Puzzle = goalPuzzle,
- RequiredItemId = goalItemId,
- RewardItemId = 0, // Mission complete!
- Hint = goalPuzzle.Strings.Count > 1 ? goalPuzzle.Strings[1] : "Complete the mission."
- });
-
- // Try to find puzzles that give us what we need (working backwards)
- int maxSteps = 5; // Limit chain length
- for (int i = 0; i < maxSteps && currentNeededItem > 0; i++)
- {
- // Look for a puzzle that rewards the item we need
- Puzzle? sourcePuzzle = null;
-
- // First try trade puzzles (item2 is reward)
- sourcePuzzle = tradePuzzles
- .Where(p => p.Item2 == currentNeededItem && !usedPuzzles.Contains(p.Id))
- .OrderBy(_ => _random.Next())
- .FirstOrDefault();
-
- // Then try use puzzles
- if (sourcePuzzle == null)
+ for (int y = 0; y < GridSize; y++)
{
- sourcePuzzle = usePuzzles
- .Where(p => p.Item2 == currentNeededItem && !usedPuzzles.Contains(p.Id))
- .OrderBy(_ => _random.Next())
- .FirstOrDefault();
- }
-
- // Then try quest puzzles
- if (sourcePuzzle == null)
- {
- sourcePuzzle = questPuzzles
- .Where(p => p.Item2 == currentNeededItem && !usedPuzzles.Contains(p.Id))
- .OrderBy(_ => _random.Next())
- .FirstOrDefault();
- }
-
- if (sourcePuzzle != null)
- {
- usedPuzzles.Add(sourcePuzzle.Id);
- chainSteps.Insert(0, new PuzzleStep
+ for (int x = 0; x < GridSize; x++)
{
- Puzzle = sourcePuzzle,
- RequiredItemId = sourcePuzzle.Item1,
- RewardItemId = sourcePuzzle.Item2,
- Hint = sourcePuzzle.Strings.FirstOrDefault() ?? ""
- });
- currentNeededItem = sourcePuzzle.Item1;
+ if (CurrentWorld.Grid[y, x].HasValue)
+ worldZoneIds.Add(CurrentWorld.Grid[y, x]!.Value);
+ }
}
- else
+ }
+
+ // Find item-bearing objects ONLY in zones that are in the world grid
+ var planetZones = _gameData.Zones
+ .Where(z => z.Planet == mission.Planet && z.Width > 0 && worldZoneIds.Contains(z.Id))
+ .ToList();
+
+ Console.WriteLine($"BuildPuzzleChain: {worldZoneIds.Count} zones in world, {planetZones.Count} planet zones to scan");
+
+ // Collect items with their zone locations
+ var itemLocations = new List<(int ItemId, int ZoneId, int X, int Y)>();
+ var npcZones = new List<(int ZoneId, int CharacterId, int X, int Y)>();
+
+ foreach (var zone in planetZones)
+ {
+ foreach (var obj in zone.Objects)
{
- // No more puzzles in the chain, this is where Yoda gives the starting item
- break;
+ if (obj.Type == ZoneObjectType.CrateItem || obj.Type == ZoneObjectType.LocatorItem)
+ {
+ if (obj.Argument > 0 && obj.Argument < _gameData.Tiles.Count)
+ {
+ itemLocations.Add((obj.Argument, zone.Id, obj.X, obj.Y));
+ }
+ }
+ else if (obj.Type == ZoneObjectType.PuzzleNPC)
+ {
+ npcZones.Add((zone.Id, obj.Argument, obj.X, obj.Y));
+ }
}
+
+ // Also check IZAX entity data for NPCs with items
+ if (zone.AuxData?.Entities != null)
+ {
+ foreach (var entity in zone.AuxData.Entities)
+ {
+ if (entity.ItemTileId > 0 && entity.ItemTileId != 0xFFFF)
+ {
+ itemLocations.Add((entity.ItemTileId, zone.Id, entity.X, entity.Y));
+ }
+ if (entity.CharacterId > 0 && entity.CharacterId != 0xFFFF)
+ {
+ npcZones.Add((zone.Id, entity.CharacterId, entity.X, entity.Y));
+ }
+ }
+ }
+ }
+
+ // Shuffle and take a subset
+ itemLocations = itemLocations.OrderBy(_ => _random.Next()).Take(5).ToList();
+ npcZones = npcZones.OrderBy(_ => _random.Next()).ToList();
+
+ // Build a puzzle chain with specific zone locations
+ if (itemLocations.Count >= 2)
+ {
+ // Step 1: Find first item at a specific location
+ var firstItem = itemLocations[0];
+ chainSteps.Add(new PuzzleStep
+ {
+ Puzzle = new Puzzle { Type = PuzzleType.Quest, Strings = { "Find an item" } },
+ RequiredItemId = 0, // No item required - just find it
+ RewardItemId = firstItem.ItemId,
+ ZoneId = firstItem.ZoneId,
+ TargetX = firstItem.X,
+ TargetY = firstItem.Y,
+ Hint = $"Search zone {firstItem.ZoneId} for useful items."
+ });
+
+ // Steps 2-N: Trade items with NPCs at specific zones
+ for (int i = 0; i < itemLocations.Count - 1; i++)
+ {
+ var npcZone = npcZones.Count > i ? npcZones[i] : (ZoneId: itemLocations[i + 1].ZoneId, CharacterId: 0, X: 0, Y: 0);
+ var nextItem = itemLocations[i + 1];
+
+ chainSteps.Add(new PuzzleStep
+ {
+ Puzzle = new Puzzle { Type = PuzzleType.Trade, Strings = { "Trade with someone" } },
+ RequiredItemId = itemLocations[i].ItemId,
+ RewardItemId = nextItem.ItemId,
+ ZoneId = npcZone.ZoneId,
+ TargetX = npcZone.X,
+ TargetY = npcZone.Y,
+ Hint = $"Find someone in zone {npcZone.ZoneId} who needs this item."
+ });
+ }
+
+ // Final step: Complete goal
+ var goalZone = npcZones.Count > 0 ? npcZones[^1] : (ZoneId: itemLocations[^1].ZoneId, CharacterId: 0, X: 0, Y: 0);
+ chainSteps.Add(new PuzzleStep
+ {
+ Puzzle = goalPuzzle,
+ RequiredItemId = itemLocations[^1].ItemId,
+ RewardItemId = 0, // Mission complete!
+ ZoneId = goalZone.ZoneId,
+ TargetX = goalZone.X,
+ TargetY = goalZone.Y,
+ Hint = goalPuzzle.Strings.Count > 1 ? goalPuzzle.Strings[1] : "Complete your mission."
+ });
+ }
+ else
+ {
+ // Fallback: simple exploration mission
+ chainSteps.Add(new PuzzleStep
+ {
+ Puzzle = goalPuzzle,
+ RequiredItemId = 0,
+ RewardItemId = 0,
+ ZoneId = CurrentWorld?.LandingZoneId,
+ Hint = "Explore the planet and find what you need."
+ });
}
mission.PuzzleChain = chainSteps;
@@ -1023,6 +1091,8 @@ public class PuzzleStep
public int RequiredItemId { get; set; } // Item needed to complete this step
public int RewardItemId { get; set; } // Item received upon completion
public int? ZoneId { get; set; } // Zone where this puzzle is solved
+ public int TargetX { get; set; } // X position in the zone
+ public int TargetY { get; set; } // Y position in the zone
public string Hint { get; set; } = ""; // Hint text for the player
public bool IsCompleted { get; set; } = false;
}
diff --git a/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs b/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs
index b6d6d9e..5084965 100644
--- a/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs
+++ b/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs
@@ -696,28 +696,42 @@ public class DtaParser
private void ParsePuzzlesSection(uint length)
{
- var endPos = _reader.BaseStream.Position + length;
+ var startPos = _reader.BaseStream.Position;
+ var endPos = startPos + length;
int puzzleId = 0;
- while (_reader.BaseStream.Position < endPos - 4)
+ // First, read the puzzle count (2 bytes)
+ var puzzleCount = _reader.ReadUInt16();
+ Console.WriteLine($"PUZ2 section: expecting {puzzleCount} puzzles");
+
+ while (_reader.BaseStream.Position < endPos - 4 && puzzleId < puzzleCount)
{
+ // Look for IPUZ marker (similar to IZON in zones)
+ var markerPos = _reader.BaseStream.Position;
+ var markerBytes = _reader.ReadBytes(4);
+ var marker = System.Text.Encoding.ASCII.GetString(markerBytes);
+
+ if (marker != "IPUZ")
+ {
+ // Not at IPUZ marker, try to find it
+ _reader.BaseStream.Seek(markerPos + 1, SeekOrigin.Begin);
+ continue;
+ }
+
+ // Found IPUZ marker - read puzzle size
+ var puzzleSize = _reader.ReadUInt32();
+ var puzzleDataStart = _reader.BaseStream.Position;
+
var puzzle = new Puzzle { Id = puzzleId++ };
- // Read puzzle header (variable format, read cautiously)
- var marker = _reader.ReadUInt32();
- if (marker == 0xFFFFFFFF || _reader.BaseStream.Position >= endPos)
- break;
-
- _reader.BaseStream.Seek(-4, SeekOrigin.Current);
-
- // PUZ2 format:
+ // IPUZ format (after size):
// - 2 bytes: puzzle type (0=Quest, 1=Transport, 2=Trade, 3=Use, 4=Goal)
- // - 2 bytes: item1 (required item or NPC)
- // - 2 bytes: item2 (reward item or destination)
- // - 2 bytes: unknown (possibly flags or zone reference)
- // - 2 bytes: unknown (possibly planet or difficulty)
+ // - 2 bytes: item1 (item to bring/find)
+ // - 2 bytes: item2 (reward item or NPC tile)
+ // - 2 bytes: unknown flags
+ // - 2 bytes: unknown (possibly planet)
// - 5 strings (length-prefixed)
- var puzzleType = _reader.ReadInt16();
+ var puzzleType = _reader.ReadUInt16();
puzzle.Type = (PuzzleType)puzzleType;
puzzle.Item1 = _reader.ReadUInt16();
puzzle.Item2 = _reader.ReadUInt16();
@@ -725,25 +739,33 @@ public class DtaParser
var unknown3 = _reader.ReadUInt16();
// Read puzzle strings (5 strings typically)
- // String 0: Puzzle name/description
- // String 1: Hint or goal text
- // String 2-4: Additional dialogue/text
+ // String 0: Puzzle name/description (e.g., "Give MUSHROOM to YODA")
+ // String 1: Hint text
+ // String 2-4: Additional dialogue
for (int i = 0; i < 5; i++)
{
+ if (_reader.BaseStream.Position >= puzzleDataStart + puzzleSize)
+ break;
+
var strLen = _reader.ReadUInt16();
- if (strLen > 0 && strLen < 1000) // Sanity check
+ if (strLen > 0 && strLen < 1000)
{
var strBytes = _reader.ReadBytes(strLen);
puzzle.Strings.Add(System.Text.Encoding.ASCII.GetString(strBytes).TrimEnd('\0'));
}
- else if (strLen >= 1000)
+ else if (strLen == 0)
{
- // Invalid length, likely parsing error
- _reader.BaseStream.Seek(-2, SeekOrigin.Current);
+ puzzle.Strings.Add("");
+ }
+ else
+ {
+ // Invalid length, skip rest
break;
}
}
+ // Skip to end of this puzzle's data
+ _reader.BaseStream.Seek(puzzleDataStart + puzzleSize, SeekOrigin.Begin);
_data.Puzzles.Add(puzzle);
}