From 1ca381922fc220bafc86a8c7acbc05ce1332094a Mon Sep 17 00:00:00 2001 From: ttipo Date: Wed, 2 Sep 2026 14:16:04 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=A0=EC=B0=A9=EC=83=9D=EB=AC=BC=EC=9D=84?= =?UTF-8?q?=20=EC=A7=91=EC=9C=BC=EB=A1=9C,=20=EC=83=9D=EB=AC=BC=EC=9D=80?= =?UTF-8?q?=20=ED=95=B4=EC=96=91=EB=8F=99=EB=AC=BC=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 거품돌·해초·산호·켈프·관벌레는 물고기가 깃드는 자리지 그 자체가 일꾼이 아니다. 실제로 물고기는 바위 틈과 해초 사이를 보금자리로 삼는다. 전부 집으로 옮기고, 생물에는 움직이는 해양동물만 남겼다. - 집이 자연 서식지(rock/plant)와 지어 올린 것(vessel/wreck/pillar) 두 갈래가 되면서 정원에 폭이 생겼다. 거품돌 1, 해초·산호 2, 켈프·관벌레 4, 지어 올린 것은 표준 3. - 빈 자리를 메우려 게·해마·새우를 넣어 층마다 생물 둘을 맞췄다. 소라게·해마·열수구 새우. 형태도 셋 새로 그렸다. - 형태 계통이 그대로 경계가 된다. 생물 fish/glow/crab/seahorse/shrimp, 집 rock/plant/vessel/wreck/pillar. 겹치지 않는 것을 테스트가 지킨다. - 예전에 생물로 사 둔 돌·해초는 같은 id의 집으로 그만큼 지어 준다. 그냥 버리면 사 둔 것이 조용히 사라진다. 버그 둘 _fish_area/_orbit_center가 생물 자리를 grid_rect(화면 테두리)로 잘랐다. 가로로 이어 스크롤하던 시절에는 화면 = 바다여서 맞았지만, 층이 옆으로 늘어선 지금은 뒤쪽 층 생물이 전부 첫 층 오른쪽 끝으로 끌려와 제 층에서 사라졌다. _home_bounds()가 그 집이 속한 층의 테두리를 돌려준다. _make_fish가 fish와 glow만 궤도에 올려, 새로 넣은 게·해마·새우가 제자리에 굳어 있었다. 고착생물이 전부 집으로 갔으므로 가드를 걷어냈다. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 6 +- autoload/catalog.gd | 101 +++++++++++++++--------- autoload/game_state.gd | 12 ++- docs/DESIGN.md | 33 ++++++++ scenes/aquarium/aquarium.gd | 22 ++++-- scenes/aquarium/tile_item.gd | 124 ++++++++++++++++++++++++++++++ tests/smoke.gd | 145 +++++++++++++++++++++-------------- 7 files changed, 343 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index c36a7a6..a78815a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ | 감상 모드 | **F11** 또는 메뉴 · Esc나 F11로 돌아온다 | | 집 짓기 | 상점 **집** 탭에서 고른 뒤 칸을 클릭 (우클릭·Esc로 취소) | | 생물 들이기 | 상점 **생물** 탭에서 클릭 — 그 자리에서 바로 들어온다 (빈 자리 필요) | + +생물은 물고기·게·해마·새우처럼 **움직이는 해양동물**뿐이다. +돌과 해초·산호·켈프·관벌레는 물고기가 깃드는 **서식지**라서 집 쪽에 있다. | 집 옮기기 | 집을 끌어다 다른 칸에 놓기 (공짜 · 살던 생물도 따라간다) | | 집 헐기 | 집을 끌어서 아래 휴지통에 (절반 환불 · 세입자가 갈 곳이 있어야 한다) | | 생물 내보내기 | 생물을 끌어서 아래 휴지통에 (절반 환불 · 마지막 한 마리는 안 된다) | @@ -64,7 +67,8 @@ tests/smoke.tscn/.gd 경제 로직 스모크 테스트 1. 바다에 있는 **생물**이 시간에 따라 알아서 **거품**을 모은다. 클릭으로 버는 길은 없다. 그래서 새로 시작하면 **항아리 집 한 채와 거품돌·구피 한 마리씩**이 이미 있고, 거품 20을 쥐고 시작한다. -2. 생물은 **집**에 산다. 한 채가 3마리를 들이므로, 더 키우려면 먼저 집을 지어야 한다. +2. 생물은 **집**에 산다. 집에는 두 갈래가 있다 — 돌·해초·산호처럼 **자연 서식지**(싸고 적게 들인다)와 + 항아리·난파선·석주처럼 **지어 올린 것**(비싸고 정원이 넓다). 더 키우려면 먼저 집을 지어야 한다. 집은 칸을 차지하고 거품을 벌지 않는다. 자리와 **아름다움**을 준다. 3. 생물을 들이면 초당 생산량과 아름다움이 오른다. 비싼 생물일수록 한 마리가 더 번다. 4. 아름다움은 다시 전체 생산량에 `1 + sqrt(아름다움) × 0.02` 배율로 되돌아온다. diff --git a/autoload/catalog.gd b/autoload/catalog.gd index dd99baa..8172e6f 100644 --- a/autoload/catalog.gd +++ b/autoload/catalog.gd @@ -54,77 +54,90 @@ const ZONES := [ ## height: 칸 높이의 몇 배로 서 있는지 const PRODUCERS := [ # --- 얕은 바다 --- - { - "id": "bubble_stone", "name": "거품돌", "zone": 0, "shape": "rock", "height": 0.75, - "desc": "바닥에서 조용히 거품을 뿜는 돌.", - "color": Color(0.62, 0.68, 0.72), "base_cost": 15.0, "bps": 0.2, "beauty": 1.0, - }, { "id": "guppy", "name": "구피", "zone": 0, "shape": "fish", "height": 0.8, "desc": "집 둘레를 쉬지 않고 맴돈다.", - "color": Color(0.98, 0.72, 0.32), "base_cost": 40.0, "bps": 0.5, "beauty": 2.0, + "color": Color(0.98, 0.72, 0.32), "base_cost": 15.0, "bps": 0.35, "beauty": 2.0, }, { - "id": "seaweed", "name": "해초", "zone": 0, "shape": "plant", "height": 0.95, - "desc": "물살에 흔들리는 초록 잎.", - "color": Color(0.35, 0.72, 0.42), "base_cost": 110.0, "bps": 1.0, "beauty": 3.0, + "id": "hermit_crab", "name": "소라게", "zone": 0, "shape": "crab", "height": 0.7, + "desc": "빈 껍데기를 지고 바닥을 훑는다.", + "color": Color(0.86, 0.46, 0.34), "base_cost": 130.0, "bps": 1.4, "beauty": 4.0, }, # --- 산호초 --- { "id": "clownfish", "name": "흰동가리", "zone": 1, "shape": "fish", "height": 0.85, - "desc": "제 자리를 벗어나지 않고 맴돈다.", - "color": Color(0.96, 0.55, 0.18), "base_cost": 1200.0, "bps": 8.0, "beauty": 8.0, + "desc": "제 집을 벗어나지 않고 맴돈다.", + "color": Color(0.96, 0.55, 0.18), "base_cost": 1200.0, "bps": 9.0, "beauty": 8.0, }, { - "id": "coral", "name": "산호", "zone": 1, "shape": "plant", "height": 0.85, - "desc": "느리게 자라지만 구역을 화사하게 만든다.", - "color": Color(0.95, 0.45, 0.62), "base_cost": 13000.0, "bps": 47.0, "beauty": 20.0, + "id": "seahorse", "name": "해마", "zone": 1, "shape": "seahorse", "height": 0.95, + "desc": "꼬리를 감고 선 채로 흔들린다.", + "color": Color(0.95, 0.78, 0.36), "base_cost": 14000.0, "bps": 55.0, "beauty": 20.0, }, # --- 켈프 숲 --- { - "id": "kelp", "name": "켈프", "zone": 2, "shape": "plant", "height": 1.35, - "desc": "수면을 향해 길게 뻗는 갈색 잎.", - "color": Color(0.34, 0.42, 0.15), "base_cost": 150000.0, "bps": 260.0, "beauty": 45.0, + "id": "turtle", "name": "바다거북", "zone": 2, "shape": "fish", "height": 0.75, + "desc": "느긋하게 제 집을 돈다.", + "color": Color(0.40, 0.60, 0.36), "base_cost": 170000.0, "bps": 300.0, "beauty": 45.0, }, { - "id": "turtle", "name": "바다거북", "zone": 2, "shape": "fish", "height": 0.75, - "desc": "느긋하게 제 자리를 지킨다.", - "color": Color(0.40, 0.60, 0.36), "base_cost": 2000000.0, "bps": 1400.0, "beauty": 90.0, + "id": "snow_crab", "name": "대게", "zone": 2, "shape": "crab", "height": 0.85, + "desc": "긴 다리로 숲 바닥을 짚고 다닌다.", + "color": Color(0.85, 0.40, 0.30), "base_cost": 2100000.0, "bps": 1500.0, "beauty": 90.0, }, # --- 심해 --- { "id": "jellyfish", "name": "해파리", "zone": 3, "shape": "glow", "height": 1.1, "desc": "은은하게 빛나며 떠다닌다.", - "color": Color(0.72, 0.60, 0.95), "base_cost": 26000000.0, "bps": 7800.0, "beauty": 170.0, + "color": Color(0.72, 0.60, 0.95), "base_cost": 26000000.0, "bps": 8200.0, "beauty": 170.0, }, { "id": "squid", "name": "대왕오징어", "zone": 3, "shape": "fish", "height": 0.95, "desc": "어둠 속에서 잠깐 모습을 드러낸다.", - "color": Color(0.62, 0.32, 0.42), "base_cost": 400000000.0, "bps": 44000.0, "beauty": 320.0, + "color": Color(0.62, 0.32, 0.42), "base_cost": 400000000.0, "bps": 46000.0, "beauty": 320.0, }, # --- 열수구 --- { - "id": "tubeworm", "name": "관벌레", "zone": 4, "shape": "plant", "height": 1.1, - "desc": "햇빛 없이 열기만으로 살아간다.", - "color": Color(0.92, 0.28, 0.30), "base_cost": 7e9, "bps": 260000.0, "beauty": 600.0, + "id": "vent_shrimp", "name": "열수구 새우", "zone": 4, "shape": "shrimp", "height": 0.7, + "desc": "끓는 물 언저리에 떼로 붙어 산다.", + "color": Color(0.94, 0.62, 0.52), "base_cost": 7e9, "bps": 275000.0, "beauty": 600.0, }, { "id": "anglerfish", "name": "심해 아귀", "zone": 4, "shape": "glow", "height": 1.0, "desc": "칠흑 속에서 등불을 흔든다.", - "color": Color(0.95, 0.85, 0.45), "base_cost": 1.2e11, "bps": 1600000.0, "beauty": 1100.0, + "color": Color(0.95, 0.85, 0.45), "base_cost": 1.2e11, "bps": 1700000.0, "beauty": 1100.0, }, ] + ## 집 — 생물이 사는 곳. 격자 칸을 차지하는 것은 이쪽뿐이다. -## 한 채당 HOUSE_CAPACITY마리를 들이고, 같은 종류라도 몇 채든 지을 수 있다. +## 자연 서식지(돌·해초·산호·켈프·관벌레)와 지어 올린 것(항아리·난파선·석주…)이 함께 들어간다. +## 물고기가 바위 틈이나 해초 사이를 보금자리로 삼는 것을 그대로 옮겼다. +## 자연 서식지는 싸지만 적게 들이고, 지어 올린 것은 비싼 대신 정원이 넓다. +## +## 한 채당 capacity마리를 들이고, 같은 종류라도 몇 채든 지을 수 있다. ## 지을수록 값이 오르는 것은 생물과 같다(COST_GROWTH). ## 집 자체는 거품을 벌지 않는다. 자리와 아름다움을 준다. -## size : 바닥에서 차지하는 칸 수 (열, 행). 넓은 집일수록 아름다움이 크다. -## shape: vessel(항아리·병) / wreck(난파선·뼈대) / pillar(기둥·석상). -## 집은 이 셋만 쓴다. 생물의 형태(plant/rock/fish/glow)와 섞지 않아야 -## 한눈에 "건물"과 "사는 것"이 갈린다. +## size : 바닥에서 차지하는 칸 수 (열, 행). 넓은 집일수록 아름다움이 크다. +## capacity: 들일 수 있는 생물 수. 없으면 HOUSE_CAPACITY. +## shape : rock/plant(자연) · vessel/wreck/pillar(지은 것). +## 생물의 형태(fish/glow/crab/seahorse/shrimp)와 섞지 않아야 +## 한눈에 "사는 곳"과 "사는 것"이 갈린다. const HOUSES := [ # --- 얕은 바다 --- + { + "id": "bubble_stone", "name": "거품돌", "zone": 0, "shape": "rock", + "size": Vector2i(1, 1), "height": 0.75, "capacity": 1, + "desc": "틈이 하나뿐이라 한 마리만 깃든다. 가장 싼 자리.", + "color": Color(0.62, 0.68, 0.72), "base_cost": 15.0, "beauty": 4.0, + }, + { + "id": "seaweed", "name": "해초", "zone": 0, "shape": "plant", + "size": Vector2i(1, 1), "height": 0.95, "capacity": 2, + "desc": "잎 사이에 몸을 숨긴다.", + "color": Color(0.35, 0.72, 0.42), "base_cost": 120.0, "beauty": 9.0, + }, { "id": "amphora", "name": "침몰한 항아리", "zone": 0, "shape": "vessel", "size": Vector2i(2, 1), "height": 1.2, @@ -138,6 +151,12 @@ const HOUSES := [ "color": Color(0.78, 0.92, 0.72), "base_cost": 1200.0, "beauty": 20.0, }, # --- 산호초 --- + { + "id": "coral", "name": "산호", "zone": 1, "shape": "plant", + "size": Vector2i(1, 1), "height": 0.85, "capacity": 2, + "desc": "가지 사이가 곧 물고기의 골목이다.", + "color": Color(0.95, 0.45, 0.62), "base_cost": 9000.0, "beauty": 26.0, + }, { "id": "anchor", "name": "낡은 닻", "zone": 1, "shape": "wreck", "size": Vector2i(2, 1), "height": 1.15, @@ -151,6 +170,12 @@ const HOUSES := [ "color": Color(0.42, 0.32, 0.24), "base_cost": 60000.0, "beauty": 110.0, }, # --- 켈프 숲 --- + { + "id": "kelp", "name": "켈프", "zone": 2, "shape": "plant", + "size": Vector2i(1, 2), "height": 1.35, "capacity": 4, + "desc": "수면까지 뻗은 줄기 사이에 여럿이 머문다.", + "color": Color(0.34, 0.42, 0.15), "base_cost": 900000.0, "beauty": 120.0, + }, { "id": "submarine", "name": "침몰한 잠수정", "zone": 2, "shape": "wreck", "size": Vector2i(3, 2), "height": 1.7, @@ -177,6 +202,12 @@ const HOUSES := [ "color": Color(0.55, 0.54, 0.50), "base_cost": 1.2e9, "beauty": 1900.0, }, # --- 열수구 --- + { + "id": "tubeworm", "name": "관벌레", "zone": 4, "shape": "plant", + "size": Vector2i(1, 1), "height": 1.1, "capacity": 4, + "desc": "빽빽한 관 다발이 통째로 보금자리가 된다.", + "color": Color(0.92, 0.28, 0.30), "base_cost": 3.5e10, "beauty": 1600.0, + }, { "id": "vent_chimney", "name": "열수 굴뚝", "zone": 4, "shape": "pillar", "size": Vector2i(2, 2), "height": 1.95, @@ -221,11 +252,11 @@ const STARTER_HOUSES := [ ## 새로 시작할 때 이미 살고 있는 생물. id -> 마리 수. ## 거품은 생물만 벌기 때문에 최소 한 마리는 처음부터 있어야 한다. -## 바닥에 붙는 것(거품돌)과 헤엄치는 것(구피)을 하나씩 두어, -## 켜자마자 "제자리에 서는 것"과 "집을 도는 것"이 둘 다 보이게 했다. -const STARTER_FISH := {"bubble_stone": 1, "guppy": 1} +## 헤엄치는 것(구피)과 바닥을 걷는 것(소라게)을 하나씩 두어, +## 켜자마자 두 가지 움직임이 다 보이게 했다. +const STARTER_FISH := {"guppy": 1, "hermit_crab": 1} -## 시작 거품. 두 번째 거품돌 값(ceil(15 × 1.15) = 18)보다 조금 넉넉하게 두어, +## 시작 거품. 두 번째 구피 값(ceil(15 × 1.15) = 18)보다 조금 넉넉하게 두어, ## 시작하자마자 남은 한 자리를 직접 채워보며 구조를 익히게 한다. const STARTER_BUBBLES := 20.0 diff --git a/autoload/game_state.gd b/autoload/game_state.gd index b79aaf8..6e3cdac 100644 --- a/autoload/game_state.gd +++ b/autoload/game_state.gd @@ -576,7 +576,17 @@ func _restore_house(id: String, col: int, row: int) -> void: ## 정원을 넘겨도 이미 살던 생물을 내쫓지는 않는다. 자리가 빌 때까지 더 못 들일 뿐이다. func _restore_fish(id: String, count: int) -> void: - if Catalog.producer(id).is_empty() or count <= 0: + if count <= 0: + return + # 돌·해초처럼 예전에 생물이었다가 집이 된 것은 그만큼 집으로 지어 준다. + # 그냥 버리면 플레이어가 사 둔 것이 조용히 사라진다. + if Catalog.producer(id).is_empty(): + if not Catalog.house(id).is_empty(): + for i in count: + var spot := first_free_spot(id) + if spot.x < 0: + break + _restore_house(id, spot.x, spot.y) return fish[id] = fish_count(id) + count recalc() diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 19af498..64f2c32 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -384,3 +384,36 @@ F11이나 메뉴로 창을 화면 전체로 키운다. 그냥 창만 키우면 함께 사라진 것: 가로 패닝, 구역 구분선(한 화면에 둘이 안 보이므로 무의미), `window_state["scroll_x"]`(→ `["layer"]`). + +## 사는 것과 사는 곳 + +처음에는 거품돌·해초·산호·켈프·관벌레가 **생물** 쪽에 있었다. 하지만 이것들은 +물고기가 깃드는 자리지 그 자체가 일꾼은 아니다. 실제로 물고기는 바위 틈과 +해초 사이를 보금자리로 삼는다. 그래서 전부 **집**으로 옮겼다. + +- **생물** — 물고기·게·해마·새우처럼 움직이는 해양동물만. 거품을 버는 것은 이쪽뿐이다. +- **집** — 자연 서식지(rock/plant)와 지어 올린 것(vessel/wreck/pillar). + 칸을 차지하고 정원과 아름다움을 준다. + +집이 두 갈래가 되면서 정원에 폭이 생겼다. `capacity`를 아이템별로 둘 수 있게 해서 +자연 서식지는 싸지만 적게 들이고(거품돌 1, 해초 2, 산호 2), 지어 올린 것은 +비싼 대신 표준 정원(3)을 준다. 켈프와 관벌레는 4까지 들인다. + +**형태 계통이 그대로 경계가 된다.** 생물은 `fish/glow/crab/seahorse/shrimp`, +집은 `rock/plant/vessel/wreck/pillar`. 두 집합이 겹치지 않는 것을 테스트가 지킨다. +겹치면 화면에서 "사는 것"과 "사는 곳"이 구분되지 않는다. + +**옛 저장본**: 예전에 생물로 사 둔 돌·해초는 `_restore_fish()`가 같은 id의 집으로 +그만큼 지어 준다. 그냥 버리면 플레이어가 사 둔 것이 조용히 사라진다. + +### 이때 드러난 버그 + +생물의 자리를 `grid_rect()`(화면 테두리)로 잘라내고 있었다. 가로로 이어 스크롤하던 +시절에는 화면 = 바다여서 맞았지만, 층이 옆으로 늘어선 지금은 **뒤쪽 층의 생물이 +전부 첫 층 오른쪽 끝으로 끌려와** 제 층에서 사라졌다. 집은 멀쩡히 보이는데 +생물만 없어서 한참 헤맸다. `_home_bounds()`가 그 집이 속한 층의 테두리를 돌려주고, +자를 때 그것을 쓴다. + +또 `_make_fish()`가 `fish`와 `glow`만 궤도에 올리고 있었다. 고착생물이 생물 쪽에 +있던 시절의 가드인데, 새로 넣은 게·해마·새우가 여기 걸려 제자리에 굳어 있었다. +고착생물이 전부 집으로 갔으므로 가드 자체를 걷어냈다. diff --git a/scenes/aquarium/aquarium.gd b/scenes/aquarium/aquarium.gd index 217a619..6dc3f5c 100644 --- a/scenes/aquarium/aquarium.gd +++ b/scenes/aquarium/aquarium.gd @@ -463,10 +463,8 @@ func _make_fish(entry: Dictionary) -> void: items.add_child(node) node.setup(d, "producer", -1, _fish_area(home, cell, slot), cell, _rng) - # 헤엄치는 것만 집 둘레를 돈다. 해초나 거품돌은 제자리에 붙어 있어야 한다. - var shape := String(d.get("shape", "rock")) - if shape != "fish" and shape != "glow": - return + # 생물은 모두 집 둘레를 돈다. 고착생물(해초·거품돌)은 이제 집 쪽으로 옮겼으므로 + # 여기 들어오는 것은 전부 헤엄치거나 걷는 해양동물이다. var house_index := int(entry.house) var cap: int = maxi(Catalog.capacity_of(String(GameState.houses[house_index].id)), 1) # 집마다 도는 방향을 바꿔 여러 채가 한 화면에 있어도 단조롭지 않게 한다. @@ -525,13 +523,23 @@ func _orbit_radius(home: Rect2, cell: Vector2) -> Vector2: ## 중심을 옮기지 않고 매 프레임 위치만 자르면 물고기가 벽에 붙어 멈춘 것처럼 보인다. func _orbit_center(home: Rect2, cell: Vector2) -> Vector2: var r := _orbit_radius(home, cell) - var g := grid_rect() + var g := _home_bounds(home, cell) return Vector2( _squeeze(home.position.x + home.size.x * 0.5, g.position.x + r.x, g.end.x - r.x), _squeeze(home.end.y - cell.y * 0.10, g.position.y + cell.y * 0.9 + r.y, g.end.y - r.y)) +## 그 집이 속한 층의 테두리. 생물을 여기 안에 가둔다. +## 예전에는 화면(grid_rect)에 맞춰 잘랐는데, 층이 옆으로 늘어선 지금 그렇게 하면 +## 뒤쪽 층의 생물이 전부 첫 층 오른쪽 끝으로 끌려와 제 층에서 사라진다. +func _home_bounds(home: Rect2, cell: Vector2) -> Rect2: + var z := Catalog.zone_of_column(int(floor(home.position.x / maxf(cell.x, 1.0)))) + var g := grid_rect() + var width := Catalog.ZONE_COLUMNS * cell.x + return Rect2(Vector2(z * width, g.position.y), Vector2(width, g.size.y)) + + ## 범위가 뒤집힐 만큼 좁으면(작은 창) 가운데로 보낸다. clampf는 그때 엉뚱한 값을 준다. func _squeeze(value: float, lo: float, hi: float) -> float: if lo > hi: @@ -552,8 +560,8 @@ func _fish_area(home: Rect2, cell: Vector2, slot: int) -> Rect2: var x := cx + spot.x * (home.size.x + cell.x * 0.9) + ring * cell.x * 0.28 var base := home.end.y + spot.y * cell.y + ring * cell.y * 0.22 - # 바다 밖으로 새어 나가지 않게 잡아둔다. - var g := grid_rect() + # 제 층 밖으로 새어 나가지 않게 잡아둔다. + var g := _home_bounds(home, cell) x = clampf(x, g.position.x + cell.x * 0.5, g.end.x - cell.x * 0.5) base = clampf(base, g.position.y + cell.y * 0.6, g.end.y) return Rect2(Vector2(x - cell.x * 0.5, base - cell.y), cell) diff --git a/scenes/aquarium/tile_item.gd b/scenes/aquarium/tile_item.gd index be18c5e..23908a1 100644 --- a/scenes/aquarium/tile_item.gd +++ b/scenes/aquarium/tile_item.gd @@ -142,6 +142,9 @@ func _draw() -> void: "plant": _draw_plant() "fish": _draw_fish() "glow": _draw_glow() + "crab": _draw_crab() + "seahorse": _draw_seahorse() + "shrimp": _draw_shrimp() "vessel": _draw_vessel() "wreck": _draw_wreck() "pillar": _draw_pillar() @@ -416,3 +419,124 @@ func _draw_pillar() -> void: draw_set_transform(Vector2(0, -h), 0.0, Vector2(1.0, 0.34)) draw_circle(Vector2.ZERO, w * 1.02, color.lightened(0.18)) draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +## 바닥을 걷는 게. 옆으로 벌어진 몸에 다리가 뻗고 집게가 앞을 짚는다. +## 물고기와 실루엣이 확실히 달라야 한눈에 갈린다. +func _draw_crab() -> void: + var r := span.x * 0.26 + var step := sin(_t * 3.0 + _phase) + draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + + # 다리. 좌우 세 쌍이 번갈아 짚는다. + for side: float in [-1.0, 1.0]: + for i in 3: + var a := -0.35 + i * 0.42 + var lift := 0.12 if i % 2 == 0 else -0.12 + var swing := step * lift * side + var knee := Vector2(cos(a + swing) * r * 1.05, side * r * (0.55 + i * 0.06)) + var foot := knee + Vector2(cos(a + swing) * r * 0.5, side * r * 0.55) + draw_polyline(PackedVector2Array([Vector2(0, side * r * 0.25), knee, foot]), + color.darkened(0.34), maxf(r * 0.14, 1.2), true) + + # 집게 둘. + for side: float in [-1.0, 1.0]: + var base := Vector2(r * 0.85, side * r * 0.5) + var tip := base + Vector2(r * 0.62, side * r * 0.18 + step * r * 0.1) + draw_polyline(PackedVector2Array([Vector2(r * 0.3, side * r * 0.3), base, tip]), + color.darkened(0.2), maxf(r * 0.18, 1.4), true) + _fill(PackedVector2Array([ + tip + Vector2(-r * 0.1, -r * 0.22), + tip + Vector2(r * 0.34, -r * 0.05), + tip + Vector2(-r * 0.05, r * 0.22), + ]), color.lightened(0.1)) + + # 등딱지. 가로로 넓은 타원. + draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing * 1.35, 0.92)) + draw_circle(Vector2.ZERO, r, color) + draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + draw_circle(Vector2(-r * 0.1, -r * 0.22), r * 0.55, color.lightened(0.16)) + for side: float in [-1.0, 1.0]: + draw_circle(Vector2(r * 0.5, side * r * 0.34), maxf(r * 0.12, 1.2), + Color(0.06, 0.07, 0.09)) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +## 선 채로 흔들리는 해마. 세로로 긴 S자가 곧 실루엣이다. +func _draw_seahorse() -> void: + var h := body_height * 0.9 + var w := span.x * 0.16 + var sway := sin(_t * 1.3 + _phase) * 0.22 + draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + + # 몸통. 아래로 갈수록 가늘어지며 꼬리가 안으로 말린다. + var left := PackedVector2Array() + var right := PackedVector2Array() + var steps := 8 + for i in steps + 1: + var f := float(i) / steps + var bend := sin(f * PI * 1.15 + sway) * w * 1.5 + var p := Vector2(bend, -h * f) + var half := w * (0.45 + sin(f * PI) * 0.75) + left.append(p + Vector2(half, 0)) + right.append(p - Vector2(half, 0)) + right.reverse() + _fill(left + right, color) + + # 등지느러미와 주둥이, 눈. + var head := Vector2(sin(PI * 1.15 + sway) * w * 1.5, -h) + _fill(PackedVector2Array([ + head + Vector2(w * 0.4, w * 0.2), + head + Vector2(w * 2.1, w * 0.05), + head + Vector2(w * 0.5, -w * 0.5), + ]), color.darkened(0.15)) + draw_circle(head + Vector2(w * 0.35, -w * 0.15), maxf(w * 0.42, 1.2), + Color(0.06, 0.07, 0.09)) + for i in 3: + var f := 0.25 + i * 0.2 + var bend := sin(f * PI * 1.15 + sway) * w * 1.5 + _fill(PackedVector2Array([ + Vector2(bend - w * 0.8, -h * f), + Vector2(bend - w * 2.0, -h * (f + 0.06)), + Vector2(bend - w * 0.8, -h * (f + 0.12)), + ]), color.darkened(0.3)) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +## 새우. 등이 굽고 더듬이가 길다. 꼬리를 튕겨 조금씩 물러난다. +func _draw_shrimp() -> void: + var l := span.x * 0.3 + var flick := sin(_t * 4.5 + _phase) + draw_set_transform(Vector2.ZERO, 0.0, Vector2(_facing, 1.0)) + + # 더듬이. 실루엣의 절반을 차지한다. + for side: float in [-0.35, 0.15]: + draw_polyline(PackedVector2Array([ + Vector2(l * 0.6, side * l), + Vector2(l * 1.3, side * l - l * 0.25 + flick * l * 0.06), + Vector2(l * 1.9, side * l - l * 0.55 + flick * l * 0.12), + ]), Color(color.r, color.g, color.b, 0.7), maxf(l * 0.07, 1.0), true) + + # 굽은 몸통. + var body := PackedVector2Array() + var steps := 7 + for i in steps + 1: + var f := float(i) / steps + var a := PI * 0.15 + f * PI * 0.75 + body.append(Vector2(cos(a) * l * 0.95, -sin(a) * l * 0.5 + l * 0.1)) + for i in range(steps, -1, -1): + var f := float(i) / steps + var a := PI * 0.15 + f * PI * 0.75 + body.append(Vector2(cos(a) * l * 0.62, -sin(a) * l * 0.16 + l * 0.1)) + _fill(body, color) + + # 부채꼴 꼬리. + var tail := Vector2(-l * 0.9, l * 0.05 + flick * l * 0.12) + _fill(PackedVector2Array([ + Vector2(-l * 0.6, l * 0.08), + tail + Vector2(-l * 0.4, -l * 0.3), + tail + Vector2(-l * 0.5, l * 0.05), + tail + Vector2(-l * 0.35, l * 0.35), + ]), color.darkened(0.25)) + draw_circle(Vector2(l * 0.68, -l * 0.12), maxf(l * 0.1, 1.2), Color(0.06, 0.07, 0.09)) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) diff --git a/tests/smoke.gd b/tests/smoke.gd index 661811a..cae260b 100644 --- a/tests/smoke.gd +++ b/tests/smoke.gd @@ -58,9 +58,15 @@ func _rich_and_open() -> void: ## 그 구역에 집을 한 채 세워 자리를 만든다. 세운 집의 인덱스를 돌려준다. +## 정원이 표준(HOUSE_CAPACITY)인 집을 고른다 -- 거품돌처럼 정원이 다른 집을 +## 기준으로 삼으면 자리 계산 검사가 통째로 흔들린다. func _house_in(zone_index: int, id := "") -> int: if id.is_empty(): - id = Catalog.first_id_of_zone("house", zone_index) + for d in Catalog.HOUSES: + var same_zone := int(d.get("zone", 0)) == zone_index + if same_zone and Catalog.capacity_of(String(d.id)) == Catalog.HOUSE_CAPACITY: + id = String(d.id) + break var spot := GameState.first_free_spot(id) return GameState.build_house(id, spot.x, spot.y) @@ -82,8 +88,8 @@ func _test_starting_state() -> void: check("정원이 생물보다 넉넉하다", GameState.free_slots_in_zone(0) > 0, "%d / %d" % [GameState.fish_total, GameState.capacity_total()]) check("시작 거품으로 한 마리를 더 들일 수 있다", - GameState.bubbles >= GameState.cost_of("producer", "bubble_stone") - and GameState.can_take_fish("bubble_stone")) + GameState.bubbles >= GameState.cost_of("producer", "guppy") + and GameState.can_take_fish("guppy")) var s: Dictionary = Catalog.STARTER_HOUSES[0] check("집이 정해 둔 칸에 선다", @@ -100,11 +106,11 @@ func _test_auto_income() -> void: check("집만으로는 거품이 나지 않는다", is_equal_approx(GameState.bps, 0.0), str(GameState.bps)) - GameState.buy_fish("seaweed") + GameState.buy_fish("hermit_crab") check("생물이 들어와야 거품이 난다", GameState.bps > 0.0, str(GameState.bps)) var before_bps := GameState.bps - GameState.buy_fish("seaweed") + GameState.buy_fish("hermit_crab") check("생물이 늘면 초당 생산도 는다", GameState.bps > before_bps, "%f -> %f" % [before_bps, GameState.bps]) @@ -124,9 +130,9 @@ func _test_auto_income() -> void: func _test_cost_curves() -> void: print("가격 곡선") _rich_and_open() - check("첫 거품돌은 15", - is_equal_approx(GameState.cost_of("producer", "bubble_stone"), 15.0), - str(GameState.cost_of("producer", "bubble_stone"))) + check("첫 구피는 15", + is_equal_approx(GameState.cost_of("producer", "guppy"), 15.0), + str(GameState.cost_of("producer", "guppy"))) check("첫 항아리는 90", is_equal_approx(GameState.cost_of("house", "amphora"), 90.0), str(GameState.cost_of("house", "amphora"))) @@ -136,8 +142,8 @@ func _test_cost_curves() -> void: check("집도 지을수록 비싸진다 (x1.15)", is_equal_approx(house_second, ceil(90.0 * 1.15)), str(house_second)) - GameState.buy_fish("bubble_stone") - var fish_second := GameState.cost_of("producer", "bubble_stone") + GameState.buy_fish("guppy") + var fish_second := GameState.cost_of("producer", "guppy") check("생물도 들일수록 비싸진다 (x1.15)", is_equal_approx(fish_second, ceil(15.0 * 1.15)), str(fish_second)) check("서로 값을 간섭하지 않는다", @@ -207,10 +213,10 @@ func _test_zone_boundaries() -> void: func _test_capacity() -> void: print("정원") _rich_and_open() - check("집이 없으면 한 마리도 못 들인다", not GameState.can_take_fish("bubble_stone")) - check("사유를 알려준다", GameState.fish_blocker("bubble_stone").contains("빈 자리"), - GameState.fish_blocker("bubble_stone")) - check("거품이 넘쳐도 못 들인다", not GameState.buy_fish("bubble_stone")) + check("집이 없으면 한 마리도 못 들인다", not GameState.can_take_fish("guppy")) + check("사유를 알려준다", GameState.fish_blocker("guppy").contains("빈 자리"), + GameState.fish_blocker("guppy")) + check("거품이 넘쳐도 못 들인다", not GameState.buy_fish("guppy")) _house_in(0) check("집을 지으면 정원이 생긴다", @@ -218,14 +224,14 @@ func _test_capacity() -> void: str(GameState.free_slots_in_zone(0))) for i in Catalog.HOUSE_CAPACITY: - check("%d번째 자리가 찬다" % (i + 1), GameState.buy_fish("bubble_stone")) + check("%d번째 자리가 찬다" % (i + 1), GameState.buy_fish("guppy")) check("정원이 다 찼다", GameState.free_slots_in_zone(0) == 0, str(GameState.free_slots_in_zone(0))) - check("정원이 차면 더 못 들인다", not GameState.buy_fish("bubble_stone")) - check("종류를 바꿔도 마찬가지", not GameState.buy_fish("seaweed")) + check("정원이 차면 더 못 들인다", not GameState.buy_fish("guppy")) + check("종류를 바꿔도 마찬가지", not GameState.buy_fish("hermit_crab")) _house_in(0) - check("집을 더 지으면 다시 들일 수 있다", GameState.buy_fish("seaweed")) + check("집을 더 지으면 다시 들일 수 있다", GameState.buy_fish("hermit_crab")) check("정원은 집 수 x %d" % Catalog.HOUSE_CAPACITY, GameState.capacity_in_zone(0) == 2 * Catalog.HOUSE_CAPACITY, str(GameState.capacity_in_zone(0))) @@ -253,7 +259,7 @@ func _test_move_house() -> void: check("실패했으면 제자리", GameState.house_at(4, 2) == index) check("제자리에 그대로 놓는 건 된다", GameState.move_house(index, 4, 2)) - GameState.buy_fish("bubble_stone") + GameState.buy_fish("guppy") check("살던 생물은 그대로 따라간다", GameState.move_house(index, 6, 4) and GameState.fish_total == 1) @@ -279,7 +285,7 @@ func _test_remove_house() -> void: # 살고 있는 생물이 갈 곳이 없으면 헐 수 없다. for i in Catalog.HOUSE_CAPACITY: - GameState.buy_fish("bubble_stone") + GameState.buy_fish("guppy") check("세입자가 꽉 찬 마지막 집은 못 헌다", not GameState.can_remove_house(0)) check("헐려고 해도 아무것도 돌아오지 않는다", is_equal_approx(GameState.remove_house(0), 0.0)) @@ -296,31 +302,31 @@ func _test_release_fish() -> void: print("생물 내보내기") _rich_and_open() _house_in(0) - GameState.buy_fish("seaweed") - var second_cost := GameState.cost_of("producer", "seaweed") - GameState.buy_fish("seaweed") + GameState.buy_fish("hermit_crab") + var second_cost := GameState.cost_of("producer", "hermit_crab") + GameState.buy_fish("hermit_crab") var before := GameState.bubbles var expected: float = floor(second_cost * Catalog.REFUND_RATE) - var refund := GameState.release_fish("seaweed") + var refund := GameState.release_fish("hermit_crab") check("절반을 돌려받는다", is_equal_approx(refund, expected), "%f vs %f" % [refund, expected]) check("잔액에 더해진다", is_equal_approx(GameState.bubbles, before + expected)) - check("마릿수가 줄었다", GameState.fish_count("seaweed") == 1) + check("마릿수가 줄었다", GameState.fish_count("hermit_crab") == 1) check("자리가 다시 빈다", GameState.free_slots_in_zone(0) == Catalog.HOUSE_CAPACITY - 1, str(GameState.free_slots_in_zone(0))) - check("마지막 한 마리는 못 내보낸다", not GameState.can_release_fish("seaweed")) + check("마지막 한 마리는 못 내보낸다", not GameState.can_release_fish("hermit_crab")) check("내보내려 해도 아무것도 돌아오지 않다", - is_equal_approx(GameState.release_fish("seaweed"), 0.0)) + is_equal_approx(GameState.release_fish("hermit_crab"), 0.0)) check("그대로 남는다", GameState.fish_total == 1, str(GameState.fish_total)) - GameState.buy_fish("bubble_stone") - check("둘이 되면 내보낼 수 있다", GameState.can_release_fish("seaweed")) - check("실제로 나간다", GameState.release_fish("seaweed") > 0.0) + GameState.buy_fish("guppy") + check("둘이 되면 내보낼 수 있다", GameState.can_release_fish("hermit_crab")) + check("실제로 나간다", GameState.release_fish("hermit_crab") > 0.0) check("한 마리가 남는다", GameState.fish_total == 1, str(GameState.fish_total)) - check("없는 종류는 못 내보낸다", not GameState.can_release_fish("seaweed")) + check("없는 종류는 못 내보낸다", not GameState.can_release_fish("hermit_crab")) func _test_space_is_the_limit() -> void: @@ -356,8 +362,9 @@ func _test_zone_gating() -> void: GameState.reset(false) GameState.bubbles = 1e15 check("시작은 1구역만 열림", GameState.unlocked_zones == 1) - check("0구역 물건은 보인다", GameState.is_revealed("producer", "bubble_stone") - and GameState.is_revealed("house", "amphora")) + # 각 구역의 첫 항목은 값을 벌기 전에도 보여야 한다. 안 그러면 상점이 비어 시작한다. + check("0구역 물건은 보인다", GameState.is_revealed("producer", "guppy") + and GameState.is_revealed("house", "bubble_stone")) check("1구역 물건은 안 보인다", not GameState.is_revealed("producer", "clownfish") and not GameState.is_revealed("house", "anchor")) check("돈이 넘쳐도 잠긴 구역에는 못 짓는다", @@ -365,7 +372,7 @@ func _test_zone_gating() -> void: check("실패했으면 잔액 그대로", is_equal_approx(GameState.bubbles, 1e15)) GameState.unlocked_zones = 2 - check("열린 뒤에는 보인다", GameState.is_revealed("house", "anchor")) + check("열린 뒤에는 보인다", GameState.is_revealed("house", "coral")) check("열린 뒤에는 지어진다", GameState.build_house("anchor", Catalog.ZONE_COLUMNS, 0) >= 0) @@ -389,7 +396,7 @@ func _test_zone_unlock() -> void: var spot := GameState.first_free_spot("amphora") if spot.x < 0 or GameState.build_house("amphora", spot.x, spot.y) < 0: break - elif not GameState.buy_fish("seaweed"): + elif not GameState.buy_fish("hermit_crab"): break check("칸 안에서 조건을 채울 수 있다", GameState.beauty_in_zone(0) >= need_beauty, "%f / %f" % [GameState.beauty_in_zone(0), need_beauty]) @@ -405,15 +412,15 @@ func _test_zone_beauty_is_per_zone() -> void: print("구역별 아름다움") _rich_and_open() _house_in(0) # 항아리, 아름다움 12 - GameState.buy_fish("seaweed") # 0구역, 아름다움 3 + GameState.buy_fish("hermit_crab") # 0구역, 아름다움 4 _house_in(1) # 낡은 닻, 아름다움 40 GameState.buy_fish("clownfish") # 1구역, 아름다움 8 - check("0구역에 15", is_equal_approx(GameState.beauty_in_zone(0), 15.0), + check("0구역에 16", is_equal_approx(GameState.beauty_in_zone(0), 16.0), str(GameState.beauty_in_zone(0))) check("1구역에 48", is_equal_approx(GameState.beauty_in_zone(1), 48.0), str(GameState.beauty_in_zone(1))) - check("총합은 63", is_equal_approx(GameState.beauty, 63.0), str(GameState.beauty)) + check("총합은 64", is_equal_approx(GameState.beauty, 64.0), str(GameState.beauty)) check("건드리지 않은 구역은 0", is_equal_approx(GameState.beauty_in_zone(4), 0.0)) @@ -456,8 +463,8 @@ func _test_serialization_round_trip() -> void: _rich_and_open() GameState.build_house("amphora", 1, 1) GameState.build_house("shipwreck", Catalog.ZONE_COLUMNS + 1, 2) - GameState.buy_fish("seaweed") - GameState.buy_fish("seaweed") + GameState.buy_fish("hermit_crab") + GameState.buy_fish("hermit_crab") GameState.buy_fish("clownfish") GameState.upgrades["u_ripple"] = true # 배치가 끝난 뒤에 잔액을 맞춘다. 짓고 들이는 데 값이 들기 때문이다. @@ -477,7 +484,7 @@ func _test_serialization_round_trip() -> void: GameState.house_at(1, 1) >= 0 and GameState.house_at(wreck, 2) >= 0) check("난파선 발자국도 복원", GameState.house_at(wreck + 2, 3) == GameState.house_at(wreck, 2)) - check("생물 마릿수 복원", GameState.fish_count("seaweed") == 2 + check("생물 마릿수 복원", GameState.fish_count("hermit_crab") == 2 and GameState.fish_count("clownfish") == 1) check("정원도 그대로", GameState.capacity_in_zone(0) == Catalog.HOUSE_CAPACITY, str(GameState.capacity_in_zone(0))) @@ -491,6 +498,7 @@ func _test_serialization_round_trip() -> void: func _test_legacy_save_migration() -> void: print("옛 저장본 이전") # 생물도 칸을 차지하던 시절. 장식이 지금의 집이 되고, 생물은 마릿수만 남는다. + # 돌·해초처럼 그때는 생물이었다가 지금은 집이 된 것은 그만큼 집으로 지어 준다. GameState.reset(false) GameState.from_dict({ "bubbles": 500.0, @@ -503,26 +511,32 @@ func _test_legacy_save_migration() -> void: {"kind": "ornament", "id": "anchor", "col": Catalog.ZONE_COLUMNS + 2, "row": 0}, ], }) - check("장식이 집이 된다", GameState.houses.size() == 2, str(GameState.houses.size())) + # 항아리 + 닻 + 해초 두 포기 = 네 채. + check("장식이 집이 된다", GameState.houses.size() == 4, str(GameState.houses.size())) check("집이 선 칸은 그대로", GameState.house_at(3, 0) >= 0) - check("생물은 마릿수로 남는다", GameState.fish_count("seaweed") == 2, - str(GameState.fish_count("seaweed"))) - check("생물은 칸을 놓아준다", GameState.house_at(0, 0) == -1) - check("각자 제 구역에 남는다", GameState.fish_in_zone(0) == 2 - and GameState.fish_in_zone(1) == 1) + check("해초는 집으로 넘어온다", GameState.house_count("seaweed") == 2, + str(GameState.house_count("seaweed"))) + check("해초는 더 이상 생물이 아니다", GameState.fish_count("seaweed") == 0) + check("헤엄치던 것은 마릿수로 남는다", GameState.fish_count("clownfish") == 1, + str(GameState.fish_count("clownfish"))) + check("생물은 칸을 놓아준다", GameState.fish_total == 1, str(GameState.fish_total)) + check("각자 제 구역에 남는다", GameState.fish_in_zone(1) == 1, + str(GameState.fish_in_zone(1))) check("보유량은 그대로", is_equal_approx(GameState.bubbles, 500.0)) - # 정원보다 많이 살고 있으면 집을 세워 다 들어가게 해준다. + # 개수만 있던 저장본도 같은 규칙으로 옮겨온다. GameState.reset(false) GameState.from_dict({ "bubbles": 1e9, - "producers": {"seaweed": 10}, + "producers": {"seaweed": 10, "guppy": 4}, "ornaments": {"amphora": true}, }) - check("옛 개수 저장본도 옮겨온다", GameState.fish_count("seaweed") == 10, - str(GameState.fish_count("seaweed"))) - check("한 마리도 내쫓지 않는다", GameState.fish_total == 10, str(GameState.fish_total)) - check("정원이 모자라면 집을 더 세워준다", GameState.capacity_in_zone(0) >= 10, + check("옛 개수 저장본도 옮겨온다", GameState.house_count("seaweed") == 10, + str(GameState.house_count("seaweed"))) + check("헤엄치던 것은 그대로 마릿수", GameState.fish_count("guppy") == 4, + str(GameState.fish_count("guppy"))) + check("한 마리도 내쫓지 않는다", GameState.fish_total == 4, str(GameState.fish_total)) + check("정원이 모자라면 집을 더 세워준다", GameState.capacity_in_zone(0) >= 4, "정원 %d / 생물 %d" % [GameState.capacity_in_zone(0), GameState.fish_total]) check("보이지 않는 생물이 남지 않는다", GameState.free_slots_in_zone(0) >= 0, str(GameState.free_slots_in_zone(0))) @@ -562,8 +576,9 @@ func _test_shape_families() -> void: print("형태 계통") # 집과 생물이 같은 도형을 쓰면 화면에서 "건물"과 "사는 것"이 구분되지 않는다. # 그래서 두 계통을 아예 겹치지 않게 갈라 두고, 그것을 여기서 지킨다. - const CREATURE_SHAPES := ["plant", "rock", "fish", "glow"] - const HOUSE_SHAPES := ["vessel", "wreck", "pillar"] + # 사는 것은 헤엄치거나 걷는 해양동물, 사는 곳은 자연 서식지와 지어 올린 것. + const CREATURE_SHAPES := ["fish", "glow", "crab", "seahorse", "shrimp"] + const HOUSE_SHAPES := ["rock", "plant", "vessel", "wreck", "pillar"] var bad_creature := "" for d in Catalog.PRODUCERS: @@ -600,6 +615,24 @@ func _test_shape_families() -> void: starter_swims = true check("시작 생물에도 헤엄치는 것이 있다", starter_swims) + # 고착생물은 전부 집으로 옮겼다. 생물 목록에 다시 섞여 들어오면 안 된다. + var sessile := "" + for d in Catalog.PRODUCERS: + if String(d.shape) in ["plant", "rock"]: + sessile = String(d.name) + check("생물에 고착생물이 없다", sessile.is_empty(), sessile) + + # 자연 서식지가 하나도 없는 구역이 있으면 값싼 첫 자리가 없어 시작이 막힌다. + var starter_home := true + for z in Catalog.zone_count(): + var cheapest := INF + for d in Catalog.HOUSES: + if int(d.get("zone", 0)) == z: + cheapest = minf(cheapest, float(d.base_cost)) + if not is_finite(cheapest): + starter_home = false + check("구역마다 지을 집이 있다", starter_home) + func _test_upgrades_are_all_production() -> void: print("업그레이드") @@ -611,7 +644,7 @@ func _test_upgrades_are_all_production() -> void: _rich_and_open() _house_in(0) - GameState.buy_fish("seaweed") + GameState.buy_fish("hermit_crab") var before := GameState.bps var id := String(Catalog.UPGRADES[0].id) check("살 수 있다", GameState.buy_upgrade(id))