Show GN: 3일만에 antigravity로 만들어본 타워디펜스
(tower.dsp.ai.kr)원래는 claude code와 cursor를 쓰다가 작년 퇴사 후 11월부터 antigravity로 갈아타고나서 이것저것 토이 웹서비스 프로젝트를 하루마다 만들고 있었는데, 갑자기 3일전(1/2)에 타워디펜스 게임을 만들어 보고 싶어졌습니다.
원래 웹frontend개발자라 앱으로 감싸기는 귀찮고, 그냥 데스크탑 브라우저 환경에서만 동작하게 하고, 캔버스 기반 동작 및 그림파일 작업은 너무 시간이 오래걸릴것 같아서 이모지를 최대한 활용하자는 아이디어로 시작했습니다.
하루만에 끝내려고 했는데, 만들다보니 재미있어서 이것저것 수정/추가하다보니 3일이나 걸렸네요. ㅠㅜ
기본적인 기술스택은 다음과 같습니다.
- Frontend: Next.js 14+ (App Router), TypeScript, HTML5 Canvas
- Backend: FastAPI, Python 3.9+, Pydantic
- Database: Supabase (PostgreSQL)
- Deployment: Vercel
이런저런 수정을 거쳐 최종적으로 게임로직은 다음과 같이 결정되었습니다.
1. 타워 공격력
타워는 머지(Merge)를 통해 레벨이 오를 때마다 사거리(Range)가 10씩 증가하지만, 공격력 효율은 1.8배로 조정됩니다 (밸런스).
-
공식:
Damage = floor(BaseDamage * (1.8 ^ (Level - 1)))- Lv 1: 10
- Lv 2: 18
- Lv 3: 32
- Lv 4: 57
2. 적 등장 패턴 (Wave Logic)
단조로움을 없애기 위해 다양한 웨이브 패턴이 적용됩니다.
- 일반 웨이브 (Horde Mix): 현재 티어 적(70%)과 이전 티어 적(30%)이 섞여서 등장합니다.
-
보스 웨이브 (Every 6th): 보스는 시대의 최강 적 1마리가 등장하며, 주변에 쫄병들이 호위합니다.
- 보스 웨이브의 적 수:
Wave / 6(웨이브 6에 1마리, 웨이브 12에 2마리...)
- 보스 웨이브의 적 수:
4. 골드 보상 (Active)
적을 처치하거나 웨이브를 클리어하면 골드를 획득합니다.
ps. 모바일 환경은 크게 고려하지 않았습니다. 터치이벤트를 추가했으나 폴드나 패드에서는 잘 되지만 작은 화면에서의 고려는 그다지 안했습니다~
ps. 개선점 및 버그는 댓글로 알려주시면 시간날때마다 반영하겠습니다.
안녕하세요 재밌게 플레이했습니다, 보안관련 피드백 드립니다.
- 현재 HMAC 시크릿(서명용 키)이 프론트엔드에 노출되어 있습니다
- 프론트에서 직접적으로 gold, wave, lives, towers 를 변경할수있습니다
vercel 과 supabase 요금제문제가 있을수있어서 알려드립니다
const state = {
towers: [],
gold: 9999,
wave: 1,
lives: 1
};
const state_raw = JSON.stringify(state);
const user_id = "11111";
const secret = "11111";
(async () => {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signatureBuffer = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(state_raw)
);
const signature = Array.from(new Uint8Array(signatureBuffer))
.map(b => b.toString(16).padStart(2, "0")).join("");
fetch("https://tower.dsp.ai.kr/api/sync", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "*/*"
},
body: JSON.stringify({
user_id,
state_raw,
signature
})
})
.then(res => res.text())
.then(console.log)
.catch(console.error);
})();