defmodule Ecspanse.Template.Component.Timer do
@moduledoc """
The `Timer` is a **Template Component** designed to facilitate the creation
of custom timer (countdown) components.
It serves as a foundation for building custom timer components with `use Ecspanse.Template.Component.Timer`.
The framework automatically decrements the Timer's time each frame,
eliminating the need for manual updates.
However, manual resetting may be necessary under certain circumstances such as:
- When game logic requires custom resetting.
- When the timer mode is set to `:once`, necessitating manual reset after reaching 0.
A dedicated `Ecspanse.System.Timer` system is provided by the framework.
This system auto-decrements the Timer component's time
and dispatches an event when time reaches 0.
To ensure functionality, this System must be manually included in the `c:Ecspanse.setup/1`.
Note that it should be added as a sync system, either at frame start or end.
This design choice allows developers control over timer operation
based on specific states or conditions.
For instance, pausing the timers when the game is in a pause state or other game states.
Pause control at a granular level can be achieved by setting the `paused` field to true.
The Timer component template comes with a **predefined state** comprising of:
- `:duration` - the timer's duration in milliseconds which also serves
as the reset value.
- `:time` - the current time of the timer in milliseconds,
auto-decremented by the framework each frame.
- `:event` - the event module dispatched when timer reaches 0.
- create special timer events using `Ecspanse.Template.Event.Timer`.
- these events require no options.
- their state is predefined to `%CustomEventModule{entity_id: entity_id}`,
where entity refers to owner of the custom timer component.
- event batch key corresponds to the component's owner entity's id.
- `:mode` - defines how timer operates and can be one of:
- `:repeat` (default) - timer resets to original duration
after reaching 0 and repeats indefinitely.
- `:once` - timer runs once and pauses after reaching 0.
Time value needs manual reset.
- `:temporary`: Timer runs once and removes itself from entity after reaching 0.
- `paused`: A boolean indicating if timer is paused (defaults to `false`).
## Example:
```elixir
defmodule Demo.Components.RestoreEnergyTimer do
use Ecspanse.Template.Component.Timer,
state: [duration: 3000, time: 3000, event: Demo.Events.RestoreEnergy, mode: :repeat, paused: false]
end
defmodule Demo.Events.RestoreEnergy do
use Ecspanse.Template.Template.Event.Timer
end
end
```
See [a working example](./tutorial.md#energy-regeneration) in the tutorial
"""
@timer_component_tag :ecs_timer
use Ecspanse.Template.Component,
tags: [@timer_component_tag],
state: [:duration, :time, :event, mode: :repeat, paused: false]
@mode [:repeat, :once, :temporary]
@impl true
def validate(state) do
with :ok <- validate_duration(state[:duration]),
:ok <- validate_time(state[:time]),
:ok <- validate_event(state[:event]),
:ok <- validate_mode(state[:mode]),
:ok <- validate_paused(state[:paused]) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp validate_duration(duration) do
if is_integer(duration) && duration > 0 do
:ok
else
raise ArgumentError,
"Invalid duration for Timer Component: #{inspect(__MODULE__)}. The `:duration` field is mandatory in the timer state and must be a positive integer."
end
end
defp validate_time(time) do
if is_integer(time) && time >= 0 do
:ok
else
raise ArgumentError,
"Invalid time for Timer Component: #{inspect(__MODULE__)}. The `:time` field is mandatory in the timer state and must be a non-negative integer."
end
end
defp validate_event(event) do
if is_atom(event) do
:ok
else
raise ArgumentError,
"Invalid event for Timer Component: #{inspect(__MODULE__)}. The `:event` field is mandatory in the timer state and must be an atom."
end
end
defp validate_mode(mode) do
if mode in @mode do
:ok
else
raise ArgumentError,
"Invalid mode for Timer Component: #{inspect(__MODULE__)}. The `:mode` field is mandatory in the timer state and must be one of the following: #{inspect(@mode)}"
end
end
defp validate_paused(paused) do
if is_boolean(paused) do
:ok
else
raise ArgumentError,
"Invalid paused for Timer Component: #{inspect(__MODULE__)}. The `:paused` field is mandatory in the timer state and must be a boolean."
end
end
@doc false
def timer_component_tag, do: @timer_component_tag
end