클리커에서 집·일꾼 방치형 구조로 전환
거품은 이제 클릭이 아니라 바다에 있는 생물이 시간에 따라 번다. 화면 가장자리에
깔아 두는 위젯인데 클릭이 주된 수입원이면 연타가 최적 플레이가 되어 형태와
어긋난다. 클릭 수익과 클릭 업그레이드를 걷어내고 업그레이드는 전부 생산 배율로
바꿨다. 물을 만지면 거품이 퍼지지만 얻는 것은 없다.
격자 칸을 차지하는 것은 집뿐이고, 생물은 칸이 아니라 집에 산다(한 채당 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
This commit is contained in:
+17
-16
@@ -5,6 +5,12 @@ 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
|
||||
@@ -38,6 +44,8 @@ func _ready() -> void:
|
||||
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)
|
||||
@@ -47,7 +55,10 @@ func _ready() -> void:
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
bubbles_label.text = NumberFormat.short(GameState.bubbles)
|
||||
bps_label.text = "초당 %s" % NumberFormat.short(GameState.bps)
|
||||
# 정원이 곧 성장의 한계라 초당 생산량 옆에 늘 같이 둔다.
|
||||
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
|
||||
]
|
||||
@@ -82,21 +93,6 @@ func show_toast(text: String, duration := 2.0) -> void:
|
||||
_toast_tween.tween_property(toast, "modulate:a", 0.0, 0.5)
|
||||
|
||||
|
||||
## 클릭했을 때 얻은 양이 위로 떠오르는 숫자.
|
||||
func float_number(at: Vector2, amount: float) -> void:
|
||||
var l := Label.new()
|
||||
add_child(l)
|
||||
l.text = "+%s" % NumberFormat.short(amount)
|
||||
UIStyle.label(l, 15, UIStyle.ACCENT)
|
||||
l.position = at - Vector2(14, 10)
|
||||
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var t := create_tween().set_parallel(true)
|
||||
t.tween_property(l, "position:y", l.position.y - 42.0, 0.9) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
t.tween_property(l, "modulate:a", 0.0, 0.9)
|
||||
t.chain().tween_callback(l.queue_free)
|
||||
|
||||
|
||||
## 무언가를 집어 든 동안만 휴지통을 보여준다. 평소에는 자리를 차지하지 않는다.
|
||||
func set_trash_visible(on: bool) -> void:
|
||||
trash.visible = on
|
||||
@@ -105,3 +101,8 @@ func set_trash_visible(on: bool) -> void:
|
||||
## 휴지통의 사각형. 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
|
||||
|
||||
+40
-26
@@ -1,18 +1,18 @@
|
||||
extends PanelContainer
|
||||
class_name ShopPanel
|
||||
## 상점. 생물 / 장식 / 업그레이드 세 탭.
|
||||
## 상점. 생물 / 집 / 업그레이드 세 탭.
|
||||
##
|
||||
## 생물과 장식은 여기서 바로 사지지 않는다. 고르면 배치 모드로 들어가고,
|
||||
## 실제 구매는 바다에서 칸을 클릭할 때 일어난다. 업그레이드만 즉시 구매다.
|
||||
## 집만 바다에 자리를 잡아야 하므로, 고르면 배치 모드로 들어가고
|
||||
## 실제 값은 칸을 클릭할 때 치러진다. 생물과 업그레이드는 여기서 바로 사진다.
|
||||
## 목록은 Catalog에서 바로 만들어지므로, 아이템을 추가할 때 이 파일은 건드릴 필요가 없다.
|
||||
|
||||
signal closed
|
||||
## 고른 아이템. kind가 "upgrade"면 즉시 구매, 아니면 배치 모드로 들어간다.
|
||||
## 고른 것. "house"면 배치 모드로 들어가고, 나머지는 즉시 구매다.
|
||||
signal item_selected(kind: String, id: String)
|
||||
|
||||
const TABS := [
|
||||
{"kind": "producer", "label": "생물"},
|
||||
{"kind": "ornament", "label": "장식"},
|
||||
{"kind": "house", "label": "집"},
|
||||
{"kind": "upgrade", "label": "업그레이드"},
|
||||
]
|
||||
|
||||
@@ -54,8 +54,8 @@ func _process(_delta: float) -> void:
|
||||
_update_rows()
|
||||
|
||||
|
||||
## 배치 모드가 켜지고 꺼질 때 main이 알려준다. 고른 행을 강조해 둔다.
|
||||
func set_selected(active: bool, _kind: String, id: String) -> void:
|
||||
## 집 짓기 모드가 켜지고 꺼질 때 main이 알려준다. 고른 행을 강조해 둔다.
|
||||
func set_selected(active: bool, id: String) -> void:
|
||||
_selected_id = id if active else ""
|
||||
_update_rows()
|
||||
|
||||
@@ -77,8 +77,8 @@ func _visible_ids() -> Array[String]:
|
||||
for d in _current_items():
|
||||
if not GameState.is_revealed(kind, d.id):
|
||||
continue
|
||||
# 한 번만 살 수 있는 것은 이미 가지고 있으면 목록에서 빠진다.
|
||||
if kind != "producer" and GameState.owned(kind, d.id):
|
||||
# 업그레이드만 한 번뿐이다. 생물과 집은 얼마든지 더 들일 수 있다.
|
||||
if GameState.owned(kind, String(d.id)):
|
||||
continue
|
||||
ids.append(String(d.id))
|
||||
return ids
|
||||
@@ -192,16 +192,16 @@ func _name_text(kind: String, d: Dictionary) -> String:
|
||||
|
||||
|
||||
func _effect_text(kind: String, d: Dictionary) -> String:
|
||||
var zone_name := String(Catalog.zone(int(d.get("zone", 0))).name)
|
||||
match kind:
|
||||
"producer":
|
||||
return "%s · 초당 +%s · 아름다움 +%s" % [
|
||||
Catalog.zone(int(d.get("zone", 0))).name,
|
||||
NumberFormat.short(d.bps), NumberFormat.short(d.beauty)
|
||||
zone_name, NumberFormat.short(d.bps), NumberFormat.short(d.beauty)
|
||||
]
|
||||
"ornament":
|
||||
return "%s · 아름다움 +%s · 생산 +%d%%" % [
|
||||
Catalog.zone(int(d.get("zone", 0))).name,
|
||||
NumberFormat.short(d.beauty), int(round(float(d.mult) * 100.0))
|
||||
"house":
|
||||
return "%s · 생물 %d마리 · 아름다움 +%s" % [
|
||||
zone_name, Catalog.capacity_of(String(d.id)),
|
||||
NumberFormat.short(d.beauty)
|
||||
]
|
||||
"upgrade":
|
||||
return String(d.desc)
|
||||
@@ -214,28 +214,42 @@ func _update_rows() -> void:
|
||||
var kind: String = r.kind
|
||||
var cost := GameState.cost_of(kind, id)
|
||||
var affordable := GameState.bubbles >= cost
|
||||
# 생물은 값을 치를 수 있어도 살 집이 없으면 못 들인다.
|
||||
var has_room: bool = kind != "producer" or GameState.can_take_fish(String(id))
|
||||
|
||||
r.cost.text = NumberFormat.short(cost)
|
||||
r.cost.modulate = Color.WHITE if affordable else Color(1, 1, 1, 0.45)
|
||||
r.button.disabled = not affordable
|
||||
# 고른 것은 밝게 남겨 두어 지금 무엇을 놓는 중인지 보이게 한다.
|
||||
r.button.disabled = not affordable or not has_room
|
||||
# 고른 것은 밝게 남겨 두어 지금 무엇을 짓는 중인지 보이게 한다.
|
||||
r.button.modulate = Color(1.0, 1.25, 1.35) if id == _selected_id else Color.WHITE
|
||||
|
||||
if kind == "producer":
|
||||
var owned := GameState.count_of(id)
|
||||
r.owned.text = "놓임 %d" % owned if owned > 0 else ""
|
||||
else:
|
||||
r.owned.text = ""
|
||||
var owned := GameState.count_of(kind, String(id))
|
||||
match kind:
|
||||
"producer":
|
||||
if not has_room and affordable:
|
||||
r.owned.text = "자리 없음"
|
||||
else:
|
||||
r.owned.text = "%d마리" % owned if owned > 0 else ""
|
||||
"house":
|
||||
r.owned.text = "%d채" % owned if owned > 0 else ""
|
||||
_:
|
||||
r.owned.text = ""
|
||||
|
||||
_update_space_label()
|
||||
|
||||
|
||||
## 구역별로 빈 칸이 얼마나 남았는지. 칸이 곧 한계이므로 늘 보이게 둔다.
|
||||
## 구역별로 생물 자리가 얼마나 남았는지. 이것이 성장의 진짜 한계다.
|
||||
## 집 탭에서는 집을 세울 빈 칸을 대신 보여준다.
|
||||
func _update_space_label() -> void:
|
||||
if TABS[_tab].kind == "upgrade":
|
||||
space_label.text = "칸을 차지하지 않아요"
|
||||
space_label.text = "자리를 차지하지 않아요"
|
||||
return
|
||||
|
||||
var parts := PackedStringArray()
|
||||
var is_house_tab: bool = TABS[_tab].kind == "house"
|
||||
for i in GameState.unlocked_zones:
|
||||
parts.append("%s %d" % [Catalog.zone(i).name, GameState.free_cells_in_zone(i)])
|
||||
space_label.text = "남은 칸 · " + " ".join(parts)
|
||||
var left := GameState.free_cells_in_zone(i) if is_house_tab \
|
||||
else maxi(GameState.free_slots_in_zone(i), 0)
|
||||
parts.append("%s %d" % [Catalog.zone(i).name, left])
|
||||
var label := "빈 칸 · " if is_house_tab else "남은 자리 · "
|
||||
space_label.text = label + " ".join(parts)
|
||||
|
||||
Reference in New Issue
Block a user