using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.UI;
namespace TKS
{
// ---------- 데이터(Resources/ChapterMap/Data/world.json · 원화 세션 chap_install.py 생성 · 좌표 = 맵 px · 왼쪽 위 원점 · y 아래로) ----------
// 맵 그림·길 = world.json(맵 id 별) · 노드 자리 = Data/<맵>.json 의 stages[].at(기획 확정판 · 없으면 slots_N) · 게임 챕터·스테이지·노드 수 = BalanceConfig.chapters·stages(스테이지 표 CSV)
[Serializable]
public class WorldKitItem
{
public string name;
public float[] pivot; // 이미지 좌표(위 → 아래) 비율
public float[] refSize; // UI 기준 px
public float[] numberCenter; // 번호 자리(이미지 좌표 비율)
}
[Serializable]
public class WorldLandmark
{
public string ko;
public float x;
public float y;
public string role;
}
[Serializable]
public class WorldChapter
{
public string id;
public int no;
public string ko;
public string years;
public string map;
public float[] roadX;
public float[] roadY;
public WorldLandmark[] landmarks;
// world.json 의 nodes·stageLink(구 임시 연결 spec/stage_plan.json) 는 읽지 않는다 — 노드 = 스테이지 표(2026-11-05 145 스테이지)
}
[Serializable]
public class WorldData
{
public float[] mapSize;
public float[] viewSize;
public float dotSpacing;
public float[] flagStars; // x0,y0,x1,y1,… (노드 기준 · y 아래로)
public float[] flagBossStars;
public float[] flagCursor;
public float[] flagBossCursor;
public float[] flagFrame;
public bool showLabels; // 장소 이름판 표시(그림 속 장소 위치와 맞춘 뒤 켬)
public WorldKitItem[] kit;
public WorldChapter[] chapters;
public const string Path = "ChapterMap/Data/world";
public const string DataRoot = "ChapterMap/Data/";
public WorldChapter FindMap(string map)
{
if (chapters != null)
foreach (var c in chapters)
if (c.map == map) return c;
return null;
}
/// Data/<map>.json 의 stages(기획 확정판 맵 · 스테이지 번호·보스·지명·노드 좌표 at). 없으면 null.
public static AnchorStage[] AnchorStages(string map)
{
var t = Resources.Load(DataRoot + map);
if (t == null) return null;
try
{
var f = JsonUtility.FromJson(t.text);
return f != null && f.stages != null && f.stages.Length > 0 ? f.stages : null;
}
catch (Exception e)
{
Debug.LogWarning("[TKS] " + DataRoot + map + " stages 읽기 실패: " + e.Message);
return null;
}
}
/// Data/<map>.json(원화 세션 anchors) 의 slots_n(맵 px · 첫 칸 = 출발). 없으면 null.
public static Vector2[] AnchorSlots(string map, int n)
{
var t = Resources.Load(DataRoot + map);
if (t == null || n <= 0) return null;
var m = Regex.Match(t.text, "\"slots_" + n + "\"\\s*:\\s*\\[((?:\\s*\\[[^\\]]*\\]\\s*,?)*)\\s*\\]");
if (!m.Success) return null;
var nums = Regex.Matches(m.Groups[1].Value, @"-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?");
if (nums.Count != n * 2) return null;
var res = new Vector2[n];
for (int i = 0; i < n; i++)
res[i] = new Vector2(float.Parse(nums[2 * i].Value, CultureInfo.InvariantCulture), float.Parse(nums[2 * i + 1].Value, CultureInfo.InvariantCulture));
return res;
}
/// 점 p 에 가장 가까운 길 위 지점의 호 길이.
public static float ArcOf(WorldChapter c, float[] acc, Vector2 p)
{
int m = acc.Length;
if (m == 1) return 0f;
float best = float.MaxValue, res = 0f;
for (int i = 1; i < m; i++)
{
var a = new Vector2(c.roadX[i - 1], c.roadY[i - 1]);
var b = new Vector2(c.roadX[i], c.roadY[i]);
var ab = b - a;
float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / Mathf.Max(ab.sqrMagnitude, 1e-6f));
float d = (a + ab * t - p).sqrMagnitude;
if (d < best)
{
best = d;
res = acc[i - 1] + (acc[i] - acc[i - 1]) * t;
}
}
return res;
}
public static WorldData Load()
{
var t = Resources.Load(Path);
if (t == null) return null;
try
{
return JsonUtility.FromJson(t.text);
}
catch (Exception e)
{
Debug.LogError("[TKS] world.json 읽기 실패: " + e.Message);
return null;
}
}
public WorldKitItem Kit(string name)
{
if (kit != null)
foreach (var k in kit)
if (k.name == name) return k;
return null;
}
/// 길(road) 을 호 길이로 n 등분한 노드 자리(맵 px). 첫 칸 = 출발 · 마지막 = 보스.
public static Vector2[] Slots(WorldChapter c, int n)
{
var acc = Arc(c);
var res = new Vector2[Mathf.Max(0, n)];
float total = acc[acc.Length - 1];
for (int i = 0; i < res.Length; i++) res[i] = At(c, acc, n <= 1 ? 0f : total * i / (n - 1));
return res;
}
public static float[] Arc(WorldChapter c)
{
int m = Mathf.Min(c.roadX.Length, c.roadY.Length);
var acc = new float[Mathf.Max(1, m)];
for (int i = 1; i < m; i++)
acc[i] = acc[i - 1] + Vector2.Distance(new Vector2(c.roadX[i - 1], c.roadY[i - 1]), new Vector2(c.roadX[i], c.roadY[i]));
return acc;
}
public static Vector2 At(WorldChapter c, float[] acc, float len)
{
int m = acc.Length;
if (m == 1) return new Vector2(c.roadX[0], c.roadY[0]);
for (int i = 1; i < m; i++)
{
if (acc[i] >= len)
{
float t = (len - acc[i - 1]) / Mathf.Max(acc[i] - acc[i - 1], 1e-4f);
return Vector2.Lerp(new Vector2(c.roadX[i - 1], c.roadY[i - 1]), new Vector2(c.roadX[i], c.roadY[i]), t);
}
}
return new Vector2(c.roadX[m - 1], c.roadY[m - 1]);
}
}
/// Data/<맵>.json 의 stages 한 칸(챕터맵 보강 세션 기획 확정판 · 기획서 §4 번호·유형·지명 + 노드 좌표).
[Serializable]
public class AnchorStage
{
public int stage;
public bool boss;
public string place;
public string title;
public float[] at; // 맵 px(왼쪽 위 원점 · y 아래로)
}
[Serializable]
public class AnchorFile
{
public AnchorStage[] stages;
}
public enum NodeState { Lock, Open, Clear }
/// 노드 자리 출처: 기획 확정판 stages[].at · 가안 slots_N · 길 위 균등(대체).
public enum NodeSource { Stages, Slots, Road }
/// 임시 진행도(실행 중 메모리 · 저장 없음). 챕터별 깬 노드 수.
public static class WorldProgress
{
static readonly Dictionary cleared = new Dictionary();
public static int Cleared(int chapter)
{
int v;
return cleared.TryGetValue(chapter, out v) ? v : 0;
}
public static void MarkCleared(int chapter, int node)
{
if (node + 1 > Cleared(chapter)) cleared[chapter] = node + 1;
}
public static void Reset()
{
cleared.Clear();
}
}
///
/// 게임 시작 화면 = 챕터 월드맵(스크롤 · 상하좌우로 끌기). 길 위 깃발 노드를 누르면 스테이지 팝업 → 출전 = 전투 시작 ·
/// 전투 결과를 탭하면 월드맵으로 돌아와 다음 노드가 열린다. 그림·길·부품 배치 = world.json · 노드 자리 = Data/<맵>.json stages[].at(없으면 slots_N → 길 위 균등) ·
/// 챕터·노드·전투 = BalanceConfig.chapters·stages(스테이지 표 · C45) · 보스 노드 = type boss.
///
public class WorldMapScreen : MonoBehaviour
{
public static WorldMapScreen Instance { get; private set; }
const string UiRoot = "ChapterMap/UI/";
const string MapRoot = "ChapterMap/Maps/";
static readonly Color Ink = new Color(0.14f, 0.09f, 0.07f);
static readonly Color Paper = new Color(1f, 0.98f, 0.93f);
BattleManager battle;
BattleHUD hud;
SlingshotInput sling;
WorldData data;
BalanceConfig cfg;
readonly List nodeStages = new List(); // 현재 챕터 노드 → stages 인덱스
GameObject root;
CanvasGroup group;
ScrollRect scroll;
RectTransform viewport;
RectTransform content;
Image mapImage;
Text bannerText;
Button prevButton;
Button nextButton;
RectTransform cursor;
Vector2 cursorBase;
readonly List spawned = new List();
readonly List