Initial Unity 6 scaffold for Cat Kirby Adventures.
Add Phase A greybox vertical slice: third-person movement, forest path, checkpoints, story triggers, and light ability gate.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
public class LightAbility : MonoBehaviour
|
||||
{
|
||||
public float Range = 18f;
|
||||
public float Cooldown = 0.75f;
|
||||
public Transform Origin;
|
||||
public LayerMask HitMask = ~0;
|
||||
|
||||
float _nextReadyTime;
|
||||
LineRenderer _line;
|
||||
float _flashUntil;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_line = gameObject.AddComponent<LineRenderer>();
|
||||
_line.positionCount = 2;
|
||||
_line.startWidth = 0.08f;
|
||||
_line.endWidth = 0.02f;
|
||||
_line.material = new Material(Shader.Find("Sprites/Default"));
|
||||
_line.startColor = new Color(1f, 0.95f, 0.6f, 1f);
|
||||
_line.endColor = new Color(1f, 0.85f, 0.2f, 0.2f);
|
||||
_line.enabled = false;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (GameFlow.Instance != null && (GameFlow.Instance.IsPaused || GameFlow.Instance.HasWon))
|
||||
return;
|
||||
|
||||
if (_line.enabled && Time.time > _flashUntil)
|
||||
_line.enabled = false;
|
||||
|
||||
bool unlocked = GameFlow.Instance == null || GameFlow.Instance.LightAbilityUnlocked;
|
||||
if (!unlocked) return;
|
||||
if (Time.time < _nextReadyTime) return;
|
||||
|
||||
var mouse = Mouse.current;
|
||||
var kb = Keyboard.current;
|
||||
bool fire = (mouse != null && mouse.leftButton.wasPressedThisFrame) ||
|
||||
(kb != null && kb.eKey.wasPressedThisFrame);
|
||||
if (fire)
|
||||
Fire();
|
||||
}
|
||||
|
||||
void Fire()
|
||||
{
|
||||
_nextReadyTime = Time.time + Cooldown;
|
||||
|
||||
Transform origin = Origin != null ? Origin : transform;
|
||||
Vector3 start = origin.position + Vector3.up * 1.0f;
|
||||
Vector3 dir = origin.forward;
|
||||
if (Camera.main != null)
|
||||
dir = Camera.main.transform.forward;
|
||||
|
||||
Vector3 end = start + dir * Range;
|
||||
if (Physics.Raycast(start, dir, out RaycastHit hit, Range, HitMask, QueryTriggerInteraction.Collide))
|
||||
{
|
||||
end = hit.point;
|
||||
var reactive = hit.collider.GetComponentInParent<ILightReactive>();
|
||||
reactive?.OnHitByLight(hit.point, dir);
|
||||
}
|
||||
|
||||
_line.SetPosition(0, start);
|
||||
_line.SetPosition(1, end);
|
||||
_line.enabled = true;
|
||||
_flashUntil = Time.time + 0.12f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures a single bootstrap exists and builds the Phase A greybox level.
|
||||
/// </summary>
|
||||
public class GameBootstrap : MonoBehaviour
|
||||
{
|
||||
static bool _initialized;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void AutoBootstrap()
|
||||
{
|
||||
if (_initialized) return;
|
||||
if (FindFirstObjectByType<GameBootstrap>() != null)
|
||||
{
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var go = new GameObject("GameBootstrap");
|
||||
go.AddComponent<GameBootstrap>();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
if (GameFlow.Instance == null)
|
||||
{
|
||||
var flowGo = new GameObject("GameFlow");
|
||||
flowGo.AddComponent<GameFlow>();
|
||||
DontDestroyOnLoad(flowGo);
|
||||
}
|
||||
|
||||
if (FindFirstObjectByType<CheckpointSystem>() == null)
|
||||
{
|
||||
var cp = new GameObject("CheckpointSystem");
|
||||
cp.AddComponent<CheckpointSystem>();
|
||||
DontDestroyOnLoad(cp);
|
||||
}
|
||||
|
||||
BuildUi();
|
||||
Level01ForestBuilder.Build();
|
||||
}
|
||||
|
||||
void BuildUi()
|
||||
{
|
||||
var dialogue = DialogueUI.EnsureExists();
|
||||
var pause = CreateOverlay("PauseOverlay", "ПАУЗА\nEsc — продолжить", new Color(0f, 0f, 0f, 0.65f));
|
||||
pause.SetActive(false);
|
||||
var win = CreateOverlay("WinOverlay", "Ворота открыты!\nДеревня ждёт Кирби.\n\nПобеда среза Phase A", new Color(0.05f, 0.2f, 0.1f, 0.8f));
|
||||
win.SetActive(false);
|
||||
|
||||
if (GameFlow.Instance != null)
|
||||
{
|
||||
GameFlow.Instance.SetPauseOverlay(pause);
|
||||
GameFlow.Instance.SetWinOverlay(win);
|
||||
}
|
||||
|
||||
// silence unused
|
||||
_ = dialogue;
|
||||
}
|
||||
|
||||
static GameObject CreateOverlay(string name, string text, Color bg)
|
||||
{
|
||||
var canvasGo = new GameObject(name);
|
||||
var canvas = canvasGo.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 100;
|
||||
canvasGo.AddComponent<UnityEngine.UI.CanvasScaler>().uiScaleMode =
|
||||
UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
canvasGo.AddComponent<UnityEngine.UI.GraphicRaycaster>();
|
||||
DontDestroyOnLoad(canvasGo);
|
||||
|
||||
var panel = new GameObject("Panel");
|
||||
panel.transform.SetParent(canvasGo.transform, false);
|
||||
var img = panel.AddComponent<UnityEngine.UI.Image>();
|
||||
img.color = bg;
|
||||
var rt = panel.GetComponent<RectTransform>();
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
|
||||
var textGo = new GameObject("Text");
|
||||
textGo.transform.SetParent(panel.transform, false);
|
||||
var uiText = textGo.AddComponent<UnityEngine.UI.Text>();
|
||||
uiText.text = text;
|
||||
uiText.alignment = TextAnchor.MiddleCenter;
|
||||
uiText.fontSize = 36;
|
||||
uiText.color = Color.white;
|
||||
uiText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
if (uiText.font == null)
|
||||
uiText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
var trt = textGo.GetComponent<RectTransform>();
|
||||
trt.anchorMin = new Vector2(0.1f, 0.2f);
|
||||
trt.anchorMax = new Vector2(0.9f, 0.8f);
|
||||
trt.offsetMin = Vector2.zero;
|
||||
trt.offsetMax = Vector2.zero;
|
||||
|
||||
return canvasGo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
/// <summary>
|
||||
/// Global game state: pause, win, light ability unlock.
|
||||
/// </summary>
|
||||
public class GameFlow : MonoBehaviour
|
||||
{
|
||||
public static GameFlow Instance { get; private set; }
|
||||
|
||||
public bool IsPaused { get; private set; }
|
||||
public bool HasWon { get; private set; }
|
||||
public bool LightAbilityUnlocked { get; private set; }
|
||||
|
||||
GameObject _pauseOverlay;
|
||||
GameObject _winOverlay;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
public void SetPauseOverlay(GameObject overlay) => _pauseOverlay = overlay;
|
||||
public void SetWinOverlay(GameObject overlay) => _winOverlay = overlay;
|
||||
|
||||
public void UnlockLightAbility()
|
||||
{
|
||||
LightAbilityUnlocked = true;
|
||||
Debug.Log("[GameFlow] Light ability unlocked.");
|
||||
}
|
||||
|
||||
public void TogglePause()
|
||||
{
|
||||
if (HasWon) return;
|
||||
SetPaused(!IsPaused);
|
||||
}
|
||||
|
||||
public void SetPaused(bool paused)
|
||||
{
|
||||
IsPaused = paused;
|
||||
Time.timeScale = paused ? 0f : 1f;
|
||||
if (_pauseOverlay != null)
|
||||
_pauseOverlay.SetActive(paused);
|
||||
|
||||
Cursor.lockState = paused ? CursorLockMode.None : CursorLockMode.Locked;
|
||||
Cursor.visible = paused;
|
||||
}
|
||||
|
||||
public void TriggerWin()
|
||||
{
|
||||
if (HasWon) return;
|
||||
HasWon = true;
|
||||
IsPaused = true;
|
||||
Time.timeScale = 0f;
|
||||
if (_winOverlay != null)
|
||||
_winOverlay.SetActive(true);
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
Debug.Log("[GameFlow] Level complete — village gate opened.");
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
var kb = UnityEngine.InputSystem.Keyboard.current;
|
||||
if (kb != null && kb.escapeKey.wasPressedThisFrame)
|
||||
TogglePause();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
Time.timeScale = 1f;
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class AbilityUnlockPedestal : MonoBehaviour
|
||||
{
|
||||
public string StoryLine =
|
||||
"Внутри вспыхнул тихий свет — не снаружи, а из самого сердца Кирби.";
|
||||
bool _used;
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (_used) return;
|
||||
if (other.GetComponent<PlayerMotor>() == null) return;
|
||||
_used = true;
|
||||
if (GameFlow.Instance != null)
|
||||
GameFlow.Instance.UnlockLightAbility();
|
||||
DialogueUI.ShowTemporary(StoryLine, 5f);
|
||||
|
||||
var rend = GetComponentInChildren<Renderer>();
|
||||
if (rend != null)
|
||||
rend.material.color = new Color(1f, 0.92f, 0.35f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class Checkpoint : MonoBehaviour
|
||||
{
|
||||
public Transform SpawnPoint;
|
||||
public string CheckpointId = "cp";
|
||||
|
||||
void Reset()
|
||||
{
|
||||
var c = GetComponent<Collider>();
|
||||
if (c != null) c.isTrigger = true;
|
||||
}
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!other.CompareTag("Player") && other.GetComponent<PlayerMotor>() == null)
|
||||
return;
|
||||
|
||||
var point = SpawnPoint != null ? SpawnPoint : transform;
|
||||
CheckpointSystem.SetCheckpoint(point.position, point.rotation, CheckpointId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
public class CheckpointSystem : MonoBehaviour
|
||||
{
|
||||
public static CheckpointSystem Instance { get; private set; }
|
||||
|
||||
public Vector3 SpawnPosition { get; private set; }
|
||||
public Quaternion SpawnRotation { get; private set; } = Quaternion.identity;
|
||||
public string CurrentId { get; private set; } = "start";
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public static void SetCheckpoint(Vector3 pos, Quaternion rot, string id)
|
||||
{
|
||||
if (Instance == null) return;
|
||||
Instance.SpawnPosition = pos;
|
||||
Instance.SpawnRotation = rot;
|
||||
Instance.CurrentId = id;
|
||||
Debug.Log($"[Checkpoint] Saved: {id}");
|
||||
}
|
||||
|
||||
public static void Respawn(Transform player, CharacterController cc = null)
|
||||
{
|
||||
if (Instance == null || player == null) return;
|
||||
if (cc != null) cc.enabled = false;
|
||||
player.SetPositionAndRotation(Instance.SpawnPosition, Instance.SpawnRotation);
|
||||
if (cc != null) cc.enabled = true;
|
||||
Physics.SyncTransforms();
|
||||
}
|
||||
|
||||
public static void EnsureStart(Vector3 pos, Quaternion rot)
|
||||
{
|
||||
if (Instance == null) return;
|
||||
if (Instance.CurrentId == "start" && Instance.SpawnPosition == Vector3.zero && pos != Vector3.zero
|
||||
|| Instance.CurrentId == "start")
|
||||
{
|
||||
// Always seed start if still default id
|
||||
}
|
||||
Instance.SpawnPosition = pos;
|
||||
Instance.SpawnRotation = rot;
|
||||
Instance.CurrentId = "start";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
public class FallKillPlane : MonoBehaviour
|
||||
{
|
||||
public float KillY = -10f;
|
||||
Transform _player;
|
||||
CharacterController _cc;
|
||||
|
||||
public void Bind(Transform player)
|
||||
{
|
||||
_player = player;
|
||||
_cc = player != null ? player.GetComponent<CharacterController>() : null;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_player == null) return;
|
||||
if (_player.position.y < KillY)
|
||||
CheckpointSystem.Respawn(_player, _cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
public interface ILightReactive
|
||||
{
|
||||
void OnHitByLight(Vector3 hitPoint, Vector3 direction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
/// <summary>
|
||||
/// Procedural greybox for Phase A — Level_01_Forest.
|
||||
/// </summary>
|
||||
public static class Level01ForestBuilder
|
||||
{
|
||||
static Transform _root;
|
||||
|
||||
public static void Build()
|
||||
{
|
||||
if (GameObject.Find("Level_01_Forest") != null)
|
||||
return;
|
||||
|
||||
_root = new GameObject("Level_01_Forest").transform;
|
||||
|
||||
EnsureLighting();
|
||||
BuildGroundAndPath();
|
||||
BuildJumpPlatforms();
|
||||
var startPos = new Vector3(0f, 1.1f, 0f);
|
||||
BuildCheckpoints();
|
||||
BuildStoryTriggers();
|
||||
BuildAbilityPedestal();
|
||||
BuildVillageGate();
|
||||
var player = BuildPlayer(startPos);
|
||||
BuildCamera(player);
|
||||
BuildKillPlane(player.transform);
|
||||
|
||||
CheckpointSystem.EnsureStart(startPos, Quaternion.identity);
|
||||
CheckpointSystem.SetCheckpoint(startPos, Quaternion.identity, "start");
|
||||
|
||||
DialogueUI.ShowTemporary("Кирби очнулся среди деревьев. Тропа ведёт к деревне — и к себе.", 4f);
|
||||
}
|
||||
|
||||
static void EnsureLighting()
|
||||
{
|
||||
// Keep scene light; add one if missing
|
||||
if (Object.FindFirstObjectByType<Light>() == null)
|
||||
{
|
||||
var lightGo = new GameObject("Directional Light");
|
||||
lightGo.transform.SetParent(_root);
|
||||
var light = lightGo.AddComponent<Light>();
|
||||
light.type = LightType.Directional;
|
||||
light.color = new Color(1f, 0.95f, 0.85f);
|
||||
light.intensity = 1.1f;
|
||||
lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
|
||||
}
|
||||
|
||||
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Trilight;
|
||||
RenderSettings.ambientSkyColor = new Color(0.45f, 0.55f, 0.65f);
|
||||
RenderSettings.ambientEquatorColor = new Color(0.35f, 0.4f, 0.3f);
|
||||
RenderSettings.ambientGroundColor = new Color(0.15f, 0.12f, 0.1f);
|
||||
}
|
||||
|
||||
static void BuildGroundAndPath()
|
||||
{
|
||||
// Start platform
|
||||
CreateBox("StartPlatform", new Vector3(0f, 0f, 0f), new Vector3(10f, 1f, 10f),
|
||||
new Color(0.35f, 0.45f, 0.28f));
|
||||
|
||||
// Forest path stretches
|
||||
CreateBox("Path_A", new Vector3(0f, 0f, 14f), new Vector3(6f, 1f, 16f),
|
||||
new Color(0.32f, 0.42f, 0.26f));
|
||||
CreateBox("Path_B", new Vector3(0f, 0f, 32f), new Vector3(5f, 1f, 12f),
|
||||
new Color(0.3f, 0.4f, 0.24f));
|
||||
|
||||
// Side tree markers (cylinders)
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
float z = 4f + i * 5f;
|
||||
float x = (i % 2 == 0) ? -5.5f : 5.5f;
|
||||
var tree = CreatePrimitive(PrimitiveType.Cylinder, $"Tree_{i}",
|
||||
new Vector3(x, 2f, z), new Vector3(0.6f, 2f, 0.6f),
|
||||
new Color(0.2f, 0.28f, 0.15f));
|
||||
tree.transform.SetParent(_root);
|
||||
}
|
||||
}
|
||||
|
||||
static void BuildJumpPlatforms()
|
||||
{
|
||||
// Gap then platforms
|
||||
CreateBox("Pad_1", new Vector3(0f, 0.5f, 42f), new Vector3(3.5f, 1f, 3.5f),
|
||||
new Color(0.45f, 0.38f, 0.28f));
|
||||
CreateBox("Pad_2", new Vector3(3f, 1.2f, 48f), new Vector3(3f, 1f, 3f),
|
||||
new Color(0.48f, 0.4f, 0.3f));
|
||||
CreateBox("Pad_3", new Vector3(-2f, 2f, 54f), new Vector3(3f, 1f, 3f),
|
||||
new Color(0.5f, 0.42f, 0.32f));
|
||||
CreateBox("Pad_4", new Vector3(0f, 2.5f, 61f), new Vector3(4f, 1f, 4f),
|
||||
new Color(0.4f, 0.45f, 0.3f));
|
||||
|
||||
// Landing after mid section
|
||||
CreateBox("Path_C", new Vector3(0f, 0f, 72f), new Vector3(8f, 1f, 14f),
|
||||
new Color(0.33f, 0.42f, 0.27f));
|
||||
CreateBox("Path_D", new Vector3(0f, 0f, 90f), new Vector3(10f, 1f, 16f),
|
||||
new Color(0.36f, 0.44f, 0.3f));
|
||||
CreateBox("VillageApproach", new Vector3(0f, 0f, 108f), new Vector3(12f, 1f, 14f),
|
||||
new Color(0.4f, 0.4f, 0.35f));
|
||||
}
|
||||
|
||||
static void BuildCheckpoints()
|
||||
{
|
||||
CreateCheckpoint("CP_Start", new Vector3(0f, 1.5f, 2f), new Vector3(4f, 2f, 2f), "cp_start");
|
||||
CreateCheckpoint("CP_Mid", new Vector3(0f, 3.5f, 61f), new Vector3(3f, 2f, 3f), "cp_mid");
|
||||
CreateCheckpoint("CP_NearGate", new Vector3(0f, 1.5f, 100f), new Vector3(5f, 2f, 3f), "cp_gate");
|
||||
}
|
||||
|
||||
static void CreateCheckpoint(string name, Vector3 pos, Vector3 size, string id)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
go.transform.SetParent(_root);
|
||||
go.transform.position = pos;
|
||||
var box = go.AddComponent<BoxCollider>();
|
||||
box.isTrigger = true;
|
||||
box.size = size;
|
||||
var cp = go.AddComponent<Checkpoint>();
|
||||
cp.CheckpointId = id;
|
||||
|
||||
var marker = CreatePrimitive(PrimitiveType.Cube, name + "_Marker",
|
||||
pos + Vector3.up * 0.5f, new Vector3(0.4f, 1.2f, 0.4f),
|
||||
new Color(0.2f, 0.7f, 0.9f, 0.7f));
|
||||
marker.transform.SetParent(go.transform);
|
||||
Object.Destroy(marker.GetComponent<Collider>());
|
||||
cp.SpawnPoint = go.transform;
|
||||
}
|
||||
|
||||
static void BuildStoryTriggers()
|
||||
{
|
||||
CreateStory("Story_Fall", new Vector3(0f, 2f, 8f), new Vector3(6f, 3f, 3f),
|
||||
"«Падение»\nЗемля приняла его мягко, будто лес давно ждал чёрного кота.", 5f);
|
||||
CreateStory("Story_Road", new Vector3(0f, 2f, 28f), new Vector3(5f, 3f, 3f),
|
||||
"«Путь в деревню»\nТропа узкая, но шаг за шагом — и чужой шум становится тише.", 5f);
|
||||
CreateStory("Story_Source", new Vector3(0f, 3.5f, 74f), new Vector3(6f, 3f, 3f),
|
||||
"«Внутренний источник»\nСвет не нужно искать в небе — он уже греет изнутри.", 5f);
|
||||
}
|
||||
|
||||
static void CreateStory(string name, Vector3 pos, Vector3 size, string quote, float secs)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
go.transform.SetParent(_root);
|
||||
go.transform.position = pos;
|
||||
var box = go.AddComponent<BoxCollider>();
|
||||
box.isTrigger = true;
|
||||
box.size = size;
|
||||
var st = go.AddComponent<StoryTrigger>();
|
||||
st.Quote = quote;
|
||||
st.DisplaySeconds = secs;
|
||||
}
|
||||
|
||||
static void BuildAbilityPedestal()
|
||||
{
|
||||
var pedestal = CreatePrimitive(PrimitiveType.Cylinder, "LightPedestal",
|
||||
new Vector3(0f, 1.2f, 78f), new Vector3(1.4f, 0.6f, 1.4f),
|
||||
new Color(0.55f, 0.5f, 0.75f));
|
||||
pedestal.transform.SetParent(_root);
|
||||
|
||||
var orb = CreatePrimitive(PrimitiveType.Sphere, "LightOrb",
|
||||
new Vector3(0f, 2.4f, 78f), Vector3.one * 0.7f,
|
||||
new Color(1f, 0.95f, 0.55f));
|
||||
orb.transform.SetParent(pedestal.transform);
|
||||
Object.Destroy(orb.GetComponent<Collider>());
|
||||
|
||||
var trigger = new GameObject("AbilityUnlock");
|
||||
trigger.transform.SetParent(_root);
|
||||
trigger.transform.position = new Vector3(0f, 1.5f, 78f);
|
||||
var col = trigger.AddComponent<BoxCollider>();
|
||||
col.isTrigger = true;
|
||||
col.size = new Vector3(3.5f, 3f, 3.5f);
|
||||
trigger.AddComponent<AbilityUnlockPedestal>();
|
||||
}
|
||||
|
||||
static void BuildVillageGate()
|
||||
{
|
||||
var posts = CreateBox("GatePosts", new Vector3(0f, 3f, 116f), new Vector3(8f, 6f, 1.2f),
|
||||
new Color(0.35f, 0.28f, 0.22f));
|
||||
// Hollow-ish: leave a door slab that moves
|
||||
var door = CreateBox("GateDoor", new Vector3(0f, 2.5f, 116.2f), new Vector3(3.2f, 4.5f, 0.6f),
|
||||
new Color(0.15f, 0.12f, 0.2f));
|
||||
var gate = door.AddComponent<LightGate>();
|
||||
gate.GateVisual = door.transform;
|
||||
|
||||
// Invisible hit proxy in front
|
||||
var hit = new GameObject("GateLightTarget");
|
||||
hit.transform.SetParent(_root);
|
||||
hit.transform.position = new Vector3(0f, 2.5f, 115.5f);
|
||||
var hc = hit.AddComponent<BoxCollider>();
|
||||
hc.size = new Vector3(3.5f, 4.5f, 1.5f);
|
||||
// forward hits to LightGate via child relationship
|
||||
hit.transform.SetParent(door.transform);
|
||||
// LightGate is on door; GetComponentInParent will find it
|
||||
}
|
||||
|
||||
static GameObject BuildPlayer(Vector3 pos)
|
||||
{
|
||||
// Black capsule named Kirby
|
||||
var body = CreatePrimitive(PrimitiveType.Capsule, "Kirby", pos, new Vector3(0.9f, 1f, 0.9f),
|
||||
new Color(0.05f, 0.05f, 0.08f));
|
||||
body.tag = "Player";
|
||||
Object.Destroy(body.GetComponent<Collider>());
|
||||
|
||||
var cc = body.AddComponent<CharacterController>();
|
||||
cc.height = 2f;
|
||||
cc.radius = 0.45f;
|
||||
cc.center = new Vector3(0f, 0f, 0f);
|
||||
cc.skinWidth = 0.08f;
|
||||
|
||||
var yaw = new GameObject("YawPivot");
|
||||
yaw.transform.SetParent(body.transform);
|
||||
yaw.transform.localPosition = Vector3.zero;
|
||||
|
||||
var motor = body.AddComponent<PlayerMotor>();
|
||||
motor.YawPivot = yaw.transform;
|
||||
|
||||
body.AddComponent<LightAbility>();
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
static void BuildCamera(GameObject player)
|
||||
{
|
||||
Camera cam = Camera.main;
|
||||
if (cam == null)
|
||||
{
|
||||
var camGo = new GameObject("Main Camera");
|
||||
cam = camGo.AddComponent<Camera>();
|
||||
cam.tag = "MainCamera";
|
||||
camGo.AddComponent<AudioListener>();
|
||||
}
|
||||
|
||||
var follow = cam.GetComponent<SimpleCameraFollow>();
|
||||
if (follow == null)
|
||||
follow = cam.gameObject.AddComponent<SimpleCameraFollow>();
|
||||
|
||||
var motor = player.GetComponent<PlayerMotor>();
|
||||
Transform yaw = player.transform.Find("YawPivot");
|
||||
follow.Target = player.transform;
|
||||
follow.YawPivot = yaw;
|
||||
motor.CameraTransform = cam.transform;
|
||||
motor.YawPivot = yaw;
|
||||
|
||||
cam.transform.position = player.transform.position + new Vector3(0f, 2.5f, -5.5f);
|
||||
cam.transform.LookAt(player.transform.position + Vector3.up * 1.2f);
|
||||
}
|
||||
|
||||
static void BuildKillPlane(Transform player)
|
||||
{
|
||||
var go = new GameObject("FallKillPlane");
|
||||
go.transform.SetParent(_root);
|
||||
var kill = go.AddComponent<FallKillPlane>();
|
||||
kill.KillY = -10f;
|
||||
kill.Bind(player);
|
||||
}
|
||||
|
||||
static GameObject CreateBox(string name, Vector3 pos, Vector3 scale, Color color)
|
||||
{
|
||||
var go = CreatePrimitive(PrimitiveType.Cube, name, pos, scale, color);
|
||||
go.transform.SetParent(_root);
|
||||
return go;
|
||||
}
|
||||
|
||||
static GameObject CreatePrimitive(PrimitiveType type, string name, Vector3 pos, Vector3 scale, Color color)
|
||||
{
|
||||
var go = GameObject.CreatePrimitive(type);
|
||||
go.name = name;
|
||||
go.transform.position = pos;
|
||||
go.transform.localScale = scale;
|
||||
var rend = go.GetComponent<Renderer>();
|
||||
if (rend != null)
|
||||
{
|
||||
// URP Lit may exist after package import; fallback to Standard / Unlit
|
||||
var shader = Shader.Find("Universal Render Pipeline/Lit");
|
||||
if (shader == null) shader = Shader.Find("Standard");
|
||||
if (shader == null) shader = Shader.Find("Sprites/Default");
|
||||
if (shader != null)
|
||||
{
|
||||
var mat = new Material(shader);
|
||||
if (mat.HasProperty("_BaseColor"))
|
||||
mat.SetColor("_BaseColor", color);
|
||||
else if (mat.HasProperty("_Color"))
|
||||
mat.SetColor("_Color", color);
|
||||
else
|
||||
mat.color = color;
|
||||
rend.material = mat;
|
||||
}
|
||||
else
|
||||
{
|
||||
rend.material.color = color;
|
||||
}
|
||||
}
|
||||
return go;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
/// <summary>
|
||||
/// Village gate that opens when hit by Light after the ability is unlocked.
|
||||
/// </summary>
|
||||
public class LightGate : MonoBehaviour, ILightReactive
|
||||
{
|
||||
public bool IsOpen { get; private set; }
|
||||
public Transform GateVisual;
|
||||
public float OpenHeight = 4f;
|
||||
public float OpenSpeed = 2.5f;
|
||||
|
||||
Vector3 _closedPos;
|
||||
Vector3 _openPos;
|
||||
bool _opening;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (GateVisual == null)
|
||||
GateVisual = transform;
|
||||
_closedPos = GateVisual.localPosition;
|
||||
_openPos = _closedPos + Vector3.up * OpenHeight;
|
||||
}
|
||||
|
||||
public void OnHitByLight(Vector3 hitPoint, Vector3 direction)
|
||||
{
|
||||
if (IsOpen) return;
|
||||
if (GameFlow.Instance != null && !GameFlow.Instance.LightAbilityUnlocked)
|
||||
{
|
||||
DialogueUI.ShowTemporary("Свет ещё спит внутри... Найди источник на тропе.", 3f);
|
||||
return;
|
||||
}
|
||||
|
||||
IsOpen = true;
|
||||
_opening = true;
|
||||
DialogueUI.ShowTemporary("Свет коснулся ворот — путь в деревню открыт.", 3.5f);
|
||||
if (GameFlow.Instance != null)
|
||||
GameFlow.Instance.TriggerWin();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!_opening || GateVisual == null) return;
|
||||
GateVisual.localPosition = Vector3.MoveTowards(
|
||||
GateVisual.localPosition, _openPos, OpenSpeed * Time.unscaledDeltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
public class PlayerMotor : MonoBehaviour
|
||||
{
|
||||
public float MoveSpeed = 6f;
|
||||
public float JumpHeight = 1.4f;
|
||||
public float Gravity = -22f;
|
||||
public float GroundStick = -2f;
|
||||
public Transform CameraTransform;
|
||||
public Transform YawPivot;
|
||||
|
||||
CharacterController _cc;
|
||||
Vector3 _velocity;
|
||||
bool _grounded;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_cc = GetComponent<CharacterController>();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (GameFlow.Instance != null && (GameFlow.Instance.IsPaused || GameFlow.Instance.HasWon))
|
||||
return;
|
||||
|
||||
HandleRespawnInput();
|
||||
ReadMoveAndJump();
|
||||
}
|
||||
|
||||
void HandleRespawnInput()
|
||||
{
|
||||
var kb = Keyboard.current;
|
||||
if (kb != null && kb.rKey.wasPressedThisFrame)
|
||||
CheckpointSystem.Respawn(transform, _cc);
|
||||
}
|
||||
|
||||
void ReadMoveAndJump()
|
||||
{
|
||||
var kb = Keyboard.current;
|
||||
float h = 0f, v = 0f;
|
||||
if (kb != null)
|
||||
{
|
||||
if (kb.aKey.isPressed || kb.leftArrowKey.isPressed) h -= 1f;
|
||||
if (kb.dKey.isPressed || kb.rightArrowKey.isPressed) h += 1f;
|
||||
if (kb.sKey.isPressed || kb.downArrowKey.isPressed) v -= 1f;
|
||||
if (kb.wKey.isPressed || kb.upArrowKey.isPressed) v += 1f;
|
||||
}
|
||||
|
||||
_grounded = _cc.isGrounded;
|
||||
if (_grounded && _velocity.y < 0f)
|
||||
_velocity.y = GroundStick;
|
||||
|
||||
Vector3 forward = Vector3.forward;
|
||||
Vector3 right = Vector3.right;
|
||||
if (YawPivot != null)
|
||||
{
|
||||
forward = YawPivot.forward;
|
||||
right = YawPivot.right;
|
||||
}
|
||||
else if (CameraTransform != null)
|
||||
{
|
||||
forward = CameraTransform.forward;
|
||||
right = CameraTransform.right;
|
||||
}
|
||||
|
||||
forward.y = 0f;
|
||||
right.y = 0f;
|
||||
forward.Normalize();
|
||||
right.Normalize();
|
||||
|
||||
Vector3 move = (forward * v + right * h);
|
||||
if (move.sqrMagnitude > 1f) move.Normalize();
|
||||
|
||||
_cc.Move(move * MoveSpeed * Time.deltaTime);
|
||||
|
||||
if (kb != null && kb.spaceKey.wasPressedThisFrame && _grounded)
|
||||
_velocity.y = Mathf.Sqrt(JumpHeight * -2f * Gravity);
|
||||
|
||||
_velocity.y += Gravity * Time.deltaTime;
|
||||
_cc.Move(_velocity * Time.deltaTime);
|
||||
|
||||
if (move.sqrMagnitude > 0.01f)
|
||||
{
|
||||
var look = Quaternion.LookRotation(move, Vector3.up);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, look, 12f * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
/// <summary>
|
||||
/// Third-person follow camera with mouse look (avoids Cinemachine 3 API coupling).
|
||||
/// </summary>
|
||||
public class SimpleCameraFollow : MonoBehaviour
|
||||
{
|
||||
public Transform Target;
|
||||
public Transform YawPivot;
|
||||
public float Distance = 5.5f;
|
||||
public float Height = 1.6f;
|
||||
public float MouseSensitivity = 0.12f;
|
||||
public float MinPitch = -30f;
|
||||
public float MaxPitch = 55f;
|
||||
public float Smooth = 12f;
|
||||
|
||||
float _yaw;
|
||||
float _pitch = 12f;
|
||||
|
||||
public float Yaw => _yaw;
|
||||
public float Pitch => _pitch;
|
||||
|
||||
void Start()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
if (YawPivot != null)
|
||||
_yaw = YawPivot.eulerAngles.y;
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (Target == null) return;
|
||||
if (GameFlow.Instance != null && (GameFlow.Instance.IsPaused || GameFlow.Instance.HasWon))
|
||||
return;
|
||||
|
||||
var mouse = Mouse.current;
|
||||
if (mouse != null)
|
||||
{
|
||||
var delta = mouse.delta.ReadValue();
|
||||
_yaw += delta.x * MouseSensitivity;
|
||||
_pitch -= delta.y * MouseSensitivity;
|
||||
_pitch = Mathf.Clamp(_pitch, MinPitch, MaxPitch);
|
||||
}
|
||||
|
||||
if (YawPivot != null)
|
||||
YawPivot.rotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
|
||||
var rot = Quaternion.Euler(_pitch, _yaw, 0f);
|
||||
var desired = Target.position + Vector3.up * Height + rot * (Vector3.back * Distance);
|
||||
transform.position = Vector3.Lerp(transform.position, desired, 1f - Mathf.Exp(-Smooth * Time.deltaTime));
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, rot, 1f - Mathf.Exp(-Smooth * Time.deltaTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class StoryTrigger : MonoBehaviour
|
||||
{
|
||||
[TextArea] public string Quote;
|
||||
public float DisplaySeconds = 4.5f;
|
||||
public bool Once = true;
|
||||
bool _shown;
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (Once && _shown) return;
|
||||
if (other.GetComponent<PlayerMotor>() == null) return;
|
||||
_shown = true;
|
||||
DialogueUI.ShowTemporary(Quote, DisplaySeconds);
|
||||
}
|
||||
|
||||
void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (other.GetComponent<PlayerMotor>() == null) return;
|
||||
// Optional: hide early when leaving — keep simple, let timer finish
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KirbyAdventures
|
||||
{
|
||||
public class DialogueUI : MonoBehaviour
|
||||
{
|
||||
public static DialogueUI Instance { get; private set; }
|
||||
|
||||
Text _text;
|
||||
Image _panel;
|
||||
float _hideAt;
|
||||
|
||||
public static DialogueUI EnsureExists()
|
||||
{
|
||||
if (Instance != null) return Instance;
|
||||
var go = new GameObject("DialogueUI");
|
||||
DontDestroyOnLoad(go);
|
||||
return go.AddComponent<DialogueUI>();
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
Build();
|
||||
}
|
||||
|
||||
void Build()
|
||||
{
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 50;
|
||||
gameObject.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
var panelGo = new GameObject("Panel");
|
||||
panelGo.transform.SetParent(transform, false);
|
||||
_panel = panelGo.AddComponent<Image>();
|
||||
_panel.color = new Color(0.05f, 0.07f, 0.1f, 0.82f);
|
||||
var prt = _panel.rectTransform;
|
||||
prt.anchorMin = new Vector2(0.12f, 0.05f);
|
||||
prt.anchorMax = new Vector2(0.88f, 0.22f);
|
||||
prt.offsetMin = Vector2.zero;
|
||||
prt.offsetMax = Vector2.zero;
|
||||
|
||||
var textGo = new GameObject("Quote");
|
||||
textGo.transform.SetParent(panelGo.transform, false);
|
||||
_text = textGo.AddComponent<Text>();
|
||||
_text.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
if (_text.font == null)
|
||||
_text.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
_text.fontSize = 26;
|
||||
_text.alignment = TextAnchor.MiddleCenter;
|
||||
_text.color = new Color(0.95f, 0.93f, 0.88f);
|
||||
_text.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
_text.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
var trt = _text.rectTransform;
|
||||
trt.anchorMin = new Vector2(0.04f, 0.1f);
|
||||
trt.anchorMax = new Vector2(0.96f, 0.9f);
|
||||
trt.offsetMin = Vector2.zero;
|
||||
trt.offsetMax = Vector2.zero;
|
||||
|
||||
_panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public static void ShowTemporary(string quote, float seconds)
|
||||
{
|
||||
EnsureExists();
|
||||
Instance.Show(quote, seconds);
|
||||
}
|
||||
|
||||
public void Show(string quote, float seconds)
|
||||
{
|
||||
if (_text == null) return;
|
||||
_text.text = quote;
|
||||
_panel.gameObject.SetActive(true);
|
||||
_hideAt = Time.unscaledTime + seconds;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_panel != null && _panel.gameObject.activeSelf && Time.unscaledTime >= _hideAt)
|
||||
_panel.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user