convert game playing..
This commit is contained in:
@@ -331,18 +331,19 @@ namespace BumpCombat.UI
|
||||
24,
|
||||
TextAnchor.MiddleLeft,
|
||||
Vector2.zero,
|
||||
new Vector2(220f, 110f));
|
||||
new Vector2(240f, 220f));
|
||||
RectTransform hintRect = hintText.rectTransform;
|
||||
hintRect.anchorMin = new Vector2(0f, 1f);
|
||||
hintRect.anchorMax = new Vector2(0f, 1f);
|
||||
hintRect.pivot = new Vector2(0f, 1f);
|
||||
hintRect.anchoredPosition = new Vector2(24f, -250f);
|
||||
hintRect.anchoredPosition = new Vector2(24f, -230f);
|
||||
hintText.supportRichText = true;
|
||||
hintText.resizeTextForBestFit = false;
|
||||
hintText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
hintText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
hintText.text = "<size=36><color=#FFD675><b>TAB</b></color></size>\n"
|
||||
+ "<size=24>(미션 / 업적)</size>";
|
||||
hintText.text = "<size=32><color=#FFD675><b>TAB</b></color></size> <size=20>(미션/업적)</size>\n\n"
|
||||
+ "<size=18><color=#79E5FF><b>조작 안내</b></color></size>\n"
|
||||
+ "<size=16><color=#FFFFFF>방향키 [▲▼◀▶]: 이동\n[A]: 스킬 / [D]: 가드\n[S]: 교체 / [M]: 지도</color></size>";
|
||||
hintText.color = Color.white;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using BumpCombat.Audio;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Progression;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BumpCombat.UI
|
||||
{
|
||||
public sealed class RunClearPanel : MonoBehaviour
|
||||
{
|
||||
private GameObject panel;
|
||||
private Text titleText;
|
||||
private Text statsText;
|
||||
private Text restartText;
|
||||
private bool isShown;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
CreatePanel();
|
||||
if (RunManager.Instance != null)
|
||||
{
|
||||
RunManager.Instance.OnRunCleared += HandleRunCleared;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (RunManager.Instance != null)
|
||||
{
|
||||
RunManager.Instance.OnRunCleared -= HandleRunCleared;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isShown || Keyboard.current == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Keyboard.current.rKey.wasPressedThisFrame)
|
||||
{
|
||||
RunManager.Instance?.RestartRun();
|
||||
}
|
||||
else if (Keyboard.current.escapeKey.wasPressedThisFrame)
|
||||
{
|
||||
RunManager.Instance?.RestartToTitle();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRunCleared()
|
||||
{
|
||||
isShown = true;
|
||||
panel.SetActive(true);
|
||||
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
||||
|
||||
float elapsed = RunManager.Instance != null ? RunManager.Instance.ElapsedTime : 0f;
|
||||
int minutes = Mathf.FloorToInt(elapsed / 60f);
|
||||
int seconds = Mathf.FloorToInt(elapsed % 60f);
|
||||
|
||||
ExperienceSystem exp = FindAnyObjectByType<ExperienceSystem>();
|
||||
int level = exp != null ? exp.Level : 1;
|
||||
|
||||
statsText.text = RunManager.Instance != null && RunManager.Instance.IsDefensePrototypeRun ? $"CLEAR TIME: {minutes:D2}:{seconds:D2}\nCONSTRUCTION REWARD COLLECTED\nDEFENSE TEST CLEARED" : $"CLEAR TIME: {minutes:D2}:{seconds:D2}\nFINAL LEVEL: LV {level}\nSTAGE: 3 / 3 COMPLETED";
|
||||
}
|
||||
|
||||
private void CreatePanel()
|
||||
{
|
||||
Canvas canvas = GetComponentInParent<Canvas>() ?? FindAnyObjectByType<Canvas>();
|
||||
if (canvas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
panel = new GameObject("RunClearPanel");
|
||||
panel.transform.SetParent(canvas.transform, false);
|
||||
RectTransform panelRect = panel.AddComponent<RectTransform>();
|
||||
panelRect.anchorMin = Vector2.zero;
|
||||
panelRect.anchorMax = Vector2.one;
|
||||
panelRect.offsetMin = Vector2.zero;
|
||||
panelRect.offsetMax = Vector2.zero;
|
||||
|
||||
// Background dark dim with gold tint
|
||||
Image bg = panel.AddComponent<Image>();
|
||||
bg.color = new Color(0.08f, 0.08f, 0.04f, 0.94f);
|
||||
|
||||
// Title
|
||||
GameObject titleObj = new GameObject("ClearTitle");
|
||||
titleObj.transform.SetParent(panel.transform, false);
|
||||
titleText = titleObj.AddComponent<Text>();
|
||||
titleText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
titleText.fontSize = 42;
|
||||
titleText.fontStyle = FontStyle.Bold;
|
||||
titleText.alignment = TextAnchor.MiddleCenter;
|
||||
titleText.color = new Color(1f, 0.85f, 0.25f);
|
||||
titleText.text = RunManager.Instance != null && RunManager.Instance.IsDefensePrototypeRun ? "★ DEFENSE TEST CLEARED ★" : "★ RUN CLEAR ★";
|
||||
RectTransform titleRect = titleObj.GetComponent<RectTransform>();
|
||||
titleRect.anchorMin = new Vector2(0f, 0.6f);
|
||||
titleRect.anchorMax = new Vector2(1f, 0.8f);
|
||||
titleRect.offsetMin = Vector2.zero;
|
||||
titleRect.offsetMax = Vector2.zero;
|
||||
|
||||
// Stats
|
||||
GameObject statsObj = new GameObject("ClearStats");
|
||||
statsObj.transform.SetParent(panel.transform, false);
|
||||
statsText = statsObj.AddComponent<Text>();
|
||||
statsText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
statsText.fontSize = 20;
|
||||
statsText.alignment = TextAnchor.MiddleCenter;
|
||||
statsText.color = new Color(0.9f, 0.9f, 0.95f);
|
||||
statsText.lineSpacing = 1.4f;
|
||||
RectTransform statsRect = statsObj.GetComponent<RectTransform>();
|
||||
statsRect.anchorMin = new Vector2(0.1f, 0.35f);
|
||||
statsRect.anchorMax = new Vector2(0.9f, 0.55f);
|
||||
statsRect.offsetMin = Vector2.zero;
|
||||
statsRect.offsetMax = Vector2.zero;
|
||||
|
||||
// Restart Hint
|
||||
GameObject restartObj = new GameObject("ClearRestartHint");
|
||||
restartObj.transform.SetParent(panel.transform, false);
|
||||
restartText = restartObj.AddComponent<Text>();
|
||||
restartText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
restartText.fontSize = 16;
|
||||
restartText.alignment = TextAnchor.MiddleCenter;
|
||||
restartText.color = new Color(0.7f, 0.7f, 0.75f);
|
||||
restartText.text = "[R] 키로 재시작 | [ESC] 키로 타이틀로 이동";
|
||||
RectTransform restartRect = restartObj.GetComponent<RectTransform>();
|
||||
restartRect.anchorMin = new Vector2(0f, 0.15f);
|
||||
restartRect.anchorMax = new Vector2(1f, 0.25f);
|
||||
restartRect.offsetMin = Vector2.zero;
|
||||
restartRect.offsetMax = Vector2.zero;
|
||||
|
||||
panel.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c63f44ba2673624d9e2fcdb546c8268
|
||||
@@ -53,13 +53,17 @@ namespace BumpCombat.UI
|
||||
private Button[] debugSpawnButtons = System.Array.Empty<Button>();
|
||||
private GameObject minimapPanel;
|
||||
private RectTransform minimapMapRect;
|
||||
private RectTransform minimapTerrainRoot;
|
||||
private RectTransform minimapMarkerRoot;
|
||||
private RectTransform minimapViewportRoot;
|
||||
private Image minimapPlayerMarker;
|
||||
private Image[] minimapViewportEdges = System.Array.Empty<Image>();
|
||||
private readonly List<Image> minimapEnemyMarkers = new();
|
||||
private readonly Dictionary<Vector2Int, Image> minimapTerrainCells = new();
|
||||
private EnemyController[] minimapEnemies = System.Array.Empty<EnemyController>();
|
||||
private float minimapEnemyRefreshElapsed;
|
||||
private Stage.ZoneInstance minimapTerrainZone;
|
||||
private int minimapTerrainVisibilityRevision = -1;
|
||||
private ArenaBounds arenaBounds;
|
||||
// The panel sprite has transparent source padding. At multiplier 1,
|
||||
// 6px of canvas space keeps the map inside the visible frame.
|
||||
@@ -68,22 +72,9 @@ namespace BumpCombat.UI
|
||||
private Image[] artifactSlotIcons = System.Array.Empty<Image>();
|
||||
private readonly Stack<Text> damagePopupPool = new();
|
||||
private readonly HashSet<Text> activeDamagePopups = new();
|
||||
private static readonly RunTimedEvent[] NextBossEvents =
|
||||
{
|
||||
RunTimedEvent.MidBoss,
|
||||
RunTimedEvent.FinalBoss,
|
||||
};
|
||||
private static readonly string[] NextBossLabels =
|
||||
{
|
||||
"다음 중간보스",
|
||||
"다음 보스",
|
||||
};
|
||||
private int activeDamagePopupCount;
|
||||
private int damagePopupLifecycleVersion;
|
||||
private float compactHudFadeElapsed;
|
||||
private int lastNextEventSecond = -1;
|
||||
private bool lastNextEventUsesDebugTimes;
|
||||
private bool lastNextEventIsProductionRun;
|
||||
private static readonly Color PlayerDamagePopupColor =
|
||||
new(244f / 255f, 117f / 255f, 112f / 255f, 1f);
|
||||
private static readonly Color GuardReadyColor = new Color32(255, 205, 92, 255);
|
||||
@@ -741,61 +732,25 @@ namespace BumpCombat.UI
|
||||
new Vector2(0f, -100f),
|
||||
new Vector2(420f, 28f));
|
||||
ConfigureCompactHudText(eventText, 14, 18);
|
||||
eventText.gameObject.SetActive(true);
|
||||
eventText.gameObject.SetActive(false);
|
||||
UpdateNextEventHud();
|
||||
}
|
||||
|
||||
private void UpdateNextEventHud()
|
||||
{
|
||||
if (eventText == null || runManager == null || showingTimedEventMessage)
|
||||
if (eventText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int elapsedSecond = Mathf.FloorToInt(runManager.ElapsedTime);
|
||||
if (elapsedSecond == lastNextEventSecond
|
||||
&& runManager.UseDebugEventTimes == lastNextEventUsesDebugTimes
|
||||
&& runManager.IsProductionRun == lastNextEventIsProductionRun)
|
||||
if (!showingTimedEventMessage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lastNextEventSecond = elapsedSecond;
|
||||
lastNextEventUsesDebugTimes = runManager.UseDebugEventTimes;
|
||||
lastNextEventIsProductionRun = runManager.IsProductionRun;
|
||||
float nextTime = float.PositiveInfinity;
|
||||
string nextLabel = "다음 이벤트 없음";
|
||||
for (int i = 0; i < runManager.CurrentEliteEventCount; i++)
|
||||
{
|
||||
float eliteTime = runManager.GetEliteEventTimeForCurrentRun(i);
|
||||
if (eliteTime > runManager.ElapsedTime)
|
||||
if (eventText.gameObject.activeSelf)
|
||||
{
|
||||
nextTime = eliteTime;
|
||||
nextLabel = "다음 엘리트";
|
||||
break;
|
||||
eventText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < NextBossEvents.Length; i++)
|
||||
{
|
||||
float eventTime = runManager.GetEventTimeForCurrentRun(
|
||||
NextBossEvents[i]);
|
||||
if (eventTime > runManager.ElapsedTime && eventTime < nextTime)
|
||||
{
|
||||
nextTime = eventTime;
|
||||
nextLabel = NextBossLabels[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (float.IsPositiveInfinity(nextTime))
|
||||
{
|
||||
eventText.text = nextLabel;
|
||||
return;
|
||||
}
|
||||
|
||||
int seconds = Mathf.FloorToInt(nextTime);
|
||||
eventText.text =
|
||||
$"{nextLabel} {seconds / 60:00}:{seconds % 60:00}";
|
||||
}
|
||||
|
||||
private void LayoutDebugText()
|
||||
@@ -844,6 +799,20 @@ namespace BumpCombat.UI
|
||||
{
|
||||
popupText = $"LAUNCH {popupText}";
|
||||
}
|
||||
|
||||
EnemyController popupEnemy = result.Target != null
|
||||
? result.Target.GetComponentInParent<EnemyController>()
|
||||
: null;
|
||||
Stage.ZoneInstance popupZone = Stage.StageManager.Instance != null
|
||||
? Stage.StageManager.Instance.CurrentZone
|
||||
: null;
|
||||
if (popupEnemy != null
|
||||
&& popupZone != null
|
||||
&& !popupZone.IsWorldPositionVisible(popupEnemy.GroundAnchorPosition))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color popupColor;
|
||||
if (!TryGetArtifactPopupColor(result, out popupColor))
|
||||
{
|
||||
@@ -1271,7 +1240,7 @@ namespace BumpCombat.UI
|
||||
RunTimedEvent.MidBoss,
|
||||
RunTimedEvent.FinalBoss,
|
||||
};
|
||||
string[] labels = { "엘리트 출현", "중간보스 출현", "보스 출현" };
|
||||
string[] labels = { "엘리트 출현", "중간보스 출현", "보스방 이동" };
|
||||
debugSpawnButtons = new Button[events.Length];
|
||||
for (int i = 0; i < events.Length; i++)
|
||||
{
|
||||
@@ -1296,16 +1265,30 @@ namespace BumpCombat.UI
|
||||
Button button = buttonObject.GetComponent<Button>();
|
||||
button.targetGraphic = image;
|
||||
PresentationUiStyle.ApplyButton(button);
|
||||
RunTimedEvent requestedEvent = events[i];
|
||||
button.onClick.AddListener(() =>
|
||||
if (i == 2)
|
||||
{
|
||||
if (spawnDirector != null
|
||||
&& spawnDirector.TryDebugSpawnEventEnemy(requestedEvent))
|
||||
button.onClick.AddListener(() =>
|
||||
{
|
||||
HandleTimedEvent(requestedEvent);
|
||||
}
|
||||
UpdateDebugSpawnHud();
|
||||
});
|
||||
if (Stage.StageManager.Instance != null)
|
||||
{
|
||||
Stage.StageManager.Instance.CheatTeleportToBossRoom();
|
||||
}
|
||||
UpdateDebugSpawnHud();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
RunTimedEvent requestedEvent = events[i];
|
||||
button.onClick.AddListener(() =>
|
||||
{
|
||||
if (spawnDirector != null
|
||||
&& spawnDirector.TryDebugSpawnEventEnemy(requestedEvent))
|
||||
{
|
||||
HandleTimedEvent(requestedEvent);
|
||||
}
|
||||
UpdateDebugSpawnHud();
|
||||
});
|
||||
}
|
||||
CreateRuntimeText(
|
||||
buttonObject.transform,
|
||||
"Label",
|
||||
@@ -1327,7 +1310,7 @@ namespace BumpCombat.UI
|
||||
|
||||
bool visible = runManager != null && runManager.UseDebugArtifactSelection;
|
||||
debugSpawnPanel.SetActive(visible);
|
||||
if (!visible || spawnDirector == null)
|
||||
if (!visible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1338,10 +1321,19 @@ namespace BumpCombat.UI
|
||||
RunTimedEvent.MidBoss,
|
||||
RunTimedEvent.FinalBoss,
|
||||
};
|
||||
bool inputEnabled = RunManager.GameplayInputEnabled;
|
||||
for (int i = 0; i < debugSpawnButtons.Length; i++)
|
||||
{
|
||||
debugSpawnButtons[i].interactable =
|
||||
spawnDirector.CanDebugSpawnEventEnemy(events[i]);
|
||||
if (i == 2)
|
||||
{
|
||||
debugSpawnButtons[i].interactable =
|
||||
inputEnabled && Stage.StageManager.Instance != null;
|
||||
}
|
||||
else
|
||||
{
|
||||
debugSpawnButtons[i].interactable =
|
||||
spawnDirector != null && spawnDirector.CanDebugSpawnEventEnemy(events[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1370,7 +1362,7 @@ namespace BumpCombat.UI
|
||||
new Vector2(panelWidth, panelHeight));
|
||||
Image minimapFrame = minimapPanel.GetComponent<Image>();
|
||||
PresentationUiStyle.ApplyPanel(minimapFrame);
|
||||
minimapFrame.pixelsPerUnitMultiplier = 1f;
|
||||
minimapFrame.pixelsPerUnitMultiplier = PresentationUiStyle.ThreePixelBorderMultiplier;
|
||||
minimapFrame.fillCenter = false;
|
||||
|
||||
GameObject map = CreateRuntimePanel(
|
||||
@@ -1384,6 +1376,7 @@ namespace BumpCombat.UI
|
||||
map.GetComponent<Image>().color = new Color(0.035f, 0.06f, 0.08f, 0.28f);
|
||||
map.AddComponent<RectMask2D>();
|
||||
|
||||
minimapTerrainRoot = CreateMinimapLayer(map.transform, "Minimap Terrain");
|
||||
minimapMarkerRoot = CreateMinimapLayer(map.transform, "Markers");
|
||||
minimapViewportRoot = CreateMinimapLayer(map.transform, "Minimap Camera Viewport");
|
||||
minimapMarkerRoot.SetAsLastSibling();
|
||||
@@ -1453,6 +1446,8 @@ namespace BumpCombat.UI
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateMinimapTerrain();
|
||||
|
||||
SetMinimapMarkerPosition(
|
||||
minimapPlayerMarker,
|
||||
playerHealth != null ? playerHealth.transform.position : Vector3.zero,
|
||||
@@ -1469,6 +1464,9 @@ namespace BumpCombat.UI
|
||||
}
|
||||
|
||||
int markerIndex = 0;
|
||||
Stage.ZoneInstance visibilityZone = Stage.StageManager.Instance != null
|
||||
? Stage.StageManager.Instance.CurrentZone
|
||||
: null;
|
||||
for (int i = 0; i < minimapEnemies.Length; i++)
|
||||
{
|
||||
EnemyController enemy = minimapEnemies[i];
|
||||
@@ -1476,6 +1474,16 @@ namespace BumpCombat.UI
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (worldCamera == null
|
||||
|| !arenaBounds.IsInsideCameraVisible(worldCamera, enemy.GroundAnchorPosition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (visibilityZone != null
|
||||
&& !visibilityZone.IsWorldPositionVisible(enemy.GroundAnchorPosition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Image marker = GetMinimapEnemyMarker(markerIndex++);
|
||||
marker.gameObject.SetActive(true);
|
||||
@@ -1499,6 +1507,132 @@ namespace BumpCombat.UI
|
||||
UpdateMinimapViewport(extents);
|
||||
}
|
||||
|
||||
private void UpdateMinimapTerrain()
|
||||
{
|
||||
Stage.ZoneInstance currentZone = Stage.StageManager.Instance != null
|
||||
? Stage.StageManager.Instance.CurrentZone
|
||||
: null;
|
||||
if (currentZone == minimapTerrainZone)
|
||||
{
|
||||
if (currentZone != null
|
||||
&& currentZone.VisibilityRevision != minimapTerrainVisibilityRevision)
|
||||
{
|
||||
UpdateMinimapTerrainVisibility(currentZone);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
minimapTerrainZone = currentZone;
|
||||
minimapTerrainVisibilityRevision = -1;
|
||||
minimapTerrainCells.Clear();
|
||||
if (minimapTerrainRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = minimapTerrainRoot.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(minimapTerrainRoot.GetChild(i).gameObject);
|
||||
}
|
||||
|
||||
Stage.RoomPreset preset = currentZone != null ? currentZone.Preset : null;
|
||||
if (preset == null || !currentZone.HasRuntimeGrid ||
|
||||
preset.GridWidth <= 0 || preset.GridHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 mapSize = minimapMapRect.rect.size;
|
||||
float cellWidth = mapSize.x / preset.GridWidth;
|
||||
float cellHeight = mapSize.y / preset.GridHeight;
|
||||
float left = -mapSize.x * 0.5f;
|
||||
float top = mapSize.y * 0.5f;
|
||||
|
||||
for (int y = 0; y < preset.GridHeight; y++)
|
||||
{
|
||||
for (int x = 0; x < preset.GridWidth; x++)
|
||||
{
|
||||
Stage.PresetCellType cellType = currentZone.GetRuntimeCell(x, y);
|
||||
if (cellType == Stage.PresetCellType.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Image cell = CreateRuntimeImage(
|
||||
minimapTerrainRoot,
|
||||
$"Terrain {x},{y}",
|
||||
Vector2.zero,
|
||||
new Vector2(cellWidth + 0.2f, cellHeight + 0.2f));
|
||||
cell.raycastTarget = false;
|
||||
minimapTerrainCells[new Vector2Int(x, y)] = cell;
|
||||
SetRectTransform(
|
||||
cell.rectTransform,
|
||||
new Vector2(0.5f, 0.5f),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
new Vector2(
|
||||
left + (x + 0.5f) * cellWidth,
|
||||
top - (y + 0.5f) * cellHeight),
|
||||
cell.rectTransform.sizeDelta);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateMinimapTerrainVisibility(currentZone);
|
||||
}
|
||||
|
||||
private void UpdateMinimapTerrainVisibility(Stage.ZoneInstance zone)
|
||||
{
|
||||
if (zone == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<Vector2Int, Image> entry in minimapTerrainCells)
|
||||
{
|
||||
Vector2Int cell = entry.Key;
|
||||
if (entry.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Value.color = GetMinimapTerrainColor(
|
||||
zone.GetRuntimeCell(cell.x, cell.y),
|
||||
zone.IsCellVisible(cell.x, cell.y),
|
||||
zone.IsCellExplored(cell.x, cell.y));
|
||||
}
|
||||
|
||||
minimapTerrainVisibilityRevision = zone.VisibilityRevision;
|
||||
}
|
||||
|
||||
private static Color GetMinimapTerrainColor(
|
||||
Stage.PresetCellType cellType,
|
||||
bool visible,
|
||||
bool explored)
|
||||
{
|
||||
Color baseColor = cellType switch
|
||||
{
|
||||
Stage.PresetCellType.Obstacle => new Color32(12, 18, 22, 240),
|
||||
Stage.PresetCellType.Socket => new Color32(54, 103, 112, 230),
|
||||
Stage.PresetCellType.RewardAnchor => new Color32(73, 88, 70, 205),
|
||||
_ => new Color32(41, 57, 62, 190),
|
||||
};
|
||||
|
||||
if (visible)
|
||||
{
|
||||
return baseColor;
|
||||
}
|
||||
|
||||
if (explored)
|
||||
{
|
||||
return new Color(
|
||||
baseColor.r * 0.35f,
|
||||
baseColor.g * 0.35f,
|
||||
baseColor.b * 0.35f,
|
||||
1f);
|
||||
}
|
||||
|
||||
return new Color32(2, 4, 5, 255);
|
||||
}
|
||||
|
||||
private Image GetMinimapEnemyMarker(int index)
|
||||
{
|
||||
while (minimapEnemyMarkers.Count <= index)
|
||||
@@ -1544,9 +1678,11 @@ namespace BumpCombat.UI
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 center = arenaBounds != null ? arenaBounds.ZoneCenter : Vector2.zero;
|
||||
Vector2 localPos = worldPosition - center;
|
||||
Vector2 normalized = new(
|
||||
Mathf.Clamp01((worldPosition.x + extents.x) / (extents.x * 2f)),
|
||||
Mathf.Clamp01((worldPosition.y + extents.y) / (extents.y * 2f)));
|
||||
Mathf.Clamp01((localPos.x + extents.x) / (extents.x * 2f)),
|
||||
Mathf.Clamp01((localPos.y + extents.y) / (extents.y * 2f)));
|
||||
Vector2 halfMarker = marker.rectTransform.rect.size * 0.5f;
|
||||
float halfMapWidth = mapSize.x * 0.5f;
|
||||
float halfMapHeight = mapSize.y * 0.5f;
|
||||
@@ -1575,8 +1711,9 @@ namespace BumpCombat.UI
|
||||
|
||||
Vector2 mapSize = minimapMapRect.rect.size;
|
||||
Bounds visible = arenaBounds.GetCameraVisibleBounds(worldCamera);
|
||||
Vector2 min = WorldToMinimapPosition(visible.min, extents, mapSize);
|
||||
Vector2 max = WorldToMinimapPosition(visible.max, extents, mapSize);
|
||||
Vector2 zoneCenter = arenaBounds != null ? arenaBounds.ZoneCenter : Vector2.zero;
|
||||
Vector2 min = WorldToMinimapPosition(visible.min, extents, mapSize, zoneCenter);
|
||||
Vector2 max = WorldToMinimapPosition(visible.max, extents, mapSize, zoneCenter);
|
||||
float width = Mathf.Max(2f, max.x - min.x);
|
||||
float height = Mathf.Max(2f, max.y - min.y);
|
||||
Vector2 center = (min + max) * 0.5f;
|
||||
@@ -1617,11 +1754,13 @@ namespace BumpCombat.UI
|
||||
private static Vector2 WorldToMinimapPosition(
|
||||
Vector2 worldPosition,
|
||||
Vector2 extents,
|
||||
Vector2 mapSize)
|
||||
Vector2 mapSize,
|
||||
Vector2 center = default)
|
||||
{
|
||||
Vector2 localPos = worldPosition - center;
|
||||
Vector2 normalized = new(
|
||||
Mathf.Clamp01((worldPosition.x + extents.x) / (extents.x * 2f)),
|
||||
Mathf.Clamp01((worldPosition.y + extents.y) / (extents.y * 2f)));
|
||||
Mathf.Clamp01((localPos.x + extents.x) / (extents.x * 2f)),
|
||||
Mathf.Clamp01((localPos.y + extents.y) / (extents.y * 2f)));
|
||||
return new Vector2(
|
||||
(normalized.x - 0.5f) * mapSize.x,
|
||||
(normalized.y - 0.5f) * mapSize.y);
|
||||
@@ -1705,12 +1844,10 @@ namespace BumpCombat.UI
|
||||
private IEnumerator ShowEventMessage(string message)
|
||||
{
|
||||
showingTimedEventMessage = true;
|
||||
lastNextEventSecond = -1;
|
||||
eventText.text = message;
|
||||
eventText.gameObject.SetActive(true);
|
||||
yield return new WaitForSecondsRealtime(3f);
|
||||
showingTimedEventMessage = false;
|
||||
lastNextEventSecond = -1;
|
||||
UpdateNextEventHud();
|
||||
}
|
||||
|
||||
@@ -1760,7 +1897,7 @@ namespace BumpCombat.UI
|
||||
"Presentation/Fonts/NanumGothic-Regular";
|
||||
private const string NeoFontResource = "Presentation/Fonts/neodgm";
|
||||
private const string GalmuriFontResource = "Presentation/Fonts/Galmuri9";
|
||||
private const float ThreePixelBorderMultiplier = 1f / 3f;
|
||||
internal const float ThreePixelBorderMultiplier = 1f / 3f;
|
||||
|
||||
private static readonly Color PanelFallback =
|
||||
new(24f / 255f, 27f / 255f, 39f / 255f, 0.94f);
|
||||
|
||||
@@ -456,6 +456,11 @@ namespace BumpCombat.UI
|
||||
BeginRun(RunMode.Production);
|
||||
}
|
||||
|
||||
private void BeginDefensePrototypeRun()
|
||||
{
|
||||
BeginRun(RunMode.DefensePrototype);
|
||||
}
|
||||
|
||||
private void OpenTitleModes()
|
||||
{
|
||||
SetScreen(Screen.TitleModes);
|
||||
@@ -469,6 +474,10 @@ namespace BumpCombat.UI
|
||||
ShowButton(index++, "개발용 시작", BeginDevelopmentRun);
|
||||
}
|
||||
ShowButton(index++, "기본 모드", BeginProductionRun);
|
||||
if (IsDevelopmentStartVisible(Application.isEditor, Debug.isDebugBuild))
|
||||
{
|
||||
ShowButton(index++, "방어전 테스트", BeginDefensePrototypeRun);
|
||||
}
|
||||
ShowDisabledButton(index++, "챌린지 모드(준비 중)");
|
||||
ShowDisabledButton(index++, "하드코어 모드(준비 중)");
|
||||
ShowButton(index, "뒤로", ReturnToTitleMenu);
|
||||
@@ -650,7 +659,7 @@ namespace BumpCombat.UI
|
||||
inputHelpText.alignment = TextAnchor.MiddleLeft;
|
||||
inputHelpText.color = new Color32(200, 208, 218, 255);
|
||||
CreateSettingsFocusRows(panel.transform);
|
||||
for (int i = 0; i < 5; i++)
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
Button button = CreateButton(panel.transform, $"Button {i + 1}", font, new Vector2(0f, 95f - i * 88f));
|
||||
button.gameObject.SetActive(false);
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
using System.Collections.Generic;
|
||||
using BumpCombat.Audio;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Stage;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BumpCombat.UI
|
||||
{
|
||||
public sealed class WorldMapPanel : MonoBehaviour
|
||||
{
|
||||
private GameObject panel;
|
||||
private Text titleText;
|
||||
private Text seedText;
|
||||
private RectTransform mapContainer;
|
||||
private readonly List<GameObject> spawnedElements = new();
|
||||
private bool isOpen;
|
||||
private RunManager runManager;
|
||||
|
||||
public bool IsOpen => isOpen;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
runManager = RunManager.Instance;
|
||||
CreatePanel();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Keyboard.current == null || runManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Keyboard.current.mKey.wasPressedThisFrame)
|
||||
{
|
||||
if (isOpen)
|
||||
{
|
||||
CloseMap();
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenMap();
|
||||
}
|
||||
}
|
||||
else if (isOpen && Keyboard.current.escapeKey.wasPressedThisFrame)
|
||||
{
|
||||
CloseMap();
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenMap()
|
||||
{
|
||||
if (isOpen || runManager == null || runManager.IsTitleScreen || runManager.IsGameOver || runManager.IsRunCleared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!runManager.TryOpenSelection(this))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen = true;
|
||||
panel.SetActive(true);
|
||||
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
||||
RefreshMap();
|
||||
}
|
||||
|
||||
public void CloseMap()
|
||||
{
|
||||
if (!isOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen = false;
|
||||
panel.SetActive(false);
|
||||
runManager?.SuppressPauseOpeningForCurrentFrame();
|
||||
runManager?.CloseSelection(this);
|
||||
BumpCombatAudioService.Instance?.PlayUi("ui_cancel");
|
||||
}
|
||||
|
||||
private void CreatePanel()
|
||||
{
|
||||
Canvas canvas = GetComponentInParent<Canvas>() ?? FindAnyObjectByType<Canvas>();
|
||||
if (canvas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
panel = new GameObject("WorldMapPanel");
|
||||
panel.transform.SetParent(canvas.transform, false);
|
||||
RectTransform panelRect = panel.AddComponent<RectTransform>();
|
||||
panelRect.anchorMin = Vector2.zero;
|
||||
panelRect.anchorMax = Vector2.one;
|
||||
panelRect.offsetMin = Vector2.zero;
|
||||
panelRect.offsetMax = Vector2.zero;
|
||||
|
||||
// Background dark dim
|
||||
Image bg = panel.AddComponent<Image>();
|
||||
bg.color = new Color(0.05f, 0.06f, 0.1f, 0.92f);
|
||||
|
||||
// Top Header: Title & Seed
|
||||
GameObject titleObj = new GameObject("MapTitle");
|
||||
titleObj.transform.SetParent(panel.transform, false);
|
||||
titleText = titleObj.AddComponent<Text>();
|
||||
titleText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
titleText.fontSize = 28;
|
||||
titleText.fontStyle = FontStyle.Bold;
|
||||
titleText.alignment = TextAnchor.MiddleCenter;
|
||||
titleText.color = new Color(0.9f, 0.85f, 0.6f);
|
||||
RectTransform titleRect = titleObj.GetComponent<RectTransform>();
|
||||
titleRect.anchorMin = new Vector2(0f, 1f);
|
||||
titleRect.anchorMax = new Vector2(1f, 1f);
|
||||
titleRect.pivot = new Vector2(0.5f, 1f);
|
||||
titleRect.anchoredPosition = new Vector2(0f, -25f);
|
||||
titleRect.sizeDelta = new Vector2(0f, 40f);
|
||||
|
||||
GameObject seedObj = new GameObject("MapSeed");
|
||||
seedObj.transform.SetParent(panel.transform, false);
|
||||
seedText = seedObj.AddComponent<Text>();
|
||||
seedText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
seedText.fontSize = 16;
|
||||
seedText.alignment = TextAnchor.MiddleCenter;
|
||||
seedText.color = new Color(0.7f, 0.7f, 0.8f);
|
||||
RectTransform seedRect = seedObj.GetComponent<RectTransform>();
|
||||
seedRect.anchorMin = new Vector2(0f, 1f);
|
||||
seedRect.anchorMax = new Vector2(1f, 1f);
|
||||
seedRect.pivot = new Vector2(0.5f, 1f);
|
||||
seedRect.anchoredPosition = new Vector2(0f, -65f);
|
||||
seedRect.sizeDelta = new Vector2(0f, 25f);
|
||||
|
||||
// Container for grid nodes
|
||||
GameObject containerObj = new GameObject("MapContainer");
|
||||
containerObj.transform.SetParent(panel.transform, false);
|
||||
mapContainer = containerObj.AddComponent<RectTransform>();
|
||||
mapContainer.anchorMin = new Vector2(0.1f, 0.1f);
|
||||
mapContainer.anchorMax = new Vector2(0.9f, 0.85f);
|
||||
mapContainer.offsetMin = Vector2.zero;
|
||||
mapContainer.offsetMax = Vector2.zero;
|
||||
|
||||
// Bottom guide
|
||||
GameObject guideObj = new GameObject("MapGuide");
|
||||
guideObj.transform.SetParent(panel.transform, false);
|
||||
Text guideText = guideObj.AddComponent<Text>();
|
||||
guideText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
guideText.fontSize = 16;
|
||||
guideText.fontStyle = FontStyle.Bold;
|
||||
guideText.alignment = TextAnchor.MiddleCenter;
|
||||
guideText.color = new Color(1f, 0.85f, 0.35f, 1f); // Prominent yellow
|
||||
guideText.text = "※ 지도는 확인용입니다. 창을 닫고 캐릭터를 움직여 방의 [출구 ▶]로 들어가면 다음 구역으로 진입합니다.\n[M] 또는 [ESC] 키를 눌러 지도를 닫으세요.";
|
||||
RectTransform guideRect = guideObj.GetComponent<RectTransform>();
|
||||
guideRect.anchorMin = new Vector2(0f, 0f);
|
||||
guideRect.anchorMax = new Vector2(1f, 0f);
|
||||
guideRect.pivot = new Vector2(0.5f, 0f);
|
||||
guideRect.anchoredPosition = new Vector2(0f, 15f);
|
||||
guideRect.sizeDelta = new Vector2(0f, 45f);
|
||||
|
||||
panel.SetActive(false);
|
||||
}
|
||||
|
||||
private void RefreshMap()
|
||||
{
|
||||
// Clear previous elements
|
||||
for (int i = 0; i < spawnedElements.Count; i++)
|
||||
{
|
||||
if (spawnedElements[i] != null)
|
||||
{
|
||||
Destroy(spawnedElements[i]);
|
||||
}
|
||||
}
|
||||
spawnedElements.Clear();
|
||||
|
||||
StageManager stageManager = StageManager.Instance;
|
||||
if (stageManager == null || stageManager.CurrentPlan == null)
|
||||
{
|
||||
titleText.text = "STAGE WORLD MAP";
|
||||
seedText.text = "NO ACTIVE STAGE";
|
||||
return;
|
||||
}
|
||||
|
||||
StagePlan plan = stageManager.CurrentPlan;
|
||||
titleText.text = $"STAGE {plan.StageIndex} — {GetStageSubtitle(plan.StageIndex)}";
|
||||
seedText.text = $"SEED: {stageManager.CurrentSeed}";
|
||||
|
||||
RectInt bounds = plan.GetGridBounds();
|
||||
float cellWidth = 140f;
|
||||
float cellHeight = 90f;
|
||||
Vector2 origin = new Vector2(
|
||||
-(bounds.x + bounds.width * 0.5f - 0.5f) * cellWidth,
|
||||
-(bounds.y + bounds.height * 0.5f - 0.5f) * cellHeight);
|
||||
|
||||
// Render Connection Lines first
|
||||
foreach (StageNode node in plan.Nodes)
|
||||
{
|
||||
Vector2 nodePos = origin + new Vector2(node.GridPosition.x * cellWidth, node.GridPosition.y * cellHeight);
|
||||
for (int d = 0; d < 4; d++)
|
||||
{
|
||||
StageDirection dir = (StageDirection)d;
|
||||
int targetId = node.GetConnection(dir);
|
||||
if (targetId > node.Id) // Draw each line once
|
||||
{
|
||||
StageNode targetNode = plan.GetNode(targetId);
|
||||
if (targetNode != null)
|
||||
{
|
||||
Vector2 targetPos = origin + new Vector2(targetNode.GridPosition.x * cellWidth, targetNode.GridPosition.y * cellHeight);
|
||||
CreateConnectionLine(nodePos, targetPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render Nodes
|
||||
foreach (StageNode node in plan.Nodes)
|
||||
{
|
||||
Vector2 nodePos = origin + new Vector2(node.GridPosition.x * cellWidth, node.GridPosition.y * cellHeight);
|
||||
CreateNodeCard(node, nodePos);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetStageSubtitle(int stageIndex)
|
||||
{
|
||||
return stageIndex switch
|
||||
{
|
||||
1 => "ELITE STAGE",
|
||||
2 => "MID BOSS STAGE",
|
||||
3 => "FINAL BOSS STAGE",
|
||||
_ => "EXPLORATION",
|
||||
};
|
||||
}
|
||||
|
||||
private void CreateConnectionLine(Vector2 from, Vector2 to)
|
||||
{
|
||||
GameObject lineObj = new GameObject("MapLine");
|
||||
lineObj.transform.SetParent(mapContainer, false);
|
||||
Image lineImg = lineObj.AddComponent<Image>();
|
||||
lineImg.color = new Color(0.3f, 0.4f, 0.55f, 0.6f);
|
||||
|
||||
RectTransform rt = lineObj.GetComponent<RectTransform>();
|
||||
Vector2 diff = to - from;
|
||||
rt.sizeDelta = new Vector2(diff.magnitude, 4f);
|
||||
rt.pivot = new Vector2(0f, 0.5f);
|
||||
rt.anchoredPosition = from;
|
||||
float angle = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;
|
||||
rt.localRotation = Quaternion.Euler(0f, 0f, angle);
|
||||
|
||||
spawnedElements.Add(lineObj);
|
||||
}
|
||||
|
||||
private void CreateNodeCard(StageNode node, Vector2 anchoredPos)
|
||||
{
|
||||
GameObject nodeObj = new GameObject($"NodeCard_{node.Id}");
|
||||
nodeObj.transform.SetParent(mapContainer, false);
|
||||
RectTransform rt = nodeObj.AddComponent<RectTransform>();
|
||||
rt.sizeDelta = new Vector2(110f, 65f);
|
||||
rt.anchoredPosition = anchoredPos;
|
||||
|
||||
Image bg = nodeObj.AddComponent<Image>();
|
||||
bg.color = GetNodeColor(node);
|
||||
|
||||
// Node Text: Name / Type
|
||||
GameObject textObj = new GameObject("Label");
|
||||
textObj.transform.SetParent(nodeObj.transform, false);
|
||||
Text label = textObj.AddComponent<Text>();
|
||||
label.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
label.fontSize = 12;
|
||||
label.fontStyle = FontStyle.Bold;
|
||||
label.alignment = TextAnchor.MiddleCenter;
|
||||
label.color = Color.white;
|
||||
label.text = GetNodeDisplayText(node);
|
||||
|
||||
RectTransform textRt = textObj.GetComponent<RectTransform>();
|
||||
textRt.anchorMin = Vector2.zero;
|
||||
textRt.anchorMax = Vector2.one;
|
||||
textRt.offsetMin = new Vector2(4f, 4f);
|
||||
textRt.offsetMax = new Vector2(-4f, -4f);
|
||||
|
||||
// Current Player Indicator Border
|
||||
if (node.IsCurrent)
|
||||
{
|
||||
GameObject borderObj = new GameObject("CurrentBorder");
|
||||
borderObj.transform.SetParent(nodeObj.transform, false);
|
||||
Image borderImg = borderObj.AddComponent<Image>();
|
||||
borderImg.color = new Color(1f, 0.85f, 0.2f, 0.9f);
|
||||
RectTransform borderRt = borderObj.GetComponent<RectTransform>();
|
||||
borderRt.anchorMin = Vector2.zero;
|
||||
borderRt.anchorMax = Vector2.one;
|
||||
borderRt.offsetMin = new Vector2(-4f, -4f);
|
||||
borderRt.offsetMax = new Vector2(4f, 4f);
|
||||
borderObj.transform.SetAsFirstSibling();
|
||||
}
|
||||
|
||||
spawnedElements.Add(nodeObj);
|
||||
}
|
||||
|
||||
private static Color GetNodeColor(StageNode node)
|
||||
{
|
||||
if (node.IsCurrent)
|
||||
{
|
||||
return new Color(0.2f, 0.5f, 0.8f, 0.95f);
|
||||
}
|
||||
|
||||
return node.NodeType switch
|
||||
{
|
||||
StageNodeType.Entrance => new Color(0.25f, 0.45f, 0.35f, 0.9f),
|
||||
StageNodeType.Combat => new Color(0.35f, 0.25f, 0.25f, 0.9f),
|
||||
StageNodeType.Fork => new Color(0.3f, 0.3f, 0.45f, 0.9f),
|
||||
StageNodeType.Reward => new Color(0.5f, 0.4f, 0.15f, 0.9f),
|
||||
StageNodeType.Recovery => new Color(0.2f, 0.45f, 0.25f, 0.9f),
|
||||
StageNodeType.Elite => new Color(0.6f, 0.25f, 0.15f, 0.95f),
|
||||
StageNodeType.MidBoss => new Color(0.65f, 0.2f, 0.2f, 0.95f),
|
||||
StageNodeType.FinalBoss => new Color(0.6f, 0.15f, 0.5f, 0.95f),
|
||||
StageNodeType.StageExit => new Color(0.2f, 0.55f, 0.45f, 0.9f),
|
||||
_ => new Color(0.3f, 0.3f, 0.35f, 0.9f),
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetNodeDisplayText(StageNode node)
|
||||
{
|
||||
string currentTag = node.IsCurrent ? "[YOU]\n" : "";
|
||||
string typeName = node.NodeType switch
|
||||
{
|
||||
StageNodeType.Entrance => "ENTRANCE",
|
||||
StageNodeType.Combat => "COMBAT",
|
||||
StageNodeType.Fork => "FORK",
|
||||
StageNodeType.Reward => $"REWARD\n<{node.RewardType}>",
|
||||
StageNodeType.Recovery => "RECOVERY\n<HEAL 50%>",
|
||||
StageNodeType.Elite => "★ ELITE ★",
|
||||
StageNodeType.MidBoss => "★ MID BOSS ★",
|
||||
StageNodeType.FinalBoss => "☠ NECROMANCER ☠",
|
||||
StageNodeType.StageExit => "STAGE EXIT",
|
||||
_ => node.NodeType.ToString().ToUpper(),
|
||||
};
|
||||
|
||||
return currentTag + typeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ac4129f6b1ac4a4789bc13d9ad8a435
|
||||
Reference in New Issue
Block a user