Files
g-world/scripts/game/game_clock.gd
T

143 lines
4.6 KiB
GDScript

class_name GameClock
extends Node
## Игровые дата и время.
##
## Отдельная сущность, а не поле сцены: ход времени понадобится производству,
## жителям и событиям, и сохранение обязано его восстанавливать.
##
## Пауза здесь своя, не движковая. `get_tree().paused` останавливает вообще всё
## и занята меню паузы; здесь же нужно останавливать только ход партии, оставляя
## интерфейс живым. При этом узел обычный, PAUSABLE, поэтому меню паузы
## замораживает время само собой.
## Старт партии: 1 апреля 2012 года, шесть утра.
const START := {"year": 2012, "month": 4, "day": 1, "hour": 6, "minute": 0, "second": 0}
## Сколько игровых минут проходит за секунду реального времени на скорости x1.
## Сутки при этом занимают 8 минут реального времени. Множители x2…x4
## умножают именно это значение, так что менять темп достаточно здесь.
const MINUTES_PER_SECOND := 3.0
## Доступные множители скорости.
const SPEEDS: Array[int] = [1, 2, 3, 4]
const MINUTES_PER_DAY := 24 * 60
const MONTHS: Array[String] = [
"января", "февраля", "марта", "апреля", "мая", "июня",
"июля", "августа", "сентября", "октября", "ноября", "декабря",
]
## Прошла игровая минута — сюда подключаются HUD, производство, события.
signal minute_passed(total_minutes: int)
## Сменились сутки.
signal day_passed(day_index: int)
## Изменились скорость или состояние паузы.
signal flow_changed
var running := true
var speed := 1
var _start_unix := int(Time.get_unix_time_from_datetime_dict(START))
var _elapsed_minutes := 0.0
var _last_whole_minute := 0
var _last_day := 0
func _process(delta: float) -> void:
if not running:
return
_advance(delta * MINUTES_PER_SECOND * float(speed))
# --- Текущий момент ---------------------------------------------------------
## Момент как словарь Time: year, month, day, hour, minute, second.
func now() -> Dictionary:
return Time.get_datetime_dict_from_unix_time(unix_time())
func unix_time() -> int:
return _start_unix + int(_elapsed_minutes) * 60
## «1 апреля 2012»
func date_text() -> String:
var moment := now()
return "%d %s %d" % [
int(moment["day"]), MONTHS[int(moment["month"]) - 1], int(moment["year"]),
]
## «06:00»
func time_text() -> String:
var moment := now()
return "%02d:%02d" % [int(moment["hour"]), int(moment["minute"])]
# --- Управление ходом -------------------------------------------------------
func set_speed(value: int) -> void:
speed = clampi(value, SPEEDS[0], SPEEDS[SPEEDS.size() - 1])
running = true
flow_changed.emit()
func play() -> void:
if running:
return
running = true
flow_changed.emit()
func pause() -> void:
if not running:
return
running = false
flow_changed.emit()
func toggle() -> void:
running = not running
flow_changed.emit()
# --- Сохранение -------------------------------------------------------------
func save_data() -> Dictionary:
return {"minutes": _elapsed_minutes, "speed": speed, "running": running}
func load_data(data: Dictionary) -> void:
_elapsed_minutes = maxf(float(data.get("minutes", 0.0)), 0.0)
speed = clampi(int(data.get("speed", 1)), SPEEDS[0], SPEEDS[SPEEDS.size() - 1])
running = bool(data.get("running", true))
_last_whole_minute = int(_elapsed_minutes)
_last_day = _day_index()
flow_changed.emit()
minute_passed.emit(_last_whole_minute)
# --- Внутреннее -------------------------------------------------------------
func _advance(minutes: float) -> void:
_elapsed_minutes += minutes
# Сигналим по целым минутам, а не каждый кадр: подписчикам нужен такт,
# а не дробное значение.
var whole := int(_elapsed_minutes)
if whole == _last_whole_minute:
return
_last_whole_minute = whole
minute_passed.emit(whole)
var day := _day_index()
if day == _last_day:
return
_last_day = day
day_passed.emit(day)
func _day_index() -> int:
return floori(_elapsed_minutes / float(MINUTES_PER_DAY))