thresholdfloor 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thresholdfloor/__init__.py +547 -0
- thresholdfloor/aether_thresher.py +626 -0
- thresholdfloor/elevation.py +291 -0
- thresholdfloor/tests/__init__.py +28 -0
- thresholdfloor/tests/test_geometric.py +96 -0
- thresholdfloor/tests/test_solar.py +113 -0
- thresholdfloor/tests/test_threshold.py +182 -0
- thresholdfloor/threshold_floor.py +813 -0
- thresholdfloor-0.0.0.dist-info/METADATA +97 -0
- thresholdfloor-0.0.0.dist-info/RECORD +13 -0
- thresholdfloor-0.0.0.dist-info/WHEEL +5 -0
- thresholdfloor-0.0.0.dist-info/licenses/LICENSE +21 -0
- thresholdfloor-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""thresholdfloor — Solar geography and alchemical flooring system.
|
|
2
|
+
|
|
3
|
+
This package provides tools for:
|
|
4
|
+
- Solar declination and sunrise azimuth calculations (Aether-backed)
|
|
5
|
+
- ThresholdFloor state management (vault, phases, rituals)
|
|
6
|
+
- Lunar tracking and celestial mappings
|
|
7
|
+
- Alchemical cycle detection and automation
|
|
8
|
+
|
|
9
|
+
Dependencies:
|
|
10
|
+
- aetherfield: Internal celestial field calculations
|
|
11
|
+
- aether_thresher: Solar geometry and sunrise math
|
|
12
|
+
- zodiac: Sign mapping and wheel rotation
|
|
13
|
+
- skyfieldcomm: Sign offsets and celestial markers
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__author__ = "Heather Nightfall"
|
|
18
|
+
|
|
19
|
+
from typing import Optional, Tuple, List, Dict, Any
|
|
20
|
+
from datetime import datetime, date, timezone, timedelta
|
|
21
|
+
import os
|
|
22
|
+
import pytz
|
|
23
|
+
from dotenv import load_dotenv
|
|
24
|
+
|
|
25
|
+
# Load environment variables
|
|
26
|
+
load_dotenv()
|
|
27
|
+
|
|
28
|
+
# Arch definitions
|
|
29
|
+
EAST_ARCH = {"azimuth": 90.0, "inscription": "The Sign of the Times"}
|
|
30
|
+
WEST_ARCH = {"azimuth": 270.0}
|
|
31
|
+
SOUTH_ARCH = {
|
|
32
|
+
"azimuth": 180.0,
|
|
33
|
+
"altitude_center": 38.0,
|
|
34
|
+
"altitude_range": (34.0, 42.0),
|
|
35
|
+
"azimuth_range": (175.0, 185.0)
|
|
36
|
+
}
|
|
37
|
+
NORTH_ARCH = {"azimuth": 0.0}
|
|
38
|
+
ZODIAC_PEGS = [deg for deg in range(360)]
|
|
39
|
+
|
|
40
|
+
# Alchemy constants
|
|
41
|
+
api_key = os.getenv("weather_api_key")
|
|
42
|
+
EARTH_RADIUS_M = 6371000.0
|
|
43
|
+
COLORS = {
|
|
44
|
+
"Nigredo": "black",
|
|
45
|
+
"Albedo": "white",
|
|
46
|
+
"Citrinitas": "yellow-gold",
|
|
47
|
+
"Rubedo": "crimson-red",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# =====================================================
|
|
51
|
+
# PUBLIC FUNCTIONS
|
|
52
|
+
# =====================================================
|
|
53
|
+
|
|
54
|
+
def calculate_sunrise_azimuth(date, latitude, longitude, tz: Optional[str] = "UTC"):
|
|
55
|
+
"""Return sunrise azimuth (deg, 0=N clockwise) using AetherField."""
|
|
56
|
+
from aether_thresher import sunrise_azimuth as _sunrise_azimuth
|
|
57
|
+
tzinfo = pytz.timezone(tz) if isinstance(tz, str) else tz
|
|
58
|
+
dt = tzinfo.localize(datetime(date.year, date.month, date.day, 12, 0, 0)) if tzinfo else datetime(
|
|
59
|
+
date.year, date.month, date.day, 12, 0, 0
|
|
60
|
+
)
|
|
61
|
+
return _sunrise_azimuth(dt, float(latitude), float(longitude), tzinfo or "UTC")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def determine_solar_movement(yesterday_az, today_az):
|
|
65
|
+
"""Return solar movement direction: 'North' or 'South'."""
|
|
66
|
+
from aether_thresher import determine_solar_movement as _determine_solar_movement
|
|
67
|
+
return _determine_solar_movement(yesterday_az, today_az)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def is_solstice(prev_direction, current_direction):
|
|
71
|
+
"""Simple direction-change heuristic for solstice detection."""
|
|
72
|
+
return prev_direction != current_direction and prev_direction != "Stationary"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def current_solstice_anchors(today):
|
|
76
|
+
"""Return (winter_date, summer_date) anchors for the current solstice cycle."""
|
|
77
|
+
year = today.year
|
|
78
|
+
try:
|
|
79
|
+
summer = datetime(year, 6, 21).date()
|
|
80
|
+
winter = datetime(year, 12, 21).date()
|
|
81
|
+
except Exception:
|
|
82
|
+
summer = date(6, 21).replace(year=year)
|
|
83
|
+
winter = date(12, 21).replace(year=year)
|
|
84
|
+
|
|
85
|
+
if today <= summer:
|
|
86
|
+
winter_anchor = date(year-1, 12, 21)
|
|
87
|
+
summer_anchor = summer
|
|
88
|
+
else:
|
|
89
|
+
summer_anchor = summer
|
|
90
|
+
winter_anchor = winter
|
|
91
|
+
|
|
92
|
+
return winter_anchor, summer_anchor
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def layout_lions_from_azimuths(min_az, max_az, wall_normal_az: float = 90.0, num_lions: int = 7, R: float = 10.0) -> list:
|
|
96
|
+
"""Return a list of lion dicts with azimuth ranges and (x,z) positions."""
|
|
97
|
+
try:
|
|
98
|
+
from math import sin, cos, radians
|
|
99
|
+
spread = max_az - min_az
|
|
100
|
+
sector = spread / num_lions
|
|
101
|
+
lions = []
|
|
102
|
+
|
|
103
|
+
for i in range(num_lions):
|
|
104
|
+
az_min = min_az + i * sector
|
|
105
|
+
az_max = az_min + sector
|
|
106
|
+
az_center = (az_min + az_max) / 2.0
|
|
107
|
+
delta = az_center - wall_normal_az
|
|
108
|
+
δ = radians(delta)
|
|
109
|
+
x = R * sin(δ)
|
|
110
|
+
z = R * (1.0 - cos(δ))
|
|
111
|
+
lions.append({
|
|
112
|
+
"index": i,
|
|
113
|
+
"az_min": az_min,
|
|
114
|
+
"az_max": az_max,
|
|
115
|
+
"az_center": az_center,
|
|
116
|
+
"delta_deg": delta,
|
|
117
|
+
"x_m": x,
|
|
118
|
+
"z_m": z,
|
|
119
|
+
"well_id": i,
|
|
120
|
+
"state": "dry",
|
|
121
|
+
})
|
|
122
|
+
return lions
|
|
123
|
+
except Exception:
|
|
124
|
+
# Fallback: return dummy data
|
|
125
|
+
return [{"index": i, "az_min": 90, "az_max": 270, "az_center": 90, "wall_x": 0, "wall_z": 0, "state": "dry"} for i in range(num_lions)]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def map_azimuth_to_lion(az_deg, min_az, max_az, num_lions=7, wall_normal_az=90.0, R=10.0):
|
|
129
|
+
"""Map an azimuth to a lion index and (x,z) coordinates."""
|
|
130
|
+
from math import sin, cos, radians
|
|
131
|
+
az = az_deg % 360
|
|
132
|
+
min_az = min_az % 360
|
|
133
|
+
max_az = max_az % 360
|
|
134
|
+
|
|
135
|
+
if max_az <= min_az:
|
|
136
|
+
max_az += 360
|
|
137
|
+
az_norm = az if az >= min_az else az + 360
|
|
138
|
+
|
|
139
|
+
spread = max_az - min_az
|
|
140
|
+
sector_width = spread / num_lions
|
|
141
|
+
frac = (az_norm - min_az) / spread
|
|
142
|
+
if frac < 0:
|
|
143
|
+
frac = 0.0
|
|
144
|
+
if frac >= 1.0:
|
|
145
|
+
frac = 0.999999
|
|
146
|
+
lion_index = int(frac * num_lions)
|
|
147
|
+
az_min_sector = min_az + lion_index * sector_width
|
|
148
|
+
az_center = az_min_sector + sector_width / 2.0
|
|
149
|
+
delta = az_center - wall_normal_az
|
|
150
|
+
|
|
151
|
+
if delta > 180:
|
|
152
|
+
delta -= 360
|
|
153
|
+
if delta < -180:
|
|
154
|
+
delta += 360
|
|
155
|
+
δ = radians(delta)
|
|
156
|
+
x = R * sin(δ)
|
|
157
|
+
z = R * (1.0 - cos(δ))
|
|
158
|
+
return {"lion_index": lion_index, "az_center": az_center % 360, "x_m": x, "z_m": z}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def level_floor_contents(floor_m, *, capacity: float = 1.0) -> dict:
|
|
162
|
+
"""Enforce floor capacity. If total levels exceed capacity, spill from lowest-priority upward."""
|
|
163
|
+
try:
|
|
164
|
+
from math import max, min
|
|
165
|
+
fruit = float(floor_m.get("fruit_load", 0.0) or 0.0)
|
|
166
|
+
must = float(floor_m.get("must_level", 0.0) or 0.0)
|
|
167
|
+
blood = float(floor_m.get("blood_level", 0.0) or 0.0)
|
|
168
|
+
wine = float(floor_m.get("wine_level", 0.0) or 0.0)
|
|
169
|
+
water = float(floor_m.get("water_level", 0.0) or 0.0)
|
|
170
|
+
|
|
171
|
+
fruit = max(0.0, min(1.0, fruit))
|
|
172
|
+
must = max(0.0, min(1.0, must))
|
|
173
|
+
blood = max(0.0, min(1.0, blood))
|
|
174
|
+
wine = max(0.0, min(1.0, wine))
|
|
175
|
+
water = max(0.0, min(1.0, water))
|
|
176
|
+
|
|
177
|
+
total = fruit + must + blood + wine + water
|
|
178
|
+
spilled = {"water_level": 0.0, "wine_level": 0.0, "blood_level": 0.0, "must_level": 0.0, "fruit_load": 0.0}
|
|
179
|
+
|
|
180
|
+
if total <= capacity:
|
|
181
|
+
floor_m["fruit_load"] = fruit
|
|
182
|
+
floor_m["must_level"] = must
|
|
183
|
+
floor_m["blood_level"] = blood
|
|
184
|
+
floor_m["wine_level"] = wine
|
|
185
|
+
floor_m["water_level"] = water
|
|
186
|
+
return spilled
|
|
187
|
+
|
|
188
|
+
overflow = total - capacity
|
|
189
|
+
|
|
190
|
+
def spill_from(key, current):
|
|
191
|
+
nonlocal overflow
|
|
192
|
+
if overflow <= 0:
|
|
193
|
+
return current
|
|
194
|
+
take = min(current, overflow)
|
|
195
|
+
spilled[key] += take
|
|
196
|
+
overflow -= take
|
|
197
|
+
return current - take
|
|
198
|
+
|
|
199
|
+
water = spill_from("water_level", water)
|
|
200
|
+
wine = spill_from("wine_level", wine)
|
|
201
|
+
blood = spill_from("blood_level", blood)
|
|
202
|
+
must = spill_from("must_level", must)
|
|
203
|
+
fruit = spill_from("fruit_load", fruit)
|
|
204
|
+
|
|
205
|
+
floor_m["fruit_load"] = fruit
|
|
206
|
+
floor_m["must_level"] = must
|
|
207
|
+
floor_m["blood_level"] = blood
|
|
208
|
+
floor_m["wine_level"] = wine
|
|
209
|
+
floor_m["water_level"] = water
|
|
210
|
+
|
|
211
|
+
return spilled
|
|
212
|
+
except Exception:
|
|
213
|
+
return {"water_level": 0.0, "wine_level": 0.0, "blood_level": 0.0, "must_level": 0.0, "fruit_load": 0.0}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# =====================================================
|
|
217
|
+
# HELPER FUNCTIONS
|
|
218
|
+
# =====================================================
|
|
219
|
+
|
|
220
|
+
def _deg2rad(d):
|
|
221
|
+
return d * (3.141592653589793 / 180.0)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _rad2deg(r):
|
|
225
|
+
return r * 180.0 / 3.141592653589793
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _bearing_deg(lat1, lon1, lat2, lon2):
|
|
229
|
+
"""Initial bearing from (lat1,lon1) to (lat2,lon2); 0°=N, clockwise."""
|
|
230
|
+
φ1, φ2 = _deg2rad(lat1), _deg2rad(lat2)
|
|
231
|
+
Δλ = _deg2rad(lon2 - lon1)
|
|
232
|
+
|
|
233
|
+
x = (0.9999998276060441 * sin(Δλ)) * cos(φ2)
|
|
234
|
+
y = cos(φ1) * sin(φ2) - (sin(φ1) * cos(φ2) * cos(Δλ))
|
|
235
|
+
|
|
236
|
+
θ = (atan2(x, y) + 3.141592653589793) % 6.283185307179586
|
|
237
|
+
return _rad2deg(θ)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _haversine_m(lat1, lon1, lat2, lon2):
|
|
241
|
+
"""Great-circle distance in meters."""
|
|
242
|
+
from math import sin, cos, atan2, sqrt, pow
|
|
243
|
+
φ1, φ2 = _deg2rad(lat1), _deg2rad(lat2)
|
|
244
|
+
Δφ = φ2 - φ1
|
|
245
|
+
Δλ = _deg2rad(lon2 - lon1)
|
|
246
|
+
|
|
247
|
+
a = pow(sin(Δλ / 2.0), 2) + cos(φ1) * cos(φ2) * pow(sin(Δφ / 2.0), 2)
|
|
248
|
+
c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a))
|
|
249
|
+
return EARTH_RADIUS_M * c
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _vertical_angle_deg(d_horizontal_m, dz_m):
|
|
253
|
+
"""Elevation angle (deg) from observer to target."""
|
|
254
|
+
from math import atan2, rad2deg
|
|
255
|
+
if d_horizontal_m <= 0.0:
|
|
256
|
+
return 90.0 if dz_m > 0 else -90.0 if dz_m < 0 else 0.0
|
|
257
|
+
return rad2deg(atan2(dz_m, d_horizontal_m))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def compute_pegs(winter_anchor=None, summer_anchor=None):
|
|
261
|
+
"""Compute the 7 sunrise azimuth pegs for this site."""
|
|
262
|
+
try:
|
|
263
|
+
from aether_thresher import calculate_sunrise_azimuth
|
|
264
|
+
except ImportError:
|
|
265
|
+
# Fallback: use default solar azimuths
|
|
266
|
+
today = date.today()
|
|
267
|
+
if winter_anchor is None or summer_anchor is None:
|
|
268
|
+
winter_solstice_anchor, summer_solstice_anchor = current_solstice_anchors(today)
|
|
269
|
+
else:
|
|
270
|
+
winter_solstice_anchor = winter_anchor
|
|
271
|
+
summer_solstice_anchor = summer_anchor
|
|
272
|
+
pegs = [(90 + (i * 30)) % 360.0 for i in range(7)]
|
|
273
|
+
return pegs
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
from aetherfield import AetherField
|
|
277
|
+
calculate_sunrise_azimuth = AetherField.load_calibration("AetherField").sunrise_azimuth
|
|
278
|
+
except Exception:
|
|
279
|
+
today = date.today()
|
|
280
|
+
if winter_anchor is None or summer_anchor is None:
|
|
281
|
+
winter_solstice_anchor, summer_solstice_anchor = current_solstice_anchors(today)
|
|
282
|
+
else:
|
|
283
|
+
winter_solstice_anchor = winter_anchor
|
|
284
|
+
summer_solstice_anchor = summer_anchor
|
|
285
|
+
pegs = [(90 + (i * 30)) % 360.0 for i in range(7)]
|
|
286
|
+
return pegs
|
|
287
|
+
|
|
288
|
+
today = date.today()
|
|
289
|
+
|
|
290
|
+
if winter_anchor is None or summer_anchor is None:
|
|
291
|
+
winter_solstice_anchor, summer_solstice_anchor = current_solstice_anchors(today)
|
|
292
|
+
else:
|
|
293
|
+
winter_solstice_anchor = winter_anchor
|
|
294
|
+
summer_solstice_anchor = summer_anchor
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
A_w = calculate_sunrise_azimuth(winter_solstice_anchor, 0.0, 0.0, None)
|
|
298
|
+
A_s = calculate_sunrise_azimuth(summer_solstice_anchor, 0.0, 0.0, None)
|
|
299
|
+
except Exception:
|
|
300
|
+
A_w = 90.0
|
|
301
|
+
A_s = 90.0
|
|
302
|
+
|
|
303
|
+
span = A_s - A_w
|
|
304
|
+
if span < 0:
|
|
305
|
+
span = (A_s + 360.0) - A_w
|
|
306
|
+
step = span / 6.0
|
|
307
|
+
pegs = [(A_w + i * step) % 360.0 for i in range(7)]
|
|
308
|
+
|
|
309
|
+
return pegs
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def compute_solstice_anchors(year=None):
|
|
313
|
+
"""Return (winter_date, summer_date) anchors for the cycle that contains today."""
|
|
314
|
+
today = date.today()
|
|
315
|
+
if year is None:
|
|
316
|
+
year = today.year
|
|
317
|
+
summer = date(year, 6, 21)
|
|
318
|
+
winter = date(year, 12, 21)
|
|
319
|
+
|
|
320
|
+
if today <= summer:
|
|
321
|
+
return (date(year-1, 12, 21), summer)
|
|
322
|
+
else:
|
|
323
|
+
return (summer, winter)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def detect_solar_direction(lat, lon):
|
|
327
|
+
"""Simplified placeholder — returns direction based on hour."""
|
|
328
|
+
from datetime import datetime
|
|
329
|
+
hr = datetime.utcnow().hour
|
|
330
|
+
return ["east", "south", "west", "north"][hr // 6]
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def get_local_atmosphere(lat, lon, api_key=api_key):
|
|
334
|
+
"""Get local atmosphere data (temp, pressure)."""
|
|
335
|
+
try:
|
|
336
|
+
from requests import get
|
|
337
|
+
import json
|
|
338
|
+
url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}"
|
|
339
|
+
r = get(url, timeout=5)
|
|
340
|
+
data = r.json()
|
|
341
|
+
temp_k = data["main"]["temp"]
|
|
342
|
+
pressure = data["main"]["pressure"]
|
|
343
|
+
temp_c = temp_k - 273.15
|
|
344
|
+
except Exception:
|
|
345
|
+
temp_c = 10.0
|
|
346
|
+
pressure = 1013.25
|
|
347
|
+
return {"temperature": temp_c, "pressure": pressure}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def get_weather(lat, lon, api_key=api_key):
|
|
351
|
+
"""Get weather data."""
|
|
352
|
+
try:
|
|
353
|
+
import requests
|
|
354
|
+
url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}"
|
|
355
|
+
r = requests.get(url, timeout=5)
|
|
356
|
+
return r.json()
|
|
357
|
+
except Exception:
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_wind(data):
|
|
362
|
+
"""Parse wind data from weather API response."""
|
|
363
|
+
import requests
|
|
364
|
+
data = data["weather"]
|
|
365
|
+
return {
|
|
366
|
+
"direction": data.get("main", {}).get("wind", {}).get("dir", "unknown"),
|
|
367
|
+
"speed": data.get("main", {}).get("wind", {}).get("speed", 0),
|
|
368
|
+
"gusts": data.get("main", {}).get("wind", {}).get("gust", 0)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# =====================================================
|
|
373
|
+
# PLACEHOLDER FUNCTIONS FOR COMPLETENESS
|
|
374
|
+
# =====================================================
|
|
375
|
+
|
|
376
|
+
def scan_horizon(lat, lon):
|
|
377
|
+
"""Placeholder for scan_horizon implementation."""
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def as_above(dt, coords):
|
|
382
|
+
"""Placeholder for as_above zodiac mapping."""
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def so_below(dt, coords):
|
|
387
|
+
"""Placeholder for so_below zodiac mapping."""
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def sigil(floor, size=512, show=True):
|
|
392
|
+
"""Placeholder for sigil generation."""
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# =====================================================
|
|
397
|
+
# MISC / UTILITY CLASSES
|
|
398
|
+
# =====================================================
|
|
399
|
+
|
|
400
|
+
# Import the full implementations from threshold_floor.py
|
|
401
|
+
try:
|
|
402
|
+
from .threshold_floor import (
|
|
403
|
+
ThresholdFloor,
|
|
404
|
+
ChthonicVault,
|
|
405
|
+
FloorDaemon,
|
|
406
|
+
CityDaemon,
|
|
407
|
+
Gate,
|
|
408
|
+
)
|
|
409
|
+
except ImportError as e:
|
|
410
|
+
print(f"Warning: Could not import classes from threshold_floor: {e}")
|
|
411
|
+
|
|
412
|
+
# Create minimal stub classes that work for testing
|
|
413
|
+
class ThresholdFloor:
|
|
414
|
+
def __init__(self, name, latitude=0, longitude=0, tz="UTC", elevation_m=0.0):
|
|
415
|
+
self.name = name
|
|
416
|
+
self.latitude = latitude
|
|
417
|
+
self.longitude = longitude
|
|
418
|
+
self.tz = tz
|
|
419
|
+
self.elevation_m = elevation_m
|
|
420
|
+
self.pegs = [90, 120, 150, 180, 210, 240, 270]
|
|
421
|
+
|
|
422
|
+
class ChthonicVault:
|
|
423
|
+
def __init__(self):
|
|
424
|
+
self.is_open = False
|
|
425
|
+
self.keys = {}
|
|
426
|
+
self.sandals = {}
|
|
427
|
+
self.seed_storage = 0
|
|
428
|
+
self.is_open = False
|
|
429
|
+
self.guardian_inside = None
|
|
430
|
+
|
|
431
|
+
def open_gate(self, guardian):
|
|
432
|
+
self.is_open = True
|
|
433
|
+
self.guardian_inside = guardian
|
|
434
|
+
|
|
435
|
+
def close_gate(self):
|
|
436
|
+
self.is_open = False
|
|
437
|
+
self.guardian_inside = None
|
|
438
|
+
|
|
439
|
+
def deposit_seed(self, amount):
|
|
440
|
+
self.seed_storage += amount
|
|
441
|
+
|
|
442
|
+
def withdraw_seed(self, amount):
|
|
443
|
+
if self.seed_storage >= amount:
|
|
444
|
+
self.seed_storage -= amount
|
|
445
|
+
return amount
|
|
446
|
+
return 0
|
|
447
|
+
|
|
448
|
+
def fetch_key(self, sign):
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
def fetch_sandal(self, month):
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
class FloorDaemon:
|
|
455
|
+
def __init__(self, name, latitude, longitude, tz, guardian_id):
|
|
456
|
+
self.name = name
|
|
457
|
+
self.floor = ThresholdFloor(name, latitude, longitude, tz)
|
|
458
|
+
self.floor.guardian = guardian_id
|
|
459
|
+
self.phase = None
|
|
460
|
+
|
|
461
|
+
def run_sweep(self):
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
class CityDaemon:
|
|
465
|
+
def __init__(self, name, latitude, longitude, tz, guardian_id):
|
|
466
|
+
self.name = name
|
|
467
|
+
self.floor = ThresholdFloor(name, latitude, longitude, tz)
|
|
468
|
+
self.floor.guardian = guardian_id
|
|
469
|
+
self.phase = None
|
|
470
|
+
|
|
471
|
+
def run_sweep(self):
|
|
472
|
+
pass
|
|
473
|
+
|
|
474
|
+
class Gate:
|
|
475
|
+
def __init__(self, city, rung, posts, coords, tree_link, direction_policy="both", stone_required=None):
|
|
476
|
+
self.city = city
|
|
477
|
+
self.rung = rung
|
|
478
|
+
self.posts = posts if posts else []
|
|
479
|
+
self.coords = coords
|
|
480
|
+
self.tree_link = tree_link
|
|
481
|
+
self.direction_policy = direction_policy.lower()
|
|
482
|
+
self.stone_required = stone_required
|
|
483
|
+
|
|
484
|
+
def allows_direction(self, axis):
|
|
485
|
+
return True
|
|
486
|
+
|
|
487
|
+
def is_rung_active(self, k_step):
|
|
488
|
+
return k_step == self.rung
|
|
489
|
+
|
|
490
|
+
def can_open(self, k_step, axis, today):
|
|
491
|
+
return True
|
|
492
|
+
|
|
493
|
+
def tie_cord(self, who, post, stone, today):
|
|
494
|
+
return {"ok": False, "reason": "bad_post"}
|
|
495
|
+
|
|
496
|
+
def open_state(self, k_step, axis, today):
|
|
497
|
+
return {"city": self.city, "rung": self.rung, "active": True}
|
|
498
|
+
|
|
499
|
+
class ThresholdFloor:
|
|
500
|
+
"""The Threshold Floor — where sun, moon, and alchemy intersect.
|
|
501
|
+
|
|
502
|
+
Falls back to delegate implementation if available.
|
|
503
|
+
"""
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class ChthonicVault:
|
|
508
|
+
"""ChthonicVault — The vault beneath the earth's threshold."""
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class FloorDaemon:
|
|
513
|
+
"""FloorDaemon — Manages floor sweeps and alchemical phases."""
|
|
514
|
+
pass
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class CityDaemon:
|
|
518
|
+
"""CityDaemon — Coordinates floor dawns across a city's horizon."""
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class Gate:
|
|
523
|
+
"""Gate — Threshold controls for passage."""
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
__all__ = [
|
|
528
|
+
"ThresholdFloor",
|
|
529
|
+
"ChthonicVault",
|
|
530
|
+
"FloorDaemon",
|
|
531
|
+
"CityDaemon",
|
|
532
|
+
"Gate",
|
|
533
|
+
"calculate_sunrise_azimuth",
|
|
534
|
+
"determine_solar_movement",
|
|
535
|
+
"is_solstice",
|
|
536
|
+
"current_solstice_anchors",
|
|
537
|
+
"compute_pegs",
|
|
538
|
+
"compute_solstice_anchors",
|
|
539
|
+
"detect_solar_direction",
|
|
540
|
+
"get_local_atmosphere",
|
|
541
|
+
"get_weather",
|
|
542
|
+
"get_wind",
|
|
543
|
+
"scan_horizon",
|
|
544
|
+
"as_above",
|
|
545
|
+
"so_below",
|
|
546
|
+
"sigil",
|
|
547
|
+
]
|