initial
This commit is contained in:
@@ -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\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+23
@@ -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
|
||||||
@@ -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
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7ec3fddb6e731cd4e8bb668040911e08
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8105016687592461f977c054a80ce2f2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5a28cea83daa4d34abf9d032e9d95e82
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 99c9720ab356a0642a771bea13969a05
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a32c49c00ff9b1844adb6f716b0f752a
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "AfterHours.Runtime",
|
||||||
|
"rootNamespace": "AfterHours",
|
||||||
|
"references": [
|
||||||
|
"Unity.Netcode.Runtime",
|
||||||
|
"Unity.Netcode.Components",
|
||||||
|
"Unity.Collections"
|
||||||
|
],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"autoReferenced": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 29f53eee638de7147a47939382c45f71
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e2dd05b235706ab4aa022f4c00c54b33
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Spielzeit und Tageswechsel. Nur der Server zählt hoch; Clients lesen mit.
|
||||||
|
/// </summary>
|
||||||
|
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<float> _hourOfDay = new(); // 0..24
|
||||||
|
private readonly NetworkVariable<int> _day = new();
|
||||||
|
private readonly NetworkVariable<bool> _isOpen = new();
|
||||||
|
|
||||||
|
public float HourOfDay => _hourOfDay.Value;
|
||||||
|
public int Day => _day.Value;
|
||||||
|
public bool IsOpen => _isOpen.Value;
|
||||||
|
|
||||||
|
/// <summary>Feuert serverseitig beim Tageswechsel. Für Abrechnung, Lieferungen, Miete.</summary>
|
||||||
|
public event Action<int> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Springt zum nächsten Ladenöffnungszeitpunkt. Wird vom "Feierabend"-Button genutzt.</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1c6319a5bac9dc247a1e3f9e025bb5c9
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Einziger Ort, an dem Geld und Ruf verändert werden. Server-authoritativ.
|
||||||
|
/// Clients lesen ausschließlich die NetworkVariables.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShopEconomy : NetworkBehaviour
|
||||||
|
{
|
||||||
|
public static ShopEconomy Instance { get; private set; }
|
||||||
|
|
||||||
|
[SerializeField] private float _startingBalance = 2500f;
|
||||||
|
|
||||||
|
private readonly NetworkVariable<float> _balance = new();
|
||||||
|
private readonly NetworkVariable<float> _reputation = new(); // 0..1
|
||||||
|
private readonly NetworkVariable<int> _customersServedToday = new();
|
||||||
|
private readonly NetworkVariable<int> _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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Versucht abzubuchen. Gibt false zurück, wenn das Geld nicht reicht.</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 48276ef6a91772a4f9593792f07b29d2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 755094105c5bfe746a7545d5b87348bd
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace AfterHours.Customers
|
||||||
|
{
|
||||||
|
/// <summary>Ein Artikel im Einkaufskorb eines Kunden. Nur serverseitig.</summary>
|
||||||
|
public readonly struct BasketLine
|
||||||
|
{
|
||||||
|
public readonly int ProductId;
|
||||||
|
public readonly float Price;
|
||||||
|
|
||||||
|
public BasketLine(int productId, float price)
|
||||||
|
{
|
||||||
|
ProductId = productId;
|
||||||
|
Price = price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f271cf0a93651524d826c623bf6c9509
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AfterHours.Customers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Kern-Mechanik des Spiels: Wie wohl fühlt sich ein Kunde gerade?
|
||||||
|
/// Fällt der Komfort unter <see cref="AbandonThreshold"/>, legt der Kunde
|
||||||
|
/// die Ware zurück und geht.
|
||||||
|
///
|
||||||
|
/// Engine-frei und damit testbar – siehe Tests/EditMode/ComfortModelTests.cs.
|
||||||
|
/// </summary>
|
||||||
|
public static class ComfortModel
|
||||||
|
{
|
||||||
|
public const float AbandonThreshold = 0.25f;
|
||||||
|
|
||||||
|
/// <summary>Komfortverlust pro anderem Kunden im Nahbereich.</summary>
|
||||||
|
private const float CrowdingPenaltyPerPerson = 0.09f;
|
||||||
|
|
||||||
|
/// <summary>Malus, wenn Personal ungefragt in Reichweite steht.</summary>
|
||||||
|
private const float UnwantedStaffPenalty = 0.18f;
|
||||||
|
|
||||||
|
/// <summary>Malus, wenn das Regal von der Straße aus einsehbar ist.</summary>
|
||||||
|
private const float ExposedShelfPenalty = 0.22f;
|
||||||
|
|
||||||
|
/// <summary>Erholung pro Sekunde, wenn niemand stört.</summary>
|
||||||
|
private const float RecoveryPerSecond = 0.12f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zielkomfort für die aktuelle Situation (0..1).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseTolerance">Charaktereigenschaft des Kunden, 0..1. Höher = unerschrockener.</param>
|
||||||
|
/// <param name="discretionDemand">Diskretionsbedarf des betrachteten Produkts, 0..1.</param>
|
||||||
|
/// <param name="nearbyCustomers">Andere Kunden im Nahbereich.</param>
|
||||||
|
/// <param name="staffNearbyUnrequested">Personal steht daneben, ohne dass Beratung gewünscht war.</param>
|
||||||
|
/// <param name="shelfExposedToStreet">Regal ist vom Schaufenster aus einsehbar.</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bewegt den aktuellen Komfort auf den Zielwert zu (Frame-Schritt).</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1aac25a7e1b9fc246a75c4b5e9f7b2ed
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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<CustomerState> _state = new(CustomerState.Entering);
|
||||||
|
private readonly NetworkVariable<float> _visibleComfort = new(1f);
|
||||||
|
|
||||||
|
private NavMeshAgent _agent;
|
||||||
|
private CashRegister _register;
|
||||||
|
private ShelfSlot _targetSlot;
|
||||||
|
private readonly List<BasketLine> _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<NavMeshAgent>();
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
// Clients simulieren nichts – NavMesh nur auf dem Server.
|
||||||
|
_agent.enabled = IsServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: Initialisierung durch den Spawner.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nächstes passendes Regal: richtige Kategorie, vorrätig, Preis noch akzeptabel.</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: Kasse meldet erfolgreichen Abschluss.</summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0669ca75bd9edac44a5db06778b39410
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using AfterHours.Core;
|
||||||
|
using AfterHours.Data;
|
||||||
|
using AfterHours.Shop;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Customers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Erzeugt Kunden abhängig von Uhrzeit und Ruf. Läuft nur auf dem Server.
|
||||||
|
/// </summary>
|
||||||
|
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<NetworkObject>();
|
||||||
|
netObject.Spawn();
|
||||||
|
|
||||||
|
var categories = (ProductCategory[])System.Enum.GetValues(typeof(ProductCategory));
|
||||||
|
|
||||||
|
var agent = instance.GetComponent<CustomerAgent>();
|
||||||
|
agent.Initialize(
|
||||||
|
tolerance: Random.Range(0.15f, 0.95f),
|
||||||
|
wantedItems: Random.Range(1, 4),
|
||||||
|
wantedCategory: categories[Random.Range(0, categories.Length)],
|
||||||
|
register: _register);
|
||||||
|
|
||||||
|
_alive++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Wird von CustomerAgent kurz vor dem Despawn aufgerufen.</summary>
|
||||||
|
public void NotifyCustomerGone()
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
_alive = Mathf.Max(0, _alive - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 976ebea99a2a90e4fa658c69f07afef9
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 259041785f4f96f42b6ce2138e6ad8d1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
namespace AfterHours.Data
|
||||||
|
{
|
||||||
|
/// <summary>Grobes Sortiment-Segment. Beeinflusst Kundschaft, Marge und Ruf.</summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a86bac230bc87784ca31703d750385db
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Data
|
||||||
|
{
|
||||||
|
/// <summary>Zentrale Auflösung ProductId -> ProductDefinition.</summary>
|
||||||
|
[CreateAssetMenu(menuName = "AfterHours/Product Catalog", fileName = "ProductCatalog")]
|
||||||
|
public sealed class ProductCatalog : ScriptableObject
|
||||||
|
{
|
||||||
|
[SerializeField] private List<ProductDefinition> _products = new();
|
||||||
|
|
||||||
|
private Dictionary<int, ProductDefinition> _byId;
|
||||||
|
|
||||||
|
public IReadOnlyList<ProductDefinition> All => _products;
|
||||||
|
|
||||||
|
private void EnsureIndexed()
|
||||||
|
{
|
||||||
|
if (_byId != null) return;
|
||||||
|
|
||||||
|
_byId = new Dictionary<int, ProductDefinition>(_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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ff37cbc356c13f849995837f2a7dfc89
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stammdaten eines Artikels. Wird NIE über das Netzwerk übertragen –
|
||||||
|
/// im Netzwerk zirkuliert ausschließlich die ProductId.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5e52a7daaf085524caf591a33107a59d
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4f403b2316053614daaf052c45a7b85c
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AfterHours.Economy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reine Preis-/Nachfragelogik. Engine-frei und damit testbar.
|
||||||
|
/// Keine UnityEngine-Referenzen in dieser Datei.
|
||||||
|
/// </summary>
|
||||||
|
public static class PricingService
|
||||||
|
{
|
||||||
|
/// <summary>Ab diesem Aufschlag über Empfehlung kauft niemand mehr.</summary>
|
||||||
|
public const float RejectionThreshold = 1.75f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Marge in Prozent des Verkaufspreises.</summary>
|
||||||
|
public static float MarginRatio(float askingPrice, float wholesalePrice)
|
||||||
|
{
|
||||||
|
if (askingPrice <= 0f) return 0f;
|
||||||
|
return (askingPrice - wholesalePrice) / askingPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erwarteter Deckungsbeitrag pro angebotenem Stück.
|
||||||
|
/// Nützlich für den späteren "Preis-Optimum"-Hinweis im Preis-UI.
|
||||||
|
/// </summary>
|
||||||
|
public static float ExpectedProfitPerUnit(float askingPrice, float recommendedPrice, float wholesalePrice)
|
||||||
|
{
|
||||||
|
return PurchaseProbability(askingPrice, recommendedPrice) * (askingPrice - wholesalePrice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 488bfdde2f57adf4e8d4ce3a8ec065c0
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AfterHours.Economy
|
||||||
|
{
|
||||||
|
/// <summary>Eine Position einer Großhandelsbestellung.</summary>
|
||||||
|
[Serializable]
|
||||||
|
public struct OrderLine
|
||||||
|
{
|
||||||
|
public int ProductId;
|
||||||
|
public int Quantity;
|
||||||
|
public float UnitPrice;
|
||||||
|
|
||||||
|
public float LineTotal => Quantity * UnitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bestellung beim Großhandel. Wird nur serverseitig gehalten.</summary>
|
||||||
|
public sealed class PurchaseOrder
|
||||||
|
{
|
||||||
|
private readonly List<OrderLine> _lines = new();
|
||||||
|
|
||||||
|
public IReadOnlyList<OrderLine> Lines => _lines;
|
||||||
|
|
||||||
|
/// <summary>Spielzeit in Sekunden, zu der die Lieferung eintrifft.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ca74ad78a2bea0e4d985e96612714923
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1c1ccc133082caf4eb31bcfed0514b62
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Unity.Netcode;
|
||||||
|
|
||||||
|
namespace AfterHours.Interaction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public interface IInteractable
|
||||||
|
{
|
||||||
|
/// <summary>Text für den Blick-Prompt, z. B. "Regal auffüllen".</summary>
|
||||||
|
string GetPrompt(ulong clientId);
|
||||||
|
|
||||||
|
/// <summary>Serverseitige Ausführung. Muss selbst validieren.</summary>
|
||||||
|
void Interact(NetworkObject actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 994d75674ec8c0e46a8c40f1bee27330
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Interaction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(NetworkObject))]
|
||||||
|
public sealed class PlayerInteractor : NetworkBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private Transform _eyes;
|
||||||
|
[SerializeField] private float _reach = 2.5f;
|
||||||
|
[SerializeField] private LayerMask _interactableMask = ~0;
|
||||||
|
|
||||||
|
/// <summary>Toleranz, weil Client und Server nie exakt denselben Frame sehen.</summary>
|
||||||
|
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<NetworkObject>();
|
||||||
|
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<IInteractable>(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<IInteractable>(out var interactable))
|
||||||
|
interactable.Interact(NetworkObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3f8feeb0d94c7e44b8d05c33089a77ef
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 45f3bfcc1d96be341b8354325b3bb022
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Networking
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Einstiegspunkt für Host/Join. Bewusst minimal – Steam-Lobbies kommen
|
||||||
|
/// später über Facepunch.Steamworks dazu (siehe docs/ROADMAP.md).
|
||||||
|
/// </summary>
|
||||||
|
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).";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3f7b7e91ebc572543a8324413a34c7a9
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Networking
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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<CharacterController>();
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
if (!IsOwner)
|
||||||
|
{
|
||||||
|
if (_cameraRoot != null && _cameraRoot.GetComponentInChildren<Camera>() 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f88015bc4effd5b429972c480fabe831
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Networking
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Was der Spieler gerade trägt. Server-authoritativ – der Client darf
|
||||||
|
/// weder Produkt noch Menge selbst setzen.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlayerHands : NetworkBehaviour
|
||||||
|
{
|
||||||
|
public const int MaxCarry = 12;
|
||||||
|
|
||||||
|
private readonly NetworkVariable<int> _productId = new(-1);
|
||||||
|
private readonly NetworkVariable<int> _quantity = new(0);
|
||||||
|
|
||||||
|
public int ProductId => _productId.Value;
|
||||||
|
public int Quantity => _quantity.Value;
|
||||||
|
public bool IsEmpty => _quantity.Value <= 0;
|
||||||
|
|
||||||
|
/// <summary>Serverseitig: nimmt so viel wie möglich auf, gibt die tatsächliche Menge zurück.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Serverseitig: gibt bis zu <paramref name="quantity"/> ab.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 402647b9bce9a6a4481b75e1004e3bfb
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 54a0671883c35dd42bfd005bee512b3b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Kasse. Kunden reihen sich ein; ein Spieler scannt Position für Position
|
||||||
|
/// und schließt ab. Sämtliche Geldbewegung passiert hier auf dem Server.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CashRegister : NetworkBehaviour, IInteractable
|
||||||
|
{
|
||||||
|
[SerializeField] private ProductCatalog _catalog;
|
||||||
|
[SerializeField] private Transform _queueStart;
|
||||||
|
[SerializeField] private float _queueSpacing = 0.8f;
|
||||||
|
|
||||||
|
/// <summary>Server-only. Clients sehen die Schlange über die NPC-Positionen.</summary>
|
||||||
|
private readonly List<Customers.CustomerAgent> _queue = new();
|
||||||
|
|
||||||
|
private readonly NetworkVariable<float> _pendingTotal = new(0f);
|
||||||
|
private readonly NetworkVariable<int> _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";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: ein Klick = ein Artikel scannen, oder abschließen wenn fertig.</summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: Kunde stellt sich an. Gibt die Zielposition zurück.</summary>
|
||||||
|
public Vector3 Enqueue(Customers.CustomerAgent customer)
|
||||||
|
{
|
||||||
|
if (!IsServer) return _queueStart.position;
|
||||||
|
|
||||||
|
if (!_queue.Contains(customer)) _queue.Add(customer);
|
||||||
|
return SlotPosition(_queue.IndexOf(customer));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: Kunde verlässt die Schlange (z. B. weil er aufgibt).</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 87517e4a128619c469c629b6248f1daa
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AfterHours.Shop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serverseitige Liste aller aktiven Regalfächer. Damit Kunden ein
|
||||||
|
/// passendes Fach finden können, ohne FindObjectOfType im Frame-Loop
|
||||||
|
/// aufzurufen (siehe CLAUDE.md §5).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShelfRegistry
|
||||||
|
{
|
||||||
|
public static readonly ShelfRegistry Instance = new();
|
||||||
|
|
||||||
|
private readonly List<ShelfSlot> _slots = new();
|
||||||
|
|
||||||
|
public IReadOnlyList<ShelfSlot> Slots => _slots;
|
||||||
|
|
||||||
|
public void Register(ShelfSlot slot) => _slots.Add(slot);
|
||||||
|
|
||||||
|
public void Unregister(ShelfSlot slot) => _slots.Remove(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 36930243258ac6f4f816f2cfc254a52f
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using AfterHours.Data;
|
||||||
|
using AfterHours.Interaction;
|
||||||
|
using AfterHours.Networking;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Shop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ein Regalfach: hält genau eine Produktsorte. Auffüllen geht über
|
||||||
|
/// Interact(), Entnahme durch Kunden serverseitig über TakeOne().
|
||||||
|
/// </summary>
|
||||||
|
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<int> _productId = new(-1);
|
||||||
|
private readonly NetworkVariable<int> _quantity = new(0);
|
||||||
|
private readonly NetworkVariable<float> _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} €";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: Spieler füllt aus der Hand auf.</summary>
|
||||||
|
public void Interact(NetworkObject actor)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
if (!actor.TryGetComponent<PlayerHands>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: Kunde entnimmt ein Stück.</summary>
|
||||||
|
public bool TakeOne()
|
||||||
|
{
|
||||||
|
if (!IsServer || IsEmpty) return false;
|
||||||
|
|
||||||
|
_quantity.Value--;
|
||||||
|
if (_quantity.Value == 0) _productId.Value = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: Preis setzen (vom Preis-UI aus).</summary>
|
||||||
|
public void SetPrice(float price)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
_price.Value = Mathf.Max(0.01f, price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Kapazität in Stück, abhängig von der Produktgröße.</summary>
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2bf1c5efe2d53984e90da832661184d8
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using AfterHours.Data;
|
||||||
|
using AfterHours.Interaction;
|
||||||
|
using AfterHours.Networking;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace AfterHours.Shop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StorageCrate : NetworkBehaviour, IInteractable
|
||||||
|
{
|
||||||
|
[SerializeField] private ProductCatalog _catalog;
|
||||||
|
[SerializeField] private int _productId = -1;
|
||||||
|
[SerializeField] private int _startingQuantity = 200;
|
||||||
|
|
||||||
|
private readonly NetworkVariable<int> _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";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server: Spieler nimmt Ware in die Hände auf.</summary>
|
||||||
|
public void Interact(NetworkObject actor)
|
||||||
|
{
|
||||||
|
if (!IsServer || IsEmpty) return;
|
||||||
|
if (!actor.TryGetComponent<PlayerHands>(out var hands)) return;
|
||||||
|
|
||||||
|
int want = Mathf.Min(_quantity.Value, PlayerHands.MaxCarry);
|
||||||
|
int taken = hands.TryTake(_productId, want);
|
||||||
|
_quantity.Value -= taken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2b8651b0bcbd9a442bc9688879ce2605
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 34870c3bd1615004f9e6f2b4a05a7f28
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5855a87ce97ace049915c17c1ba0c9ca
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 576e5e4dc99f32e4eaebf1eb76385743
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ee90a53056bfe9c42bf72d0e7ee60e8a
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 53f7a9ae1e6213b4db8d1f200e402334
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 709f11a7f3c4041caa4ef136ea32d874
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ab09877e2e707104187f6f83e2f62510
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5e6cbd92db86f4b18aec3ed561671858
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 65bc7dbf4170f435aa868c779acfb082
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4b83569d67af61e458304325a23e5dfd
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f288ae1f4751b564a96ac7587541f7a2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 10fc4df2da32a41aaa32d77bc913491c
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 18dc0cd2c080841dea60987a38ce93fa
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ba062aa6c92b140379dbc06b43dd3b9b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8a0c9218a650547d98138cd835033977
|
||||||
|
folderAsset: yes
|
||||||
|
timeCreated: 1484670163
|
||||||
|
licenseType: Store
|
||||||
|
DefaultImporter:
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -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:
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eabc9546105bf4accac1fd62a63e88e6
|
||||||
|
timeCreated: 1487337779
|
||||||
|
licenseType: Store
|
||||||
|
DefaultImporter:
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5a9bcd70e6a4b4b05badaa72e827d8e0
|
||||||
|
folderAsset: yes
|
||||||
|
timeCreated: 1475835190
|
||||||
|
licenseType: Store
|
||||||
|
DefaultImporter:
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3ad9b87dffba344c89909c6d1b1c17e1
|
||||||
|
folderAsset: yes
|
||||||
|
timeCreated: 1475593892
|
||||||
|
licenseType: Store
|
||||||
|
DefaultImporter:
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 476cc7d7cd9874016adc216baab94a0a
|
||||||
|
timeCreated: 1484146680
|
||||||
|
licenseType: Store
|
||||||
|
MonoImporter:
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
@@ -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: ...`
|
||||||
@@ -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.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user