2026-11-05 16:20:07 +00:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
namespace TKS
|
|
|
|
|
|
{
|
|
|
|
|
|
public enum GimmickKind
|
|
|
|
|
|
{
|
|
|
|
|
|
Block = 0, // 장애물: 이동·(설정에 따라) 투사체를 막는 콜라이더 + 내구도
|
|
|
|
|
|
Zone = 1 // 지대: 겹치면 효과(경로 스윕 판정)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public enum GimmickProjectile
|
|
|
|
|
|
{
|
|
|
|
|
|
Pass = 0, // 투사체 통과(목책 · 강)
|
|
|
|
|
|
Block = 1, // 투사체 막힘(덤불 · 바위 · 성문 · 성벽)
|
|
|
|
|
|
Destroy = 2 // 맞으면 파괴(폭약 수레)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public enum GimmickZone
|
|
|
|
|
|
{
|
|
|
|
|
|
None = 0,
|
|
|
|
|
|
Poison = 1, // 독늪
|
|
|
|
|
|
Slow = 2, // 늪지
|
|
|
|
|
|
Burn = 3, // 불타는 잔해
|
|
|
|
|
|
Slide = 4, // 빙판
|
|
|
|
|
|
High = 5, // 고지
|
|
|
|
|
|
Fog = 6, // 안개
|
|
|
|
|
|
Capture = 7, // 군기
|
|
|
|
|
|
Trap = 8, // 함정
|
|
|
|
|
|
River = 9 // 강(장애물 레코드의 효과)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public enum GimmickShapeType
|
|
|
|
|
|
{
|
|
|
|
|
|
Circle = 0,
|
|
|
|
|
|
Rect = 1,
|
|
|
|
|
|
Poly = 2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>기믹 정의 1행(gimmicks.csv · SOT = 기획 tables/ · C45 — 코드에는 열 이름과 뜻만).</summary>
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
|
public class GimmickDef
|
|
|
|
|
|
{
|
|
|
|
|
|
public string id;
|
|
|
|
|
|
public string nameKo;
|
|
|
|
|
|
public GimmickKind kind;
|
|
|
|
|
|
public bool durable;
|
|
|
|
|
|
public int hp;
|
|
|
|
|
|
public int dmgInf;
|
|
|
|
|
|
public int dmgSpear;
|
|
|
|
|
|
public int dmgCav;
|
|
|
|
|
|
public float spearPush;
|
|
|
|
|
|
public float cavSelfPct;
|
|
|
|
|
|
public float bouncePct;
|
|
|
|
|
|
public GimmickProjectile projectile;
|
|
|
|
|
|
public bool strategistClear;
|
|
|
|
|
|
public GimmickZone zone;
|
|
|
|
|
|
[Tooltip("빈 칸 = NaN (desc_ko 끝 [ ] 메모가 뜻)")]
|
|
|
|
|
|
public float p1 = float.NaN;
|
|
|
|
|
|
public float p2 = float.NaN;
|
|
|
|
|
|
public float p3 = float.NaN;
|
|
|
|
|
|
public string art = "";
|
|
|
|
|
|
public string artBroken = "";
|
|
|
|
|
|
public string descKo = "";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>병종이 부딪혀 주는 내구 피해(궁·책 = 0 · 내구 없는 기믹 = 0).</summary>
|
|
|
|
|
|
public int DamageBy(UnitType t)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!durable) return 0;
|
|
|
|
|
|
switch (t)
|
|
|
|
|
|
{
|
|
|
|
|
|
case UnitType.Infantry: return dmgInf;
|
|
|
|
|
|
case UnitType.Spear: return dmgSpear;
|
|
|
|
|
|
case UnitType.Cavalry: return dmgCav;
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>스테이지 배치 1행(stage_gimmicks.csv). 좌표 = 전장 월드(가운데 0,0 · 아군 아래).</summary>
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
|
public class GimmickPlacement
|
|
|
|
|
|
{
|
|
|
|
|
|
public string stage = "";
|
|
|
|
|
|
public int chapter;
|
|
|
|
|
|
public int stageNo;
|
|
|
|
|
|
public string gimmickId = "";
|
|
|
|
|
|
public GimmickShapeType shape;
|
|
|
|
|
|
public float cx;
|
|
|
|
|
|
public float cy;
|
|
|
|
|
|
[Tooltip("circle = 반지름 · rect = 가로")]
|
|
|
|
|
|
public float wOrR;
|
|
|
|
|
|
public float h;
|
|
|
|
|
|
[Tooltip("rect 회전(도 · 반시계)")]
|
|
|
|
|
|
public float rot;
|
|
|
|
|
|
[Tooltip("poly 꼭짓점(월드) · x:y;x:y;…")]
|
|
|
|
|
|
public Vector2[] points = new Vector2[0];
|
|
|
|
|
|
[Tooltip("-1 = 정의 hp")]
|
|
|
|
|
|
public int hpOverride = -1;
|
|
|
|
|
|
public string paramsRaw = "";
|
|
|
|
|
|
public float pOver1 = float.NaN;
|
|
|
|
|
|
public float pOver2 = float.NaN;
|
|
|
|
|
|
public float pOver3 = float.NaN;
|
|
|
|
|
|
public int line;
|
|
|
|
|
|
|
|
|
|
|
|
public GimmickPlacement Clone()
|
|
|
|
|
|
{
|
|
|
|
|
|
var c = (GimmickPlacement)MemberwiseClone();
|
|
|
|
|
|
c.points = points != null ? (Vector2[])points.Clone() : new Vector2[0];
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 기믹 표(Assets/Data/Gimmicks/*.csv · utf-8-sig) 해석·검증(PD #1097). 형식·참조 오류는 errors 에 모은다(하나라도 있으면 임포터가 자산을 바꾸지 않는다).
|
|
|
|
|
|
/// 기믹 id 중 동작이 특별한 것(바위 굴림 · 다리 · 불타는 잔해 생성) = GimmickIds — 수치는 모두 CSV.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static class GimmickTables
|
|
|
|
|
|
{
|
|
|
|
|
|
public const string Folder = "Data/Gimmicks"; // Assets/ 아래
|
|
|
|
|
|
/// <summary>정의 · 배치 · 스테이지 시작 팁(1장 튜토리얼 문구 · PD #1098 §3).</summary>
|
|
|
|
|
|
public static readonly string[] Files = { "gimmicks.csv", "stage_gimmicks.csv", "tutorial_tips.csv" };
|
|
|
|
|
|
public const string ResourcesFolder = "Gimmicks/";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>배치 params 에 쓸 수 있는 키(p1~p3 = 정의 p 덮어쓰기 · 별칭 = 불 burn_turns→p2 · spawn_on_splash→p3 · 빙판 slide_max→p1).</summary>
|
|
|
|
|
|
public static readonly string[] ParamKeys = { "p1", "p2", "p3", "burn_turns", "spawn_on_splash", "slide_max" };
|
|
|
|
|
|
|
|
|
|
|
|
public class Result
|
|
|
|
|
|
{
|
|
|
|
|
|
public readonly List<GimmickDef> defs = new List<GimmickDef>();
|
|
|
|
|
|
public readonly List<GimmickPlacement> rows = new List<GimmickPlacement>();
|
|
|
|
|
|
public readonly List<StageGimmickTable.Tip> tips = new List<StageGimmickTable.Tip>();
|
|
|
|
|
|
public readonly List<string> errors = new List<string>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static GimmickDb db;
|
|
|
|
|
|
static StageGimmickTable table;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>임포트한 자산(없으면 null · 전투 시작마다 다시 읽으려면 Invalidate).</summary>
|
|
|
|
|
|
public static GimmickDb Db
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (db == null) db = Resources.Load<GimmickDb>(ResourcesFolder + "GimmickDb");
|
|
|
|
|
|
return db;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static StageGimmickTable Table
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (table == null) table = Resources.Load<StageGimmickTable>(ResourcesFolder + "StageGimmickTable");
|
|
|
|
|
|
return table;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void Invalidate()
|
|
|
|
|
|
{
|
|
|
|
|
|
db = null;
|
|
|
|
|
|
table = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>texts = Files 순서 · stageExists(장, 번호) = 스테이지 표에 있는가(null = 검사 생략).</summary>
|
|
|
|
|
|
public static Result Parse(string[] texts, Func<int, int, bool> stageExists = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = new Result();
|
|
|
|
|
|
var ids = new HashSet<string>();
|
|
|
|
|
|
foreach (var row in StageTable.ReadCsv(texts.Length > 0 ? texts[0] : null, "gimmicks.csv", r.errors))
|
|
|
|
|
|
{
|
|
|
|
|
|
string at = "gimmicks.csv 줄 " + Get(row, "#");
|
|
|
|
|
|
var d = new GimmickDef
|
|
|
|
|
|
{
|
|
|
|
|
|
id = Get(row, "id"),
|
|
|
|
|
|
nameKo = Get(row, "name_ko"),
|
|
|
|
|
|
art = Get(row, "art"),
|
|
|
|
|
|
artBroken = Get(row, "art_broken"),
|
|
|
|
|
|
descKo = Get(row, "desc_ko")
|
|
|
|
|
|
};
|
|
|
|
|
|
if (d.id.Length == 0) r.errors.Add(at + ": id 비어 있음");
|
|
|
|
|
|
else if (!ids.Add(d.id)) r.errors.Add(at + ": id 중복 「" + d.id + "」");
|
|
|
|
|
|
switch (Get(row, "kind"))
|
|
|
|
|
|
{
|
|
|
|
|
|
case "block": d.kind = GimmickKind.Block; break;
|
|
|
|
|
|
case "zone": d.kind = GimmickKind.Zone; break;
|
|
|
|
|
|
default: r.errors.Add(at + ": kind 는 block|zone(「" + Get(row, "kind") + "」)"); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
d.durable = Bool(row, "durable", r.errors, at);
|
|
|
|
|
|
d.hp = Int(row, "hp", r.errors, at);
|
|
|
|
|
|
d.dmgInf = Int(row, "dmg_inf", r.errors, at);
|
|
|
|
|
|
d.dmgSpear = Int(row, "dmg_spear", r.errors, at);
|
|
|
|
|
|
d.dmgCav = Int(row, "dmg_cav", r.errors, at);
|
|
|
|
|
|
d.spearPush = Float(row, "spear_push", r.errors, at);
|
|
|
|
|
|
d.cavSelfPct = Float(row, "cav_self_pct", r.errors, at);
|
|
|
|
|
|
d.bouncePct = Float(row, "bounce_pct", r.errors, at);
|
|
|
|
|
|
switch (Get(row, "projectile"))
|
|
|
|
|
|
{
|
|
|
|
|
|
case "pass": d.projectile = GimmickProjectile.Pass; break;
|
|
|
|
|
|
case "block": d.projectile = GimmickProjectile.Block; break;
|
|
|
|
|
|
case "destroy": d.projectile = GimmickProjectile.Destroy; break;
|
|
|
|
|
|
default: r.errors.Add(at + ": projectile 는 pass|block|destroy(「" + Get(row, "projectile") + "」)"); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
d.strategistClear = Bool(row, "strategist_clear", r.errors, at);
|
|
|
|
|
|
string z = Get(row, "zone_effect");
|
|
|
|
|
|
if (!TryZone(z, out d.zone)) r.errors.Add(at + ": zone_effect 알 수 없음(「" + z + "」)");
|
|
|
|
|
|
d.p1 = OptFloat(row, "p1", r.errors, at);
|
|
|
|
|
|
d.p2 = OptFloat(row, "p2", r.errors, at);
|
|
|
|
|
|
d.p3 = OptFloat(row, "p3", r.errors, at);
|
|
|
|
|
|
if (d.durable && d.hp <= 0) r.errors.Add(at + ": durable 1 인데 hp ≤ 0");
|
|
|
|
|
|
if (d.kind == GimmickKind.Zone && d.durable) r.errors.Add(at + ": 지대(zone)는 내구도가 없다");
|
|
|
|
|
|
r.defs.Add(d);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var row in StageTable.ReadCsv(texts.Length > 1 ? texts[1] : null, "stage_gimmicks.csv", r.errors))
|
|
|
|
|
|
{
|
|
|
|
|
|
string at = "stage_gimmicks.csv 줄 " + Get(row, "#");
|
|
|
|
|
|
var p = new GimmickPlacement { stage = Get(row, "stage"), gimmickId = Get(row, "gimmick_id"), paramsRaw = Get(row, "params") };
|
|
|
|
|
|
int.TryParse(Get(row, "#"), NumberStyles.Integer, CultureInfo.InvariantCulture, out p.line);
|
|
|
|
|
|
var parts = p.stage.Split('-');
|
|
|
|
|
|
if (parts.Length != 2 || !int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out p.chapter) ||
|
|
|
|
|
|
!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out p.stageNo) || p.chapter <= 0 || p.stageNo <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
r.errors.Add(at + ": stage 는 「장-번호」(「" + p.stage + "」)");
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (stageExists != null && !stageExists(p.chapter, p.stageNo))
|
|
|
|
|
|
{
|
|
|
|
|
|
r.errors.Add(at + ": 스테이지 표에 없는 스테이지 「" + p.stage + "」");
|
|
|
|
|
|
}
|
|
|
|
|
|
GimmickDef def = null;
|
|
|
|
|
|
foreach (var d in r.defs)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (d.id == p.gimmickId) def = d;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (def == null) r.errors.Add(at + ": gimmicks.csv 에 없는 gimmick_id 「" + p.gimmickId + "」");
|
|
|
|
|
|
|
|
|
|
|
|
switch (Get(row, "shape"))
|
|
|
|
|
|
{
|
|
|
|
|
|
case "circle": p.shape = GimmickShapeType.Circle; break;
|
|
|
|
|
|
case "rect": p.shape = GimmickShapeType.Rect; break;
|
|
|
|
|
|
case "poly": p.shape = GimmickShapeType.Poly; break;
|
|
|
|
|
|
default: r.errors.Add(at + ": shape 는 circle|rect|poly(「" + Get(row, "shape") + "」)"); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (p.shape != GimmickShapeType.Poly)
|
|
|
|
|
|
{
|
|
|
|
|
|
p.cx = Float(row, "cx", r.errors, at);
|
|
|
|
|
|
p.cy = Float(row, "cy", r.errors, at);
|
|
|
|
|
|
p.wOrR = Float(row, "w_or_r", r.errors, at);
|
|
|
|
|
|
if (p.wOrR <= 0f) r.errors.Add(at + ": w_or_r ≤ 0");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (p.shape == GimmickShapeType.Rect)
|
|
|
|
|
|
{
|
|
|
|
|
|
p.h = Float(row, "h", r.errors, at);
|
|
|
|
|
|
if (p.h <= 0f) r.errors.Add(at + ": rect h ≤ 0");
|
|
|
|
|
|
}
|
|
|
|
|
|
float rot = OptFloat(row, "rot", r.errors, at);
|
|
|
|
|
|
p.rot = float.IsNaN(rot) ? 0f : rot;
|
|
|
|
|
|
if (p.shape == GimmickShapeType.Poly)
|
|
|
|
|
|
{
|
|
|
|
|
|
p.points = ParsePoints(Get(row, "points"), r.errors, at);
|
|
|
|
|
|
if (p.points.Length < 3) r.errors.Add(at + ": poly 꼭짓점 3개 이상(x:y;x:y;x:y)");
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector2 c = Vector2.zero;
|
|
|
|
|
|
foreach (var v in p.points) c += v;
|
|
|
|
|
|
c /= p.points.Length;
|
|
|
|
|
|
p.cx = c.x;
|
|
|
|
|
|
p.cy = c.y;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
string hpo = Get(row, "hp_override");
|
|
|
|
|
|
if (hpo.Length > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!int.TryParse(hpo, NumberStyles.Integer, CultureInfo.InvariantCulture, out p.hpOverride) || p.hpOverride <= 0)
|
|
|
|
|
|
r.errors.Add(at + ": hp_override 양의 정수(「" + hpo + "」)");
|
|
|
|
|
|
}
|
|
|
|
|
|
ParseParams(p, def, r.errors, at);
|
|
|
|
|
|
r.rows.Add(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (texts.Length > 2 && !string.IsNullOrEmpty(texts[2]))
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var row in StageTable.ReadCsv(texts[2], "tutorial_tips.csv", r.errors))
|
|
|
|
|
|
{
|
|
|
|
|
|
string at = "tutorial_tips.csv 줄 " + Get(row, "#");
|
|
|
|
|
|
var t = new StageGimmickTable.Tip { stage = Get(row, "stage"), text = Get(row, "tip_ko") };
|
|
|
|
|
|
var parts = t.stage.Split('-');
|
|
|
|
|
|
if (parts.Length != 2 || !int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out t.chapter) ||
|
|
|
|
|
|
!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out t.stageNo))
|
|
|
|
|
|
r.errors.Add(at + ": stage 는 「장-번호」(「" + t.stage + "」)");
|
|
|
|
|
|
else if (stageExists != null && !stageExists(t.chapter, t.stageNo)) r.errors.Add(at + ": 스테이지 표에 없는 스테이지 「" + t.stage + "」");
|
|
|
|
|
|
if (t.text.Length == 0) r.errors.Add(at + ": tip_ko 비어 있음");
|
|
|
|
|
|
r.tips.Add(t);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void ParseParams(GimmickPlacement p, GimmickDef def, List<string> errors, string at)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(p.paramsRaw)) return;
|
|
|
|
|
|
foreach (var kv in p.paramsRaw.Split(';'))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (kv.Trim().Length == 0) continue;
|
|
|
|
|
|
var f = kv.Split('=');
|
|
|
|
|
|
float v;
|
|
|
|
|
|
if (f.Length != 2 || !float.TryParse(f[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v))
|
|
|
|
|
|
{
|
|
|
|
|
|
errors.Add(at + ": params 형식 「키=숫자;…」(「" + kv + "」)");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
switch (f[0].Trim())
|
|
|
|
|
|
{
|
|
|
|
|
|
case "p1":
|
|
|
|
|
|
case "slide_max":
|
|
|
|
|
|
p.pOver1 = v;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "p2":
|
|
|
|
|
|
case "burn_turns":
|
|
|
|
|
|
p.pOver2 = v;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "p3":
|
|
|
|
|
|
case "spawn_on_splash":
|
|
|
|
|
|
p.pOver3 = v;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
errors.Add(at + ": params 키 「" + f[0].Trim() + "」 는 " + string.Join("·", ParamKeys) + " 중 하나");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (def != null && p.paramsRaw.Contains("spawn_on_splash") && def.zone != GimmickZone.Burn) errors.Add(at + ": spawn_on_splash 는 불(burn) 기믹만");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Vector2[] ParsePoints(string s, List<string> errors, string at)
|
|
|
|
|
|
{
|
|
|
|
|
|
var list = new List<Vector2>();
|
|
|
|
|
|
if (string.IsNullOrEmpty(s)) return list.ToArray();
|
|
|
|
|
|
foreach (var part in s.Split(';'))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (part.Trim().Length == 0) continue;
|
|
|
|
|
|
var xy = part.Split(':');
|
|
|
|
|
|
float x, y;
|
|
|
|
|
|
if (xy.Length != 2 || !float.TryParse(xy[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out x) ||
|
|
|
|
|
|
!float.TryParse(xy[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out y))
|
|
|
|
|
|
{
|
|
|
|
|
|
errors.Add(at + ": points 형식 x:y;x:y(「" + part + "」)");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
list.Add(new Vector2(x, y));
|
|
|
|
|
|
}
|
|
|
|
|
|
return list.ToArray();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static string FormatPoints(Vector2[] pts)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (pts == null || pts.Length == 0) return "";
|
|
|
|
|
|
var s = new string[pts.Length];
|
|
|
|
|
|
for (int i = 0; i < pts.Length; i++) s[i] = F(pts[i].x) + ":" + F(pts[i].y);
|
|
|
|
|
|
return string.Join(";", s);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static string F(float v)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Mathf.Approximately(v, Mathf.Round(v)) ? Mathf.RoundToInt(v).ToString(CultureInfo.InvariantCulture) : v.ToString("0.###", CultureInfo.InvariantCulture);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static bool TryZone(string s, out GimmickZone z)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (s)
|
|
|
|
|
|
{
|
|
|
|
|
|
case "": z = GimmickZone.None; return true;
|
|
|
|
|
|
case "poison": z = GimmickZone.Poison; return true;
|
|
|
|
|
|
case "slow": z = GimmickZone.Slow; return true;
|
|
|
|
|
|
case "burn": z = GimmickZone.Burn; return true;
|
|
|
|
|
|
case "slide": z = GimmickZone.Slide; return true;
|
|
|
|
|
|
case "high": z = GimmickZone.High; return true;
|
|
|
|
|
|
case "fog": z = GimmickZone.Fog; return true;
|
|
|
|
|
|
case "capture": z = GimmickZone.Capture; return true;
|
|
|
|
|
|
case "trap": z = GimmickZone.Trap; return true;
|
|
|
|
|
|
case "river": z = GimmickZone.River; return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
z = GimmickZone.None;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static string Get(Dictionary<string, string> r, string key)
|
|
|
|
|
|
{
|
|
|
|
|
|
string v;
|
|
|
|
|
|
return r.TryGetValue(key, out v) ? v : "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static int Int(Dictionary<string, string> r, string key, List<string> errors, string at)
|
|
|
|
|
|
{
|
|
|
|
|
|
int v;
|
|
|
|
|
|
if (int.TryParse(Get(r, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) return v;
|
|
|
|
|
|
errors.Add(at + ": " + key + " 정수 아님(「" + Get(r, key) + "」)");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static bool Bool(Dictionary<string, string> r, string key, List<string> errors, string at)
|
|
|
|
|
|
{
|
|
|
|
|
|
string s = Get(r, key);
|
|
|
|
|
|
if (s == "1") return true;
|
|
|
|
|
|
if (s == "0") return false;
|
|
|
|
|
|
errors.Add(at + ": " + key + " 는 0|1(「" + s + "」)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static float Float(Dictionary<string, string> r, string key, List<string> errors, string at)
|
|
|
|
|
|
{
|
|
|
|
|
|
float v;
|
|
|
|
|
|
if (float.TryParse(Get(r, key), NumberStyles.Float, CultureInfo.InvariantCulture, out v)) return v;
|
|
|
|
|
|
errors.Add(at + ": " + key + " 숫자 아님(「" + Get(r, key) + "」)");
|
|
|
|
|
|
return 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static float OptFloat(Dictionary<string, string> r, string key, List<string> errors, string at)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Get(r, key).Length == 0 ? float.NaN : Float(r, key, errors, at);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>동작이 id 로 정해지는 기믹(수치는 CSV · 새 기믹이 같은 동작을 쓰면 여기 한 줄).</summary>
|
|
|
|
|
|
public static class GimmickIds
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>보·창이 밀면 굴러가는 바위(남은 거리 × spear_push · 적에 닿으면 공 ×p1 + 밀기 p2).</summary>
|
|
|
|
|
|
public const string Boulder = "boulder";
|
|
|
|
|
|
/// <summary>강 위 통로(겹친 강 구간은 콜라이더에서 뺀다 · 다리 위에서 옆으로 밀리면 강 판정).</summary>
|
|
|
|
|
|
public const string Bridge = "bridge";
|
|
|
|
|
|
/// <summary>폭약 수레 폭발 · 책사 광역(spawn_on_splash)이 만드는 불 지대의 정의.</summary>
|
|
|
|
|
|
public const string BurningDebris = "burning_debris";
|
2026-11-10 10:00:18 +00:00
|
|
|
|
/// <summary>목책(노숙 녹각진이 세우는 장애물 · 2026-11-10 #1112).</summary>
|
|
|
|
|
|
public const string Palisade = "palisade";
|
2026-11-05 16:20:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|