바탕화면 바닷속 방치형 위젯 첫 커밋

화면 아래 가장자리에 가로로 깔리는 Godot 4.7 위젯.
거품을 모아 바다를 꾸미고, 꾸밀수록 거품이 더 빨리 모인다.

- 도킹 창: 투명·테두리 없음, 화면 폭 전체, 위/아래 가장자리 스냅,
  높이 프리셋, 클릭 통과(모서리로 복귀), DPI 배율 보정
- 3/4 부감 격자: 구역당 9x6, 전체 45x6 = 270칸.
  바닥은 위에서, 물체는 서 있는 모습으로 그리고 Y 정렬로 앞뒤를 가린다
- 구역 5개(얕은 바다 -> 열수구): 앞 구역을 꾸며야 다음이 열린다
- 칸이 곧 한계인 경제: 배치/이동/치우기(절반 환불), 여러 칸짜리 장식
- 저장: 30초 자동 저장, 백업 한 세대, 8시간 상한 오프라인 보상,
  격자 이전 형식 저장본 자동 이전
- 스모크 테스트 60여 개 (test.ps1)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 17:59:50 +09:00
commit 446d8d177c
48 changed files with 3584 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
class_name Korean
## 한글 조사 처리. 앞 글자의 받침 유무에 따라 을/를, 이/가, 은/는이 갈린다.
## 구역 이름을 문장에 끼워 넣을 때 "켈프 숲를" 같은 어색한 말이 나오지 않게 한다.
const HANGUL_FIRST := 0xAC00
const HANGUL_LAST := 0xD7A3
## 마지막 글자에 받침이 있는지. 한글 음절이 아니면 없는 것으로 친다.
static func has_final(word: String) -> bool:
if word.is_empty():
return false
var c := word.unicode_at(word.length() - 1)
if c < HANGUL_FIRST or c > HANGUL_LAST:
return false
return (c - HANGUL_FIRST) % 28 != 0
static func josa(word: String, with_final: String, without_final: String) -> String:
return word + (with_final if has_final(word) else without_final)
static func eul(word: String) -> String:
return josa(word, "", "")
static func i(word: String) -> String:
return josa(word, "", "")
static func eun(word: String) -> String:
return josa(word, "", "")
+1
View File
@@ -0,0 +1 @@
uid://bo2o0ge2vbhp1
+40
View File
@@ -0,0 +1,40 @@
class_name NumberFormat
## 방치형 게임 특유의 큰 수를 짧게 표기한다. 1234 -> "1.23K"
const SUFFIXES := ["", "K", "M", "B", "T", "aa", "ab", "ac", "ad", "ae", "af", "ag"]
static func short(value: float) -> String:
if value < 0.0:
return "-" + short(-value)
if value < 1000.0:
# 1 미만은 소수점을 보여줘야 초당 생산량이 0으로 보이지 않는다.
if value < 10.0 and value != floor(value):
return "%.1f" % value
return "%d" % int(value)
var tier := 0
var v := value
while v >= 1000.0 and tier < SUFFIXES.size() - 1:
v /= 1000.0
tier += 1
if v < 10.0:
return "%.2f%s" % [v, SUFFIXES[tier]]
if v < 100.0:
return "%.1f%s" % [v, SUFFIXES[tier]]
return "%d%s" % [int(v), SUFFIXES[tier]]
## 경과 시간을 "3시간 12분" 형태로.
static func duration(seconds: int) -> String:
if seconds < 60:
return "%d" % seconds
var m := seconds / 60
if m < 60:
return "%d" % m
var h := m / 60
var rem := m % 60
if h < 24:
return "%d시간 %d" % [h, rem] if rem > 0 else "%d시간" % h
return "%d%d시간" % [h / 24, h % 24]
+1
View File
@@ -0,0 +1 @@
uid://crfj2c2detfxk
+64
View File
@@ -0,0 +1,64 @@
class_name UIStyle
## 위젯 UI의 색과 스타일박스를 한곳에서 정의한다.
## 테마 리소스(.tres) 대신 코드로 두어, 값 하나 바꾸면 전체가 따라오게 했다.
const INK := Color(0.96, 0.99, 1.0)
const INK_DIM := Color(0.78, 0.88, 0.94)
const ACCENT := Color(0.62, 0.92, 1.0)
const GOLD := Color(1.0, 0.85, 0.45)
const PANEL_BG := Color(0.03, 0.10, 0.18, 0.82)
const ROW_BG := Color(1, 1, 1, 0.06)
const ROW_BG_HOVER := Color(1, 1, 1, 0.13)
const ROW_BG_DISABLED := Color(0, 0, 0, 0.18)
static func panel(bg: Color = PANEL_BG, radius: int = 14) -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = bg
sb.set_corner_radius_all(radius)
sb.content_margin_left = 12
sb.content_margin_right = 12
sb.content_margin_top = 10
sb.content_margin_bottom = 10
return sb
static func row(bg: Color, radius: int = 10) -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = bg
sb.set_corner_radius_all(radius)
sb.content_margin_left = 10
sb.content_margin_right = 10
sb.content_margin_top = 8
sb.content_margin_bottom = 8
return sb
## 버튼의 네 가지 상태 스타일을 한 번에 입힌다.
static func style_button(b: Button, radius: int = 10) -> void:
b.add_theme_stylebox_override("normal", row(ROW_BG, radius))
b.add_theme_stylebox_override("hover", row(ROW_BG_HOVER, radius))
b.add_theme_stylebox_override("pressed", row(ROW_BG_HOVER, radius))
b.add_theme_stylebox_override("disabled", row(ROW_BG_DISABLED, radius))
b.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
b.add_theme_color_override("font_color", INK)
b.add_theme_color_override("font_hover_color", INK)
b.add_theme_color_override("font_pressed_color", ACCENT)
b.add_theme_color_override("font_disabled_color", Color(0.6, 0.66, 0.72, 0.7))
static func label(l: Label, size: int, color: Color = INK) -> void:
l.add_theme_font_size_override("font_size", size)
l.add_theme_color_override("font_color", color)
# 물 위에서도 글씨가 읽히도록 옅은 외곽선을 준다.
l.add_theme_color_override("font_shadow_color", Color(0, 0.05, 0.1, 0.55))
l.add_theme_constant_override("shadow_offset_x", 0)
l.add_theme_constant_override("shadow_offset_y", 1)
l.add_theme_constant_override("shadow_outline_size", 2)
## 잠긴 구역을 덮는 어두운 베일.
static func veil(alpha: float) -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.01, 0.03, 0.07, alpha)
return sb
+1
View File
@@ -0,0 +1 @@
uid://b7lu7lfy32jd8