Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
고양이 열다섯 마리로 집을 지키는 타워디펜스
Translation pending
GamesJavaScript · CSSGitHub @kimd10041004-gifOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
고양이 열다섯 마리로 집을 지키는 타워디펜스. HTML5 Canvas + PWA + 안드로이드 WebView 앱. 의존성 0개 — 게임 코드는 순수 바닐라 JS ES 모듈이다. 효과음과 배경음은 오디오 파일 없이 WebAudio 오실레이터로 그 자리에서 만들어낸다. 캐릭터는 프레임 스트립 PNG 로 그린다 — 고양이 5마리는 5장(발사 순간 · 되돌아옴 · 거의 제자리 · 대기 · 자는 중), 해충·보스는 3장(걷기 A · 걷기 B · 멈춤). 32종 전부 그림이 붙었다. 그림이 없거나 로드에 실패하면 캔버스 도형으로 떨어져 게임이 그대로 돌아간다 — 그래서 캐릭터 그림을 한 장씩 넣을 수 있다. 지도도 같다. 길과 막힌 칸은 CanvasPattern 으로 칠하므로 경로 기하는 한 글자도 바뀌지 않는다 — 길 그림이 실제 경로와 어긋나는 사고가 구조적으로 일어나지 않는다.Read the full README ↗
고양이 열다섯 마리로 집을 지키는 타워디펜스. HTML5 Canvas + PWA + 안드로이드 WebView 앱.
의존성 0개 — 게임 코드는 순수 바닐라 JS ES 모듈이다. 효과음과 배경음은 오디오 파일 없이 WebAudio 오실레이터로 그 자리에서 만들어낸다.
캐릭터는 프레임 스트립 PNG 로 그린다 — 고양이 5마리는 5장(발사 순간 · 되돌아옴 · 거의 제자리 · 대기 · 자는 중), 해충·보스는 3장(걷기 A · 걷기 B · 멈춤). 32종 전부 그림이 붙었다. 그림이 없거나 로드에 실패하면 캔버스 도형으로 떨어져 게임이 그대로 돌아간다 — 그래서 캐릭터 그림을 한 장씩 넣을 수 있다.
지도도 같다. 길과 막힌 칸은 CanvasPattern 으로 칠하므로 경로 기하는 한 글자도
바뀌지 않는다 — 길 그림이 실제 경로와 어긋나는 사고가 구조적으로 일어나지 않는다.
| 타이틀 | 전투 | 최종 보스 | 상점 |
|---|---|---|---|
공식 사이트 (받는 곳) — https://kimd10041004-gif.github.io/-catpaw-defense/
이 주소에는 GitHub 계정 이름이 그대로 들어 있다. 도메인으로 바꾸려고 준비해 뒀다 (R) — 코드 쪽은
web/js/build.js의FULL_APP_URL한 줄이면 끝나고, 계정 이름이 다시 새면site.test가 잡는다. 남은 것은 도메인을 사는 것뿐이다 (절차는docs/확장가이드.md§17).
| 안드로이드 | APK 한 번 탭 (docs/실기기설치.md) — 사이트가 APK 를 직접 준다. GitHub 을 안 거친다 |
| 아이폰·아이패드·브라우저 | /play/ — 설치하는 앱은 없다. 사파리에서 열고 홈 화면에 추가 (docs/아이폰.md) |
/play/ 는 유료 콘텐츠를 뺀 데모 빌드다. web/ 을 그대로 올리면 데모 결제가 3막·스킨 팩을 공짜로 열기
때문에, tools/demo-build.mjs 가 그 파일들을 빼고 굽는다 — 잠근 게 아니라 없다(tests/node/demo.test.mjs).
사이트 소스는 site/, 배포는 .github/workflows/pages.yml.
node tools/bundle.mjs
# → dist/catpaw-defense.html (220KB, 완전 독립)
브라우저로 그냥 열면 끝이다. ES 모듈 25개를 각자 함수 스코프로 감싸 합쳤기 때문에
file://에서도 CORS에 막히지 않는다. 폰에 파일을 넣어서 열어도 되고, 어디든 올려도 된다.
npx --yes http-server web -p 8080 -c-1
# → http://localhost:8080
file://로 직접 열면 안 된다 — ES 모듈이 CORS로 막힌다. 반드시 HTTP로 서빙해야 한다.
위 서버를 폰과 같은 네트워크에서 띄우거나 GitHub Pages 등에 올린 뒤, 안드로이드 크롬에서 열고 ⋮ → 홈 화면에 추가.
폰에서 한 번 탭해서 (권장) — Releases 의
dev 에 APK 두 개가 올라가 있다. 갱신하려면 Actions → 릴리스 (설치용 APK) → Run workflow.
…-release.apk (com.catpaw.defense) — R8 이 켜진 진짜 빌드. R8 이 뭘 잘못 지웠는지는 기기에서만 드러난다…-debug.apk (com.catpaw.defense.debug) — 릴리스와 나란히 깔리고 세이브도 따로다키스토어 비밀이 없으면 둘 다 디버그 키로 서명되므로 "출처를 알 수 없는 앱 설치"만 허용하면 깔린다.
자세한 건 docs/실기기설치.md — 설치 허용·ADB·chrome://inspect 로 폰 콘솔 보기까지.
Actions 아티팩트로 — 아무 푸시에나 catpaw-defense-release-apk · catpaw-defense-debug-apk 가 남는다.
ZIP 으로 감싸이고 로그인이 필요해 폰에서는 위쪽이 낫다.
Play 에 올리는 AAB — 같은 실행의 catpaw-defense-release-aab. 저장소 비밀
CATPAW_KEYSTORE_B64 · CATPAW_KEYSTORE_PW · CATPAW_KEY_ALIAS · CATPAW_KEY_PW 가 있으면 그 키로,
없으면 디버그 키로 서명한다(빌드 검증용, Play 가 거부한다). 절차 전체는 docs/출시체크리스트.md,
개인정보처리방침은 docs/개인정보처리방침.md. compileSdk/targetSdk 36 · R8 · 리소스 축소 · 서비스 워커 배선은
CI 에서 실제로 컴파일된다(디버그 APK 8.5MB · 릴리스 AAB 6.1MB). 다만 R8 이 뭘 잘못 지웠는지는 기기에서
돌려야만 드러난다 — 그래서 릴리스 APK 를 같이 올린다.
Android Studio로
android/ 폴더를 열고 Run. 또는 명령줄에서:
cd android
./gradlew assembleDebug
# → app/build/outputs/apk/debug/app-debug.apk
web/ 폴더는 빌드할 때마다 Gradle이 자동으로 assets에 복사한다. 웹 코드를 고치면 APK에 그대로 반영된다.
rules(적 체력 배수)를 받아서
뒤로 갈수록 압박이 커진다. 잘 두는 봇(deck · 시드 셋 × 3판) 구간 평균 완주율 1scenario-curve.test 와 같은 방법 — Q 에서 다시 잼). 봇이 한 번도 못 깨는 장은 없다 — 검사가 장마다 바닥을 깐다. 12·18·24장이 22% 로 그 막에서 가장 어렵다. 고양이 세 마리(샴냥·검은냥·뚱냥)가 2·4·6장 보상으로
풀리고, 2막에서는 펫 두 마리(부엉이·너구리)가 14·17장 보상으로 온다. 맵을 18개 만들지 않고
waveSet·waveLimit 으로 맵과 웨이브셋을 조합한다docs/속성과카드.md).
잿불 길은 룬 둘(삼색 불 · 검은 흙)이면 되고, 천둥 고개는 룬 둘(치즈 번개 · 삼색 빛)로 버릴 칸을 고르는 판이며,
서릿길은 얼음 하나면 되고 얼음 도배도 답이다. 상성 배수는 방어력을 빼기 전에 곱한다(L) — 뒤에 곱하면
장갑 앞에서 ×1.5 가 +0.5/발로 눌려 상성이 정작 장갑 상대로 사라졌다node tools/balance-sim.mjs --growth 3 --seed 7) 만렙(+15%)은 치즈냥만 쓰는 바닥 봇의 골목길을 20 → 60%, 부엌을 20 → 100% 로 올리고 지붕부터는 여전히 0% 다 — 앞 맵을 편하게 할 뿐 뒤를 대신 깨 주지 않는다 (K·L 로 난이도가 올라간 뒤 다시 잰 값이다. 그 전 문장은 '골목길~창고를 100% 로 만든다'였는데 K 전의 값이라 오래전에 사실이 아니게 돼 있었다)
아홉 마리 전부 2,520 캣닢 — 캣닢의 장기 소비처다목숨 20개로 시작한다. 해충이 길 끝까지 도달하면 목숨이 줄고, 0이 되면 진다. 30웨이브를 모두 막으면 승리.
고양이와 해충 모두 속성을 하나씩 갖는다. 여섯이 하나의 고리를 이루고, 각 속성은 고리의 다음 속성에게 강하다.
흙 → 번개 → 얼음 → 불 → 어둠 → 빛 → 흙 유리 ×1.5 · 무관 ×1.0 · 불리 ×0.7
빛과 어둠을 서로 강하게 두지 않은 이유가 있다. 그러면 그 둘만 약점이 없어지고, 고양이는 공격만 하므로
약점 없는 속성이 언제나 정답이 된다. 고리면 여섯 전부 강점 1 · 약점 1 · 무관 3 으로 대칭이다
(elements.test 가 이 대칭을 센다).
상성은 원정 모드에서만 걸린다. 자유 모드와 시나리오는 그대로다 — 맵을 깨고 나가는 길이 속성 수집에 걸리면 안 되기 때문이다(아래 과금 절의 무료 범위 약속과 같은 원칙). 도감에는 늘 배지로 보인다.
속성은 룬으로 바꾼다. 도감의 고양이 행 → 속성 칩. 룬은 소모되지 않고, 가진 개수가
"몇 마리에게 동시에 끼울 수 있나"를 뜻한다. 원정이 시작되면 덱과 속성은 굳는다.
한 속성으로 도배하는 것은 함정이다 — 다만 함정이 되게 만드는 데 손이 갔다. 원정 여섯 칸이 여섯 속성을
한 칸씩 쓰므로 같은 속성으로 넷을 채우면 그 속성이 약한 칸에서 네 마리가 전부 불리해진다. 그런데
약점이 어느 칸에 떨어지느냐가 전부였다: 쉬운 칸에 떨어지면 그냥 통과해서, 잿불 길에서 도배 얼음이
83% 를 완주했다(도배 흙은 0%. 같은 '도배'인데 고른 속성으로 갈렸다). 그래서 칸이 보스를 따로 고르고
그 보스는 지배 속성에 안 덮이게 했다 — 칸마다 답할 속성이 둘이 되어 도배 최고 완주율이 83% → 25% 로
내려왔다(그때의 smart 봇 값). 지금 deck 봇 기준(L) 잿불 길은 맞춘 덱 100% · 룬 없음 25% · 도배 최고 흙 67% 다
(사다리 셋의 표는 docs/속성과카드.md "지금 밸런스").
고양이 카드와 속성 룬을 뽑는다. 캣닢은 현금으로도 사므로 이 뽑기는 확률형 아이템이고, 한국 게임산업법이 확률 공개를 의무화한다(2024-03 시행).
그래서 표를 두 벌 만들지 않았다. domain/gacha.js 의 GACHA_TABLE 하나가 굴리는 표이자
화면에 뜨는 표다. gacha.test 가 셋을 고정한다 — 합이 정확히 1.0 · 화면 문구가 표에서 나온다 ·
20만 번 굴린 실측이 표와 맞는다. 마지막 것이 진짜 증거다.
약속 셋:
티켓은 현금으로 못 산다. 출석(하루 1장)과 원정 보상으로만 벌리고, 뽑기는 티켓부터 쓴다 — "결제 없이도 모을 수 있다"는 약속이 뽑기까지 이어진다.
화면은 셋이다: 타이틀의 뽑기(확률 표가 화면의 절반이다) · 도감 카드 탭(가진 카드·조각·룬, 조각 교환) · 고양이 행의 속성 칩에서 여는 룬 시트(6속성 + 타고난 속성). 뽑기 난수는 시드를 안 박는다 — 시드가 있으면 저장을 백업했다 덮어써서 결과를 고를 수 있다.
카드로만 얻는 고양이 6종 — 먼치킨냥(장갑 벗기기) · 벵갈냥(공중 전용) · 노르웨이숲냥(사거리 아우라) ·
터키시앙고라냥(장갑 무시) · 사바나냥(표식) · 메인쿤냥(밀어내기).
도감 고양이 탭에는 여섯도 처음부터 보인다 — 무엇을 노릴지 알아야 뽑기가 선택이 된다. 대신 아직 없는 줄은
흐리게 '카드 필요' 로 표시되고 훈련·룬이 닫힌다(캣닢과 룬 자리가 못 쓰는 고양이에 들어가지 않게). 전부 기존 아홉에 없던 메커니즘이고,
한 방·사거리·초당 피해·값은 전부 기존 1등 아래다 — 뽑기가 무료 고양이를 밀어내지 않게
effects-cards.test 가 넷 다 못 박는다. 자세한 건 docs/속성과카드.md.
업적은 뽑기에 안 걸린다. '아홉 마리 전부'와 '훈련 교관'은 등급이 없는 고양이만 센다.
| 고양이 | 건설 | 특징 |
|---|---|---|
| 🧀 치즈냥 | 80 | 빠른 연사. 싸고 무난하지만 두꺼운 장갑엔 힘을 못 쓴다 |
| 🎨 삼색냥 | 160 | 헤어볼 폭발로 주변까지 타격. 지상만 — 박쥐를 못 잡는다 |
| 💙 샴냥 | 130 | 눈빛으로 최대 65% 둔화. 피해는 약하지만 시간을 벌어준다 |
| 🖤 검은냥 | 240 | 사거리 6칸 저격. 중장갑을 뚫는 유일한 답 |
| ⬜ 뚱냥 | 200 | 사거리 안 전체를 동시 타격. 적이 뭉칠수록 무섭다 |
| 해충 | 체력 | 특징 |
|---|---|---|
| 생쥐 | 32 | 기본 |
| 바퀴 | 26 | 매우 빠르고 떼로 온다 |
| 시궁쥐 | 95 | 방어 2 |
| 박쥐 | 62 | 공중 — 삼색냥 무효 |
| 두더지 | 240 | 방어 8, 둔화 저항 50% |
체력만 많은 보스는 없다. 전부 고유 능력을 갖는다.
| 등급 | 보스 | 체력 | 능력 | 등장 |
|---|---|---|---|---|
| ★ | 쥐왕 👑 | 1,400 | 부하 소환 | 10웨이브 |
| ★★ | 두더지 대장 | 2,600 | 보호막(체력 35%) + 주변 적 방어·속도 강화 | 15웨이브 |
| ★★ | 바퀴 여왕 | 2,100 | 죽으면 새끼 8마리로 분열 + 소환 | 20웨이브 |
| ★★ | 번개 집게벌레 ⚡ | 2,000 | 길을 앞으로 건너뛴다 + 광폭화 | 유리 온실 8·13·19웨이브 |
| ★★ | 서리 지렁이 여왕 ❄ | 2,400 | 맞을수록 장갑이 오른다(최대 +10) + 재생 | 유리 온실 14·17웨이브 |
| ★★ | 눈부신 비둘기 ☀ | 2,200 | 주변 고양이의 사거리를 줄인다 + 소환 · 공중 | 유리 온실 20웨이브 |
| ★★ | 흡혈 박쥐왕 | 2,300 | 공중 + 자가 회복 + 체력 45% 이하에서 광폭화 | 25웨이브 |
| ★★★ | 마왕 쥐 | 9,000 | 보호막 · 재생 · 소환 · 광폭화 · 전투함성 전부 | 30웨이브 |
보스가 화면에 뜨면 상단에 전용 체력바가 나온다. 보호막은 파란 층으로 겹쳐 보인다.
| 필살기 | 효과 | 속성 | 마나 · 쿨다운 |
|---|---|---|---|
| 🍥 츄르 폭격 | 화면의 모든 적에게 큰 피해 (웨이브에 따라 위력 상승) | 무속성 | 60 · 20초 |
| 💤 자장가 | 모든 적 75% 둔화 5초 | — | 35 · 18초 |
| 🥛 우유 홍수 | 지상 적 전체 피해 + 둔화 (공중은 안 맞는다) | ❄ 얼음 | 50 · 22초 |
| ✨ 황금 발바닥 | 모든 고양이 공격 속도 2.2배, 10초 | — | 45 · 25초 |
| 😾 하악질 | 화면의 모든 적 장갑 −4, 6초 — 장갑을 정면으로 맞는 광역기 넷의 약점을 메운다 | — | 40 · 20초 |
| 🌀 헤어볼 | 체력이 가장 높은 적 하나에 큰 피해 — 보스용 한 방 | ☾ 어둠 | 55 · 24초 |
속성 — 피해를 주는 것만 가진다. 여섯 중 피해기는 셋(츄르·우유·헤어볼)이고, 그중 둘에만 속성이 있다. 원정에서만 상성이 걸린다(유리 ×1.5 · 불리 ×0.7). 둔화·버프·장갑 벗기기에는 곱할 피해가 없어 속성이 없다. 츄르 폭격은 일부러 무속성으로 남겼다 — 넷이 다 속성이면 로드아웃 고르기가 "이번 사다리 색 맞추기" 한 줄로 줄어든다. 무속성 한 방이 있어야 맞춰서 크게 와 어느 색이든 안 흔들리게 가 서로 다른 선택이 된다.
로드아웃 — 넷만 들고 들어간다. 도감 필살기 탭에서 장착·빼기·순서(↑↓)를 정하고, 그 순서가 판의 버튼 순서다. 기본은 앞의 넷. 판 중에 바꿔도 HUD 가 바로 따라온다. 성장 — 필살기마다 두 트리. 세기(피해·지속 +10%/단계) · 쿨다운(−10%/단계), 3단계 · 40/80/160 캣닢(프리미엄 20% 할인). 여섯 전부 만렙이면 3,360 캣닢 — 훈련(2,520) 옆의 둘째 장기 소비처다. 밸런스 봇은 기본 로드아웃·0단계로 돈다 — 새 필살기와 성장이 봇 숫자를 한 톨도 안 움직인다(전후 시뮬 출력이 바이트 단위로 같다). (전에 이 표에 적혀 있던 쿨다운 45/40/50/60 은 콘텐츠와 달랐다 — 실제 값은 20/18/22/25 다.)
크리티컬(8% · 2배, 방어력보다 먼저 곱해져 중장갑에도 통한다), 보스 처치 시 히트스톱과 화면 섬광·폭발, 총구 화염, 투사체 꼬리, 광폭화 붉은 기운, 보호막 파괴 연출. 전부 설정의 "화면 흔들림 줄이기" 로 완화할 수 있다.
피해량 = max(1, 공격력 − 방어력)
이것 때문에 치즈냥(공격 12)은 두더지(방어 8) 앞에서 4밖에 못 넣는다. 속사로 다 되지 않는다. 원정의 상성 배수(×1.5 / ×0.7)는 방어력을 빼기 전에 공격력에 곱한다 — 유리하면 관통이 되고 불리하면 더 자주 바닥(1)에 닿는다.
| 적 체력 | 목숨 | 골드 | 캣닢 보상 | 봇 클리어율 (t1~t7) | |
|---|---|---|---|---|---|
| 아깽이 (쉬움) | ×0.75 | ×1.5 | ×1.25 | ×1.0 | 100 · 100 · 100 · 100 · 100 · 100 · 100% |
| 집냥이 (보통) | ×1.03 | ×1.0 | ×1.00 | ×1.0 | 100 · 100 · 100 · 89 · 0 · 0 · 0% (smart) — deck 봇은 100 · 100 · 100 · 100 · 56 · 44 · 22% |
| 길냥이 (어려움) | ×1.35 | ×0.85 | ×1.00 | ×1.5 | 11 · 33 · 11 · 0 · 0 · 0 · 0% |
완주율이 100% 인 칸은 아직 안 재진 칸이다 (M). 그래서 같은 판의 남은 목숨(lives / maxLives)을 같이 낸다 —
deck 봇 · 보통 · 시드 셋 기준 0.80 · 0.40 · 0.47 · 0.33 · 0.09 · 0.07 · 0.00. 완주율로는 티어 1~4 가 한 덩어리인데
목숨으로 보면 사다리가 보인다. 시뮬레이터 표와 --json 에 '남은 목숨' 칸이 있다.
자유 맵 사다리는 L 에서 계단이 됐다(보통 · deck 봇 · 시드 셋). 전에는 티어 14 가 100%, 5·6 이 0%, 7 이 11% 로
벽이었다. 지하실 1.78 이 전부 22% 인 자리를 골랐다.
사람에겐 다를 수 있다 — 봇 기준이다.hpMul 1.20 → 0.95, 다락방 0.72/420 → 0.60/480(시작 골드로 초반을 산다), 온실 1.80 → 1.76 으로
100 · 100 · 100 · 100 · 56 · 44 · 22%. 검사가 이웃 티어 사이 50%p 넘는 절벽을 막는다(고치기 전엔 창고 100 → 지하실 0 에서
빨갰다). 온실은 절벽이 가팔라(1.80 이 0%, 1.70 이 56%) 1.74
M 에서 윗칸을 고쳤다. L 은 "창고는 손잡이가 없다(1.20 까지 올려도 100%)"로 끝냈는데 그건 완주율로만 잰 값이었다. 남은 목숨으로 다시 보니 지붕·창고가 아홉 판 전부 목숨을 하나도 안 잃고 끝나 있었다 — 티어 3·4 가 공짜였다. 창고를 1.05/300 → 1.15/440 으로 옮겨 남은 목숨 1.00 → 0.33(시드 셋 전부 같은 값), 완주율은 100% 그대로다. 새 검사가 "잘 두는 봇이 매번 깨는 구간의 마지막 맵은 목숨을 15% 는 내놓는다" 를 못 박는다. 창고를 쓰는 8·9·16·21장은 실효 난이도가 그대로이게 보정했다.
Q 에서 지붕을 고쳤다 — 손잡이가 체력도 마릿수도 아니었다. M 은 hpMul 로 실패했고, 다음으로 잡은 웨이브 표도
실패했다: ROOFTOP_30 뒷줄에 두더지 1,680마리를 더 넣어도 남은 목숨이 1.00 에서 안 움직인다(박쥐왕 60마리를
한 줄에 넣으면 그제야 전멸한다 — 절벽이 아니라 벽이다). 원인은 길 모양이었다. 지붕은 길이 46칸에 꺾임 13번인
지그재그라 타워 한 채가 여러 줄을 동시에 보고, 봇이 49채를 짓는다(창고는 32칸·9꺾임·46채). 그래서 손잡이는
적의 양이 아니라 타워가 설 자리다 — 2·6·10행을 막아 지을 수 있는 칸을 79 → 55 로 줄이자 남은 목숨이
1.00 → 0.47, 완주율은 100% 그대로다(시드를 통째로 바꿔도 0.47). 새 검사가 "완주율 100% 인 맵 중 첫 맵보다
후한 맵은 없다" 를 못 박는다 — M 의 검사는 구간의 마지막 칸만 봐서 가운데가 솟은 것을 못 잡았다.
지붕을 쓰는 7·20장과 잿불 길·천둥 고개의 지붕 칸 넷은 전 값으로 보정했다. 쓸이표는 docs/확장가이드.md §5.
부엌(t2 · 0.40)이 지붕(0.47)보다 여전히 조금 어렵다 — kitchen30 마지막 웨이브를 건드려야 하는 별개의 일이다.
보통 1.00 → 1.03, 어려움 1.20 → 1.35 로 올렸다(2026-09). 근거는 봇이 아니라 사람이다 — 직접 해 보고 "지금 난이도 쉬움, 시나리오도 생각보다 쉬움, 조금씩 높여야 함"이라고 했다. 0.05 씩 올리며 기존 난이도 검사 넷이 빨개지는 지점을 상한으로 잡았다: 보통은 1.05 에서 맵 순서가 깨졌고, 어려움은 1.40 에서 창고 첫 실점이 2웨이브가 됐다. 아깽이는 안 건드렸다. 프리셋은 사실 약한 손잡이다 — 잘 두는 봇 기준 자유 맵은 체력을 20% 올려도 완주율이 그대로다. 그래서 같은 변경에서 장별·맵별 조율을 같이 했다(아래 시나리오·유리 온실).
난이도는 골드를 깎지 않는다. 길냥이는 원래 골드 ×0.85 였고 그래서 놀 수가 없었다 —
첫 실점이 4~6웨이브였고, 실점이 보스가 아니라 잡몹 웨이브에 흩어져 있었다. "어렵다"가 아니라
"시작하자마자 무너진다"다. 초반 골드 삭감은 복리가 붙는다: 첫 두세 마리를 못 산다 → 샌다 →
목숨이 준다 → 더 못 산다. 골드 배수만 1.00 으로 돌리자 부엌 도달 중앙이 5 → 30웨이브로 뛰었다.
그래서 난이도는 적 체력과 목숨으로만 가른다. 검사가 이걸 지킨다(balance-sim.test).
어려움에는 고를 이유가 있어야 하므로 캣닢을 1.5배 준다. 쉬움에는 벌점이 없다(×1.0) — "결제 없이도 모을 수 있다"는 약속을 난이도로 깨지 않는다.
클리어율은 node tools/balance-sim.mjs --policy smart --specials 로 잰 봇 기준(8판·시드 7)이라
사람보다 비관적이다. 봇은 조합·판매·소모품·표적 전환을 안 쓴다.
기기 언어가 한국어가 아니면 영어로 뜬다(설정 → 언어: 기기 언어 / 한국어 / English). 문구는 코드 곳곳의
tr('한국어 원문') 과 콘텐츠 정의의 이름·설명·대사가 그대로 사전의 키이고, 영어는 web/js/i18n/en.js 한 파일이다.
사전에 없는 문구는 한국어 원문이 그대로 보인다(빈 칸이나 키 이름을 보이지 않는다). node tools/i18n-scan.mjs 가
감싸지 않은 한글 · 사전에 없는 키 · 안 쓰는 키 · 자리표시자 불일치를 세고, 검사(i18n.test)가 전부 0 을 강제한다.
스모크의 영어 패스는 로딩부터 결과 시트까지 실제 화면 텍스트에 한글이 없는지 본다. 영어 번역은 원어민 감수 전이다.
프리미엄 재화 캣닢은 플레이만으로 모인다: 보스 처치(등급별 2/4/6), 5웨이브마다 2, 맵 클리어 20, 도전·주간 도전 첫 클리어 15~30. 한 판을 다 깨면 52개가 모이고, 이어하기 한 번이 50이다.
| 캣닢으로 | 실제 결제로 |
|---|---|
| 이어하기 · 골드 러시 · 필살기 충전 · 목숨 보충 · 펫 · 훈련(고양이당 3단계) · 필살기 성장(6종 × 세기·쿨다운 3단계) · 스킨 3종 | 캣닢 충전 2종 · 프리미엄 팩 · 스타터 팩 · 시나리오 3막 · 도전 팩 2 · 스킨 팩 2개 |
무료 범위 (약속): 자유 모드 6맵 · 시나리오 12막(18장) · 도전 5종 · 펫 · 훈련 · 필살기 로드아웃·성장 · 무한 모드 · 주간 도전.
결제 없이 전 맵을 클리어할 수 있게 설계했고, 그 조건은 검사가 지킨다.
유료: 시나리오 3막(1924장, 새 웨이브 구성 2개) · 도전 팩 2(규칙 5) · 스킨 팩(겉모습만, 능력치 없음) · 스타터 팩.
유료 콘텐츠는 무료 콘텐츠를 잠그지 않고(content.test), 유료 전용 타워·맵·보스는 없다.
지금은 실제 청구가 일어나지 않는다. 웹은 "데모 결제 (실제 청구 없음)",
안드로이드는 "Google Play 결제 (미설정)"으로 표시되고 결제 시도는 정직하게 실패한다.
켜는 방법은 docs/결제연동.md에 정리했다.
web/js/domain/ DOM을 전혀 모르는 순수 로직 — node --test 대상
(balance, waves, economy, path, targeting, status, settings, save,
shop, billing, mana, elite, frames, objectives)
web/js/content/ 고양이·적·보스능력·필살기·맵·웨이브 데이터 + 레지스트리
web/js/ sprites / framesets / mapart / render / game / ui / audio / main
web/art/ 프레임 아트 스트립 29장 (고양이 5프레임 · 해충 3프레임, 칸은 169~396px
으로 발주 차수마다 다르다 — framesets.js 의 body 가 화면 크기를 맞춘다)
+ 지도 길 질감·소품.
없으면 sprites.js / 단색 렌더링으로 폴백
art-src/ 그림 원본 시트 (슬라이스를 다시 돌릴 수 있게 보관)
tests/node/ 단위 테스트 (의존성 0, node 내장 러너)
tools/ 아이콘 생성기, 시트 슬라이서, 모션 시트, 헤드리스 실행 검증기
android/ Kotlin + WebView 래퍼 (APK 소스)
docs/확장가이드.md ★ 새 고양이·적·보스능력·필살기·맵·설정·상품 추가하는 법
docs/결제연동.md 실제 Google Play 결제를 켜는 절차
모든 콘텐츠가 데이터다. 새 고양이 한 마리를 추가할 때 game.js/render.js/ui.js를 건드리지 않는다.
자세한 방법은 docs/확장가이드.md.
부팅할 때 validateAll()이 참조 무결성(미등록 스프라이트·효과·적·웨이브셋, 성립하지 않는 경로)을
전부 검사하므로, 잘못 추가한 콘텐츠는 조용히 깨지지 않고 한국어 에러로 즉시 드러난다.
npm test # 단위 테스트 (node --test, 의존성 0)
npm run serve # 로컬 서버
node tools/bump-version.mjs patch # 버전 올리기 — package.json · version.js · gradle · sw.js 캐시 이름을 함께
배포마다 최소 patch 를 올린다(에셋만 바뀌어도). 서비스 워커 캐시 이름이 버전을 따르므로 안 올리면 설치된 PWA 가
옛 파일을 계속 쓴다. version.test 가 다섯 곳이 같은지 대조한다.
실행 검증 (헤드리스 크로미움으로 실제 조작 + 스크린샷, playwright 필요):
NODE_PATH=/opt/node22/lib/node_modules node tools/screenshot.mjs
# → tools/out/*.png, 콘솔 에러가 1건이라도 있으면 실패로 끝난다
밸런스 곡선 (웨이브셋의 모양 — 절벽·공백을 숫자로, 자동 플레이어의 실점까지):
node tools/curve-report.mjs # 셋 전부 — 규칙 위반만
node tools/curve-report.mjs --map alley --runs 5 --seed 7 --policy mixed # 표 + 실점 곡선
node tools/balance-sim.mjs --seed 7 --runs 5 # 맵 × 난이도 자동 플레이 (--policy cheese|mixed|smart)
서브경로 검증 (안드로이드는 게임을 /assets/에 서빙하므로 루트가 아니다):
NODE_PATH=/opt/node22/lib/node_modules node tools/subpath-check.mjs
절대경로가 섞여 들어가면 웹은 멀쩡한데 APK만 빈 화면이 되는데, 이 검사가 그 사고를 잡는다.
아이콘 다시 만들기 (web/icons/icon.svg를 고친 뒤):
NODE_PATH=/opt/node22/lib/node_modules node tools/make-icons.mjs
그림 다시 굽기 / 모션 눈으로 확인:
NODE_PATH=/opt/node22/lib/node_modules node tools/slice-sheet.mjs # 원본 시트 → web/art/*.png
NODE_PATH=/opt/node22/lib/node_modules node tools/pose-sheet.mjs # 게임과 같은 방식으로 모션 렌더
발사 모션은 0.17초라 일반 스크린샷으로는 한 프레임도 못 잡는다. 그래서 단계별로
격자에 늘어놓는 도구를 따로 둔다 (tools/out/12-poses.png).
새 캐릭터 그림을 넣는 절차는 docs/확장가이드.md §3-b,
지도 길 질감·소품은 §5-b.