mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Turn the daytime map in theater into a real type.
No (intended) user visible effects, but this is the groundwork that will support https://github.com/dcs-liberation/dcs_liberation/issues/2400.
This commit is contained in:
parent
f49833646d
commit
82939a446b
@ -4,7 +4,7 @@ import datetime
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
from uuid import UUID
|
||||
|
||||
from dcs.mapping import Point
|
||||
@ -20,6 +20,7 @@ from dcs.terrain import (
|
||||
from dcs.terrain.terrain import Terrain
|
||||
from shapely import geometry, ops
|
||||
|
||||
from .daytimemap import DaytimeMap
|
||||
from .frontline import FrontLine
|
||||
from .iadsnetwork.iadsnetwork import IadsNetwork
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
@ -42,19 +43,11 @@ class ConflictTheater:
|
||||
|
||||
overview_image: str
|
||||
landmap: Optional[Landmap]
|
||||
"""
|
||||
land_poly = None # type: Polygon
|
||||
"""
|
||||
daytime_map: Dict[str, Tuple[int, int]]
|
||||
daytime_map: DaytimeMap
|
||||
iads_network: IadsNetwork
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
"""
|
||||
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
||||
for x in self.landmap[1]:
|
||||
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
||||
"""
|
||||
|
||||
def add_controlpoint(self, point: ControlPoint) -> None:
|
||||
self.controlpoints.append(point)
|
||||
@ -266,12 +259,12 @@ class CaucasusTheater(ConflictTheater):
|
||||
overview_image = "caumap.gif"
|
||||
|
||||
landmap = load_landmap(Path("resources/caulandmap.p"))
|
||||
daytime_map = {
|
||||
"dawn": (6, 9),
|
||||
"day": (9, 18),
|
||||
"dusk": (18, 20),
|
||||
"night": (0, 5),
|
||||
}
|
||||
daytime_map = DaytimeMap(
|
||||
dawn=(datetime.time(hour=6), datetime.time(hour=9)),
|
||||
day=(datetime.time(hour=9), datetime.time(hour=18)),
|
||||
dusk=(datetime.time(hour=18), datetime.time(hour=20)),
|
||||
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||
)
|
||||
|
||||
@property
|
||||
def timezone(self) -> datetime.timezone:
|
||||
@ -288,12 +281,12 @@ class PersianGulfTheater(ConflictTheater):
|
||||
terrain = persiangulf.PersianGulf()
|
||||
overview_image = "persiangulf.gif"
|
||||
landmap = load_landmap(Path("resources/gulflandmap.p"))
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
daytime_map = DaytimeMap(
|
||||
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||
day=(datetime.time(hour=8), datetime.time(hour=16)),
|
||||
dusk=(datetime.time(hour=16), datetime.time(hour=18)),
|
||||
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||
)
|
||||
|
||||
@property
|
||||
def timezone(self) -> datetime.timezone:
|
||||
@ -310,12 +303,12 @@ class NevadaTheater(ConflictTheater):
|
||||
terrain = nevada.Nevada()
|
||||
overview_image = "nevada.gif"
|
||||
landmap = load_landmap(Path("resources/nevlandmap.p"))
|
||||
daytime_map = {
|
||||
"dawn": (4, 6),
|
||||
"day": (6, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
daytime_map = DaytimeMap(
|
||||
dawn=(datetime.time(hour=4), datetime.time(hour=6)),
|
||||
day=(datetime.time(hour=6), datetime.time(hour=17)),
|
||||
dusk=(datetime.time(hour=17), datetime.time(hour=18)),
|
||||
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||
)
|
||||
|
||||
@property
|
||||
def timezone(self) -> datetime.timezone:
|
||||
@ -332,12 +325,12 @@ class NormandyTheater(ConflictTheater):
|
||||
terrain = normandy.Normandy()
|
||||
overview_image = "normandy.gif"
|
||||
landmap = load_landmap(Path("resources/normandylandmap.p"))
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (10, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
daytime_map = DaytimeMap(
|
||||
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||
day=(datetime.time(hour=10), datetime.time(hour=17)),
|
||||
dusk=(datetime.time(hour=17), datetime.time(hour=18)),
|
||||
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||
)
|
||||
|
||||
@property
|
||||
def timezone(self) -> datetime.timezone:
|
||||
@ -354,12 +347,12 @@ class TheChannelTheater(ConflictTheater):
|
||||
terrain = thechannel.TheChannel()
|
||||
overview_image = "thechannel.gif"
|
||||
landmap = load_landmap(Path("resources/channellandmap.p"))
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (10, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
daytime_map = DaytimeMap(
|
||||
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||
day=(datetime.time(hour=10), datetime.time(hour=17)),
|
||||
dusk=(datetime.time(hour=17), datetime.time(hour=18)),
|
||||
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||
)
|
||||
|
||||
@property
|
||||
def timezone(self) -> datetime.timezone:
|
||||
@ -376,12 +369,12 @@ class SyriaTheater(ConflictTheater):
|
||||
terrain = syria.Syria()
|
||||
overview_image = "syria.gif"
|
||||
landmap = load_landmap(Path("resources/syrialandmap.p"))
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
daytime_map = DaytimeMap(
|
||||
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||
day=(datetime.time(hour=8), datetime.time(hour=16)),
|
||||
dusk=(datetime.time(hour=16), datetime.time(hour=18)),
|
||||
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||
)
|
||||
|
||||
@property
|
||||
def timezone(self) -> datetime.timezone:
|
||||
@ -399,12 +392,12 @@ class MarianaIslandsTheater(ConflictTheater):
|
||||
overview_image = "marianaislands.gif"
|
||||
|
||||
landmap = load_landmap(Path("resources/marianaislandslandmap.p"))
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
daytime_map = DaytimeMap(
|
||||
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||
day=(datetime.time(hour=8), datetime.time(hour=16)),
|
||||
dusk=(datetime.time(hour=16), datetime.time(hour=18)),
|
||||
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||
)
|
||||
|
||||
@property
|
||||
def timezone(self) -> datetime.timezone:
|
||||
|
||||
87
game/theater/daytimemap.py
Normal file
87
game/theater/daytimemap.py
Normal file
@ -0,0 +1,87 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import time
|
||||
from typing import TypeAlias
|
||||
|
||||
from game.weather import TimeOfDay
|
||||
|
||||
TimeRange: TypeAlias = tuple[time, time]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DaytimeMap:
|
||||
dawn: TimeRange
|
||||
day: TimeRange
|
||||
dusk: TimeRange
|
||||
night: TimeRange
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Checks that we only are even given whole-hour intervals. There's no reason to
|
||||
# not support this eventually, but for now the fact that missions always start
|
||||
# on the hour is a nice gameplay property. That'll have to go as a part of the
|
||||
# mid-mission starts and removal of turns, but for now we can keep it to
|
||||
# preserve the old behavior.
|
||||
#
|
||||
# Mission start time generation (currently in Conditions.generate_start_time)
|
||||
# will need to be updated if and when this changes.
|
||||
def check_time_is_hours(descr: str, t: time) -> None:
|
||||
if t.minute:
|
||||
raise ValueError(
|
||||
f"{descr} has non-zero minutes; only hour intervals are currently "
|
||||
"supported"
|
||||
)
|
||||
if t.second:
|
||||
raise ValueError(
|
||||
f"{descr} has non-zero seconds; only hour intervals are currently "
|
||||
"supported"
|
||||
)
|
||||
if t.microsecond:
|
||||
raise ValueError(
|
||||
f"{descr} has non-zero microseconds; only hour intervals are "
|
||||
"currently supported"
|
||||
)
|
||||
|
||||
check_time_is_hours("dawn start", self.dawn[0])
|
||||
check_time_is_hours("dawn end", self.dawn[1])
|
||||
check_time_is_hours("day start", self.day[0])
|
||||
check_time_is_hours("day end", self.day[1])
|
||||
check_time_is_hours("dusk start", self.dusk[0])
|
||||
check_time_is_hours("dusk end", self.dusk[1])
|
||||
check_time_is_hours("night start", self.night[0])
|
||||
check_time_is_hours("night end", self.night[1])
|
||||
|
||||
def range_of(self, item: TimeOfDay) -> TimeRange:
|
||||
match item:
|
||||
case TimeOfDay.Dawn:
|
||||
return self.dawn
|
||||
case TimeOfDay.Day:
|
||||
return self.day
|
||||
case TimeOfDay.Dusk:
|
||||
return self.dusk
|
||||
case TimeOfDay.Night:
|
||||
return self.night
|
||||
case _:
|
||||
raise ValueError(f"Invalid value for TimeOfDay: {item}")
|
||||
|
||||
def best_guess_time_of_day_at(self, at: time) -> TimeOfDay:
|
||||
"""Returns an approximation of the time of day at the given time.
|
||||
|
||||
This is the best guess because time ranges need not cover the whole day. For the
|
||||
Caucasus, for example, dusk ends at 20:00 but night does not begin until 24:00.
|
||||
If a time between those hours is given, we call it dusk.
|
||||
"""
|
||||
if self.night[0] < self.dawn[0] and at < self.night[0]:
|
||||
# Night happens at or before midnight, so there's a time before dawn but
|
||||
# after midnight where it can still be dusk.
|
||||
return TimeOfDay.Dusk
|
||||
if at < self.dawn[0]:
|
||||
return TimeOfDay.Night
|
||||
if at < self.day[0]:
|
||||
return TimeOfDay.Dawn
|
||||
if at < self.dusk[0]:
|
||||
return TimeOfDay.Day
|
||||
if self.night[0] > self.dusk[0] and at >= self.night[0]:
|
||||
# Night happens before midnight, so it might still be dusk or night.
|
||||
return TimeOfDay.Night
|
||||
# If night starts at or before midnight, and it's at least dusk, it's definitely
|
||||
# dusk.
|
||||
return TimeOfDay.Dusk
|
||||
@ -9,13 +9,13 @@ from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.cloud_presets import Clouds as PydcsClouds
|
||||
from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
|
||||
from game.utils import Distance, Heading, meters, interpolate, Pressure, inches_hg
|
||||
|
||||
from game.theater.seasonalconditions import determine_season
|
||||
from game.utils import Distance, Heading, Pressure, inches_hg, interpolate, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.settings import Settings
|
||||
from game.theater import ConflictTheater
|
||||
from game.theater import ConflictTheater, DaytimeMap
|
||||
from game.theater.seasonalconditions import SeasonalConditions
|
||||
|
||||
|
||||
@ -318,16 +318,22 @@ class Conditions:
|
||||
) -> datetime.datetime:
|
||||
if night_disabled:
|
||||
logging.info("Skip Night mission due to user settings")
|
||||
time_range = {
|
||||
TimeOfDay.Dawn: (8, 9),
|
||||
TimeOfDay.Day: (10, 12),
|
||||
TimeOfDay.Dusk: (12, 14),
|
||||
TimeOfDay.Night: (14, 17),
|
||||
}[time_of_day]
|
||||
time_range = DaytimeMap(
|
||||
dawn=(datetime.time(hour=8), datetime.time(hour=9)),
|
||||
day=(datetime.time(hour=10), datetime.time(hour=12)),
|
||||
dusk=(datetime.time(hour=12), datetime.time(hour=14)),
|
||||
night=(datetime.time(hour=14), datetime.time(hour=17)),
|
||||
).range_of(time_of_day)
|
||||
else:
|
||||
time_range = theater.daytime_map[time_of_day.value]
|
||||
time_range = theater.daytime_map.range_of(time_of_day)
|
||||
|
||||
time = datetime.time(hour=random.randint(*time_range))
|
||||
# Starting missions on the hour is a nice gameplay property, so keep the random
|
||||
# time constrained to that. DaytimeMap enforces that we have only whole hour
|
||||
# ranges for now, so we don't need to worry about accidentally changing the time
|
||||
# of day by truncating sub-hours.
|
||||
time = datetime.time(
|
||||
hour=random.randint(time_range[0].hour, time_range[1].hour)
|
||||
)
|
||||
return datetime.datetime.combine(day, time)
|
||||
|
||||
@classmethod
|
||||
|
||||
107
tests/test_daytimemap.py
Normal file
107
tests/test_daytimemap.py
Normal file
@ -0,0 +1,107 @@
|
||||
from datetime import time
|
||||
|
||||
import pytest
|
||||
|
||||
from game.theater.daytimemap import DaytimeMap
|
||||
from game.weather import TimeOfDay
|
||||
|
||||
|
||||
def test_range_of() -> None:
|
||||
m = DaytimeMap(
|
||||
dawn=(time(hour=6), time(hour=9)),
|
||||
day=(time(hour=9), time(hour=18)),
|
||||
dusk=(time(hour=18), time(hour=20)),
|
||||
night=(time(hour=0), time(hour=5)),
|
||||
)
|
||||
|
||||
assert m.range_of(TimeOfDay.Dawn) == (time(hour=6), time(hour=9))
|
||||
assert m.range_of(TimeOfDay.Day) == (time(hour=9), time(hour=18))
|
||||
assert m.range_of(TimeOfDay.Dusk) == (time(hour=18), time(hour=20))
|
||||
assert m.range_of(TimeOfDay.Night) == (time(hour=0), time(hour=5))
|
||||
|
||||
|
||||
def test_best_guess_time_of_day_at() -> None:
|
||||
night_at_midnight = DaytimeMap(
|
||||
dawn=(time(hour=6), time(hour=9)),
|
||||
day=(time(hour=9), time(hour=18)),
|
||||
dusk=(time(hour=18), time(hour=20)),
|
||||
night=(time(hour=0), time(hour=5)),
|
||||
)
|
||||
|
||||
assert night_at_midnight.best_guess_time_of_day_at(time(hour=0)) == TimeOfDay.Night
|
||||
assert night_at_midnight.best_guess_time_of_day_at(time(hour=5)) == TimeOfDay.Night
|
||||
assert night_at_midnight.best_guess_time_of_day_at(time(hour=6)) == TimeOfDay.Dawn
|
||||
assert night_at_midnight.best_guess_time_of_day_at(time(hour=7)) == TimeOfDay.Dawn
|
||||
assert night_at_midnight.best_guess_time_of_day_at(time(hour=9)) == TimeOfDay.Day
|
||||
assert night_at_midnight.best_guess_time_of_day_at(time(hour=10)) == TimeOfDay.Day
|
||||
assert night_at_midnight.best_guess_time_of_day_at(time(hour=18)) == TimeOfDay.Dusk
|
||||
assert night_at_midnight.best_guess_time_of_day_at(time(hour=19)) == TimeOfDay.Dusk
|
||||
|
||||
night_before_midnight = DaytimeMap(
|
||||
dawn=(time(hour=6), time(hour=9)),
|
||||
day=(time(hour=9), time(hour=18)),
|
||||
dusk=(time(hour=18), time(hour=20)),
|
||||
night=(time(hour=22), time(hour=5)),
|
||||
)
|
||||
|
||||
assert (
|
||||
night_before_midnight.best_guess_time_of_day_at(time(hour=0)) == TimeOfDay.Night
|
||||
)
|
||||
assert (
|
||||
night_before_midnight.best_guess_time_of_day_at(time(hour=1)) == TimeOfDay.Night
|
||||
)
|
||||
assert (
|
||||
night_before_midnight.best_guess_time_of_day_at(time(hour=22))
|
||||
== TimeOfDay.Night
|
||||
)
|
||||
assert (
|
||||
night_before_midnight.best_guess_time_of_day_at(time(hour=23))
|
||||
== TimeOfDay.Night
|
||||
)
|
||||
assert (
|
||||
night_before_midnight.best_guess_time_of_day_at(time(hour=6)) == TimeOfDay.Dawn
|
||||
)
|
||||
|
||||
night_after_midnight = DaytimeMap(
|
||||
dawn=(time(hour=6), time(hour=9)),
|
||||
day=(time(hour=9), time(hour=18)),
|
||||
dusk=(time(hour=18), time(hour=20)),
|
||||
night=(time(hour=2), time(hour=5)),
|
||||
)
|
||||
|
||||
assert (
|
||||
night_after_midnight.best_guess_time_of_day_at(time(hour=0)) == TimeOfDay.Dusk
|
||||
)
|
||||
assert (
|
||||
night_after_midnight.best_guess_time_of_day_at(time(hour=23)) == TimeOfDay.Dusk
|
||||
)
|
||||
assert (
|
||||
night_after_midnight.best_guess_time_of_day_at(time(hour=2)) == TimeOfDay.Night
|
||||
)
|
||||
assert (
|
||||
night_after_midnight.best_guess_time_of_day_at(time(hour=6)) == TimeOfDay.Dawn
|
||||
)
|
||||
|
||||
|
||||
def test_whole_hours_only() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
DaytimeMap(
|
||||
dawn=(time(minute=6), time(hour=9)),
|
||||
day=(time(hour=9), time(hour=18)),
|
||||
dusk=(time(hour=18), time(hour=20)),
|
||||
night=(time(hour=2), time(hour=5)),
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
DaytimeMap(
|
||||
dawn=(time(hour=6), time(hour=9)),
|
||||
day=(time(second=9), time(hour=18)),
|
||||
dusk=(time(hour=18), time(hour=20)),
|
||||
night=(time(hour=2), time(hour=5)),
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
DaytimeMap(
|
||||
dawn=(time(hour=6), time(hour=9)),
|
||||
day=(time(hour=9), time(hour=18)),
|
||||
dusk=(time(hour=18), time(microsecond=20)),
|
||||
night=(time(hour=2), time(hour=5)),
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user