This commit is contained in:
2026-08-10 19:23:04 +02:00
commit 404d15a56c
134 changed files with 5374 additions and 0 deletions
+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);
}
}