5 Commits

Author SHA1 Message Date
43c8ccd6d2 release: v2.0.3 (versionCode 5) 2026-09-29 13:22:55 +09:00
f8c6bfab49 조작부: 하드 드롭을 반으로 줄이고 옆에 홀드 배치, 회전 버튼 확대
아랫줄을 하드 드롭 + HOLD 로 정확히 반씩 나눴다(실측 각 276px).
윗줄에서 HOLD 가 빠져 생긴 한 칸은 ↺·↻ 가 나눠 가졌다. 180° 는 폭을
유지해야 해서 4등분 중 1칸(weight 1)을 그대로 두고 회전 두 개에 1.5 씩
줬다 — 실측으로 180° 는 127 → 132px(간격 하나가 줄어든 만큼), ↺·↻ 는
126 → 198px 이 됐다.

검증: 새 위치에서 HOLD 탭 시 홀드 패널 유채색 0 → 5476, 하드 드롭 탭 시
보드 43377px·넥스트 47948px 갱신, 회전 3종 모두 탭 직후 보드 변화 확인.
엔진 테스트 28개 통과. 바뀐 조작부가 찍힌 스토어 스크린샷 2장 교체.
2026-09-29 13:22:50 +09:00
5e674fa686 release: v2.0.2 (versionCode 4) 2026-09-29 13:10:39 +09:00
fe13a61c09 조작 버튼 재배치 + 홀드/넥스트 패널이 갱신되지 않던 버그 수정
세로 화면 조작부를 요청대로 바꿨다. 회전(↺ ↻ 180°)과 HOLD 를 윗줄로 올리고,
하드 드롭은 맞은편 소프트 드롭과 같은 자리(맨 아래 가로로 긴 낮은 버튼)로
내렸다. 한 줄에 버튼이 4개가 되어 액션 컬럼 폭을 1.1 → 1.35 로 늘렸다.
1.1 로는 버튼이 세로로 길쭉해져 엄지로 누르기 불편했다.

그 과정에서 HOLD 를 눌러도 패널이 그대로인 것을 발견했다. game.hold 와
game.peekNext() 는 Compose 가 관찰하지 못하는 평범한 엔진 필드인데,
BoardCanvas 와 달리 패널 쪽은 state.version 을 구독하지 않아 게임 시작
시점의 넥스트 큐가 끝까지 멈춰 있었다. 넥스트 미리보기는 이 장르의 핵심이라
출시 전에 잡아야 하는 버그다. 패널을 SidePanels 로 분리해 version 을
구독시켰다 — 재구성 범위를 패널 서브트리로 좁혀 매 프레임
BoxWithConstraints 측정이 다시 돌지 않게 했다.

검증: 에뮬레이터에서 HOLD 탭 후 패널 유채색 픽셀 0 → 5474, 넥스트 패널은
하드 드롭 뒤 49370 픽셀 변화(수정 전에는 보드가 116848 픽셀 바뀌는 동안
넥스트는 0 픽셀이었다). 회전 3종·하드 드롭·소프트 드롭 모두 동작 확인.
세로/가로 양쪽 확인. 엔진 테스트 28개 통과.

바뀐 조작부가 찍힌 스토어 스크린샷 3장도 다시 촬영했다.
2026-09-29 13:10:31 +09:00
da0f355b98 release.sh: Gitea 푸시에 토큰 인증 붙이기
자격증명 헬퍼가 없어 'could not read Username' 으로 푸시가 실패했다.
토큰을 URL 에 실어 보낸다.
2026-09-24 16:29:46 +09:00
6 changed files with 41 additions and 13 deletions

View File

@@ -181,14 +181,33 @@ private fun BoardWithPanels(state: GameState, ghostOn: Boolean, l: Layout, stack
BoardCanvas(state, ghostOn) BoardCanvas(state, ghostOn)
} }
Column(Modifier.width(panelW)) { Column(Modifier.width(panelW)) {
SidePanel("HOLD", listOfNotNull(state.game.hold), s, 1, l.isTiny) SidePanels(state, s, l.isTiny)
Spacer(Modifier.height((8 * s).dp))
SidePanel("NEXT", state.game.peekNext(if (l.isTiny) 3 else 5), s, if (l.isTiny) 3 else 5, l.isTiny)
} }
} }
} }
} }
/**
* HOLD / NEXT 패널.
*
* `game.hold` 와 `game.peekNext()` 는 Compose 가 관찰하지 못하는 평범한 엔진
* 필드라, 값이 바뀌어도 그것만으로는 재구성이 일어나지 않는다. BoardCanvas 와
* 똑같이 여기서 `state.version` 을 읽어 구독해야 한다. 이 구독이 없으면 게임이
* 시작될 때의 넥스트 큐가 끝까지 그대로 멈춰 있는다.
*
* 보드 전체가 아니라 패널만 따로 떼어 둔 이유는, 재구성 범위를 이 서브트리로
* 좁혀 매 프레임 BoxWithConstraints 측정까지 다시 돌지 않게 하기 위해서다.
*/
@Composable
private fun SidePanels(state: GameState, s: Float, tiny: Boolean) {
@Suppress("UNUSED_EXPRESSION")
state.version
val nextCount = if (tiny) 3 else 5
SidePanel("HOLD", listOfNotNull(state.game.hold), s, 1, tiny)
Spacer(Modifier.height((8 * s).dp))
SidePanel("NEXT", state.game.peekNext(nextCount), s, nextCount, tiny)
}
@Composable @Composable
private fun BoardCanvas(state: GameState, ghostOn: Boolean) { private fun BoardCanvas(state: GameState, ghostOn: Boolean) {
@Suppress("UNUSED_EXPRESSION") @Suppress("UNUSED_EXPRESSION")
@@ -307,15 +326,20 @@ private fun ControlPad(state: GameState, lefty: Boolean, scale: Float, compactHe
horizontalArrangement = Arrangement.spacedBy((8 * scale).dp), horizontalArrangement = Arrangement.spacedBy((8 * scale).dp),
) { ) {
val actions: @Composable (Modifier) -> Unit = { m -> val actions: @Composable (Modifier) -> Unit = { m ->
// 회전은 위쪽 한 줄, 아래는 하드 드롭 + 홀드를 반씩 나눈다.
// 두 컬럼의 행 높이 비율(1.4 : 1)을 맞춰야 좌우가 나란히 떨어진다.
Column(m.fillMaxHeight(), verticalArrangement = Arrangement.spacedBy((8 * scale).dp)) { Column(m.fillMaxHeight(), verticalArrangement = Arrangement.spacedBy((8 * scale).dp)) {
Row(Modifier.weight(1f).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy((8 * scale).dp)) { Row(Modifier.weight(1.4f).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy((8 * scale).dp)) {
CtrlButton("HOLD", Modifier.weight(2f).fillMaxHeight(), scale) { state.hold() } // HOLD 가 아랫줄로 내려가며 빈 한 칸을 회전 두 개가 나눠 갖는다.
// 180° 는 폭을 유지해야 하므로 4등분 중 1칸(weight 1)을 그대로
// 두고, ↺·↻ 가 1.5 씩 받아 3/8 폭이 된다.
CtrlButton("↺", Modifier.weight(1.5f).fillMaxHeight(), scale) { state.rotate("ccw") }
CtrlButton("↻", Modifier.weight(1.5f).fillMaxHeight(), scale) { state.rotate("cw") }
CtrlButton("180°", Modifier.weight(1f).fillMaxHeight(), scale) { state.rotate("180") } CtrlButton("180°", Modifier.weight(1f).fillMaxHeight(), scale) { state.rotate("180") }
} }
Row(Modifier.weight(1f).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy((8 * scale).dp)) { Row(Modifier.weight(1f).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy((8 * scale).dp)) {
CtrlButton("↺", Modifier.weight(1f).fillMaxHeight(), scale) { state.rotate("ccw") } CtrlButton("하드 드롭", Modifier.weight(1f).fillMaxHeight(), scale, accent = true) { state.hardDrop() }
CtrlButton("↻", Modifier.weight(1f).fillMaxHeight(), scale) { state.rotate("cw") } CtrlButton("HOLD", Modifier.weight(1f).fillMaxHeight(), scale) { state.hold() }
CtrlButton("하드\n드롭", Modifier.weight(1f).fillMaxHeight(), scale, accent = true) { state.hardDrop() }
} }
} }
} }
@@ -328,11 +352,13 @@ private fun ControlPad(state: GameState, lefty: Boolean, scale: Float, compactHe
HoldCtrl("▼ 소프트", Modifier.weight(1f).fillMaxWidth(), scale, { state.setSoft(true) }, { state.setSoft(false) }) HoldCtrl("▼ 소프트", Modifier.weight(1f).fillMaxWidth(), scale, { state.setSoft(true) }, { state.setSoft(false) })
} }
} }
// 액션쪽이 한 줄에 버튼 4개라 폭을 더 준다. 1.1 이면 버튼이 세로로 길쭉해져
// 엄지로 누르기 불편했다.
if (lefty) { if (lefty) {
moves(Modifier.weight(1f)) moves(Modifier.weight(1f))
actions(Modifier.weight(1.1f)) actions(Modifier.weight(1.35f))
} else { } else {
actions(Modifier.weight(1.1f)) actions(Modifier.weight(1.35f))
moves(Modifier.weight(1f)) moves(Modifier.weight(1f))
} }
} }

View File

@@ -10,5 +10,5 @@ kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx2g kotlin.daemon.jvmargs=-Xmx2g
# 버전은 CI/로컬에서 -PappVersionName -PappVersionCode 로 덮어쓸 수 있다. # 버전은 CI/로컬에서 -PappVersionName -PappVersionCode 로 덮어쓸 수 있다.
appVersionName=2.0.1 appVersionName=2.0.3
appVersionCode=3 appVersionCode=5

View File

@@ -60,7 +60,9 @@ git tag -a "v$new_name" -m "TETRIG v$new_name"
TOKEN="${GITEA_TOKEN:-$(node -e 'console.log(require(process.env.HOME+"/.config/ejclaw/secrets.json").credentials["git.tkrmagid.kr"].token)' 2>/dev/null || true)}" TOKEN="${GITEA_TOKEN:-$(node -e 'console.log(require(process.env.HOME+"/.config/ejclaw/secrets.json").credentials["git.tkrmagid.kr"].token)' 2>/dev/null || true)}"
[ -n "$TOKEN" ] || { echo "GITEA_TOKEN 이 없습니다"; exit 1; } [ -n "$TOKEN" ] || { echo "GITEA_TOKEN 이 없습니다"; exit 1; }
git push "$GITEA_HOST/$REPO.git" HEAD:main --tags # 토큰을 URL 에 넣어 푸시한다. 그냥 호스트만 주면 자격증명 헬퍼가 없어
# "could not read Username" 으로 죽는다. URL 은 로그에 남지 않게 변수로만 쓴다.
git push "https://oauth2:${TOKEN}@${GITEA_HOST#https://}/$REPO.git" HEAD:main --tags
echo "Gitea 릴리스 생성..." echo "Gitea 릴리스 생성..."
REL_ID=$(curl -sS -X POST "$GITEA_HOST/api/v1/repos/$REPO/releases" \ REL_ID=$(curl -sS -X POST "$GITEA_HOST/api/v1/repos/$REPO/releases" \

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 64 KiB