Phase 40: clock tempo at 1 min/s, x5/x10 heavy stride.
This commit is contained in:
@@ -327,6 +327,6 @@ describe('decodeServerMessage', () => {
|
||||
|
||||
describe('clock speeds', () => {
|
||||
it('matches the server table', () => {
|
||||
expect([...CLOCK_SPEEDS]).toEqual([0.5, 1, 2, 3, 4]);
|
||||
expect([...CLOCK_SPEEDS]).toEqual([0.5, 1, 2, 5, 10]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ export const WireLocale = {
|
||||
* Speed buttons, in wire order — the index travels, not the multiplier.
|
||||
* Mirror of `ClockSpeed.Multipliers` in `src/HSchool.Simulation/ClockSpeed.cs`.
|
||||
*/
|
||||
export const CLOCK_SPEEDS = [0.5, 1, 2, 3, 4] as const;
|
||||
export const CLOCK_SPEEDS = [0.5, 1, 2, 5, 10] as const;
|
||||
|
||||
export const DEFAULT_SPEED_INDEX = 1;
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { GameScreen } from './gameScreen.ts';
|
||||
|
||||
describe('GameScreen speed buttons', () => {
|
||||
it('shows five speed buttons ×½ ×1 ×2 ×5 ×10', () => {
|
||||
const onSetSpeed = vi.fn();
|
||||
const screen = new GameScreen({
|
||||
onLeave: () => {},
|
||||
onSetRunning: () => {},
|
||||
onSetSpeed,
|
||||
onSkip: () => {},
|
||||
});
|
||||
|
||||
document.body.append(screen.element);
|
||||
|
||||
const labels = [...screen.element.querySelectorAll('.clock__controls .button--small')]
|
||||
.map((button) => button.textContent ?? '')
|
||||
.filter((text) => text.startsWith('×'));
|
||||
|
||||
expect(labels).toEqual(['×½', '×1', '×2', '×5', '×10']);
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ interface GameScreenOptions {
|
||||
readonly onSkip: () => void;
|
||||
}
|
||||
|
||||
const SPEED_LABELS = ['×½', '×1', '×2', '×3', '×4'];
|
||||
const SPEED_LABELS = ['×½', '×1', '×2', '×5', '×10'];
|
||||
|
||||
/**
|
||||
* The inside of a school: calendar controls plus the manager shell. The tree comes from one map
|
||||
|
||||
@@ -23,7 +23,7 @@ interface MainMenuOptions {
|
||||
* timer — the server is the only thing that knows what time it is in each of them.
|
||||
*
|
||||
* One second is well below the resolution the cards show: at ×1 a game minute passes every
|
||||
* 12 real seconds, at ×4 every 3.
|
||||
* real second, at ×10 every tenth of a second on the clock (still one poll per second here).
|
||||
*/
|
||||
const REFRESH_INTERVAL_MS = 1000;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"Simulation": {
|
||||
"TickRate": 20,
|
||||
"MaxSchools": 6,
|
||||
"GameMinutesPerRealSecond": 5,
|
||||
"GameMinutesPerRealSecond": 1,
|
||||
"DefaultStartDate": "2012-03-31T06:00:00",
|
||||
"SavesDirectory": "saves",
|
||||
"ModsDirectory": "mods",
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace HSchool.Simulation;
|
||||
/// </summary>
|
||||
public static class ClockSpeed
|
||||
{
|
||||
/// <summary>×½, ×1, ×2, ×3, ×4.</summary>
|
||||
public static ReadOnlySpan<double> Multipliers => [0.5d, 1d, 2d, 3d, 4d];
|
||||
/// <summary>×½, ×1, ×2, ×5, ×10.</summary>
|
||||
public static ReadOnlySpan<double> Multipliers => [0.5d, 1d, 2d, 5d, 10d];
|
||||
|
||||
/// <summary>Index of ×1, the speed a school starts at.</summary>
|
||||
public const int DefaultIndex = 1;
|
||||
@@ -18,4 +18,22 @@ public static class ClockSpeed
|
||||
|
||||
/// <summary>Multiplier for a validated index; out-of-range values fall back to ×1.</summary>
|
||||
public static double MultiplierAt(int index) => IsValid(index) ? Multipliers[index] : Multipliers[DefaultIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Calendar impulses between heavy-system ticks. ×5 and ×10 stride so walking and needs keep
|
||||
/// one game minute per heavy step instead of twenty tiny ones per second.
|
||||
/// </summary>
|
||||
public static int HeavySystemsStride(int speedIndex) => speedIndex switch
|
||||
{
|
||||
3 => 4,
|
||||
4 => 2,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Game minutes passed into presence, needs and learning on a heavy tick. High speeds use a
|
||||
/// fixed one-minute quantum; lower speeds pass the impulse's own advance.
|
||||
/// </summary>
|
||||
public static double HeavyGameMinutes(int speedIndex, double impulseMinutes) =>
|
||||
speedIndex is 3 or 4 ? 1d : impulseMinutes;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,12 @@ public sealed class School : IDisposable
|
||||
|
||||
internal int DecisionBudget { get; set; }
|
||||
|
||||
internal int HeavySystemsInvocations { get; private set; }
|
||||
|
||||
internal double LastHeavyGameMinutes { get; private set; }
|
||||
|
||||
private int _impulseCounter;
|
||||
|
||||
public int PendingDecisionCount => DecisionQueue.Count;
|
||||
|
||||
/// <summary>
|
||||
@@ -303,24 +309,14 @@ public sealed class School : IDisposable
|
||||
LastDecisionSlot = null;
|
||||
}
|
||||
|
||||
PresenceSystem.Apply(this, gameMinutes);
|
||||
foreach (var id in ActivitySystem.Apply(this, gameMinutes))
|
||||
{
|
||||
PresenceSystem.Enqueue(this, id);
|
||||
}
|
||||
SyncWeather(force: false);
|
||||
|
||||
PersonDayLog.Sync(this);
|
||||
|
||||
if (Catalog is not null)
|
||||
_impulseCounter++;
|
||||
var stride = ClockSpeed.HeavySystemsStride(Clock.SpeedIndex);
|
||||
if (_impulseCounter % stride == 0)
|
||||
{
|
||||
var below = PresenceSystem.BelowThreshold(this);
|
||||
SyncWeather(force: false);
|
||||
NeedDecay.Apply(World, Catalog, gameMinutes);
|
||||
WarmthDecay.Apply(this, gameMinutes);
|
||||
PresenceSystem.EnqueueNewlyUrgent(this, below);
|
||||
PresenceSystem.DrainDecisions(this);
|
||||
LessonLearningSystem.Apply(this, gameMinutes);
|
||||
peopleChanged |= ApparelWear.Apply(this, gameMinutes, before);
|
||||
var heavyMinutes = ClockSpeed.HeavyGameMinutes(Clock.SpeedIndex, gameMinutes);
|
||||
peopleChanged |= ApplyHeavySystems(heavyMinutes);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -331,6 +327,34 @@ public sealed class School : IDisposable
|
||||
return peopleChanged;
|
||||
}
|
||||
|
||||
private bool ApplyHeavySystems(double gameMinutes)
|
||||
{
|
||||
HeavySystemsInvocations++;
|
||||
LastHeavyGameMinutes = gameMinutes;
|
||||
|
||||
var peopleChanged = false;
|
||||
PresenceSystem.Apply(this, gameMinutes);
|
||||
foreach (var id in ActivitySystem.Apply(this, gameMinutes))
|
||||
{
|
||||
PresenceSystem.Enqueue(this, id);
|
||||
}
|
||||
|
||||
PersonDayLog.Sync(this);
|
||||
|
||||
if (Catalog is not null)
|
||||
{
|
||||
var below = PresenceSystem.BelowThreshold(this);
|
||||
NeedDecay.Apply(World, Catalog, gameMinutes);
|
||||
WarmthDecay.Apply(this, gameMinutes);
|
||||
PresenceSystem.EnqueueNewlyUrgent(this, below);
|
||||
PresenceSystem.DrainDecisions(this);
|
||||
LessonLearningSystem.Apply(this, gameMinutes);
|
||||
peopleChanged |= ApparelWear.Apply(this, gameMinutes, Clock.Time.AddMinutes(-gameMinutes));
|
||||
}
|
||||
|
||||
return peopleChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes the street from the preset, seed and current time. Commits when the clock
|
||||
/// tenths or precipitation change, so warmth does not jitter every tick. A skip must force
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class SimulationOptions
|
||||
public int MaxSchools { get; set; } = 6;
|
||||
|
||||
/// <summary>Base speed of the game clock: real seconds are multiplied by this many game minutes.</summary>
|
||||
public double GameMinutesPerRealSecond { get; set; } = 5d;
|
||||
public double GameMinutesPerRealSecond { get; set; } = 1d;
|
||||
|
||||
/// <summary>Prefilled start of a new school; the client shows it in the creation form.</summary>
|
||||
public DateTime DefaultStartDate
|
||||
|
||||
Reference in New Issue
Block a user