/*---------------------------------------- * flagrally.js * Copyright (C) 2020 StraightApps.com, All Rights Reserved * September 18, 2020 初期版 * September 25, 2020 実用版 *-----------------------------------------*/ //---------------------------------------- // ブロック情報 var block = new Object(); block.w = 13; // 横方向の表示コマ数 block.h = 13; // 縦方向の表示コマ数 block.offsetx = 6; block.offsety = 6; block.size = 0; // 1コマの表示サイズ(ピクセル・正方形) //---------------------------------------- // canvas の設定 // この部分は、id "canvas-main" の定義後でないと、失敗します。 var canvas = document.getElementById('canvas-main'); var w = scr.innerWidth; // クライアント領域の幅 var h = scr.innerHeight; // クライアント領域の高さ var rect = canvas.getBoundingClientRect(); // Canvas の絶対座標位置を取得 w -= Math.floor( rect.left ); // rectが小数で返されている w = Math.min( 520, w ); // 十分広い場合は 520 ピクセルまでとします block.size = Math.floor( w / block.w ); // 13コマ表示可能な1コマのサイズ w = block.size * block.w; if (h > block.size * block.h){ // 高さ方向が十分なサイズなら h = block.size * block.h; // 13コマ表示できればOK } else if (h < block.size * block.h){ // 高さが不足の場合 block.size = Math.floor( h / block.h ); // 縦に13コマ表示可能とします h = block.size * block.h; w = block.size * block.w; // 横も縦サイズに合わせます } canvas.width = w; // canvas の幅設定 canvas.height = h; // canvas の高さ設定 //---------------------------------------- // コンテキストを取得 var ctx = canvas.getContext('2d'); //---------------------------------------- // ボールイメージの読み込み var ball = new Object(); ball.img = new Image(); ball.img.src = 'jsimg/pin.png'; // 通常 ball.imgF1 = new Image(); ball.imgF1.src = 'jsimg/zfr_flash1.png' // フラッグを獲った直後 ball.cx = 24; // キャラクタベースの座標 ball.cy = 23; //---------------------------------------- // フラッグイメージの読み込み var treasure = new Object(); treasure.img = new Image(); treasure.img.src = 'jsimg/zfr_treasure.png'; treasure.num = 0; // 残りフラッグ数(自動計算) treasure.flash = 0; // フラッグ取得後のカウンタ //---------------------------------------- // キー入力用オブジェクトを作成 var key = new Object(); key.reserve = ''; // 次に移動する方向 key.push = ''; // 今移動している方向 key.prev = ''; // 前に移動していた方向 //---------------------------------------- // マップ用配列を初期化します。 // このうち、13x13 領域が画面に表示されます。 // ふち 6マスは予備領域です。 // 1配列の長さ(横方向のマス数)は、49 です。 var map = new Object(); map.map = [ "9999999999999999999999999999999999999999999999999", // 1 "9999999999999999999999999999999999999999999999999", "9999999999999999999999999999999999999999999999999", "9999999999999999999999999999999999999999999999999", "9999999999999999999999999999999999999999999999999", // 5 "9999999999999999999999999999999999999999999999999", "9999991000000011100000000111000000000010000999999", "9999991010020011100011100011000020011110020999999", "9999991010111000000011100001110011000110001999999", "9999991010111000000000001101110011000110001999999", // 10 "9999990010000011110110001100000011001110000999999", "9999990000011111110111000020000000001100100999999", "9999990100000000000111101110111100000000110999999", "9999990100000111100111100000011110001110110999999", "9999990120000111100110001110000000200110010999999", // 15 "9999990100001110000200100000011100001110000999999", "9999990101101110110110100000011100000200001999999", "9999990001100000110110000000200000011000001999999", "9999990000011000110000010100000110011000000999999", "9999991111011000000101010101010110011101010999999", // 20 "9999991111000001110101010101010110011101010999999", "9999991100001101110101010101010110000001010999999", "9999991100011100000000000000002000000001010999999", "9999990020110111011011110001111011011001010999999", "9999990000110000020000000000200000000001020999999", // 25 "9999999999999999999999999999999999999999999999999", "9999999999999999999999999999999999999999999999999", "9999999999999999999999999999999999999999999999999", "9999999999999999999999999999999999999999999999999", "9999999999999999999999999999999999999999999999999", // 30 "9999999999999999999999999999999999999999999999999" ]; // 0123456789abcdefghij0123456789abcdefghij012345678 map.h = map.map.length; // マップの縦のデータ数 map.w = map.map[0].length; // マップの横のデータ数 map.cx = ball.cx - block.offsetx; // キャラクタベースの座標 map.cy = ball.cy - block.offsety; map.x = 0; // ピクセルベースの座標 map.y = 0; map.move = 0; // 移動量(ピクセル) //---------------------------------------- // マップを解析します。 checkMap(); //---------------------------------------- // ゲーム状態 var state = 'ready'; // ready/play/pause/clear var oneStep = 4; // 1回で4ピクセル移動 //---------------------------------------- // キー入力を受け付けます。 // keydown イベントは、第3引数をこの指定でないと、矢印下キーでスクロールしてしまう。 addEventListener("keydown", keydownfunc, { passive: false }); //---------------------------------------- // マウスクリック/タッチイベントを受け付けます。 canvas.addEventListener('click', onClick, false); /*---------------------------------------- * メインループ *-----------------------------------------*/ function main() { //---------------------------------------- // canvas を指定の色で塗りつぶします。 ctx.fillStyle = "rgb( 230, 255, 230 )"; ctx.fillRect(0, 0, canvas.width, canvas.height); //---------------------------------------- // スクロール・マップを表示します。 showMap(); //---------------------------------------- // ゲーム開始前 if (state === 'ready') { ctx.font = "bold 36px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif"; ctx.fillStyle = '#faf'; var str = 'CLICK TO START!'; var tx = (canvas.width - ctx.measureText(str).width) / 2; // センタリング var th = ctx.measureText(str).actualBoundingBoxAscent + ctx.measureText(str).actualBoundingBoxDescent; var ty = (canvas.height - th) / 2; ctx.fillText(str, tx, ty); // canvas 領域の座標(y座標は文字の下を指している) ctx.strokeText(str, tx, ty); } //---------------------------------------- // ゲームクリア後 else if (state === 'clear') { ctx.font = "48px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif"; ctx.fillStyle = '#ffa'; var str = 'FINISHED !'; var tx = (canvas.width - ctx.measureText(str).width) / 2; var th = ctx.measureText(str).actualBoundingBoxAscent + ctx.measureText(str).actualBoundingBoxDescent; var ty = (canvas.height - th) / 2; ctx.fillText(str, tx, ty); } //---------------------------------------- // ポーズ中 else if (state === 'pause') { ctx.font = "48px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif"; ctx.fillStyle = '#fff'; var str = 'PAUSE'; var tx = (canvas.width - ctx.measureText(str).width) / 2; var th = ctx.measureText(str).actualBoundingBoxAscent + ctx.measureText(str).actualBoundingBoxDescent; var ty = (canvas.height - th) / 2; ctx.strokeText(str, tx, ty); ctx.fillText(str, tx, ty); } //---------------------------------------- // ゲーム中 else { //---------------------------------------- // ぴったりの座標にいる場合、 // 方向キーが押されている場合は、スクロールさせます。 if (map.move === 0) { makeTurn(); } //---------------------------------------- // ぴったりの座標にいない場合、 // map.move が 0 より大きい場合は、最大4ピクセルずつ移動を続けます。 else if (map.move > 0) { keepMoving(); } } //---------------------------------------- // ボールをマップ中央に表示(常に中央) if (treasure.flash < 1) { // フラッグ獲得直後ではないとき ctx.drawImage(ball.img, block.offsetx * block.size, block.offsety * block.size, block.size, block.size); } else { // フラッグ獲得直後のとき treasure.flash--; if (treasure.flash > block.size / 4) { // 少しの間 ctx.drawImage(treasure.img, block.offsetx * block.size, block.offsety * block.size, block.size, block.size); // フラッグを引きずる ctx.fillStyle = '#ff6'; } else{ ctx.fillStyle = '#400'; } ctx.font = "36px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif"; var str = 'あと' + treasure.num + '個!'; var tx = (canvas.width - ctx.measureText(str).width) / 2; var th = ctx.measureText(str).actualBoundingBoxAscent + ctx.measureText(str).actualBoundingBoxDescent; var ty = (canvas.height - th) / 2; ctx.fillText(str, tx, ty); ctx.drawImage(ball.imgF1, block.offsetx * block.size, block.offsety * block.size, block.size, block.size); } requestAnimationFrame( main ); } /*---------------------------------------- * ページと、依存している全てのデータが読み込まれたら、 * メインループを開始します。 *-----------------------------------------*/ addEventListener('load', main(), false); /*---------------------------------------- * キーが押されたときに呼び出される関数 *-----------------------------------------*/ function keydownfunc( event ) { var key_code = event.keyCode; if ( key_code === 37 ) key.reserve = 'left'; if ( key_code === 38 ) key.reserve = 'up'; if ( key_code === 39 ) key.reserve = 'right'; if ( key_code === 40 ) key.reserve = 'down'; if ( key_code >= 37 && key_code <= 40 ){ // 使用するキーのみに限定 event.preventDefault(); } } /*---------------------------------------- * クリックされたときに呼び出される関数 *-----------------------------------------*/ function onClick( event ) { // ready 状態でクリックされた場合、ゲームを開始します。 if (state === 'ready') { // 自動的に上に走行開始 key.push = 'up'; map.cy -= 1; map.move = Math.abs(1 * block.size); state = 'play'; return; } // ポーズ中にクリックされた場合は、ゲームを再開します。 if (state === 'pause') { state = 'play'; return; } // クリア後は、クリックは何もしません。 if (state === 'clear') { return; } // ゲーム中以外の想定外の場合、何もしません。 if (state != 'play') { return; } // ゲーム中にクリックされた場合、スマホやタブレットだと矢印キーがないのでタップで操作します。 var rect = event.target.getBoundingClientRect(); // Canvas の絶対座標位置を取得 var mx = Math.floor(event.clientX - rect.left); // rectが実数で返されているため var my = Math.floor(event.clientY - rect.top); var bx = Math.floor(mx / block.size) - 6; // マス単位の座標に変換(13x13座標) var by = Math.floor(my / block.size) - 6; // -6 して、-6 〜 0 〜 +6 // 中央部近く(ボールまわり1マス以内)ならポーズにします。 if (bx >= -1 && bx <= 1 && by >= -1 && by <= 1 ) { state = 'pause'; return; } // 操作可能領域? if (bx < 0 && Math.abs(bx) > Math.abs(by)) { key.reserve = 'left'; } else if (bx > 0 && bx > Math.abs(by)) { key.reserve = 'right'; } else if (by < 0) { key.reserve = 'up'; } else if (by > 0) { key.reserve = 'down'; } } /*---------------------------------------- * 【メインループ】マップを表示 *-----------------------------------------*/ function showMap() { // 端数となるピクセル数を計算します。 // x または y のどちらかのみが端数があり、どちらかは必ずぴったりになります。 var zx = 0, zy = 0; if ( key.push === 'left' ) zx = -map.move; // 左に移動中 if ( key.push === 'up' ) zy = -map.move; if ( key.push === 'right' ) zx = map.move; // 右に移動中 if ( key.push === 'down' ) zy = map.move; // 端数を表示するため、上下・左右で各1マスおおきくループしています。 for (var y = -1; y < block.h + 1; y ++){ // 念のため、範囲外を除外 if ( map.cy + y < 0 || map.cy + y >= 31 ){ continue; } for (var x = -1; x < block.w + 1; x ++){ // 念のため、範囲外を除外 if ( map.cx + x < 0 || map.cx + x >= 49 ){ continue; } // マス描画位置 var px = x * block.size + zx; var py = y * block.size + zy; // 項目別に描画 var n = map.map[map.cy + y][map.cx + x]; // 【マス】描画数による速度の変化を減らす目的で if ( n == 0 ){ ctx.fillStyle = '#d81'; ctx.fillRect(px, py, block.size, block.size); } // 【マス】マップ外側 if ( n == 9 ){ ctx.fillStyle = '#444'; ctx.fillRect(px, py, block.size, block.size); } // 【マス】グリーン(壁) else if ( n == 1 ){ ctx.fillStyle = '#8F8'; ctx.fillRect(px, py, block.size, block.size); } // 【マス】フラッグ else if ( n == 2 ){ ctx.fillStyle = '#d81'; ctx.fillRect(px, py, block.size, block.size); ctx.drawImage( treasure.img, px, py, block.size, block.size ); } } } // ミニマップ(レーダーマップ)を作成 var dotsize = Math.max(2, Math.floor(block.size / 10)); // 最低2ピクセル ctx.fillStyle = '#FFF'; ctx.fillRect(20 - 2, 20 - 2, dotsize * (map.w - 12) + 4, dotsize * (map.h - 12) + 4); for (var y = 6; y < map.h - 6; y++) { for (var x = 6; x < map.w - 6; x++) { var sx = 20 + (x-6) * dotsize; var sy = 20 + (y-6) * dotsize; // 項目別に描画 var n = map.map[y][x]; // マスに関係なく、自分の位置を描画 if (x == map.cx + 6 && y == map.cy + 6) { ctx.fillStyle = '#ff8'; ctx.fillRect(sx, sy, dotsize, dotsize); continue; } // 【マス】描画数による速度の変化を嫌う if (n == 0) { ctx.fillStyle = '#200'; ctx.fillRect(sx, sy, dotsize, dotsize); } // 【マス】マップ外側(描画領域外) else if (n == 9) { ctx.fillStyle = '#444'; ctx.fillRect(sx, sy, dotsize, dotsize); } // 【マス】グリーン(壁) else if (n == 1) { ctx.fillStyle = '#484'; ctx.fillRect(sx, sy, dotsize, dotsize); } // 【マス】フラッグ else if (n == 2) { ctx.fillStyle = '#f88'; ctx.fillRect(sx, sy, dotsize, dotsize); } } } } /*---------------------------------------- * 【メインループ】向きを変える判定 * スクロール位置がぴったりの場合のみ呼び出されます。 * (map.move が 0 のとき) *-----------------------------------------*/ function makeTurn() { // 行けない方向が押されている場合があるので、 // 今進んでいる方向を保存しておきます。 var copy_key_push = key.push; // 移動方向を決定します。 var dx = 0, dy = 0; if ( key.reserve === 'left' ) { dx = -1; key.push = 'left'; } else if ( key.reserve === 'up' ) { dy = -1; key.push = 'up'; } else if ( key.reserve === 'right' ) { dx = 1; key.push = 'right'; } else if ( key.reserve === 'down' ) { dy = 1; key.push = 'down'; } // キーが押されたとき if (dx != 0 || dy != 0){ map.cx += dx; map.cy += dy; map.move = Math.abs( dx * block.size ) + Math.abs( dy * block.size ); var n = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx); // 通れない場所の場合 if (n == 1) { map.cx -= dx; // 計算を戻します map.cy -= dy; key.prev = key.push; // 元の動きをキープします。 key.push = copy_key_push; map.move = 0; return false; } else { key.reserve = ''; // 予約を解除 if (n == 2) { getFlag(block.offsety + map.cy, block.offsetx + map.cx); } } return true; } // キーは押されていませんでした。 return false; } /*---------------------------------------- * 【メインループ】動き続ける処理 * スクロール位置がぴったりでない場合のみ呼び出されます。 * (map.move が 0 ではないとき) *-----------------------------------------*/ function keepMoving() { // 移動量(ピクセル)を決定して、移動します。 var nowMove = Math.min(oneStep, map.move); map.move -= nowMove; // 残りの移動量(ピクセル) // 移動している方向により、座標を調整します。 if ( key.push === 'left' ) map.x -= nowMove; if ( key.push === 'up' ) map.y -= nowMove; if ( key.push === 'right' ) map.x += nowMove; if ( key.push === 'down' ) map.y += nowMove; // まだ移動途中の場合は、ここで終わります。 if ( map.move > 0 ){ return; } // ぴったりの座標では、キー入力による向き変えを優先します。 if ( makeTurn() == true ){ return; } // キー入力がなかった場合、今の向きで移動を続けます。 var dx = 0, dy = 0; if ( key.push === 'left' ) dx = -1; if ( key.push === 'up' ) dy = -1; if ( key.push === 'right' ) dx = 1; if ( key.push === 'down' ) dy = 1; // とりあえず進みます。 map.cx += dx; map.cy += dy; map.move = Math.abs( dx * block.size ) + Math.abs( dy * block.size ); // 残りの移動量(ピクセル) // 移動先を確認します。 var n = map.map[block.offsety + map.cy][block.offsetx + map.cx]; // 通れる場合は、そのまま進みます。 if ( n == 0 ){ return; } // フラッグの場合は、ゲットします。 if (n == 2) { getFlag(block.offsety + map.cy, block.offsetx + map.cx); return; } // 1(壁)と 9(外側)は、通れません。 // ぶつかった場合、 // 最後に進行方向右に向かったなら右へ、左なら左へ、どちらもダメならバウンド。 // 余計に移動したぶんを戻します。 map.cx -= dx; map.cy -= dy; map.move = 0; key.reserve = ''; dx = 0; dy = 0; var n1, n2; // 左に進んでいた場合 if (key.push === 'left') { n1 = canGoThrough(block.offsety + map.cy - 1, block.offsetx + map.cx); // 上のマス n2 = canGoThrough(block.offsety + map.cy + 1, block.offsetx + map.cx); // 下のマス if (key.prev === 'down') { // 下を優先したい場合 n1 = canGoThrough(block.offsety + map.cy + 1, block.offsetx + map.cx); // 下のマス n2 = canGoThrough(block.offsety + map.cy - 1, block.offsetx + map.cx); // 上のマス } if (n1 == 0 || n1 == 2) { // 上が通れる場合 if (key.prev === 'down') { // 下を優先したい場合 key.push = 'down'; dy = 1; } else { key.push = 'up'; dy = -1; } if (n1 == 2) { getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx); // フラッグなら取る } key.prev = 'left'; } else if (n2 == 0 || n2 == 2) { if (key.prev === 'down') { // 下を優先したい場合 key.push = 'up'; dy = -1; } else{ key.push = 'down'; dy = 1; } if (n2 == 2) { getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx); // フラッグなら取る } key.prev = 'left'; } else { key.push = 'right'; // バウンドは、必ずできる dx = 1; } } // 上に進んでいた場合 else if (key.push === 'up') { n1 = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx - 1); // 左のマス n2 = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx + 1); // 右のマス if (key.prev === 'right') { // 右を優先したい場合 n1 = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx + 1); // 右のマス n2 = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx - 1); // 左のマス } if (n1 == 0 || n1 == 2) { // 左が通れる場合 if (key.prev === 'right') { // 右を優先したい場合 key.push = 'right'; dx = 1; } else { key.push = 'left'; dx = -1; } if (n1 == 2) { getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx); // フラッグなら取る } key.prev = 'up'; } else if (n2 == 0 || n2 == 2) { if (key.prev === 'right') { // 左を優先したい場合 key.push = 'left'; dx = -1; } else { key.push = 'right'; dx = 1; } if (n2 == 2) { getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx); // フラッグなら取る } key.prev = 'up'; } else { key.push = 'down'; // バウンドは、必ずできる dy = 1; } } // 右に進んでいた場合 else if (key.push === 'right') { n1 = canGoThrough(block.offsety + map.cy - 1, block.offsetx + map.cx); // 上のマス n2 = canGoThrough(block.offsety + map.cy + 1, block.offsetx + map.cx); // 下のマス if (key.prev === 'down') { // 下を優先したい場合 n1 = canGoThrough(block.offsety + map.cy + 1, block.offsetx + map.cx); // 下のマス n2 = canGoThrough(block.offsety + map.cy - 1, block.offsetx + map.cx); // 上のマス } if (n1 == 0 || n1 == 2) { // 上が通れる場合 if (key.prev === 'down') { // 下を優先したい場合 key.push = 'down'; dy = 1; } else { key.push = 'up'; dy = -1; } if (n1 == 2) { getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx); // フラッグなら取る } key.prev = 'right'; } else if (n2 == 0 || n2 == 2) { if (key.prev === 'down') { // 下を優先したい場合 key.push = 'up'; dy = -1; } else { key.push = 'down'; dy = 1; } if (n2 == 2) { getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx); // フラッグなら取る } key.prev = 'right'; } else { key.push = 'left'; // バウンドは、必ずできる dx = -1; } } // 下に進んでいた場合 else if (key.push === 'down') { n1 = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx - 1); // 左のマス n2 = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx + 1); // 右のマス if (key.prev === 'right') { // 右を優先したい場合 n1 = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx + 1); // 右のマス n2 = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx - 1); // 左のマス } if (n1 == 0 || n1 == 2) { // 左が通れる場合 if (key.prev === 'right') { // 右を優先したい場合 key.push = 'right'; dx = 1; } else { key.push = 'left'; dx = -1; } if (n1 == 2) { getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx); // フラッグなら取る } key.prev = 'down'; } else if (n2 == 0 || n2 == 2) { if (key.prev === 'right') { // 右を優先したい場合 key.push = 'left'; dx = -1; } else { key.push = 'right'; dx = 1; } if (n2 == 2) { getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx); // フラッグなら取る } key.prev = 'down'; } else { key.push = 'up'; // バウンドは、必ずできる dy = -1; } } map.cx += dx; map.cy += dy; map.move = Math.abs( dx * block.size ) + Math.abs( dy * block.size ); } /*---------------------------------------- * 指定のマスが通れるかどうか * 通れるときは 0、 * フラッグの場合は 2、 * 通れない場合は区別なく 1 を返します。 *-----------------------------------------*/ function canGoThrough( y, x ) { var n = map.map[ y ][ x ]; if ( n == 0 ){ return 0; // 通れる } else if (n == 2) { return 2; // 通れる } return 1; // 通れない } /*---------------------------------------- * フラッグをゲットします。 *-----------------------------------------*/ function getFlag(y, x) { map.map[y] = replaceValue(map.map[y], x, 0); treasure.num--; if (treasure.num == 0) { state = 'clear'; } else { treasure.flash = Math.floor(block.size / 2); } } /*---------------------------------------- * マップを解析 *-----------------------------------------*/ function checkMap() { var n; for (var y = 0; y < map.h; y++) { for (var x = 0; x < map.w; x++) { n = map.map[y][x]; switch (n) { // 壁 case '1': // 上下左右を見て角を丸める? break; // フラッグ case '2': treasure.num++; // 数をカウント break; // 外側 case '9': break; // スタート位置と方向も指定可能に? } } } } /*---------------------------------------- * 文字列のうち1文字を置き換える関数 * mapstr 文字列の pos 位置の文字を value に置き換えます。 *-----------------------------------------*/ function replaceValue( mapstr, pos, value ) { var m = mapstr.substr(0,pos) + value + mapstr.substr(pos + 1); return m; } /*---------------------------------------- * 環境によるスピード設定 *-----------------------------------------*/ function speed( step ) { oneStep = step; }