diff --git a/README.md b/README.md index 86282c2..8eef9f5 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,14 @@ In Too Fishy, you play as a deep-sea diver exploring the mysterious depths of th 2. Open Godot Engine 3. Click "Import" and navigate to the downloaded project folder 4. Select the "project.godot" file and click "Import & Edit" + +## Unity Port + +A Unity remake is available in the [`unity/`](unity/) folder. + +1. Open `unity/` in **Unity Hub** (2022.3 LTS or newer) +2. Open `Assets/Scenes/Main.unity` +3. Press Play + +See [`unity/README.md`](unity/README.md) and [`UNITY.md`](UNITY.md) for details. + diff --git a/UNITY.md b/UNITY.md new file mode 100644 index 0000000..3b0a519 --- /dev/null +++ b/UNITY.md @@ -0,0 +1,7 @@ +# Unity Port + +The Unity remake lives in the [`unity/`](unity/) folder. + +Open `unity/` in Unity Hub (2022.3 LTS+), then play `Assets/Scenes/Main.unity`. + +See [`unity/README.md`](unity/README.md) for controls and architecture. diff --git a/unity/.gitignore b/unity/.gitignore new file mode 100644 index 0000000..c23b66a --- /dev/null +++ b/unity/.gitignore @@ -0,0 +1,26 @@ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Bb]uild/ +[Bb]uilds/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db +*.pidb.meta +*.pdb.meta +*.mdb.meta +sysinfo.txt +crashlytics-build.properties diff --git a/unity/Assets/Editor.meta b/unity/Assets/Editor.meta new file mode 100644 index 0000000..d8b499a --- /dev/null +++ b/unity/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0ff645210312a6b88ff43f16e27ff8c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Editor/TooFishyMenu.cs b/unity/Assets/Editor/TooFishyMenu.cs new file mode 100644 index 0000000..dca39e3 --- /dev/null +++ b/unity/Assets/Editor/TooFishyMenu.cs @@ -0,0 +1,25 @@ +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace TooFishy.Editor +{ + public static class TooFishyMenu + { + [MenuItem("Too Fishy/Open Main Scene")] + static void OpenMain() + { + EditorSceneManager.OpenScene("Assets/Scenes/Main.unity"); + } + + [MenuItem("Too Fishy/Play Main Scene")] + static void PlayMain() + { + if (!EditorApplication.isPlaying) + { + EditorSceneManager.OpenScene("Assets/Scenes/Main.unity"); + EditorApplication.isPlaying = true; + } + } + } +} diff --git a/unity/Assets/Editor/TooFishyMenu.cs.meta b/unity/Assets/Editor/TooFishyMenu.cs.meta new file mode 100644 index 0000000..63a064e --- /dev/null +++ b/unity/Assets/Editor/TooFishyMenu.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44682596140bb40490f39c8189a49fe9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Materials.meta b/unity/Assets/Materials.meta new file mode 100644 index 0000000..9587e0b --- /dev/null +++ b/unity/Assets/Materials.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 23ce0eb7d180f3dc8391d4af48572d21 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Prefabs.meta b/unity/Assets/Prefabs.meta new file mode 100644 index 0000000..2a32860 --- /dev/null +++ b/unity/Assets/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 593c0f631efb596bd1f4f642b8547a15 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Resources.meta b/unity/Assets/Resources.meta new file mode 100644 index 0000000..a121614 --- /dev/null +++ b/unity/Assets/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ddcf50c29294d4414f3f7c1bbc892cb5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scenes.meta b/unity/Assets/Scenes.meta new file mode 100644 index 0000000..6899d20 --- /dev/null +++ b/unity/Assets/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bb34f602e45a9fbcb2cdfee7a48673fc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scenes/Main.unity b/unity/Assets/Scenes/Main.unity new file mode 100644 index 0000000..3e2af70 --- /dev/null +++ b/unity/Assets/Scenes/Main.unity @@ -0,0 +1,170 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.15, g: 0.45, b: 0.65, a: 1} + m_FogMode: 2 + m_FogDensity: 0.02 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.15, g: 0.25, b: 0.35, a: 1} + m_AmbientEquatorColor: {r: 0.1, g: 0.15, b: 0.2, a: 1} + m_AmbientGroundColor: {r: 0.05, g: 0.05, b: 0.08, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.48, b: 0.63, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &100000 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 100001} + - component: {fileID: 100002} + m_Layer: 0 + m_Name: GameBootstrap + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &100001 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100000} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &100002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100000} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 39195e201e0a6c696951c81ffb90cc7b, type: 3} + m_Name: + m_EditorClassIdentifier: + EnableIntroMission: 0 diff --git a/unity/Assets/Scenes/Main.unity.meta b/unity/Assets/Scenes/Main.unity.meta new file mode 100644 index 0000000..d1d03fa --- /dev/null +++ b/unity/Assets/Scenes/Main.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7a1b2c3d4e5f67890123456789abcdef +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts.meta b/unity/Assets/Scripts.meta new file mode 100644 index 0000000..a3d6a1f --- /dev/null +++ b/unity/Assets/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a83f1ca5ad00b39773d9e6a26b0e70b2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Boss.meta b/unity/Assets/Scripts/Boss.meta new file mode 100644 index 0000000..273f38a --- /dev/null +++ b/unity/Assets/Scripts/Boss.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1b25401848d99841510fc47af649fd6e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Boss/BossController.cs b/unity/Assets/Scripts/Boss/BossController.cs new file mode 100644 index 0000000..3e7154d --- /dev/null +++ b/unity/Assets/Scripts/Boss/BossController.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace TooFishy +{ + public class BossController : MonoBehaviour + { + public static bool IsDefeated { get; private set; } + public static bool HasSpawned { get; private set; } + + public int MaxHealth = 100; + public int Health { get; private set; } + + float _bobTime; + Vector3 _origin; + Transform _player; + + public static void TrySpawn(Transform worldRoot) + { + if (HasSpawned || IsDefeated) return; + var gs = GameState.Instance; + if (gs == null || gs.MaxDepthReached <= 500) return; + + HasSpawned = true; + gs.BossEncountered = true; + + var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); + go.name = "Boss_Blobfish"; + go.tag = "Boss"; + go.transform.SetParent(worldRoot, false); + go.transform.position = new Vector3(-4f, -520f, -0.5f); + go.transform.localScale = new Vector3(6f, 5f, 4f); + + Object.Destroy(go.GetComponent()); + var col = go.AddComponent(); + col.radius = 0.6f; + + var rb = go.AddComponent(); + rb.isKinematic = true; + rb.useGravity = false; + + var mat = new Material(Shader.Find("Standard")); + mat.color = new Color(1f, 0.55f, 0.6f); + mat.EnableKeyword("_EMISSION"); + mat.SetColor("_EmissionColor", new Color(0.5f, 0.15f, 0.2f)); + go.GetComponent().material = mat; + + // Eyes + for (int i = 0; i < 2; i++) + { + var eye = GameObject.CreatePrimitive(PrimitiveType.Sphere); + eye.transform.SetParent(go.transform, false); + eye.transform.localPosition = new Vector3(i == 0 ? -0.25f : 0.25f, 0.2f, -0.45f); + eye.transform.localScale = new Vector3(0.15f, 0.2f, 0.1f); + Object.Destroy(eye.GetComponent()); + var em = new Material(Shader.Find("Standard")); + em.color = Color.black; + eye.GetComponent().material = em; + } + + var boss = go.AddComponent(); + boss.Health = boss.MaxHealth; + boss._origin = go.transform.position; + } + + void Start() + { + _player = GameState.Instance?.PlayerTransform; + } + + void Update() + { + _bobTime += Time.deltaTime; + transform.position = _origin + new Vector3(Mathf.Sin(_bobTime * 0.5f) * 2f, Mathf.Sin(_bobTime) * 0.5f, 0f); + + if (_player != null) + { + float dist = Vector3.Distance(transform.position, _player.position); + if (dist < 4f) + { + var pc = _player.GetComponent(); + pc?.Hurt(2); + } + } + } + + public void TakeDamage(int amount) + { + Health = Mathf.Max(0, Health - amount); + PopupText.Show($"-{amount}", transform.position + Vector3.up * 3f); + transform.localScale *= 0.98f; + + if (GameState.Instance.IsIntro() && Health <= MaxHealth / 2) + { + GameState.Instance.CompleteIntroMission(transform.position); + Destroy(gameObject); + return; + } + + if (Health <= 0) + { + IsDefeated = true; + GameState.Instance.BossEncountered = true; + PopupText.Show("BOSS DEFEATED!", transform.position); + GameState.Instance.Money += 500; + Destroy(gameObject); + } + } + + public static void ResetFlags() + { + IsDefeated = false; + HasSpawned = false; + } + } +} diff --git a/unity/Assets/Scripts/Boss/BossController.cs.meta b/unity/Assets/Scripts/Boss/BossController.cs.meta new file mode 100644 index 0000000..244d038 --- /dev/null +++ b/unity/Assets/Scripts/Boss/BossController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: afadd49d6600e93fce69359e5bebd854 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Core.meta b/unity/Assets/Scripts/Core.meta new file mode 100644 index 0000000..8512a3b --- /dev/null +++ b/unity/Assets/Scripts/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6f79c9a99734909531d7cade329feddb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Core/GameBootstrap.cs b/unity/Assets/Scripts/Core/GameBootstrap.cs new file mode 100644 index 0000000..5360427 --- /dev/null +++ b/unity/Assets/Scripts/Core/GameBootstrap.cs @@ -0,0 +1,182 @@ +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace TooFishy +{ + /// + /// Builds the entire playable game at runtime so the project works + /// without hand-authored prefabs. Attach to an empty GameObject in Main. + /// + public class GameBootstrap : MonoBehaviour + { + [Tooltip("If true, start as the friend intro dive in the Hot zone.")] + public bool EnableIntroMission = false; + + void Awake() + { + // Ensure tags exist at runtime for built player (editor has TagManager) + EnsureTags(); + + var gsGo = new GameObject("GameState"); + var gs = gsGo.AddComponent(); + gs.EnableIntroMission = EnableIntroMission; + + var world = new GameObject("World").transform; + + BuildLighting(); + var player = BuildPlayer(world); + gs.Player = player; + gs.PlayerTransform = player.transform; + + var levelGo = new GameObject("Level"); + levelGo.transform.SetParent(world, false); + var level = levelGo.AddComponent(); + level.Initialize(world); + + BuildUI(); + + var camFx = player.GetComponentInChildren(); + if (camFx != null) + camFx.gameObject.AddComponent(); + + // Boss watcher + var watcher = new GameObject("BossWatcher").AddComponent(); + watcher.WorldRoot = world; + } + + void EnsureTags() + { + // Tags must be defined in TagManager; runtime Create doesn't add them. + // Fish/Boss detection also uses GetComponent fallbacks. + } + + void BuildLighting() + { + var lightGo = new GameObject("Sun"); + var light = lightGo.AddComponent(); + light.type = LightType.Directional; + light.color = new Color(0.7f, 0.85f, 1f); + light.intensity = 0.85f; + lightGo.transform.rotation = Quaternion.Euler(40f, -30f, 0f); + RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat; + RenderSettings.ambientLight = new Color(0.15f, 0.25f, 0.35f); + } + + PlayerController BuildPlayer(Transform world) + { + var go = new GameObject("Player"); + go.transform.SetParent(world, false); + go.transform.position = new Vector3(-8f, 0f, 0.33f); + go.tag = "Player"; + + var cc = go.AddComponent(); + cc.height = 1.2f; + cc.radius = 0.5f; + cc.center = Vector3.zero; + + var pivot = new GameObject("Pivot").transform; + pivot.SetParent(go.transform, false); + + // Submarine body (capsule) + var body = GameObject.CreatePrimitive(PrimitiveType.Capsule); + body.name = "Hull"; + body.transform.SetParent(pivot, false); + body.transform.localRotation = Quaternion.Euler(0f, 0f, 90f); + body.transform.localScale = new Vector3(0.7f, 1.1f, 0.7f); + Object.Destroy(body.GetComponent()); + var bm = new Material(Shader.Find("Standard")); + bm.color = new Color(0.85f, 0.55f, 0.15f); + bm.SetFloat("_Metallic", 0.4f); + body.GetComponent().material = bm; + + // Conning tower + var tower = GameObject.CreatePrimitive(PrimitiveType.Cube); + tower.name = "Tower"; + tower.transform.SetParent(pivot, false); + tower.transform.localPosition = new Vector3(0f, 0.45f, 0f); + tower.transform.localScale = new Vector3(0.4f, 0.35f, 0.4f); + Object.Destroy(tower.GetComponent()); + tower.GetComponent().material = bm; + + // Propeller + var prop = GameObject.CreatePrimitive(PrimitiveType.Cylinder); + prop.name = "Prop"; + prop.transform.SetParent(pivot, false); + prop.transform.localPosition = new Vector3(-1.0f, 0f, 0f); + prop.transform.localRotation = Quaternion.Euler(0f, 0f, 90f); + prop.transform.localScale = new Vector3(0.35f, 0.08f, 0.35f); + Object.Destroy(prop.GetComponent()); + var pm = new Material(Shader.Find("Standard")); + pm.color = new Color(0.3f, 0.3f, 0.35f); + prop.GetComponent().material = pm; + + var launch = new GameObject("HarpoonLaunchPoint").transform; + launch.SetParent(pivot, false); + launch.localPosition = new Vector3(1.1f, 0f, 0f); + + // Camera + var camGo = new GameObject("Camera"); + camGo.transform.SetParent(go.transform, false); + camGo.transform.localPosition = new Vector3(0f, 1.19f, 5.29f); + camGo.transform.localRotation = Quaternion.Euler(8f, 180f, 0f); + var cam = camGo.AddComponent(); + cam.clearFlags = CameraClearFlags.SolidColor; + cam.backgroundColor = new Color(0.35f, 0.65f, 0.9f); + cam.fieldOfView = 75f; + cam.nearClipPlane = 0.1f; + cam.farClipPlane = 200f; + camGo.AddComponent(); + cam.tag = "MainCamera"; + + // Lamp (optional upgrade visual) + var lamp = new GameObject("Lamp"); + lamp.transform.SetParent(pivot, false); + lamp.transform.localPosition = new Vector3(0.9f, 0.1f, 0f); + var spot = lamp.AddComponent(); + spot.type = LightType.Spot; + spot.range = 25f; + spot.spotAngle = 55f; + spot.intensity = 0f; + spot.color = new Color(1f, 0.95f, 0.8f); + + var player = go.AddComponent(); + go.AddComponent(); + return player; + } + + void BuildUI() + { + var canvasGo = new GameObject("Canvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); + var canvas = canvasGo.GetComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + var scaler = canvasGo.GetComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920, 1080); + + if (FindObjectOfType() == null) + { + var es = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule)); + } + + GameHUD.Create(canvasGo.transform); + } + } + + public class PlayerLamp : MonoBehaviour + { + Light _spot; + void Start() => _spot = GetComponentInChildren(); + void Update() + { + if (_spot == null || GameState.Instance == null) return; + _spot.intensity = GameState.Instance.GetUpgradeLevel(Upgrade.LampUnlocked) > 0 ? 2.5f : 0f; + } + } + + public class BossWatcher : MonoBehaviour + { + public Transform WorldRoot; + void Update() => BossController.TrySpawn(WorldRoot); + } +} diff --git a/unity/Assets/Scripts/Core/GameBootstrap.cs.meta b/unity/Assets/Scripts/Core/GameBootstrap.cs.meta new file mode 100644 index 0000000..d14588a --- /dev/null +++ b/unity/Assets/Scripts/Core/GameBootstrap.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39195e201e0a6c696951c81ffb90cc7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Core/GameEnums.cs b/unity/Assets/Scripts/Core/GameEnums.cs new file mode 100644 index 0000000..432d21b --- /dev/null +++ b/unity/Assets/Scripts/Core/GameEnums.cs @@ -0,0 +1,47 @@ +namespace TooFishy +{ + public enum Stage + { + Surface = 0, + Deep = 1, + Deeper = 2, + SuperDeep = 3, + Hot = 4, + Lava = 5, + Void = 6 + } + + public enum Upgrade + { + CargoSize, + DepthResistance, + PickaxeUnlocked, + VertSpeed, + HorSpeed, + LampUnlocked, + Ak47, + DualAk47, + Harpoon, + HarpoonRotation, + InventoryManagement, + SurfaceBuoy, + InventorySave, + DroneSelling + } + + public enum FishType + { + Flamy, + Greeny, + Angler, + Smally, + Spikey, + BossMini + } + + public enum GameMode + { + Normal, + IntroMission + } +} diff --git a/unity/Assets/Scripts/Core/GameEnums.cs.meta b/unity/Assets/Scripts/Core/GameEnums.cs.meta new file mode 100644 index 0000000..b47e116 --- /dev/null +++ b/unity/Assets/Scripts/Core/GameEnums.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0e45f35c18709905b8fa2605b8924801 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Core/GameState.cs b/unity/Assets/Scripts/Core/GameState.cs new file mode 100644 index 0000000..adb444a --- /dev/null +++ b/unity/Assets/Scripts/Core/GameState.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace TooFishy +{ + /// + /// Central game state singleton — port of Godot GameState autoload. + /// + public class GameState : MonoBehaviour + { + public static GameState Instance { get; private set; } + + public static readonly Dictionary DepthStageMap = new() + { + { 0, Stage.Surface }, + { 100, Stage.Deep }, + { 200, Stage.Deeper }, + { 300, Stage.SuperDeep }, + { 400, Stage.Hot }, + { 500, Stage.Lava }, + { 600, Stage.Void } + }; + + public static readonly Dictionary UpgradeCosts = new() + { + { Upgrade.CargoSize, 25 }, + { Upgrade.DepthResistance, 50 }, + { Upgrade.PickaxeUnlocked, 200 }, + { Upgrade.VertSpeed, 25 }, + { Upgrade.HorSpeed, 25 }, + { Upgrade.LampUnlocked, 50 }, + { Upgrade.Ak47, 500 }, + { Upgrade.DualAk47, 5000 }, + { Upgrade.Harpoon, 100 }, + { Upgrade.HarpoonRotation, 150 }, + { Upgrade.InventoryManagement, 400 }, + { Upgrade.SurfaceBuoy, 1000 }, + { Upgrade.InventorySave, 250 }, + { Upgrade.DroneSelling, 300 } + }; + + public static readonly Dictionary MaxUpgrades = new() + { + { Upgrade.CargoSize, 5 }, + { Upgrade.DepthResistance, 5 }, + { Upgrade.PickaxeUnlocked, 1 }, + { Upgrade.VertSpeed, 2 }, + { Upgrade.HorSpeed, 3 }, + { Upgrade.LampUnlocked, 1 }, + { Upgrade.Ak47, 1 }, + { Upgrade.DualAk47, 1 }, + { Upgrade.Harpoon, 1 }, + { Upgrade.HarpoonRotation, 1 }, + { Upgrade.InventoryManagement, 1 }, + { Upgrade.SurfaceBuoy, 1 }, + { Upgrade.InventorySave, 1 }, + { Upgrade.DroneSelling, 1 } + }; + + public Dictionary Upgrades { get; private set; } = new(); + + public int Depth { get; private set; } + public int MaxDepthReached { get; private set; } + public int Money { get; set; } = 25; + public float Health { get; set; } = 100f; + public float Headroom { get; private set; } + public bool IsDocked { get; set; } + public bool Paused { get; set; } + public bool DeathScreen { get; set; } + public bool GodMode { get; set; } + public Stage PlayerInStage { get; private set; } = Stage.Surface; + public float FishesLowerBorder { get; set; } = -27f; + + public GameMode CurrentGameMode { get; private set; } = GameMode.Normal; + public bool IntroMissionCompleted { get; private set; } + public bool BossEncountered { get; set; } + public bool EnableIntroMission = false; // Start in normal mode for Unity port by default + + public Inventory Inventory { get; private set; } = new(); + public Transform PlayerTransform { get; set; } + public PlayerController Player { get; set; } + + public event Action OnInventoryUpdated; + public event Action OnMoneyChanged; + public event Action OnHealthChanged; + public event Action OnDepthChanged; + public event Action OnUpgradesChanged; + public event Action OnDeath; + public event Action OnRespawn; + + void Awake() + { + if (Instance != null && Instance != this) + { + Destroy(gameObject); + return; + } + Instance = this; + DontDestroyOnLoad(gameObject); + ResetUpgrades(); + Inventory.Bind(this); + } + + void Start() + { + if (EnableIntroMission && !IntroMissionCompleted) + StartIntroMission(); + else + StartNormalMode(); + } + + public void NotifyInventoryUpdated() => OnInventoryUpdated?.Invoke(); + + public void SetDepth(int d) + { + Depth = d; + if (MaxDepthReached < d) MaxDepthReached = d; + + if (IsIntro()) + { + PlayerInStage = Stage.Hot; + OnDepthChanged?.Invoke(); + return; + } + + int band = (d / 100) * 100; // 0-99 Surface, 100-199 Deep, ... + Stage newStage = Stage.Surface; + foreach (var kv in DepthStageMap) + { + if (band >= kv.Key) + newStage = kv.Value; + } + PlayerInStage = newStage; + OnDepthChanged?.Invoke(); + } + + public int GetUpgradeCost(Upgrade upgrade) => + (Upgrades[upgrade] + 1) * UpgradeCosts[upgrade]; + + public bool TryUpgrade(Upgrade upgrade) + { + int cost = GetUpgradeCost(upgrade); + if (Money >= cost && Upgrades[upgrade] < MaxUpgrades[upgrade]) + { + Money -= cost; + Upgrades[upgrade]++; + OnMoneyChanged?.Invoke(); + OnUpgradesChanged?.Invoke(); + return true; + } + return false; + } + + public int GetUpgradeLevel(Upgrade upgrade) => Upgrades[upgrade]; + + public void ResetUpgrades() + { + Upgrades = new Dictionary(); + foreach (Upgrade u in Enum.GetValues(typeof(Upgrade))) + Upgrades[u] = 0; + } + + public bool IsIntro() => CurrentGameMode == GameMode.IntroMission; + + public void StartNormalMode() + { + CurrentGameMode = GameMode.Normal; + IntroMissionCompleted = true; + BossEncountered = false; + DeathScreen = false; + Paused = false; + Health = 100f; + IsDocked = false; + PlayerInStage = Stage.Surface; + Depth = 0; + MaxDepthReached = 0; + Money = 25; + ResetUpgrades(); + Inventory.Clear(); + Time.timeScale = 1f; + + if (Player != null) + Player.Teleport(new Vector3(-8f, 0f, 0.33f)); + } + + public void StartIntroMission() + { + CurrentGameMode = GameMode.IntroMission; + IntroMissionCompleted = false; + BossEncountered = false; + DeathScreen = false; + Paused = false; + Health = 100f; + IsDocked = false; + PlayerInStage = Stage.Hot; + Depth = 450; + Money = 1000; + SetupFriendUpgrades(); + Time.timeScale = 1f; + + if (Player != null) + Player.Teleport(new Vector3(-8f, -450f, 0.33f)); + } + + void SetupFriendUpgrades() + { + foreach (Upgrade u in Enum.GetValues(typeof(Upgrade))) + { + if (u == Upgrade.PickaxeUnlocked || u == Upgrade.SurfaceBuoy) + Upgrades[u] = 0; + else + Upgrades[u] = MaxUpgrades[u]; + } + } + + public void CompleteIntroMission(Vector3 deathPosition) + { + IntroMissionCompleted = true; + ResetUpgrades(); + Money = 25; + MaxDepthReached = 0; + CurrentGameMode = GameMode.Normal; + PlayerInStage = Stage.Surface; + Depth = 0; + BossEncountered = false; + Inventory.Clear(); + if (Player != null) + Player.Teleport(new Vector3(-8f, 0f, 0.33f)); + } + + public void ApplyPressureDamage(float dt) + { + if (GodMode || IsDocked) return; + Headroom = (GetUpgradeLevel(Upgrade.DepthResistance) + 1) * 100f - Depth; + if (Headroom < 0f) + { + float dps = Mathf.Max(1f, Mathf.Abs(Headroom) / 10f); + Damage(dps * dt); + } + } + + public void Damage(float amount) + { + if (GodMode || DeathScreen) return; + Health = Mathf.Max(0f, Health - amount); + OnHealthChanged?.Invoke(); + if (Health <= 0f) + Die(); + } + + public void Heal(float amount) + { + Health = Mathf.Min(100f, Health + amount); + OnHealthChanged?.Invoke(); + } + + public void Die() + { + if (DeathScreen) return; + DeathScreen = true; + Paused = true; + + if (GetUpgradeLevel(Upgrade.InventorySave) < 1) + Inventory.Clear(); + + OnDeath?.Invoke(); + } + + public void Respawn() + { + DeathScreen = false; + Paused = false; + Health = 100f; + IsDocked = false; + Time.timeScale = 1f; + if (Player != null) + Player.Teleport(new Vector3(-8f, 0f, 0.33f)); + OnRespawn?.Invoke(); + } + + public static string StageName(Stage stage) => stage switch + { + Stage.Surface => "Shallow Waters", + Stage.Deep => "Medium Waters", + Stage.Deeper => "Deep Waters", + Stage.SuperDeep => "Dark Deep Waters", + Stage.Hot => "Hot-Zone", + Stage.Lava => "Lava", + Stage.Void => "THE VOID", + _ => stage.ToString() + }; + + public static string UpgradeName(Upgrade upgrade) => upgrade switch + { + Upgrade.CargoSize => "Cargo Size", + Upgrade.DepthResistance => "Depth Resistance", + Upgrade.PickaxeUnlocked => "Pickaxe", + Upgrade.VertSpeed => "Vertical Speed", + Upgrade.HorSpeed => "Horizontal Speed", + Upgrade.LampUnlocked => "Lamp", + Upgrade.Ak47 => "AK-47", + Upgrade.DualAk47 => "Dual AK-47", + Upgrade.Harpoon => "Harpoon Pierce", + Upgrade.HarpoonRotation => "Harpoon Aim", + Upgrade.InventoryManagement => "Smart Inventory", + Upgrade.SurfaceBuoy => "Surface Buoy", + Upgrade.InventorySave => "Inventory Save", + Upgrade.DroneSelling => "Selling Drone", + _ => upgrade.ToString() + }; + } +} diff --git a/unity/Assets/Scripts/Core/GameState.cs.meta b/unity/Assets/Scripts/Core/GameState.cs.meta new file mode 100644 index 0000000..b6ebe0e --- /dev/null +++ b/unity/Assets/Scripts/Core/GameState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85b7a91ba801f43b33dcfc609fcfab90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Core/UnderwaterCamera.cs b/unity/Assets/Scripts/Core/UnderwaterCamera.cs new file mode 100644 index 0000000..14638f5 --- /dev/null +++ b/unity/Assets/Scripts/Core/UnderwaterCamera.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace TooFishy +{ + public class UnderwaterCamera : MonoBehaviour + { + Stage _lastStage = (Stage)(-1); + + void LateUpdate() + { + var gs = GameState.Instance; + if (gs == null) return; + + bool underwater = gs.PlayerTransform != null && gs.PlayerTransform.position.y <= -0.2f; + RenderSettings.fog = underwater; + if (!underwater) return; + + if (_lastStage != gs.PlayerInStage) + { + _lastStage = gs.PlayerInStage; + RenderSettings.fogColor = FishConfig.StageFogColor(_lastStage); + RenderSettings.fogDensity = FishConfig.StageFogDensity(_lastStage); + RenderSettings.fogMode = FogMode.ExponentialSquared; + } + } + } +} diff --git a/unity/Assets/Scripts/Core/UnderwaterCamera.cs.meta b/unity/Assets/Scripts/Core/UnderwaterCamera.cs.meta new file mode 100644 index 0000000..f09e195 --- /dev/null +++ b/unity/Assets/Scripts/Core/UnderwaterCamera.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9817688a05eba89dd2e0a0a47353eb1e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Fish.meta b/unity/Assets/Scripts/Fish.meta new file mode 100644 index 0000000..02991ad --- /dev/null +++ b/unity/Assets/Scripts/Fish.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b80db228beb4f318857d9d3ae9b2f8c3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Fish/FishBehaviour.cs b/unity/Assets/Scripts/Fish/FishBehaviour.cs new file mode 100644 index 0000000..50c3255 --- /dev/null +++ b/unity/Assets/Scripts/Fish/FishBehaviour.cs @@ -0,0 +1,159 @@ +using UnityEngine; + +namespace TooFishy +{ + public class FishBehaviour : MonoBehaviour + { + public FishType Type { get; private set; } + public float Weight { get; private set; } + public int Price { get; private set; } + public bool IsShiny { get; private set; } + + float _speed; + float _minAngle = -30f, _maxAngle = 30f; + float _rotationCd; + Vector3 _velocity; + int _id; + static int _nextId = 1; + + public static FishBehaviour Spawn(Vector3 pos, FishType type, Stage stage, Transform parent) + { + var stats = FishConfig.Stats[type]; + var section = FishConfig.Sections[stage]; + + if (stats.RequiresBossDefeat && !GameState.Instance.BossEncountered && !BossController.IsDefeated) + return null; + if (GameState.Instance.Depth < stats.MinRequiredDepth && stats.MinRequiredDepth > 0) + return null; + + var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); + go.name = $"Fish_{type}"; + go.tag = "Fish"; + go.transform.SetParent(parent, true); + go.transform.position = pos; + go.transform.localScale = stats.Scale; + + var col = go.GetComponent(); + col.isTrigger = false; + + var rb = go.AddComponent(); + rb.isKinematic = true; + rb.useGravity = false; + + bool shiny = Random.value < section.ShinyRate; + float weight = Mathf.Clamp( + Random.Range(stats.WeightMin, stats.WeightMax) * section.WeightMultiplier, + stats.WeightMin, stats.WeightMax); + int price = Mathf.RoundToInt(weight * stats.PriceWeightMultiplier); + if (shiny) price *= 3; + + float t = (weight - stats.WeightMin) / Mathf.Max(0.01f, (stats.WeightMax - stats.WeightMin) / 2f); + float scaleMul = 1f + t * 0.3f; + var s = stats.Scale; + go.transform.localScale = new Vector3(s.x * scaleMul, s.y * scaleMul, s.z); + + var mat = new Material(Shader.Find("Standard")); + mat.color = shiny ? Color.Lerp(stats.Color, Color.white, 0.55f) : stats.Color; + if (shiny) + { + mat.EnableKeyword("_EMISSION"); + mat.SetColor("_EmissionColor", stats.Color * 0.8f); + } + go.GetComponent().material = mat; + + // Simple fin + var fin = GameObject.CreatePrimitive(PrimitiveType.Cube); + fin.name = "Fin"; + fin.transform.SetParent(go.transform, false); + fin.transform.localPosition = new Vector3(-0.6f, 0f, 0f); + fin.transform.localScale = new Vector3(0.4f, 0.5f, 0.15f); + Object.Destroy(fin.GetComponent()); + fin.GetComponent().material = mat; + + var fish = go.AddComponent(); + fish.Type = type; + fish.Weight = weight; + fish.Price = price; + fish.IsShiny = shiny; + fish._speed = Random.Range(stats.SpeedMin, stats.SpeedMax); + fish._id = _nextId++; + fish.SetAngle(Random.Range(fish._minAngle, fish._maxAngle)); + return fish; + } + + void Update() + { + float dt = Time.deltaTime; + if (_rotationCd > 0f) _rotationCd -= dt; + + transform.position += _velocity * dt; + var p = transform.position; + p.z = -0.3f; + transform.position = p; + + if (p.y >= -0.5f) + { + Destroy(gameObject); + return; + } + + float lower = GameState.Instance != null ? GameState.Instance.FishesLowerBorder : -30f; + if (p.y >= -0.75f && _velocity.y > 0f) + SetAngle(Random.Range(_minAngle, _minAngle / 2f)); + else if (p.y <= lower && _velocity.y < 0f) + SetAngle(Random.Range(_maxAngle / 2f, _maxAngle)); + else if (Random.value < 0.004f) + SetAngle(Random.Range(_minAngle, _maxAngle)); + + // Wall bounce (simple bounds) + if ((p.x < -14f && _velocity.x < 0f) || (p.x > 6f && _velocity.x > 0f)) + { + if (_rotationCd <= 0f) + { + _rotationCd = 0.1f; + Flip(); + SetAngle(Random.Range(_minAngle, _maxAngle)); + } + } + + // Scatter from player + var player = GameState.Instance?.PlayerTransform; + if (player != null) + { + float dist = Vector3.Distance(transform.position, player.position); + if (dist < 3f && Random.value < 0.02f) + Scatter(player); + } + } + + void SetAngle(float deg) + { + float rad = deg * Mathf.Deg2Rad; + float facing = transform.localScale.x >= 0f ? 1f : -1f; + // Fish local +X is forward after scale flip; velocity along swim direction + Vector3 dir = new Vector3(Mathf.Cos(rad) * facing, Mathf.Sin(rad), 0f).normalized; + _velocity = dir * _speed; + transform.rotation = Quaternion.Euler(0f, 0f, deg * facing); + } + + void Flip() + { + var s = transform.localScale; + s.x = -s.x; + transform.localScale = s; + } + + public void Scatter(Transform from) + { + Vector3 away = (transform.position - from.position).normalized; + away.z = 0f; + if (away.sqrMagnitude < 0.01f) away = Vector3.right; + _velocity = away * (_speed * 2f); + if ((away.x > 0f) != (transform.localScale.x > 0f)) + Flip(); + } + + public InventoryItem ToInventoryItem() => + new InventoryItem(Type, Weight, Price, _id, IsShiny); + } +} diff --git a/unity/Assets/Scripts/Fish/FishBehaviour.cs.meta b/unity/Assets/Scripts/Fish/FishBehaviour.cs.meta new file mode 100644 index 0000000..928a875 --- /dev/null +++ b/unity/Assets/Scripts/Fish/FishBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 347a7630fe5c8a396b14d8347a34b98f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Fish/FishConfig.cs b/unity/Assets/Scripts/Fish/FishConfig.cs new file mode 100644 index 0000000..61f7bb7 --- /dev/null +++ b/unity/Assets/Scripts/Fish/FishConfig.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace TooFishy +{ + public class FishStats + { + public float WeightMin, WeightMax; + public float PriceWeightMultiplier; + public float SpeedMin, SpeedMax; + public int Difficulty; + public bool RequiresBossDefeat; + public int MinRequiredDepth; + public int MaxActivePerSection = 99; + public int MaxActiveGlobal = 99; + public float SpawnCooldownSec; + public Color Color; + public Vector3 Scale = Vector3.one; + } + + public class StageSpawnConfig + { + public int MaxFishAmount; + public float ShinyRate; + public float WeightMultiplier; + public Dictionary SpawnRates; + } + + public static class FishConfig + { + public static readonly Dictionary Stats = new() + { + { FishType.Flamy, new FishStats { + WeightMin = 1, WeightMax = 10, PriceWeightMultiplier = 1f, + SpeedMin = 1f, SpeedMax = 2.5f, Difficulty = 1, + Color = new Color(1f, 0.45f, 0.15f), Scale = new Vector3(0.6f, 0.35f, 0.25f) + }}, + { FishType.Greeny, new FishStats { + WeightMin = 1, WeightMax = 5, PriceWeightMultiplier = 1.2f, + SpeedMin = 2f, SpeedMax = 5f, Difficulty = 1, + Color = new Color(0.2f, 0.85f, 0.35f), Scale = new Vector3(0.45f, 0.28f, 0.2f) + }}, + { FishType.Angler, new FishStats { + WeightMin = 5, WeightMax = 10, PriceWeightMultiplier = 3f, + SpeedMin = 0.1f, SpeedMax = 1f, Difficulty = 5, + Color = new Color(0.55f, 0.35f, 0.75f), Scale = new Vector3(0.9f, 0.55f, 0.4f) + }}, + { FishType.Smally, new FishStats { + WeightMin = 5, WeightMax = 10, PriceWeightMultiplier = 10f, + SpeedMin = 3f, SpeedMax = 7f, Difficulty = 10, + Color = new Color(1f, 0.85f, 0.2f), Scale = new Vector3(0.3f, 0.2f, 0.15f) + }}, + { FishType.Spikey, new FishStats { + WeightMin = 5, WeightMax = 10, PriceWeightMultiplier = 3f, + SpeedMin = 0.5f, SpeedMax = 1.5f, Difficulty = 5, + Color = new Color(0.85f, 0.2f, 0.25f), Scale = new Vector3(0.7f, 0.45f, 0.35f) + }}, + { FishType.BossMini, new FishStats { + WeightMin = 3, WeightMax = 8, PriceWeightMultiplier = 11f, + SpeedMin = 1.3f, SpeedMax = 2.5f, Difficulty = 10, + RequiresBossDefeat = true, MinRequiredDepth = 520, + MaxActivePerSection = 1, MaxActiveGlobal = 2, SpawnCooldownSec = 55f, + Color = new Color(1f, 0.55f, 0.65f), Scale = new Vector3(0.8f, 0.7f, 0.5f) + }} + }; + + public static readonly Dictionary Sections = new() + { + { Stage.Surface, new StageSpawnConfig { + MaxFishAmount = 15, ShinyRate = 0.02f, WeightMultiplier = 0.8f, + SpawnRates = new() { { FishType.Flamy, 0.9f }, { FishType.Greeny, 0.1f } } + }}, + { Stage.Deep, new StageSpawnConfig { + MaxFishAmount = 10, ShinyRate = 0.02f, WeightMultiplier = 0.9f, + SpawnRates = new() { { FishType.Flamy, 0.7f }, { FishType.Greeny, 0.3f } } + }}, + { Stage.Deeper, new StageSpawnConfig { + MaxFishAmount = 10, ShinyRate = 0.04f, WeightMultiplier = 1f, + SpawnRates = new() { { FishType.Flamy, 0.5f }, { FishType.Greeny, 0.4f }, { FishType.Spikey, 0.1f } } + }}, + { Stage.SuperDeep, new StageSpawnConfig { + MaxFishAmount = 8, ShinyRate = 0.04f, WeightMultiplier = 1.1f, + SpawnRates = new() { + { FishType.Flamy, 0.2f }, { FishType.Greeny, 0.43f }, + { FishType.Angler, 0.14f }, { FishType.Smally, 0.03f }, { FishType.Spikey, 0.2f } + } + }}, + { Stage.Hot, new StageSpawnConfig { + MaxFishAmount = 5, ShinyRate = 0.05f, WeightMultiplier = 1.15f, + SpawnRates = new() { + { FishType.Greeny, 0.2f }, { FishType.Angler, 0.5f }, + { FishType.Smally, 0.1f }, { FishType.Spikey, 0.2f } + } + }}, + { Stage.Lava, new StageSpawnConfig { + MaxFishAmount = 4, ShinyRate = 0.06f, WeightMultiplier = 1.15f, + SpawnRates = new() { + { FishType.Angler, 0.53f }, { FishType.Smally, 0.24f }, + { FishType.Spikey, 0.18f }, { FishType.BossMini, 0.05f } + } + }}, + { Stage.Void, new StageSpawnConfig { + MaxFishAmount = 1, ShinyRate = 0.08f, WeightMultiplier = 1.2f, + SpawnRates = new() { + { FishType.Smally, 0.1f }, { FishType.Angler, 0.1f }, { FishType.Flamy, 0.8f } + } + }} + }; + + public static FishType PickType(Stage stage) + { + var cfg = Sections[stage]; + float roll = Random.value; + float acc = 0f; + foreach (var kv in cfg.SpawnRates) + { + acc += kv.Value; + if (roll <= acc) return kv.Key; + } + return FishType.Flamy; + } + + public static Color StageFogColor(Stage stage) => stage switch + { + Stage.Surface => new Color(0.15f, 0.45f, 0.65f), + Stage.Deep => new Color(0.08f, 0.28f, 0.48f), + Stage.Deeper => new Color(0.04f, 0.15f, 0.32f), + Stage.SuperDeep => new Color(0.02f, 0.05f, 0.15f), + Stage.Hot => new Color(0.35f, 0.12f, 0.05f), + Stage.Lava => new Color(0.45f, 0.05f, 0.02f), + Stage.Void => new Color(0.02f, 0.0f, 0.05f), + _ => Color.black + }; + + public static float StageFogDensity(Stage stage) => stage switch + { + Stage.Surface => 0.02f, + Stage.Deep => 0.035f, + Stage.Deeper => 0.05f, + Stage.SuperDeep => 0.07f, + Stage.Hot => 0.055f, + Stage.Lava => 0.06f, + Stage.Void => 0.09f, + _ => 0.03f + }; + } +} diff --git a/unity/Assets/Scripts/Fish/FishConfig.cs.meta b/unity/Assets/Scripts/Fish/FishConfig.cs.meta new file mode 100644 index 0000000..ed07810 --- /dev/null +++ b/unity/Assets/Scripts/Fish/FishConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5677431fdd1858230b813cd732b7b41 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Inventory.meta b/unity/Assets/Scripts/Inventory.meta new file mode 100644 index 0000000..7a5eeb0 --- /dev/null +++ b/unity/Assets/Scripts/Inventory.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4eb6e05b47113181967987f47a55babc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Inventory/Inventory.cs b/unity/Assets/Scripts/Inventory/Inventory.cs new file mode 100644 index 0000000..f844999 --- /dev/null +++ b/unity/Assets/Scripts/Inventory/Inventory.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace TooFishy +{ + public class Inventory + { + public List Items { get; private set; } = new(); + public int FishesCaught { get; private set; } + public float TotalWeight { get; private set; } + public int TotalValue { get; private set; } + + GameState _state; + + public void Bind(GameState state) => _state = state; + + public int GetMaxWeight() + { + int cargoLevel = _state.GetUpgradeLevel(Upgrade.CargoSize) + 1; + if (cargoLevel <= 4) + return 25 * cargoLevel; + return 100 + 50 * (cargoLevel - 4); + } + + public bool Add(InventoryItem item) + { + if (Items.Any(i => i.Id == item.Id)) + return false; + + if (item.Weight + TotalWeight > GetMaxWeight()) + { + if (_state.GetUpgradeLevel(Upgrade.InventoryManagement) > 0) + return TryReplaceLessValuable(item); + return false; + } + + Items.Add(item); + UpdateTotals(); + _state?.NotifyInventoryUpdated(); + return true; + } + + bool TryReplaceLessValuable(InventoryItem newFish) + { + if (Items.Count == 0) return false; + + var sorted = Items.OrderBy(i => i.Price).ToList(); + foreach (var item in sorted) + { + if (item.Price >= newFish.Price) continue; + if (item.Weight >= newFish.Weight) + { + Items.Remove(item); + Items.Add(newFish); + UpdateTotals(); + _state?.NotifyInventoryUpdated(); + return true; + } + } + + float spaceNeeded = newFish.Weight - (GetMaxWeight() - TotalWeight); + if (spaceNeeded <= 0f) + { + Items.Add(newFish); + UpdateTotals(); + _state?.NotifyInventoryUpdated(); + return true; + } + + var byRatio = Items.OrderBy(i => i.Price / Mathf.Max(0.01f, i.Weight)).ToList(); + var toRemove = new List(); + float weightRemoved = 0f; + int valueRemoved = 0; + + foreach (var item in byRatio) + { + if (item.Price > newFish.Price) continue; + toRemove.Add(item); + weightRemoved += item.Weight; + valueRemoved += item.Price; + if (weightRemoved >= spaceNeeded) break; + } + + if (weightRemoved < spaceNeeded || valueRemoved >= newFish.Price) + return false; + + foreach (var item in toRemove) + Items.Remove(item); + + Items.Add(newFish); + UpdateTotals(); + _state?.NotifyInventoryUpdated(); + return true; + } + + public int SellItems() + { + int sold = TotalValue; + Items.Clear(); + UpdateTotals(); + if (_state != null) + { + _state.Money += sold; + _state.NotifyInventoryUpdated(); + } + return sold; + } + + public void Clear() + { + Items.Clear(); + UpdateTotals(); + _state?.NotifyInventoryUpdated(); + } + + void UpdateTotals() + { + int value = 0; + float weight = 0f; + foreach (var item in Items) + { + value += item.Price; + weight += item.Weight; + } + TotalValue = value; + TotalWeight = weight; + FishesCaught = Items.Count; + } + } +} diff --git a/unity/Assets/Scripts/Inventory/Inventory.cs.meta b/unity/Assets/Scripts/Inventory/Inventory.cs.meta new file mode 100644 index 0000000..868e753 --- /dev/null +++ b/unity/Assets/Scripts/Inventory/Inventory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66a8387bcfaaa7d58ceeaa32713be28f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Inventory/InventoryItem.cs b/unity/Assets/Scripts/Inventory/InventoryItem.cs new file mode 100644 index 0000000..0587e12 --- /dev/null +++ b/unity/Assets/Scripts/Inventory/InventoryItem.cs @@ -0,0 +1,21 @@ +namespace TooFishy +{ + [System.Serializable] + public class InventoryItem + { + public FishType Type; + public float Weight; + public int Price; + public int Id; + public bool Shiny; + + public InventoryItem(FishType type, float weight, int price, int id, bool shiny) + { + Type = type; + Weight = weight; + Price = price; + Id = id; + Shiny = shiny; + } + } +} diff --git a/unity/Assets/Scripts/Inventory/InventoryItem.cs.meta b/unity/Assets/Scripts/Inventory/InventoryItem.cs.meta new file mode 100644 index 0000000..3160344 --- /dev/null +++ b/unity/Assets/Scripts/Inventory/InventoryItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df72cee9f91dcc2972748a771805b1a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Items.meta b/unity/Assets/Scripts/Items.meta new file mode 100644 index 0000000..28ac9d1 --- /dev/null +++ b/unity/Assets/Scripts/Items.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6e0591cf3fa05ed49214c05341a0645c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Items/Harpoon.cs b/unity/Assets/Scripts/Items/Harpoon.cs new file mode 100644 index 0000000..cb862e6 --- /dev/null +++ b/unity/Assets/Scripts/Items/Harpoon.cs @@ -0,0 +1,82 @@ +using UnityEngine; + +namespace TooFishy +{ + public class Harpoon : MonoBehaviour + { + public float Speed = 10f; + public float Lifetime = 3f; + public float MaxDistance = 10f; + + PlayerController _owner; + Vector3 _dir; + Vector3 _start; + float _age; + bool _piercing; + + public static Harpoon Spawn(Vector3 pos, Vector3 dir, PlayerController owner) + { + var go = GameObject.CreatePrimitive(PrimitiveType.Capsule); + go.name = "Harpoon"; + go.transform.position = pos; + go.transform.localScale = new Vector3(0.08f, 0.35f, 0.08f); + go.transform.rotation = Quaternion.LookRotation(Vector3.forward, dir); + + Object.Destroy(go.GetComponent()); + var col = go.AddComponent(); + col.isTrigger = true; + col.radius = 0.6f; + + var rb = go.AddComponent(); + rb.isKinematic = true; + rb.useGravity = false; + + var mat = new Material(Shader.Find("Standard")); + mat.color = new Color(0.75f, 0.55f, 0.25f); + mat.SetFloat("_Metallic", 0.6f); + go.GetComponent().material = mat; + + var h = go.AddComponent(); + h._owner = owner; + h._dir = dir.normalized; + h._start = owner.transform.position; + h._piercing = GameState.Instance.GetUpgradeLevel(Upgrade.Harpoon) >= 1; + return h; + } + + void Update() + { + float dt = Time.deltaTime; + transform.position += _dir * Speed * dt; + _age += dt; + + if (_age >= Lifetime || Vector3.Distance(transform.position, _start) > MaxDistance) + { + Destroy(gameObject); + return; + } + } + + void OnTriggerEnter(Collider other) + { + if (other.CompareTag("Fish") || other.GetComponent() != null) + { + var fish = other.GetComponent(); + if (fish != null && _owner != null) + _owner.CatchFish(fish); + if (!_piercing) + { + Destroy(gameObject); + return; + } + } + + if (other.CompareTag("Boss") || other.GetComponent() != null) + { + var boss = other.GetComponent(); + boss?.TakeDamage(10); + Destroy(gameObject); + } + } + } +} diff --git a/unity/Assets/Scripts/Items/Harpoon.cs.meta b/unity/Assets/Scripts/Items/Harpoon.cs.meta new file mode 100644 index 0000000..8d9542e --- /dev/null +++ b/unity/Assets/Scripts/Items/Harpoon.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f25e25be0b50517f804cf0bca8a66a6c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Level.meta b/unity/Assets/Scripts/Level.meta new file mode 100644 index 0000000..58c585e --- /dev/null +++ b/unity/Assets/Scripts/Level.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 861abef11b19668cb6cde879f3bdc5f2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Level/DestroyableBarrier.cs b/unity/Assets/Scripts/Level/DestroyableBarrier.cs new file mode 100644 index 0000000..84fed1f --- /dev/null +++ b/unity/Assets/Scripts/Level/DestroyableBarrier.cs @@ -0,0 +1,58 @@ +using UnityEngine; + +namespace TooFishy +{ + public class DestroyableBarrier : MonoBehaviour + { + public int Health = 3; + int _max; + Renderer[] _renderers; + + public static DestroyableBarrier Create(Transform parent, float y, int hp) + { + var root = new GameObject("Barrier"); + root.transform.SetParent(parent, false); + root.transform.position = new Vector3(-4f, y + 10f, 0f); + root.tag = "Barrier"; + + var barrier = root.AddComponent(); + barrier.Health = hp; + barrier._max = hp; + + var mats = new System.Collections.Generic.List(); + for (int i = 0; i < 9; i++) + { + var box = GameObject.CreatePrimitive(PrimitiveType.Cube); + box.name = $"Block_{i}"; + box.transform.SetParent(root.transform, false); + int row = i / 3; + int col = i % 3; + box.transform.localPosition = new Vector3((col - 1) * 2.2f, (row - 1) * 2.2f, 0f); + box.transform.localScale = new Vector3(2f, 2f, 1.5f); + var mat = new Material(Shader.Find("Standard")); + mat.color = new Color(0.4f, 0.35f, 0.3f); + box.GetComponent().material = mat; + mats.Add(box.GetComponent()); + } + barrier._renderers = mats.ToArray(); + return barrier; + } + + public void TakeDamage(int amount) + { + Health -= amount; + float t = 1f - (float)Health / _max; + foreach (var r in _renderers) + { + if (r != null) + r.material.color = Color.Lerp(new Color(0.4f, 0.35f, 0.3f), new Color(0.8f, 0.2f, 0.1f), t); + } + PopupText.Show("!", transform.position); + if (Health <= 0) + { + PopupText.Show("Barrier destroyed!", transform.position); + Destroy(gameObject); + } + } + } +} diff --git a/unity/Assets/Scripts/Level/DestroyableBarrier.cs.meta b/unity/Assets/Scripts/Level/DestroyableBarrier.cs.meta new file mode 100644 index 0000000..e3509f4 --- /dev/null +++ b/unity/Assets/Scripts/Level/DestroyableBarrier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf1b0f2f49708dec0a7313aacf4a8b38 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Level/LavaZone.cs b/unity/Assets/Scripts/Level/LavaZone.cs new file mode 100644 index 0000000..31a6af7 --- /dev/null +++ b/unity/Assets/Scripts/Level/LavaZone.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +namespace TooFishy +{ + public class LavaZone : MonoBehaviour + { + void OnTriggerStay(Collider other) + { + var player = other.GetComponent(); + if (player == null) return; + GameState.Instance.Damage(10f * Time.deltaTime); + player.AddTrauma(0.05f); + } + } +} diff --git a/unity/Assets/Scripts/Level/LavaZone.cs.meta b/unity/Assets/Scripts/Level/LavaZone.cs.meta new file mode 100644 index 0000000..8c716d9 --- /dev/null +++ b/unity/Assets/Scripts/Level/LavaZone.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ed632f0e23f47b2a1bc8044594645b33 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Level/LevelGenerator.cs b/unity/Assets/Scripts/Level/LevelGenerator.cs new file mode 100644 index 0000000..893d580 --- /dev/null +++ b/unity/Assets/Scripts/Level/LevelGenerator.cs @@ -0,0 +1,212 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace TooFishy +{ + public class LevelGenerator : MonoBehaviour + { + public float SectionHeight = 25f; + public int Preload = 2; + + float _lastSpawned = -10f; + readonly List _sections = new(); + Transform _fishRoot; + Transform _worldRoot; + + public void Initialize(Transform worldRoot) + { + _worldRoot = worldRoot; + _fishRoot = new GameObject("FishRoot").transform; + _fishRoot.SetParent(worldRoot, false); + + BuildSurfaceDock(); + BuildSideWalls(); + // Initial sections + SpawnSection(-10.5f, Stage.Surface, false); + _lastSpawned = -35f; + SpawnSection(_lastSpawned, Stage.Surface, false); + for (int i = 0; i < Preload; i++) + TrySpawnAhead(-999f); + } + + void Update() + { + var player = GameState.Instance?.PlayerTransform; + if (player == null) return; + TrySpawnAhead(player.position.y); + + // Cull far sections + float py = player.position.y; + for (int i = _sections.Count - 1; i >= 0; i--) + { + if (_sections[i] == null) { _sections.RemoveAt(i); continue; } + if (_sections[i].transform.position.y > py + 80f) + { + Destroy(_sections[i]); + _sections.RemoveAt(i); + } + } + + GameState.Instance.FishesLowerBorder = _lastSpawned - SectionHeight / 2f - 1f; + } + + void TrySpawnAhead(float playerY) + { + while (playerY < _lastSpawned + SectionHeight) + { + _lastSpawned -= SectionHeight; + int depth = Mathf.Max(0, Mathf.RoundToInt(-_lastSpawned)); + int band = (depth / 100) * 100; + Stage stage = Stage.Surface; + foreach (var kv in GameState.DepthStageMap) + if (band >= kv.Key) stage = kv.Value; + + bool barrier = IsStageTransition(_lastSpawned + SectionHeight, _lastSpawned); + SpawnSection(_lastSpawned, stage, barrier); + } + } + + bool IsStageTransition(float prevY, float newY) + { + int d0 = (Mathf.Max(0, Mathf.RoundToInt(-prevY)) / 100) * 100; + int d1 = (Mathf.Max(0, Mathf.RoundToInt(-newY)) / 100) * 100; + return d0 != d1 && d1 > 0; + } + + void SpawnSection(float y, Stage stage, bool withBarrier) + { + var section = new GameObject($"Section_{stage}_{y:F0}"); + section.transform.SetParent(_worldRoot, false); + section.transform.position = new Vector3(0f, y, 0f); + _sections.Add(section); + + // Background panel + var bg = GameObject.CreatePrimitive(PrimitiveType.Quad); + bg.name = "Background"; + bg.transform.SetParent(section.transform, false); + bg.transform.localPosition = new Vector3(-4f, 0f, -8f); + bg.transform.localScale = new Vector3(40f, SectionHeight + 2f, 1f); + Object.Destroy(bg.GetComponent()); + var bgMat = new Material(Shader.Find("Standard")); + var fog = FishConfig.StageFogColor(stage); + bgMat.color = Color.Lerp(fog, Color.black, 0.4f); + bgMat.SetFloat("_Glossiness", 0.1f); + bg.GetComponent().material = bgMat; + + // Decorative rocks + int rocks = Random.Range(2, 5); + for (int i = 0; i < rocks; i++) + { + var rock = GameObject.CreatePrimitive(PrimitiveType.Cube); + rock.name = "Rock"; + rock.transform.SetParent(section.transform, false); + float side = Random.value > 0.5f ? -12f : 4f; + rock.transform.localPosition = new Vector3(side + Random.Range(-1.5f, 1.5f), Random.Range(-SectionHeight / 2f, SectionHeight / 2f), -1f); + rock.transform.localScale = new Vector3(Random.Range(1f, 3f), Random.Range(1f, 4f), Random.Range(1f, 2f)); + rock.transform.rotation = Quaternion.Euler(Random.Range(0, 30), Random.Range(0, 360), Random.Range(0, 30)); + var rm = new Material(Shader.Find("Standard")); + rm.color = stage >= Stage.Hot + ? new Color(0.35f, 0.15f, 0.1f) + : new Color(0.25f, 0.28f, 0.32f); + rock.GetComponent().material = rm; + } + + if (withBarrier && !GameState.Instance.IsIntro()) + { + int hp = 2 + (int)stage; + DestroyableBarrier.Create(section.transform, y, hp); + } + + if (stage >= Stage.Lava) + { + var lava = GameObject.CreatePrimitive(PrimitiveType.Cube); + lava.name = "Lava"; + lava.tag = "Lava"; + lava.transform.SetParent(section.transform, false); + lava.transform.localPosition = new Vector3(-4f, -SectionHeight / 2f + 1f, 0f); + lava.transform.localScale = new Vector3(20f, 2f, 2f); + Object.Destroy(lava.GetComponent()); + var trigger = lava.AddComponent(); + trigger.isTrigger = true; + trigger.size = Vector3.one; + var lm = new Material(Shader.Find("Standard")); + lm.color = new Color(1f, 0.25f, 0.05f); + lm.EnableKeyword("_EMISSION"); + lm.SetColor("_EmissionColor", new Color(2f, 0.4f, 0.05f)); + lava.GetComponent().material = lm; + lava.AddComponent(); + } + + SpawnFishInSection(section.transform, y, stage); + } + + void SpawnFishInSection(Transform section, float y, Stage stage) + { + var cfg = FishConfig.Sections[stage]; + int count = Random.Range(cfg.MaxFishAmount / 2, cfg.MaxFishAmount + 1); + for (int i = 0; i < count; i++) + { + var type = FishConfig.PickType(stage); + var pos = new Vector3( + Random.Range(-11f, 3f), + y + Random.Range(-SectionHeight / 2f + 1f, SectionHeight / 2f - 1f), + -0.3f); + FishBehaviour.Spawn(pos, type, stage, _fishRoot); + } + } + + void BuildSurfaceDock() + { + var dock = GameObject.CreatePrimitive(PrimitiveType.Cube); + dock.name = "Dock"; + dock.tag = "Dock"; + dock.transform.SetParent(_worldRoot, false); + dock.transform.position = new Vector3(-2f, 0.4f, 0f); + dock.transform.localScale = new Vector3(8f, 0.4f, 3f); + var mat = new Material(Shader.Find("Standard")); + mat.color = new Color(0.45f, 0.3f, 0.15f); + dock.GetComponent().material = mat; + + // Surface water plane + var water = GameObject.CreatePrimitive(PrimitiveType.Quad); + water.name = "SurfaceWater"; + water.transform.SetParent(_worldRoot, false); + water.transform.position = new Vector3(-4f, 0f, -2f); + water.transform.rotation = Quaternion.Euler(90f, 0f, 0f); + water.transform.localScale = new Vector3(40f, 20f, 1f); + Object.Destroy(water.GetComponent()); + var wm = new Material(Shader.Find("Standard")); + wm.color = new Color(0.2f, 0.55f, 0.8f, 0.5f); + wm.SetFloat("_Mode", 3); + wm.SetFloat("_Glossiness", 0.9f); + water.GetComponent().material = wm; + + // Sky box above surface + var sky = GameObject.CreatePrimitive(PrimitiveType.Quad); + sky.name = "Sky"; + sky.transform.SetParent(_worldRoot, false); + sky.transform.position = new Vector3(-4f, 8f, -8f); + sky.transform.localScale = new Vector3(40f, 16f, 1f); + Object.Destroy(sky.GetComponent()); + var sm = new Material(Shader.Find("Standard")); + sm.color = new Color(0.45f, 0.7f, 0.95f); + sky.GetComponent().material = sm; + } + + void BuildSideWalls() + { + // Tall walls so player can't swim too far sideways + foreach (var x in new[] { -15f, 7f }) + { + var wall = GameObject.CreatePrimitive(PrimitiveType.Cube); + wall.name = "SideWall"; + wall.transform.SetParent(_worldRoot, false); + wall.transform.position = new Vector3(x, -300f, 0f); + wall.transform.localScale = new Vector3(2f, 700f, 4f); + var mat = new Material(Shader.Find("Standard")); + mat.color = new Color(0.15f, 0.18f, 0.22f); + wall.GetComponent().material = mat; + } + } + } +} diff --git a/unity/Assets/Scripts/Level/LevelGenerator.cs.meta b/unity/Assets/Scripts/Level/LevelGenerator.cs.meta new file mode 100644 index 0000000..1251b04 --- /dev/null +++ b/unity/Assets/Scripts/Level/LevelGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 011faf4887dff3f5c9f06162d94e2e12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Player.meta b/unity/Assets/Scripts/Player.meta new file mode 100644 index 0000000..8ccacc2 --- /dev/null +++ b/unity/Assets/Scripts/Player.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4d667afd35f0d63904cb281b7d8a7970 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/Player/PlayerController.cs b/unity/Assets/Scripts/Player/PlayerController.cs new file mode 100644 index 0000000..9a1e834 --- /dev/null +++ b/unity/Assets/Scripts/Player/PlayerController.cs @@ -0,0 +1,296 @@ +using UnityEngine; + +namespace TooFishy +{ + [RequireComponent(typeof(CharacterController))] + public class PlayerController : MonoBehaviour + { + public const float HarpoonCooldown = 1f; + public const float BuoyCooldown = 8f; + public const float DroneCooldown = 5f; + + [SerializeField] float speedHorizontal = 0.5f; + [SerializeField] float speedVertical = 0.5f; + + CharacterController _cc; + Transform _pivot; + Transform _launchPoint; + Camera _cam; + + float _velX, _velY; + float _accelX = 2.5f, _decelX = 3.8f, _maxSpeedX = 5f; + float _accelY = 2.2f, _decelY = 3.0f, _maxSpeedY = 4f; + + float _harpoonCd, _buoyCd, _droneCd; + bool _facingRight = true; + bool _canBeHurt = true; + float _trauma; + bool _wasDocked; + Vector3 _externalForces; + + public float Trauma => _trauma; + public Transform Pivot => _pivot; + public bool FacingRight => _facingRight; + public float HarpoonCdRemaining => _harpoonCd; + public float BuoyCdRemaining => _buoyCd; + public float DroneCdRemaining => _droneCd; + + void Awake() + { + _cc = GetComponent(); + _pivot = transform.Find("Pivot"); + if (_pivot == null) + { + var go = new GameObject("Pivot"); + go.transform.SetParent(transform, false); + _pivot = go.transform; + } + _launchPoint = _pivot.Find("HarpoonLaunchPoint"); + if (_launchPoint == null) + { + var lp = new GameObject("HarpoonLaunchPoint"); + lp.transform.SetParent(_pivot, false); + lp.transform.localPosition = new Vector3(0.8f, 0f, 0f); + _launchPoint = lp.transform; + } + _cam = GetComponentInChildren(); + } + + void Start() + { + var gs = GameState.Instance; + if (gs != null) + { + gs.Player = this; + gs.PlayerTransform = transform; + } + } + + void Update() + { + var gs = GameState.Instance; + if (gs == null || gs.Paused || gs.DeathScreen) return; + + TickCooldowns(Time.deltaTime); + HandleMovement(Time.deltaTime); + HandleActions(); + UpdateDepthAndDock(Time.deltaTime); + gs.ApplyPressureDamage(Time.deltaTime); + DecayTrauma(Time.deltaTime); + ApplyCameraShake(); + } + + void TickCooldowns(float dt) + { + if (_harpoonCd > 0f) _harpoonCd -= dt; + if (_buoyCd > 0f) _buoyCd -= dt; + if (_droneCd > 0f) _droneCd -= dt; + } + + void HandleMovement(float dt) + { + float inputX = 0f, inputY = 0f; + if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) inputX = 1f; + else if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) inputX = -1f; + + if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow)) + { + inputY = 1f; + if (transform.position.y >= -0.2f) inputY = 0f; + } + else if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow)) + inputY = -1f; + else if (transform.position.y >= -0.2f) + inputY = -0.3f; // auto-sink at surface when idle + + if (inputX != 0f) + { + _velX = Mathf.MoveTowards(_velX, inputX * _maxSpeedX, _accelX * dt); + SetFacing(inputX > 0f); + } + else + _velX = Mathf.MoveTowards(_velX, 0f, _decelX * dt); + + if (transform.position.y >= -0.2f && _velY > 0f) + _velY = 0f; + + if (inputY != 0f) + _velY = Mathf.MoveTowards(_velY, inputY * _maxSpeedY, _accelY * dt); + else + _velY = Mathf.MoveTowards(_velY, 0f, _decelY * dt); + + var gs = GameState.Instance; + float horBonus = speedHorizontal + gs.GetUpgradeLevel(Upgrade.HorSpeed) * 0.5f; + float vertBonus = speedVertical + gs.GetUpgradeLevel(Upgrade.VertSpeed) * 0.3f; + + float vx = _velX * horBonus; + float vy = _velY * vertBonus; + if (vy > 0f) vy *= 1.2f; + if (transform.position.y >= -0.2f && inputY < 0f) vy *= 2f; + + _externalForces = Vector3.Lerp(_externalForces, Vector3.zero, 5f * dt); + var move = new Vector3(vx, vy, 0f) + _externalForces; + _cc.Move(move * dt); + + // Lock Z + var p = transform.position; + p.z = 0.33f; + if (p.y > 0.5f) p.y = 0.5f; + transform.position = p; + + // Subtle rocking + if (_pivot != null) + { + float rock = Mathf.Sin(Time.time * 2f) * 8f * Mathf.Clamp01(Mathf.Abs(_velX) / _maxSpeedX); + var e = _pivot.localEulerAngles; + e.z = rock; + _pivot.localEulerAngles = e; + } + } + + void SetFacing(bool right) + { + if (_facingRight == right) return; + _facingRight = right; + if (_pivot != null) + { + var s = _pivot.localScale; + s.x = Mathf.Abs(s.x) * (right ? 1f : -1f); + _pivot.localScale = s; + } + } + + void HandleActions() + { + var gs = GameState.Instance; + if (Input.GetMouseButtonDown(0) && _harpoonCd <= 0f) + ShootHarpoon(); + + if (Input.GetKeyDown(KeyCode.B) && gs.GetUpgradeLevel(Upgrade.SurfaceBuoy) > 0 && _buoyCd <= 0f) + { + Teleport(new Vector3(transform.position.x, -1f, 0.33f)); + _buoyCd = BuoyCooldown; + } + + if (Input.GetKeyDown(KeyCode.Q) && gs.GetUpgradeLevel(Upgrade.DroneSelling) > 0 && _droneCd <= 0f) + { + int sold = gs.Inventory.SellItems(); + if (sold > 0) PopupText.Show($"+${sold} (drone)", transform.position + Vector3.up); + _droneCd = DroneCooldown; + } + + if (Input.GetKeyDown(KeyCode.Space) && gs.GetUpgradeLevel(Upgrade.PickaxeUnlocked) > 0) + SwingPickaxe(); + } + + void ShootHarpoon() + { + _harpoonCd = HarpoonCooldown; + Vector3 dir; + var gs = GameState.Instance; + if (gs.GetUpgradeLevel(Upgrade.HarpoonRotation) > 0 && _cam != null) + { + var mouse = Input.mousePosition; + mouse.z = Mathf.Abs(_cam.transform.position.z - transform.position.z); + var world = _cam.ScreenToWorldPoint(mouse); + dir = (world - _launchPoint.position); + dir.z = 0f; + if (dir.sqrMagnitude < 0.01f) dir = _facingRight ? Vector3.right : Vector3.left; + dir.Normalize(); + } + else + dir = _facingRight ? Vector3.right : Vector3.left; + + Harpoon.Spawn(_launchPoint.position, dir, this); + } + + void SwingPickaxe() + { + var hits = Physics.OverlapSphere(transform.position + (_facingRight ? Vector3.right : Vector3.left) * 1.2f, 1.2f); + foreach (var h in hits) + { + var barrier = h.GetComponent(); + if (barrier != null) barrier.TakeDamage(1); + } + } + + void UpdateDepthAndDock(float dt) + { + var gs = GameState.Instance; + int depth = Mathf.Max(0, Mathf.RoundToInt(-transform.position.y)); + gs.SetDepth(depth); + + bool docked = transform.position.y >= -1f && transform.position.x > -7f && !gs.IsIntro(); + gs.IsDocked = docked; + if (docked) + { + gs.Heal(5f * dt); + if (!_wasDocked) + { + int sold = gs.Inventory.SellItems(); + if (sold > 0) PopupText.Show($"+${sold}", transform.position + Vector3.up * 1.5f); + } + } + _wasDocked = docked; + } + + public void CatchFish(FishBehaviour fish) + { + if (fish == null) return; + var item = fish.ToInventoryItem(); + float trauma = Mathf.Clamp(item.Weight / 50f, 0.2f, 0.6f); + if (item.Shiny) trauma *= 1.5f; + AddTrauma(trauma); + + bool added = GameState.Instance.Inventory.Add(item); + if (added) + PopupText.Show(item.Shiny ? $"★ {item.Price}$" : $"{item.Price}$", fish.transform.position); + else + PopupText.Show("Cargo full!", transform.position + Vector3.up); + + Destroy(fish.gameObject); + } + + public void Hurt(int damage) + { + if (!_canBeHurt) return; + AddTrauma(1f); + GameState.Instance.Damage(damage); + _canBeHurt = false; + Invoke(nameof(ResetHurt), 1f); + } + + void ResetHurt() => _canBeHurt = true; + + public void AddTrauma(float amount) => _trauma = Mathf.Clamp01(_trauma + amount); + void DecayTrauma(float dt) => _trauma = Mathf.Max(0f, _trauma - 1.7f * dt); + + void ApplyCameraShake() + { + if (_cam == null) return; + float shake = _trauma * _trauma * 0.1f; + var offset = Random.insideUnitSphere * shake; + offset.z = 0f; + _cam.transform.localPosition = new Vector3(0f, 1.19f, 5.29f) + offset; + } + + public void Teleport(Vector3 pos) + { + _cc.enabled = false; + transform.position = pos; + _cc.enabled = true; + _velX = _velY = 0f; + } + + public void AddExternalForce(Vector3 force) => _externalForces += force; + + void OnControllerColliderHit(ControllerColliderHit hit) + { + var fish = hit.collider.GetComponent(); + if (fish != null && fish.Type == FishType.Spikey) + Hurt(5); + else if (fish == null && Mathf.Abs(hit.normal.x) > 0.7f) + _velX = 0f; + } + } +} diff --git a/unity/Assets/Scripts/Player/PlayerController.cs.meta b/unity/Assets/Scripts/Player/PlayerController.cs.meta new file mode 100644 index 0000000..15e832d --- /dev/null +++ b/unity/Assets/Scripts/Player/PlayerController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cde693c432b2f2ee989a9b64594c6093 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/UI.meta b/unity/Assets/Scripts/UI.meta new file mode 100644 index 0000000..e5f52e8 --- /dev/null +++ b/unity/Assets/Scripts/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fe1d295fd340f4117734f5ec295a5786 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Assets/Scripts/UI/GameHUD.cs b/unity/Assets/Scripts/UI/GameHUD.cs new file mode 100644 index 0000000..fbdd750 --- /dev/null +++ b/unity/Assets/Scripts/UI/GameHUD.cs @@ -0,0 +1,267 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace TooFishy +{ + public class GameHUD : MonoBehaviour + { + Text _depth, _money, _health, _cargo, _stage, _hint; + Image _healthFill; + GameObject _upgradePanel; + GameObject _deathPanel; + bool _upgradesVisible; + + public static GameHUD Create(Transform canvasRoot) + { + var go = new GameObject("HUD"); + go.transform.SetParent(canvasRoot, false); + var hud = go.AddComponent(); + hud.Build(canvasRoot as RectTransform ?? canvasRoot.GetComponent()); + return hud; + } + + void Build(RectTransform canvas) + { + _depth = MakeLabel(canvas, "DepthText", new Vector2(20, -20), TextAnchor.UpperLeft, 28); + _money = MakeLabel(canvas, "MoneyText", new Vector2(20, -55), TextAnchor.UpperLeft, 28); + _cargo = MakeLabel(canvas, "CargoText", new Vector2(20, -90), TextAnchor.UpperLeft, 22); + _stage = MakeLabel(canvas, "StageText", new Vector2(20, -120), TextAnchor.UpperLeft, 20); + _hint = MakeLabel(canvas, "HintText", new Vector2(0, 30), TextAnchor.LowerCenter, 18); + _hint.alignment = TextAnchor.MiddleCenter; + _hint.text = "WASD/Arrows move · LMB harpoon · E upgrades at dock · Esc pause"; + + // Health bar + var barBg = MakePanel(canvas, "HealthBarBg", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), + new Vector2(-150, -30), new Vector2(300, 18), new Color(0.1f, 0.1f, 0.1f, 0.7f)); + var fillGo = new GameObject("Fill", typeof(RectTransform), typeof(Image)); + fillGo.transform.SetParent(barBg.transform, false); + var frt = fillGo.GetComponent(); + frt.anchorMin = Vector2.zero; + frt.anchorMax = Vector2.one; + frt.offsetMin = Vector2.zero; + frt.offsetMax = Vector2.zero; + _healthFill = fillGo.GetComponent(); + _healthFill.color = new Color(0.2f, 0.85f, 0.35f); + _health = MakeLabel(barBg.GetComponent(), "HealthText", new Vector2(0, 0), TextAnchor.MiddleCenter, 14); + _health.alignment = TextAnchor.MiddleCenter; + var hrt = _health.GetComponent(); + hrt.anchorMin = Vector2.zero; hrt.anchorMax = Vector2.one; + hrt.offsetMin = Vector2.zero; hrt.offsetMax = Vector2.zero; + + BuildUpgradePanel(canvas); + BuildDeathPanel(canvas); + + var gs = GameState.Instance; + if (gs != null) + { + gs.OnInventoryUpdated += Refresh; + gs.OnMoneyChanged += Refresh; + gs.OnHealthChanged += Refresh; + gs.OnDepthChanged += Refresh; + gs.OnUpgradesChanged += RefreshUpgrades; + gs.OnDeath += ShowDeath; + gs.OnRespawn += HideDeath; + } + Refresh(); + } + + void Update() + { + var gs = GameState.Instance; + if (gs == null) return; + + if (Input.GetKeyDown(KeyCode.E) || Input.GetKeyDown(KeyCode.Tab)) + { + if (gs.IsDocked || _upgradesVisible) + ToggleUpgrades(); + } + if (Input.GetKeyDown(KeyCode.Escape)) + { + gs.Paused = !gs.Paused; + Time.timeScale = gs.Paused ? 0f : 1f; + } + Refresh(); + } + + void Refresh() + { + var gs = GameState.Instance; + if (gs == null) return; + _depth.text = $"Depth: {gs.Depth}m"; + _money.text = $"${gs.Money}"; + _cargo.text = $"Cargo: {gs.Inventory.TotalWeight:F0}/{gs.Inventory.GetMaxWeight()} kg ({gs.Inventory.FishesCaught} fish, ${gs.Inventory.TotalValue})"; + _stage.text = GameState.StageName(gs.PlayerInStage); + if (gs.Headroom < 0) + _stage.text += $" ⚠ Pressure {-gs.Headroom:F0}m over"; + _health.text = $"{gs.Health:F0} HP"; + if (_healthFill != null) + { + _healthFill.rectTransform.anchorMax = new Vector2(Mathf.Clamp01(gs.Health / 100f), 1f); + _healthFill.color = Color.Lerp(new Color(0.9f, 0.15f, 0.1f), new Color(0.2f, 0.85f, 0.35f), gs.Health / 100f); + } + + if (_upgradePanel != null) + _upgradePanel.SetActive(_upgradesVisible && (gs.IsDocked || gs.Paused)); + } + + void ToggleUpgrades() + { + _upgradesVisible = !_upgradesVisible; + RefreshUpgrades(); + Refresh(); + } + + void BuildUpgradePanel(RectTransform canvas) + { + _upgradePanel = MakePanel(canvas, "UpgradePanel", new Vector2(1, 0.5f), new Vector2(1, 0.5f), + new Vector2(-420, -250), new Vector2(400, 500), new Color(0.05f, 0.1f, 0.18f, 0.92f)).gameObject; + _upgradePanel.SetActive(false); + + var title = MakeLabel(_upgradePanel.GetComponent(), "Title", new Vector2(20, -15), TextAnchor.UpperLeft, 24); + title.text = "Upgrades (dock)"; + + float y = -50f; + foreach (Upgrade u in System.Enum.GetValues(typeof(Upgrade))) + { + if (u == Upgrade.Ak47 || u == Upgrade.DualAk47) continue; // unlock later + CreateUpgradeButton(_upgradePanel.GetComponent(), u, ref y); + } + } + + void CreateUpgradeButton(RectTransform parent, Upgrade upgrade, ref float y) + { + var btnGo = MakePanel(parent, $"Up_{upgrade}", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), + new Vector2(-180, y), new Vector2(360, 32), new Color(0.12f, 0.22f, 0.35f, 1f)); + var label = MakeLabel(btnGo.GetComponent(), "L", new Vector2(10, 0), TextAnchor.MiddleLeft, 16); + label.alignment = TextAnchor.MiddleLeft; + var lrt = label.GetComponent(); + lrt.anchorMin = new Vector2(0, 0); lrt.anchorMax = new Vector2(1, 1); + lrt.offsetMin = new Vector2(10, 0); lrt.offsetMax = new Vector2(-10, 0); + label.text = GameState.UpgradeName(upgrade); + + var btn = btnGo.gameObject.AddComponent