89069b06f0
거품은 이제 클릭이 아니라 바다에 있는 생물이 시간에 따라 번다. 화면 가장자리에
깔아 두는 위젯인데 클릭이 주된 수입원이면 연타가 최적 플레이가 되어 형태와
어긋난다. 클릭 수익과 클릭 업그레이드를 걷어내고 업그레이드는 전부 생산 배율로
바꿨다. 물을 만지면 거품이 퍼지지만 얻는 것은 없다.
격자 칸을 차지하는 것은 집뿐이고, 생물은 칸이 아니라 집에 산다(한 채당 3마리).
칸 → 집 → 생물 → 거품으로 한계가 두 겹 걸린다. 예전에는 생물과 장식이 같은
자원(칸)을 놓고 경쟁해서 최적 플레이가 늘 "장식을 사지 말 것"이었는데, 집과
일꾼으로 나누니 그 경쟁이 의존으로 바뀐다. 집은 같은 종류도 여러 채 지을 수 있고
거품을 벌지 않는다 — 정원과 생산을 한 물건이 주면 곱연산이 두 번 걸린다.
어느 생물이 어느 집에 사는지는 저장하지 않는다. 집은 정원만 주므로 어디 살든
결과가 같고, 저장하면 재배정과 인덱스 관리만 붙는다. 그릴 때 구역마다 배정한다.
그래서 저장 형식에서 생물은 위치 없이 {id: 마리 수}다. 옛 저장본은 그때의
장식을 집으로, 생물을 마릿수로 옮겨오고 정원이 모자라면 집을 세워 메운다.
수입이 생물에서만 나오므로 막다른 길을 두 곳에서 막았다. 마지막 생물은 내보낼 수
없고, 살던 생물이 갈 곳이 없어지는 집은 헐 수 없다.
집과 생물의 그림 형태를 갈랐다. 집 10채 중 8채가 생물과 같은 rock이어서 항아리와
거품돌이 색만 다른 같은 덩어리로 보였다. 집은 vessel/wreck/pillar만, 생물은
plant/rock/fish/glow만 쓴다. 헤엄치는 생물은 집을 도는 타원 궤도에 올라가고,
원점이 궤도를 따라 움직이므로 Y 정렬이 앞뒤를 알아서 바꿔준다 — 뒤로 돌면 집에
가렸다가 앞으로 나온다. 얕은 바다 생물이 거품돌과 해초뿐이라 이 움직임을 볼 수
없어서 구피를 넣었다.
실행해 보고 잡은 버그 셋:
- 저장본이 없으면 load_game이 그냥 반환해 빈 바다로 시작했다. 집도 생물도 없어
아무 일도 일어나지 않는다. 이제 새 게임을 시작한다.
- 창 높이·도킹·모니터 설정이 복원되지 않고 기본값으로 덮어써지고 있었다.
window_state에 미리 있던 키만 복사하던 것을 저장본에 있는 것을 전부 싣도록 고쳤다.
- 정보 바가 격자 첫 줄을 덮어 왼쪽 위에 놓인 것이 보이지 않았다. GRID_TOP 상수를
버리고 HUD가 잰 실제 바 높이를 받아 쓴다.
- main.gd가 초기화할 때 존재하지 않는 aquarium.rebuild()를 부르고 있었다.
스모크 테스트 159개.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uze4mF1NAGfFRio77cMFqg
109 lines
4.1 KiB
GDScript
109 lines
4.1 KiB
GDScript
extends Control
|
|
class_name HUDLayer
|
|
## 스트립 왼쪽 위에 붙는 한 줄짜리 정보 바와 짧은 알림.
|
|
## 스트립은 화면에 도킹되므로 창을 끌어 옮기는 기능은 없다. 위치는 메뉴에서 바꾼다.
|
|
|
|
signal shop_pressed
|
|
signal menu_pressed
|
|
## 정보 바가 위에서 자리를 얼마나 먹는지. 바 높이는 글꼴과 내용에 따라 달라지므로
|
|
## 재어서 알려주고, main이 받아 수조 격자를 그만큼 아래로 내린다.
|
|
signal reserved_top_changed(px: float)
|
|
|
|
## 바 아래로 이만큼은 띄워야 첫 줄 칸이 바에 닿지 않는다.
|
|
const BAR_CLEARANCE := 6.0
|
|
|
|
@onready var control_bar: PanelContainer = $ControlBar
|
|
@onready var bubbles_label: Label = %BubblesLabel
|
|
@onready var bps_label: Label = %BpsLabel
|
|
@onready var beauty_label: Label = %BeautyLabel
|
|
@onready var zone_label: Label = %ZoneLabel
|
|
@onready var shop_button: Button = %ShopButton
|
|
@onready var menu_button: Button = %MenuButton
|
|
@onready var toast: Label = %Toast
|
|
@onready var trash: PanelContainer = %Trash
|
|
@onready var trash_label: Label = %TrashLabel
|
|
|
|
var _toast_tween: Tween
|
|
|
|
|
|
func _ready() -> void:
|
|
control_bar.add_theme_stylebox_override("panel", UIStyle.panel(UIStyle.PANEL_BG, 12))
|
|
UIStyle.label(bubbles_label, 24, UIStyle.INK)
|
|
UIStyle.label(bps_label, 13, UIStyle.INK_DIM)
|
|
UIStyle.label(beauty_label, 13, UIStyle.GOLD)
|
|
UIStyle.label(zone_label, 13, UIStyle.ACCENT)
|
|
UIStyle.label(toast, 14, UIStyle.INK)
|
|
toast.modulate.a = 0.0
|
|
|
|
trash.add_theme_stylebox_override("panel", UIStyle.panel(Color(0.35, 0.06, 0.10, 0.88), 12))
|
|
UIStyle.label(trash_label, 13, UIStyle.INK)
|
|
trash.hide()
|
|
|
|
UIStyle.style_button(shop_button)
|
|
UIStyle.style_button(menu_button)
|
|
shop_button.pressed.connect(func(): shop_pressed.emit())
|
|
menu_button.pressed.connect(func(): menu_pressed.emit())
|
|
|
|
control_bar.resized.connect(func(): reserved_top_changed.emit(reserved_top()))
|
|
|
|
GameState.zone_unlocked.connect(func(_i: int): _refresh_zone_label())
|
|
GameState.state_changed.connect(_refresh_zone_label)
|
|
GameState.reloaded.connect(_refresh_zone_label)
|
|
SaveManager.offline_earned.connect(_on_offline_earned)
|
|
_refresh_zone_label()
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
bubbles_label.text = NumberFormat.short(GameState.bubbles)
|
|
# 정원이 곧 성장의 한계라 초당 생산량 옆에 늘 같이 둔다.
|
|
bps_label.text = "초당 %s · 생물 %d/%d" % [
|
|
NumberFormat.short(GameState.bps), GameState.fish_total, GameState.capacity_total()
|
|
]
|
|
beauty_label.text = "아름다움 %s · x%.2f" % [
|
|
NumberFormat.short(GameState.beauty), GameState.beauty_mult
|
|
]
|
|
|
|
|
|
## 몇 개 구역을 열었고 다음은 무엇인지. 해금 조건 자체는 잠긴 구역 위에 직접 표시된다.
|
|
func _refresh_zone_label() -> void:
|
|
var opened := GameState.unlocked_zones
|
|
var total := Catalog.zone_count()
|
|
if not GameState.has_next_zone():
|
|
zone_label.text = "구역 %d/%d · 모두 열었어요" % [opened, total]
|
|
return
|
|
var next_zone := Catalog.zone(GameState.next_zone_index())
|
|
zone_label.text = "구역 %d/%d · 다음 %s %s" % [
|
|
opened, total, next_zone.name, NumberFormat.short(GameState.next_zone_cost())
|
|
]
|
|
|
|
|
|
func _on_offline_earned(amount: float, seconds: int) -> void:
|
|
show_toast("%s 동안 거품 %s를 모았어요" % [
|
|
NumberFormat.duration(seconds), NumberFormat.short(amount)
|
|
], 4.5)
|
|
|
|
|
|
func show_toast(text: String, duration := 2.0) -> void:
|
|
toast.text = text
|
|
if _toast_tween and _toast_tween.is_valid():
|
|
_toast_tween.kill()
|
|
_toast_tween = create_tween()
|
|
_toast_tween.tween_property(toast, "modulate:a", 1.0, 0.2)
|
|
_toast_tween.tween_interval(duration)
|
|
_toast_tween.tween_property(toast, "modulate:a", 0.0, 0.5)
|
|
|
|
|
|
## 무언가를 집어 든 동안만 휴지통을 보여준다. 평소에는 자리를 차지하지 않는다.
|
|
func set_trash_visible(on: bool) -> void:
|
|
trash.visible = on
|
|
|
|
|
|
## 휴지통의 사각형. HUD와 바다가 같은 좌표계(Main의 전체 영역)를 쓰므로 그대로 넘길 수 있다.
|
|
func trash_area() -> Rect2:
|
|
return Rect2(trash.position, trash.size)
|
|
|
|
|
|
## 정보 바가 먹는 세로 자리. 격자는 이 아래에서 시작해야 한다.
|
|
func reserved_top() -> float:
|
|
return control_bar.position.y + control_bar.size.y + BAR_CLEARANCE
|