画像付きのJavaScriptでのXonix

Xonixは人気のDOSゲームで、 Qixビデオゲームのクローンです。

XonixはすでにJavascriptに数回移植されています。 これまでの既存の実装のオリジナル、おそらくこれに最も近いもの 。 最初は、実装/変更に合わせて調整しようとしました...しかし、残念なことに、難読化解除後のコードも明らかになりませんでした(少なくとも私にとって)。 さらに、私が理解できる限りでは、そこにあるコードは完全に効果的でないか、完全に古くなっている場合があります。 そのため、すべてをゼロから作成する必要がありました。

その結果、私は写真と答えが付いた「自分の」Xonixを手に入れました。



デモ | ソースコード

コードは非常に膨大であることが判明したため、ここではすべてを説明するのではなく、(私の観点から)最も重要な点のみを説明します。

ご存じのとおり、Xonixの競技場は正方形のセルのグリッドです。 ゲームの開始時(レベル)、フィールドの大部分は長方形の黒い領域(「海」)で占められており、すべての側面がライトフレーム(「土地」)で囲まれています。

私の実装と従来のXonixの主な違いは、黒い領域の背後に各レベルに固有の画像があることです。 このように、Xonixの別の修正版、Sexonixのようなものです。 しかし、私の写真には理由があります。 これは答えられるべき質問の一部です。 したがって、ゲーム全体がクイズになります。

画像の大きさによって黒い領域のサイズが決まります。これはセルのサイズの倍数でなければなりません。 一般に、ゲームのさまざまなレベルの画像はサイズが異なる場合があるため、黒い領域のサイズは、レベルの最初に常に2セルの固定幅を持つライトフレームとは対照的に、レベルごとに異なる場合があります。

すべてのオブジェクト( カーソルポイント )の移動はセル内で厳密に行われるため、各瞬間に各オブジェクトは1つのセルを占有します。 競技場のこの構造は、ゲームの実装を大幅に簡素化します。 難易度は、カーソルが「海」を横切るときに形成される「征服された」領域の定義だけですが、それについては後で詳しく説明します。

オブジェクトの動き


「ピクセル」のセルを取ることができるという事実は、動き、バウンス、コリジョンなどの多くの動くオブジェクトを持つゲームでよく見られるほとんどの計算問題を排除します。

Xonixでは、オブジェクトには移動方向のオプションが4つしかありません。カーソルの場合-上/下/右/左、ポイントの場合(両方のタイプ)-同じこと、斜め方向のみ。 これらのオプションを多くの可能な方向に組み合わせて、度単位で設定します。 0から315度まで、45刻みで8つの移動角度を取得します。各角度値は、方向ベクトルの座標のペアに関連付けられています。 その結果、動きの計算に使用する構造が得られます。

コードスニペット
dirset = { vecs: { 0: [1, 0], 45: [1, 1], 90: [0, 1], 135: [-1, 1], 180: [-1, 0], 225: [-1, -1], 270: [0, -1], 315: [1, -1] }, get: function(v) { return v in this.vecs? this.vecs[v] : [0, 0]; }, find: function(x, y) { x = x == 0? 0 : (x > 0? 1 : -1); y = y == 0? 0 : (y > 0? 1 : -1); for (var v in this.vecs) { var vec = this.vecs[v]; if (vec[0] == x && vec[1] == y) return parseInt(v); } return false; } }; 

getメソッドは、指定された角度(度単位)に対応するモーションベクトルを返します。 findメソッドは逆の処理を行います。特定の動きベクトル(必ずしも単位ではない)に対して、対応する角度(度単位)またはそれに最も近い利用可能な角度を返します。

ポイント同士の衝突は無視することができ、単に「通過」させるだけです。 (同じタイプの)すべてのポイントは同じように見えるため、外側からは衝突やリバウンドと違いはありません。

ポイントとカーソルの衝突、「自分の」領域の境界からのポイントの跳ね返りなどを計算するには、すべてのセルの状態行列が必要です。2次元配列(n+4) * (m+4)
ここで、 (n+4)(m+4)はそれぞれセル内の競技場の幅と高さであり、最初のマトリックス要素は競技場の左上隅のセルに対応します。

各要素は対応するセルの状態を保存します。これには2つの兆候が含まれます。所属する領域の種類(海/土地)、およびカーソルが「海」に沿って移動した瞬間に通過するかどうかです。 この状態は、2ビットのビットフィールドに格納されます。 これを行うには、それぞれの属性ごとに2つのマスク定数を宣言する必要があります。

 var CA_CLEAR = 1 << 0; //  , ..    var CA_TRAIL = 1 << 1; //  -      "" 

最適化のために、状態の配列を2次元ではなく1次元にして、すべての要素が1行目(先頭)から1行ずつ格納されるようにします。 セル座標を配列インデックスと逆変換に変換するには、次の式を使用します。

 i = n * (y + 2) + x + 2; x = i % n - 2; y = Math.floor(i / n) - 2 

「私たちの」エリアの境界からのポイント(「海」または「陸」)のバウンスを計算するには、ポイント自体のセルをカウントせずに、3つのセルの状態を知る必要があります。 以下の擬似グラフィックを使用すると、これらのセルの位置が、45度の移動角度でのポイントの現在のセルに対して相対的に表示されます。

 OOO
 OX1
 O23

ポイントセルは十字でマークされ、1、2、3の数字が付いた目的のセルは単純に隣接セルを示します。 この場合のポイントの方向は南東に取得されます。これは、グリッドの縦座標軸(Y)が下を向いているためです。

示された3つのセルの少なくとも1つで、領域タイプがポイントタイプと反対の場合、境界からのリバウンドが発生します。 つまり たとえば、ポイントが「海」の場合、これらのセルの1つは「陸」でなければなりません。 さらに、この条件がセル1または2のいずれか(両方ではない)で満たされる場合、 +90または-90度がそれぞれ移動角度に追加されます。 それ以外の場合、移動角度は逆になります( +180度)。

他の動きの角度の場合、反発のロジックは明らかにまったく同じになります。

ポイントとカーソルの衝突によりゲームが一時停止し、その後、カーソルと「ランド」ポイントがゲームを開始するために元の位置に戻ります。カーソルは2番目の行の中央のセルに移動します。 。 「海」ポイントの位置は変わりません。

「ランド」ポイントとカーソルの衝突を判断するのは簡単です。 ポイントとカーソルのセルの座標を比較して、ポイントとカーソルの接触を確認するだけです。 「海」のポイントとカーソルの衝突の判定はもう少し複雑です。ポイント自体とカーソルの接触だけでなく、カーソルのトレースのタッチもチェックする必要があります。 これを行うには、セルの状態の2番目のビットを使用します。ポイントに隣接するすべてのセルをチェックします。

「征服された」領域の識別


すでに述べたように、実装が最も難しいのは、「海」から「征服された」領域の定義です。 これらは、カーソルが「海」を横切る結果として形成される閉じた領域であり、その内部には「海」の点はありません。 ほとんどの場合、これにより、利用可能な「海洋」領域を2つの部分に分割(カーソルの後に)して得られる2つの閉じた領域が作成され、そのうち1つだけが「征服」されます(スクリーンショット1を参照)。 しかし、いくつかの、特に複雑なケースでは、「征服された」エリアを含む多くの閉じたものがすぐに形成されます(スクリーンショット2を参照)。 さらに、カーソルトレース自体が閉じた領域を形成している場合もあります(スクリーンショット3を参照)。

スクリーンショット1

スクリーンショット2

スクリーンショット3

そのため、このような閉じた領域をすべて見つけて、それぞれのタイプ(「征服済み」/「非征服済み」)を判別する必要があります。

閉じた領域を見つけるには、近隣のセルを比較するための一般的なアルゴリズムを使用して、すべてのセルを反復処理できます。 たとえば、 ここでの実装方法。

しかし、別の方法があります(最終的に私はそれを選択しました):カーソルが「海」を横切るときに形成される閉じた領域の輪郭を使用します。 輪郭とは、1つのセルの厚さを持つ閉じたポリラインを意味します。 閉じた領域の輪郭がわかっている場合、その内容を見つけるためだけに残ります。 その中のすべてのセル。 しかし、これらの輪郭を見つける方法は?

閉じた領域の輪郭


一般的なケースでは、カーソルのトレースと部分的に交差する、つまり各回路についてのみ知っています。 少なくとも1つのカーソルトレースセルが含まれます。 ほとんどの場合、スクリーンショット1に示すように、正確に2つの輪郭が形成され、それぞれにカーソルトレース全体(すべてのセル)が含まれます。 ただし、スクリーンショット2のように、このような輪郭が多数存在する場合があり、それらの多くにはカーソルトレースセルの一部しか含まれない場合があります。 さらに、カーソルトレース全体が閉じたループを形成する場合(スクリーンショット3)を考慮する必要があります。 したがって、カーソルトレースデータに基づいて、できるだけ少ないセルをチェックしながら、それに隣接する閉じた領域の輪郭に関するすべての情報を取得する必要があります。

等高線の分布のさまざまなオプションを考慮すると、いくつかのパターンを見つけることができます。 第一に、カーソルトレース全体を含むパスの数は常に2以下です。第二に、カーソルトレースの両側ですべてのパスを2つのグループに分割すると、1つのグループのパスに共通のトレースセルがなくなります。 つまり、カーソルトレースの各セルは、トレースの各側の1つの輪郭にのみ属します。

上記に基づいて、輪郭を見つけるためのアルゴリズムを導き出すことができます。 一般的に、彼はこのように見えます。

カーソルのトレースに属するすべてのセルを、最初から(つまり、動きの開始から)ループします。 各トレースセルについて、2つの隣接するセル(移動方向で左右)の両側を確認します。 セルが「海洋」の場合、これは目的のエリアの1つの輪郭の一部です。 それ以外の場合は、トラックのこちら側に「海洋」セルが見つかるまで検索を続けます。 見つかったセルから、それに隣接する輪郭全体を描くことができます。 これを行うには、まずこのセルからトレースセルの方向に移動し、次に現在のセルの少なくとも片側に常に「ランド」セルが存在する方向に移動する必要があります(この場合、現在のセルは「海洋」でなければなりません)。 トレースセルが見つかるまで、つまり、回路が閉じられるまで続きます。 渡されたセルにポリラインを閉じるのに必要なトレースセルの一部を追加すると、目的の輪郭が得られます。

その後、トレースのセルを検索する外部サイクルを続け、1つのセルから輪郭を見つけるための上記の手順が停止したサイクルから開始します。

トラックの両側で輪郭を見つける手順は、互いに独立して実行する必要があることに注意してください。 これは、トレースのセルを検索するサイクルを、各側で1回ずつ、2回の繰り返しの別のサイクルでラップする必要があることを意味します。

両側に輪郭が見つからない場合があります。 これは、モーショントラックが「海洋」領域の境界に隣接していることを意味します。 この場合、トレース自体のみで構成される単一の閉じた領域があります。

スクリーンショット3から状況がある場合は、見つかったすべてのアウトラインに、カーソル移動のトレース全体によって形成されたアウトラインを追加する必要があります。

閉鎖エリアのコンテンツとタイプ


閉じた領域の輪郭が見つかったので、各回路の対応する領域(それに含まれるすべてのセル)の内容とそのタイプ(「征服された」または「されていない」)を決定する必要があります。 Xonixではカーソルは垂直/水平方向にしか移動できないため、各閉じた領域はサイズの異なる複数の長方形に分割できます。 したがって、閉じた領域の内容を決定するタスクは、その構成長方形を見つけることに削減されます。 ちなみに、これを行うことにより、2つの「鳥を1つの石で」殺すことができます。閉じた領域内のポイントの計算、および「征服された」領域の陰影(またはむしろ消去)を容易にします。

長方形を見つけるには、領域の輪郭の頂点を知るだけで十分です。 多角形の頂点。

主なアイデアは、各反復で最大の幅または高さを持つ突出した長方形を切り取ることです。 ここで言うと、3つの頂点が元のポリゴンに属する長方形を意味します。 これに基づいて、閉領域を長方形に分割するアルゴリズムを導出できます。 次のようになります。

最初の反復で、突出している長方形の一部である、最も長い長さの多角形の辺(セグメント)を見つけます。 そのようなセグメントが複数ある場合は、それらのいずれかを選択します。 見つかったセグメントは、目的の長方形の辺の1つになります。 次に、残りの部分を見つける必要があります。 これを行うには、見つかった側の両端から垂直にセグメントを取得し、最短のセグメントを選択します。 これは、長方形の2番目の辺になります。 3番目の側面を見つけるには、最初の側面に垂直な2番目のセグメントで、指定されたセグメントの2番目の端の(直交)投影を見つける必要があります。 見つかったポイントを最初の辺の対応する端に接続し、3番目の辺を取得します。 ここから、目的の突出した長方形全体を取得します。 次に、元のポリゴンから切り取る必要があります。 これを行うには、最初に見つかったセグメントと2番目に見つかったセグメント、および対応する頂点をポリゴンから削除し、見つかった投影ポイントを追加して、2番目に見つかったセグメントに接続されていた頂点に接続します。 その結果、元の頂点よりも2つの頂点が少ないポリゴンが得られます。

次の反復では、最初の反復と同じ方法で切り捨てられたポリゴンを処理します...など、クリッピングの結果として4つの頂点のポリゴンが得られるまで、つまり 長方形。

検討中のプロセスで切り取られるすべての長方形は、閉じた領域の内容になります。

特定の例で長方形に分割するプロセスを検討してください。
領域の輪郭であるポリゴンABCDEFGHIJKL (図1を参照)があるとします。 ステップごとに説明した分割アルゴリズムを適用します。

1.ポリゴンABCDEFGHIJKLの最も長い長さの辺を見つけます。 これは4の長さを持つCDセグメントです。 突出した長方形の一部ではありません。 したがって、それを無視してさらに調べます。 長さ3の3つのセグメントが見つかりました: ALFGGHGHCDと同じ理由で私たちに適していない。 そのため、セグメントALFGます。 それらのいずれかを選択します。 ALましょう。 それに垂直なセグメントはABKLで、そのうち最短はABです。 点BのセグメントKLへの射影を見つけます-これは点M (図2を参照)。 したがって、カットオフ長方形ABMLます。 クリッピング後、ポリゴンCDEFGHIJKMは残ります。

2.最長の長さを持つポリゴンCDEFGHIJKM辺を見つけます。 これは、長さ3のセグメントFGです...切り取られた長方形はFGNE (図2を参照)。 クリッピング後、ポリゴンCDNHIJKMは残ります。

3.ポリゴンCDNHIJKMの最も長い辺を見つけます。 これは、すでにおなじみの長さ4のCDセグメントです...切り取られた長方形はCDNOです。 クリッピング後、 OHIJKMポリゴンOHIJKMます。

4.最も長い長さのポリゴンOHIJKM辺を見つけます。 そのような当事者は2つあります。 これらは、長さ2のセグメントOHおよびHIです。最初のセグメントを選択しますOH ...カットオフ長方形はOHPMです。 クリッピング後、 KPIJ長方形KPIJます。 今、切断するものは何もありません。 これでアルゴリズムが完成しました。

その結果、閉じた領域のコンテンツを構成する5つの長方形、 ABMLFGNECDNOOHPMおよびKPIJ (図2を参照)。

1

2

閉じた領域が見つかったら、それらのそれぞれのタイプを判別する必要があります(「征服されている」かどうか)。 エリアのタイプは、その中の「航海」ポイントをカウントすることによって決定されます。 領域内のすべてのポイントを数える必要はありません。少なくとも1つのポイントがあるかどうかを調べるだけで十分です。 存在する場合、この領域は「征服」されません(したがって、私たちはそれを消去しません)。「征服された」領域に単一の点があってはならないからです。

一般に、任意の形状のポリゴンに特定の位置(座標)を持つポイントが含まれているかどうかを判断することは非常に困難です。 ただし、このために、このタスクを容易にするために、ポリゴンを長方形に分割します。 任意のポリゴンとは異なり、ポイントが長方形に属するかどうかを判断するのは簡単な作業です。 与えられたポイントの各座標が長方形の境界の対応する範囲に属しているかどうかを確認する必要があるだけです。

したがって、閉じた領域のタイプを決定するタスクは、この領域を構成する各長方形の内側の「海」点を検索することになります。

見つかったすべての「征服された」領域は消去の対象となります。消去はさらに簡単に実装されます。この領域を構成するすべての長方形を消去するだけです( clearRectメソッドを使用)。

アニメーション、ゲームコントロールなど


タイトルは少し不正です)。 記事はすでに終わりましたので、残念ながら上記のどれもここにはありません。 この記事translation )に基づいて書いたアニメーションコードと、 このtranslation )にのみ気付くことができます。

興味のある方のために、以下はゲームのコード全体です。 ただし、その値は疑わしいです。 そこにコメント-猫は泣いた。 しかし、私はうまくいけば基本的なロジックを説明しました。

ゲームコード
 // requestAnimationFrame/cancelAnimationFrame polyfill: (function() { var tLast = 0; var vendors = ['webkit', 'moz']; for(var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) { var v = vendors[i]; window.requestAnimationFrame = window[v+'RequestAnimationFrame']; window.cancelAnimationFrame = window[v+'CancelAnimationFrame'] || window[v+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var tNow = Date.now(); var dt = Math.max(0, 17 - tNow + tLast); var id = setTimeout(function() { callback(tNow + dt); }, dt); tLast = tNow + dt; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }()); (function() { window.picxonix = function(v1, v2) { if (typeof v1 != 'string') { return init(v1, v2); } switch (v1) { case 'level': //    loadLevel(v2); break; case 'end': //   endLevel(v2); break; case 'play': // /  setPlayMode(v2); break; case 'cursorDir': //    typeof v2 == 'string'? setDir(v2) : setDirToward(v2); break; case 'cursorSpeed': //    setCursorSpeed(v2); break; case 'enemySpeed': //    setEnemySpeed(v2); break; case 'enemySpawn': //     spawn(); break; case 'state': //    return buildLevelState(); default: } return 0; } var cfgMain = { width: 600, height: 400, sizeCell: 10, colorFill: '#000000', colorBorder: '#00aaaa', colorBall: '#ffffff', colorBallIn: '#000000', colorWarder: '#000000', colorWarderIn: '#f80000', colorCursor: '#aa00aa', colorCursorIn: '#00aaaa', colorTrail: '#a800a8', timeoutCollision: 1000, callback: null, callbackOnFrame: false }; var cfgLevel = { nBalls: 1, nWarders: 1, speedCursor: 5, speedEnemy: 5 }; // cell attributes: var CA_CLEAR = 1 << 0; var CA_TRAIL = 1 << 1; // : var sizeCell; var width, height; // : var elContainer; var ctxPic; var ctxMain; var imgPic; var imgBall; var imgWarder; var imgCursor; //  : var dirset; var cellset; var cursor; var aBalls = [], aWarders = []; var nBalls = 0, nWarders = 0; //   : var idFrame = 0; var tLevel = 0; var tLastFrame = 0; var tLocked = 0; var bCollision = false; var bConquer = false; var dirhash = { 'left': 180, 'right': 0, 'up': 270, 'down': 90, 'stop': false }; function init(el, opts) { if (elContainer || !el || !el.appendChild) return false; elContainer = el; //    : merge(cfgMain, opts); if (!cfgMain.sizeCell) return false; sizeCell = cfgMain.sizeCell; if (typeof cfgMain.callback != 'function') cfgMain.callback = null; //   : if (opts.speedCursor ^ opts.speedEnemy) { opts.speedCursor = opts.speedEnemy = Math.max(opts.speedCursor || 0, opts.speedEnemy || 0); } merge(cfgLevel, opts); setLevelData(cfgMain.width, cfgMain.height); var oWrap = document.createElement('div'); oWrap.style.position = 'relative'; //    (): (function() { var canvas = document.createElement('canvas'); ctxPic = canvas.getContext('2d'); canvas.width = width; canvas.height = height; canvas.style.position = 'absolute'; canvas.style.left = canvas.style.top = (2*sizeCell) + 'px'; ctxPic.fillStyle = cfgMain.colorTrail; ctxPic.fillRect(0, 0, width, height); oWrap.appendChild(canvas); }()); //    : (function() { var canvas = document.createElement('canvas'); ctxMain = canvas.getContext('2d'); canvas.width = width+ 4*sizeCell; canvas.height = height+ 4*sizeCell; canvas.style.position = 'absolute'; canvas.style.left = canvas.style.top = 0; fillCanvas(); ctxMain.fillStyle = cfgMain.colorFill; ctxMain.fillRect(2*sizeCell, 2*sizeCell, width, height); oWrap.appendChild(canvas); }()); elContainer.appendChild(oWrap); //   : var canvas = document.createElement('canvas'); var ctxTmp = canvas.getContext('2d'); canvas.width = sizeCell; canvas.height = sizeCell; //    : var r = sizeCell / 2, q = sizeCell / 4; ctxTmp.clearRect(0, 0, sizeCell, sizeCell); ctxTmp.beginPath(); ctxTmp.arc(r, r, r, 0, Math.PI * 2, false); ctxTmp.fillStyle = cfgMain.colorBall; ctxTmp.fill(); if (cfgMain.colorBallIn) { ctxTmp.beginPath(); ctxTmp.arc(r, r, q, 0, Math.PI * 2, false); ctxTmp.fillStyle = cfgMain.colorBallIn; ctxTmp.fill(); } imgBall = new Image(); imgBall.src = ctxTmp.canvas.toDataURL(); function prepareSquare(colorOut, colorIn) { ctxTmp.clearRect(0, 0, sizeCell, sizeCell); ctxTmp.fillStyle = colorOut; ctxTmp.fillRect(0, 0, sizeCell, sizeCell); if (colorIn) { ctxTmp.fillStyle = colorIn; ctxTmp.fillRect(q, q, sizeCell - r, sizeCell - r); } } //    : prepareSquare(cfgMain.colorWarder, cfgMain.colorWarderIn); imgWarder = new Image(); imgWarder.src = ctxTmp.canvas.toDataURL(); //   : prepareSquare(cfgMain.colorCursor, cfgMain.colorCursorIn); imgCursor = new Image(); imgCursor.src = ctxTmp.canvas.toDataURL(); return {width: width+ 4*sizeCell, height: height+ 4*sizeCell}; } function loadLevel(data) { if (tLevel || tLastFrame || !data || !data.image) return; if (!data.image) return; var img = new Image(); img.onload = function() { applyLevel(img, data); }; img.src = data.image; } function applyLevel(img, data) { imgPic = img; merge(cfgLevel, data, true); setLevelData(img.width, img.height); ctxMain.canvas.width = width+ 4*sizeCell; ctxMain.canvas.height = height+ 4*sizeCell; fillCanvas(); cellset.reset(); ctxPic.canvas.width = width; ctxPic.canvas.height = height; ctxPic.drawImage(imgPic, 0, 0, width, height, 0, 0, width, height); var pos = cellset.placeCursor(); cursor.reset(pos[0], pos[1]); aBalls = []; aWarders = []; var i, aPos; aPos = cellset.placeBalls(nBalls); for (i = 0; i < nBalls; i++) aBalls.push(new Enemy(aPos[i][0], aPos[i][1], false)); aPos = cellset.placeWarders(nWarders); for (i = 0; i < nWarders; i++) aWarders.push(new Enemy(aPos[i][0], aPos[i][1], true, 45)); tLevel = Date.now(); tLastFrame = 0; startLoop(); } function endLevel(bClear) { if (tLastFrame) return; tLevel = 0; if (!bClear) return; fillCanvas(); ctxMain.clearRect(2*sizeCell, 2*sizeCell, width, height); } function setLevelData(w, h) { if (w) width = w - w % (2*sizeCell); if (h) height = h - h % (2*sizeCell); if (cfgLevel.nBalls) nBalls = cfgLevel.nBalls; if (cfgLevel.nWarders) nWarders = cfgLevel.nWarders; } function setPlayMode(bOn) { if (bOn ^ !tLastFrame) return; tLastFrame? endLoop() : startLoop(); } function setDir(key) { if (!tLastFrame) return; if (key in dirhash) cursor.setDir(dirhash[key]); } function setDirToward(pos) { if (!tLastFrame || !pos || pos.length < 2) return; var xc = Math.floor(pos[0] / sizeCell) - 2, yc = Math.floor(pos[1] / sizeCell) - 2; var b = cellset.isPosValid(xc, yc); if (!b) return; var posCr = cursor.pos(), dirCr = cursor.getDir(), dir = false; if (dirCr === false) { var dx = xc - posCr[0], dy = yc - posCr[1], dc = Math.abs(dx) - Math.abs(dy); if (dc == 0) return; dir = dirset.find(dx, dy); if (dir % 90 != 0) { var dir1 = dir-45, dir2 = dir+45; dir = dir1 % 180 == 0 ^ dc < 0? dir1 : dir2; } } else { var delta = dirCr % 180? xc - posCr[0] : yc - posCr[1]; if (!delta) return; dir = (delta > 0? 0 : 180) + (dirCr % 180? 0 : 90); } cursor.setDir(dir); } function setCursorSpeed(v) { if (v > 0) cfgLevel.speedCursor = v; } function setEnemySpeed(v) { if (v > 0) cfgLevel.speedEnemy = v; } function startLoop() { if (!tLevel) return; idFrame = requestAnimationFrame(loop); } function endLoop() { if (idFrame) cancelAnimationFrame(idFrame); tLastFrame = idFrame = 0; } //    function loop(now) { var dt = tLastFrame? (now - tLastFrame) / 1000 : 0; bCollision = bConquer = false; if (!tLastFrame || update(dt)) { render(); tLastFrame = now; } if (bCollision) { lock(); cfgMain.callback && cfgMain.callback(1); return; } if (bConquer) { bConquer = false; tLastFrame = 0; cellset.conquer(); if (cfgMain.callback && cfgMain.callback(2)) return; } else cfgMain.callback && cfgMain.callbackOnFrame && cfgMain.callback(0); startLoop(); } function update(dt) { var distCursor = Math.round(dt * cfgLevel.speedCursor), distEnemy = Math.round(dt * cfgLevel.speedEnemy); if (!(distCursor >= 1 || distEnemy >= 1)) return false; cursor.update(distCursor); var i; for (i = 0; i < nBalls; i++) aBalls[i].update(distEnemy); for (i = 0; i < nWarders; i++) aWarders[i].update(distEnemy); return true; } function render() { cellset.render(); cursor.render(); var i; for (i = 0; i < nBalls; i++) aBalls[i].render(); for (i = 0; i < nWarders; i++) aWarders[i].render(); } function lock() { tLastFrame = 0; bCollision = false; var posCr = cursor.pos(); cellset.add2Trail(posCr[0], posCr[1], false); setTimeout(unlock, cfgMain.timeoutCollision); } function unlock() { if (!tLevel) return; cellset.clearTrail(); var pos = cellset.placeCursor(); cursor.reset(pos[0], pos[1], true); var aPos = cellset.placeWarders(nWarders); for (var i = 0; i < nWarders; i++) aWarders[i].reset(aPos[i][0], aPos[i][1]); startLoop(); } function spawn() { if (!tLevel) return; var pos = cellset.placeSpawned(); if (!pos) return; aWarders.push(new Enemy(pos[0], pos[1], true)); nWarders++; } function buildLevelState() { return { play: Boolean(tLastFrame), posCursor: cursor.pos(), warders: nWarders, speedCursor: cfgLevel.speedCursor, speedEnemy: cfgLevel.speedEnemy, cleared: cellset.getPercentage() }; } function fillCanvas() { ctxMain.fillStyle = cfgMain.colorBorder; ctxMain.fillRect(0, 0, width+ 4*sizeCell, height+ 4*sizeCell); } function drawCellImg(img, x, y) { ctxMain.drawImage(img, 0, 0, sizeCell, sizeCell, (x+2)*sizeCell, (y+2)*sizeCell, sizeCell, sizeCell ); } function clearCellArea(x, y, w, h) { ctxMain.clearRect( (x+2)*sizeCell, (y+2)*sizeCell, (w || 1)* sizeCell, (h || 1)* sizeCell ); } function fillCellArea(color, x, y, w, h) { ctxMain.fillStyle = color; ctxMain.fillRect( (x+2)*sizeCell, (y+2)*sizeCell, (w || 1)* sizeCell, (h || 1)* sizeCell ); } //   : dirset = { vecs: { 0: [1, 0], 45: [1, 1], 90: [0, 1], 135: [-1, 1], 180: [-1, 0], 225: [-1, -1], 270: [0, -1], 315: [1, -1] }, get: function(v) { return v in this.vecs? this.vecs[v] : [0, 0]; }, find: function(x, y) { x = x == 0? 0 : (x > 0? 1 : -1); y = y == 0? 0 : (y > 0? 1 : -1); for (var v in this.vecs) { var vec = this.vecs[v]; if (vec[0] == x && vec[1] == y) return parseInt(v); } return false; } }; //    : cellset = { nW: 0, nH: 0, nWx: 0, nCleared: 0, dirTrail: 0, iPreTrail: 0, aCells: [], aTrail: [], aTrailNodes: [], aTrailRects: [], reset: function() { var nW = this.nW = Math.floor(width / sizeCell); var nH = this.nH = Math.floor(height / sizeCell); var n = (this.nWx = nW+4)* (nH+4); this.nCleared = 0; this.aCells = []; var aAll = []; for (var i = 0; i < n; i++) { var pos = this.pos(i), x = pos[0], y = pos[1]; this.aCells.push(x >= 0 && x < nW && y >= 0 && y < nH? 0 : CA_CLEAR); aAll.push(i); } fillCellArea(cfgMain.colorFill, 0, 0, nW, nH); }, render: function() { if (this.aTrailRects.length) { for (var i = this.aTrailRects.length-1; i >= 0; i--) { fillCellArea.apply(null, [cfgMain.colorFill].concat(this.aTrailRects[i])); } this.aTrailRects = []; } }, isPosIn: function(x, y) { return x >= 0 && x < this.nW && y >= 0 && y < this.nH; }, isPosValid: function(x, y) { return x >= -2 && x < this.nW+2 && y >= -2 && y < this.nH+2; }, find: function(x, y) { return this.isPosValid(x, y) ? (this.nWx)*(y+2) + x+2 : -1; }, pos: function(i) { return [i % this.nWx - 2, Math.floor(i / this.nWx)-2]; }, posMap: function(arr) { var _this = this; return arr.map(function(v) { return _this.pos(v) }); }, value: function(x, y) { var i = this.find(x,y); return i >= 0? this.aCells[i] : 0; }, set: function(x, y, v) { var i = this.find(x,y); if (i >= 0) this.aCells[i] = v; return i; }, setOn: function(x, y, v) { var i = this.find(x,y); if (i >= 0) this.aCells[i] |= v; return i; }, setOff: function(x, y, v) { var i = this.find(x,y); if (i >= 0) this.aCells[i] &= ~v; return i; }, placeCursor: function() { return [Math.floor(this.nW/2), -2]; }, placeBalls: function(n) { var a = [], ret = []; for (var i = 0; i < n; i++) { var k; do k = Math.floor(Math.random() * this.nW * this.nH); while (a.indexOf(k) >= 0); a.push(k); var x = k % this.nW, y = Math.floor(k / this.nW); ret.push([x, y]); } return ret; }, placeWarders: function(n) { var z; var aPos = [ [Math.floor(this.nW/2), this.nH+1], [-1, this.nH+1], [this.nW, this.nH+1], [-1, -2], [this.nW, -2], [-1, z = Math.floor(this.nH/2)], [this.nW, z], [z = Math.floor(this.nW/4), this.nH+1], [3*z, this.nH+1] ]; var i0 = (n+ 1)% 2; return aPos.slice(i0, Math.min(n+ i0, 9)); }, placeSpawned: function() { if (nWarders >= 9) return false; function dist(pos1, pos2) { return Math.pow(pos1[0]- pos2[0], 2) + Math.pow(pos1[1]- pos2[1], 2); } function find(pos0) { var n = nWarders; for (var l = 0; l < x0; l++) { for (var dx = -1; dx <= 1; dx+= 2) { var p = [pos0[0]+ l* dx, pos0[1]]; for (var i = 0; i < n && dist(aWarders[i].pos(), p) >= 4; i++) ; if (i >= n) return p; } } return pos0; } var x0 = Math.floor(this.nW/2); var aPos = [[x0, this.nH+1], [x0, -2]]; var posCr = cursor.pos(); var posSt = dist(aPos[0], posCr) > dist(aPos[1], posCr)? aPos[0] : aPos[1]; var ret = find(posSt); return ret; }, applyRelDirs: function(x, y, dir, aDeltas) { var ret = []; for (var n = aDeltas.length, i = 0; i < n; i++) { var d = (dir + aDeltas[i] + 360) % 360; var vec = dirset.get(d), xt, yt; ret.push([xt = x + vec[0], yt = y + vec[1], d, this.value(xt, yt)]); } return ret; }, add2Trail: function(x, y, dir) { var i = this.setOn(x, y, CA_TRAIL); if (i < 0) return; var n = this.aTrail.length; if (!n || dir !== this.dirTrail) { var iNode = n? this.aTrail[n-1] : i; if (!n || iNode != this.aTrailNodes[this.aTrailNodes.length-1]) this.aTrailNodes.push(iNode); if (!n) { var aPos = this.applyRelDirs(x, y, dir, [180]); this.iPreTrail = this.find(aPos[0][0], aPos[0][1]); } } this.aTrail.push(i); this.dirTrail = dir; }, lastTrailLine: function() { var pos0 = this.pos(this.aTrailNodes[this.aTrailNodes.length-1]), pos = this.pos(this.aTrail[this.aTrail.length-1]); return [ Math.min(pos[0], pos0[0]), Math.min(pos[1], pos0[1]), Math.abs(pos[0] - pos0[0])+1, Math.abs(pos[1] - pos0[1])+1 ]; }, clearTrail: function() { this.aTrailRects = this._buildTrailRects(); for (var n = this.aTrail.length, i = 0; i < n; i++) { this.aCells[this.aTrail[i]] &= ~CA_TRAIL; } this.aTrail = []; this.aTrailNodes = []; }, getPreTrail: function() { return this.iPreTrail; }, conquer: function() { var nTrail = this.aTrail.length; if (!nTrail) return; if (nTrail > 1) this.aTrailNodes.push(this.aTrail[nTrail-1]); var aConqRects = this._conquer() || this._buildTrailRects(); this.aTrail = []; this.aTrailNodes = []; if (!aConqRects || !aConqRects.length) return; for (var n = aConqRects.length, i = 0; i < n; i++) { var rect = aConqRects[i]; var x0 = rect[0], y0 = rect[1], w = rect[2], h = rect[3]; for (var x = 0; x < w; x++) { for (var y = 0; y < h; y++) { if (this.value(x + x0, y + y0, CA_CLEAR) & CA_CLEAR) continue; this.set(x + x0, y + y0, CA_CLEAR); this.nCleared++; } } } for (i = 0; i < n; i++) { clearCellArea.apply(null, aConqRects[i]); } aConqRects = []; }, getPercentage: function() { return this.nCleared / (this.nW * this.nH) * 100; }, _conquer: function() { var nTrail = this.aTrail.length, nNodes = this.aTrailNodes.length; var dz = Math.abs(this.aTrailNodes[0] - this.aTrailNodes[nNodes-1]); var aOutlineset = [], bClosedTrail = false; if (bClosedTrail = nNodes >= 4 && dz == 1 || dz == this.nWx) { aOutlineset.push([this.aTrailNodes, 1]); } var bAddTrail = false; var posPre = this.pos(this.iPreTrail), posCr = cursor.pos(); var aDeltas = [-90, 90]; for (var d = 0; d < 2; d++) { var dd = aDeltas[d]; var k = 0; var sum = 0, bSum = false, bEndAtNode = false; for (var l = 0; l < nTrail && sum < nTrail; l++) { var iStart = this.aTrail[l]; var pos = this.pos(iStart); var pos0 = l? this.pos(this.aTrail[l - 1]) : posPre; var x = pos[0], y = pos[1]; var dir = (dirset.find(x - pos0[0], y - pos0[1]) + dd + 360) % 360; var aDirs = bEndAtNode? [] : [dir]; if (this.aTrailNodes.indexOf(iStart) >= 0) { var pos2 = l < nTrail - 1? this.pos(this.aTrail[l + 1]) : posCr; dir = (dirset.find(pos2[0] - x, pos2[1] - y) + dd + 360) % 360; if (dir != aDirs[0]) aDirs.push(dir); } if (this.aTrail[l] == this.aTrailNodes[k+1]) ++k; var ret = 0; for (var nDs = aDirs.length, j = 0; j < nDs && !ret; j++) { dir = aDirs[j]; var vec = dirset.get(dir); var xt = x + vec[0], yt = y + vec[1]; var v = this.value(xt, yt); if (v & CA_CLEAR || v & CA_TRAIL) continue; ret = this._outline(xt, yt, dir); if (!ret || ret.length < 3) return false; } bEndAtNode = false; if (!ret) continue; var len = ret[0], aNodes = ret[1], bClosed = ret[2], iEnd = aNodes[aNodes.length-1]; if (bClosed) { aOutlineset.push([aNodes, len]); bSum = true; continue; } var aXtra = [iStart]; for (var i = l+1; i < nTrail && this.aTrail[i] != iEnd; i++) { if (this.aTrail[i] == this.aTrailNodes[k+1]) aXtra.push(this.aTrailNodes[++k]); } if (i >= nTrail) continue; aOutlineset.push([aNodes.concat(aXtra.reverse()), len + i - l]); sum += i - l + 1; l = (bEndAtNode = this.aTrail[i] == this.aTrailNodes[k+1])? i-1 : i; } if (!sum && !bSum && !bClosedTrail) return false; if (sum < nTrail && !bClosedTrail) bAddTrail = true; } if (!aOutlineset.length) return false; aOutlineset.sort(function (el1, el2) { return el1[1] - el2[1]; }); var aRects = [], n = aOutlineset.length, b = false; for (i = 0; i < n; i++) { if (i == n- 1 && !b) break; ret = this._buildConquerRects(aOutlineset[i][0]); if (ret) aRects = aRects.concat(ret); else b = true; } if (!aRects.length) return false; return bAddTrail? aRects.concat(this._buildTrailRects()) : aRects; }, _outline: function(x0, y0, dir) { var aNodes = [], aUniqNodes = [], aUsedDirs = [], aBackDirs = []; var x = x0, y = y0, lim = 6 * (this.nW + this.nH), n = 0, bClosed = false; function isClear(arr) { return arr[3] & CA_CLEAR; } do { bClosed = n && x == x0 && y == y0; var iCurr = this.find(x,y), iUniq = aUniqNodes.indexOf(iCurr); var aCurrUsed = iUniq >= 0? aUsedDirs[iUniq] : []; var aCurrBack = iUniq >= 0? aBackDirs[iUniq] : []; var aPosOpts = this.applyRelDirs(x,y, dir, [-90, 90, 0]); var aTestDirs = [180+45, -45, 45, 180-45, -45, 45]; var aPassIdx = [], aPassWeight = []; for (var i = 0; i < 3; i++) { var d = aPosOpts[i][2]; if (aCurrUsed.indexOf(d) >= 0) continue; if (isClear(aPosOpts[i])) continue; var aTestOpts = this.applyRelDirs(x,y, dir, aTestDirs.slice(i*2,i*2+2)); var b1 = isClear(aTestOpts[0]), b2 = isClear(aTestOpts[1]); var b = b1 || b2 || (i == 2? isClear(aPosOpts[0]) || isClear(aPosOpts[1]) : isClear(aPosOpts[2])); if (!b) continue; aPassIdx.push(i); aPassWeight.push( (b1 && b2? 0 : b1 || b2? 1 : 2) + (aCurrBack.indexOf(d) >= 0? 3 : 0) ); } var nPass = aPassIdx.length; var min = false, idx = false; for (i = 0; i < nPass; i++) { if (!i || aPassWeight[i] < min) { min = aPassWeight[i]; idx = aPassIdx[i]; } } var pos = nPass? aPosOpts[idx] : this.applyRelDirs(x,y, dir, [180])[0]; var dir0 = dir; x = pos[0]; y = pos[1]; dir = pos[2]; if (pos[2] == dir0) continue; nPass? aNodes.push(iCurr) : aNodes.push(iCurr, iCurr); dir0 = (dir0 + 180) % 360; if (iUniq < 0) { aUniqNodes.push(iCurr); aUsedDirs.push([dir]); aBackDirs.push([dir0]); } else { aUsedDirs[iUniq].push(dir); aBackDirs[iUniq].push(dir0); } } while (n++ < lim && !(this.value(x, y) & CA_TRAIL)); if (!(n < lim)) return false; if (bClosed) { aNodes.push(iCurr); if (aNodes[0] != (iCurr = this.find(x0,y0))) aNodes.unshift(iCurr); var nNodes = aNodes.length; if (nNodes % 2 && aNodes[0] == aNodes[nNodes-1]) aNodes.pop(); } else aNodes.push(this.find(x,y)); return [n+1, aNodes, bClosed]; }, _buildTrailRects: function() { if (this.aTrailNodes.length == 1) this.aTrailNodes.push(this.aTrailNodes[0]); var aRects = []; for (var n = this.aTrailNodes.length, i = 0; i < n-1; i++) { var pos1 = this.pos(this.aTrailNodes[i]), pos2 = this.pos(this.aTrailNodes[i+1]); var x0 = Math.min(pos1[0], pos2[0]), y0 = Math.min(pos1[1], pos2[1]); var w = Math.max(pos1[0], pos2[0]) - x0 + 1, h = Math.max(pos1[1], pos2[1]) - y0 + 1; var rect = [x0, y0, w, h]; aRects.push(rect); } return aRects; }, _buildConquerRects: function(aOutline) { if (aOutline.length < 4) return false; var aNodes = this.posMap(aOutline); var n = aNodes.length; if (n > 4 && n % 2 != 0) { var b1 = aNodes[0][0] == aNodes[n-1][0], b2; if (b1 ^ aNodes[0][1] == aNodes[n-1][1]) { b2 = aNodes[n-2][0] == aNodes[n-1][0]; if (!(b2 ^ b1) && b2 ^ aNodes[n-2][1] == aNodes[n-1][1]) aNodes.pop(); b2 = aNodes[0][0] == aNodes[1][0]; if (!(b2 ^ b1) && b2 ^ aNodes[0][1] == aNodes[1][1]) aNodes.shift(); } b1 = aNodes[0][0] == aNodes[1][0]; b2 = aNodes[1][0] == aNodes[2][0]; if (!(b1 ^ b2) && b1 ^ aNodes[0][1] == aNodes[1][1] && b2 ^ aNodes[1][1] == aNodes[2][1]) aNodes.shift(); } if (aNodes.length % 2 != 0) return false; var aRects = []; for (var l = 0; l < 10 && aNodes.length > 4; l++) { n = aNodes.length; var dim1 = 0, dim2 = 0, iBase = 0, iCo = 0; var posB1, posB2, posT1, posT2; for (var i = 0; i < n; i++) { posB1 = aNodes[i]; posB2 = aNodes[(i+1)%n]; posT1 = aNodes[(i-1+n)%n]; posT2 = aNodes[(i+2)%n]; var dir = dirset.find(posT1[0]-posB1[0], posT1[1]-posB1[1]); if (dir != dirset.find(posT2[0]-posB2[0], posT2[1]-posB2[1])) continue; var dirTest = Math.floor((dirset.find(posB2[0]-posB1[0], posB2[1]-posB1[1])+ dir) / 2); var vec = dirset.get(dirTest - dirTest% 45); if (this.value([posB1[0]+ vec[0], posB1[1]+ vec[1]]) & CA_CLEAR) continue; var b = false, t, w, k; if ((t = Math.abs(posB1[0]-posB2[0])) > dim1) { b = true; k = 0; w = t; } if ((t = Math.abs(posB1[1]-posB2[1])) > dim1) { b = true; k = 1; w = t; } if (!b) continue; var k2 = (k+1)%2; vec = dirset.get(dir); var sgn = vec[k2]; var co2 = posB1[k2]; var left = Math.min(posB1[k], posB2[k]), right = Math.max(posB1[k], posB2[k]); var min = Math.min(sgn* (posT1[k2]- co2), sgn* (posT2[k2]- co2)); for (var j = i% 2; j < n; j+= 2) { if (j == i) continue; var pos = aNodes[j], pos2 = aNodes[(j+1)%n], h; if (pos[k2] == pos2[k2] && (h = sgn*(pos[k2]- co2)) >= 0 && h < min && pos[k] > left && pos[k] < right && pos2[k] > left && pos2[k] < right) break; } if (j < n) continue; dim1 = w; dim2 = sgn*min; iBase = i; iCo = k; } var iB2 = (iBase+1)%n, iT1 = (iBase-1+n)%n, iT2 = (iBase+2)%n; posB1 = aNodes[iBase]; posB2 = aNodes[iB2]; posT1 = aNodes[iT1]; posT2 = aNodes[iT2]; var aDim = [0, 0], pos0 = []; var iCo2 = (iCo+1)%2; aDim[iCo] = dim1; aDim[iCo2] = dim2; pos0[iCo] = Math.min(posB1[iCo], posB2[iCo]); pos0[iCo2] = Math.min(posB1[iCo2], posB2[iCo2]) + (aDim[iCo2] < 0? aDim[iCo2]: 0); var rect = [pos0[0], pos0[1], Math.abs(aDim[0])+1, Math.abs(aDim[1])+1]; var bC = Math.abs(posT1[iCo2] - posB1[iCo2]) == Math.abs(dim2); if (this._containBall(rect)) return false; aRects.push(rect); if (bC) { posB2[iCo2] += dim2; aNodes.splice(iBase,1); aNodes.splice(iT1 < iBase? iT1 : iT1-1, 1); } else { posB1[iCo2] += dim2; aNodes.splice(iT2,1); aNodes.splice(iB2 < iT2? iB2 : iB2-1, 1); } } var aX = aNodes.map(function(v) {return v[0]}); var aY = aNodes.map(function(v) {return v[1]}); var x0 = Math.min.apply(null, aX); var y0 = Math.min.apply(null, aY); rect = [x0, y0, Math.max.apply(null, aX)-x0+1, Math.max.apply(null, aY)-y0+1]; if (this._containBall(rect)) return false; aRects.push(rect); return aRects; }, // ,   .   : _containBall: function(rect) { var x1 = rect[0], x2 = x1+ rect[2] - 1; var y1 = rect[1], y2 = y1+ rect[3] - 1; for (var i = 0; i < nBalls; i++) { var o = aBalls[i], x = ox, y = oy; if (x >= x1 && x <= x2 && y >= y1 && y <= y2) return true; } return false; } }; // : cursor = { x: 0, //  x  y: 0, //  y  x0: 0, //  x  y0: 0, //  y  dir: false, //    ( ) state: false, //    (true -  ) state0: false, //    //   : reset: function(x, y, bUnlock) { var bPre = bUnlock && cellset.value(this.x, this.y) & CA_CLEAR; this.x0 = bPre? this.x : x; this.y0 = bPre? this.y : y; this.x = x; this.y = y; this.dir = this.state = this.state0 = false; }, //   -    : update: function(dist) { if (this.dir === false) return; var x = this.x, y = this.y; var vec = dirset.get(this.dir), vecX = vec[0], vecY = vec[1]; var bEnd = false; for (var n = 0; n < dist; n++) { if (cellset.find(x + vecX, y + vecY) < 0) { this.dir = false; break; } x += vecX; y += vecY; if (cellset.value(x, y) & CA_TRAIL) { bCollision = true; break; } var b = cellset.value(x, y) & CA_CLEAR; if (this.state && b) { bEnd = true; break; } this.state = !b; if (this.state) cellset.add2Trail(x, y, this.dir); } this.x = x; this.y = y; if (!bEnd) return; if (cellset.getPreTrail() == cellset.find(x,y)) bCollision = true; else { this.dir = this.state = false; bConquer = true; } }, //   : render: function() { if (this.x0 == this.x && this.y0 == this.y) { if (tLastFrame) return; } else { if (this.state0) { var rect = cellset.lastTrailLine(); fillCellArea.apply(null, [cfgMain.colorTrail].concat(rect)); } else { if (cellset.isPosIn(this.x0, this.y0)) clearCellArea(this.x0, this.y0); else fillCellArea(cfgMain.colorBorder, this.x0, this.y0); } this.x0 = this.x; this.y0 = this.y; } this.state0 = this.state; drawCellImg(imgCursor, this.x, this.y); }, //   : pos: function() { return [this.x, this.y]; }, //    : getDir: function() { return this.dir; }, //   : setDir: function(dir) { if (dir === this.dir) return; if (this.state && this.dir !== false && Math.abs(dir - this.dir) == 180) return; this.dir = dir; } }; //    (  ): function Enemy(x, y, type, dir) { this.x = x; this.y = y; this.x0 = x; this.y0 = y; var aDirs = [45, 135, 225, 315]; this.dir = dir === undefined? aDirs[Math.floor(Math.random()*4)] : dir; //    this.type = Boolean(type); // (boolean)   (false - , true - ) } //   : Enemy.prototype = { //  : reset: function(x, y) { this.x = x; this.y = y; }, //   -    : update: function(dist) { var ret = this._calcPath(this.x, this.y, dist, this.dir); this.x = ret.x; this.y = ret.y; this.dir = ret.dir; }, //   : render: function() { if (this.x0 == this.x && this.y0 == this.y) { if (tLastFrame) return; } else { if (this.type && cellset.isPosIn(this.x0, this.y0)) clearCellArea(this.x0, this.y0); else fillCellArea(this.type? cfgMain.colorBorder : cfgMain.colorFill, this.x0, this.y0); this.x0 = this.x; this.y0 = this.y; } drawCellImg(this.type? imgWarder : imgBall, this.x, this.y); }, //   : pos: function() { return [this.x, this.y]; }, //    (): _calcPath: function(x, y, dist, dir) { var vec = dirset.get(dir), vecX = vec[0], vecY = vec[1]; var posCr = cursor.pos(); var xC = posCr[0], yC = posCr[1], vC = cellset.value(xC, yC), bC = !this.type ^ vC & CA_CLEAR; if (bC && Math.abs(x - xC) <= 1 && Math.abs(y - yC) <= 1 || !this.type && this._isCollision(x, y, dir)) { bCollision = true; } for (var n = 0; n < dist && !bCollision; n++) { var xt = x + vecX, yt = y + vecY; var dirB = this._calcBounce(x, y, dir, xt, yt); if (dirB !== false) return this._calcPath(x, y, dist - n, dirB); if (bC && Math.abs(xt - xC) <= 1 && Math.abs(yt - yC) <= 1 || !this.type && this._isCollision(xt, yt, dir)) bCollision = true; if (!this.type && !cellset.isPosIn(xt, yt)) break; x = xt; y = yt; } return {x: x, y: y, dir: dir}; }, //       ( ): _calcBounce: function(x, y, dir, xt, yt) { var ret = cellset.applyRelDirs(x,y, dir, [-45, 45]); var b1 = this.type ^ ret[0][3] & CA_CLEAR, b2 = this.type ^ ret[1][3] & CA_CLEAR; return b1 ^ b2? (b1? dir + 90 : dir + 270) % 360 : this.type ^ cellset.value(xt, yt) & CA_CLEAR || b1 && b2? (dir+180) % 360 : false; }, //     : _isCollision: function(x, y, dir) { if (cellset.value(x, y) & CA_TRAIL) return true; var aDirs = [-45, 45, -90, 90]; for (var i = 0; i < 4; i++) { var d = (dir + aDirs[i] + 360) % 360, vec = dirset.get(d); if (cellset.value(x + vec[0], y + vec[1]) & CA_TRAIL) return true; } return false; } }; function merge(dest, src, bFilter) { if (!src) return dest; for(var key in dest) { if (!dest.hasOwnProperty(key) || !src.hasOwnProperty(key)) continue; var v = src[key]; if ((!bFilter || v) && (typeof v != 'number' || v >= 0)) dest[key] = v; } return dest; } })(); 

シムの締めくくり。 ご清聴ありがとうございました。

Source: https://habr.com/ru/post/J261853/


All Articles