1ca381922f
거품돌·해초·산호·켈프·관벌레는 물고기가 깃드는 자리지 그 자체가 일꾼이 아니다. 실제로 물고기는 바위 틈과 해초 사이를 보금자리로 삼는다. 전부 집으로 옮기고, 생물에는 움직이는 해양동물만 남겼다. - 집이 자연 서식지(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) <noreply@anthropic.com>
661 lines
20 KiB
GDScript
661 lines
20 KiB
GDScript
extends Node
|
|
## 게임의 진행 상태와 파생 수치를 들고 있는 단일 저장소.
|
|
## 저장/불러오기는 SaveManager가, 밸런스 수치는 Catalog가 담당한다.
|
|
##
|
|
## 두 가지가 서로 다른 규칙으로 움직인다.
|
|
## 집(house) — 격자 칸을 차지한다. "어디에 무엇을 지을까"가 진짜 상태다.
|
|
## 생물(producer) — 칸을 차지하지 않고 집에 산다. 몇 마리인지만 센다.
|
|
##
|
|
## 거품은 오직 생물이 시간에 따라 번다. 집은 거품을 벌지 않고 자리와 아름다움을 준다.
|
|
## 그래서 성장은 "집을 지어 자리를 늘리고 그 자리를 생물로 채운다"의 반복이다.
|
|
##
|
|
## 어느 생물이 어느 집에 사는지는 저장하지 않는다. 집은 정원만 주므로 어느 집에
|
|
## 살든 결과가 같고, 화면에 그릴 때 구역마다 차례로 배정하면 그만이다.
|
|
|
|
## 구매·이동·업그레이드처럼 화면을 다시 그려야 하는 이산적인 변화.
|
|
signal state_changed
|
|
## 새 구역이 열렸을 때.
|
|
signal zone_unlocked(index: int)
|
|
## 배치가 통째로 바뀌어 다시 그려야 할 때(불러오기, 초기화, 생물 증감, 집 철거).
|
|
signal reloaded
|
|
## 집이 새로 지어졌을 때. 등장 연출에 쓴다.
|
|
signal placed(index: int)
|
|
|
|
# --- 저장되는 상태 ---
|
|
var bubbles := 0.0
|
|
var total_bubbles := 0.0
|
|
## [{id, col, row}, ...] — 지은 순서대로. 인덱스가 곧 식별자다.
|
|
var houses: Array[Dictionary] = []
|
|
## id -> 마리 수. 생물은 칸을 차지하지 않으므로 위치가 없다.
|
|
var fish := {}
|
|
var upgrades := {} ## id -> true
|
|
var unlocked_zones := 1 ## 왼쪽부터 몇 개 구역이 열려 있는지 (최소 1)
|
|
var first_played_unix := 0
|
|
|
|
# --- recalc()로 계산되는 파생 값 (저장하지 않음) ---
|
|
var bps := 0.0 ## 초당 거품 생산량 (모든 보너스 반영)
|
|
var beauty := 0.0 ## 총 아름다움
|
|
var zone_beauty: Array[float] = [] ## 구역별 아름다움
|
|
var beauty_mult := 1.0
|
|
var fish_total := 0 ## 살고 있는 생물 수. 마지막 한 마리를 지키는 데 쓴다.
|
|
|
|
var _house_counts := {} ## id -> 지은 채수
|
|
var _zone_capacity: Array[int] = [] ## 구역별 정원 합
|
|
var _zone_fish: Array[int] = [] ## 구역별 살고 있는 수
|
|
var _occupancy := PackedInt32Array() ## 칸 -> houses 인덱스, 빈 칸은 -1
|
|
|
|
|
|
func _ready() -> void:
|
|
first_played_unix = int(Time.get_unix_time_from_system())
|
|
recalc()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if bps > 0.0:
|
|
_earn(bps * delta)
|
|
|
|
|
|
func _earn(amount: float) -> void:
|
|
bubbles += amount
|
|
total_bubbles += amount
|
|
|
|
|
|
# --- 격자 (집만 쓴다) ---
|
|
|
|
func columns() -> int:
|
|
return Catalog.total_columns()
|
|
|
|
|
|
func rows() -> int:
|
|
return Catalog.GRID_ROWS
|
|
|
|
|
|
func in_grid(col: int, row: int) -> bool:
|
|
return col >= 0 and row >= 0 and col < columns() and row < rows()
|
|
|
|
|
|
func _cell(col: int, row: int) -> int:
|
|
return row * columns() + col
|
|
|
|
|
|
## 그 칸에 선 집의 houses 인덱스. 비었으면 -1.
|
|
func house_at(col: int, row: int) -> int:
|
|
if not in_grid(col, row):
|
|
return -1
|
|
return _occupancy[_cell(col, row)]
|
|
|
|
|
|
## 집이 (col,row)에 섰을 때 덮는 칸들. 격자를 벗어나면 빈 배열.
|
|
func footprint(id: String, col: int, row: int) -> Array[Vector2i]:
|
|
var s := Catalog.size_of("house", id)
|
|
var cells: Array[Vector2i] = []
|
|
for dx in s.x:
|
|
for dy in s.y:
|
|
if not in_grid(col + dx, row + dy):
|
|
return []
|
|
cells.append(Vector2i(col + dx, row + dy))
|
|
return cells
|
|
|
|
|
|
## 여기에 지을 수 있는지. ignore_index는 자기 집을 옮길 때 제 칸을 비었다고 치기 위한 것.
|
|
func can_build_at(id: String, col: int, row: int, ignore_index := -1) -> bool:
|
|
var item_zone := Catalog.zone_of("house", id)
|
|
if not is_zone_unlocked(item_zone):
|
|
return false
|
|
var cells := footprint(id, col, row)
|
|
if cells.is_empty():
|
|
return false
|
|
for c in cells:
|
|
# 집은 자기 구역을 벗어날 수 없다. 구역 경계를 걸치는 것도 안 된다.
|
|
if Catalog.zone_of_column(c.x) != item_zone:
|
|
return false
|
|
var occupant := house_at(c.x, c.y)
|
|
if occupant != -1 and occupant != ignore_index:
|
|
return false
|
|
return true
|
|
|
|
|
|
## 그 구역에 남은 빈 칸 수. 집을 더 지을 수 있는지 알려준다.
|
|
func free_cells_in_zone(zone_index: int) -> int:
|
|
var free := 0
|
|
for col in range(zone_index * Catalog.ZONE_COLUMNS, (zone_index + 1) * Catalog.ZONE_COLUMNS):
|
|
for row in rows():
|
|
if house_at(col, row) == -1:
|
|
free += 1
|
|
return free
|
|
|
|
|
|
## 그 집을 지을 수 있는 첫 칸. 없으면 Vector2i(-1, -1).
|
|
## 아래 줄부터 훑는다. 게임이 알아서 세우는 집은 앞줄(화면 아래쪽)에 서야 눈에 띄고,
|
|
## 뒷줄은 플레이어가 직접 고르도록 남겨두는 편이 낫다.
|
|
func first_free_spot(id: String) -> Vector2i:
|
|
var z := Catalog.zone_of("house", id)
|
|
for col in range(z * Catalog.ZONE_COLUMNS, (z + 1) * Catalog.ZONE_COLUMNS):
|
|
for row in range(rows() - 1, -1, -1):
|
|
if can_build_at(id, col, row):
|
|
return Vector2i(col, row)
|
|
return Vector2i(-1, -1)
|
|
|
|
|
|
# --- 정원 (생물이 실제로 부딪히는 한계) ---
|
|
|
|
func capacity_in_zone(zone_index: int) -> int:
|
|
if zone_index < 0 or zone_index >= _zone_capacity.size():
|
|
return 0
|
|
return _zone_capacity[zone_index]
|
|
|
|
|
|
func fish_in_zone(zone_index: int) -> int:
|
|
if zone_index < 0 or zone_index >= _zone_fish.size():
|
|
return 0
|
|
return _zone_fish[zone_index]
|
|
|
|
|
|
## 그 구역에 남은 자리. 옛 저장본을 옮겨오면 음수가 될 수 있다(정원을 넘겨 살고 있다).
|
|
func free_slots_in_zone(zone_index: int) -> int:
|
|
return capacity_in_zone(zone_index) - fish_in_zone(zone_index)
|
|
|
|
|
|
func capacity_total() -> int:
|
|
var total := 0
|
|
for c in _zone_capacity:
|
|
total += c
|
|
return total
|
|
|
|
|
|
# --- 조회 ---
|
|
|
|
func house_count(id: String) -> int:
|
|
return int(_house_counts.get(id, 0))
|
|
|
|
|
|
func fish_count(id: String) -> int:
|
|
return int(fish.get(id, 0))
|
|
|
|
|
|
func count_of(kind: String, id: String) -> int:
|
|
match kind:
|
|
"producer": return fish_count(id)
|
|
"house": return house_count(id)
|
|
"upgrade": return 1 if has_upgrade(id) else 0
|
|
return 0
|
|
|
|
|
|
func has_upgrade(id: String) -> bool:
|
|
return upgrades.has(id)
|
|
|
|
|
|
## 생물과 집은 늘어날수록 값이 오른다. 업그레이드는 한 번뿐이라 정가다.
|
|
func cost_of(kind: String, id: String) -> float:
|
|
var d := Catalog.item(kind, id)
|
|
if d.is_empty():
|
|
return INF
|
|
if kind == "upgrade":
|
|
return float(d.get("cost", INF))
|
|
return _grown_cost(float(d.get("base_cost", INF)), count_of(kind, id))
|
|
|
|
|
|
func _grown_cost(base: float, owned_count: int) -> float:
|
|
if not is_finite(base):
|
|
return INF
|
|
return ceil(base * pow(Catalog.COST_GROWTH, owned_count))
|
|
|
|
|
|
## 치울 때 기준이 되는 값 — 지금 값이 아니라 마지막으로 산 그 하나의 값이다.
|
|
func _last_paid(kind: String, id: String) -> float:
|
|
var d := Catalog.item(kind, id)
|
|
if d.is_empty():
|
|
return 0.0
|
|
return _grown_cost(float(d.get("base_cost", 0.0)), maxi(count_of(kind, id) - 1, 0))
|
|
|
|
|
|
## 한 번만 살 수 있는 것(업그레이드)을 이미 가졌는지.
|
|
func owned(kind: String, id: String) -> bool:
|
|
return kind == "upgrade" and has_upgrade(id)
|
|
|
|
|
|
func is_zone_unlocked(index: int) -> bool:
|
|
return index < unlocked_zones
|
|
|
|
|
|
## 상점에 노출할지 여부.
|
|
## 아직 열리지 않은 구역의 것은 숨기고, 열린 구역 안에서는
|
|
## 가격의 절반까지 벌어본 적이 있으면 보여준다(점진 공개).
|
|
func is_revealed(kind: String, id: String) -> bool:
|
|
if not is_zone_unlocked(Catalog.zone_of(kind, id)):
|
|
return false
|
|
# 구역을 막 열었을 때 목록이 비어 보이지 않도록 그 구역의 첫 항목은 항상 보인다.
|
|
if id == Catalog.first_id_of_zone(kind, Catalog.zone_of(kind, id)):
|
|
return true
|
|
if count_of(kind, id) > 0:
|
|
return true
|
|
return total_bubbles >= cost_of(kind, id) * 0.5
|
|
|
|
|
|
# --- 집 짓기 ---
|
|
|
|
## 사서 그 칸에 짓는다. 성공하면 houses 인덱스, 실패하면 -1.
|
|
func build_house(id: String, col: int, row: int) -> int:
|
|
if Catalog.house(id).is_empty():
|
|
return -1
|
|
if not can_build_at(id, col, row):
|
|
return -1
|
|
var cost := cost_of("house", id)
|
|
if not is_finite(cost) or bubbles < cost:
|
|
return -1
|
|
|
|
bubbles -= cost
|
|
houses.append({"id": id, "col": col, "row": row})
|
|
recalc()
|
|
placed.emit(houses.size() - 1)
|
|
state_changed.emit()
|
|
return houses.size() - 1
|
|
|
|
|
|
## 이미 지은 집을 다른 칸으로 옮긴다. 공짜다. 살던 생물도 같이 따라간다.
|
|
func move_house(index: int, col: int, row: int) -> bool:
|
|
if index < 0 or index >= houses.size():
|
|
return false
|
|
var h := houses[index]
|
|
if not can_build_at(String(h.id), col, row, index):
|
|
return false
|
|
h.col = col
|
|
h.row = row
|
|
recalc()
|
|
state_changed.emit()
|
|
return true
|
|
|
|
|
|
## 헐 수 있는지. 살고 있는 생물이 남는 집에 다 들어가야 한다.
|
|
func can_remove_house(index: int) -> bool:
|
|
if index < 0 or index >= houses.size():
|
|
return false
|
|
var id := String(houses[index].id)
|
|
var z := Catalog.zone_of("house", id)
|
|
return fish_in_zone(z) <= capacity_in_zone(z) - Catalog.capacity_of(id)
|
|
|
|
|
|
## 헐고 절반을 돌려받는다. 돌려받은 양을 반환한다. 헐 수 없으면 0.
|
|
func remove_house(index: int) -> float:
|
|
if not can_remove_house(index):
|
|
return 0.0
|
|
var id := String(houses[index].id)
|
|
var refund: float = floor(_last_paid("house", id) * Catalog.REFUND_RATE)
|
|
|
|
houses.remove_at(index)
|
|
bubbles += refund
|
|
recalc()
|
|
reloaded.emit()
|
|
state_changed.emit()
|
|
return refund
|
|
|
|
|
|
# --- 생물 들이기 ---
|
|
|
|
## 그 생물을 들일 수 있는지. 구역이 열려 있고, 그 구역에 빈 자리가 있어야 한다.
|
|
func can_take_fish(id: String) -> bool:
|
|
var d := Catalog.producer(id)
|
|
if d.is_empty():
|
|
return false
|
|
var z := int(d.get("zone", 0))
|
|
return is_zone_unlocked(z) and free_slots_in_zone(z) > 0
|
|
|
|
|
|
## 왜 못 들이는지. 상점과 알림에 그대로 쓴다.
|
|
func fish_blocker(id: String) -> String:
|
|
var d := Catalog.producer(id)
|
|
if d.is_empty():
|
|
return "그런 생물은 없어요"
|
|
var z := int(d.get("zone", 0))
|
|
if not is_zone_unlocked(z):
|
|
return "%s 아직 잠겨 있어요" % Korean.i(String(Catalog.zone(z).name))
|
|
if free_slots_in_zone(z) <= 0:
|
|
return "%s에 빈 자리가 없어요 · 집을 더 지으세요" % Catalog.zone(z).name
|
|
if bubbles < cost_of("producer", id):
|
|
return "거품이 모자라요"
|
|
return ""
|
|
|
|
|
|
## 한 마리 들인다.
|
|
func buy_fish(id: String) -> bool:
|
|
if not can_take_fish(id):
|
|
return false
|
|
var cost := cost_of("producer", id)
|
|
if not is_finite(cost) or bubbles < cost:
|
|
return false
|
|
|
|
bubbles -= cost
|
|
fish[id] = fish_count(id) + 1
|
|
recalc()
|
|
reloaded.emit()
|
|
state_changed.emit()
|
|
return true
|
|
|
|
|
|
## 마지막 한 마리는 내보낼 수 없다. 생물이 0이 되면 거품이 영영 늘지 않기 때문이다.
|
|
func can_release_fish(id: String) -> bool:
|
|
return fish_count(id) > 0 and fish_total > 1
|
|
|
|
|
|
## 한 마리 내보내고 절반을 돌려받는다. 돌려받은 양을 반환한다.
|
|
func release_fish(id: String) -> float:
|
|
if not can_release_fish(id):
|
|
return 0.0
|
|
var refund: float = floor(_last_paid("producer", id) * Catalog.REFUND_RATE)
|
|
|
|
var left := fish_count(id) - 1
|
|
if left > 0:
|
|
fish[id] = left
|
|
else:
|
|
fish.erase(id)
|
|
bubbles += refund
|
|
recalc()
|
|
reloaded.emit()
|
|
state_changed.emit()
|
|
return refund
|
|
|
|
|
|
func buy_upgrade(id: String) -> bool:
|
|
if has_upgrade(id):
|
|
return false
|
|
var cost := cost_of("upgrade", id)
|
|
if not is_finite(cost) or bubbles < cost:
|
|
return false
|
|
bubbles -= cost
|
|
upgrades[id] = true
|
|
recalc()
|
|
state_changed.emit()
|
|
return true
|
|
|
|
|
|
# --- 구역 해금 ---
|
|
|
|
func next_zone_index() -> int:
|
|
return unlocked_zones
|
|
|
|
|
|
func has_next_zone() -> bool:
|
|
return unlocked_zones < Catalog.zone_count()
|
|
|
|
|
|
func next_zone_cost() -> float:
|
|
if not has_next_zone():
|
|
return INF
|
|
return float(Catalog.zone(next_zone_index()).unlock_cost)
|
|
|
|
|
|
## 다음 구역을 열려면 바로 앞 구역에 이만큼의 아름다움이 있어야 한다.
|
|
func next_zone_required_beauty() -> float:
|
|
if not has_next_zone():
|
|
return 0.0
|
|
return float(Catalog.zone(next_zone_index()).require_beauty)
|
|
|
|
|
|
func beauty_in_zone(index: int) -> float:
|
|
if index < 0 or index >= zone_beauty.size():
|
|
return 0.0
|
|
return zone_beauty[index]
|
|
|
|
|
|
## 다음 구역을 열 수 없다면 그 이유를, 열 수 있다면 빈 문자열을 돌려준다.
|
|
func next_zone_blocker() -> String:
|
|
if not has_next_zone():
|
|
return "마지막 구역까지 모두 열었어요"
|
|
var need_beauty := next_zone_required_beauty()
|
|
var have := beauty_in_zone(next_zone_index() - 1)
|
|
if have < need_beauty:
|
|
return "%s 더 꾸며야 해요 (아름다움 %s / %s)" % [
|
|
Korean.eul(String(Catalog.zone(next_zone_index() - 1).name)),
|
|
NumberFormat.short(have), NumberFormat.short(need_beauty),
|
|
]
|
|
if bubbles < next_zone_cost():
|
|
return "거품이 모자라요 (%s / %s)" % [
|
|
NumberFormat.short(bubbles), NumberFormat.short(next_zone_cost()),
|
|
]
|
|
return ""
|
|
|
|
|
|
func can_unlock_next() -> bool:
|
|
return next_zone_blocker().is_empty()
|
|
|
|
|
|
func unlock_next_zone() -> bool:
|
|
if not can_unlock_next():
|
|
return false
|
|
bubbles -= next_zone_cost()
|
|
var opened := unlocked_zones
|
|
unlocked_zones += 1
|
|
recalc()
|
|
zone_unlocked.emit(opened)
|
|
state_changed.emit()
|
|
return true
|
|
|
|
|
|
# --- 파생 값 재계산 ---
|
|
|
|
func recalc() -> void:
|
|
var zones := Catalog.zone_count()
|
|
_house_counts.clear()
|
|
_occupancy.resize(columns() * rows())
|
|
_occupancy.fill(-1)
|
|
|
|
zone_beauty.clear()
|
|
zone_beauty.resize(zones)
|
|
_zone_capacity.clear()
|
|
_zone_capacity.resize(zones)
|
|
_zone_fish.clear()
|
|
_zone_fish.resize(zones)
|
|
|
|
# 집 — 칸을 차지하고, 정원과 아름다움을 낸다. 거품은 벌지 않는다.
|
|
for i in houses.size():
|
|
var id := String(houses[i].id)
|
|
var d := Catalog.house(id)
|
|
if d.is_empty():
|
|
continue
|
|
_house_counts[id] = house_count(id) + 1
|
|
for c in footprint(id, int(houses[i].col), int(houses[i].row)):
|
|
_occupancy[_cell(c.x, c.y)] = i
|
|
var z := int(d.get("zone", 0))
|
|
_add_beauty(z, float(d.beauty))
|
|
_add_capacity(z, Catalog.capacity_of(id))
|
|
|
|
# 생물 — 거품을 버는 것은 이쪽뿐이다.
|
|
fish_total = 0
|
|
var raw_bps := 0.0
|
|
for key in fish:
|
|
var id := String(key)
|
|
var d := Catalog.producer(id)
|
|
var n := fish_count(id)
|
|
if d.is_empty() or n <= 0:
|
|
continue
|
|
fish_total += n
|
|
raw_bps += float(d.bps) * n
|
|
var z := int(d.get("zone", 0))
|
|
_add_beauty(z, float(d.beauty) * n)
|
|
_add_fish(z, n)
|
|
|
|
# 업그레이드는 전부 전체 생산 배율이다. 클릭으로 버는 길은 없다.
|
|
var upgrade_bps_mult := 1.0
|
|
for key in upgrades:
|
|
var u := Catalog.upgrade(String(key))
|
|
if u.is_empty():
|
|
continue
|
|
if String(u.kind) == "bps_mult":
|
|
upgrade_bps_mult *= float(u.value)
|
|
|
|
beauty = 0.0
|
|
for b in zone_beauty:
|
|
beauty += b
|
|
|
|
# 꾸밀수록 생산이 빨라지도록. 제곱근이라 초반 보상이 크고 후반은 완만하다.
|
|
beauty_mult = 1.0 + sqrt(beauty) * Catalog.BEAUTY_MULT_SCALE
|
|
bps = raw_bps * upgrade_bps_mult * beauty_mult
|
|
|
|
|
|
func _add_beauty(index: int, amount: float) -> void:
|
|
if index >= 0 and index < zone_beauty.size():
|
|
zone_beauty[index] += amount
|
|
|
|
|
|
func _add_capacity(index: int, amount: int) -> void:
|
|
if index >= 0 and index < _zone_capacity.size():
|
|
_zone_capacity[index] += amount
|
|
|
|
|
|
func _add_fish(index: int, amount: int) -> void:
|
|
if index >= 0 and index < _zone_fish.size():
|
|
_zone_fish[index] += amount
|
|
|
|
|
|
# --- 직렬화 ---
|
|
|
|
func to_dict() -> Dictionary:
|
|
var rows_out: Array = []
|
|
for h in houses:
|
|
rows_out.append({"id": h.id, "col": h.col, "row": h.row})
|
|
return {
|
|
"bubbles": bubbles,
|
|
"total_bubbles": total_bubbles,
|
|
"houses": rows_out,
|
|
"fish": fish.duplicate(),
|
|
"upgrades": upgrades.duplicate(),
|
|
"unlocked_zones": unlocked_zones,
|
|
"first_played_unix": first_played_unix,
|
|
}
|
|
|
|
|
|
func from_dict(d: Dictionary) -> void:
|
|
bubbles = float(d.get("bubbles", 0.0))
|
|
total_bubbles = float(d.get("total_bubbles", bubbles))
|
|
unlocked_zones = clampi(int(d.get("unlocked_zones", 1)), 1, Catalog.zone_count())
|
|
first_played_unix = int(d.get("first_played_unix", Time.get_unix_time_from_system()))
|
|
|
|
upgrades.clear()
|
|
for id in d.get("upgrades", {}):
|
|
upgrades[id] = true
|
|
|
|
houses.clear()
|
|
fish.clear()
|
|
recalc()
|
|
|
|
if d.has("houses") or d.has("fish"):
|
|
for raw in d.get("houses", []):
|
|
_restore_house(String(raw.get("id", "")),
|
|
int(raw.get("col", 0)), int(raw.get("row", 0)))
|
|
for id in d.get("fish", {}):
|
|
_restore_fish(String(id), int(d["fish"][id]))
|
|
else:
|
|
_migrate_old_save(d)
|
|
|
|
# 집이나 생물이 하나도 없으면 아무 일도 일어나지 않는다. 시작 상태를 채워준다.
|
|
if houses.is_empty():
|
|
_grant_starter_houses()
|
|
if fish_total == 0:
|
|
_grant_starter_fish()
|
|
_grant_homes_for_overflow()
|
|
|
|
recalc()
|
|
reloaded.emit()
|
|
state_changed.emit()
|
|
|
|
|
|
## 저장된 칸이 비어 있으면 그대로, 겹치거나 규칙에 어긋나면 빈 칸을 찾아 옮겨 짓는다.
|
|
## 격자 크기나 구역 구성이 바뀌어도 진행도를 잃지 않기 위한 것.
|
|
func _restore_house(id: String, col: int, row: int) -> void:
|
|
if Catalog.house(id).is_empty():
|
|
return
|
|
if not can_build_at(id, col, row):
|
|
var spot := first_free_spot(id)
|
|
if spot.x < 0:
|
|
return
|
|
col = spot.x
|
|
row = spot.y
|
|
houses.append({"id": id, "col": col, "row": row})
|
|
recalc()
|
|
|
|
|
|
## 정원을 넘겨도 이미 살던 생물을 내쫓지는 않는다. 자리가 빌 때까지 더 못 들일 뿐이다.
|
|
func _restore_fish(id: String, count: int) -> void:
|
|
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()
|
|
|
|
|
|
## 생물도 칸을 차지하던 시절의 저장본을 옮겨온다.
|
|
## 그때의 장식(ornament)이 지금의 집이고, 생물은 위치를 버리고 마릿수만 남긴다.
|
|
func _migrate_old_save(d: Dictionary) -> void:
|
|
if d.has("placements"):
|
|
for raw in d["placements"]:
|
|
var kind := String(raw.get("kind", "producer"))
|
|
var id := String(raw.get("id", ""))
|
|
if kind == "ornament" or kind == "house":
|
|
_restore_house(id, int(raw.get("col", 0)), int(raw.get("row", 0)))
|
|
else:
|
|
_restore_fish(id, 1)
|
|
return
|
|
# 격자가 없던 더 옛날 저장본. 개수만 있다.
|
|
for id in d.get("producers", {}):
|
|
_restore_fish(String(id), int(d["producers"][id]))
|
|
for id in d.get("ornaments", {}):
|
|
var spot := first_free_spot(String(id))
|
|
if spot.x >= 0:
|
|
_restore_house(String(id), spot.x, spot.y)
|
|
|
|
|
|
## 처음부터 다시. with_starter를 끄면 집도 생물도 없는 빈 바다가 된다(테스트용).
|
|
func reset(with_starter := true) -> void:
|
|
bubbles = 0.0
|
|
total_bubbles = 0.0
|
|
houses.clear()
|
|
fish.clear()
|
|
upgrades.clear()
|
|
unlocked_zones = 1
|
|
first_played_unix = int(Time.get_unix_time_from_system())
|
|
recalc()
|
|
if with_starter:
|
|
_grant_starter_houses()
|
|
_grant_starter_fish()
|
|
bubbles = Catalog.STARTER_BUBBLES
|
|
total_bubbles = Catalog.STARTER_BUBBLES
|
|
recalc()
|
|
reloaded.emit()
|
|
state_changed.emit()
|
|
|
|
|
|
## 시작 집을 값 없이 짓는다. 정해 둔 칸이 차 있으면 빈 칸을 찾는다.
|
|
func _grant_starter_houses() -> void:
|
|
for s in Catalog.STARTER_HOUSES:
|
|
_restore_house(String(s.id), int(s.col), int(s.row))
|
|
|
|
|
|
## 시작 생물을 값 없이 들인다.
|
|
func _grant_starter_fish() -> void:
|
|
for id in Catalog.STARTER_FISH:
|
|
_restore_fish(String(id), int(Catalog.STARTER_FISH[id]))
|
|
|
|
|
|
## 옛 저장본에서 옮겨온 생물이 정원을 넘길 수 있다(그때는 생물이 칸을 직접 차지했다).
|
|
## 그러면 집을 공짜로 세워 모두 들어가게 해준다. 살 곳 없는 생물은 화면에도
|
|
## 그려지지 않으므로, 보이지 않는 생물을 남기지 않으려는 것이다.
|
|
func _grant_homes_for_overflow() -> void:
|
|
for z in Catalog.zone_count():
|
|
var id := Catalog.first_id_of_zone("house", z)
|
|
if id.is_empty():
|
|
continue
|
|
while fish_in_zone(z) > capacity_in_zone(z):
|
|
var spot := first_free_spot(id)
|
|
if spot.x < 0:
|
|
break # 칸이 다 찼다. 더는 해줄 수 있는 게 없다.
|
|
_restore_house(id, spot.x, spot.y)
|