클리커에서 집·일꾼 방치형 구조로 전환
거품은 이제 클릭이 아니라 바다에 있는 생물이 시간에 따라 번다. 화면 가장자리에
깔아 두는 위젯인데 클릭이 주된 수입원이면 연타가 최적 플레이가 되어 형태와
어긋난다. 클릭 수익과 클릭 업그레이드를 걷어내고 업그레이드는 전부 생산 배율로
바꿨다. 물을 만지면 거품이 퍼지지만 얻는 것은 없다.
격자 칸을 차지하는 것은 집뿐이고, 생물은 칸이 아니라 집에 산다(한 채당 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:
+294
-119
@@ -2,14 +2,16 @@ extends Control
|
||||
class_name Aquarium
|
||||
## 화면 하단에 깔리는 탑다운 바다.
|
||||
## 가로를 구역 수만큼 나눠, 왼쪽(얕은 바다)에서 오른쪽(열수구)으로 갈수록 깊어진다.
|
||||
## 구역마다 격자가 있고, 산 것은 플레이어가 고른 칸에 놓인다.
|
||||
##
|
||||
## 격자 칸을 차지하는 것은 집뿐이다. 생물은 칸이 아니라 집에 살기 때문에
|
||||
## 그릴 때마다 구역별로 집에 차례로 배정해서 그 집 둘레에 세운다.
|
||||
|
||||
## 알림을 띄워야 할 때 (해금 실패, 배치 실패, 환불 등). main이 받아 HUD로 넘긴다.
|
||||
## 알림을 띄워야 할 때 (해금 실패, 건축 실패, 환불 등). main이 받아 HUD로 넘긴다.
|
||||
signal message(text: String)
|
||||
## 빈 곳을 눌렀을 때. 거품 클릭 연출에 쓴다.
|
||||
## 빈 곳을 눌렀을 때. 거품이 퍼지는 연출에만 쓴다 — 거품은 생물이 번다.
|
||||
signal clicked(local_position: Vector2)
|
||||
## 배치 모드가 켜지거나 꺼졌을 때. 상점이 선택 상태를 표시하는 데 쓴다.
|
||||
signal placement_changed(active: bool, kind: String, id: String)
|
||||
## 집 짓기 모드가 켜지거나 꺼졌을 때. 상점이 선택 상태를 표시하는 데 쓴다.
|
||||
signal build_mode_changed(active: bool, id: String)
|
||||
## 이미 놓인 것을 집었거나 놓았을 때. HUD가 휴지통을 보였다 감추는 데 쓴다.
|
||||
signal drag_changed(dragging: bool)
|
||||
## 물 위에서 우클릭. main이 메뉴를 연다.
|
||||
@@ -29,21 +31,33 @@ var trash_rect := Rect2()
|
||||
|
||||
## 위쪽 이만큼은 정보 바가 앉는 자리라 격자에서 뺀다.
|
||||
## 여기를 비워두지 않으면 첫 줄 칸이 바에 가려 보이지도, 눌리지도 않는다.
|
||||
const GRID_TOP := 30.0
|
||||
## 바 높이는 글꼴과 담긴 내용에 따라 달라지므로 main이 HUD에서 재어 넣어준다.
|
||||
var grid_top := 30.0:
|
||||
set(value):
|
||||
if is_equal_approx(grid_top, value):
|
||||
return
|
||||
grid_top = value
|
||||
if is_node_ready():
|
||||
_on_layout_changed()
|
||||
|
||||
const DRAG_THRESHOLD := 5.0
|
||||
## _press_index에 쓰는 표식. 이번 누름은 클릭이 아니라 드래그였다는 뜻.
|
||||
const PRESS_WAS_DRAG := -2
|
||||
## 물고기가 집을 한 바퀴 도는 속도(라디안/초). 위젯이라 눈에 거슬리지 않게 느리다.
|
||||
const ORBIT_SPEED := 0.5
|
||||
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var _lock_reasons := {} ## 구역 인덱스 -> 사유 Label
|
||||
var _base_material: ShaderMaterial
|
||||
|
||||
var _placing_kind := ""
|
||||
var _placing_id := ""
|
||||
## 지금 짓는 중인 집의 id. 비어 있으면 짓는 중이 아니다.
|
||||
var _building_id := ""
|
||||
var _hover := Vector2i(-1, -1)
|
||||
var _press_position := Vector2.ZERO
|
||||
var _press_index := -1
|
||||
var _drag_index := -1
|
||||
## 누르거나 끌고 있는 대상. {} 이면 없음. TileItem의 ref_* 를 그대로 담는다.
|
||||
var _press_ref := {}
|
||||
var _drag_ref := {}
|
||||
var _drag_node: TileItem = null
|
||||
## 이번 누름이 클릭이 아니라 드래그였다는 표식.
|
||||
var _press_was_drag := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -72,7 +86,9 @@ func _process(_delta: float) -> void:
|
||||
|
||||
## 격자가 실제로 깔리는 영역. 위쪽 정보 바 자리는 뺀다.
|
||||
func grid_rect() -> Rect2:
|
||||
return Rect2(Vector2(0.0, GRID_TOP), Vector2(size.x, maxf(size.y - GRID_TOP, 1.0)))
|
||||
# 스트립이 낮을 때 바가 격자를 다 먹어버리지 않도록 위쪽 절반은 넘기지 않는다.
|
||||
var top := clampf(grid_top, 0.0, size.y * 0.45)
|
||||
return Rect2(Vector2(0.0, top), Vector2(size.x, maxf(size.y - top, 1.0)))
|
||||
|
||||
|
||||
func cell_size() -> Vector2:
|
||||
@@ -82,13 +98,14 @@ func cell_size() -> Vector2:
|
||||
|
||||
func cell_at(pos: Vector2) -> Vector2i:
|
||||
var c := cell_size()
|
||||
return Vector2i(int(floor(pos.x / c.x)), int(floor((pos.y - GRID_TOP) / c.y)))
|
||||
var top := grid_rect().position.y
|
||||
return Vector2i(int(floor(pos.x / c.x)), int(floor((pos.y - top) / c.y)))
|
||||
|
||||
|
||||
func cell_rect(col: int, row: int, span := Vector2i.ONE) -> Rect2:
|
||||
var c := cell_size()
|
||||
return Rect2(
|
||||
Vector2(col * c.x, GRID_TOP + row * c.y),
|
||||
Vector2(col * c.x, grid_rect().position.y + row * c.y),
|
||||
Vector2(c.x * span.x, c.y * span.y))
|
||||
|
||||
|
||||
@@ -99,9 +116,9 @@ func zone_rect(index: int) -> Rect2:
|
||||
Vector2(Catalog.ZONE_COLUMNS * c.x, size.y))
|
||||
|
||||
|
||||
## 커서가 가리키는 칸을 기준으로 발자국을 가운데 맞춘 왼쪽 위 칸.
|
||||
func _anchor_for(kind: String, id: String, hover: Vector2i) -> Vector2i:
|
||||
var s := Catalog.size_of(kind, id)
|
||||
## 커서가 가리키는 칸을 기준으로 집 발자국을 가운데 맞춘 왼쪽 위 칸.
|
||||
func _anchor_for(id: String, hover: Vector2i) -> Vector2i:
|
||||
var s := Catalog.size_of("house", id)
|
||||
return hover - Vector2i((s.x - 1) / 2, (s.y - 1) / 2)
|
||||
|
||||
|
||||
@@ -111,7 +128,7 @@ func _anchor_for(kind: String, id: String, hover: Vector2i) -> Vector2i:
|
||||
## 스트립은 자주 바뀌지 않으므로 부분 갱신보다 통째로 다시 만드는 쪽이 단순하다.
|
||||
func _on_layout_changed() -> void:
|
||||
ground.rect_size = size
|
||||
ground.grid_top = GRID_TOP
|
||||
ground.grid_top = grid_rect().position.y
|
||||
ground.queue_redraw()
|
||||
|
||||
_build_water()
|
||||
@@ -259,7 +276,9 @@ func _on_zone_unlocked(index: int) -> void:
|
||||
_on_layout_changed()
|
||||
|
||||
|
||||
# --- 놓인 것들 ---
|
||||
|
||||
|
||||
# --- 집과 생물 그리기 ---
|
||||
|
||||
func _setup_bubbles() -> void:
|
||||
bubbles.emitting = true
|
||||
@@ -277,37 +296,153 @@ func _setup_bubbles() -> void:
|
||||
func _rebuild_items() -> void:
|
||||
for child in items.get_children():
|
||||
child.queue_free()
|
||||
for i in GameState.placements.size():
|
||||
_make_item(i)
|
||||
for i in GameState.houses.size():
|
||||
_make_house(i)
|
||||
for entry in _assign_fish():
|
||||
_make_fish(entry)
|
||||
|
||||
|
||||
func _make_item(index: int) -> TileItem:
|
||||
var p := GameState.placements[index]
|
||||
var d := Catalog.item(p.kind, p.id)
|
||||
func _house_rect(index: int) -> Rect2:
|
||||
var h := GameState.houses[index]
|
||||
return cell_rect(int(h.col), int(h.row), Catalog.size_of("house", String(h.id)))
|
||||
|
||||
|
||||
func _make_house(index: int) -> TileItem:
|
||||
var id := String(GameState.houses[index].id)
|
||||
var d := Catalog.house(id)
|
||||
if d.is_empty():
|
||||
return null
|
||||
var node := TileItem.new()
|
||||
items.add_child(node)
|
||||
node.setup(d, p.kind, index, cell_rect(p.col, p.row, Catalog.size_of(p.kind, p.id)),
|
||||
cell_size(), _rng)
|
||||
node.setup(d, "house", index, _house_rect(index), cell_size(), _rng)
|
||||
return node
|
||||
|
||||
|
||||
func _make_fish(entry: Dictionary) -> void:
|
||||
var d := Catalog.producer(String(entry.id))
|
||||
if d.is_empty():
|
||||
return
|
||||
var home := _house_rect(int(entry.house))
|
||||
var cell := cell_size()
|
||||
var slot := int(entry.slot)
|
||||
|
||||
var node := TileItem.new()
|
||||
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)
|
||||
# 집마다 도는 방향을 바꿔 여러 채가 한 화면에 있어도 단조롭지 않게 한다.
|
||||
var dir := 1.0 if house_index % 2 == 0 else -1.0
|
||||
node.set_orbit(_orbit_center(home, cell), _orbit_radius(home, cell),
|
||||
TAU * float(slot) / float(cap), ORBIT_SPEED * dir)
|
||||
|
||||
|
||||
## 어느 생물이 어느 집에 사는지 그릴 때마다 정한다. 집은 정원만 주므로 배정을
|
||||
## 저장할 필요가 없다. 구역마다 집을 격자 순서로, 생물을 카탈로그 순서로 세워 채운다.
|
||||
## 돌려주는 것: [{id, house, slot}, ...]. 정원을 넘겨 사는 것들은 빠진다.
|
||||
func _assign_fish() -> Array:
|
||||
var out: Array = []
|
||||
for z in Catalog.zone_count():
|
||||
var homes := _homes_in_zone(z)
|
||||
if homes.is_empty():
|
||||
continue
|
||||
var hi := 0
|
||||
var used := 0
|
||||
for d in Catalog.PRODUCERS:
|
||||
if int(d.get("zone", 0)) != z:
|
||||
continue
|
||||
for n in GameState.fish_count(String(d.id)):
|
||||
while hi < homes.size() \
|
||||
and used >= Catalog.capacity_of(String(GameState.houses[homes[hi]].id)):
|
||||
hi += 1
|
||||
used = 0
|
||||
if hi >= homes.size():
|
||||
break
|
||||
out.append({"id": String(d.id), "house": homes[hi], "slot": used})
|
||||
used += 1
|
||||
return out
|
||||
|
||||
|
||||
## 그 구역의 집들을 격자 순서(왼쪽 위부터)로. 배정이 매번 같아야 그림이 안 튄다.
|
||||
func _homes_in_zone(zone_index: int) -> Array[int]:
|
||||
var homes: Array[int] = []
|
||||
for i in GameState.houses.size():
|
||||
if Catalog.zone_of_column(int(GameState.houses[i].col)) == zone_index:
|
||||
homes.append(i)
|
||||
homes.sort_custom(func(a: int, b: int) -> bool:
|
||||
var ha := GameState.houses[a]
|
||||
var hb := GameState.houses[b]
|
||||
if int(ha.col) != int(hb.col):
|
||||
return int(ha.col) < int(hb.col)
|
||||
return int(ha.row) < int(hb.row))
|
||||
return homes
|
||||
|
||||
|
||||
## 물고기가 도는 궤도의 반지름. 집을 감싸되 3/4 부감이라 세로로 납작하다.
|
||||
func _orbit_radius(home: Rect2, cell: Vector2) -> Vector2:
|
||||
return Vector2(home.size.x * 0.5 + cell.x * 0.55, cell.y * 0.42)
|
||||
|
||||
|
||||
## 궤도의 중심. 집 발치에 두되, 궤도가 통째로 바다 안에 들어오도록 밀어 넣는다.
|
||||
## 중심을 옮기지 않고 매 프레임 위치만 자르면 물고기가 벽에 붙어 멈춘 것처럼 보인다.
|
||||
func _orbit_center(home: Rect2, cell: Vector2) -> Vector2:
|
||||
var r := _orbit_radius(home, cell)
|
||||
var g := grid_rect()
|
||||
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))
|
||||
|
||||
|
||||
## 범위가 뒤집힐 만큼 좁으면(작은 창) 가운데로 보낸다. clampf는 그때 엉뚱한 값을 준다.
|
||||
func _squeeze(value: float, lo: float, hi: float) -> float:
|
||||
if lo > hi:
|
||||
return (lo + hi) * 0.5
|
||||
return clampf(value, lo, hi)
|
||||
|
||||
|
||||
## 집 둘레의 붙박이 자리 하나. 해초·거품돌처럼 바닥에 서는 것이 쓴다.
|
||||
## 왼쪽 → 오른쪽 → 앞 순으로 돌고, 정원이 더 크면 한 겹 더 벌어진다.
|
||||
func _fish_area(home: Rect2, cell: Vector2, slot: int) -> Rect2:
|
||||
# 앞자리는 살짝 오른쪽으로 밀어 둔다. 바다 왼쪽 끝에 붙은 집은 왼쪽 자리가
|
||||
# 안으로 밀려 들어오는데, 그때 앞자리와 겹쳐 한 마리처럼 보이기 때문이다.
|
||||
const SPOTS := [Vector2(-0.5, -0.1), Vector2(0.5, -0.1), Vector2(0.12, 0.62)]
|
||||
var spot: Vector2 = SPOTS[slot % SPOTS.size()]
|
||||
var ring := float(slot / SPOTS.size())
|
||||
|
||||
var cx := home.position.x + home.size.x * 0.5
|
||||
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()
|
||||
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)
|
||||
|
||||
|
||||
func _on_placed(index: int) -> void:
|
||||
var node := _make_item(index)
|
||||
# 집이 하나 늘면 생물 배정도 달라지므로 통째로 다시 그린다.
|
||||
_rebuild_items()
|
||||
var node := _node_for_house(index)
|
||||
if node:
|
||||
# 새로 놓은 것은 살짝 부풀며 등장한다.
|
||||
# 새로 지은 집은 살짝 부풀며 등장한다.
|
||||
node.scale = Vector2.ZERO
|
||||
var t := create_tween()
|
||||
t.tween_property(node, "scale", Vector2.ONE, 0.28) \
|
||||
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## 커서 아래에 있는 놓인 것. 그려진 모양으로 먼저 찾는다.
|
||||
## 커서 아래에 있는 것. 그려진 모양으로 먼저 찾는다.
|
||||
## 키 큰 것은 제 칸보다 위쪽에 그려지므로, 보이는 자리를 눌러야 집히는 게 자연스럽다.
|
||||
## 앞줄(화면 아래쪽)에 있는 것이 이긴다.
|
||||
func _pick_at(pos: Vector2) -> int:
|
||||
var best := -1
|
||||
func _pick_at(pos: Vector2) -> Dictionary:
|
||||
var best: TileItem = null
|
||||
var best_y := -INF
|
||||
for child in items.get_children():
|
||||
var it := child as TileItem
|
||||
@@ -317,90 +452,102 @@ func _pick_at(pos: Vector2) -> int:
|
||||
r.position += it.position
|
||||
if r.has_point(pos) and it.position.y > best_y:
|
||||
best_y = it.position.y
|
||||
best = it.placement_index
|
||||
if best >= 0:
|
||||
return best
|
||||
# 그림에 안 걸렸으면 발자국 칸으로 한 번 더 본다.
|
||||
var cell := cell_at(pos)
|
||||
return GameState.placement_at(cell.x, cell.y)
|
||||
best = it
|
||||
if best == null:
|
||||
# 그림에 안 걸렸으면 발자국 칸으로 한 번 더 본다. 칸을 가진 것은 집뿐이다.
|
||||
var cell := cell_at(pos)
|
||||
var index := GameState.house_at(cell.x, cell.y)
|
||||
if index < 0:
|
||||
return {}
|
||||
return {"kind": "house", "index": index, "id": String(GameState.houses[index].id)}
|
||||
return {"kind": best.ref_kind, "index": best.ref_index, "id": best.ref_id}
|
||||
|
||||
|
||||
func _item_node(index: int) -> TileItem:
|
||||
func _node_for_house(index: int) -> TileItem:
|
||||
for child in items.get_children():
|
||||
var it := child as TileItem
|
||||
if it and it.placement_index == index:
|
||||
if it and it.ref_kind == "house" and it.ref_index == index:
|
||||
return it
|
||||
return null
|
||||
|
||||
|
||||
# --- 배치 모드 ---
|
||||
func _node_for(ref: Dictionary) -> TileItem:
|
||||
if ref.is_empty():
|
||||
return null
|
||||
if String(ref.kind) == "house":
|
||||
return _node_for_house(int(ref.index))
|
||||
for child in items.get_children():
|
||||
var it := child as TileItem
|
||||
if it and it.ref_kind == "producer" and it.ref_id == String(ref.id):
|
||||
return it
|
||||
return null
|
||||
|
||||
func begin_placement(kind: String, id: String) -> void:
|
||||
if kind == "upgrade":
|
||||
|
||||
# --- 집 짓기 모드 ---
|
||||
|
||||
func begin_build(id: String) -> void:
|
||||
if Catalog.house(id).is_empty():
|
||||
return
|
||||
_cancel_drag()
|
||||
_placing_kind = kind
|
||||
_placing_id = id
|
||||
placement_changed.emit(true, kind, id)
|
||||
_building_id = id
|
||||
build_mode_changed.emit(true, id)
|
||||
_update_ghost()
|
||||
|
||||
|
||||
func cancel_placement() -> void:
|
||||
if _placing_id.is_empty():
|
||||
func cancel_build() -> void:
|
||||
if _building_id.is_empty():
|
||||
return
|
||||
_placing_kind = ""
|
||||
_placing_id = ""
|
||||
_building_id = ""
|
||||
ghost.hide()
|
||||
placement_changed.emit(false, "", "")
|
||||
build_mode_changed.emit(false, "")
|
||||
|
||||
|
||||
func is_placing() -> bool:
|
||||
return not _placing_id.is_empty()
|
||||
func is_building() -> bool:
|
||||
return not _building_id.is_empty()
|
||||
|
||||
|
||||
func _update_ghost() -> void:
|
||||
if not is_placing() or _hover.x < 0:
|
||||
if not is_building() or _hover.x < 0:
|
||||
ghost.hide()
|
||||
return
|
||||
var anchor := _anchor_for(_placing_kind, _placing_id, _hover)
|
||||
var span := Catalog.size_of(_placing_kind, _placing_id)
|
||||
var ok := GameState.can_place_at(_placing_kind, _placing_id, anchor.x, anchor.y) \
|
||||
and GameState.bubbles >= GameState.cost_of(_placing_kind, _placing_id)
|
||||
var anchor := _anchor_for(_building_id, _hover)
|
||||
var span := Catalog.size_of("house", _building_id)
|
||||
var ok := GameState.can_build_at(_building_id, anchor.x, anchor.y) \
|
||||
and GameState.bubbles >= GameState.cost_of("house", _building_id)
|
||||
ghost.show_at(cell_rect(anchor.x, anchor.y, span), ok)
|
||||
|
||||
|
||||
func _try_place() -> void:
|
||||
var kind := _placing_kind
|
||||
var id := _placing_id
|
||||
var anchor := _anchor_for(kind, id, _hover)
|
||||
func _try_build() -> void:
|
||||
var id := _building_id
|
||||
var anchor := _anchor_for(id, _hover)
|
||||
|
||||
if GameState.bubbles < GameState.cost_of(kind, id):
|
||||
if GameState.bubbles < GameState.cost_of("house", id):
|
||||
message.emit("거품이 모자라요")
|
||||
cancel_placement()
|
||||
cancel_build()
|
||||
return
|
||||
if not GameState.can_place_at(kind, id, anchor.x, anchor.y):
|
||||
message.emit(_why_not(kind, id, anchor))
|
||||
if not GameState.can_build_at(id, anchor.x, anchor.y):
|
||||
message.emit(_why_not(id, anchor))
|
||||
return
|
||||
|
||||
GameState.place(kind, id, anchor.x, anchor.y)
|
||||
# 계속 놓을 수 있으면 모드를 유지한다. 같은 걸 여러 개 깔 때 편하다.
|
||||
if kind == "ornament" or GameState.bubbles < GameState.cost_of(kind, id):
|
||||
cancel_placement()
|
||||
GameState.build_house(id, anchor.x, anchor.y)
|
||||
# 계속 지을 수 있으면 모드를 유지한다. 같은 집을 여러 채 깔 때 편하다.
|
||||
if GameState.bubbles < GameState.cost_of("house", id):
|
||||
cancel_build()
|
||||
else:
|
||||
_update_ghost()
|
||||
|
||||
|
||||
func _why_not(kind: String, id: String, anchor: Vector2i) -> String:
|
||||
var item_zone := Catalog.zone_of(kind, id)
|
||||
func _why_not(id: String, anchor: Vector2i) -> String:
|
||||
var item_zone := Catalog.zone_of("house", id)
|
||||
if not GameState.is_zone_unlocked(item_zone):
|
||||
return "%s 아직 잠겨 있어요" % Korean.i(String(Catalog.zone(item_zone).name))
|
||||
var cells := GameState.footprint(kind, id, anchor.x, anchor.y)
|
||||
var cells := GameState.footprint(id, anchor.x, anchor.y)
|
||||
if cells.is_empty():
|
||||
return "바다 밖으로 나가요"
|
||||
for c in cells:
|
||||
if Catalog.zone_of_column(c.x) != item_zone:
|
||||
return "%s 안에만 놓을 수 있어요" % Catalog.zone(item_zone).name
|
||||
return "이미 무언가 놓여 있어요"
|
||||
return "%s 안에만 지을 수 있어요" % Catalog.zone(item_zone).name
|
||||
return "이미 무언가 서 있어요"
|
||||
|
||||
|
||||
# --- 입력 ---
|
||||
@@ -408,11 +555,12 @@ func _why_not(kind: String, id: String, anchor: Vector2i) -> String:
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
_hover = cell_at(event.position)
|
||||
if _drag_index >= 0:
|
||||
if not _drag_ref.is_empty():
|
||||
_update_drag(event.position)
|
||||
elif is_placing():
|
||||
elif is_building():
|
||||
_update_ghost()
|
||||
elif _press_index >= 0 and event.position.distance_to(_press_position) > DRAG_THRESHOLD:
|
||||
elif not _press_ref.is_empty() and not _press_was_drag \
|
||||
and event.position.distance_to(_press_position) > DRAG_THRESHOLD:
|
||||
_begin_drag()
|
||||
return
|
||||
|
||||
@@ -420,9 +568,9 @@ func _gui_input(event: InputEvent) -> void:
|
||||
return
|
||||
|
||||
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
||||
if is_placing():
|
||||
cancel_placement()
|
||||
elif _drag_index >= 0:
|
||||
if is_building():
|
||||
cancel_build()
|
||||
elif not _drag_ref.is_empty():
|
||||
_cancel_drag()
|
||||
else:
|
||||
context_menu_requested.emit()
|
||||
@@ -433,80 +581,107 @@ func _gui_input(event: InputEvent) -> void:
|
||||
|
||||
if event.pressed:
|
||||
_hover = cell_at(event.position)
|
||||
if is_placing():
|
||||
_try_place()
|
||||
if is_building():
|
||||
_try_build()
|
||||
return
|
||||
_press_position = event.position
|
||||
_press_index = _pick_at(event.position)
|
||||
_press_ref = _pick_at(event.position)
|
||||
_press_was_drag = false
|
||||
else:
|
||||
if _drag_index >= 0:
|
||||
if not _drag_ref.is_empty():
|
||||
_drop(event.position)
|
||||
elif _press_index != PRESS_WAS_DRAG:
|
||||
# 끌지 않고 뗐으면 그냥 물을 클릭한 것으로 친다.
|
||||
elif not _press_was_drag:
|
||||
# 끌지 않고 뗐으면 물을 만진 것으로 친다.
|
||||
clicked.emit(event.position)
|
||||
_press_index = -1
|
||||
_press_ref = {}
|
||||
_press_was_drag = false
|
||||
|
||||
|
||||
func _begin_drag() -> void:
|
||||
if _press_index < 0:
|
||||
if _press_ref.is_empty():
|
||||
return
|
||||
_drag_index = _press_index
|
||||
_press_index = PRESS_WAS_DRAG
|
||||
_drag_ref = _press_ref
|
||||
_drag_node = _node_for(_drag_ref)
|
||||
# 끄는 동안에는 궤도를 멈춘다. 안 그러면 손을 따라오지 않고 제 길을 간다.
|
||||
if _drag_node:
|
||||
_drag_node.orbit_enabled = false
|
||||
_press_was_drag = true
|
||||
drag_changed.emit(true)
|
||||
|
||||
|
||||
func _update_drag(pos: Vector2) -> void:
|
||||
var node := _item_node(_drag_index)
|
||||
if node:
|
||||
node.position = pos
|
||||
if _drag_node and is_instance_valid(_drag_node):
|
||||
_drag_node.position = pos
|
||||
|
||||
if trash_rect.has_point(pos):
|
||||
# 생물은 칸을 차지하지 않으므로 놓일 자리를 미리 보여줄 것이 없다.
|
||||
if trash_rect.has_point(pos) or String(_drag_ref.kind) != "house":
|
||||
ghost.hide()
|
||||
return
|
||||
|
||||
var p := GameState.placements[_drag_index]
|
||||
var anchor := _anchor_for(p.kind, p.id, _hover)
|
||||
var span := Catalog.size_of(p.kind, p.id)
|
||||
var ok := GameState.can_place_at(p.kind, p.id, anchor.x, anchor.y, _drag_index)
|
||||
ghost.show_at(cell_rect(anchor.x, anchor.y, span), ok)
|
||||
var id := String(_drag_ref.id)
|
||||
var anchor := _anchor_for(id, _hover)
|
||||
var ok := GameState.can_build_at(id, anchor.x, anchor.y, int(_drag_ref.index))
|
||||
ghost.show_at(cell_rect(anchor.x, anchor.y, Catalog.size_of("house", id)), ok)
|
||||
|
||||
|
||||
func _drop(pos: Vector2) -> void:
|
||||
var index := _drag_index
|
||||
_drag_index = -1
|
||||
var ref := _drag_ref
|
||||
_drag_ref = {}
|
||||
_drag_node = null
|
||||
ghost.hide()
|
||||
drag_changed.emit(false)
|
||||
|
||||
if index < 0 or index >= GameState.placements.size():
|
||||
if trash_rect.has_point(pos):
|
||||
if String(ref.kind) == "house":
|
||||
_trash_house(int(ref.index))
|
||||
else:
|
||||
_release_fish(String(ref.id))
|
||||
return
|
||||
|
||||
# 집은 끌어다 옮길 수 있다. 생물은 자기 집을 따라다니므로 제자리로 돌아간다.
|
||||
if String(ref.kind) == "house":
|
||||
var anchor := _anchor_for(String(ref.id), _hover)
|
||||
if not GameState.move_house(int(ref.index), anchor.x, anchor.y):
|
||||
message.emit("여기에는 놓을 수 없어요")
|
||||
_rebuild_items()
|
||||
|
||||
|
||||
func _trash_house(index: int) -> void:
|
||||
if index < 0 or index >= GameState.houses.size():
|
||||
_rebuild_items()
|
||||
return
|
||||
|
||||
if trash_rect.has_point(pos):
|
||||
var p := GameState.placements[index]
|
||||
var item_name := String(Catalog.item(p.kind, p.id).get("name", ""))
|
||||
var refund := GameState.remove(index)
|
||||
message.emit("%s 치우고 거품 %s를 돌려받았어요" % [
|
||||
Korean.eul(item_name), NumberFormat.short(refund)])
|
||||
if not GameState.can_remove_house(index):
|
||||
message.emit("살고 있는 생물이 갈 곳이 없어요")
|
||||
_rebuild_items()
|
||||
return
|
||||
var house_name := String(Catalog.house(String(GameState.houses[index].id)).get("name", ""))
|
||||
var refund := GameState.remove_house(index)
|
||||
message.emit("%s 헐고 거품 %s를 돌려받았어요" % [
|
||||
Korean.eul(house_name), NumberFormat.short(refund)])
|
||||
|
||||
var placement := GameState.placements[index]
|
||||
var anchor := _anchor_for(placement.kind, placement.id, _hover)
|
||||
if not GameState.move(index, anchor.x, anchor.y):
|
||||
message.emit("여기에는 놓을 수 없어요")
|
||||
# 성공이든 실패든 제자리를 다시 잡아준다.
|
||||
_rebuild_items()
|
||||
|
||||
func _release_fish(id: String) -> void:
|
||||
if not GameState.can_release_fish(id):
|
||||
message.emit("마지막 생물은 내보낼 수 없어요")
|
||||
_rebuild_items()
|
||||
return
|
||||
var fish_name := String(Catalog.producer(id).get("name", ""))
|
||||
var refund := GameState.release_fish(id)
|
||||
message.emit("%s 내보내고 거품 %s를 돌려받았어요" % [
|
||||
Korean.eul(fish_name), NumberFormat.short(refund)])
|
||||
|
||||
|
||||
func _cancel_drag() -> void:
|
||||
if _drag_index < 0:
|
||||
if _drag_ref.is_empty():
|
||||
return
|
||||
_drag_index = -1
|
||||
_drag_ref = {}
|
||||
_drag_node = null
|
||||
ghost.hide()
|
||||
drag_changed.emit(false)
|
||||
_rebuild_items()
|
||||
|
||||
|
||||
## 클릭 지점에서 거품이 터지는 연출.
|
||||
## 만진 자리에서 거품이 퍼지는 연출.
|
||||
func pop_at(pos: Vector2) -> void:
|
||||
var burst := CPUParticles2D.new()
|
||||
add_child(burst)
|
||||
|
||||
Reference in New Issue
Block a user