> ## Documentation Index
> Fetch the complete documentation index at: https://newbot-feature-time-manager.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Time Manager

> Guide for synchronized game time, calendars, and optional lighting in RPGJS.

# Time Manager

Use `withTimeManager()` to add synchronized game time without adding methods to `RpgMap`.

The server owns the canonical time. The client receives a map snapshot and infers the displayed time locally from `elapsedMinutes`, `scale`, `paused`, and `serverTimestamp`.

## Register the Module

You can use the same module object on both sides:

```ts theme={null}
// modules/time.ts
import { withTimeManager } from '@rpgjs/common'

export const TimeManagerModule = withTimeManager({
  start: '0001-01-01 08:00',
  scale: 10,
  calendar: {
    months: 12,
    daysPerMonth: 30,
    daysPerWeek: 7,
    seasons: ['spring', 'summer', 'autumn', 'winter']
  },
  lighting: {
    enabled: true
  }
})
```

```ts theme={null}
// server.ts
import { provideServerModules } from '@rpgjs/server'
import { TimeManagerModule } from './modules/time'

provideServerModules([
  TimeManagerModule
])
```

```ts theme={null}
// client.ts
import { provideClientModules } from '@rpgjs/client'
import { TimeManagerModule } from './modules/time'

provideClientModules([
  TimeManagerModule
])
```

For a client-only declaration, this also works:

```ts theme={null}
provideClientModules([
  withTimeManager()
])
```

## Server API

Use `TimeManager` from server hooks, events, or services:

```ts theme={null}
import { TimeManager, inject } from '@rpgjs/server'

const time = inject(TimeManager)

time.set({ hour: 22, minute: 30 })
time.advance({ hours: 2 })
time.pause()
time.resume()
time.setScale(30)
```

Read the derived state:

```ts theme={null}
const now = time.state()

console.log(now.year, now.month, now.day)
console.log(now.hour, now.minute, now.weekday, now.season)
```

## Client API

Use `ClientTimeManager` for display logic:

```ts theme={null}
import { ClientTimeManager, inject } from '@rpgjs/client'

const time = inject(ClientTimeManager)
const now = time.state()

if (now) {
  console.log(`${now.hour}:${String(now.minute).padStart(2, '0')}`)
}
```

The client does not write time. It projects the current display value from the last synchronized map snapshot.

## Snapshot

The synchronized map field is internal and namespaced:

```ts theme={null}
{
  __rpgjsTime: {
    elapsedMinutes: 480,
    scale: 10,
    paused: false,
    serverTimestamp: 1783013400000,
    calendar: {
      months: 12,
      daysPerMonth: 30,
      daysPerWeek: 7,
      seasons: ['spring', 'summer', 'autumn', 'winter']
    }
  }
}
```

Game code should use `TimeManager` and `ClientTimeManager` instead of reading `__rpgjsTime` directly.

## Lighting

When `lighting.enabled` is true, the server can apply map lighting from time phases:

```ts theme={null}
withTimeManager({
  lighting: {
    enabled: true,
    transitionMs: 1500,
    phases: {
      dawn: { hour: 6, lighting: { ambient: { darkness: 0.2 } } },
      day: { hour: 8, lighting: { ambient: { darkness: 0 } } },
      dusk: { hour: 18, lighting: { ambient: { darkness: 0.25 } } },
      night: { hour: 21, lighting: { ambient: { darkness: 0.55 } } }
    }
  }
})
```

If lighting is omitted or disabled, the time manager does not modify map lighting.

## Weather Ambiences

When `weather.enabled` is true, the server can roll weather ambiences per map. Each ambience has a weight and a game-time duration. When the duration expires, the server rolls the next ambience and applies it with the existing map weather API.

```ts theme={null}
withTimeManager({
  weather: {
    enabled: true,
    default: {
      ambiences: {
        clear: {
          weather: null,
          weight: { default: 60, months: { 6: 80, 7: 85, 8: 80 } },
          duration: { min: { hours: 2 }, max: { hours: 6 } }
        },
        rain: {
          weather: {
            effect: 'rain',
            preset: 'steadyRain',
            params: { density: 220, speed: 0.7 },
            transitionMs: 900
          },
          weight: { default: 20, months: { 3: 45, 4: 50, 10: 40, 11: 45 } },
          duration: { min: { hours: 1 }, max: { hours: 4 } }
        },
        fog: {
          weather: {
            effect: 'fog',
            preset: 'morningFog',
            params: { density: 0.8, alpha: 0.45 }
          },
          weight: { default: 10, seasons: { autumn: 30, winter: 20 } },
          duration: { hours: 2 }
        }
      }
    },
    maps: {
      forest: {
        ambiences: {
          rain: {
            weather: { effect: 'rain', params: { density: 260 } },
            weight: 100,
            duration: { hours: 2 }
          }
        }
      }
    }
  }
})
```

`maps[mapId]` overrides the `default` weather table for that map. A `weather: null` ambience clears the map weather. If weather is omitted, disabled, or no table exists for a map, the time manager does not modify that map's weather.

Weather remains server-owned. Clients receive the regular `weatherState` map update and should read `engine.sceneMap.weather()` or `engine.sceneMap.getWeather()` for rendering.

## Environment Hooks

Use `hooks` on `withTimeManager()` when the environment should drive gameplay rules from the plugin itself:

```ts theme={null}
withTimeManager({
  hooks: {
    onDayChange({ map, current }) {
      console.log(`New day ${current.day}`)
    },
    onLightingPhaseChange({ previousKey, currentKey }) {
      console.log(`${previousKey} -> ${currentKey}`)
    },
    onBeforeWeatherChange({ candidate, time }) {
      if (time.season === 'summer' && candidate.key === 'snow') {
        return false
      }
    },
    onWeatherChange({ currentKey }) {
      console.log(`Weather is now ${currentKey}`)
    }
  }
})
```

`onBeforeWeatherChange()` can return `false` to cancel the roll, or another `{ key, ambience }` candidate to replace it. Initial map registration is silent: hooks run only after an actual time, lighting, or weather transition.

Transition payloads include the current map so plugin hooks can update synchronized map state or trigger map-local systems.

Events can react locally by declaring matching methods:

```ts theme={null}
import { EventData, RpgEvent } from '@rpgjs/server'
import type { TimeWeatherTransitionPayload } from '@rpgjs/common'

@EventData({ name: 'Crop' })
export class CropEvent extends RpgEvent {
  onDayChange() {
    this.setGraphic('crop-stage-2')
  }

  onWeatherChange(payload: TimeWeatherTransitionPayload) {
    if (payload.currentKey === 'rain') {
      this.setGraphic('crop-watered')
    }
  }
}
```

Global event hooks work too:

```ts theme={null}
const server = {
  event: {
    onWeatherChange(event, payload) {
      if (payload.currentKey === 'rain') {
        event.setGraphic('crop-watered')
      }
    }
  }
}
```

This keeps the environment logic centralized in the time plugin while still letting map events behave like Stardew Valley objects: crops grow on day changes, lamps react to lighting phases, and NPCs or interactables react to weather changes.
