게임 안에서 바다의 하루를 정할 수 있게
메뉴 > 시간 하위 메뉴에 세 갈래를 둔다. - 실제 시각: 바깥 시계 그대로 (기본) - 빠르게: 하루를 5분으로 압축해 돌린다 - 한 시각에 멈추기: 새벽/아침/낮/저녁/밤 프리셋의 이름과 시각을 DayCycle.FIXED_PRESETS 한곳에 같이 두어, 메뉴 항목과 실제로 적용되는 시각이 어긋날 수 없게 했다. 고른 값은 window_state의 time_mode/time_hour에 저장한다. 모드를 바꿀 때 지금 보이던 시각을 먼저 잡아 둔다. mode를 먼저 바꾼 뒤 hour()를 부르면 이미 새 모드의 시계를 읽어 0시로 튀었다 -- 낮에 "빠르게"를 켜면 한밤중으로 떨어지는 버그였다. 명령줄 --hour=21은 그대로 살아 있고 FIXED 모드의 지름길이 되었다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,10 @@
|
||||
| 상점 열기 | 왼쪽 위 **상점** (패널은 오른쪽에 열린다) |
|
||||
| 메뉴 | **···** 버튼 또는 물 위 우클릭 |
|
||||
|
||||
메뉴: 항상 위에 표시 · 클릭 통과 · 높이(낮게/보통/높게) · 위아래 도킹 · 화면 선택(모니터가 여러 대일 때) · 지금 저장 · 처음부터 다시 · 종료
|
||||
메뉴: 항상 위에 표시 · 클릭 통과 · 전체화면으로 감상 · **시간** · 높이(낮게/보통/높게) · 위아래 도킹 · 화면 선택(모니터가 여러 대일 때) · 지금 저장 · 처음부터 다시 · 종료
|
||||
|
||||
**시간** 하위 메뉴에서 바다의 하루를 정한다 — 실제 시각(기본), 빠르게(하루 5분),
|
||||
또는 새벽/아침/낮/저녁/밤 한 시각에 멈추기. 고른 값은 저장된다.
|
||||
**클릭 통과**를 켜면 클릭이 바탕화면으로 지나가고, 안쪽 모서리(84×56px)만 계속 눌린다.
|
||||
이 모서리를 눌러 메뉴로 돌아와 통과를 끌 수 있다. (아래 도킹이면 오른쪽 위, 위 도킹이면 오른쪽 아래)
|
||||
|
||||
|
||||
+51
-6
@@ -38,16 +38,49 @@ const LIGHT_KEYS := [
|
||||
## 빛은 천천히 변하므로 매 프레임 다시 잴 이유가 없다.
|
||||
const REFRESH_INTERVAL := 0.5
|
||||
|
||||
## 0 이상이면 그 시각으로 고정한다. 테스트와 눈으로 확인할 때 쓴다.
|
||||
var forced_hour := -1.0:
|
||||
## 시간을 어떻게 굴릴지.
|
||||
## REAL 바깥 시계를 그대로 (기본)
|
||||
## FIXED 한 시각에 멈춰 둔다. 밤바다만 보고 싶을 때.
|
||||
## FAST 하루를 몇 분으로 압축해 돌린다. 하루가 지나가는 걸 보고 싶을 때.
|
||||
enum Mode { REAL, FIXED, FAST }
|
||||
|
||||
## 메뉴에 올리는 고정 시각들. 이름과 시각을 한곳에 둬서 메뉴와 어긋나지 않게 한다.
|
||||
const FIXED_PRESETS := [
|
||||
{"name": "새벽", "hour": 4.5},
|
||||
{"name": "아침", "hour": 8.0},
|
||||
{"name": "낮", "hour": 13.0},
|
||||
{"name": "저녁", "hour": 18.7},
|
||||
{"name": "밤", "hour": 22.0},
|
||||
]
|
||||
|
||||
## 빠르게 돌릴 때 하루가 몇 분인지.
|
||||
const FAST_DAY_MINUTES := 5.0
|
||||
|
||||
var mode := Mode.REAL:
|
||||
set(value):
|
||||
forced_hour = value
|
||||
if mode == value:
|
||||
return
|
||||
# 지금 보이던 시각을 먼저 잡아 둔다. mode를 바꾼 뒤에 hour()를 부르면
|
||||
# 이미 새 모드의 시계를 읽어 0시로 튀어 버린다.
|
||||
var carry := hour()
|
||||
mode = value
|
||||
# 빠르게로 넘어갈 때는 보던 시각에서 이어 흐르게 한다.
|
||||
# 0시로 떨어지면 한밤중으로 뚝 끊긴다.
|
||||
if mode == Mode.FAST:
|
||||
_fast_hour = carry
|
||||
refresh()
|
||||
|
||||
## 고정 모드일 때 멈춰 있을 시각.
|
||||
var fixed_hour := 13.0:
|
||||
set(value):
|
||||
fixed_hour = fposmod(value, 24.0)
|
||||
refresh()
|
||||
|
||||
var tint := Color.WHITE
|
||||
var light := 1.0
|
||||
var phase := ""
|
||||
|
||||
var _fast_hour := 0.0
|
||||
var _timer := 0.0
|
||||
|
||||
|
||||
@@ -55,11 +88,16 @@ func _ready() -> void:
|
||||
# 개발 중 특정 시각을 눈으로 확인할 때: godot --path . -- --hour=21
|
||||
for arg in OS.get_cmdline_user_args():
|
||||
if arg.begins_with("--hour="):
|
||||
forced_hour = float(arg.trim_prefix("--hour="))
|
||||
fixed_hour = float(arg.trim_prefix("--hour="))
|
||||
mode = Mode.FIXED
|
||||
refresh()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# 압축한 시계는 매 틀 굴려야 매끄럽게 흐른다. 화면 갱신만 뜸하게 한다.
|
||||
if mode == Mode.FAST:
|
||||
_fast_hour = fposmod(_fast_hour + delta * 24.0 / (FAST_DAY_MINUTES * 60.0), 24.0)
|
||||
|
||||
_timer += delta
|
||||
if _timer < REFRESH_INTERVAL:
|
||||
return
|
||||
@@ -69,12 +107,19 @@ func _process(delta: float) -> void:
|
||||
|
||||
## 지금 몇 시인지. 소수점까지 있는 시각(13.5 = 오후 1시 30분).
|
||||
func hour() -> float:
|
||||
if forced_hour >= 0.0:
|
||||
return fposmod(forced_hour, 24.0)
|
||||
match mode:
|
||||
Mode.FIXED: return fixed_hour
|
||||
Mode.FAST: return _fast_hour
|
||||
var t := Time.get_time_dict_from_system()
|
||||
return float(t.hour) + float(t.minute) / 60.0 + float(t.second) / 3600.0
|
||||
|
||||
|
||||
## "13:24" 처럼 보여줄 문자열.
|
||||
func clock_text() -> String:
|
||||
var h := hour()
|
||||
return "%02d:%02d" % [int(h), int(fposmod(h * 60.0, 60.0))]
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var h := hour()
|
||||
tint = tint_at(h)
|
||||
|
||||
+15
-2
@@ -302,8 +302,21 @@
|
||||
같이 묻힌다. `_draw_glow()`가 `1 - light`만큼 색을 끌어올리고 후광을 넓혀,
|
||||
어두울수록 오히려 도드라지게 했다. 심해 해파리가 밤에 살아나는 것이 이 때문이다.
|
||||
|
||||
**눈으로 확인하기**: `godot --path . -- --hour=21` 처럼 시각을 고정할 수 있다.
|
||||
테스트도 `DayCycle.forced_hour`로 같은 문을 쓴다.
|
||||
**게임 안에서 정하기**: `···` 메뉴의 **시간** 하위 메뉴에 세 갈래가 있다.
|
||||
|
||||
- `Mode.REAL` — 바깥 시계 그대로. 기본값이다.
|
||||
- `Mode.FIXED` — 한 시각에 멈춘다. 밤바다만 보고 싶을 때.
|
||||
메뉴에 올리는 프리셋은 `FIXED_PRESETS`에 이름과 시각을 같이 둬서
|
||||
메뉴 항목과 실제 시각이 어긋나지 않게 했다.
|
||||
- `Mode.FAST` — 하루를 `FAST_DAY_MINUTES`(5분)로 압축해 돌린다.
|
||||
압축한 시계는 매 틀 굴리고 화면 갱신만 뜸하게 한다.
|
||||
|
||||
모드를 바꿀 때 **지금 보이던 시각을 먼저 잡아 둔다.** `mode`를 먼저 바꾼 뒤
|
||||
`hour()`를 부르면 이미 새 모드의 시계를 읽어 0시로 튄다 -- 빠르게로 넘어갈 때마다
|
||||
한밤중으로 뚝 떨어지는 버그였다.
|
||||
|
||||
고른 값은 `window_state["time_mode"]/["time_hour"]`에 저장된다.
|
||||
명령줄 `--hour=21`은 FIXED 모드의 지름길이고, 테스트도 같은 문을 쓴다.
|
||||
|
||||
## 감상 모드 (전체화면)
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ const HEIGHTS := [
|
||||
const EDGE_BOTTOM := 20
|
||||
const EDGE_TOP := 21
|
||||
const SCREEN_ID_BASE := 100
|
||||
## 시간 하위 메뉴. 200 실제, 201 빠르게, 210+i 고정 프리셋.
|
||||
const TIME_REAL := 200
|
||||
const TIME_FAST := 201
|
||||
const TIME_FIXED_BASE := 210
|
||||
|
||||
## 클릭 통과 중에도 이 크기의 모서리는 계속 클릭을 받는다.
|
||||
## 이게 없으면 클릭 통과를 켠 순간 위젯으로 돌아올 방법이 없어진다.
|
||||
@@ -29,6 +33,7 @@ const ESCAPE_HATCH := Vector2(84, 56)
|
||||
@onready var hud: HUDLayer = $HUD
|
||||
@onready var shop: ShopPanel = $ShopPanel
|
||||
@onready var menu: PopupMenu = $ContextMenu
|
||||
@onready var time_menu: PopupMenu = $ContextMenu/TimeMenu
|
||||
|
||||
var _always_on_top := true
|
||||
var _click_through := false
|
||||
@@ -46,6 +51,8 @@ func _ready() -> void:
|
||||
# state는 SaveManager의 딕셔너리를 그대로 가리킨다. 아래 _apply_dock()이
|
||||
# _remember_window()로 같은 딕셔너리를 덮어쓰므로, 나중에 쓸 값은 미리 빼둔다.
|
||||
var saved_layer := int(state.get("layer", 0))
|
||||
var saved_time_mode := int(state.get("time_mode", DayCycle.Mode.REAL))
|
||||
var saved_time_hour := float(state.get("time_hour", 13.0))
|
||||
_always_on_top = bool(state.get("always_on_top", true))
|
||||
_click_through = bool(state.get("click_through", false))
|
||||
_edge_bottom = bool(state.get("edge_bottom", true))
|
||||
@@ -67,6 +74,8 @@ func _ready() -> void:
|
||||
hud.reserved_top_changed.connect(func(px: float): aquarium.grid_top = px)
|
||||
aquarium.grid_top = hud.reserved_top()
|
||||
aquarium.current_zone = saved_layer
|
||||
DayCycle.fixed_hour = saved_time_hour
|
||||
DayCycle.mode = saved_time_mode as DayCycle.Mode
|
||||
|
||||
hud.shop_pressed.connect(_toggle_shop)
|
||||
hud.menu_pressed.connect(_open_menu)
|
||||
@@ -128,6 +137,8 @@ func _remember_window() -> void:
|
||||
SaveManager.window_state["always_on_top"] = _always_on_top
|
||||
SaveManager.window_state["click_through"] = _click_through
|
||||
SaveManager.window_state["layer"] = aquarium.current_zone
|
||||
SaveManager.window_state["time_mode"] = int(DayCycle.mode)
|
||||
SaveManager.window_state["time_hour"] = DayCycle.fixed_hour
|
||||
SaveManager.window_state["edge_bottom"] = _edge_bottom
|
||||
SaveManager.window_state["height"] = _strip_height
|
||||
SaveManager.window_state["screen"] = _screen
|
||||
@@ -257,6 +268,9 @@ func _build_menu() -> void:
|
||||
menu.add_check_item("클릭 통과 (모서리로 복귀)", MenuId.CLICK_THROUGH)
|
||||
menu.add_check_item("전체화면으로 감상 (F11)", MenuId.FULLSCREEN)
|
||||
menu.add_separator()
|
||||
_build_time_menu()
|
||||
menu.add_submenu_node_item("시간", time_menu)
|
||||
menu.add_separator()
|
||||
for h in HEIGHTS:
|
||||
menu.add_radio_check_item(h.label, h.id)
|
||||
menu.add_separator()
|
||||
@@ -275,12 +289,57 @@ func _build_menu() -> void:
|
||||
_sync_menu()
|
||||
|
||||
|
||||
## 시간을 바깥 시계에 맡길지, 한 시각에 멈출지, 빠르게 돌릴지.
|
||||
func _build_time_menu() -> void:
|
||||
time_menu.clear()
|
||||
time_menu.add_radio_check_item("실제 시각", TIME_REAL)
|
||||
time_menu.add_radio_check_item(
|
||||
"빠르게 (하루 %d분)" % int(DayCycle.FAST_DAY_MINUTES), TIME_FAST)
|
||||
time_menu.add_separator("한 시각에 멈추기")
|
||||
for i in DayCycle.FIXED_PRESETS.size():
|
||||
var p: Dictionary = DayCycle.FIXED_PRESETS[i]
|
||||
time_menu.add_radio_check_item(String(p.name), TIME_FIXED_BASE + i)
|
||||
time_menu.id_pressed.connect(_on_time_menu_id)
|
||||
|
||||
|
||||
func _sync_time_menu() -> void:
|
||||
_check_in(time_menu, TIME_REAL, DayCycle.mode == DayCycle.Mode.REAL)
|
||||
_check_in(time_menu, TIME_FAST, DayCycle.mode == DayCycle.Mode.FAST)
|
||||
for i in DayCycle.FIXED_PRESETS.size():
|
||||
var fixed := DayCycle.mode == DayCycle.Mode.FIXED
|
||||
var same := is_equal_approx(
|
||||
DayCycle.fixed_hour, float(DayCycle.FIXED_PRESETS[i].hour))
|
||||
_check_in(time_menu, TIME_FIXED_BASE + i, fixed and same)
|
||||
|
||||
|
||||
func _on_time_menu_id(id: int) -> void:
|
||||
if id == TIME_REAL:
|
||||
DayCycle.mode = DayCycle.Mode.REAL
|
||||
hud.show_toast("시간을 실제 시각에 맞췄어요 · 지금 %s" % DayCycle.clock_text())
|
||||
elif id == TIME_FAST:
|
||||
DayCycle.mode = DayCycle.Mode.FAST
|
||||
hud.show_toast("하루가 %d분에 한 바퀴 돕니다" % int(DayCycle.FAST_DAY_MINUTES))
|
||||
elif id >= TIME_FIXED_BASE:
|
||||
var p: Dictionary = DayCycle.FIXED_PRESETS[id - TIME_FIXED_BASE]
|
||||
DayCycle.fixed_hour = float(p.hour)
|
||||
DayCycle.mode = DayCycle.Mode.FIXED
|
||||
hud.show_toast("%s로 멈춰 뒀어요 · %s" % [p.name, DayCycle.clock_text()])
|
||||
_sync_time_menu()
|
||||
|
||||
|
||||
func _check_in(target: PopupMenu, id: int, on: bool) -> void:
|
||||
var index := target.get_item_index(id)
|
||||
if index >= 0:
|
||||
target.set_item_checked(index, on)
|
||||
|
||||
|
||||
func _sync_menu() -> void:
|
||||
_check(MenuId.ALWAYS_ON_TOP, _always_on_top)
|
||||
_check(MenuId.CLICK_THROUGH, _click_through)
|
||||
_check(MenuId.FULLSCREEN, _fullscreen)
|
||||
for h in HEIGHTS:
|
||||
_check(h.id, _strip_height == int(h.px))
|
||||
_sync_time_menu()
|
||||
_check(EDGE_BOTTOM, _edge_bottom)
|
||||
_check(EDGE_TOP, not _edge_bottom)
|
||||
if DisplayServer.get_screen_count() > 1:
|
||||
|
||||
@@ -26,3 +26,5 @@ visible = false
|
||||
layout_mode = 1
|
||||
|
||||
[node name="ContextMenu" type="PopupMenu" parent="."]
|
||||
|
||||
[node name="TimeMenu" type="PopupMenu" parent="ContextMenu"]
|
||||
|
||||
+66
-12
@@ -33,6 +33,7 @@ func _ready() -> void:
|
||||
_test_shape_families()
|
||||
_test_upgrades_are_all_production()
|
||||
_test_day_cycle()
|
||||
_test_time_modes()
|
||||
|
||||
if _failures == 0:
|
||||
print("모든 검사 통과")
|
||||
@@ -623,20 +624,22 @@ func _test_upgrades_are_all_production() -> void:
|
||||
|
||||
func _test_day_cycle() -> void:
|
||||
print("하루의 빛")
|
||||
var before := DayCycle.forced_hour
|
||||
var before_mode := DayCycle.mode
|
||||
var before_hour := DayCycle.fixed_hour
|
||||
DayCycle.mode = DayCycle.Mode.FIXED
|
||||
|
||||
DayCycle.forced_hour = 5.0
|
||||
DayCycle.fixed_hour = 5.0
|
||||
check("새벽 5시는 새벽", DayCycle.phase == "새벽", DayCycle.phase)
|
||||
DayCycle.forced_hour = 8.0
|
||||
DayCycle.fixed_hour = 8.0
|
||||
check("아침 8시는 아침", DayCycle.phase == "아침", DayCycle.phase)
|
||||
DayCycle.forced_hour = 13.0
|
||||
DayCycle.fixed_hour = 13.0
|
||||
check("오후 1시는 낮", DayCycle.phase == "낮", DayCycle.phase)
|
||||
DayCycle.forced_hour = 18.0
|
||||
DayCycle.fixed_hour = 18.0
|
||||
check("저녁 6시는 저녁", DayCycle.phase == "저녁", DayCycle.phase)
|
||||
DayCycle.forced_hour = 22.0
|
||||
DayCycle.fixed_hour = 22.0
|
||||
check("밤 10시는 밤", DayCycle.phase == "밤", DayCycle.phase)
|
||||
# 자정을 넘긴 시각은 전날 밤이 이어져야 한다. 여기서 구간이 비면 이름이 빈다.
|
||||
DayCycle.forced_hour = 1.0
|
||||
DayCycle.fixed_hour = 1.0
|
||||
check("새벽 1시도 밤", DayCycle.phase == "밤", DayCycle.phase)
|
||||
|
||||
# 가장 밝은 시각이 한낮 언저리에 있어야 한다. 정점 시각 자체는 표에서 조절한다.
|
||||
@@ -651,18 +654,18 @@ func _test_day_cycle() -> void:
|
||||
check("가장 밝은 때가 한낮 언저리", peak_hour >= 10.0 and peak_hour <= 14.0,
|
||||
"%.1f시" % peak_hour)
|
||||
check("한낮 밝기는 1.0", is_equal_approx(peak_light, 1.0), str(peak_light))
|
||||
DayCycle.forced_hour = 0.0
|
||||
DayCycle.fixed_hour = 0.0
|
||||
var midnight := DayCycle.light
|
||||
DayCycle.forced_hour = 24.0
|
||||
DayCycle.fixed_hour = 24.0
|
||||
check("자정과 24시가 이어진다", is_equal_approx(DayCycle.light, midnight),
|
||||
"%f vs %f" % [DayCycle.light, midnight])
|
||||
|
||||
# 빛이 튀면 해가 순간이동하는 것처럼 보인다. 하루를 훑으며 급변이 없는지 본다.
|
||||
var max_jump := 0.0
|
||||
DayCycle.forced_hour = 0.0
|
||||
DayCycle.fixed_hour = 0.0
|
||||
var prev := DayCycle.light
|
||||
for i in range(1, 481):
|
||||
DayCycle.forced_hour = float(i) / 20.0
|
||||
DayCycle.fixed_hour = float(i) / 20.0
|
||||
max_jump = maxf(max_jump, absf(DayCycle.light - prev))
|
||||
prev = DayCycle.light
|
||||
check("빛이 튀는 곳이 없다", max_jump < 0.05, "최대 변화 %f" % max_jump)
|
||||
@@ -672,4 +675,55 @@ func _test_day_cycle() -> void:
|
||||
check("밤은 낮보다 어둡다", night.r + night.g + night.b < noon.r + noon.g + noon.b)
|
||||
check("저녁은 붉은 쪽으로 기운다", DayCycle.tint_at(18.5).r > DayCycle.tint_at(18.5).b)
|
||||
|
||||
DayCycle.forced_hour = before
|
||||
DayCycle.fixed_hour = before_hour
|
||||
DayCycle.mode = before_mode
|
||||
|
||||
|
||||
func _test_time_modes() -> void:
|
||||
print("시간 설정")
|
||||
var before_mode := DayCycle.mode
|
||||
var before_hour := DayCycle.fixed_hour
|
||||
|
||||
DayCycle.fixed_hour = 22.0
|
||||
DayCycle.mode = DayCycle.Mode.FIXED
|
||||
check("고정하면 그 시각에 멈춘다", is_equal_approx(DayCycle.hour(), 22.0),
|
||||
str(DayCycle.hour()))
|
||||
check("고정한 시각의 이름이 맞다", DayCycle.phase == "밤", DayCycle.phase)
|
||||
check("시계 문자열", DayCycle.clock_text() == "22:00", DayCycle.clock_text())
|
||||
DayCycle.fixed_hour = 18.7
|
||||
check("분까지 나온다", DayCycle.clock_text() == "18:42", DayCycle.clock_text())
|
||||
|
||||
# 24시를 넘겨 넣어도 하루 안으로 돌아와야 한다. 안 그러면 램프가 끝에 붙어 멈춘다.
|
||||
DayCycle.fixed_hour = 26.0
|
||||
check("24시를 넘기면 돌아 들어온다", is_equal_approx(DayCycle.hour(), 2.0),
|
||||
str(DayCycle.hour()))
|
||||
|
||||
# 빠르게로 넘어갈 때 보던 시각에서 이어져야 한다. 0시로 튀면 한밤중으로 뚝 떨어진다.
|
||||
DayCycle.fixed_hour = 15.0
|
||||
DayCycle.mode = DayCycle.Mode.FIXED
|
||||
DayCycle.mode = DayCycle.Mode.FAST
|
||||
check("빠르게는 보던 시각에서 이어진다", absf(DayCycle.hour() - 15.0) < 0.01,
|
||||
str(DayCycle.hour()))
|
||||
|
||||
var start := DayCycle.hour()
|
||||
DayCycle._process(30.0)
|
||||
var moved := DayCycle.hour() - start
|
||||
# 하루 5분이면 30초에 2.4시간이 흐른다.
|
||||
var expected := 30.0 * 24.0 / (DayCycle.FAST_DAY_MINUTES * 60.0)
|
||||
check("빠르게는 압축한 만큼 흐른다", absf(moved - expected) < 0.05,
|
||||
"%f vs %f" % [moved, expected])
|
||||
|
||||
DayCycle.mode = DayCycle.Mode.REAL
|
||||
var now := Time.get_time_dict_from_system()
|
||||
check("실제 시각으로 돌아온다", absf(DayCycle.hour() - float(now.hour)) < 1.01,
|
||||
"%f vs %d시" % [DayCycle.hour(), now.hour])
|
||||
|
||||
# 메뉴에 올리는 프리셋이 모두 제 이름의 구간에 들어 있어야 한다.
|
||||
var named := true
|
||||
for p in DayCycle.FIXED_PRESETS:
|
||||
if DayCycle.phase_at(float(p.hour)) != String(p.name):
|
||||
named = false
|
||||
check("고정 프리셋 이름이 구간과 맞는다", named)
|
||||
|
||||
DayCycle.fixed_hour = before_hour
|
||||
DayCycle.mode = before_mode
|
||||
|
||||
Reference in New Issue
Block a user