Summary
Add a day/night cycle that periodically dims and brightens the game world by applying a brightness overlay to the rendered game area, affecting all visible entities uniformly.
Background
Roam's renderer draws entities tile-by-tile in Room.drawLocation() (src/world/room.py) and Room.drawWithOffset(), blitting scaled pygame surfaces directly to graphik.gameDisplay. The simplest way to apply a global brightness effect without touching individual entity textures is to blit a semi-transparent black overlay onto the game area rect (Graphik.getGameAreaRect()) after all tiles are drawn each frame, with opacity driven by the current tick and a configurable cycle length. Note: if PR #344 is merged before this, any new print() calls should use getLogger from src/gameLogging/logger.py instead.
Requirements
Config
- Add
dayNightCycleEnabled boolean (default true) and dayNightCycleLengthTicks integer (default 43200 — 30 minutes at 30 tps) to config.yml and load both in Config (src/config/config.py)
- Add a Day/Night Cycle toggle button to
ConfigScreen (src/screen/configScreen.py), consistent with existing toggles
Cycle Logic
- Create
src/world/dayNightCycle.py with a DayNightCycle class registered as a @component in the DI container
- The class should expose a
getOverlayOpacity(tick) method that maps the current tick to an opacity value in the range 0 (full brightness, midday) to 200 (near-dark, midnight) using a sine curve so transitions are smooth rather than linear
- Midday should occur at
tick % cycleLengthTicks == 0 and midnight at the halfway point
Rendering
- In
WorldScreen.draw() (src/screen/worldScreen.py), after all rooms are drawn and before HUD elements, blit a pygame.Surface filled with (0, 0, 0) at the opacity returned by getOverlayOpacity onto the game area rect (graphik.getGameAreaRect())
- Use
pygame.SRCALPHA or Surface.set_alpha() for the overlay — do not create a new surface every frame; cache it as a WorldScreen attribute and only recreate it when the game area rect size changes
- The overlay must be clipped to the game area rect so letterbox bars outside the square game area are not affected
- The cycle must be skipped entirely (no overlay drawn) when
config.dayNightCycleEnabled is False
Debug Info
- When
config.debug is True, include the current cycle phase (e.g. day, dusk, night, dawn) and overlay opacity alongside the existing debug text drawn in WorldScreen
Save/Load
- The cycle derives its state entirely from
TickCounter.getTick(), which is already persisted in tick.json — no additional save/load logic is needed
Acceptance Criteria
Summary
Add a day/night cycle that periodically dims and brightens the game world by applying a brightness overlay to the rendered game area, affecting all visible entities uniformly.
Background
Roam's renderer draws entities tile-by-tile in
Room.drawLocation()(src/world/room.py) andRoom.drawWithOffset(), blitting scaled pygame surfaces directly tographik.gameDisplay. The simplest way to apply a global brightness effect without touching individual entity textures is to blit a semi-transparent black overlay onto the game area rect (Graphik.getGameAreaRect()) after all tiles are drawn each frame, with opacity driven by the current tick and a configurable cycle length. Note: if PR #344 is merged before this, any newprint()calls should usegetLoggerfromsrc/gameLogging/logger.pyinstead.Requirements
Config
dayNightCycleEnabledboolean (defaulttrue) anddayNightCycleLengthTicksinteger (default43200— 30 minutes at 30 tps) toconfig.ymland load both inConfig(src/config/config.py)ConfigScreen(src/screen/configScreen.py), consistent with existing togglesCycle Logic
src/world/dayNightCycle.pywith aDayNightCycleclass registered as a@componentin the DI containergetOverlayOpacity(tick)method that maps the current tick to an opacity value in the range0(full brightness, midday) to200(near-dark, midnight) using a sine curve so transitions are smooth rather than lineartick % cycleLengthTicks == 0and midnight at the halfway pointRendering
WorldScreen.draw()(src/screen/worldScreen.py), after all rooms are drawn and before HUD elements, blit apygame.Surfacefilled with(0, 0, 0)at the opacity returned bygetOverlayOpacityonto the game area rect (graphik.getGameAreaRect())pygame.SRCALPHAorSurface.set_alpha()for the overlay — do not create a new surface every frame; cache it as aWorldScreenattribute and only recreate it when the game area rect size changesconfig.dayNightCycleEnabledisFalseDebug Info
config.debugisTrue, include the current cycle phase (e.g.day,dusk,night,dawn) and overlay opacity alongside the existing debug text drawn inWorldScreenSave/Load
TickCounter.getTick(), which is already persisted intick.json— no additional save/load logic is neededAcceptance Criteria
dayNightCycleEnabledin the settings screen immediately stops or resumes the overlaydayNightCycleLengthTicksinconfig.ymlcontrols the cycle duration and is respected without a restartDayNightCycle.getOverlayOpacity()at tick values corresponding to midday, midnight, dusk, and dawn