This commit is contained in:
2026-08-10 19:23:04 +02:00
commit 404d15a56c
134 changed files with 5374 additions and 0 deletions
Binary file not shown.
+8
View File
@@ -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
View File
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8105016687592461f977c054a80ce2f2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5a28cea83daa4d34abf9d032e9d95e82
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+432
View File
@@ -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}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 99c9720ab356a0642a771bea13969a05
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a32c49c00ff9b1844adb6f716b0f752a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+12
View File
@@ -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:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e2dd05b235706ab4aa022f4c00c54b33
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+72
View File
@@ -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);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1c6319a5bac9dc247a1e3f9e025bb5c9
+83
View File
@@ -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);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 48276ef6a91772a4f9593792f07b29d2
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 755094105c5bfe746a7545d5b87348bd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -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
+75
View File
@@ -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
+227
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 259041785f4f96f42b6ce2138e6ad8d1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+31
View File
@@ -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
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a86bac230bc87784ca31703d750385db
+46
View File
@@ -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
+45
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4f403b2316053614daaf052c45a7b85c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+49
View File
@@ -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
+42
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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
+71
View File
@@ -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
+57
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 54a0671883c35dd42bfd005bee512b3b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+102
View File
@@ -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);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 87517e4a128619c469c629b6248f1daa
+22
View File
@@ -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
+112
View File
@@ -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.
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2bf1c5efe2d53984e90da832661184d8
+49
View File
@@ -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;
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2b8651b0bcbd9a442bc9688879ce2605
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 34870c3bd1615004f9e6f2b4a05a7f28
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -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
+8
View File
@@ -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.
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b83569d67af61e458304325a23e5dfd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
+8
View File
@@ -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:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 18dc0cd2c080841dea60987a38ce93fa
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ba062aa6c92b140379dbc06b43dd3b9b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 8a0c9218a650547d98138cd835033977
folderAsset: yes
timeCreated: 1484670163
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
+134
View File
@@ -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:
+654
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eabc9546105bf4accac1fd62a63e88e6
timeCreated: 1487337779
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 5a9bcd70e6a4b4b05badaa72e827d8e0
folderAsset: yes
timeCreated: 1475835190
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -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:
+16
View File
@@ -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: