mirror of
https://github.com/Nighthawk42/YodaStoriesNG.git
synced 2026-08-30 09:02:26 +00:00
Fix bot zone transitions and puzzle parsing
- Fix HandleBotAction to use direction when coords are (0,0) - Fix edge detection mismatch in MissionBot (use consistent <=2 threshold) - Update puzzle parser to look for IPUZ markers inside PUZ2 section - Add debug logging for zone connections - Improve puzzle chain building with zone filtering - Add FUTURE_FEATURES.md for WebFun-style development tools Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
3d3cb2920a
commit
1d10d368a3
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// Finds any collectable item in the current zone.
|
||||
/// Checks both zone objects (crates) and items placed in the tile grid.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ public class WorldGenerator
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs mission details to console.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a random mission from available goal puzzles and builds the puzzle chain.
|
||||
/// </summary>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<int>();
|
||||
var chainSteps = new List<PuzzleStep>();
|
||||
|
||||
// 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<int>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user