Dot War

ドットが戦う! 1,2,3どれかの入力をしてね

1. 自分でドット絵を描く (左のみ)

2. 左と右の枠にローカル画像をドラッグ&ドロップ

3. Twitter/Xのユーザー名からアイコン画像を取得

view source

JavaScript

document.title = 'Dot War ドット絵アイコンが戦うゲーム! ';

const dotCanvas1 = document.getElementById('dotCanvas1');
const dotCanvas2 = document.getElementById('dotCanvas2');
const mapCanvas = document.getElementById('mapCanvas');
const unitPreview = document.getElementById('unitPreview');
const palette = document.getElementById('palette');

const dotCtx1 = dotCanvas1.getContext('2d');
const dotCtx2 = dotCanvas2.getContext('2d');
const mapCtx = mapCanvas.getContext('2d');

const gridSize = 16;
const mapSize = 32;
const cellSize = 20;

dotCanvas1.width = gridSize * cellSize;
dotCanvas1.height = gridSize * cellSize;
dotCanvas2.width = gridSize * cellSize;
dotCanvas2.height = gridSize * cellSize;
mapCanvas.width = mapSize * cellSize;
mapCanvas.height = (mapSize / 2) * cellSize;

const colors = [
  '#1A1A1A', // 暗グレー
  '#CCCCCC', // 明グレー
  '#D32F2F', // 暖色赤
  '#388E3C', // 落ち着いた緑
  '#1976D2', // 青
  '#FBC02D', // 黄(濁った)
  '#8E24AA', // 紫
  '#0097A7', // 青緑
];

const unitPresets = [
  {
    type: 0,
    name: "闇",
    color: "#000000",
    hp: 9,
    speed: 0.05,
    effectiveness: [100, 250, 50, 50, 50, 50, 50, 50],
    moveStyle: "outer",     // 外周へ逃げつつ接近
  },
  {
    type: 1,
    name: "光",
    color: "#CCCCCC",
    hp: 11,
    speed: 0.7,
    effectiveness: [400, 100, 50, 50, 50, 50, 50, 50],
    moveStyle: "simple",    // 直進型
  },
  {
    type: 2,
    name: "炎",
    color: "#FF0000",
    hp: 10,
    speed: 0.2,
    effectiveness: [200, 150, 50, 200, 20, 100, 100, 100],
    moveStyle: "simple",    // 直進型
  },
  {
    type: 3,
    name: "草",
    color: "#00FF00",
    hp: 10,
    speed: 0.3,
    effectiveness: [200, 150, 20, 50, 200, 100, 100, 100],
    moveStyle: "zigzag",    // ジグザグ前進
  },
  {
    type: 4,
    name: "水",
    color: "#0000FF",
    hp: 10,
    speed: 0.2,
    effectiveness: [200, 150, 200, 20, 50, 100, 100, 100],
    moveStyle: "simple",    // 直進型(遠距離狙い)
  },
  {
    type: 5,
    name: "雷",
    color: "#FFFF00",
    hp: 8,
    speed: 0.4,
    effectiveness: [80, 80,  40, 100, 150, 120, 50, 300],
    moveStyle: "zigzag",    // 電撃的に動き回る
  },
  {
    type: 6,
    name: "土",
    color: "#FF00FF",
    hp: 8,
    speed: 0.15,
    effectiveness: [80, 80, 150, 40, 100, 300, 120, 50],
    moveStyle: "outer",     // 外を回って包囲
  },
  {
    type: 7,
    name: "風",
    color: "#00FFFF",
    hp: 8,
    speed: 0.4,
    effectiveness: [80, 80, 150, 100, 40, 50, 300, 120],
    moveStyle: "simple",    // 軽快に突撃
  },
];




  let currentColor = colors[0];
  let isDrawing = false;

  // パレット生成
  colors.forEach((color, i) => {
    const box = document.createElement('div');
    box.className = 'color-box';
    box.style.backgroundColor = color;
    if (i === 0) box.classList.add('selected');
    box.onclick = () => {
      document.querySelectorAll('.color-box').forEach(b => b.classList.remove('selected'));
      box.classList.add('selected');
      currentColor = color;
    };
    box.ondblclick = () => {
      dotCtx1.fillStyle = color;
      dotCtx1.fillRect(0, 0, dotCanvas1.width, dotCanvas1.height);
    };
    palette.appendChild(box);
  });
  const clearBtn = document.getElementById('clear');
  clearBtn.onclick = () => {
    clear();
  };

  function clear(event, canvas) {
    dotCtx1.clearRect(0, 0, mapCanvas.width, mapCanvas.height);
    dotCtx2.clearRect(0, 0, mapCanvas.width, mapCanvas.height);
  }
  function getCanvasRelativePosition(event, canvas) {
    const rect = canvas.getBoundingClientRect(); // 表示上のサイズ
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;

    const x = (event.clientX - rect.left) * scaleX;
    const y = (event.clientY - rect.top) * scaleY;

    return { x, y };
  }

  // 描画処理
  function drawPixel(x, y) {
    const gx = Math.floor(x / cellSize);
    const gy = Math.floor(y / cellSize);
    dotCtx1.fillStyle = currentColor;
    dotCtx1.fillRect(gx * cellSize, gy * cellSize, cellSize, cellSize);
  }

  dotCanvas1.onmousedown = (e) => {
    dotCanvas1.style.opacity=1;
    dotCanvas2.style.opacity=1;
    isDrawing = true;
    const pos = getCanvasRelativePosition(e, dotCanvas1);
    drawPixel(pos.x, pos.y);
  };

  dotCanvas1.onmousemove = (e) => {
    if (!isDrawing) return;
    const pos = getCanvasRelativePosition(e, dotCanvas1);
    drawPixel(pos.x, pos.y);
  };
  document.onmouseup = () => { isDrawing = false; };




// 解析して unitTypes[] と unitMap[][] を作る
let unitTypes = {};// hexColor -> canvas(1ユニットの画像)
let unitMap1 = []; // 8x8 array: 各セルの色(hex)
let unitMap2 = []; // 敵用の 8x8 map(unitMap1 は味方用)
let units = [];
let enemyUnits = [];   // 敵(右下にランダム配置)


/*
################################################################
################################################################
*/

function analyzeUnits(ctx, mapTarget, updateUnitTypes = true) {
  const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  const data = imageData.data;

  mapTarget.length = 0;

  for (let y = 0; y < gridSize; y++) {
    const row = [];
    for (let x = 0; x < gridSize; x++) {
      const i = (y * cellSize * ctx.canvas.width + x * cellSize) * 4;
      const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3];
      if (a < 10) {
        row.push(null);
        continue;
      }

      const type = getClosestPaletteIndex(r, g, b);
      const hex = colors[type];

      if (updateUnitTypes && !unitTypes[hex]) {
        const canvas = createEmptyCanvas(cellSize);
        const unitCtx = canvas.getContext('2d');
        unitCtx.fillStyle = `rgb(${r},${g},${b})`;
        unitCtx.fillRect(0, 0, cellSize * gridSize, cellSize * gridSize);
        unitTypes[hex] = canvas;
      }

      row.push(hex);
    }
    mapTarget.push(row);
  }
}





  function createEmptyCanvas(size) {
    const c = document.createElement('canvas');
    c.width = size;
    c.height = size;
    return c;
  }

  function rgbToHex(r, g, b) {
    return "#" + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('').toUpperCase();
  }



// mapCanvas に 8x8 ユニットを自動配置してみる
function drawUnitsOnMap() {
  mapCtx.clearRect(0, 0, mapCanvas.width, mapCanvas.height);
  for (let y = 0; y < gridSize; y++) {
    for (let x = 0; x < gridSize; x++) {
      const hex = unitMap1[y][x];
      if (!hex || !unitTypes[hex]) continue;
      mapCtx.drawImage(unitTypes[hex], x * cellSize, y * cellSize, cellSize, cellSize);
    }
  }
}

/*
################################################################
################################################################
*/

class Unit {

  constructor({
    x = 0,
    y = 0,
    color = '#000000',
    image = null,
    isEnemy = false,
    type = 0,
    hp = 3,
    speed = 0.12
  }) {
    this.x = x;
    this.y = y;
    this.color = color;
    this.image = image;
    this.isEnemy = isEnemy;
    this.type = type;
    this.hp = hp;
    this.maxHp = hp;
    this.speed = speed * 0.6;
    this.t = 0;
  }


  moveToward(targetX, targetY) {
    this.t++;
    const moveStyle = unitPresets[this.type].moveStyle;

    if (this.t % 5 !== 0) return;

    switch (moveStyle) {
      case "outer":
        this.moveTowardWithOuterBias(targetX, targetY);
        break;
      case "zigzag":
        this.moveTowardZigzag(targetX, targetY);
        break;
      case "simple":
        this.moveTowardSimple(targetX, targetY);
        break;
      case "random":
      default:
        this.moveTowardRandom(targetX, targetY);
    }

    // 範囲制限(マップ内)
    this.x = Math.max(0, Math.min(mapSize - 1, this.x));
    this.y = Math.max(0, Math.min(mapSize / 2 - 1, this.y));
  }

  moveTowardSimple(targetX, targetY) {
    if (this.t % 5 !== 0) return; // たまにしか動かない
    const noiseX = 0;
    const noiseY = 0;
    const dx = targetX - this.x + noiseX;
    const dy = targetY - this.y + noiseY;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist > 0.1) {
      this.x += (dx / dist) * this.speed;
      this.y += (dy / dist) * this.speed;
    }
  }
  moveTowardRandom(targetX, targetY) {
    if (this.t % 5 !== 0) return; // たまにしか動かない
    // ブレ幅を追加(たとえば ±0.5マス分)
    const noiseX = (Math.random() - 0.5) * 0.2;
    const noiseY = (Math.random() - 0.5) * 0.2;
    const dx = targetX - this.x + noiseX;
    const dy = targetY - this.y + noiseY;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist > 0.1) {
      this.x += (dx / dist) * this.speed;
      this.y += (dy / dist) * this.speed;
    }
  }


  moveTowardWithOuterBias(targetX, targetY) {
    if (this.t % 5 !== 0) return;

    const dx = targetX - this.x;
    const dy = targetY - this.y;

    // ★ 中心からのベクトル(=外方向)
    const cx = mapSize / 2 - this.x;
    const cy = mapSize / 4 - this.y; // 高さ半分の場合

    const outerBiasX = -cx * 0.05; // 中心から外へ向かう力(小さめ)
    const outerBiasY = -cy * 0.05;

    const totalDx = dx + outerBiasX;
    const totalDy = dy + outerBiasY;

    const dist = Math.sqrt(totalDx * totalDx + totalDy * totalDy);
    if (dist > 0.1) {
      this.x += (totalDx / dist) * this.speed;
      this.y += (totalDy / dist) * this.speed;
    }
  }

  moveTowardZigzag(targetX, targetY) {
    if (this.t % 5 !== 0) return;

    const dx = targetX - this.x;
    const dy = targetY - this.y;
    const dist = Math.sqrt(dx * dx + dy * dy);

    if (dist < 0.1) return;

    const dirX = dx / dist;
    const dirY = dy / dist;

    // 💡 ジグザグ用のノイズ成分(sin波で左右にブレさせる)
    const angle = Math.sin(this.t / 10) * 1.4; // 波の周期&大きさ
    const zigX = -dirY * angle;
    const zigY = dirX * angle;

    const finalX = dirX + zigX;
    const finalY = dirY + zigY;
    const finalDist = Math.sqrt(finalX * finalX + finalY * finalY);

    this.x += (finalX / finalDist) * this.speed;
    this.y += (finalY / finalDist) * this.speed;
  }


/*
  moveRandom(gridSize) {
    this.t++;
    if(this.t % 60 !== 0){return;}
    const dx = Math.floor(Math.random() * 3) - 1; // -1, 0, 1
    const dy = Math.floor(Math.random() * 3) - 1;
    this.x = Math.max(0, Math.min(mapSize - 1, this.x + dx));
    this.y = Math.max(0, Math.min(mapSize - 1, this.y + dy));
  }
*/

  takeDamage(amount = 1) {
    this.hp -= amount;
  }

  isDead() {
    return this.hp <= 0;
  }

  draw(ctx, size) {

    const scale = Math.max(0.3, this.hp / this.maxHp); // 小さすぎ防止で最小0.3
    const drawSize = size * scale;
    const offset = (size - drawSize) / 2;

    ctx.drawImage(
      this.image,
      (this.x * size) + offset,
      (this.y * size) + offset,
      drawSize,
      drawSize
    );
    // HP表示(確認用)
    //ctx.fillStyle = "#000";
    //ctx.font = `${size / 2}px sans-serif`;
    //ctx.fillText(this.hp, this.x * size + 3, this.y * size + size - 4);
  }
}


/*
################################################################
################################################################
*/


function handleCollisions() {
  for (let i = units.length - 1; i >= 0; i--) {
    const ally = units[i];
    for (let j = enemyUnits.length - 1; j >= 0; j--) {
      const enemy = enemyUnits[j];

      // ★ float距離で接触判定(1マス分未満でぶつかったとみなす)
      const dx = ally.x - enemy.x;
      const dy = ally.y - enemy.y;
      const dist = Math.sqrt(dx * dx + dy * dy);

      if (dist < 0.5) {  // ← 接触の閾値(0.5〜0.8くらいが自然)


        const attackerPreset = unitPresets[ally.type];
        const defenderPreset = unitPresets[enemy.type];

        // 味方 → 敵への補正倍率

        const atkToDef = (attackerPreset.effectiveness[enemy.type] || 100) / 100;
        // 敵 → 味方への補正倍率(逆方向)
        const defToAtk = (defenderPreset.effectiveness[ally.type] || 100) / 100;

        // 最終的なダメージ(1を基準に)
        const allyDamage = 1 * defToAtk;//Math.ceil()
        const enemyDamage = 1 * atkToDef;

        ally.takeDamage(allyDamage);
        enemy.takeDamage(enemyDamage);

        if (ally.isDead()) {
          units.splice(i, 1);
          break; // この味方は消えたので次へ
        }
        if (enemy.isDead()) {
          enemyUnits.splice(j, 1);
        }
      }
    }
  }
}

function moveUnitsTowardEnemies() {
  const sides = [
    { self: units, enemy: enemyUnits },
    { self: enemyUnits, enemy: units }
  ];

  for (const side of sides) {
    for (const u of side.self) {
      let target;
      if (u.type === 4) {
        target = findFarthestTarget(u, side.enemy);
      } else {
        target = findClosestTarget(u, side.enemy);
      }
      if (target) u.moveToward(target.x, target.y);
    }
  }

  applyFriendlyRepulsion(units);       // 💡味方同士の押し合い
  applyFriendlyRepulsion(enemyUnits);  // 💡敵同士も同様に
}

//近い敵に接近する
function findClosestTarget(unit, targets) {
  let minDist = Infinity;
  let closest = null;
  if (!targets.length) return null;
  for (const t of targets) {
    const dx = t.x - unit.x;
    const dy = t.y - unit.y;
    const dist = dx * dx + dy * dy;
    if (dist < minDist) {
      minDist = dist;
      closest = t;
    }
  }
  return closest;
}



//接近相性考慮
function findPreferredTarget(unit, targets) {
  let bestScore = -Infinity;
  let chosen = null;

  const selfPreset = unitPresets[unit.type];

  for (const t of targets) {
    const dx = t.x - unit.x;
    const dy = t.y - unit.y;
    const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;

    const effectiveness = selfPreset.effectiveness[t.type] || 100;
    const score = effectiveness / dist; // 相性が高くて近い敵ほどスコア高い

    if (score > bestScore) {
      bestScore = score;
      chosen = t;
    }
  }

  return chosen;
}
//遠くの敵へ
function findFarthestTarget(unit, targets) {
  let maxDist = -Infinity;
  let farthest = null;

  for (const t of targets) {
    const dx = t.x - unit.x;
    const dy = t.y - unit.y;
    const dist = dx * dx + dy * dy; // 距離の2乗(√不要)

    if (dist > maxDist) {
      maxDist = dist;
      farthest = t;
    }
  }

  return farthest;
}

//味方重なりすぎ防止
function applyFriendlyRepulsion(units, minDist = 0.8, repulsionStrength = 0.1) {

  //minDist:距離
  //0.2	けっこう密集
  //0.6	全体に距離が開き
  //repulsionStrength(反発力の強さ)

  for (let i = 0; i < units.length; i++) {
    const u1 = units[i];
    for (let j = i + 1; j < units.length; j++) {
      const u2 = units[j];
      const dx = u2.x - u1.x;
      const dy = u2.y - u1.y;
      const distSq = dx * dx + dy * dy;

      if (distSq < minDist * minDist && distSq > 0.0001) {
        const dist = Math.sqrt(distSq);
        const overlap = minDist - dist;

        const pushX = (dx / dist) * (overlap * repulsionStrength);
        const pushY = (dy / dist) * (overlap * repulsionStrength);

        // 反発力(押し合う)
        u1.x -= pushX;
        u1.y -= pushY;
        u2.x += pushX;
        u2.y += pushY;
      }
    }
  }
}


/*
################################################################
################################################################
*/


function createUnitsFromMap() {
  units = [];

  for (let y = 0; y < gridSize; y++) {
    for (let x = 0; x < gridSize; x++) {
      const hex = unitMap1[y][x];
      if (hex && unitTypes[hex]) {
        const type = colors.indexOf(hex); // ← index を type として使う
        if (type === -1) continue; // 対応外の色はスキップ
        const preset = unitPresets[type];
        units.push(new Unit({
x,
y,
color: preset.color,
image: unitTypes[hex],
type: preset.type,
isEnemy: false,
hp: preset.hp,
speed: preset.speed,
        }));
      }
    }
  }
}



//敵生成
function createEnemyUnits() {
  enemyUnits = [];

  // dotCanvas2 に画像があるかどうか判定(全部nullなら空)
  const flat = unitMap2.flat();
  const hasData = flat.some(c => c !== null && c !== undefined);

  if (hasData) {
    // dotCanvas2ベースで敵を作る
    for (let y = 0; y < gridSize; y++) {
      for (let x = 0; x < gridSize; x++) {
        const hex = unitMap2[y]?.[x];
        if (!hex || !unitTypes[hex]) continue;

        const type = colors.indexOf(hex);
        if (type === -1) continue;

        const preset = unitPresets[type];
        const mirroredX = mapSize - 1 - x; // 右側に配置
        const mirroredY = y;

        enemyUnits.push(new Unit({
          x: mirroredX,
          y: mirroredY,
          color: preset.color,
          image: unitTypes[hex],
          type: preset.type,
          isEnemy: true,
          hp: preset.hp,
          speed: preset.speed
        }));
      }
    }
  } else {
    // fallback: いつものランダム生成
    console.log("dotCanvas2が空なのでランダム敵ユニットを生成します");
    const count = units.length || 16;
    // ↓ここは従来のランダム生成関数を呼んでOK
    createRandomEnemyUnits(count);
  }
}




//敵構成
function createRandomEnemyUnits(count = 16) {
  enemyUnits = [];

  // 全タイプからランダムに2〜3種選ぶ
  const enemyKinds = Math.floor(Math.random() * 4) || 1;
  const enemyTypes = [];
  while (enemyTypes.length < enemyKinds) {
    const t = Math.floor(Math.random() * unitPresets.length);
    if (!enemyTypes.includes(t)) enemyTypes.push(t);
  }

  for (let i = 0; i < count; i++) {
    // ランダムなtype(0~7)

    //完全ランダム
    //const type = Math.floor(Math.random() * unitPresets.length);



    // その中からランダムにtypeを決定
    const type = enemyTypes[Math.floor(Math.random() * enemyTypes.length)];


    const preset = unitPresets[type];

    // ランダムな右側の位置に配置(mapSize = 32, gridSize = 16)
    const x = Math.floor(mapSize / 2) + Math.floor(Math.random() * (mapSize / 2));
    const y = Math.floor(Math.random() * (mapCanvas.height / cellSize));

    // ユニット画像作成
    const canvas = createEmptyCanvas(cellSize);
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = preset.color;
    ctx.fillRect(0, 0, cellSize, cellSize);

    enemyUnits.push(new Unit({
      x,
      y,
      color: preset.color,
      image: canvas,
      type: preset.type,
      isEnemy: true,
      hp: preset.hp,
      speed: preset.speed
    }));
  }

  console.log(`🔹 ランダム敵ユニット生成: ${enemyUnits.length}体`);
}


function drawUnitMapToCanvas(map, ctx) {
  console.log('drawUnitMapToCanvas:')
  ctx.clearRect(0, 0, dotCanvas2.width, dotCanvas2.height);

  for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
      const hex = map[y][x];
      if (!hex) continue;
      ctx.fillStyle = hex;
      ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
    }
  }
}

/*
################################################################
################################################################
*/


// ユニットを描画
function drawAllUnits() {
  mapCtx.clearRect(0, 0, mapCanvas.width, mapCanvas.height);

  for (const unit of units) {
    unit.draw(mapCtx, cellSize);
  }

  for (const enemy of enemyUnits) {
    enemy.draw(mapCtx, cellSize);
  }
}

function showMessage(message) {
  document.getElementById('message').textContent = message;
  console.log(message);
}

function stopGame() {
  dotCanvas1.style.opacity=0.2;
  dotCanvas2.style.opacity=0.2;
}


function announceWinner() {
  const y = 20;
  const w = mapCanvas.width;
  const pixelSize = 8;
  if (units.length === 0) {
    // 敵の勝ち
    drawDotText("WIN", w - mapSize*6, y, pixelSize, 2, "#D32F2F");
    drawDotText( "LOSE", 30, y, pixelSize, 2, "#1976D2");
    showMessage("右側の勝利!");
  } else if (enemyUnits.length === 0) {
    // 味方の勝ち
    drawDotText("WIN", 30, y, pixelSize, 2, "#D32F2F");
    drawDotText("LOSE", w - mapSize*6, y, pixelSize, 2, "#1976D2");
    showMessage("左側の勝利!");
  }
  stopGame();
}

function drawDotText(text, startX, startY=30, pixelSize = 5, spacing = 2, color = "#FF0000") {
  const ctx = mapCtx;
  ctx.fillStyle = color;
  console.log(text);

  for (let i = 0; i < text.length; i++) {
    const char = text[i].toUpperCase();
    const glyph = dotFont[char];
    if (!glyph) continue; // 未定義ならスキップ

    for (let y = 0; y < glyph.length; y++) {
      for (let x = 0; x < glyph[y].length; x++) {
        if (glyph[y][x] === "1") {
          ctx.fillRect(
            startX + (i * (6 * pixelSize + spacing)) + x * pixelSize,
            startY + y * pixelSize,
            pixelSize,
            pixelSize
          );
        }
      }
    }
  }
}
const dotFont = {
  W: [
    "1...1",
    "1...1",
    "1.1.1",
    "1.1.1",
    ".1.1.",
  ],
  I: [
    "111",
    ".1.",
    ".1.",
    ".1.",
    "111",
  ],
  N: [
    "1...1",
    "11..1",
    "1.1.1",
    "1..11",
    "1...1",
  ],
  L: [
    "1....",
    "1....",
    "1....",
    "1....",
    "11111",
  ],
  O: [
    ".111.",
    "1...1",
    "1...1",
    "1...1",
    ".111.",
  ],
  S: [
    ".1111",
    "1....",
    ".111.",
    "....1",
    "1111.",
  ],
  E: [
    "11111",
    "1....",
    "1111.",
    "1....",
    "11111",
  ]
};













/*
################################################################
################################################################
*/


function animateUnits() {
  moveUnitsTowardEnemies();
  handleCollisions();
  drawAllUnits();

  // 決着
  if (units.length === 0 || enemyUnits.length === 0) {
    announceWinner();
    return; // 停止
  }
  requestAnimationFrame(animateUnits);
}

function init(){
  analyzeUnits(dotCtx1,unitMap1);
  analyzeUnits(dotCtx2,unitMap2);
  drawUnitsOnMap();
  createUnitsFromMap();   // ユニット生成
  createEnemyUnits();   // 右下:敵ユニットランダム配置
  drawUnitMapToCanvas(unitMap2, dotCtx2);
  dotCanvas1.style.opacity=0.04;
  dotCanvas2.style.opacity=0.04;
  animateUnits();              // 両方を動かす
}


/*
################################################################
################################################################
*/


dotCanvas1.addEventListener('dragover', e => e.preventDefault());
dotCanvas1.addEventListener('drop', (e) => {
  e.preventDefault();
  const file = e.dataTransfer.files[0];
  if (!file.type.startsWith('image/')) return;

  const reader = new FileReader();
  reader.onload = (event) => {
    const img = new Image();
    img.onload = () => {
      convertImageTo8x8(img, dotCtx1, unitMap1);
    };
    img.src = event.target.result;
  };
  reader.readAsDataURL(file);
});

dotCanvas2.addEventListener('dragover', e => e.preventDefault());
dotCanvas2.addEventListener('drop', e => {
  e.preventDefault();
  const file = e.dataTransfer.files[0];
  if (!file || !file.type.startsWith('image/')) return;

  const reader = new FileReader();
  reader.onload = ev => {
    const img = new Image();
    img.onload = () => {
      convertImageTo8x8(img, dotCtx2, unitMap2);
    };
    img.src = ev.target.result;
  };
  reader.readAsDataURL(file);
});

 // 初期表示に使用
window.addEventListener('DOMContentLoaded', () => {
  const img1 = new Image();
  img1.src = 'dot-war-sample.png'; //
  img1.onload = () => {
    convertImageTo8x8(img1, dotCtx1, unitMap1, true);
  };
  const img2 = new Image();
  img2.src = 'dot-war-enemy.png'; //
  img2.onload = () => {
    convertImageTo8x8(img2, dotCtx2, unitMap2, true);
  };
  setTimeout(()=>{
    init();
    clear();
  },1999);

  img1.onerror = () => {
    console.warn('サンプル画像1 の読み込みに失敗しました');
  };
  img2.onerror = () => {
    console.warn('サンプル画像2 の読み込みに失敗しました');
  };
});


function loadIcon(n, service) {
  const id = 'userName'+n+service;
  const username = document.getElementById(id).value.trim();
  const url = `https://unavatar.io/${service}/${username}`;

  const img = document.createElement('img');
  img.crossOrigin = "anonymous";
  img.src = url;
  img.width = 64;

  img.onload = () => {
    try {
      if (n === 1) {
        convertImageTo8x8(img, dotCtx1, unitMap1);
        dotCanvas1.style.opacity = 1;
      } else {
        convertImageTo8x8(img, dotCtx2, unitMap2, true);
        dotCanvas2.style.opacity = 1;
      }
    } catch (err) {
      console.warn("❌ convertImageTo8x8 failed:", err);
      alert("⚠️ 画像の解析に失敗しました(CORS制限の可能性)");
    }
  };

  img.onerror = () => {
    alert("⚠️ 画像の読み込みに失敗しました。ユーザー名やサービス名が正しいか確認してください。");
  };
}

/*
################################################################
################################################################
*/
function convertImageTo8x8(img, ctx, targetMap, updateUnitTypes = false) {
  const tmp = createEmptyCanvas(gridSize);
  const tmpCtx = tmp.getContext('2d');
  tmpCtx.drawImage(img, 0, 0, gridSize, gridSize);

  const imageData = tmpCtx.getImageData(0, 0, gridSize, gridSize);
  const data = imageData.data;

  const tempMap = []; //

  for (let y = 0; y < gridSize; y++) {
    const row = [];
    for (let x = 0; x < gridSize; x++) {
      const i = (y * gridSize + x) * 4;
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];
      const a = data[i + 3];
      //if (a === 0) {
      //  row.push(null);
      //  continue;
      //}
      if (a < 10) { // 0ではなく「ほぼ透明」を無視
        row.push(null);
        continue;
      }

      const type = getClosestPaletteIndex(r, g, b);
      const displayColor = `rgb(${r},${g},${b})`;
      ctx.fillStyle = displayColor;
      ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
      row.push(colors[type]);
    }
    tempMap.push(row);
  }

  targetMap.length = 0;
  targetMap.push(...tempMap);

  const usedTypes = new Set();
  tempMap.flat().forEach(c => { if (c) usedTypes.add(colors.indexOf(c)); });
  console.log('使用されたtype:', [...usedTypes]);
}






function getClosestPaletteColor(r, g, b) {
  let minDist = Infinity;
  let closest = colors[0];

  for (const hex of colors) {
    const cr = parseInt(hex.substr(1, 2), 16);
    const cg = parseInt(hex.substr(3, 2), 16);
    const cb = parseInt(hex.substr(5, 2), 16);

    // 明度を考慮した加重距離
    const dr = r - cr;
    const dg = g - cg;
    const db = b - cb;

    const dist = 0.3 * dr * dr + 0.59 * dg * dg + 0.11 * db * db; // ←目の感度重視
    if (dist < minDist) {
      minDist = dist;
      closest = hex;
    }
  }

  return closest;
}


function getClosestPaletteIndex(r, g, b) {
  let minDist = Infinity;
  let bestIndex = 0;

  for (let i = 0; i < colors.length; i++) {
    const hex = colors[i];
    const cr = parseInt(hex.substr(1, 2), 16);
    const cg = parseInt(hex.substr(3, 2), 16);
    const cb = parseInt(hex.substr(5, 2), 16);

    const dr = r - cr;
    const dg = g - cg;
    const db = b - cb;
    const dist = 0.3 * dr * dr + 0.59 * dg * dg + 0.11 * db * db;

    if (dist < minDist) {
      minDist = dist;
      bestIndex = i;
    }
  }

  return bestIndex;
}

CSS

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/demo/-js/js-game-dot-war/

ABOUT

hi0a.com 「ひまアプリ」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

動く便利なものが好きなだけで技術自体に興味はないのでコードは汚いです。

途中放置や実験状態、仕様変更、API廃止等で動かないページもあります。