Newer
Older
2025-shino / fog.js
(() => {
  'use strict';

  document.addEventListener("DOMContentLoaded", () => {

    const START_POS = [38.891, 139.824];
    const REVEAL_RADIUS = 40;
    const MAX_EXPLORE_POINTS = 300;
    const BASE_CAPTURE_RADIUS = 30;

    const titleEl = document.getElementById("title");
    const startBtn = document.getElementById("start");
    const stopBtn  = document.getElementById("stop");
    const rateEl = document.getElementById("rate");
    const baseStatusEl = document.getElementById("baseStatus");

    let map;
    let playerMarker;
    let watchId = null;

    const revealedPoints = [];

    const bases = [
      { latlng: L.latLng(38.892, 139.825), captured: false, marker: null },
      { latlng: L.latLng(38.889, 139.823), captured: false, marker: null },
      { latlng: L.latLng(38.893, 139.827), captured: false, marker: null }
    ];


    const FogLayer = L.GridLayer.extend({
      createTile: function (coords) {
        const tile = document.createElement("canvas");
        const size = this.getTileSize();
        tile.width = size.x;
        tile.height = size.y;

        const ctx = tile.getContext("2d");

        ctx.fillStyle = "rgba(0,0,0,0.85)";
        ctx.fillRect(0, 0, size.x, size.y);

        const bounds = this._tileCoordsToBounds(coords);
        ctx.globalCompositeOperation = "destination-out";

        revealedPoints.forEach(latlng => {
          if (!bounds.contains(latlng)) return;

          const point = map.latLngToContainerPoint(latlng);
          const tilePoint = map.latLngToContainerPoint(bounds.getNorthWest());

          ctx.beginPath();
          ctx.arc(
            point.x - tilePoint.x,
            point.y - tilePoint.y,
            metersToPixels(REVEAL_RADIUS, latlng.lat),
            0,
            Math.PI * 2
          );
          ctx.fill();
        });

        return tile;
      }
    });

    let fogLayer;

    function metersToPixels(m, lat) {
      return (
        m / 40075017 *
        256 *
        Math.pow(2, map.getZoom()) /
        Math.cos(lat * Math.PI / 180)
      );
    }

    function isNearExisting(latlng, threshold = 10) {
      return revealedPoints.some(p => p.distanceTo(latlng) < threshold);
    }

    function updateExploreRate() {
      const rate = Math.min(
        100,
        Math.floor((revealedPoints.length / MAX_EXPLORE_POINTS) * 100)
      );
      rateEl.textContent = `制圧率: ${rate}%`;
    }


    function updateBaseStatus() {
      const captured = bases.filter(b => b.captured).length;
      baseStatusEl.textContent = `拠点: ${captured} / ${bases.length}`;

      if (captured === bases.length) {
        titleEl.textContent = "全拠点制圧!";
      }
    }

    function initMap() {
      map = L.map("locationmap").setView(START_POS, 16);

      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution: "&copy; OpenStreetMap contributors"
      }).addTo(map);

      fogLayer = new FogLayer();
      fogLayer.addTo(map);

      playerMarker = L.marker(map.getCenter()).addTo(map);
      playerMarker.bindPopup("STARTで探索開始").openPopup();

      bases.forEach(b => {
        b.marker = L.circleMarker(b.latlng, {
          radius: 8,
          color: "red",
          fillColor: "red",
          fillOpacity: 1
        }).addTo(map).bindPopup("未制圧拠点");
      });

      map.on("click", e => updateLocation(e.latlng));
      updateBaseStatus();
    }

    function checkBases(latlng) {
      bases.forEach(b => {
        if (b.captured) return;

        if (latlng.distanceTo(b.latlng) <= BASE_CAPTURE_RADIUS) {
          b.captured = true;
          b.marker.setStyle({
            color: "blue",
            fillColor: "blue"
          });
          b.marker.setPopupContent("制圧済み拠点");
          updateBaseStatus();
        }
      });
    }

    function reveal(latlng) {
      if (isNearExisting(latlng)) return;

      revealedPoints.push(latlng);
      fogLayer.redraw();
      updateExploreRate();
    }

    function updateLocation(latlng) {
      map.panTo(latlng, { animate: false });
      playerMarker.setLatLng(latlng);

      reveal(latlng);
      checkBases(latlng);
    }

    function onSuccess(pos) {
      updateLocation(
        L.latLng(pos.coords.latitude, pos.coords.longitude)
      );
    }

    function startWatch() {
      stopWatch();
      watchId = navigator.geolocation.watchPosition(
        onSuccess,
        null,
        { enableHighAccuracy: true }
      );
      titleEl.textContent = "探索中";
    }

    function stopWatch() {
      if (watchId !== null) {
        navigator.geolocation.clearWatch(watchId);
        watchId = null;
      }
      titleEl.textContent = "霧解除探索ゲーム";
    }

    startBtn.addEventListener("click", startWatch);
    stopBtn.addEventListener("click", stopWatch);

    initMap();
  });
})();