Add tank stage 5 with forest cover and a commander tank

Water in stage 4 stops tanks; forest is its opposite - tanks and shells
both pass, but the canopy draws over the tanks so anything sitting in it
is hidden. It needs no collision change at all, only a draw pass after
the tanks.

The enemy roster ends on a commander: 3 hits to kill, faster, fires more
often, worth 300. Kinds carry a worth so the score follows the roster
rather than a flat 100. Crates here can also grant 10s of 3-shot, faster
fire, and five enemies hold the field at once.

Enemy pips shrank to 9px so twelve of them still leave the label room.

Checked headlessly: stage 5 keeps 125 reachable cells and leaves the base
standing in 6 of 10 idle runs; the power badge fires from a crate; and
with the roster reordered in memory the commander pays 300 against 100
for the rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 17:08:45 +09:00
co-authored by Claude Opus 5
parent 440af0df2a
commit 353197fae7
3 changed files with 80 additions and 15 deletions
+10 -6
View File
@@ -29,13 +29,14 @@
| ↳ | `/tank.html#2` | 2단계 미로 | 6대 (동시 4) | 밀도 43%, 1칸 폭 통로가 얽힌 미로 | | ↳ | `/tank.html#2` | 2단계 미로 | 6대 (동시 4) | 밀도 43%, 1칸 폭 통로가 얽힌 미로 |
| ↳ | `/tank.html#3` | 3단계 강철 회랑 | 8대 (동시 4) | 강철 31칸, 기지 외곽 2중 + 좁은 진입로 | | ↳ | `/tank.html#3` | 3단계 강철 회랑 | 8대 (동시 4) | 강철 31칸, 기지 외곽 2중 + 좁은 진입로 |
| ↳ | `/tank.html#4` | 4단계 참호 | 10대 (동시 4) | **물길** + **보급 상자**(시간 정지 / 기지 강화) | | ↳ | `/tank.html#4` | 4단계 참호 | 10대 (동시 4) | **물길** + **보급 상자**(시간 정지 / 기지 강화) |
| ↳ | `/tank.html#5` | 5단계 최후 방어선 | 12대 (동시 5) | **숲**(은폐) + **화력 강화** 상자 + **지휘 탱크** |
방향키 이동, 스페이스 발사. 한 단계를 돌파하면 다음 단계로 이어지고, 점수와 남은 탱크를 방향키 이동, 스페이스 발사. 한 단계를 돌파하면 다음 단계로 이어지고, 점수와 남은 탱크를
그대로 가져간다. **단계를 거치지 않고 바로 들어가는 방법**은 셋이다. 그대로 가져간다. **단계를 거치지 않고 바로 들어가는 방법**은 셋이다.
- 왼쪽 탭의 `휴게실 > 탱크게임` 아래 단계 칩 (셸 주소로는 `/#tank2` … `/#tank4`) - 왼쪽 탭의 `휴게실 > 탱크게임` 아래 단계 칩 (셸 주소로는 `/#tank2` … `/#tank5`)
- 게임 화면 오른쪽 아래의 단계 버튼, 또는 `1` ~ `4` 키 - 게임 화면 오른쪽 아래의 단계 버튼, 또는 `1` ~ `5` 키
- 주소 직접 입력 — `/tank.html#4`, `/tank.html?stage=4` (범위를 벗어난 값은 1단계) - 주소 직접 입력 — `/tank.html#5`, `/tank.html?stage=5` (범위를 벗어난 값은 1단계)
## 특수 요소 ## 특수 요소
@@ -46,9 +47,12 @@
| 요소 | 동작 | 쓰인 단계 | | 요소 | 동작 | 쓰인 단계 |
|---|---|---| |---|---|---|
| 물길 `W` | 탱크는 건널 수 없고 포탄은 넘어간다. 건너려면 도선(ford)으로 돌아야 한다 | 4 | | 물길 `W` | 탱크는 건널 수 없고 포탄은 넘어간다. 건너려면 도선(ford)으로 돌아야 한다 | 4 |
| 보급 상자 | 일정 주기로 빈 칸에 하나 놓인다. 밟으면 효과 + 50점 | 4 | | 숲 `F` | 탱크도 포탄도 지나가지만 잎이 탱크를 덮어 가린다. 매복이 된다 | 5 |
| ↳ 시간 정지 | 적 전원이 3초간 멈춘다 (포탄은 계속 난다) | 4 | | 보급 상자 | 일정 주기로 빈 칸에 하나 놓인다. 밟으면 효과 + 50점 | 4, 5 |
| ↳ 기지 강화 | 기지 외곽 5칸이 10초간 강철이 된다. 끝나면 벽돌로 돌아오며 부서졌던 칸도 메워진다 | 4 | | ↳ 시간 정지 | 적 전원이 3초간 멈춘다 (포탄은 계속 난다) | 4, 5 |
| ↳ 기지 강화 | 기지 외곽 5칸이 10초간 강철이 된다. 끝나면 벽돌로 돌아오며 부서졌던 칸도 메워진다 | 4, 5 |
| ↳ 화력 강화 | 10초간 동시 3발 + 탄속 1.35배 | 5 |
| 지휘 탱크 | 편성 마지막에 나오는 대장. 체력 3, 빠르고 연사가 잦다. 격파 300점 | 5 |
효과가 걸린 동안에는 화면 왼쪽 위에 남은 시간이 배지로 표시된다. 효과가 걸린 동안에는 화면 왼쪽 위에 남은 시간이 배지로 표시된다.
+3 -1
View File
@@ -175,6 +175,7 @@ body{
<a href="tank.html#2" target="stage" data-key="tank2" title="2단계 미로">2단계</a> <a href="tank.html#2" target="stage" data-key="tank2" title="2단계 미로">2단계</a>
<a href="tank.html#3" target="stage" data-key="tank3" title="3단계 강철 회랑">3단계</a> <a href="tank.html#3" target="stage" data-key="tank3" title="3단계 강철 회랑">3단계</a>
<a href="tank.html#4" target="stage" data-key="tank4" title="4단계 참호 — 물길·보급 상자">4단계</a> <a href="tank.html#4" target="stage" data-key="tank4" title="4단계 참호 — 물길·보급 상자">4단계</a>
<a href="tank.html#5" target="stage" data-key="tank5" title="5단계 최후 방어선 — 숲·지휘 탱크">5단계</a>
</div> </div>
</div> </div>
@@ -201,7 +202,8 @@ var PAGES = {
tank1: {file:'tank.html#1', name:'휴게실 · 탱크게임 1단계'}, tank1: {file:'tank.html#1', name:'휴게실 · 탱크게임 1단계'},
tank2: {file:'tank.html#2', name:'휴게실 · 탱크게임 2단계'}, tank2: {file:'tank.html#2', name:'휴게실 · 탱크게임 2단계'},
tank3: {file:'tank.html#3', name:'휴게실 · 탱크게임 3단계'}, tank3: {file:'tank.html#3', name:'휴게실 · 탱크게임 3단계'},
tank4: {file:'tank.html#4', name:'휴게실 · 탱크게임 4단계'} tank4: {file:'tank.html#4', name:'휴게실 · 탱크게임 4단계'},
tank5: {file:'tank.html#5', name:'휴게실 · 탱크게임 5단계'}
}; };
var DEFAULT = 'korean'; var DEFAULT = 'korean';
+67 -8
View File
@@ -49,8 +49,11 @@
} }
.row:last-of-type{border-bottom:0} .row:last-of-type{border-bottom:0}
.row b{font-size:16px; color:#fff; font-variant-numeric:tabular-nums} .row b{font-size:16px; color:#fff; font-variant-numeric:tabular-nums}
.pips{display:flex; gap:4px; align-items:center} .pips{
.pip{width:12px;height:12px;border-radius:2px;background:#dcc76d;box-shadow:inset 0 -3px 0 rgba(0,0,0,.28)} display:flex; flex-wrap:wrap; gap:3px; align-items:center;
justify-content:flex-end; max-width:152px;
}
.pip{width:9px;height:9px;border-radius:2px;background:#dcc76d;box-shadow:inset 0 -2px 0 rgba(0,0,0,.28)}
.pip.e{background:#b9c2cc} .pip.e{background:#b9c2cc}
.pip.off{background:#463728; box-shadow:none} .pip.off{background:#463728; box-shadow:none}
.keys{margin-top:11px; font-size:12px; line-height:1.72; color:#cbb79a} .keys{margin-top:11px; font-size:12px; line-height:1.72; color:#cbb79a}
@@ -122,7 +125,7 @@
var COLS=17, ROWS=15, CELL=32; var COLS=17, ROWS=15, CELL=32;
var W=COLS*CELL, H=ROWS*CELL; var W=COLS*CELL, H=ROWS*CELL;
var SIZE=28, OFF=(CELL-SIZE)/2; var SIZE=28, OFF=(CELL-SIZE)/2;
var EMPTY=0, BRICK=1, STEEL=2, BASE=3, RUIN=4, WATER=5; var EMPTY=0, BRICK=1, STEEL=2, BASE=3, RUIN=4, WATER=5, FOREST=6;
var BASE_C=8, BASE_R=14; var BASE_C=8, BASE_R=14;
var PLAYER_SPAWN={c:5,r:13}; var PLAYER_SPAWN={c:5,r:13};
@@ -134,7 +137,9 @@ var KIND={
scout: {speed:66, hp:1, body:"#b9c2cc", dark:"#6f7884", light:"#e4eaf0", fire:1.10, bspd:190, pHunt:0.35, pRush:0.10}, scout: {speed:66, hp:1, body:"#b9c2cc", dark:"#6f7884", light:"#e4eaf0", fire:1.10, bspd:190, pHunt:0.35, pRush:0.10},
heavy: {speed:46, hp:2, body:"#9d8f7a", dark:"#5f5648", light:"#cbbda6", fire:1.35, bspd:165, pHunt:0.20, pRush:0.18}, heavy: {speed:46, hp:2, body:"#9d8f7a", dark:"#5f5648", light:"#cbbda6", fire:1.35, bspd:165, pHunt:0.20, pRush:0.18},
hunter:{speed:86, hp:1, body:"#c98a6a", dark:"#7d4f38", light:"#efb797", fire:0.85, bspd:215, pHunt:0.70, pRush:0.05}, hunter:{speed:86, hp:1, body:"#c98a6a", dark:"#7d4f38", light:"#efb797", fire:0.85, bspd:215, pHunt:0.70, pRush:0.05},
guard: {speed:58, hp:1, body:"#a7b58e", dark:"#5f6b4c", light:"#d5e0bd", fire:1.00, bspd:195, pHunt:0.15, pRush:0.08} guard: {speed:58, hp:1, body:"#a7b58e", dark:"#5f6b4c", light:"#d5e0bd", fire:1.00, bspd:195, pHunt:0.15, pRush:0.08},
commander:{speed:76, hp:3, body:"#c2708e", dark:"#7a3b52", light:"#f0a8c0", fire:0.70, bspd:235,
pHunt:0.65, pRush:0.20, worth:300, crown:true}
}; };
var STAGES=[ var STAGES=[
@@ -228,6 +233,31 @@ var STAGES=[
".B.B...BBB...B.B.", ".B.B...BBB...B.B.",
".SS....BEB....SS." ".SS....BEB....SS."
] ]
},
{
name:"STAGE 5 · 최후 방어선",
maxOnField:5,
enemies:["hunter","heavy","scout","hunter","heavy","guard","hunter","heavy","scout","hunter","heavy","commander"],
items:["freeze","shield","power"],
itemEvery:12,
specials:"숲(탱크·포탄 통과, 은폐) · 화력 강화 상자 · 동시 5대 · 지휘 탱크",
map:[
".................",
".BB.BB..S..BB.BB.",
".B...B..B..B...B.",
".B.BBB.B.B.BBB.B.",
"..B.FFF...FFF.B..",
".BB.BSB...BSB.BB.",
".B..B..BBB..B..B.",
"SS..B..B.B..B..SS",
".B..B..BBB..B..B.",
".BB.BSB...BSB.BB.",
"..B.FFF...FFF.B..",
".B.BBB.B.B.BBB.B.",
".BFFFB.BBB.BFFFB.",
".BB.B..BBB..B.BB.",
".S.....BEB.....S."
]
} }
]; ];
@@ -300,7 +330,7 @@ var elSpec=document.getElementById("spec");
(function(){ /* 단계 키 안내 */ (function(){ /* 단계 키 안내 */
var t="",i; var t="",i;
for(i=1;i<=STAGES.length;i++) t+="<kbd>"+i+"</kbd>"; for(i=1;i<=STAGES.length;i++) t+="<kbd>"+i+"</kbd>";
document.getElementById("stagekeys").innerHTML=t+" 단계 직접 선택"; document.getElementById("stagekeys").innerHTML=t+" 단계 선택";
})(); })();
var grid, player, enemies, bullets, fx, spawns, enemyQueue; var grid, player, enemies, bullets, fx, spawns, enemyQueue;
@@ -324,7 +354,7 @@ function buildGrid(src){
var row=[]; var row=[];
for(c=0;c<COLS;c++){ for(c=0;c<COLS;c++){
var ch=line.charAt(c); var ch=line.charAt(c);
row.push(ch==="B"?BRICK:ch==="S"?STEEL:ch==="E"?BASE:ch==="W"?WATER:EMPTY); row.push(ch==="B"?BRICK:ch==="S"?STEEL:ch==="E"?BASE:ch==="W"?WATER:ch==="F"?FOREST:EMPTY);
} }
g.push(row); g.push(row);
} }
@@ -409,6 +439,7 @@ function makeTank(o){
speed:o.speed, hp:o.hp||1, isPlayer:!!o.isPlayer, speed:o.speed, hp:o.hp||1, isPlayer:!!o.isPlayer,
body:o.body, dark:o.dark, light:o.light, body:o.body, dark:o.dark, light:o.light,
bspd:o.bspd||220, fireCd:o.fire||1, fireT:0, bspd:o.bspd||220, fireCd:o.fire||1, fireT:0,
worth:o.worth||100, crown:!!o.crown, maxHp:o.hp||1,
pHunt:o.pHunt||0, pRush:o.pRush||0, mode:"wander", thinkT:0, pHunt:o.pHunt||0, pRush:o.pRush||0, mode:"wander", thinkT:0,
shield:o.shield||0, blockT:0, hitFlash:0, tread:0, kind:o.kind||"" shield:o.shield||0, blockT:0, hitFlash:0, tread:0, kind:o.kind||""
}; };
@@ -446,7 +477,7 @@ function hatch(s){
if(!k) return; if(!k) return;
var e=makeTank({x:s.x,y:s.y,dx:0,dy:1,speed:k.speed,hp:k.hp, var e=makeTank({x:s.x,y:s.y,dx:0,dy:1,speed:k.speed,hp:k.hp,
body:k.body,dark:k.dark,light:k.light,bspd:k.bspd,fire:k.fire, body:k.body,dark:k.dark,light:k.light,bspd:k.bspd,fire:k.fire,
pHunt:k.pHunt,pRush:k.pRush,kind:k.name}); pHunt:k.pHunt,pRush:k.pRush,kind:k.name,worth:k.worth,crown:k.crown});
rollMode(e); rollMode(e);
e.thinkT=0.2+Math.random()*0.4; e.thinkT=0.2+Math.random()*0.4;
e.fireT=0.6+Math.random()*0.9; e.fireT=0.6+Math.random()*0.9;
@@ -726,7 +757,7 @@ function updateBullets(dt){
if(e.hp<=0){ if(e.hp<=0){
boom(e.x+SIZE/2,e.y+SIZE/2,false); boom(e.x+SIZE/2,e.y+SIZE/2,false);
enemies.splice(j,1); enemies.splice(j,1);
kills++; score+=100; kills++; score+=e.worth;
afterKill(); afterKill();
}else{ }else{
spark(cx,cy); e.hitFlash=0.18; spark(cx,cy); e.hitFlash=0.18;
@@ -912,6 +943,26 @@ function drawWaterCell(c,r){
ctx.strokeStyle="rgba(8,26,36,.55)"; ctx.lineWidth=1; ctx.strokeStyle="rgba(8,26,36,.55)"; ctx.lineWidth=1;
ctx.strokeRect(x+0.5,y+0.5,CELL-1,CELL-1); ctx.strokeRect(x+0.5,y+0.5,CELL-1,CELL-1);
} }
function drawForestCell(c,r){
var x=c*CELL, y=r*CELL;
/* 바닥 잔풀 */
ctx.fillStyle="#17240f";
ctx.fillRect(x,y,CELL,CELL);
/* 잎 덩어리 — 탱크를 덮어 은폐한다 */
var i, pts=[[7,8],[18,6],[25,14],[12,17],[4,22],[21,25],[14,27],[28,22]];
for(i=0;i<pts.length;i++){
var px=x+pts[i][0], py=y+pts[i][1];
var sw=(i%3===0)?9:7;
ctx.fillStyle=(i%2)?"#2f5a1e":"#3c7026";
ctx.beginPath(); ctx.arc(px,py,sw*0.62,0,Math.PI*2); ctx.fill();
ctx.fillStyle="rgba(150,205,110,.22)";
ctx.beginPath(); ctx.arc(px-1.4,py-1.8,sw*0.28,0,Math.PI*2); ctx.fill();
}
}
function drawForest(){
for(var r=0;r<ROWS;r++) for(var c=0;c<COLS;c++)
if(grid[r][c]===FOREST) drawForestCell(c,r);
}
function drawBase(c,r,ruin){ function drawBase(c,r,ruin){
var x=c*CELL, y=r*CELL; var x=c*CELL, y=r*CELL;
ctx.fillStyle="#2a221a"; ctx.fillStyle="#2a221a";
@@ -974,6 +1025,13 @@ function drawTank(t){
ctx.beginPath(); ctx.arc(0,1,5.6,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(0,1,5.6,0,Math.PI*2); ctx.fill();
ctx.fillStyle=t.dark; ctx.fillStyle=t.dark;
ctx.beginPath(); ctx.arc(0,1,2.4,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(0,1,2.4,0,Math.PI*2); ctx.fill();
if(t.crown){ /* 지휘 탱크 표식 */
ctx.fillStyle="#ffd86a";
ctx.fillRect(-5,-s/2+6,10,2);
ctx.fillRect(-5,-s/2+6,2,4);
ctx.fillRect(-1,-s/2+6,2,4);
ctx.fillRect(3,-s/2+6,2,4);
}
ctx.fillStyle=t.dark; ctx.fillStyle=t.dark;
ctx.fillRect(-2.2,-s/2-4,4.4,s/2+5); ctx.fillRect(-2.2,-s/2-4,4.4,s/2+5);
ctx.fillStyle=t.light; ctx.fillStyle=t.light;
@@ -1113,6 +1171,7 @@ function render(){
if(player) drawTank(player); if(player) drawTank(player);
for(i=0;i<items.length;i++) drawItem(items[i]); for(i=0;i<items.length;i++) drawItem(items[i]);
for(i=0;i<bullets.length;i++) drawBullet(bullets[i]); for(i=0;i<bullets.length;i++) drawBullet(bullets[i]);
drawForest(); /* 숲은 탱크 위에 덮인다 */
drawFx(); drawFx();
drawBadges(); drawBadges();
ctx.restore(); ctx.restore();