From 404d15a56cafdff1aa5eb3d554012767e4f4278a Mon Sep 17 00:00:00 2001 From: Sven Mandera Date: Mon, 10 Aug 2026 19:23:04 +0200 Subject: [PATCH] initial --- .claude/settings.local.json | 14 + .gitattributes | 8 + .gitignore | 23 + ARCHITECTURE.md | 38 + Assets/DefaultNetworkPrefabs.asset | 3 + Assets/DefaultNetworkPrefabs.asset.meta | 8 + Assets/InputSystem_Actions.inputactions | 1057 +++++++++++++++++ Assets/InputSystem_Actions.inputactions.meta | 14 + Assets/Readme.asset | 3 + Assets/Readme.asset.meta | 8 + Assets/Scenes.meta | 8 + Assets/Scenes/SampleScene.unity | 432 +++++++ Assets/Scenes/SampleScene.unity.meta | 7 + Assets/Scripts.meta | 8 + Assets/Scripts/AfterHours.Runtime.asmdef | 12 + Assets/Scripts/AfterHours.Runtime.asmdef.meta | 7 + Assets/Scripts/Core.meta | 8 + Assets/Scripts/Core/GameClock.cs | 72 ++ Assets/Scripts/Core/GameClock.cs.meta | 2 + Assets/Scripts/Core/ShopEconomy.cs | 83 ++ Assets/Scripts/Core/ShopEconomy.cs.meta | 2 + Assets/Scripts/Customers.meta | 8 + Assets/Scripts/Customers/BasketLine.cs | 15 + Assets/Scripts/Customers/BasketLine.cs.meta | 2 + Assets/Scripts/Customers/ComfortModel.cs | 75 ++ Assets/Scripts/Customers/ComfortModel.cs.meta | 2 + Assets/Scripts/Customers/CustomerAgent.cs | 227 ++++ .../Scripts/Customers/CustomerAgent.cs.meta | 2 + Assets/Scripts/Customers/CustomerSpawner.cs | 85 ++ .../Scripts/Customers/CustomerSpawner.cs.meta | 2 + Assets/Scripts/Data.meta | 8 + Assets/Scripts/Data/Enums.cs | 31 + Assets/Scripts/Data/Enums.cs.meta | 2 + Assets/Scripts/Data/ProductCatalog.cs | 46 + Assets/Scripts/Data/ProductCatalog.cs.meta | 2 + Assets/Scripts/Data/ProductDefinition.cs | 45 + Assets/Scripts/Data/ProductDefinition.cs.meta | 2 + Assets/Scripts/Economy.meta | 8 + Assets/Scripts/Economy/PricingService.cs | 49 + Assets/Scripts/Economy/PricingService.cs.meta | 2 + Assets/Scripts/Economy/PurchaseOrder.cs | 42 + Assets/Scripts/Economy/PurchaseOrder.cs.meta | 2 + Assets/Scripts/Interaction.meta | 8 + Assets/Scripts/Interaction/IInteractable.cs | 18 + .../Scripts/Interaction/IInteractable.cs.meta | 2 + .../Scripts/Interaction/PlayerInteractor.cs | 69 ++ .../Interaction/PlayerInteractor.cs.meta | 2 + Assets/Scripts/Networking.meta | 8 + Assets/Scripts/Networking/NetworkBootstrap.cs | 49 + .../Networking/NetworkBootstrap.cs.meta | 2 + Assets/Scripts/Networking/PlayerAvatar.cs | 71 ++ .../Scripts/Networking/PlayerAvatar.cs.meta | 2 + Assets/Scripts/Networking/PlayerHands.cs | 57 + Assets/Scripts/Networking/PlayerHands.cs.meta | 2 + Assets/Scripts/Shop.meta | 8 + Assets/Scripts/Shop/CashRegister.cs | 102 ++ Assets/Scripts/Shop/CashRegister.cs.meta | 2 + Assets/Scripts/Shop/ShelfRegistry.cs | 22 + Assets/Scripts/Shop/ShelfRegistry.cs.meta | 2 + Assets/Scripts/Shop/ShelfSlot.cs | 112 ++ Assets/Scripts/Shop/ShelfSlot.cs.meta | 2 + Assets/Scripts/Shop/StorageCrate.cs | 49 + Assets/Scripts/Shop/StorageCrate.cs.meta | 2 + Assets/Scripts/Tests.meta | 8 + Assets/Scripts/Tests/EditMode.meta | 8 + .../Tests/EditMode/AfterHours.Tests.asmdef | 13 + .../EditMode/AfterHours.Tests.asmdef.meta | 7 + .../Tests/EditMode/ComfortModelTests.cs | 52 + .../Tests/EditMode/ComfortModelTests.cs.meta | 2 + .../Tests/EditMode/PricingServiceTests.cs | 55 + .../EditMode/PricingServiceTests.cs.meta | 2 + Assets/Settings.meta | 8 + Assets/Settings/DefaultVolumeProfile.asset | 3 + .../Settings/DefaultVolumeProfile.asset.meta | 8 + Assets/Settings/Mobile_RPAsset.asset | 3 + Assets/Settings/Mobile_RPAsset.asset.meta | 8 + Assets/Settings/Mobile_Renderer.asset | 3 + Assets/Settings/Mobile_Renderer.asset.meta | 8 + Assets/Settings/PC_RPAsset.asset | 3 + Assets/Settings/PC_RPAsset.asset.meta | 8 + Assets/Settings/PC_Renderer.asset | 3 + Assets/Settings/PC_Renderer.asset.meta | 8 + Assets/Settings/SampleSceneProfile.asset | 3 + Assets/Settings/SampleSceneProfile.asset.meta | 8 + ...niversalRenderPipelineGlobalSettings.asset | 3 + ...salRenderPipelineGlobalSettings.asset.meta | 8 + Assets/TutorialInfo.meta | 8 + Assets/TutorialInfo/Icons.meta | 9 + Assets/TutorialInfo/Icons/URP.png | 3 + Assets/TutorialInfo/Icons/URP.png.meta | 134 +++ Assets/TutorialInfo/Layout.wlt | 654 ++++++++++ Assets/TutorialInfo/Layout.wlt.meta | 8 + Assets/TutorialInfo/Scripts.meta | 9 + Assets/TutorialInfo/Scripts/Editor.meta | 9 + .../Scripts/Editor/ReadmeEditor.cs | 242 ++++ .../Scripts/Editor/ReadmeEditor.cs.meta | 12 + Assets/TutorialInfo/Scripts/Readme.cs | 16 + Assets/TutorialInfo/Scripts/Readme.cs.meta | 12 + CLAUDE.md | 143 +++ DESIGN.md | 36 + Packages/manifest.json | 50 + Packages/packages-lock.json | 492 ++++++++ ProjectSettings/AudioManager.asset | 3 + ProjectSettings/ClusterInputManager.asset | 3 + ProjectSettings/DynamicsManager.asset | 3 + ProjectSettings/EditorBuildSettings.asset | 3 + ProjectSettings/EditorSettings.asset | 3 + ProjectSettings/GraphicsSettings.asset | 3 + ProjectSettings/InputManager.asset | 3 + ProjectSettings/MemorySettings.asset | 3 + ProjectSettings/MultiplayerManager.asset | 3 + ProjectSettings/NavMeshAreas.asset | 3 + ProjectSettings/PackageManagerSettings.asset | 3 + ProjectSettings/Physics2DSettings.asset | 3 + .../PhysicsCoreProjectSettings2D.asset | 3 + ProjectSettings/PresetManager.asset | 3 + ProjectSettings/ProjectAuditorSettings.asset | 3 + ProjectSettings/ProjectSettings.asset | 3 + ProjectSettings/ProjectVersion.txt | 2 + ProjectSettings/QualitySettings.asset | 3 + ProjectSettings/ShaderGraphSettings.asset | 3 + ProjectSettings/TagManager.asset | 3 + ProjectSettings/TimeManager.asset | 3 + ProjectSettings/URPProjectSettings.asset | 3 + ProjectSettings/UnityConnectSettings.asset | 3 + ProjectSettings/VFXManager.asset | 3 + ProjectSettings/VersionControlSettings.asset | 3 + ProjectSettings/XRSettings.asset | 3 + README.md | 17 + ROADMAP.md | 33 + SixShopSimulator.slnx | 6 + docs/ARCHITECTURE.md | 38 + docs/DESIGN.md | 36 + docs/ROADMAP.md | 33 + 134 files changed, 5374 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 ARCHITECTURE.md create mode 100644 Assets/DefaultNetworkPrefabs.asset create mode 100644 Assets/DefaultNetworkPrefabs.asset.meta create mode 100644 Assets/InputSystem_Actions.inputactions create mode 100644 Assets/InputSystem_Actions.inputactions.meta create mode 100644 Assets/Readme.asset create mode 100644 Assets/Readme.asset.meta create mode 100644 Assets/Scenes.meta create mode 100644 Assets/Scenes/SampleScene.unity create mode 100644 Assets/Scenes/SampleScene.unity.meta create mode 100644 Assets/Scripts.meta create mode 100644 Assets/Scripts/AfterHours.Runtime.asmdef create mode 100644 Assets/Scripts/AfterHours.Runtime.asmdef.meta create mode 100644 Assets/Scripts/Core.meta create mode 100644 Assets/Scripts/Core/GameClock.cs create mode 100644 Assets/Scripts/Core/GameClock.cs.meta create mode 100644 Assets/Scripts/Core/ShopEconomy.cs create mode 100644 Assets/Scripts/Core/ShopEconomy.cs.meta create mode 100644 Assets/Scripts/Customers.meta create mode 100644 Assets/Scripts/Customers/BasketLine.cs create mode 100644 Assets/Scripts/Customers/BasketLine.cs.meta create mode 100644 Assets/Scripts/Customers/ComfortModel.cs create mode 100644 Assets/Scripts/Customers/ComfortModel.cs.meta create mode 100644 Assets/Scripts/Customers/CustomerAgent.cs create mode 100644 Assets/Scripts/Customers/CustomerAgent.cs.meta create mode 100644 Assets/Scripts/Customers/CustomerSpawner.cs create mode 100644 Assets/Scripts/Customers/CustomerSpawner.cs.meta create mode 100644 Assets/Scripts/Data.meta create mode 100644 Assets/Scripts/Data/Enums.cs create mode 100644 Assets/Scripts/Data/Enums.cs.meta create mode 100644 Assets/Scripts/Data/ProductCatalog.cs create mode 100644 Assets/Scripts/Data/ProductCatalog.cs.meta create mode 100644 Assets/Scripts/Data/ProductDefinition.cs create mode 100644 Assets/Scripts/Data/ProductDefinition.cs.meta create mode 100644 Assets/Scripts/Economy.meta create mode 100644 Assets/Scripts/Economy/PricingService.cs create mode 100644 Assets/Scripts/Economy/PricingService.cs.meta create mode 100644 Assets/Scripts/Economy/PurchaseOrder.cs create mode 100644 Assets/Scripts/Economy/PurchaseOrder.cs.meta create mode 100644 Assets/Scripts/Interaction.meta create mode 100644 Assets/Scripts/Interaction/IInteractable.cs create mode 100644 Assets/Scripts/Interaction/IInteractable.cs.meta create mode 100644 Assets/Scripts/Interaction/PlayerInteractor.cs create mode 100644 Assets/Scripts/Interaction/PlayerInteractor.cs.meta create mode 100644 Assets/Scripts/Networking.meta create mode 100644 Assets/Scripts/Networking/NetworkBootstrap.cs create mode 100644 Assets/Scripts/Networking/NetworkBootstrap.cs.meta create mode 100644 Assets/Scripts/Networking/PlayerAvatar.cs create mode 100644 Assets/Scripts/Networking/PlayerAvatar.cs.meta create mode 100644 Assets/Scripts/Networking/PlayerHands.cs create mode 100644 Assets/Scripts/Networking/PlayerHands.cs.meta create mode 100644 Assets/Scripts/Shop.meta create mode 100644 Assets/Scripts/Shop/CashRegister.cs create mode 100644 Assets/Scripts/Shop/CashRegister.cs.meta create mode 100644 Assets/Scripts/Shop/ShelfRegistry.cs create mode 100644 Assets/Scripts/Shop/ShelfRegistry.cs.meta create mode 100644 Assets/Scripts/Shop/ShelfSlot.cs create mode 100644 Assets/Scripts/Shop/ShelfSlot.cs.meta create mode 100644 Assets/Scripts/Shop/StorageCrate.cs create mode 100644 Assets/Scripts/Shop/StorageCrate.cs.meta create mode 100644 Assets/Scripts/Tests.meta create mode 100644 Assets/Scripts/Tests/EditMode.meta create mode 100644 Assets/Scripts/Tests/EditMode/AfterHours.Tests.asmdef create mode 100644 Assets/Scripts/Tests/EditMode/AfterHours.Tests.asmdef.meta create mode 100644 Assets/Scripts/Tests/EditMode/ComfortModelTests.cs create mode 100644 Assets/Scripts/Tests/EditMode/ComfortModelTests.cs.meta create mode 100644 Assets/Scripts/Tests/EditMode/PricingServiceTests.cs create mode 100644 Assets/Scripts/Tests/EditMode/PricingServiceTests.cs.meta create mode 100644 Assets/Settings.meta create mode 100644 Assets/Settings/DefaultVolumeProfile.asset create mode 100644 Assets/Settings/DefaultVolumeProfile.asset.meta create mode 100644 Assets/Settings/Mobile_RPAsset.asset create mode 100644 Assets/Settings/Mobile_RPAsset.asset.meta create mode 100644 Assets/Settings/Mobile_Renderer.asset create mode 100644 Assets/Settings/Mobile_Renderer.asset.meta create mode 100644 Assets/Settings/PC_RPAsset.asset create mode 100644 Assets/Settings/PC_RPAsset.asset.meta create mode 100644 Assets/Settings/PC_Renderer.asset create mode 100644 Assets/Settings/PC_Renderer.asset.meta create mode 100644 Assets/Settings/SampleSceneProfile.asset create mode 100644 Assets/Settings/SampleSceneProfile.asset.meta create mode 100644 Assets/Settings/UniversalRenderPipelineGlobalSettings.asset create mode 100644 Assets/Settings/UniversalRenderPipelineGlobalSettings.asset.meta create mode 100644 Assets/TutorialInfo.meta create mode 100644 Assets/TutorialInfo/Icons.meta create mode 100644 Assets/TutorialInfo/Icons/URP.png create mode 100644 Assets/TutorialInfo/Icons/URP.png.meta create mode 100644 Assets/TutorialInfo/Layout.wlt create mode 100644 Assets/TutorialInfo/Layout.wlt.meta create mode 100644 Assets/TutorialInfo/Scripts.meta create mode 100644 Assets/TutorialInfo/Scripts/Editor.meta create mode 100644 Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs create mode 100644 Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta create mode 100644 Assets/TutorialInfo/Scripts/Readme.cs create mode 100644 Assets/TutorialInfo/Scripts/Readme.cs.meta create mode 100644 CLAUDE.md create mode 100644 DESIGN.md create mode 100644 Packages/manifest.json create mode 100644 Packages/packages-lock.json create mode 100644 ProjectSettings/AudioManager.asset create mode 100644 ProjectSettings/ClusterInputManager.asset create mode 100644 ProjectSettings/DynamicsManager.asset create mode 100644 ProjectSettings/EditorBuildSettings.asset create mode 100644 ProjectSettings/EditorSettings.asset create mode 100644 ProjectSettings/GraphicsSettings.asset create mode 100644 ProjectSettings/InputManager.asset create mode 100644 ProjectSettings/MemorySettings.asset create mode 100644 ProjectSettings/MultiplayerManager.asset create mode 100644 ProjectSettings/NavMeshAreas.asset create mode 100644 ProjectSettings/PackageManagerSettings.asset create mode 100644 ProjectSettings/Physics2DSettings.asset create mode 100644 ProjectSettings/PhysicsCoreProjectSettings2D.asset create mode 100644 ProjectSettings/PresetManager.asset create mode 100644 ProjectSettings/ProjectAuditorSettings.asset create mode 100644 ProjectSettings/ProjectSettings.asset create mode 100644 ProjectSettings/ProjectVersion.txt create mode 100644 ProjectSettings/QualitySettings.asset create mode 100644 ProjectSettings/ShaderGraphSettings.asset create mode 100644 ProjectSettings/TagManager.asset create mode 100644 ProjectSettings/TimeManager.asset create mode 100644 ProjectSettings/URPProjectSettings.asset create mode 100644 ProjectSettings/UnityConnectSettings.asset create mode 100644 ProjectSettings/VFXManager.asset create mode 100644 ProjectSettings/VersionControlSettings.asset create mode 100644 ProjectSettings/XRSettings.asset create mode 100644 README.md create mode 100644 ROADMAP.md create mode 100644 SixShopSimulator.slnx create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DESIGN.md create mode 100644 docs/ROADMAP.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c84fb18 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(where.exe Unity.exe)", + "Read(//c/Program Files/Unity/Hub/Editor/**)", + "Read(//c/Program Files/Unity/**)", + "Bash(\"/c/Program Files/Unity/Hub/Editor/6000.5.7f1/Editor/Unity.exe\" -batchmode -nographics -quit -projectPath \"C:\\\\Arbeit\\\\Projekte\\\\spiele\\\\SixShopSimulator\" -logFile \"C:\\\\Arbeit\\\\Projekte\\\\spiele\\\\SixShopSimulator\\\\pkg-resolve.log\")", + "Bash(echo \"EXIT CODE: $?\")", + "Bash(curl -s \"https://packages.unity.com/com.unity.netcode.gameobjects\")", + "Bash(sort -u -t'\\(' -k1,1)", + "Bash(\"/c/Program Files/Unity/Hub/Editor/6000.5.7f1/Editor/Unity.exe\" -batchmode -nographics -runTests -projectPath \"C:\\\\Arbeit\\\\Projekte\\\\spiele\\\\SixShopSimulator\" -testPlatform EditMode -testResults \"C:\\\\Arbeit\\\\Projekte\\\\spiele\\\\SixShopSimulator\\\\test-results.xml\" -logFile \"C:\\\\Arbeit\\\\Projekte\\\\spiele\\\\SixShopSimulator\\\\unity-test.log\")" + ] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..772eec4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +*.cubemap filter=lfs diff=lfs merge=lfs -text +*.psd filter=lfs diff=lfs merge=lfs -text +*.fbx filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.asset filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea45535 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Bb]uild/ +[Bb]uilds/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +[Mm]emoryCaptures/ +*.csproj +*.sln +*.userprefs +*.pidb +*.booproj +*.svd +*.apk +*.aab +*.unitypackage +.vs/ +.idea/ +.vscode/ +.DS_Store +sysinfo.txt +crashlytics-build.properties diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..b1c253f --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,38 @@ +# Architektur + +## Autoritätsmodell + +``` +Client (Owner) Server (Host) +────────────── ───────────── +Bewegung ───── prediction ──▶ NetworkTransform (einzige Client-Autorität) +Raycast ───── ServerRpc ───▶ Distanz prüfen ──▶ IInteractable.Interact() +UI-Anzeige ◀─── NetworkVariable ─── Geld / Bestand / Preise +NPC-Optik ◀─── NetworkTransform ── NavMesh-Simulation (nur Server) +``` + +**Merksatz:** Wenn eine Variable Auswirkung auf Geld oder Bestand hat, darf sie +nur auf dem Server geschrieben werden. + +## Warum NPCs nicht jeden Frame ticken +`CustomerAgent` läuft mit `TickInterval = 0.25 s`. Bei 20–30 Kunden spart das +den Großteil der Server-CPU, ohne dass man es sieht — Laufwege macht ohnehin +der NavMeshAgent. Wer die KI reaktiver braucht, senkt das Intervall gezielt für +den Kunden an der Kasse, nicht global. + +## Netzwerk-Traffic +- Produkte: nur `int ProductId`, nie Referenzen oder Strings. +- `ShelfSlot`: 3 NetworkVariables (Produkt, Menge, Preis). Bei vielen Fächern + später zu einem gebündelten `NetworkList` pro Regal zusammenfassen. +- NPC-Zustand: ein Enum + ein Float für Sprechblasen. Mehr braucht der Client nicht. + +## Manuelles Netzwerk-Testen +1. `File > Build Settings > Build` (Development Build an) +2. Build starten → "Host" +3. Im Editor Play drücken → "Join" + +Testfälle, die regelmäßig brechen: +- Zwei Spieler greifen gleichzeitig aufs selbe Regalfach +- Client verlässt das Spiel, während er Ware trägt +- Kunde verlässt die Schlange, während ein Spieler gerade scannt +- Client mit hoher Latenz interagiert am Rand der Reichweite diff --git a/Assets/DefaultNetworkPrefabs.asset b/Assets/DefaultNetworkPrefabs.asset new file mode 100644 index 0000000..35c65af --- /dev/null +++ b/Assets/DefaultNetworkPrefabs.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a37fe3fe19e609f5006fbf971b12a0dcc306aad1298cff10ae61afde9047e8ec +size 491 diff --git a/Assets/DefaultNetworkPrefabs.asset.meta b/Assets/DefaultNetworkPrefabs.asset.meta new file mode 100644 index 0000000..3d527cd --- /dev/null +++ b/Assets/DefaultNetworkPrefabs.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7ec3fddb6e731cd4e8bb668040911e08 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/InputSystem_Actions.inputactions b/Assets/InputSystem_Actions.inputactions new file mode 100644 index 0000000..1a12cb9 --- /dev/null +++ b/Assets/InputSystem_Actions.inputactions @@ -0,0 +1,1057 @@ +{ + "name": "InputSystem_Actions", + "maps": [ + { + "name": "Player", + "id": "df70fa95-8a34-4494-b137-73ab6b9c7d37", + "actions": [ + { + "name": "Move", + "type": "Value", + "id": "351f2ccd-1f9f-44bf-9bec-d62ac5c5f408", + "expectedControlType": "Vector2", + "processors": "", + "interactions": "", + "initialStateCheck": true + }, + { + "name": "Look", + "type": "Value", + "id": "6b444451-8a00-4d00-a97e-f47457f736a8", + "expectedControlType": "Vector2", + "processors": "", + "interactions": "", + "initialStateCheck": true + }, + { + "name": "Attack", + "type": "Button", + "id": "6c2ab1b8-8984-453a-af3d-a3c78ae1679a", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "Interact", + "type": "Button", + "id": "852140f2-7766-474d-8707-702459ba45f3", + "expectedControlType": "Button", + "processors": "", + "interactions": "Hold", + "initialStateCheck": false + }, + { + "name": "Crouch", + "type": "Button", + "id": "27c5f898-bc57-4ee1-8800-db469aca5fe3", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "Jump", + "type": "Button", + "id": "f1ba0d36-48eb-4cd5-b651-1c94a6531f70", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "Previous", + "type": "Button", + "id": "2776c80d-3c14-4091-8c56-d04ced07a2b0", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "Next", + "type": "Button", + "id": "b7230bb6-fc9b-4f52-8b25-f5e19cb2c2ba", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "Sprint", + "type": "Button", + "id": "641cd816-40e6-41b4-8c3d-04687c349290", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + } + ], + "bindings": [ + { + "name": "", + "id": "978bfe49-cc26-4a3d-ab7b-7d7a29327403", + "path": "/leftStick", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Move", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "WASD", + "id": "00ca640b-d935-4593-8157-c05846ea39b3", + "path": "Dpad", + "interactions": "", + "processors": "", + "groups": "", + "action": "Move", + "isComposite": true, + "isPartOfComposite": false + }, + { + "name": "up", + "id": "e2062cb9-1b15-46a2-838c-2f8d72a0bdd9", + "path": "/w", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Move", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "up", + "id": "8180e8bd-4097-4f4e-ab88-4523101a6ce9", + "path": "/upArrow", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Move", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "down", + "id": "320bffee-a40b-4347-ac70-c210eb8bc73a", + "path": "/s", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Move", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "down", + "id": "1c5327b5-f71c-4f60-99c7-4e737386f1d1", + "path": "/downArrow", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Move", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "left", + "id": "d2581a9b-1d11-4566-b27d-b92aff5fabbc", + "path": "/a", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Move", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "left", + "id": "2e46982e-44cc-431b-9f0b-c11910bf467a", + "path": "/leftArrow", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Move", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "right", + "id": "fcfe95b8-67b9-4526-84b5-5d0bc98d6400", + "path": "/d", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Move", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "right", + "id": "77bff152-3580-4b21-b6de-dcd0c7e41164", + "path": "/rightArrow", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Move", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "", + "id": "1635d3fe-58b6-4ba9-a4e2-f4b964f6b5c8", + "path": "/{Primary2DAxis}", + "interactions": "", + "processors": "", + "groups": "XR", + "action": "Move", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "3ea4d645-4504-4529-b061-ab81934c3752", + "path": "/stick", + "interactions": "", + "processors": "", + "groups": "Joystick", + "action": "Move", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "c1f7a91b-d0fd-4a62-997e-7fb9b69bf235", + "path": "/rightStick", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Look", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "8c8e490b-c610-4785-884f-f04217b23ca4", + "path": "/delta", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse;Touch", + "action": "Look", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "3e5f5442-8668-4b27-a940-df99bad7e831", + "path": "/{Hatswitch}", + "interactions": "", + "processors": "", + "groups": "Joystick", + "action": "Look", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "143bb1cd-cc10-4eca-a2f0-a3664166fe91", + "path": "/buttonWest", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Attack", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "05f6913d-c316-48b2-a6bb-e225f14c7960", + "path": "/leftButton", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Attack", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "886e731e-7071-4ae4-95c0-e61739dad6fd", + "path": "/primaryTouch/tap", + "interactions": "", + "processors": "", + "groups": ";Touch", + "action": "Attack", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "ee3d0cd2-254e-47a7-a8cb-bc94d9658c54", + "path": "/trigger", + "interactions": "", + "processors": "", + "groups": "Joystick", + "action": "Attack", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "8255d333-5683-4943-a58a-ccb207ff1dce", + "path": "/{PrimaryAction}", + "interactions": "", + "processors": "", + "groups": "XR", + "action": "Attack", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "b3c1c7f0-bd20-4ee7-a0f1-899b24bca6d7", + "path": "/enter", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Attack", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "cbac6039-9c09-46a1-b5f2-4e5124ccb5ed", + "path": "/2", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Next", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "e15ca19d-e649-4852-97d5-7fe8ccc44e94", + "path": "/dpad/right", + "interactions": "", + "processors": "", + "groups": "Gamepad", + "action": "Next", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "f2e9ba44-c423-42a7-ad56-f20975884794", + "path": "/leftShift", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Sprint", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "8cbb2f4b-a784-49cc-8d5e-c010b8c7f4e6", + "path": "/leftStickPress", + "interactions": "", + "processors": "", + "groups": "Gamepad", + "action": "Sprint", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "d8bf24bf-3f2f-4160-a97c-38ec1eb520ba", + "path": "/trigger", + "interactions": "", + "processors": "", + "groups": "XR", + "action": "Sprint", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "eb40bb66-4559-4dfa-9a2f-820438abb426", + "path": "/space", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Jump", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "daba33a1-ad0c-4742-a909-43ad1cdfbeb6", + "path": "/buttonSouth", + "interactions": "", + "processors": "", + "groups": "Gamepad", + "action": "Jump", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "603f3daf-40bd-4854-8724-93e8017f59e3", + "path": "/secondaryButton", + "interactions": "", + "processors": "", + "groups": "XR", + "action": "Jump", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "1534dc16-a6aa-499d-9c3a-22b47347b52a", + "path": "/1", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Previous", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "25060bbd-a3a6-476e-8fba-45ae484aad05", + "path": "/dpad/left", + "interactions": "", + "processors": "", + "groups": "Gamepad", + "action": "Previous", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "1c04ea5f-b012-41d1-a6f7-02e963b52893", + "path": "/e", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Interact", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "b3f66d0b-7751-423f-908b-a11c5bd95930", + "path": "/buttonNorth", + "interactions": "", + "processors": "", + "groups": "Gamepad", + "action": "Interact", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "4f4649ac-64a8-4a73-af11-b3faef356a4d", + "path": "/buttonEast", + "interactions": "", + "processors": "", + "groups": "Gamepad", + "action": "Crouch", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "36e52cba-0905-478e-a818-f4bfcb9f3b9a", + "path": "/c", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Crouch", + "isComposite": false, + "isPartOfComposite": false + } + ] + }, + { + "name": "UI", + "id": "272f6d14-89ba-496f-b7ff-215263d3219f", + "actions": [ + { + "name": "Navigate", + "type": "PassThrough", + "id": "c95b2375-e6d9-4b88-9c4c-c5e76515df4b", + "expectedControlType": "Vector2", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "Submit", + "type": "Button", + "id": "7607c7b6-cd76-4816-beef-bd0341cfe950", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "Cancel", + "type": "Button", + "id": "15cef263-9014-4fd5-94d9-4e4a6234a6ef", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "Point", + "type": "PassThrough", + "id": "32b35790-4ed0-4e9a-aa41-69ac6d629449", + "expectedControlType": "Vector2", + "processors": "", + "interactions": "", + "initialStateCheck": true + }, + { + "name": "Click", + "type": "PassThrough", + "id": "3c7022bf-7922-4f7c-a998-c437916075ad", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": true + }, + { + "name": "RightClick", + "type": "PassThrough", + "id": "44b200b1-1557-4083-816c-b22cbdf77ddf", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "MiddleClick", + "type": "PassThrough", + "id": "dad70c86-b58c-4b17-88ad-f5e53adf419e", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "ScrollWheel", + "type": "PassThrough", + "id": "0489e84a-4833-4c40-bfae-cea84b696689", + "expectedControlType": "Vector2", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "TrackedDevicePosition", + "type": "PassThrough", + "id": "24908448-c609-4bc3-a128-ea258674378a", + "expectedControlType": "Vector3", + "processors": "", + "interactions": "", + "initialStateCheck": false + }, + { + "name": "TrackedDeviceOrientation", + "type": "PassThrough", + "id": "9caa3d8a-6b2f-4e8e-8bad-6ede561bd9be", + "expectedControlType": "Quaternion", + "processors": "", + "interactions": "", + "initialStateCheck": false + } + ], + "bindings": [ + { + "name": "Gamepad", + "id": "809f371f-c5e2-4e7a-83a1-d867598f40dd", + "path": "2DVector", + "interactions": "", + "processors": "", + "groups": "", + "action": "Navigate", + "isComposite": true, + "isPartOfComposite": false + }, + { + "name": "up", + "id": "14a5d6e8-4aaf-4119-a9ef-34b8c2c548bf", + "path": "/leftStick/up", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "up", + "id": "9144cbe6-05e1-4687-a6d7-24f99d23dd81", + "path": "/rightStick/up", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "down", + "id": "2db08d65-c5fb-421b-983f-c71163608d67", + "path": "/leftStick/down", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "down", + "id": "58748904-2ea9-4a80-8579-b500e6a76df8", + "path": "/rightStick/down", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "left", + "id": "8ba04515-75aa-45de-966d-393d9bbd1c14", + "path": "/leftStick/left", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "left", + "id": "712e721c-bdfb-4b23-a86c-a0d9fcfea921", + "path": "/rightStick/left", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "right", + "id": "fcd248ae-a788-4676-a12e-f4d81205600b", + "path": "/leftStick/right", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "right", + "id": "1f04d9bc-c50b-41a1-bfcc-afb75475ec20", + "path": "/rightStick/right", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "", + "id": "fb8277d4-c5cd-4663-9dc7-ee3f0b506d90", + "path": "/dpad", + "interactions": "", + "processors": "", + "groups": ";Gamepad", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "Joystick", + "id": "e25d9774-381c-4a61-b47c-7b6b299ad9f9", + "path": "2DVector", + "interactions": "", + "processors": "", + "groups": "", + "action": "Navigate", + "isComposite": true, + "isPartOfComposite": false + }, + { + "name": "up", + "id": "3db53b26-6601-41be-9887-63ac74e79d19", + "path": "/stick/up", + "interactions": "", + "processors": "", + "groups": "Joystick", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "down", + "id": "0cb3e13e-3d90-4178-8ae6-d9c5501d653f", + "path": "/stick/down", + "interactions": "", + "processors": "", + "groups": "Joystick", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "left", + "id": "0392d399-f6dd-4c82-8062-c1e9c0d34835", + "path": "/stick/left", + "interactions": "", + "processors": "", + "groups": "Joystick", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "right", + "id": "942a66d9-d42f-43d6-8d70-ecb4ba5363bc", + "path": "/stick/right", + "interactions": "", + "processors": "", + "groups": "Joystick", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "Keyboard", + "id": "ff527021-f211-4c02-933e-5976594c46ed", + "path": "2DVector", + "interactions": "", + "processors": "", + "groups": "", + "action": "Navigate", + "isComposite": true, + "isPartOfComposite": false + }, + { + "name": "up", + "id": "563fbfdd-0f09-408d-aa75-8642c4f08ef0", + "path": "/w", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "up", + "id": "eb480147-c587-4a33-85ed-eb0ab9942c43", + "path": "/upArrow", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "down", + "id": "2bf42165-60bc-42ca-8072-8c13ab40239b", + "path": "/s", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "down", + "id": "85d264ad-e0a0-4565-b7ff-1a37edde51ac", + "path": "/downArrow", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "left", + "id": "74214943-c580-44e4-98eb-ad7eebe17902", + "path": "/a", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "left", + "id": "cea9b045-a000-445b-95b8-0c171af70a3b", + "path": "/leftArrow", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "right", + "id": "8607c725-d935-4808-84b1-8354e29bab63", + "path": "/d", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "right", + "id": "4cda81dc-9edd-4e03-9d7c-a71a14345d0b", + "path": "/rightArrow", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Navigate", + "isComposite": false, + "isPartOfComposite": true + }, + { + "name": "", + "id": "9e92bb26-7e3b-4ec4-b06b-3c8f8e498ddc", + "path": "*/{Submit}", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse;Gamepad;Touch;Joystick;XR", + "action": "Submit", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "82627dcc-3b13-4ba9-841d-e4b746d6553e", + "path": "*/{Cancel}", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse;Gamepad;Touch;Joystick;XR", + "action": "Cancel", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "c52c8e0b-8179-41d3-b8a1-d149033bbe86", + "path": "/position", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Point", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "e1394cbc-336e-44ce-9ea8-6007ed6193f7", + "path": "/position", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "Point", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "5693e57a-238a-46ed-b5ae-e64e6e574302", + "path": "/touch*/position", + "interactions": "", + "processors": "", + "groups": "Touch", + "action": "Point", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "4faf7dc9-b979-4210-aa8c-e808e1ef89f5", + "path": "/leftButton", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Click", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "8d66d5ba-88d7-48e6-b1cd-198bbfef7ace", + "path": "/tip", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "Click", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "47c2a644-3ebc-4dae-a106-589b7ca75b59", + "path": "/touch*/press", + "interactions": "", + "processors": "", + "groups": "Touch", + "action": "Click", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "bb9e6b34-44bf-4381-ac63-5aa15d19f677", + "path": "/trigger", + "interactions": "", + "processors": "", + "groups": "XR", + "action": "Click", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "38c99815-14ea-4617-8627-164d27641299", + "path": "/scroll", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "ScrollWheel", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "4c191405-5738-4d4b-a523-c6a301dbf754", + "path": "/rightButton", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "RightClick", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "24066f69-da47-44f3-a07e-0015fb02eb2e", + "path": "/middleButton", + "interactions": "", + "processors": "", + "groups": "Keyboard&Mouse", + "action": "MiddleClick", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "7236c0d9-6ca3-47cf-a6ee-a97f5b59ea77", + "path": "/devicePosition", + "interactions": "", + "processors": "", + "groups": "XR", + "action": "TrackedDevicePosition", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "23e01e3a-f935-4948-8d8b-9bcac77714fb", + "path": "/deviceRotation", + "interactions": "", + "processors": "", + "groups": "XR", + "action": "TrackedDeviceOrientation", + "isComposite": false, + "isPartOfComposite": false + } + ] + } + ], + "controlSchemes": [ + { + "name": "Keyboard&Mouse", + "bindingGroup": "Keyboard&Mouse", + "devices": [ + { + "devicePath": "", + "isOptional": false, + "isOR": false + }, + { + "devicePath": "", + "isOptional": false, + "isOR": false + } + ] + }, + { + "name": "Gamepad", + "bindingGroup": "Gamepad", + "devices": [ + { + "devicePath": "", + "isOptional": false, + "isOR": false + } + ] + }, + { + "name": "Touch", + "bindingGroup": "Touch", + "devices": [ + { + "devicePath": "", + "isOptional": false, + "isOR": false + } + ] + }, + { + "name": "Joystick", + "bindingGroup": "Joystick", + "devices": [ + { + "devicePath": "", + "isOptional": false, + "isOR": false + } + ] + }, + { + "name": "XR", + "bindingGroup": "XR", + "devices": [ + { + "devicePath": "", + "isOptional": false, + "isOR": false + } + ] + } + ] +} \ No newline at end of file diff --git a/Assets/InputSystem_Actions.inputactions.meta b/Assets/InputSystem_Actions.inputactions.meta new file mode 100644 index 0000000..6b38b04 --- /dev/null +++ b/Assets/InputSystem_Actions.inputactions.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 052faaac586de48259a63d0c4782560b +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3} + generateWrapperCode: 0 + wrapperCodePath: + wrapperClassName: + wrapperCodeNamespace: diff --git a/Assets/Readme.asset b/Assets/Readme.asset new file mode 100644 index 0000000..e29b9ad --- /dev/null +++ b/Assets/Readme.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d098604e909b6a0e64fe5910c2719704864d49678bcf5413bd47b67e5490107 +size 1144 diff --git a/Assets/Readme.asset.meta b/Assets/Readme.asset.meta new file mode 100644 index 0000000..ab3ad45 --- /dev/null +++ b/Assets/Readme.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8105016687592461f977c054a80ce2f2 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes.meta b/Assets/Scenes.meta new file mode 100644 index 0000000..2b76064 --- /dev/null +++ b/Assets/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5a28cea83daa4d34abf9d032e9d95e82 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity new file mode 100644 index 0000000..1c63aa8 --- /dev/null +++ b/Assets/Scenes/SampleScene.unity @@ -0,0 +1,432 @@ +%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: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 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.18028378, g: 0.22571412, b: 0.30692285, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + 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_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: 20201, guid: 0000000000000000f000000000000000, type: 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 &330585543 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 330585546} + - component: {fileID: 330585545} + - component: {fileID: 330585544} + - component: {fileID: 330585547} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &330585544 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + m_Enabled: 1 +--- !u!20 &330585545 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &330585546 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + 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 &330585547 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 1 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + quality: 3 + frameInfluence: 0.1 + jitterScale: 1 + mipBias: 0 + varianceClampScale: 0.9 + contrastAdaptiveSharpening: 0 +--- !u!1 &410087039 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 410087041} + - component: {fileID: 410087040} + - component: {fileID: 410087042} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &410087040 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 410087039} + m_Enabled: 1 + serializedVersion: 11 + m_Type: 1 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 2 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 5000 + m_UseColorTemperature: 1 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &410087041 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 410087039} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!114 &410087042 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 410087039} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Version: 3 + m_UsePipelineSettings: 1 + m_AdditionalLightsShadowResolutionTier: 2 + m_LightLayerMask: 1 + m_RenderingLayers: 1 + m_CustomShadowLayers: 0 + m_ShadowLayerMask: 1 + m_ShadowRenderingLayers: 1 + m_LightCookieSize: {x: 1, y: 1} + m_LightCookieOffset: {x: 0, y: 0} + m_SoftShadowQuality: 1 +--- !u!1 &832575517 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 832575519} + - component: {fileID: 832575518} + m_Layer: 0 + m_Name: Global Volume + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &832575518 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 832575517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 172515602e62fb746b5d573b38a5fe58, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IsGlobal: 1 + priority: 0 + blendDistance: 0 + weight: 1 + sharedProfile: {fileID: 11400000, guid: 10fc4df2da32a41aaa32d77bc913491c, type: 2} +--- !u!4 &832575519 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 832575517} + 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!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 330585546} + - {fileID: 410087041} + - {fileID: 832575519} diff --git a/Assets/Scenes/SampleScene.unity.meta b/Assets/Scenes/SampleScene.unity.meta new file mode 100644 index 0000000..9531828 --- /dev/null +++ b/Assets/Scenes/SampleScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 99c9720ab356a0642a771bea13969a05 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts.meta b/Assets/Scripts.meta new file mode 100644 index 0000000..36a2f2a --- /dev/null +++ b/Assets/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a32c49c00ff9b1844adb6f716b0f752a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/AfterHours.Runtime.asmdef b/Assets/Scripts/AfterHours.Runtime.asmdef new file mode 100644 index 0000000..5beeb27 --- /dev/null +++ b/Assets/Scripts/AfterHours.Runtime.asmdef @@ -0,0 +1,12 @@ +{ + "name": "AfterHours.Runtime", + "rootNamespace": "AfterHours", + "references": [ + "Unity.Netcode.Runtime", + "Unity.Netcode.Components", + "Unity.Collections" + ], + "includePlatforms": [], + "allowUnsafeCode": false, + "autoReferenced": true +} diff --git a/Assets/Scripts/AfterHours.Runtime.asmdef.meta b/Assets/Scripts/AfterHours.Runtime.asmdef.meta new file mode 100644 index 0000000..9b65899 --- /dev/null +++ b/Assets/Scripts/AfterHours.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 29f53eee638de7147a47939382c45f71 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core.meta b/Assets/Scripts/Core.meta new file mode 100644 index 0000000..a3e3f39 --- /dev/null +++ b/Assets/Scripts/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e2dd05b235706ab4aa022f4c00c54b33 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/GameClock.cs b/Assets/Scripts/Core/GameClock.cs new file mode 100644 index 0000000..1e62411 --- /dev/null +++ b/Assets/Scripts/Core/GameClock.cs @@ -0,0 +1,72 @@ +using System; +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Core +{ + /// + /// Spielzeit und Tageswechsel. Nur der Server zählt hoch; Clients lesen mit. + /// + public sealed class GameClock : NetworkBehaviour + { + public static GameClock Instance { get; private set; } + + [Header("Konfiguration")] + [SerializeField] private float _realSecondsPerGameHour = 60f; + [SerializeField] private int _openingHour = 10; + [SerializeField] private int _closingHour = 22; + + private readonly NetworkVariable _hourOfDay = new(); // 0..24 + private readonly NetworkVariable _day = new(); + private readonly NetworkVariable _isOpen = new(); + + public float HourOfDay => _hourOfDay.Value; + public int Day => _day.Value; + public bool IsOpen => _isOpen.Value; + + /// Feuert serverseitig beim Tageswechsel. Für Abrechnung, Lieferungen, Miete. + public event Action DayEnded; + + private void Awake() => Instance = this; + + public override void OnNetworkSpawn() + { + if (!IsServer) return; + _day.Value = 1; + _hourOfDay.Value = _openingHour; + _isOpen.Value = true; + } + + public override void OnNetworkDespawn() + { + if (Instance == this) Instance = null; + } + + private void Update() + { + if (!IsServer) return; + + _hourOfDay.Value += Time.deltaTime / _realSecondsPerGameHour; + _isOpen.Value = _hourOfDay.Value >= _openingHour && _hourOfDay.Value < _closingHour; + + if (_hourOfDay.Value >= 24f) + { + _hourOfDay.Value -= 24f; + int finished = _day.Value; + _day.Value = finished + 1; + DayEnded?.Invoke(finished); + } + } + + /// Springt zum nächsten Ladenöffnungszeitpunkt. Wird vom "Feierabend"-Button genutzt. + public void SkipToOpening() + { + if (!IsServer) return; + // TODO: erst prüfen, ob noch Kunden im Laden sind. + _hourOfDay.Value = _openingHour; + int finished = _day.Value; + _day.Value = finished + 1; + DayEnded?.Invoke(finished); + } + } +} diff --git a/Assets/Scripts/Core/GameClock.cs.meta b/Assets/Scripts/Core/GameClock.cs.meta new file mode 100644 index 0000000..d3c549b --- /dev/null +++ b/Assets/Scripts/Core/GameClock.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1c6319a5bac9dc247a1e3f9e025bb5c9 \ No newline at end of file diff --git a/Assets/Scripts/Core/ShopEconomy.cs b/Assets/Scripts/Core/ShopEconomy.cs new file mode 100644 index 0000000..c9cd625 --- /dev/null +++ b/Assets/Scripts/Core/ShopEconomy.cs @@ -0,0 +1,83 @@ +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Core +{ + /// + /// Einziger Ort, an dem Geld und Ruf verändert werden. Server-authoritativ. + /// Clients lesen ausschließlich die NetworkVariables. + /// + public sealed class ShopEconomy : NetworkBehaviour + { + public static ShopEconomy Instance { get; private set; } + + [SerializeField] private float _startingBalance = 2500f; + + private readonly NetworkVariable _balance = new(); + private readonly NetworkVariable _reputation = new(); // 0..1 + private readonly NetworkVariable _customersServedToday = new(); + private readonly NetworkVariable _customersLostToday = new(); + + public float Balance => _balance.Value; + public float Reputation => _reputation.Value; + public int CustomersServedToday => _customersServedToday.Value; + public int CustomersLostToday => _customersLostToday.Value; + + private void Awake() => Instance = this; + + public override void OnNetworkSpawn() + { + if (!IsServer) return; + _balance.Value = _startingBalance; + _reputation.Value = 0.5f; + } + + public override void OnNetworkDespawn() + { + if (Instance == this) Instance = null; + } + + /// Versucht abzubuchen. Gibt false zurück, wenn das Geld nicht reicht. + public bool TrySpend(float amount) + { + if (!IsServer) { Debug.LogError("[ShopEconomy] TrySpend auf Client aufgerufen."); return false; } + if (amount <= 0f || _balance.Value < amount) return false; + + _balance.Value -= amount; + return true; + } + + public void Earn(float amount) + { + if (!IsServer) return; + if (amount <= 0f) return; + _balance.Value += amount; + } + + public void RegisterSale(float satisfaction) + { + if (!IsServer) return; + _customersServedToday.Value++; + AdjustReputation(Mathf.Lerp(-0.005f, 0.01f, Mathf.Clamp01(satisfaction))); + } + + public void RegisterAbandonment() + { + if (!IsServer) return; + _customersLostToday.Value++; + AdjustReputation(-0.015f); + } + + public void ResetDailyCounters() + { + if (!IsServer) return; + _customersServedToday.Value = 0; + _customersLostToday.Value = 0; + } + + private void AdjustReputation(float delta) + { + _reputation.Value = Mathf.Clamp01(_reputation.Value + delta); + } + } +} diff --git a/Assets/Scripts/Core/ShopEconomy.cs.meta b/Assets/Scripts/Core/ShopEconomy.cs.meta new file mode 100644 index 0000000..1c73c63 --- /dev/null +++ b/Assets/Scripts/Core/ShopEconomy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 48276ef6a91772a4f9593792f07b29d2 \ No newline at end of file diff --git a/Assets/Scripts/Customers.meta b/Assets/Scripts/Customers.meta new file mode 100644 index 0000000..ea7e99d --- /dev/null +++ b/Assets/Scripts/Customers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 755094105c5bfe746a7545d5b87348bd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Customers/BasketLine.cs b/Assets/Scripts/Customers/BasketLine.cs new file mode 100644 index 0000000..17469cd --- /dev/null +++ b/Assets/Scripts/Customers/BasketLine.cs @@ -0,0 +1,15 @@ +namespace AfterHours.Customers +{ + /// Ein Artikel im Einkaufskorb eines Kunden. Nur serverseitig. + public readonly struct BasketLine + { + public readonly int ProductId; + public readonly float Price; + + public BasketLine(int productId, float price) + { + ProductId = productId; + Price = price; + } + } +} diff --git a/Assets/Scripts/Customers/BasketLine.cs.meta b/Assets/Scripts/Customers/BasketLine.cs.meta new file mode 100644 index 0000000..d6f44d1 --- /dev/null +++ b/Assets/Scripts/Customers/BasketLine.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f271cf0a93651524d826c623bf6c9509 \ No newline at end of file diff --git a/Assets/Scripts/Customers/ComfortModel.cs b/Assets/Scripts/Customers/ComfortModel.cs new file mode 100644 index 0000000..2d3a16f --- /dev/null +++ b/Assets/Scripts/Customers/ComfortModel.cs @@ -0,0 +1,75 @@ +using System; + +namespace AfterHours.Customers +{ + /// + /// Kern-Mechanik des Spiels: Wie wohl fühlt sich ein Kunde gerade? + /// Fällt der Komfort unter , legt der Kunde + /// die Ware zurück und geht. + /// + /// Engine-frei und damit testbar – siehe Tests/EditMode/ComfortModelTests.cs. + /// + public static class ComfortModel + { + public const float AbandonThreshold = 0.25f; + + /// Komfortverlust pro anderem Kunden im Nahbereich. + private const float CrowdingPenaltyPerPerson = 0.09f; + + /// Malus, wenn Personal ungefragt in Reichweite steht. + private const float UnwantedStaffPenalty = 0.18f; + + /// Malus, wenn das Regal von der Straße aus einsehbar ist. + private const float ExposedShelfPenalty = 0.22f; + + /// Erholung pro Sekunde, wenn niemand stört. + private const float RecoveryPerSecond = 0.12f; + + /// + /// Zielkomfort für die aktuelle Situation (0..1). + /// + /// Charaktereigenschaft des Kunden, 0..1. Höher = unerschrockener. + /// Diskretionsbedarf des betrachteten Produkts, 0..1. + /// Andere Kunden im Nahbereich. + /// Personal steht daneben, ohne dass Beratung gewünscht war. + /// Regal ist vom Schaufenster aus einsehbar. + public static float EvaluateTarget( + float baseTolerance, + float discretionDemand, + int nearbyCustomers, + bool staffNearbyUnrequested, + bool shelfExposedToStreet) + { + if (nearbyCustomers < 0) throw new ArgumentOutOfRangeException(nameof(nearbyCustomers)); + + float tolerance = Clamp01(baseTolerance); + float sensitivity = Clamp01(discretionDemand); + + // Ein toleranter Kunde bei unkritischer Ware ist praktisch unstörbar. + float exposure = sensitivity * (1f - tolerance * 0.6f); + + float penalty = 0f; + penalty += nearbyCustomers * CrowdingPenaltyPerPerson; + if (staffNearbyUnrequested) penalty += UnwantedStaffPenalty; + if (shelfExposedToStreet) penalty += ExposedShelfPenalty; + + return Clamp01(1f - penalty * exposure * 2f); + } + + /// Bewegt den aktuellen Komfort auf den Zielwert zu (Frame-Schritt). + public static float Step(float current, float target, float deltaTime) + { + if (target >= current) + { + // Erholung ist träge … + return Clamp01(current + RecoveryPerSecond * deltaTime); + } + // … Unbehagen schlägt sofort durch. + return Clamp01(target); + } + + public static bool ShouldAbandon(float comfort) => comfort < AbandonThreshold; + + private static float Clamp01(float v) => v < 0f ? 0f : (v > 1f ? 1f : v); + } +} diff --git a/Assets/Scripts/Customers/ComfortModel.cs.meta b/Assets/Scripts/Customers/ComfortModel.cs.meta new file mode 100644 index 0000000..4b2c71b --- /dev/null +++ b/Assets/Scripts/Customers/ComfortModel.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1aac25a7e1b9fc246a75c4b5e9f7b2ed \ No newline at end of file diff --git a/Assets/Scripts/Customers/CustomerAgent.cs b/Assets/Scripts/Customers/CustomerAgent.cs new file mode 100644 index 0000000..7676400 --- /dev/null +++ b/Assets/Scripts/Customers/CustomerAgent.cs @@ -0,0 +1,227 @@ +using System.Collections.Generic; +using AfterHours.Core; +using AfterHours.Data; +using AfterHours.Economy; +using AfterHours.Shop; +using Unity.Netcode; +using UnityEngine; +using UnityEngine.AI; + +namespace AfterHours.Customers +{ + /// + /// Ein Kunde. Die gesamte Entscheidungslogik läuft AUSSCHLIESSLICH auf dem + /// Server; Clients bekommen nur Transform-Sync und den sichtbaren Zustand + /// (für Sprechblasen / Emotes). + /// + /// Performance: Der Server tickt die Kunden bewusst nicht jeden Frame, + /// sondern in Intervallen (TickInterval). 30 NPCs à 60 fps wären + /// verschwendete CPU-Zeit. + /// + [RequireComponent(typeof(NavMeshAgent))] + public sealed class CustomerAgent : NetworkBehaviour + { + private const float TickInterval = 0.25f; + private const float ArrivalDistance = 0.6f; + + [SerializeField] private ProductCatalog _catalog; + [SerializeField] private float _personalSpaceRadius = 1.6f; + [SerializeField] private LayerMask _customerMask; + + private readonly NetworkVariable _state = new(CustomerState.Entering); + private readonly NetworkVariable _visibleComfort = new(1f); + + private NavMeshAgent _agent; + private CashRegister _register; + private ShelfSlot _targetSlot; + private readonly List _basket = new(); + private readonly Collider[] _neighbourBuffer = new Collider[8]; + + private float _tickTimer; + private float _baseTolerance = 0.5f; + private float _comfort = 1f; + private int _wantedItems = 1; + private ProductCategory _wantedCategory; + + public CustomerState State => _state.Value; + public float Comfort => _comfort; + public int BasketCount => _basket.Count; + + public BasketLine BasketAt(int index) => _basket[index]; + + private void Awake() => _agent = GetComponent(); + + public override void OnNetworkSpawn() + { + // Clients simulieren nichts – NavMesh nur auf dem Server. + _agent.enabled = IsServer; + } + + /// Server: Initialisierung durch den Spawner. + public void Initialize(float tolerance, int wantedItems, ProductCategory wantedCategory, CashRegister register) + { + if (!IsServer) return; + + _baseTolerance = Mathf.Clamp01(tolerance); + _wantedItems = Mathf.Max(1, wantedItems); + _wantedCategory = wantedCategory; + _register = register; + _comfort = 1f; + _state.Value = CustomerState.Browsing; + } + + private void Update() + { + if (!IsServer) return; + + _tickTimer -= Time.deltaTime; + if (_tickTimer > 0f) return; + + float dt = TickInterval - _tickTimer; + _tickTimer = TickInterval; + + UpdateComfort(dt); + UpdateBehaviour(); + } + + private void UpdateComfort(float deltaTime) + { + float discretion = 0.3f; + if (_targetSlot != null && _catalog.TryGet(_targetSlot.ProductId, out var product)) + discretion = product.DiscretionDemand; + + float target = ComfortModel.EvaluateTarget( + _baseTolerance, + discretion, + CountNearbyCustomers(), + staffNearbyUnrequested: IsStaffCrowding(), + shelfExposedToStreet: _targetSlot != null && _targetSlot.ExposedToStreet); + + _comfort = ComfortModel.Step(_comfort, target, deltaTime); + _visibleComfort.Value = _comfort; + + if (ComfortModel.ShouldAbandon(_comfort) && _state.Value != CustomerState.Leaving) + Abandon(); + } + + private void UpdateBehaviour() + { + switch (_state.Value) + { + case CustomerState.Browsing: + if (_basket.Count >= _wantedItems) { GoToCheckout(); break; } + + if (_targetSlot == null) + { + _targetSlot = FindShelfToVisit(); + if (_targetSlot != null) MoveTo(_targetSlot.transform.position); + } + else if (HasArrivedAtTarget()) + { + TryTakeFromTarget(); + } + break; + + case CustomerState.Queueing: + // Positionierung übernimmt CashRegister.ReflowQueue(). + break; + + case CustomerState.Leaving: + if (!_agent.pathPending && _agent.remainingDistance < 0.4f) Despawn(); + break; + } + } + + /// Nächstes passendes Regal: richtige Kategorie, vorrätig, Preis noch akzeptabel. + private ShelfSlot FindShelfToVisit() + { + ShelfSlot best = null; + float bestSqrDistance = float.MaxValue; + + foreach (var slot in ShelfRegistry.Instance.Slots) + { + if (slot == null || slot.IsEmpty) continue; + if (!_catalog.TryGet(slot.ProductId, out var product)) continue; + if (product.Category != _wantedCategory) continue; + + float probability = PricingService.PurchaseProbability(slot.Price, product.RecommendedPrice); + if (probability <= 0f || Random.value > probability) continue; + + float sqrDistance = (slot.transform.position - transform.position).sqrMagnitude; + if (sqrDistance < bestSqrDistance) + { + bestSqrDistance = sqrDistance; + best = slot; + } + } + + return best; + } + + private bool HasArrivedAtTarget() + { + return !_agent.pathPending && _agent.remainingDistance <= ArrivalDistance; + } + + private void TryTakeFromTarget() + { + int productId = _targetSlot.ProductId; + float price = _targetSlot.Price; + + if (_targetSlot.TakeOne()) + _basket.Add(new BasketLine(productId, price)); + + _targetSlot = null; + } + + private int CountNearbyCustomers() + { + int hits = Physics.OverlapSphereNonAlloc( + transform.position, _personalSpaceRadius, _neighbourBuffer, _customerMask); + return Mathf.Max(0, hits - 1); // sich selbst abziehen + } + + private bool IsStaffCrowding() + { + // TODO: Spieler in Nähe prüfen, sofern der Kunde keine Beratung wollte. + return false; + } + + private void GoToCheckout() + { + _state.Value = CustomerState.Queueing; + MoveTo(_register.Enqueue(this)); + } + + private void Abandon() + { + _state.Value = CustomerState.Leaving; + _basket.Clear(); + _register?.Remove(this); + ShopEconomy.Instance.RegisterAbandonment(); + // TODO: zurückgelegte Ware am Regal wieder gutschreiben (oder als "Chaos" liegen lassen). + MoveTo(CustomerSpawner.Instance.ExitPoint.position); + } + + /// Server: Kasse meldet erfolgreichen Abschluss. + public void OnPaid() + { + if (!IsServer) return; + _state.Value = CustomerState.Leaving; + MoveTo(CustomerSpawner.Instance.ExitPoint.position); + } + + public void MoveTo(Vector3 destination) + { + if (!IsServer || !_agent.enabled) return; + _agent.SetDestination(destination); + } + + private void Despawn() + { + if (!IsServer) return; + CustomerSpawner.Instance?.NotifyCustomerGone(); + NetworkObject.Despawn(); + } + } +} diff --git a/Assets/Scripts/Customers/CustomerAgent.cs.meta b/Assets/Scripts/Customers/CustomerAgent.cs.meta new file mode 100644 index 0000000..1788aea --- /dev/null +++ b/Assets/Scripts/Customers/CustomerAgent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0669ca75bd9edac44a5db06778b39410 \ No newline at end of file diff --git a/Assets/Scripts/Customers/CustomerSpawner.cs b/Assets/Scripts/Customers/CustomerSpawner.cs new file mode 100644 index 0000000..3c1ddfd --- /dev/null +++ b/Assets/Scripts/Customers/CustomerSpawner.cs @@ -0,0 +1,85 @@ +using AfterHours.Core; +using AfterHours.Data; +using AfterHours.Shop; +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Customers +{ + /// + /// Erzeugt Kunden abhängig von Uhrzeit und Ruf. Läuft nur auf dem Server. + /// + public sealed class CustomerSpawner : NetworkBehaviour + { + public static CustomerSpawner Instance { get; private set; } + + [SerializeField] private GameObject _customerPrefab; + [SerializeField] private Transform _entryPoint; + [SerializeField] private Transform _exitPoint; + [SerializeField] private CashRegister _register; + + [Header("Andrang")] + [SerializeField] private int _maxConcurrent = 20; + [SerializeField] private float _baseSecondsBetweenSpawns = 8f; + + [Tooltip("Multiplikator über den Tag: x-Achse 0..24 Uhr, y = Andrang.")] + [SerializeField] private AnimationCurve _rushCurve = AnimationCurve.Linear(0f, 1f, 24f, 1f); + + public Transform ExitPoint => _exitPoint; + + private float _cooldown; + private int _alive; + + private void Awake() => Instance = this; + + public override void OnNetworkDespawn() + { + if (Instance == this) Instance = null; + } + + private void Update() + { + if (!IsServer) return; + if (GameClock.Instance == null || !GameClock.Instance.IsOpen) return; + if (_alive >= _maxConcurrent) return; + + _cooldown -= Time.deltaTime; + if (_cooldown > 0f) return; + + Spawn(); + _cooldown = NextInterval(); + } + + private float NextInterval() + { + float rush = Mathf.Max(0.1f, _rushCurve.Evaluate(GameClock.Instance.HourOfDay)); + float reputation = Mathf.Lerp(1.6f, 0.6f, ShopEconomy.Instance.Reputation); + return _baseSecondsBetweenSpawns / rush * reputation * Random.Range(0.7f, 1.3f); + } + + private void Spawn() + { + var instance = Instantiate(_customerPrefab, _entryPoint.position, _entryPoint.rotation); + var netObject = instance.GetComponent(); + netObject.Spawn(); + + var categories = (ProductCategory[])System.Enum.GetValues(typeof(ProductCategory)); + + var agent = instance.GetComponent(); + agent.Initialize( + tolerance: Random.Range(0.15f, 0.95f), + wantedItems: Random.Range(1, 4), + wantedCategory: categories[Random.Range(0, categories.Length)], + register: _register); + + _alive++; + } + + /// Wird von CustomerAgent kurz vor dem Despawn aufgerufen. + public void NotifyCustomerGone() + { + if (!IsServer) return; + _alive = Mathf.Max(0, _alive - 1); + } + } +} diff --git a/Assets/Scripts/Customers/CustomerSpawner.cs.meta b/Assets/Scripts/Customers/CustomerSpawner.cs.meta new file mode 100644 index 0000000..ae614c0 --- /dev/null +++ b/Assets/Scripts/Customers/CustomerSpawner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 976ebea99a2a90e4fa658c69f07afef9 \ No newline at end of file diff --git a/Assets/Scripts/Data.meta b/Assets/Scripts/Data.meta new file mode 100644 index 0000000..8d0ab9e --- /dev/null +++ b/Assets/Scripts/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 259041785f4f96f42b6ce2138e6ad8d1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Data/Enums.cs b/Assets/Scripts/Data/Enums.cs new file mode 100644 index 0000000..7a6f57f --- /dev/null +++ b/Assets/Scripts/Data/Enums.cs @@ -0,0 +1,31 @@ +namespace AfterHours.Data +{ + /// Grobes Sortiment-Segment. Beeinflusst Kundschaft, Marge und Ruf. + public enum ProductCategory + { + Wellness, // Massage, Öle, Pflege – niedrige Hemmschwelle, kleine Marge + Couples, // Paar-Sortiment – Kern des Ladens + Lingerie, // Textil – hohe Marge, braucht Umkleide + Novelty, // Scherzartikel – Laufkundschaft, kaum Beratung + Specialty // Nische – hohe Marge, hohe Hemmschwelle, braucht Beratung + } + + public enum ShelfSize + { + Small = 1, + Medium = 2, + Large = 4 + } + + public enum CustomerState + { + Entering, + Browsing, + SeekingAdvice, + WaitingForStaff, + Queueing, + Paying, + Leaving, + Abandoning // geht ohne Kauf – Komfort unter Schwelle + } +} diff --git a/Assets/Scripts/Data/Enums.cs.meta b/Assets/Scripts/Data/Enums.cs.meta new file mode 100644 index 0000000..1ae1ada --- /dev/null +++ b/Assets/Scripts/Data/Enums.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a86bac230bc87784ca31703d750385db \ No newline at end of file diff --git a/Assets/Scripts/Data/ProductCatalog.cs b/Assets/Scripts/Data/ProductCatalog.cs new file mode 100644 index 0000000..fb370fd --- /dev/null +++ b/Assets/Scripts/Data/ProductCatalog.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AfterHours.Data +{ + /// Zentrale Auflösung ProductId -> ProductDefinition. + [CreateAssetMenu(menuName = "AfterHours/Product Catalog", fileName = "ProductCatalog")] + public sealed class ProductCatalog : ScriptableObject + { + [SerializeField] private List _products = new(); + + private Dictionary _byId; + + public IReadOnlyList All => _products; + + private void EnsureIndexed() + { + if (_byId != null) return; + + _byId = new Dictionary(_products.Count); + foreach (var product in _products) + { + if (product == null) continue; + + if (_byId.ContainsKey(product.ProductId)) + { + Debug.LogError($"[ProductCatalog] Doppelte ProductId {product.ProductId} bei '{product.name}'."); + continue; + } + _byId[product.ProductId] = product; + } + } + + public bool TryGet(int productId, out ProductDefinition product) + { + EnsureIndexed(); + return _byId.TryGetValue(productId, out product); + } + + public ProductDefinition Get(int productId) => TryGet(productId, out var p) ? p : null; + +#if UNITY_EDITOR + private void OnValidate() => _byId = null; +#endif + } +} diff --git a/Assets/Scripts/Data/ProductCatalog.cs.meta b/Assets/Scripts/Data/ProductCatalog.cs.meta new file mode 100644 index 0000000..95bb656 --- /dev/null +++ b/Assets/Scripts/Data/ProductCatalog.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ff37cbc356c13f849995837f2a7dfc89 \ No newline at end of file diff --git a/Assets/Scripts/Data/ProductDefinition.cs b/Assets/Scripts/Data/ProductDefinition.cs new file mode 100644 index 0000000..e232d09 --- /dev/null +++ b/Assets/Scripts/Data/ProductDefinition.cs @@ -0,0 +1,45 @@ +using UnityEngine; + +namespace AfterHours.Data +{ + /// + /// Stammdaten eines Artikels. Wird NIE über das Netzwerk übertragen – + /// im Netzwerk zirkuliert ausschließlich die ProductId. + /// + [CreateAssetMenu(menuName = "AfterHours/Product", fileName = "Product_")] + public sealed class ProductDefinition : ScriptableObject + { + [Header("Identität")] + [SerializeField] private int _productId = -1; + [SerializeField] private string _displayName = "Unbenannt"; + [SerializeField] private ProductCategory _category = ProductCategory.Wellness; + + [Header("Wirtschaft")] + [SerializeField, Min(0.01f)] private float _wholesalePrice = 5f; + [SerializeField, Min(0.01f)] private float _recommendedPrice = 12f; + + [Header("Laden")] + [SerializeField] private ShelfSize _shelfSize = ShelfSize.Small; + + [Tooltip("0 = greift jeder mit, 1 = Kunde will damit nicht gesehen werden.")] + [SerializeField, Range(0f, 1f)] private float _discretionDemand = 0.3f; + + [Tooltip("Wahrscheinlichkeit, dass der Kunde vor dem Kauf Beratung sucht.")] + [SerializeField, Range(0f, 1f)] private float _adviceLikelihood = 0.2f; + + [Header("Darstellung (stilisierte Verpackung – siehe CLAUDE.md §7)")] + [SerializeField] private GameObject _packagePrefab; + [SerializeField] private Sprite _icon; + + public int ProductId => _productId; + public string DisplayName => _displayName; + public ProductCategory Category => _category; + public float WholesalePrice => _wholesalePrice; + public float RecommendedPrice => _recommendedPrice; + public ShelfSize ShelfSize => _shelfSize; + public float DiscretionDemand => _discretionDemand; + public float AdviceLikelihood => _adviceLikelihood; + public GameObject PackagePrefab => _packagePrefab; + public Sprite Icon => _icon; + } +} diff --git a/Assets/Scripts/Data/ProductDefinition.cs.meta b/Assets/Scripts/Data/ProductDefinition.cs.meta new file mode 100644 index 0000000..2075beb --- /dev/null +++ b/Assets/Scripts/Data/ProductDefinition.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5e52a7daaf085524caf591a33107a59d \ No newline at end of file diff --git a/Assets/Scripts/Economy.meta b/Assets/Scripts/Economy.meta new file mode 100644 index 0000000..dd1a4cc --- /dev/null +++ b/Assets/Scripts/Economy.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4f403b2316053614daaf052c45a7b85c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Economy/PricingService.cs b/Assets/Scripts/Economy/PricingService.cs new file mode 100644 index 0000000..d7e019c --- /dev/null +++ b/Assets/Scripts/Economy/PricingService.cs @@ -0,0 +1,49 @@ +using System; + +namespace AfterHours.Economy +{ + /// + /// Reine Preis-/Nachfragelogik. Engine-frei und damit testbar. + /// Keine UnityEngine-Referenzen in dieser Datei. + /// + public static class PricingService + { + /// Ab diesem Aufschlag über Empfehlung kauft niemand mehr. + public const float RejectionThreshold = 1.75f; + + /// + /// Kaufwahrscheinlichkeit relativ zum empfohlenen Preis. + /// 1.0 bei Empfehlungspreis oder darunter, 0.0 ab RejectionThreshold. + /// Dazwischen quadratisch abfallend – kleine Aufschläge tun kaum weh, + /// große brechen die Nachfrage schnell ein. + /// + public static float PurchaseProbability(float askingPrice, float recommendedPrice) + { + if (recommendedPrice <= 0f) throw new ArgumentOutOfRangeException(nameof(recommendedPrice)); + if (askingPrice <= 0f) return 1f; + + float ratio = askingPrice / recommendedPrice; + if (ratio <= 1f) return 1f; + if (ratio >= RejectionThreshold) return 0f; + + float t = (ratio - 1f) / (RejectionThreshold - 1f); // 0..1 + return 1f - (t * t); + } + + /// Marge in Prozent des Verkaufspreises. + public static float MarginRatio(float askingPrice, float wholesalePrice) + { + if (askingPrice <= 0f) return 0f; + return (askingPrice - wholesalePrice) / askingPrice; + } + + /// + /// Erwarteter Deckungsbeitrag pro angebotenem Stück. + /// Nützlich für den späteren "Preis-Optimum"-Hinweis im Preis-UI. + /// + public static float ExpectedProfitPerUnit(float askingPrice, float recommendedPrice, float wholesalePrice) + { + return PurchaseProbability(askingPrice, recommendedPrice) * (askingPrice - wholesalePrice); + } + } +} diff --git a/Assets/Scripts/Economy/PricingService.cs.meta b/Assets/Scripts/Economy/PricingService.cs.meta new file mode 100644 index 0000000..9b2a41a --- /dev/null +++ b/Assets/Scripts/Economy/PricingService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 488bfdde2f57adf4e8d4ce3a8ec065c0 \ No newline at end of file diff --git a/Assets/Scripts/Economy/PurchaseOrder.cs b/Assets/Scripts/Economy/PurchaseOrder.cs new file mode 100644 index 0000000..ee76e39 --- /dev/null +++ b/Assets/Scripts/Economy/PurchaseOrder.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace AfterHours.Economy +{ + /// Eine Position einer Großhandelsbestellung. + [Serializable] + public struct OrderLine + { + public int ProductId; + public int Quantity; + public float UnitPrice; + + public float LineTotal => Quantity * UnitPrice; + } + + /// Bestellung beim Großhandel. Wird nur serverseitig gehalten. + public sealed class PurchaseOrder + { + private readonly List _lines = new(); + + public IReadOnlyList Lines => _lines; + + /// Spielzeit in Sekunden, zu der die Lieferung eintrifft. + public float DeliveryTime { get; set; } + + public bool Delivered { get; set; } + + public void Add(int productId, int quantity, float unitPrice) + { + if (quantity <= 0) return; + _lines.Add(new OrderLine { ProductId = productId, Quantity = quantity, UnitPrice = unitPrice }); + } + + public float Total() + { + float sum = 0f; + foreach (var line in _lines) sum += line.LineTotal; + return sum; + } + } +} diff --git a/Assets/Scripts/Economy/PurchaseOrder.cs.meta b/Assets/Scripts/Economy/PurchaseOrder.cs.meta new file mode 100644 index 0000000..20573cc --- /dev/null +++ b/Assets/Scripts/Economy/PurchaseOrder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ca74ad78a2bea0e4d985e96612714923 \ No newline at end of file diff --git a/Assets/Scripts/Interaction.meta b/Assets/Scripts/Interaction.meta new file mode 100644 index 0000000..5347700 --- /dev/null +++ b/Assets/Scripts/Interaction.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1c1ccc133082caf4eb31bcfed0514b62 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Interaction/IInteractable.cs b/Assets/Scripts/Interaction/IInteractable.cs new file mode 100644 index 0000000..0d96126 --- /dev/null +++ b/Assets/Scripts/Interaction/IInteractable.cs @@ -0,0 +1,18 @@ +using Unity.Netcode; + +namespace AfterHours.Interaction +{ + /// + /// Alles, was der Spieler per Blick + Taste bedienen kann. + /// WICHTIG: Interact() läuft nur auf dem Server. Clients rufen es niemals + /// direkt auf, sondern gehen über PlayerInteractor.RequestInteractRpc. + /// + public interface IInteractable + { + /// Text für den Blick-Prompt, z. B. "Regal auffüllen". + string GetPrompt(ulong clientId); + + /// Serverseitige Ausführung. Muss selbst validieren. + void Interact(NetworkObject actor); + } +} diff --git a/Assets/Scripts/Interaction/IInteractable.cs.meta b/Assets/Scripts/Interaction/IInteractable.cs.meta new file mode 100644 index 0000000..c53c88e --- /dev/null +++ b/Assets/Scripts/Interaction/IInteractable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 994d75674ec8c0e46a8c40f1bee27330 \ No newline at end of file diff --git a/Assets/Scripts/Interaction/PlayerInteractor.cs b/Assets/Scripts/Interaction/PlayerInteractor.cs new file mode 100644 index 0000000..0daea9b --- /dev/null +++ b/Assets/Scripts/Interaction/PlayerInteractor.cs @@ -0,0 +1,69 @@ +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Interaction +{ + /// + /// Raycast auf dem Owner-Client für den Prompt, Ausführung auf dem Server. + /// Der Server vertraut dem Client die Zielauswahl NICHT blind – er prüft + /// Distanz und Sichtbarkeit erneut (siehe CLAUDE.md §3). + /// + [RequireComponent(typeof(NetworkObject))] + public sealed class PlayerInteractor : NetworkBehaviour + { + [SerializeField] private Transform _eyes; + [SerializeField] private float _reach = 2.5f; + [SerializeField] private LayerMask _interactableMask = ~0; + + /// Toleranz, weil Client und Server nie exakt denselben Frame sehen. + private const float ServerReachTolerance = 1.5f; + + private IInteractable _current; + public string CurrentPrompt { get; private set; } + + private void Update() + { + if (!IsOwner) return; + + RefreshTarget(); + + if (_current != null && Input.GetKeyDown(KeyCode.E)) + { + var target = ((MonoBehaviour)_current).GetComponent(); + if (target != null) RequestInteractRpc(target); + } + } + + private void RefreshTarget() + { + _current = null; + CurrentPrompt = null; + + if (!Physics.Raycast(_eyes.position, _eyes.forward, out var hit, _reach, _interactableMask)) + return; + + if (!hit.collider.TryGetComponent(out var interactable)) + return; + + _current = interactable; + CurrentPrompt = interactable.GetPrompt(OwnerClientId); + } + + [Rpc(SendTo.Server)] + private void RequestInteractRpc(NetworkObjectReference targetRef) + { + if (!targetRef.TryGet(out var target)) return; + + // Server-Validierung: ist der Spieler überhaupt in Reichweite? + float distance = Vector3.Distance(transform.position, target.transform.position); + if (distance > _reach + ServerReachTolerance) + { + Debug.LogWarning($"[PlayerInteractor] Client {OwnerClientId} zu weit entfernt ({distance:F1} m)."); + return; + } + + if (target.TryGetComponent(out var interactable)) + interactable.Interact(NetworkObject); + } + } +} diff --git a/Assets/Scripts/Interaction/PlayerInteractor.cs.meta b/Assets/Scripts/Interaction/PlayerInteractor.cs.meta new file mode 100644 index 0000000..6ca3bc6 --- /dev/null +++ b/Assets/Scripts/Interaction/PlayerInteractor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3f8feeb0d94c7e44b8d05c33089a77ef \ No newline at end of file diff --git a/Assets/Scripts/Networking.meta b/Assets/Scripts/Networking.meta new file mode 100644 index 0000000..fd5535e --- /dev/null +++ b/Assets/Scripts/Networking.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45f3bfcc1d96be341b8354325b3bb022 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Networking/NetworkBootstrap.cs b/Assets/Scripts/Networking/NetworkBootstrap.cs new file mode 100644 index 0000000..3c0f863 --- /dev/null +++ b/Assets/Scripts/Networking/NetworkBootstrap.cs @@ -0,0 +1,49 @@ +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Networking +{ + /// + /// Einstiegspunkt für Host/Join. Bewusst minimal – Steam-Lobbies kommen + /// später über Facepunch.Steamworks dazu (siehe docs/ROADMAP.md). + /// + public sealed class NetworkBootstrap : MonoBehaviour + { + public const int MaxPlayers = 4; + + [SerializeField] private string _gameSceneName = "Shop"; + + public void HostGame() + { + NetworkManager.Singleton.ConnectionApprovalCallback = ApproveConnection; + + if (!NetworkManager.Singleton.StartHost()) + { + Debug.LogError("[NetworkBootstrap] Host konnte nicht gestartet werden."); + return; + } + + NetworkManager.Singleton.SceneManager.LoadScene( + _gameSceneName, UnityEngine.SceneManagement.LoadSceneMode.Single); + } + + public void JoinGame() + { + if (!NetworkManager.Singleton.StartClient()) + Debug.LogError("[NetworkBootstrap] Client konnte nicht verbinden."); + } + + public void Leave() => NetworkManager.Singleton.Shutdown(); + + private void ApproveConnection( + NetworkManager.ConnectionApprovalRequest request, + NetworkManager.ConnectionApprovalResponse response) + { + bool hasRoom = NetworkManager.Singleton.ConnectedClientsIds.Count < MaxPlayers; + + response.Approved = hasRoom; + response.CreatePlayerObject = true; + response.Reason = hasRoom ? null : "Der Laden ist voll (max. 4 Angestellte)."; + } + } +} diff --git a/Assets/Scripts/Networking/NetworkBootstrap.cs.meta b/Assets/Scripts/Networking/NetworkBootstrap.cs.meta new file mode 100644 index 0000000..58e1eac --- /dev/null +++ b/Assets/Scripts/Networking/NetworkBootstrap.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3f7b7e91ebc572543a8324413a34c7a9 \ No newline at end of file diff --git a/Assets/Scripts/Networking/PlayerAvatar.cs b/Assets/Scripts/Networking/PlayerAvatar.cs new file mode 100644 index 0000000..d3e9a22 --- /dev/null +++ b/Assets/Scripts/Networking/PlayerAvatar.cs @@ -0,0 +1,71 @@ +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Networking +{ + /// + /// Spielerbewegung mit Client-Prediction: der Owner bewegt sich sofort, + /// NetworkTransform repliziert. Bewegung ist der EINZIGE Bereich, in dem + /// wir dem Client vertrauen – siehe CLAUDE.md §3. + /// + [RequireComponent(typeof(CharacterController))] + public sealed class PlayerAvatar : NetworkBehaviour + { + [SerializeField] private float _walkSpeed = 3.4f; + [SerializeField] private float _mouseSensitivity = 2f; + [SerializeField] private Transform _cameraRoot; + [SerializeField] private float _gravity = -12f; + + private CharacterController _controller; + private float _pitch; + private float _verticalVelocity; + + private void Awake() => _controller = GetComponent(); + + public override void OnNetworkSpawn() + { + if (!IsOwner) + { + if (_cameraRoot != null && _cameraRoot.GetComponentInChildren() is { } cam) + cam.gameObject.SetActive(false); + enabled = false; + return; + } + + Cursor.lockState = CursorLockMode.Locked; + } + + private void Update() + { + if (!IsOwner) return; + + Look(); + Move(); + } + + private void Look() + { + float mouseX = Input.GetAxis("Mouse X") * _mouseSensitivity; + float mouseY = Input.GetAxis("Mouse Y") * _mouseSensitivity; + + transform.Rotate(Vector3.up, mouseX); + + _pitch = Mathf.Clamp(_pitch - mouseY, -85f, 85f); + if (_cameraRoot != null) + _cameraRoot.localRotation = Quaternion.Euler(_pitch, 0f, 0f); + } + + private void Move() + { + Vector3 input = new(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical")); + Vector3 direction = transform.TransformDirection(Vector3.ClampMagnitude(input, 1f)); + + _verticalVelocity = _controller.isGrounded + ? -1f + : _verticalVelocity + _gravity * Time.deltaTime; + + Vector3 velocity = direction * _walkSpeed + Vector3.up * _verticalVelocity; + _controller.Move(velocity * Time.deltaTime); + } + } +} diff --git a/Assets/Scripts/Networking/PlayerAvatar.cs.meta b/Assets/Scripts/Networking/PlayerAvatar.cs.meta new file mode 100644 index 0000000..034feb1 --- /dev/null +++ b/Assets/Scripts/Networking/PlayerAvatar.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f88015bc4effd5b429972c480fabe831 \ No newline at end of file diff --git a/Assets/Scripts/Networking/PlayerHands.cs b/Assets/Scripts/Networking/PlayerHands.cs new file mode 100644 index 0000000..ebc699c --- /dev/null +++ b/Assets/Scripts/Networking/PlayerHands.cs @@ -0,0 +1,57 @@ +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Networking +{ + /// + /// Was der Spieler gerade trägt. Server-authoritativ – der Client darf + /// weder Produkt noch Menge selbst setzen. + /// + public sealed class PlayerHands : NetworkBehaviour + { + public const int MaxCarry = 12; + + private readonly NetworkVariable _productId = new(-1); + private readonly NetworkVariable _quantity = new(0); + + public int ProductId => _productId.Value; + public int Quantity => _quantity.Value; + public bool IsEmpty => _quantity.Value <= 0; + + /// Serverseitig: nimmt so viel wie möglich auf, gibt die tatsächliche Menge zurück. + public int TryTake(int productId, int quantity) + { + if (!IsServer) return 0; + if (quantity <= 0) return 0; + + if (!IsEmpty && _productId.Value != productId) + return 0; // Hände sind mit etwas anderem belegt + + int free = MaxCarry - _quantity.Value; + int taken = Mathf.Min(free, quantity); + if (taken <= 0) return 0; + + _productId.Value = productId; + _quantity.Value += taken; + return taken; + } + + /// Serverseitig: gibt bis zu ab. + public int TryGive(int quantity) + { + if (!IsServer || IsEmpty) return 0; + + int given = Mathf.Min(_quantity.Value, quantity); + _quantity.Value -= given; + if (_quantity.Value == 0) _productId.Value = -1; + return given; + } + + public void Clear() + { + if (!IsServer) return; + _productId.Value = -1; + _quantity.Value = 0; + } + } +} diff --git a/Assets/Scripts/Networking/PlayerHands.cs.meta b/Assets/Scripts/Networking/PlayerHands.cs.meta new file mode 100644 index 0000000..13f4991 --- /dev/null +++ b/Assets/Scripts/Networking/PlayerHands.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 402647b9bce9a6a4481b75e1004e3bfb \ No newline at end of file diff --git a/Assets/Scripts/Shop.meta b/Assets/Scripts/Shop.meta new file mode 100644 index 0000000..61818e0 --- /dev/null +++ b/Assets/Scripts/Shop.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 54a0671883c35dd42bfd005bee512b3b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Shop/CashRegister.cs b/Assets/Scripts/Shop/CashRegister.cs new file mode 100644 index 0000000..90b4726 --- /dev/null +++ b/Assets/Scripts/Shop/CashRegister.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using AfterHours.Core; +using AfterHours.Data; +using AfterHours.Interaction; +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Shop +{ + /// + /// Kasse. Kunden reihen sich ein; ein Spieler scannt Position für Position + /// und schließt ab. Sämtliche Geldbewegung passiert hier auf dem Server. + /// + public sealed class CashRegister : NetworkBehaviour, IInteractable + { + [SerializeField] private ProductCatalog _catalog; + [SerializeField] private Transform _queueStart; + [SerializeField] private float _queueSpacing = 0.8f; + + /// Server-only. Clients sehen die Schlange über die NPC-Positionen. + private readonly List _queue = new(); + + private readonly NetworkVariable _pendingTotal = new(0f); + private readonly NetworkVariable _scannedItems = new(0); + + public float PendingTotal => _pendingTotal.Value; + public int QueueLength => _queue.Count; + + public string GetPrompt(ulong clientId) + { + if (_queue.Count == 0) return "Kasse (frei)"; + return _scannedItems.Value > 0 + ? $"Kassieren — {_pendingTotal.Value:0.00} €" + : "Artikel scannen"; + } + + /// Server: ein Klick = ein Artikel scannen, oder abschließen wenn fertig. + public void Interact(NetworkObject actor) + { + if (!IsServer || _queue.Count == 0) return; + + var customer = _queue[0]; + if (customer == null) { _queue.RemoveAt(0); return; } + + if (_scannedItems.Value < customer.BasketCount) + { + ScanNext(customer); + return; + } + + Finalize(customer); + } + + private void ScanNext(Customers.CustomerAgent customer) + { + var line = customer.BasketAt(_scannedItems.Value); + _pendingTotal.Value += line.Price; + _scannedItems.Value++; + } + + private void Finalize(Customers.CustomerAgent customer) + { + ShopEconomy.Instance.Earn(_pendingTotal.Value); + ShopEconomy.Instance.RegisterSale(customer.Comfort); + + customer.OnPaid(); + _queue.RemoveAt(0); + + _pendingTotal.Value = 0f; + _scannedItems.Value = 0; + + ReflowQueue(); + } + + /// Server: Kunde stellt sich an. Gibt die Zielposition zurück. + public Vector3 Enqueue(Customers.CustomerAgent customer) + { + if (!IsServer) return _queueStart.position; + + if (!_queue.Contains(customer)) _queue.Add(customer); + return SlotPosition(_queue.IndexOf(customer)); + } + + /// Server: Kunde verlässt die Schlange (z. B. weil er aufgibt). + public void Remove(Customers.CustomerAgent customer) + { + if (!IsServer) return; + if (_queue.Remove(customer)) ReflowQueue(); + } + + private void ReflowQueue() + { + for (int i = 0; i < _queue.Count; i++) + if (_queue[i] != null) _queue[i].MoveTo(SlotPosition(i)); + } + + private Vector3 SlotPosition(int index) + { + return _queueStart.position + _queueStart.forward * (-_queueSpacing * index); + } + } +} diff --git a/Assets/Scripts/Shop/CashRegister.cs.meta b/Assets/Scripts/Shop/CashRegister.cs.meta new file mode 100644 index 0000000..9dbb53c --- /dev/null +++ b/Assets/Scripts/Shop/CashRegister.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 87517e4a128619c469c629b6248f1daa \ No newline at end of file diff --git a/Assets/Scripts/Shop/ShelfRegistry.cs b/Assets/Scripts/Shop/ShelfRegistry.cs new file mode 100644 index 0000000..cafe337 --- /dev/null +++ b/Assets/Scripts/Shop/ShelfRegistry.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace AfterHours.Shop +{ + /// + /// Serverseitige Liste aller aktiven Regalfächer. Damit Kunden ein + /// passendes Fach finden können, ohne FindObjectOfType im Frame-Loop + /// aufzurufen (siehe CLAUDE.md §5). + /// + public sealed class ShelfRegistry + { + public static readonly ShelfRegistry Instance = new(); + + private readonly List _slots = new(); + + public IReadOnlyList Slots => _slots; + + public void Register(ShelfSlot slot) => _slots.Add(slot); + + public void Unregister(ShelfSlot slot) => _slots.Remove(slot); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Shop/ShelfRegistry.cs.meta b/Assets/Scripts/Shop/ShelfRegistry.cs.meta new file mode 100644 index 0000000..a45e319 --- /dev/null +++ b/Assets/Scripts/Shop/ShelfRegistry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 36930243258ac6f4f816f2cfc254a52f \ No newline at end of file diff --git a/Assets/Scripts/Shop/ShelfSlot.cs b/Assets/Scripts/Shop/ShelfSlot.cs new file mode 100644 index 0000000..b051422 --- /dev/null +++ b/Assets/Scripts/Shop/ShelfSlot.cs @@ -0,0 +1,112 @@ +using AfterHours.Data; +using AfterHours.Interaction; +using AfterHours.Networking; +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Shop +{ + /// + /// Ein Regalfach: hält genau eine Produktsorte. Auffüllen geht über + /// Interact(), Entnahme durch Kunden serverseitig über TakeOne(). + /// + public sealed class ShelfSlot : NetworkBehaviour, IInteractable + { + [SerializeField] private ProductCatalog _catalog; + [SerializeField] private int _capacityUnits = 12; + + [Tooltip("Ist dieses Fach vom Schaufenster aus einsehbar? Setzt der Level-Designer.")] + [SerializeField] private bool _exposedToStreet; + + [SerializeField] private Transform _displayRoot; + + private readonly NetworkVariable _productId = new(-1); + private readonly NetworkVariable _quantity = new(0); + private readonly NetworkVariable _price = new(0f); + + public int ProductId => _productId.Value; + public int Quantity => _quantity.Value; + public float Price => _price.Value; + public bool ExposedToStreet => _exposedToStreet; + public bool IsEmpty => _quantity.Value <= 0; + + public override void OnNetworkSpawn() + { + _quantity.OnValueChanged += (_, _) => RefreshVisuals(); + _productId.OnValueChanged += (_, _) => RefreshVisuals(); + RefreshVisuals(); + + if (IsServer) ShelfRegistry.Instance.Register(this); + } + + public override void OnNetworkDespawn() + { + if (IsServer) ShelfRegistry.Instance.Unregister(this); + } + + public string GetPrompt(ulong clientId) + { + if (IsEmpty) return "Regal befüllen"; + + var product = _catalog.Get(_productId.Value); + string name = product != null ? product.DisplayName : "?"; + return $"{name} — {_quantity.Value}/{Capacity()} — {_price.Value:0.00} €"; + } + + /// Server: Spieler füllt aus der Hand auf. + public void Interact(NetworkObject actor) + { + if (!IsServer) return; + if (!actor.TryGetComponent(out var hands) || hands.IsEmpty) return; + + // Fach ist mit anderer Ware belegt -> nichts tun. + if (!IsEmpty && _productId.Value != hands.ProductId) return; + + int free = Capacity() - _quantity.Value; + if (free <= 0) return; + + int moved = hands.TryGive(Mathf.Min(free, hands.Quantity)); + if (moved <= 0) return; + + if (IsEmpty) + { + _productId.Value = hands.ProductId >= 0 ? hands.ProductId : _productId.Value; + var def = _catalog.Get(_productId.Value); + if (def != null && _price.Value <= 0f) _price.Value = def.RecommendedPrice; + } + + _quantity.Value += moved; + } + + /// Server: Kunde entnimmt ein Stück. + public bool TakeOne() + { + if (!IsServer || IsEmpty) return false; + + _quantity.Value--; + if (_quantity.Value == 0) _productId.Value = -1; + return true; + } + + /// Server: Preis setzen (vom Preis-UI aus). + public void SetPrice(float price) + { + if (!IsServer) return; + _price.Value = Mathf.Max(0.01f, price); + } + + /// Kapazität in Stück, abhängig von der Produktgröße. + private int Capacity() + { + var product = _catalog.Get(_productId.Value); + int size = product != null ? (int)product.ShelfSize : 1; + return Mathf.Max(1, _capacityUnits / size); + } + + private void RefreshVisuals() + { + // TODO: Verpackungs-Prefabs nach Menge ein-/ausblenden (Object Pool). + // Läuft auf allen Peers, rein kosmetisch. + } + } +} diff --git a/Assets/Scripts/Shop/ShelfSlot.cs.meta b/Assets/Scripts/Shop/ShelfSlot.cs.meta new file mode 100644 index 0000000..d714bd7 --- /dev/null +++ b/Assets/Scripts/Shop/ShelfSlot.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2bf1c5efe2d53984e90da832661184d8 \ No newline at end of file diff --git a/Assets/Scripts/Shop/StorageCrate.cs b/Assets/Scripts/Shop/StorageCrate.cs new file mode 100644 index 0000000..5abde2c --- /dev/null +++ b/Assets/Scripts/Shop/StorageCrate.cs @@ -0,0 +1,49 @@ +using AfterHours.Data; +using AfterHours.Interaction; +using AfterHours.Networking; +using Unity.Netcode; +using UnityEngine; + +namespace AfterHours.Shop +{ + /// + /// Lagerkiste im Backroom: hält Nachschub einer einzigen Produktsorte. + /// Spieler nehmen daraus in die Hände auf und tragen die Ware zum Regal + /// (siehe ShelfSlot.Interact). + /// + public sealed class StorageCrate : NetworkBehaviour, IInteractable + { + [SerializeField] private ProductCatalog _catalog; + [SerializeField] private int _productId = -1; + [SerializeField] private int _startingQuantity = 200; + + private readonly NetworkVariable _quantity = new(0); + + public int ProductId => _productId; + public int Quantity => _quantity.Value; + public bool IsEmpty => _quantity.Value <= 0; + + public override void OnNetworkSpawn() + { + if (IsServer) _quantity.Value = _startingQuantity; + } + + public string GetPrompt(ulong clientId) + { + var product = _catalog.Get(_productId); + string name = product != null ? product.DisplayName : "?"; + return IsEmpty ? $"{name} — Lager leer" : $"{name} nehmen — {_quantity.Value} im Lager"; + } + + /// Server: Spieler nimmt Ware in die Hände auf. + public void Interact(NetworkObject actor) + { + if (!IsServer || IsEmpty) return; + if (!actor.TryGetComponent(out var hands)) return; + + int want = Mathf.Min(_quantity.Value, PlayerHands.MaxCarry); + int taken = hands.TryTake(_productId, want); + _quantity.Value -= taken; + } + } +} diff --git a/Assets/Scripts/Shop/StorageCrate.cs.meta b/Assets/Scripts/Shop/StorageCrate.cs.meta new file mode 100644 index 0000000..f7e0bf4 --- /dev/null +++ b/Assets/Scripts/Shop/StorageCrate.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2b8651b0bcbd9a442bc9688879ce2605 \ No newline at end of file diff --git a/Assets/Scripts/Tests.meta b/Assets/Scripts/Tests.meta new file mode 100644 index 0000000..cf58eb7 --- /dev/null +++ b/Assets/Scripts/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 34870c3bd1615004f9e6f2b4a05a7f28 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/EditMode.meta b/Assets/Scripts/Tests/EditMode.meta new file mode 100644 index 0000000..4db4604 --- /dev/null +++ b/Assets/Scripts/Tests/EditMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5855a87ce97ace049915c17c1ba0c9ca +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/EditMode/AfterHours.Tests.asmdef b/Assets/Scripts/Tests/EditMode/AfterHours.Tests.asmdef new file mode 100644 index 0000000..4240239 --- /dev/null +++ b/Assets/Scripts/Tests/EditMode/AfterHours.Tests.asmdef @@ -0,0 +1,13 @@ +{ + "name": "AfterHours.Tests", + "rootNamespace": "AfterHours.Tests", + "references": [ + "AfterHours.Runtime", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": ["Editor"], + "precompiledReferences": ["nunit.framework.dll"], + "defineConstraints": ["UNITY_INCLUDE_TESTS"], + "autoReferenced": false +} diff --git a/Assets/Scripts/Tests/EditMode/AfterHours.Tests.asmdef.meta b/Assets/Scripts/Tests/EditMode/AfterHours.Tests.asmdef.meta new file mode 100644 index 0000000..dc001b6 --- /dev/null +++ b/Assets/Scripts/Tests/EditMode/AfterHours.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 576e5e4dc99f32e4eaebf1eb76385743 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/EditMode/ComfortModelTests.cs b/Assets/Scripts/Tests/EditMode/ComfortModelTests.cs new file mode 100644 index 0000000..216c662 --- /dev/null +++ b/Assets/Scripts/Tests/EditMode/ComfortModelTests.cs @@ -0,0 +1,52 @@ +using AfterHours.Customers; +using NUnit.Framework; + +namespace AfterHours.Tests +{ + public sealed class ComfortModelTests + { + [Test] + public void EmptyStore_LeavesCustomerFullyComfortable() + { + float comfort = ComfortModel.EvaluateTarget(0.5f, 0.8f, 0, false, false); + Assert.AreEqual(1f, comfort, 0.0001f); + } + + [Test] + public void CrowdingReducesComfort() + { + float alone = ComfortModel.EvaluateTarget(0.5f, 0.8f, 0, false, false); + float crowded = ComfortModel.EvaluateTarget(0.5f, 0.8f, 4, false, false); + Assert.Less(crowded, alone); + } + + [Test] + public void ToleranceCushionsTheSameSituation() + { + float shy = ComfortModel.EvaluateTarget(0.1f, 0.9f, 3, true, false); + float bold = ComfortModel.EvaluateTarget(0.9f, 0.9f, 3, true, false); + Assert.Greater(bold, shy); + } + + [Test] + public void HarmlessProductIsUnaffectedByCrowding() + { + float comfort = ComfortModel.EvaluateTarget(0.5f, 0f, 6, true, true); + Assert.AreEqual(1f, comfort, 0.0001f); + } + + [Test] + public void WorstCaseTriggersAbandonment() + { + float comfort = ComfortModel.EvaluateTarget(0f, 1f, 5, true, true); + Assert.IsTrue(ComfortModel.ShouldAbandon(comfort), $"Komfort war {comfort}"); + } + + [Test] + public void RecoveryIsGradual_DiscomfortIsImmediate() + { + Assert.AreEqual(0.3f, ComfortModel.Step(1f, 0.3f, 0.25f), 0.0001f); // sofort + Assert.Less(ComfortModel.Step(0.3f, 1f, 0.25f), 1f); // träge + } + } +} diff --git a/Assets/Scripts/Tests/EditMode/ComfortModelTests.cs.meta b/Assets/Scripts/Tests/EditMode/ComfortModelTests.cs.meta new file mode 100644 index 0000000..3968020 --- /dev/null +++ b/Assets/Scripts/Tests/EditMode/ComfortModelTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ee90a53056bfe9c42bf72d0e7ee60e8a \ No newline at end of file diff --git a/Assets/Scripts/Tests/EditMode/PricingServiceTests.cs b/Assets/Scripts/Tests/EditMode/PricingServiceTests.cs new file mode 100644 index 0000000..fb1f926 --- /dev/null +++ b/Assets/Scripts/Tests/EditMode/PricingServiceTests.cs @@ -0,0 +1,55 @@ +using AfterHours.Economy; +using NUnit.Framework; + +namespace AfterHours.Tests +{ + public sealed class PricingServiceTests + { + [Test] + public void AtOrBelowRecommendedPrice_EveryoneBuys() + { + Assert.AreEqual(1f, PricingService.PurchaseProbability(10f, 12f), 0.0001f); + Assert.AreEqual(1f, PricingService.PurchaseProbability(12f, 12f), 0.0001f); + } + + [Test] + public void AtRejectionThreshold_NobodyBuys() + { + float price = 12f * PricingService.RejectionThreshold; + Assert.AreEqual(0f, PricingService.PurchaseProbability(price, 12f), 0.0001f); + } + + [Test] + public void DemandFallsMonotonically() + { + float previous = 1f; + for (float price = 12f; price < 21f; price += 0.5f) + { + float current = PricingService.PurchaseProbability(price, 12f); + Assert.LessOrEqual(current, previous); + previous = current; + } + } + + [Test] + public void SmallMarkupHurtsLessThanLargeOne() + { + float small = 1f - PricingService.PurchaseProbability(13f, 12f); + float large = 1f - PricingService.PurchaseProbability(19f, 12f); + Assert.Less(small, large * 0.5f); + } + + [Test] + public void ExpectedProfitPeaksAboveWholesale() + { + float best = 0f, bestPrice = 0f; + for (float price = 5f; price < 21f; price += 0.25f) + { + float profit = PricingService.ExpectedProfitPerUnit(price, 12f, 5f); + if (profit > best) { best = profit; bestPrice = price; } + } + Assert.Greater(bestPrice, 5f); + Assert.Greater(best, 0f); + } + } +} diff --git a/Assets/Scripts/Tests/EditMode/PricingServiceTests.cs.meta b/Assets/Scripts/Tests/EditMode/PricingServiceTests.cs.meta new file mode 100644 index 0000000..8bda94f --- /dev/null +++ b/Assets/Scripts/Tests/EditMode/PricingServiceTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 53f7a9ae1e6213b4db8d1f200e402334 \ No newline at end of file diff --git a/Assets/Settings.meta b/Assets/Settings.meta new file mode 100644 index 0000000..39b94dd --- /dev/null +++ b/Assets/Settings.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 709f11a7f3c4041caa4ef136ea32d874 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Settings/DefaultVolumeProfile.asset b/Assets/Settings/DefaultVolumeProfile.asset new file mode 100644 index 0000000..bbaa559 --- /dev/null +++ b/Assets/Settings/DefaultVolumeProfile.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a6f825e8d07fe8b9f3b52e7d4e2dfa0e22c555b22335adb19f0ecc66dfa38dd +size 23987 diff --git a/Assets/Settings/DefaultVolumeProfile.asset.meta b/Assets/Settings/DefaultVolumeProfile.asset.meta new file mode 100644 index 0000000..53b314a --- /dev/null +++ b/Assets/Settings/DefaultVolumeProfile.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ab09877e2e707104187f6f83e2f62510 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Settings/Mobile_RPAsset.asset b/Assets/Settings/Mobile_RPAsset.asset new file mode 100644 index 0000000..fcef451 --- /dev/null +++ b/Assets/Settings/Mobile_RPAsset.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba642b95fe751d42a081a346b4dee94ced67b3e7c8ed8c4f2929f6eb51d55b18 +size 4672 diff --git a/Assets/Settings/Mobile_RPAsset.asset.meta b/Assets/Settings/Mobile_RPAsset.asset.meta new file mode 100644 index 0000000..3660d15 --- /dev/null +++ b/Assets/Settings/Mobile_RPAsset.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5e6cbd92db86f4b18aec3ed561671858 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Settings/Mobile_Renderer.asset b/Assets/Settings/Mobile_Renderer.asset new file mode 100644 index 0000000..e26ada9 --- /dev/null +++ b/Assets/Settings/Mobile_Renderer.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa93274c9521fa941170becec9e87c5fa36b8587eefd5eeb5c0eed0d1d4b1dbe +size 1713 diff --git a/Assets/Settings/Mobile_Renderer.asset.meta b/Assets/Settings/Mobile_Renderer.asset.meta new file mode 100644 index 0000000..a3588b1 --- /dev/null +++ b/Assets/Settings/Mobile_Renderer.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 65bc7dbf4170f435aa868c779acfb082 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Settings/PC_RPAsset.asset b/Assets/Settings/PC_RPAsset.asset new file mode 100644 index 0000000..f9710a7 --- /dev/null +++ b/Assets/Settings/PC_RPAsset.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cfa894e760f2aa087d24b2fa260e2efa497b58b89d52e4c2b28371cf6336aaf +size 4691 diff --git a/Assets/Settings/PC_RPAsset.asset.meta b/Assets/Settings/PC_RPAsset.asset.meta new file mode 100644 index 0000000..e286b2f --- /dev/null +++ b/Assets/Settings/PC_RPAsset.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4b83569d67af61e458304325a23e5dfd +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Settings/PC_Renderer.asset b/Assets/Settings/PC_Renderer.asset new file mode 100644 index 0000000..1d091f9 --- /dev/null +++ b/Assets/Settings/PC_Renderer.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d0b01d8ded3327263b6690be128a3de58d17c514943ae42fe743a3e084a9c79 +size 3439 diff --git a/Assets/Settings/PC_Renderer.asset.meta b/Assets/Settings/PC_Renderer.asset.meta new file mode 100644 index 0000000..ddae6a5 --- /dev/null +++ b/Assets/Settings/PC_Renderer.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f288ae1f4751b564a96ac7587541f7a2 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Settings/SampleSceneProfile.asset b/Assets/Settings/SampleSceneProfile.asset new file mode 100644 index 0000000..8d640bc --- /dev/null +++ b/Assets/Settings/SampleSceneProfile.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8677d4bac051d24caa11cd622e39f968155ee277bfce132274f1de225692fe9 +size 3703 diff --git a/Assets/Settings/SampleSceneProfile.asset.meta b/Assets/Settings/SampleSceneProfile.asset.meta new file mode 100644 index 0000000..b82270c --- /dev/null +++ b/Assets/Settings/SampleSceneProfile.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 10fc4df2da32a41aaa32d77bc913491c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset b/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset new file mode 100644 index 0000000..fa98d05 --- /dev/null +++ b/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10a1c871cc7ef662d0a7daec5cd9aaa670586c2251f1ce89117fc0435a5f86d7 +size 26897 diff --git a/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset.meta b/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset.meta new file mode 100644 index 0000000..81b84f2 --- /dev/null +++ b/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 18dc0cd2c080841dea60987a38ce93fa +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TutorialInfo.meta b/Assets/TutorialInfo.meta new file mode 100644 index 0000000..a700bca --- /dev/null +++ b/Assets/TutorialInfo.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ba062aa6c92b140379dbc06b43dd3b9b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TutorialInfo/Icons.meta b/Assets/TutorialInfo/Icons.meta new file mode 100644 index 0000000..1d19fb9 --- /dev/null +++ b/Assets/TutorialInfo/Icons.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 8a0c9218a650547d98138cd835033977 +folderAsset: yes +timeCreated: 1484670163 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TutorialInfo/Icons/URP.png b/Assets/TutorialInfo/Icons/URP.png new file mode 100644 index 0000000..bdbf408 --- /dev/null +++ b/Assets/TutorialInfo/Icons/URP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d17a9ff3537859abe2c008603d29d90cf43191ae77aa386167cc65e53ba03d9 +size 24069 diff --git a/Assets/TutorialInfo/Icons/URP.png.meta b/Assets/TutorialInfo/Icons/URP.png.meta new file mode 100644 index 0000000..0f2cab0 --- /dev/null +++ b/Assets/TutorialInfo/Icons/URP.png.meta @@ -0,0 +1,134 @@ +fileFormatVersion: 2 +guid: 727a75301c3d24613a3ebcec4a24c2c8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TutorialInfo/Layout.wlt b/Assets/TutorialInfo/Layout.wlt new file mode 100644 index 0000000..7b50a25 --- /dev/null +++ b/Assets/TutorialInfo/Layout.wlt @@ -0,0 +1,654 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12004, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_PixelRect: + serializedVersion: 2 + x: 0 + y: 45 + width: 1666 + height: 958 + m_ShowMode: 4 + m_Title: + m_RootView: {fileID: 6} + m_MinSize: {x: 950, y: 542} + m_MaxSize: {x: 10000, y: 10000} +--- !u!114 &2 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 466 + width: 290 + height: 442 + m_MinSize: {x: 234, y: 271} + m_MaxSize: {x: 10004, y: 10021} + m_ActualView: {fileID: 14} + m_Panes: + - {fileID: 14} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &3 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 4} + - {fileID: 2} + m_Position: + serializedVersion: 2 + x: 973 + y: 0 + width: 290 + height: 908 + m_MinSize: {x: 234, y: 492} + m_MaxSize: {x: 10004, y: 14042} + vertical: 1 + controlID: 226 +--- !u!114 &4 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 290 + height: 466 + m_MinSize: {x: 204, y: 221} + m_MaxSize: {x: 4004, y: 4021} + m_ActualView: {fileID: 17} + m_Panes: + - {fileID: 17} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &5 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 466 + width: 973 + height: 442 + m_MinSize: {x: 202, y: 221} + m_MaxSize: {x: 4002, y: 4021} + m_ActualView: {fileID: 15} + m_Panes: + - {fileID: 15} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &6 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12008, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 7} + - {fileID: 8} + - {fileID: 9} + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 1666 + height: 958 + m_MinSize: {x: 950, y: 542} + m_MaxSize: {x: 10000, y: 10000} +--- !u!114 &7 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12011, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 1666 + height: 30 + m_MinSize: {x: 0, y: 0} + m_MaxSize: {x: 0, y: 0} + m_LastLoadedLayoutName: Tutorial +--- !u!114 &8 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 10} + - {fileID: 3} + - {fileID: 11} + m_Position: + serializedVersion: 2 + x: 0 + y: 30 + width: 1666 + height: 908 + m_MinSize: {x: 713, y: 492} + m_MaxSize: {x: 18008, y: 14042} + vertical: 0 + controlID: 74 +--- !u!114 &9 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12042, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 938 + width: 1666 + height: 20 + m_MinSize: {x: 0, y: 0} + m_MaxSize: {x: 0, y: 0} +--- !u!114 &10 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 12} + - {fileID: 5} + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 973 + height: 908 + m_MinSize: {x: 202, y: 442} + m_MaxSize: {x: 4002, y: 8042} + vertical: 1 + controlID: 75 +--- !u!114 &11 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 1263 + y: 0 + width: 403 + height: 908 + m_MinSize: {x: 277, y: 71} + m_MaxSize: {x: 4002, y: 4021} + m_ActualView: {fileID: 13} + m_Panes: + - {fileID: 13} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &12 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 973 + height: 466 + m_MinSize: {x: 202, y: 221} + m_MaxSize: {x: 4002, y: 4021} + m_ActualView: {fileID: 16} + m_Panes: + - {fileID: 16} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &13 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12019, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_AutoRepaintOnSceneChange: 0 + m_MinSize: {x: 275, y: 50} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Inspector + m_Image: {fileID: -6905738622615590433, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_DepthBufferBits: 0 + m_Pos: + serializedVersion: 2 + x: 2 + y: 19 + width: 401 + height: 887 + m_ScrollPosition: {x: 0, y: 0} + m_InspectorMode: 0 + m_PreviewResizer: + m_CachedPref: -160 + m_ControlHash: -371814159 + m_PrefName: Preview_InspectorPreview + m_PreviewWindow: {fileID: 0} +--- !u!114 &14 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12014, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_AutoRepaintOnSceneChange: 0 + m_MinSize: {x: 230, y: 250} + m_MaxSize: {x: 10000, y: 10000} + m_TitleContent: + m_Text: Project + m_Image: {fileID: -7501376956915960154, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_DepthBufferBits: 0 + m_Pos: + serializedVersion: 2 + x: 2 + y: 19 + width: 286 + height: 421 + m_SearchFilter: + m_NameFilter: + m_ClassNames: [] + m_AssetLabels: [] + m_AssetBundleNames: [] + m_VersionControlStates: [] + m_ReferencingInstanceIDs: + m_ScenePaths: [] + m_ShowAllHits: 0 + m_SearchArea: 0 + m_Folders: + - Assets + m_ViewMode: 0 + m_StartGridSize: 64 + m_LastFolders: + - Assets + m_LastFoldersGridSize: -1 + m_LastProjectPath: /Users/danielbrauer/Unity Projects/New Unity Project 47 + m_IsLocked: 0 + m_FolderTreeState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: ee240000 + m_LastClickedID: 9454 + m_ExpandedIDs: ee24000000ca9a3bffffff7f + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_AssetTreeState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: 68fbffff + m_LastClickedID: 0 + m_ExpandedIDs: ee240000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_ListAreaState: + m_SelectedInstanceIDs: 68fbffff + m_LastClickedInstanceID: -1176 + m_HadKeyboardFocusLastEvent: 0 + m_ExpandedInstanceIDs: c6230000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 0} + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_NewAssetIndexInList: -1 + m_ScrollPosition: {x: 0, y: 0} + m_GridSize: 64 + m_DirectoriesAreaWidth: 110 +--- !u!114 &15 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12015, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_AutoRepaintOnSceneChange: 1 + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Game + m_Image: {fileID: -2087823869225018852, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_DepthBufferBits: 32 + m_Pos: + serializedVersion: 2 + x: 0 + y: 19 + width: 971 + height: 421 + m_MaximizeOnPlay: 0 + m_Gizmos: 0 + m_Stats: 0 + m_SelectedSizes: 00000000000000000000000000000000000000000000000000000000000000000000000000000000 + m_TargetDisplay: 0 + m_ZoomArea: + m_HRangeLocked: 0 + m_VRangeLocked: 0 + m_HBaseRangeMin: -242.75 + m_HBaseRangeMax: 242.75 + m_VBaseRangeMin: -101 + m_VBaseRangeMax: 101 + m_HAllowExceedBaseRangeMin: 1 + m_HAllowExceedBaseRangeMax: 1 + m_VAllowExceedBaseRangeMin: 1 + m_VAllowExceedBaseRangeMax: 1 + m_ScaleWithWindow: 0 + m_HSlider: 0 + m_VSlider: 0 + m_IgnoreScrollWheelUntilClicked: 0 + m_EnableMouseInput: 1 + m_EnableSliderZoom: 0 + m_UniformScale: 1 + m_UpDirection: 1 + m_DrawArea: + serializedVersion: 2 + x: 0 + y: 17 + width: 971 + height: 404 + m_Scale: {x: 2, y: 2} + m_Translation: {x: 485.5, y: 202} + m_MarginLeft: 0 + m_MarginRight: 0 + m_MarginTop: 0 + m_MarginBottom: 0 + m_LastShownAreaInsideMargins: + serializedVersion: 2 + x: -242.75 + y: -101 + width: 485.5 + height: 202 + m_MinimalGUI: 1 + m_defaultScale: 2 + m_TargetTexture: {fileID: 0} + m_CurrentColorSpace: 0 + m_LastWindowPixelSize: {x: 1942, y: 842} + m_ClearInEditMode: 1 + m_NoCameraWarning: 1 + m_LowResolutionForAspectRatios: 01000000000100000100 +--- !u!114 &16 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12013, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_AutoRepaintOnSceneChange: 1 + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Scene + m_Image: {fileID: 2318424515335265636, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_DepthBufferBits: 32 + m_Pos: + serializedVersion: 2 + x: 0 + y: 19 + width: 971 + height: 445 + m_SceneLighting: 1 + lastFramingTime: 0 + m_2DMode: 0 + m_isRotationLocked: 0 + m_AudioPlay: 0 + m_Position: + m_Target: {x: 0, y: 0, z: 0} + speed: 2 + m_Value: {x: 0, y: 0, z: 0} + m_RenderMode: 0 + m_ValidateTrueMetals: 0 + m_SceneViewState: + showFog: 1 + showMaterialUpdate: 0 + showSkybox: 1 + showFlares: 1 + showImageEffects: 1 + grid: + xGrid: + m_Target: 0 + speed: 2 + m_Value: 0 + yGrid: + m_Target: 1 + speed: 2 + m_Value: 1 + zGrid: + m_Target: 0 + speed: 2 + m_Value: 0 + m_Rotation: + m_Target: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} + speed: 2 + m_Value: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} + m_Size: + m_Target: 10 + speed: 2 + m_Value: 10 + m_Ortho: + m_Target: 0 + speed: 2 + m_Value: 0 + m_LastSceneViewRotation: {x: 0, y: 0, z: 0, w: 0} + m_LastSceneViewOrtho: 0 + m_ReplacementShader: {fileID: 0} + m_ReplacementString: + m_LastLockedObject: {fileID: 0} + m_ViewIsLockedToObject: 0 +--- !u!114 &17 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12061, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_AutoRepaintOnSceneChange: 0 + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Hierarchy + m_Image: {fileID: -590624980919486359, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_DepthBufferBits: 0 + m_Pos: + serializedVersion: 2 + x: 2 + y: 19 + width: 286 + height: 445 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: 68fbffff + m_LastClickedID: -1176 + m_ExpandedIDs: 7efbffff00000000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ExpandedScenes: + - + m_CurrenRootInstanceID: 0 + m_Locked: 0 + m_CurrentSortingName: TransformSorting diff --git a/Assets/TutorialInfo/Layout.wlt.meta b/Assets/TutorialInfo/Layout.wlt.meta new file mode 100644 index 0000000..c0c8c77 --- /dev/null +++ b/Assets/TutorialInfo/Layout.wlt.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eabc9546105bf4accac1fd62a63e88e6 +timeCreated: 1487337779 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts.meta b/Assets/TutorialInfo/Scripts.meta new file mode 100644 index 0000000..02da605 --- /dev/null +++ b/Assets/TutorialInfo/Scripts.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 5a9bcd70e6a4b4b05badaa72e827d8e0 +folderAsset: yes +timeCreated: 1475835190 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts/Editor.meta b/Assets/TutorialInfo/Scripts/Editor.meta new file mode 100644 index 0000000..f59f099 --- /dev/null +++ b/Assets/TutorialInfo/Scripts/Editor.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 3ad9b87dffba344c89909c6d1b1c17e1 +folderAsset: yes +timeCreated: 1475593892 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs b/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs new file mode 100644 index 0000000..ad55eca --- /dev/null +++ b/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs @@ -0,0 +1,242 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using System; +using System.IO; +using System.Reflection; + +[CustomEditor(typeof(Readme))] +[InitializeOnLoad] +public class ReadmeEditor : Editor +{ + static string s_ShowedReadmeSessionStateName = "ReadmeEditor.showedReadme"; + + static string s_ReadmeSourceDirectory = "Assets/TutorialInfo"; + + const float k_Space = 16f; + + static ReadmeEditor() + { + EditorApplication.delayCall += SelectReadmeAutomatically; + } + + static void RemoveTutorial() + { + if (EditorUtility.DisplayDialog("Remove Readme Assets", + + $"All contents under {s_ReadmeSourceDirectory} will be removed, are you sure you want to proceed?", + "Proceed", + "Cancel")) + { + if (Directory.Exists(s_ReadmeSourceDirectory)) + { + FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory); + FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory + ".meta"); + } + else + { + Debug.Log($"Could not find the Readme folder at {s_ReadmeSourceDirectory}"); + } + + var readmeAsset = SelectReadme(); + if (readmeAsset != null) + { + var path = AssetDatabase.GetAssetPath(readmeAsset); + FileUtil.DeleteFileOrDirectory(path + ".meta"); + FileUtil.DeleteFileOrDirectory(path); + } + + AssetDatabase.Refresh(); + } + } + + static void SelectReadmeAutomatically() + { + if (!SessionState.GetBool(s_ShowedReadmeSessionStateName, false)) + { + var readme = SelectReadme(); + SessionState.SetBool(s_ShowedReadmeSessionStateName, true); + + if (readme && !readme.loadedLayout) + { + LoadLayout(); + readme.loadedLayout = true; + } + } + } + + static void LoadLayout() + { + var assembly = typeof(EditorApplication).Assembly; + var windowLayoutType = assembly.GetType("UnityEditor.WindowLayout", true); + var method = windowLayoutType.GetMethod("LoadWindowLayout", BindingFlags.Public | BindingFlags.Static); + method.Invoke(null, new object[] { Path.Combine(Application.dataPath, "TutorialInfo/Layout.wlt"), false }); + } + + static Readme SelectReadme() + { + var ids = AssetDatabase.FindAssets("Readme t:Readme"); + if (ids.Length == 1) + { + var readmeObject = AssetDatabase.LoadMainAssetAtPath(AssetDatabase.GUIDToAssetPath(ids[0])); + + Selection.objects = new UnityEngine.Object[] { readmeObject }; + + return (Readme)readmeObject; + } + else + { + Debug.Log("Couldn't find a readme"); + return null; + } + } + + protected override void OnHeaderGUI() + { + var readme = (Readme)target; + Init(); + + var iconWidth = Mathf.Min(EditorGUIUtility.currentViewWidth / 3f - 20f, 128f); + + GUILayout.BeginHorizontal("In BigTitle"); + { + if (readme.icon != null) + { + GUILayout.Space(k_Space); + GUILayout.Label(readme.icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth)); + } + GUILayout.Space(k_Space); + GUILayout.BeginVertical(); + { + + GUILayout.FlexibleSpace(); + GUILayout.Label(readme.title, TitleStyle); + GUILayout.FlexibleSpace(); + } + GUILayout.EndVertical(); + GUILayout.FlexibleSpace(); + } + GUILayout.EndHorizontal(); + } + + public override void OnInspectorGUI() + { + var readme = (Readme)target; + Init(); + + foreach (var section in readme.sections) + { + if (!string.IsNullOrEmpty(section.heading)) + { + GUILayout.Label(section.heading, HeadingStyle); + } + + if (!string.IsNullOrEmpty(section.text)) + { + GUILayout.Label(section.text, BodyStyle); + } + + if (!string.IsNullOrEmpty(section.linkText)) + { + if (LinkLabel(new GUIContent(section.linkText))) + { + Application.OpenURL(section.url); + } + } + + GUILayout.Space(k_Space); + } + + if (GUILayout.Button("Remove Readme Assets", ButtonStyle)) + { + RemoveTutorial(); + } + } + + bool m_Initialized; + + GUIStyle LinkStyle + { + get { return m_LinkStyle; } + } + + [SerializeField] + GUIStyle m_LinkStyle; + + GUIStyle TitleStyle + { + get { return m_TitleStyle; } + } + + [SerializeField] + GUIStyle m_TitleStyle; + + GUIStyle HeadingStyle + { + get { return m_HeadingStyle; } + } + + [SerializeField] + GUIStyle m_HeadingStyle; + + GUIStyle BodyStyle + { + get { return m_BodyStyle; } + } + + [SerializeField] + GUIStyle m_BodyStyle; + + GUIStyle ButtonStyle + { + get { return m_ButtonStyle; } + } + + [SerializeField] + GUIStyle m_ButtonStyle; + + void Init() + { + if (m_Initialized) + return; + m_BodyStyle = new GUIStyle(EditorStyles.label); + m_BodyStyle.wordWrap = true; + m_BodyStyle.fontSize = 14; + m_BodyStyle.richText = true; + + m_TitleStyle = new GUIStyle(m_BodyStyle); + m_TitleStyle.fontSize = 26; + + m_HeadingStyle = new GUIStyle(m_BodyStyle); + m_HeadingStyle.fontStyle = FontStyle.Bold; + m_HeadingStyle.fontSize = 18; + + m_LinkStyle = new GUIStyle(m_BodyStyle); + m_LinkStyle.wordWrap = false; + + // Match selection color which works nicely for both light and dark skins + m_LinkStyle.normal.textColor = new Color(0x00 / 255f, 0x78 / 255f, 0xDA / 255f, 1f); + m_LinkStyle.stretchWidth = false; + + m_ButtonStyle = new GUIStyle(EditorStyles.miniButton); + m_ButtonStyle.fontStyle = FontStyle.Bold; + + m_Initialized = true; + } + + bool LinkLabel(GUIContent label, params GUILayoutOption[] options) + { + var position = GUILayoutUtility.GetRect(label, LinkStyle, options); + + Handles.BeginGUI(); + Handles.color = LinkStyle.normal.textColor; + Handles.DrawLine(new Vector3(position.xMin, position.yMax), new Vector3(position.xMax, position.yMax)); + Handles.color = Color.white; + Handles.EndGUI(); + + EditorGUIUtility.AddCursorRect(position, MouseCursor.Link); + + return GUI.Button(position, label, LinkStyle); + } +} diff --git a/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta b/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta new file mode 100644 index 0000000..f038618 --- /dev/null +++ b/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 476cc7d7cd9874016adc216baab94a0a +timeCreated: 1484146680 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts/Readme.cs b/Assets/TutorialInfo/Scripts/Readme.cs new file mode 100644 index 0000000..95f6269 --- /dev/null +++ b/Assets/TutorialInfo/Scripts/Readme.cs @@ -0,0 +1,16 @@ +using System; +using UnityEngine; + +public class Readme : ScriptableObject +{ + public Texture2D icon; + public string title; + public Section[] sections; + public bool loadedLayout; + + [Serializable] + public class Section + { + public string heading, text, linkText, url; + } +} diff --git a/Assets/TutorialInfo/Scripts/Readme.cs.meta b/Assets/TutorialInfo/Scripts/Readme.cs.meta new file mode 100644 index 0000000..935153f --- /dev/null +++ b/Assets/TutorialInfo/Scripts/Readme.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: fcf7219bab7fe46a1ad266029b2fee19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - icon: {instanceID: 0} + executionOrder: 0 + icon: {fileID: 2800000, guid: a186f8a87ca4f4d3aa864638ad5dfb65, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9b07c69 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,143 @@ +# CLAUDE.md — AfterHours (Boutique Simulator) + +Diese Datei ist die Arbeitsanweisung für Claude Code in diesem Repo. +Lies sie vollständig, bevor du Code änderst. + +--- + +## 1. Projekt in einem Satz + +Koop-Ladensimulator (2–4 Spieler) für PC: Die Spieler betreiben gemeinsam eine +diskrete Erwachsenen-Boutique — Ware bestellen, einräumen, bepreisen, Kunden +beraten, kassieren, Laden ausbauen. + +**Codename:** AfterHours +**Zielplattform:** Windows (Steam), später Linux/Proton +**Zielrating:** Mature, NICHT Adult-Only. Siehe §7 — das ist eine harte Constraint. + +--- + +## 2. Tech-Stack + +| Bereich | Entscheidung | +|--------------|-----------------------------------------------| +| Engine | Unity 6 LTS (6000.0.x) | +| Sprache | C# (.NET Standard 2.1) | +| Netcode | Unity Netcode for GameObjects (NGO) 2.x | +| Transport | Unity Transport + Steam Relay (Facepunch) | +| KI-Pathing | Unity NavMesh (NavMeshAgent, nur serverseitig)| +| Tests | Unity Test Framework (NUnit), EditMode | +| Daten | ScriptableObjects (`Assets/Data/...`) | + +**Warum NGO und nicht FishNet:** erstklassige Doku, First-Party-Support, reicht +für 4 Spieler + ~30 NPCs vollkommen aus. Nicht ohne Rücksprache wechseln. + +--- + +## 3. Die wichtigste Regel: Server-Authority + +> **Der Host simuliert. Clients fragen und zeigen an.** + +Alles, was Geld, Inventar, Warenbestand oder Kundenlogik betrifft, läuft +**ausschließlich** auf dem Host. Clients senden Absichten via `ServerRpc`, +der Server validiert und schreibt in `NetworkVariable`s. + +**Konkret:** +- ✅ `[Rpc(SendTo.Server)]` → validieren → `NetworkVariable` setzen +- ❌ Niemals Geld/Bestand clientseitig verändern und "hoffen" +- ❌ Niemals einem Client-Wert vertrauen, ohne ihn zu prüfen + (Beispiel: Client sagt "ich scanne Produkt X" → Server prüft, ob der Client + dieses Produkt tatsächlich in der Hand hält) +- Kunden-NPCs: Bewegung + Entscheidungen nur auf dem Server. + Clients bekommen nur Transform-Sync und Zustandsflags. + +Jede Methode, die Zustand ändert, wird mit `if (!IsServer) return;` +abgesichert — auch wenn sie "eigentlich" nur serverseitig aufgerufen wird. + +--- + +## 4. Ordnerstruktur + +``` +Assets/Scripts/ +├── Core/ GameClock, ShopEconomy, GameBootstrap — Kern-Loop & Zustand +├── Data/ ScriptableObjects: ProductDefinition, ProductCatalog, Enums +├── Economy/ PricingService, SupplierService, PurchaseOrder +├── Shop/ Shelf, ShelfSlot, CashRegister, StorageCrate +├── Customers/ CustomerAgent, CustomerBrain (FSM), ComfortModel, Spawner +├── Interaction/ IInteractable, PlayerInteractor, Carryable +├── Networking/ NetworkBootstrap, PlayerAvatar, PlayerHands +└── Tests/EditMode/ NUnit-Tests für reine Logik +``` + +**Namespaces spiegeln Ordner:** `AfterHours.Economy`, `AfterHours.Customers`, … + +--- + +## 5. Code-Konventionen + +- Klassen/Methoden `PascalCase`, Felder `_camelCase` mit Unterstrich, + Konstanten `PascalCase`. +- `[SerializeField] private` statt `public` für Inspector-Felder. +- **Reine Logik gehört in statische, engine-freie Klassen** (`PricingService`, + `ComfortModel`). Nur die sind testbar — MonoBehaviours sind es nicht. + Wenn du eine Formel schreibst und sie in einem `MonoBehaviour` landet: + rausziehen. +- Produkte werden im Netzwerk **nie als Referenz**, sondern als + `ProductId` (int) übertragen. Auflösung über `ProductCatalog`. +- Keine `FindObjectOfType` im Frame-Loop. Referenzen im `Awake` cachen. +- Kein `async void` außer in Event-Handlern. + +--- + +## 6. Tests + +```bash +# Unity CLI (Pfad ggf. anpassen) +Unity -runTests -batchmode -projectPath . -testPlatform EditMode +``` + +Neue Logik in `Economy/` oder `Customers/` **braucht** einen EditMode-Test. +Netzwerk- und Szenenlogik wird nicht getestet — dort zählt manuelles +Host+Client-Testing (siehe `docs/ARCHITECTURE.md`). + +--- + +## 7. Content-Constraint (nicht verhandelbar) + +Das Spiel darf **nicht** in Steams "Adult Only"-Kategorie landen, weil das +Sichtbarkeit, Regionen und Zahlungsdienstleister massiv einschränkt. + +**Deshalb gilt:** +- Produkte werden als **stilisierte Verpackungen / Icons** dargestellt, + nie explizit modelliert. +- Kein sexuell explizites Bildmaterial, keine Nacktheit, keine sexuellen Akte. +- Humor ist trocken und situativ (Verlegenheit, Beratungspannen, + Stoßzeiten-Chaos) — nicht anzüglich. +- Ton der Kundendialoge: höflich, vage, alltäglich. Die Komik entsteht aus + dem, was *nicht* gesagt wird. + +Wenn ein Feature-Vorschlag dagegen verstößt: melde es, statt es umzusetzen. + +--- + +## 8. Aktueller Stand + +Gerüst steht. Implementiert sind Datenmodell, Preis-/Komfort-Formeln, +Regal-/Kassen-Grundgerüst und der Netzwerk-Bootstrap. Es existieren **noch +keine Unity-Szenen, Prefabs oder Assets** — die musst du bzw. der Nutzer im +Editor anlegen. + +Offene Punkte in Prioritätsreihenfolge: siehe `docs/ROADMAP.md`. +`// TODO:`-Marker im Code zeigen die konkreten Lücken. + +--- + +## 9. Arbeitsweise mit mir + +- Bevor du eine neue Datei anlegst: prüfe, ob die Logik in eine bestehende + gehört. Das Projekt soll klein und lesbar bleiben. +- Ändere nie mehrere Systeme in einem Rutsch. Ein System, ein Commit. +- Wenn eine Anforderung mit §3 (Server-Authority) oder §7 (Content) kollidiert: + nachfragen, nicht raten. +- Commit-Messages: `feat(economy): ...`, `fix(customers): ...`, `docs: ...` diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..64fa81e --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,36 @@ +# Design-Kern + +## Der Loop +Bestellen → Einräumen → Bepreisen → Beraten → Kassieren → Ausbauen + +## Was das Spiel von anderen Shop-Sims unterscheidet + +**1. Komfort als Ressource.** +Kunden haben einen Komfortwert. Enge Gänge, Personal, das ungefragt danebensteht, +und Regale mit Sichtachse zum Schaufenster senken ihn. Fällt er unter 25 %, legt +der Kunde die Ware zurück und geht. Das macht *Ladenlayout* zur zentralen +Designentscheidung — nicht Regalfläche, sondern Blickachsen. + +**2. Beratung statt bloßem Kassieren.** +Kunden umschreiben, was sie suchen. Der Spieler leitet daraus die Kategorie ab. +Treffer = größerer Warenkorb + Stammkunde. Fehlgriff = peinliche Stille und ein +Kunde, der schneller geht. + +**3. Sortimentspolitik.** +Wellness zieht Laufkundschaft mit kleiner Marge. Specialty bringt Marge, verlangt +aber Diskretion und Beratung. Wer beides mischt, braucht räumliche Trennung. + +## Koop-Rollen (Anspielung an Overcooked in Stoßzeiten) +| Rolle | Aufgabe | +|----------|--------------------------------------| +| Kasse | Scannen, Schlange abarbeiten | +| Lager | Kisten holen, Regale auffüllen | +| Beratung | Kunden mit `SeekingAdvice` bedienen | +| Einkauf | Bestellungen, Preise, Layout | + +Die Rollen sind nicht hart zugewiesen — die Rush-Kurve erzwingt Arbeitsteilung +von selbst. + +## Ton +Trocken, situativ, nie anzüglich. Die Komik entsteht aus Verlegenheit und +Zeitdruck, nicht aus dem Produkt. Siehe CLAUDE.md §7. diff --git a/Packages/manifest.json b/Packages/manifest.json new file mode 100644 index 0000000..72274b0 --- /dev/null +++ b/Packages/manifest.json @@ -0,0 +1,50 @@ +{ + "dependencies": { + "com.unity.ai.navigation": "2.0.14", + "com.unity.collab-proxy": "2.13.5", + "com.unity.ide.rider": "3.0.38", + "com.unity.ide.visualstudio": "2.0.26", + "com.unity.inputsystem": "1.20.0", + "com.unity.multiplayer.center": "1.0.1", + "com.unity.netcode.gameobjects": "2.13.1", + "com.unity.render-pipelines.universal": "17.5.0", + "com.unity.test-framework": "1.7.0", + "com.unity.timeline": "1.8.12", + "com.unity.ugui": "2.5.0", + "com.unity.visualscripting": "1.9.12", + "com.unity.modules.accessibility": "1.0.0", + "com.unity.modules.adaptiveperformance": "1.0.0", + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.cloth": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.physicscore2d": "1.0.0", + "com.unity.modules.screencapture": "1.0.0", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.terrainphysics": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.umbra": "1.0.0", + "com.unity.modules.unityanalytics": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vectorgraphics": "1.0.0", + "com.unity.modules.vehicles": "1.0.0", + "com.unity.modules.video": "1.0.0", + "com.unity.modules.wind": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } +} diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json new file mode 100644 index 0000000..e936cbe --- /dev/null +++ b/Packages/packages-lock.json @@ -0,0 +1,492 @@ +{ + "dependencies": { + "com.unity.ai.navigation": { + "version": "2.0.14", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.ai": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.burst": { + "version": "1.8.30", + "depth": 2, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collab-proxy": { + "version": "2.13.5", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.collections": "2.6.8" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collections": { + "version": "6.5.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.burst": "1.8.25", + "com.unity.nuget.mono-cecil": "1.11.6", + "com.unity.test-framework": "1.4.6", + "com.unity.test-framework.performance": "3.2.0" + } + }, + "com.unity.ext.nunit": { + "version": "2.1.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, + "com.unity.ide.rider": { + "version": "3.0.38", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.visualstudio": { + "version": "2.0.26", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.33" + }, + "url": "https://packages.unity.com" + }, + "com.unity.inputsystem": { + "version": "1.20.0", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.uielements": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.mathematics": { + "version": "1.4.0", + "depth": 2, + "source": "builtin", + "dependencies": {} + }, + "com.unity.multiplayer.center": { + "version": "1.0.1", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.uielements": "1.0.0" + } + }, + "com.unity.netcode.gameobjects": { + "version": "2.13.1", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.transport": "2.6.0", + "com.unity.nuget.mono-cecil": "1.11.4" + }, + "url": "https://packages.unity.com" + }, + "com.unity.nuget.mono-cecil": { + "version": "1.11.6", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.render-pipelines.core": { + "version": "17.5.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.burst": "1.8.14", + "com.unity.collections": "2.4.3", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.render-pipelines.universal": { + "version": "17.5.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.5.0", + "com.unity.shadergraph": "17.5.0", + "com.unity.render-pipelines.universal-config": "17.5.0" + } + }, + "com.unity.render-pipelines.universal-config": { + "version": "17.5.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.5.0" + } + }, + "com.unity.searcher": { + "version": "4.9.4", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.shadergraph": { + "version": "17.5.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.5.0", + "com.unity.searcher": "4.9.3" + } + }, + "com.unity.test-framework": { + "version": "1.7.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.ext.nunit": "2.1.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.test-framework.performance": { + "version": "3.5.0", + "depth": 2, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.33", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.timeline": { + "version": "1.8.12", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.transport": { + "version": "6.5.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.collections": "6.5.0", + "com.unity.burst": "1.8.24", + "com.unity.mathematics": "1.4.0" + } + }, + "com.unity.ugui": { + "version": "2.5.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.visualscripting": { + "version": "1.9.12", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "2.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.modules.accessibility": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.adaptiveperformance": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.subsystems": "1.0.0" + } + }, + "com.unity.modules.ai": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.androidjni": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.animation": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.assetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.audio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.cloth": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.director": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.hierarchycore": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imageconversion": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imgui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.jsonserialize": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physicscore2d": "1.0.0" + } + }, + "com.unity.modules.physicscore2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.screencapture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.subsystems": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.terrainphysics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.terrain": "1.0.0" + } + }, + "com.unity.modules.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics2d": "1.0.0" + } + }, + "com.unity.modules.ui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.uielements": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.hierarchycore": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.umbra": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unityanalytics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unitywebrequestassetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestaudio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.audio": "1.0.0" + } + }, + "com.unity.modules.unitywebrequesttexture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestwww": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.vectorgraphics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, + "com.unity.modules.vehicles": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.video": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.wind": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.xr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.subsystems": "1.0.0" + } + } + } +} diff --git a/ProjectSettings/AudioManager.asset b/ProjectSettings/AudioManager.asset new file mode 100644 index 0000000..b19c873 --- /dev/null +++ b/ProjectSettings/AudioManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aeedeb3243f07b61dbbe854aa08b5ef6f0b5274dd9852ad9f6129dde3e105a31 +size 413 diff --git a/ProjectSettings/ClusterInputManager.asset b/ProjectSettings/ClusterInputManager.asset new file mode 100644 index 0000000..53b8289 --- /dev/null +++ b/ProjectSettings/ClusterInputManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40e65dd214bf254955d2c8829ca8ce2130fd3510485560ce012e9a1ddd93e623 +size 114 diff --git a/ProjectSettings/DynamicsManager.asset b/ProjectSettings/DynamicsManager.asset new file mode 100644 index 0000000..104a796 --- /dev/null +++ b/ProjectSettings/DynamicsManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7c9a9dc08b98990109d4346f68745e15e238de400f96c583e53e1c83c743a78 +size 1254 diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset new file mode 100644 index 0000000..9729841 --- /dev/null +++ b/ProjectSettings/EditorBuildSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d1712535531522f729d34e843ea20772c01d002227338f8f802f472d7e8d78e +size 371 diff --git a/ProjectSettings/EditorSettings.asset b/ProjectSettings/EditorSettings.asset new file mode 100644 index 0000000..86fcdf9 --- /dev/null +++ b/ProjectSettings/EditorSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20e7399984b858ef7485c4e58f26a42345169749cf0d3a63efaaa82b33f8a883 +size 1687 diff --git a/ProjectSettings/GraphicsSettings.asset b/ProjectSettings/GraphicsSettings.asset new file mode 100644 index 0000000..68d26aa --- /dev/null +++ b/ProjectSettings/GraphicsSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac0d402c463fc830a46a1ddfab132e617bb2ee00dbc9834784d6df415014768 +size 2957 diff --git a/ProjectSettings/InputManager.asset b/ProjectSettings/InputManager.asset new file mode 100644 index 0000000..f002ee5 --- /dev/null +++ b/ProjectSettings/InputManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a1bb836a1008d26d0860d143e9d9df7ca41731d30a72901b518063819c62df2 +size 9731 diff --git a/ProjectSettings/MemorySettings.asset b/ProjectSettings/MemorySettings.asset new file mode 100644 index 0000000..cd6af44 --- /dev/null +++ b/ProjectSettings/MemorySettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e8379a461b10ae09f3f696911088c16875d63e87ed5f2d95df9e1892703f9fd +size 1192 diff --git a/ProjectSettings/MultiplayerManager.asset b/ProjectSettings/MultiplayerManager.asset new file mode 100644 index 0000000..a241e60 --- /dev/null +++ b/ProjectSettings/MultiplayerManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aba89fd1a1ddad182727ad82727e536d27372e90328a8196da2a4560398983dd +size 157 diff --git a/ProjectSettings/NavMeshAreas.asset b/ProjectSettings/NavMeshAreas.asset new file mode 100644 index 0000000..c788824 --- /dev/null +++ b/ProjectSettings/NavMeshAreas.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80106cfa7e6181a1928b142c075f3e510d527441e3d79bc83590e4ac91e7cd66 +size 1308 diff --git a/ProjectSettings/PackageManagerSettings.asset b/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 0000000..a34c538 --- /dev/null +++ b/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7715216fa872aa66e4c6d5ff4ee3f3ee47f536608d26a7c83532388fcebc9fc +size 1157 diff --git a/ProjectSettings/Physics2DSettings.asset b/ProjectSettings/Physics2DSettings.asset new file mode 100644 index 0000000..36189c0 --- /dev/null +++ b/ProjectSettings/Physics2DSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3cbdbf0be15244b7b67d2f07bdb8e8981911c11a8c7fe8035f5261272fec658 +size 2028 diff --git a/ProjectSettings/PhysicsCoreProjectSettings2D.asset b/ProjectSettings/PhysicsCoreProjectSettings2D.asset new file mode 100644 index 0000000..6a51bcb --- /dev/null +++ b/ProjectSettings/PhysicsCoreProjectSettings2D.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bedc5339a832c60dc3f11b84abc4f4384dbfa20b0cd9ad6c444b907e751fc35e +size 151 diff --git a/ProjectSettings/PresetManager.asset b/ProjectSettings/PresetManager.asset new file mode 100644 index 0000000..f781dbe --- /dev/null +++ b/ProjectSettings/PresetManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29c125923fe6e29f90190d4fce1c82d660e7051546e2c9d02908d18c9cf2436c +size 146 diff --git a/ProjectSettings/ProjectAuditorSettings.asset b/ProjectSettings/ProjectAuditorSettings.asset new file mode 100644 index 0000000..e278ca2 --- /dev/null +++ b/ProjectSettings/ProjectAuditorSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22c699d7b424bd59053e552e6c358d9cca02d0663c276ec46ea0d0bd96651d85 +size 1036 diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset new file mode 100644 index 0000000..8bc1370 --- /dev/null +++ b/ProjectSettings/ProjectSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:437658caa280c3a99a3b6c3a3785dc5680ebd18ab25025cfd86f5fa47dddab24 +size 25530 diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..413e386 --- /dev/null +++ b/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 6000.5.7f1 +m_EditorVersionWithRevision: 6000.5.7f1 (017862109af0) diff --git a/ProjectSettings/QualitySettings.asset b/ProjectSettings/QualitySettings.asset new file mode 100644 index 0000000..1b536a9 --- /dev/null +++ b/ProjectSettings/QualitySettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6814d6cc3eb850dd7d2a069a8b17ba9f7591deba8b1758a855978bd9c8627643 +size 3662 diff --git a/ProjectSettings/ShaderGraphSettings.asset b/ProjectSettings/ShaderGraphSettings.asset new file mode 100644 index 0000000..30d0e44 --- /dev/null +++ b/ProjectSettings/ShaderGraphSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdba1e5d65bcf6e3269d76ed3e60b6c21f98322002996642c9d74f1eb89f6b91 +size 556 diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset new file mode 100644 index 0000000..6d49bef --- /dev/null +++ b/ProjectSettings/TagManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23fc56b484aa3d7edf5914cd3591f7a823ccef5e3f3496f8a51f30388ead63dc +size 657 diff --git a/ProjectSettings/TimeManager.asset b/ProjectSettings/TimeManager.asset new file mode 100644 index 0000000..38794bf --- /dev/null +++ b/ProjectSettings/TimeManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a83e54adbbda7c9f4851103a0c6ab7f6448a3343d4ea5b7620452fa08416ecd +size 202 diff --git a/ProjectSettings/URPProjectSettings.asset b/ProjectSettings/URPProjectSettings.asset new file mode 100644 index 0000000..2572cd6 --- /dev/null +++ b/ProjectSettings/URPProjectSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3a626cb529ccfc0a82e388b8cd32bc60888f7226c33412fad3bc50aba802cd7 +size 410 diff --git a/ProjectSettings/UnityConnectSettings.asset b/ProjectSettings/UnityConnectSettings.asset new file mode 100644 index 0000000..ec5284e --- /dev/null +++ b/ProjectSettings/UnityConnectSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7dd1e8290598f5ce86919ba1c98b42b766c8c42191a30fa2c4d12195c4ecd00 +size 1063 diff --git a/ProjectSettings/VFXManager.asset b/ProjectSettings/VFXManager.asset new file mode 100644 index 0000000..e0b8a4e --- /dev/null +++ b/ProjectSettings/VFXManager.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f066c6483919961332415ee64204f74fe52f38c02e82892eb02b7a5b2e381f0e +size 308 diff --git a/ProjectSettings/VersionControlSettings.asset b/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 0000000..6e1e402 --- /dev/null +++ b/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7bda45bf7a394745fee1316e6c71fa676004d591ad1f3aa247fd3e4ceb86071 +size 188 diff --git a/ProjectSettings/XRSettings.asset b/ProjectSettings/XRSettings.asset new file mode 100644 index 0000000..bfefb85 --- /dev/null +++ b/ProjectSettings/XRSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edd2beb4a2e388312b4b7da241aa51070434005d82c59363f1237dd2e567585f +size 158 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ec663a --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# AfterHours + +Koop-Ladensimulator für 2–4 Spieler. Unity 6 LTS / C# / Netcode for GameObjects. + +## Stand +Code-Gerüst ohne Unity-Projektdateien. `Assets/Scripts/` in ein frisches +Unity-6-Projekt kopieren, dann `docs/ROADMAP.md` Meilenstein 0 abarbeiten. + +## Für Claude Code +Alles Wesentliche steht in `CLAUDE.md`. Die zwei harten Regeln vorab: +1. Der Host simuliert, Clients fragen an (§3). +2. Zielrating ist Mature, nicht Adult Only (§7). + +## Doku +- `docs/DESIGN.md` — Spielmechanik +- `docs/ARCHITECTURE.md` — Autoritätsmodell, Netzwerk, Testen +- `docs/ROADMAP.md` — Aufgaben nach Meilensteinen diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..688c2af --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,33 @@ +# Roadmap + +## Meilenstein 0 — Editor-Setup (manuell, nicht durch Claude Code) +- [ ] Unity 6 LTS Projekt anlegen, `Assets/Scripts` einhängen +- [ ] Package Manager: `com.unity.netcode.gameobjects`, `com.unity.ai.navigation` +- [ ] Szenen `Boot` und `Shop` anlegen, NetworkManager konfigurieren +- [ ] Prefabs: Player, Customer, ShelfSlot, CashRegister — alle mit NetworkObject +- [ ] 8–10 `ProductDefinition`-Assets + `ProductCatalog` befüllen + +## Meilenstein 1 — Vertikaler Schnitt (Singleplayer-Host) +- [ ] Kunde spawnt, geht zu einem Regal, nimmt Ware, zahlt, verlässt Laden +- [ ] `CustomerAgent.UpdateBehaviour()` ausimplementieren (aktuell TODO) +- [ ] Einkaufsliste: Produktwahl anhand Kategorie + Preis (`PricingService`) +- [ ] Regal-Auffüllen aus Lagerkiste + +## Meilenstein 2 — Koop +- [ ] Zweiter Spieler joint, beide sehen dieselben Bestände +- [ ] Race Conditions am Regal und an der Kasse abfangen +- [ ] Steam-Lobby via Facepunch.Steamworks + Unity Relay +- [ ] Reconnect: Spieler trägt Ware, verbindet neu → Ware zurück ins Lager + +## Meilenstein 3 — Substanz +- [ ] Beratungs-Minispiel (Kundenandeutung → passende Kategorie) +- [ ] Bestellsystem mit Lieferzeit (`SupplierService`, noch nicht angelegt) +- [ ] Tagesabrechnung, Miete, Personalkosten +- [ ] Laden-Upgrades: Umkleide, Vorhang, zweite Kasse, Regalreihen +- [ ] Sichtachsen-System (`ExposedToStreet` automatisch aus Geometrie ableiten) + +## Meilenstein 4 — Release-Vorbereitung +- [ ] Speichern/Laden (JSON, serverseitig) +- [ ] Steam-Seite, Age-Gate, Rating-Fragebogen (siehe CLAUDE.md §7) +- [ ] Controller-Support, Rebinding +- [ ] Lokalisierung DE/EN diff --git a/SixShopSimulator.slnx b/SixShopSimulator.slnx new file mode 100644 index 0000000..dda8622 --- /dev/null +++ b/SixShopSimulator.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b1c253f --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,38 @@ +# Architektur + +## Autoritätsmodell + +``` +Client (Owner) Server (Host) +────────────── ───────────── +Bewegung ───── prediction ──▶ NetworkTransform (einzige Client-Autorität) +Raycast ───── ServerRpc ───▶ Distanz prüfen ──▶ IInteractable.Interact() +UI-Anzeige ◀─── NetworkVariable ─── Geld / Bestand / Preise +NPC-Optik ◀─── NetworkTransform ── NavMesh-Simulation (nur Server) +``` + +**Merksatz:** Wenn eine Variable Auswirkung auf Geld oder Bestand hat, darf sie +nur auf dem Server geschrieben werden. + +## Warum NPCs nicht jeden Frame ticken +`CustomerAgent` läuft mit `TickInterval = 0.25 s`. Bei 20–30 Kunden spart das +den Großteil der Server-CPU, ohne dass man es sieht — Laufwege macht ohnehin +der NavMeshAgent. Wer die KI reaktiver braucht, senkt das Intervall gezielt für +den Kunden an der Kasse, nicht global. + +## Netzwerk-Traffic +- Produkte: nur `int ProductId`, nie Referenzen oder Strings. +- `ShelfSlot`: 3 NetworkVariables (Produkt, Menge, Preis). Bei vielen Fächern + später zu einem gebündelten `NetworkList` pro Regal zusammenfassen. +- NPC-Zustand: ein Enum + ein Float für Sprechblasen. Mehr braucht der Client nicht. + +## Manuelles Netzwerk-Testen +1. `File > Build Settings > Build` (Development Build an) +2. Build starten → "Host" +3. Im Editor Play drücken → "Join" + +Testfälle, die regelmäßig brechen: +- Zwei Spieler greifen gleichzeitig aufs selbe Regalfach +- Client verlässt das Spiel, während er Ware trägt +- Kunde verlässt die Schlange, während ein Spieler gerade scannt +- Client mit hoher Latenz interagiert am Rand der Reichweite diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..64fa81e --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,36 @@ +# Design-Kern + +## Der Loop +Bestellen → Einräumen → Bepreisen → Beraten → Kassieren → Ausbauen + +## Was das Spiel von anderen Shop-Sims unterscheidet + +**1. Komfort als Ressource.** +Kunden haben einen Komfortwert. Enge Gänge, Personal, das ungefragt danebensteht, +und Regale mit Sichtachse zum Schaufenster senken ihn. Fällt er unter 25 %, legt +der Kunde die Ware zurück und geht. Das macht *Ladenlayout* zur zentralen +Designentscheidung — nicht Regalfläche, sondern Blickachsen. + +**2. Beratung statt bloßem Kassieren.** +Kunden umschreiben, was sie suchen. Der Spieler leitet daraus die Kategorie ab. +Treffer = größerer Warenkorb + Stammkunde. Fehlgriff = peinliche Stille und ein +Kunde, der schneller geht. + +**3. Sortimentspolitik.** +Wellness zieht Laufkundschaft mit kleiner Marge. Specialty bringt Marge, verlangt +aber Diskretion und Beratung. Wer beides mischt, braucht räumliche Trennung. + +## Koop-Rollen (Anspielung an Overcooked in Stoßzeiten) +| Rolle | Aufgabe | +|----------|--------------------------------------| +| Kasse | Scannen, Schlange abarbeiten | +| Lager | Kisten holen, Regale auffüllen | +| Beratung | Kunden mit `SeekingAdvice` bedienen | +| Einkauf | Bestellungen, Preise, Layout | + +Die Rollen sind nicht hart zugewiesen — die Rush-Kurve erzwingt Arbeitsteilung +von selbst. + +## Ton +Trocken, situativ, nie anzüglich. Die Komik entsteht aus Verlegenheit und +Zeitdruck, nicht aus dem Produkt. Siehe CLAUDE.md §7. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..f83d213 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,33 @@ +# Roadmap + +## Meilenstein 0 — Editor-Setup (manuell, nicht durch Claude Code) +- [ ] Unity 6 LTS Projekt anlegen, `Assets/Scripts` einhängen +- [x] Package Manager: `com.unity.netcode.gameobjects`, `com.unity.ai.navigation` +- [ ] Szenen `Boot` und `Shop` anlegen, NetworkManager konfigurieren +- [ ] Prefabs: Player, Customer, ShelfSlot, CashRegister — alle mit NetworkObject +- [ ] 8–10 `ProductDefinition`-Assets + `ProductCatalog` befüllen + +## Meilenstein 1 — Vertikaler Schnitt (Singleplayer-Host) +- [x] Kunde spawnt, geht zu einem Regal, nimmt Ware, zahlt, verlässt Laden +- [x] `CustomerAgent.UpdateBehaviour()` ausimplementieren (aktuell TODO) +- [x] Einkaufsliste: Produktwahl anhand Kategorie + Preis (`PricingService`) +- [x] Regal-Auffüllen aus Lagerkiste (`StorageCrate`, `PlayerHands`) + +## Meilenstein 2 — Koop +- [ ] Zweiter Spieler joint, beide sehen dieselben Bestände +- [ ] Race Conditions am Regal und an der Kasse abfangen +- [ ] Steam-Lobby via Facepunch.Steamworks + Unity Relay +- [ ] Reconnect: Spieler trägt Ware, verbindet neu → Ware zurück ins Lager + +## Meilenstein 3 — Substanz +- [ ] Beratungs-Minispiel (Kundenandeutung → passende Kategorie) +- [ ] Bestellsystem mit Lieferzeit (`SupplierService`, noch nicht angelegt) +- [ ] Tagesabrechnung, Miete, Personalkosten +- [ ] Laden-Upgrades: Umkleide, Vorhang, zweite Kasse, Regalreihen +- [ ] Sichtachsen-System (`ExposedToStreet` automatisch aus Geometrie ableiten) + +## Meilenstein 4 — Release-Vorbereitung +- [ ] Speichern/Laden (JSON, serverseitig) +- [ ] Steam-Seite, Age-Gate, Rating-Fragebogen (siehe CLAUDE.md §7) +- [ ] Controller-Support, Rebinding +- [ ] Lokalisierung DE/EN