diff --git a/README.md b/README.md index b28fbce..9ddde14 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,14 @@ | ↳ | `/tank.html#3` | 3단계 강철 회랑 | 8대 (동시 4) | 강철 31칸, 기지 외곽 2중 + 좁은 진입로 | | ↳ | `/tank.html#4` | 4단계 참호 | 10대 (동시 4) | **물길** + **보급 상자**(시간 정지 / 기지 강화) | | ↳ | `/tank.html#5` | 5단계 최후 방어선 | 12대 (동시 5) | **숲**(은폐) + **화력 강화** 상자 + **지휘 탱크** | +| ↳ | `/tank.html#6` | 6단계 거울 요새 | 12대 (동시 4) | **반사판**(포탄 90° 꺾임) + **적 본진 습격** | 방향키 이동, 스페이스 발사. 한 단계를 돌파하면 다음 단계로 이어지고, 점수와 남은 탱크를 그대로 가져간다. **단계를 거치지 않고 바로 들어가는 방법**은 셋이다. -- 왼쪽 탭의 `휴게실 > 탱크게임` 아래 단계 칩 (셸 주소로는 `/#tank2` … `/#tank5`) -- 게임 화면 오른쪽 아래의 단계 버튼, 또는 `1` ~ `5` 키 -- 주소 직접 입력 — `/tank.html#5`, `/tank.html?stage=5` (범위를 벗어난 값은 1단계) +- 왼쪽 탭의 `휴게실 > 탱크게임` 아래 단계 칩 (셸 주소로는 `/#tank2` … `/#tank6`) +- 게임 화면 오른쪽 아래의 단계 버튼, 또는 `1` ~ `6` 키 +- 주소 직접 입력 — `/tank.html#6`, `/tank.html?stage=6` (범위를 벗어난 값은 1단계) ## 특수 요소 @@ -52,10 +53,20 @@ | ↳ 시간 정지 | 적 전원이 3초간 멈춘다 (포탄은 계속 난다) | 4, 5 | | ↳ 기지 강화 | 기지 외곽 5칸이 10초간 강철이 된다. 끝나면 벽돌로 돌아오며 부서졌던 칸도 메워진다 | 4, 5 | | ↳ 화력 강화 | 10초간 동시 3발 + 탄속 1.35배 | 5 | -| 지휘 탱크 | 편성 마지막에 나오는 대장. 체력 3, 빠르고 연사가 잦다. 격파 300점 | 5 | +| 지휘 탱크 | 편성 마지막에 나오는 대장. 체력 3, 빠르고 연사가 잦다. 격파 300점 | 5, 6 | +| 반사판 `/` `\` | 포탄이 맞으면 90° 꺾인다. 탱크는 못 지나가고 부술 수도 없다. 한 포탄이 5번까지 꺾인다 | 6 | +| 적 본진 `X` | 위쪽 강철 안에 갇힌 적 독수리. **아군 포탄으로 부수면 적을 다 잡지 않아도 즉시 클리어**(+1000점). 적 포탄은 제 본진을 부수지 못한다 | 6 | 효과가 걸린 동안에는 화면 왼쪽 위에 남은 시간이 배지로 표시된다. +### 6단계 저격 경로 + +적 본진 `(8,2)` 위·좌·우는 강철이고, 아래 `(8,3)`~`(8,5)` 는 좌우가 강철로 막힌 +**탄도 전용 갱도**다. 그 아래 `(8,6)` 에 반사판 `/` 이 있어, 6행을 따라 **오른쪽으로** +쏜 포탄만 위로 꺾여 갱도를 타고 본진에 닿는다. 왼쪽으로 쏘면 아래로, 아래에서 위로 +쏘면 오른쪽으로 꺾이므로 각을 맞춰야 한다. 아군 출발점 `(5,13)` 에서 6열을 따라 위로 +올라가면 사격 위치가 나온다. + 낱말풀이와 공유하는 자산이 없는 단독 페이지다. 캔버스와 WebAudio 만 쓰므로 자산 해시 스탬프(아래 캐시 항목)의 영향도 받지 않는다. iframe 안에서는 **화면을 한 번 클릭해 포커스를 준 뒤** 키를 눌러야 한다. 셸이 iframe 의 조각(`#2`)만 바꿀 때는 문서가 다시 diff --git a/site/index.html b/site/index.html index 31a1031..1ea9899 100644 --- a/site/index.html +++ b/site/index.html @@ -176,6 +176,7 @@ body{ 3단계 4단계 5단계 + 6단계 @@ -203,7 +204,8 @@ var PAGES = { tank2: {file:'tank.html#2', name:'휴게실 · 탱크게임 2단계'}, tank3: {file:'tank.html#3', name:'휴게실 · 탱크게임 3단계'}, tank4: {file:'tank.html#4', name:'휴게실 · 탱크게임 4단계'}, - tank5: {file:'tank.html#5', name:'휴게실 · 탱크게임 5단계'} + tank5: {file:'tank.html#5', name:'휴게실 · 탱크게임 5단계'}, + tank6: {file:'tank.html#6', name:'휴게실 · 탱크게임 6단계'} }; var DEFAULT = 'korean'; diff --git a/site/tank.html b/site/tank.html index 11e4c46..764fa71 100644 --- a/site/tank.html +++ b/site/tank.html @@ -83,9 +83,10 @@ } button:hover{filter:brightness(1.1)} button:active{transform:translateY(1px); border-bottom-width:2px} - .btns{display:flex; gap:4px; margin-top:10px} + .btns{display:flex; flex-wrap:wrap; gap:5px; margin-top:10px} .btns button{ - flex:1 1 0; min-width:0; margin-top:0; padding:6px 0; font-size:11px; + flex:0 0 calc(33.333% - 4px); min-width:0; margin-top:0; + padding:6px 0; font-size:11px; background:linear-gradient(#5c4a34,#463725); border-color:#3a2c1c; } @@ -126,6 +127,8 @@ var COLS=17, ROWS=15, CELL=32; var W=COLS*CELL, H=ROWS*CELL; var SIZE=28, OFF=(CELL-SIZE)/2; var EMPTY=0, BRICK=1, STEEL=2, BASE=3, RUIN=4, WATER=5, FOREST=6; +var MIRROR_A=7, MIRROR_B=8, EBASE=9, ERUIN=10; /* 반사판 / 적 본진 */ +var CH_MA="/", CH_MB=String.fromCharCode(92); var BASE_C=8, BASE_R=14; var PLAYER_SPAWN={c:5,r:13}; @@ -258,6 +261,31 @@ var STAGES=[ ".BB.B..BBB..B.BB.", ".S.....BEB.....S." ] + }, + { + name:"STAGE 6 · 거울 요새", + maxOnField:4, + enemies:["hunter","heavy","scout","hunter","heavy","guard","hunter","heavy","scout","hunter","heavy","commander"], + items:["freeze","shield","power"], + itemEvery:13, + specials:"반사판(포탄 90° 꺾임) · 적 본진 습격(부수면 즉시 승리)", + map:[ + ".................", + ".BB....SSS....BB.", + ".B...B.SXS.B...B.", + ".B.BBB.S.S.BBB.B.", + "...B...S.S...B...", + ".BB.BB.S.S.BB.BB.", + "...B..../..B...B.", + "SS..B..BBB..B..SS", + ".B.\\B.......B/.B.", + ".BBB.B.B.B.B.BBB.", + "..B./.......\\.B..", + ".B.BBB.BBB.BBB.B.", + ".B...B.BBB.B...B.", + ".BB.B..BBB..B.BB.", + ".S.....BEB.....S." + ] } ]; @@ -317,6 +345,7 @@ var sfx={ item:function(){ tone("sine",900,1400,0.10,0.05); tone("sine",1400,1900,0.10,0.04,0.09); }, pickup:function(){ var n=[784,1047,1319],i; for(i=0;i<3;i++) tone("square",n[i],n[i],0.11,0.09,i*0.07); }, splash:function(){ noise(0.16,0.13,900,240,"bandpass"); }, + mirror:function(){ tone("triangle",1800,2600,0.07,0.07); tone("sine",2600,1400,0.09,0.04,0.03); }, win:function(){ var n=[523,659,784,1047],i; for(i=0;i<4;i++) tone("square",n[i],n[i],0.16,0.11,i*0.14); }, lose:function(){ var n=[392,330,262,196],i; for(i=0;i<4;i++) tone("sawtooth",n[i],n[i]*0.95,0.26,0.12,i*0.18); } }; @@ -354,7 +383,8 @@ function buildGrid(src){ var row=[]; for(c=0;c=COLS||r>=ROWS) return false; - if(grid[r][c]===STEEL) return false; + var t=grid[r][c]; + if(t===STEEL||t===MIRROR_A||t===MIRROR_B||t===EBASE) return false; } return true; } @@ -727,6 +760,30 @@ function updateBullets(dt){ sfx.brick(); b.gone=true; break; } if(t===STEEL){ spark(cx,cy); sfx.steel(); b.gone=true; break; } + if(t===MIRROR_A||t===MIRROR_B){ + if((b.bounces||0)>=5){ spark(cx,cy); b.gone=true; break; } /* 무한 반사 방지 */ + b.bounces=(b.bounces||0)+1; + var nd; + if(t===MIRROR_A) nd=[-b.dy,-b.dx]; /* "/" */ + else nd=[ b.dy, b.dx]; /* "\\" */ + b.dx=nd[0]; b.dy=nd[1]; + /* 칸 중심에 맞춘 뒤 다음 칸으로 한 칸 밀어 같은 반사판을 두 번 타지 않게 한다 */ + b.x=c*CELL+CELL/2+b.dx*(CELL/2+1)-3; + b.y=r*CELL+CELL/2+b.dy*(CELL/2+1)-3; + spark(cx,cy); sfx.mirror(); + continue; + } + if(t===EBASE){ + if(b.player){ + grid[r][c]=ERUIN; + boom(c*CELL+CELL/2,r*CELL+CELL/2,true); + b.gone=true; + clearStage(1000,"적 본진 격파 +1000"); + }else{ + spark(cx,cy); sfx.steel(); b.gone=true; /* 적 포탄은 제 본진을 못 부순다 */ + } + break; + } if(t===BASE){ if(b.player){ /* 아군 포탄은 기지를 부수지 못한다 */ spark(cx,cy); sfx.steel(); @@ -780,26 +837,27 @@ function updateBullets(dt){ bullets=keep; } +function clearStage(bonus,note){ + if(state!=="play") return; + score+=bonus; + if(stage