foxygit / GZDoom Log in
commits tags

/src/net/webjoin/wj_page.h · 5.79 KB

raw
// wj_page.h -- the static mobile controller page served over HTTP by
// wj_server.cpp. Kept as a single dependency-free HTML file (inline CSS +
// JS) so the engine doesn't need an asset pipeline or extra HTTP routes
// for scripts/stylesheets. Two touch sticks (move, look) plus three
// action buttons; sends a 12-byte binary WJInputPacket (see wj_input.h)
// over the WebSocket at ~35 Hz to match TICRATE.
#pragma once

inline const char *WJ_GetPageHTML()
{
	return R"WJHTML(<!doctype html>
<html><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Join Game</title>
<style>
  html,body { margin:0; padding:0; height:100%; background:#111; color:#eee;
    font-family:sans-serif; overscroll-behavior:none; touch-action:none; user-select:none; }
  #status { position:fixed; top:0; left:0; right:0; padding:6px; text-align:center;
    font-size:14px; background:#222; z-index:10; }
  #field { position:fixed; top:32px; left:0; right:0; bottom:0; }
  /* vmin (percent of the SHORTER viewport dimension) instead of vw so
     sizing stays sane in both portrait and landscape -- vw alone blows
     up the stick/button sizes in landscape, which is what pushed the
     buttons on top of the look stick's own drag zone. Landscape is the
     orientation this is actually meant to be played in. */
  .stick { position:absolute; bottom:16px; width:30vmin; height:30vmin;
    border-radius:50%; background:#222; border:2px solid #444; touch-action:none; }
  .stick .knob { position:absolute; width:40%; height:40%; left:30%; top:30%;
    border-radius:50%; background:#3a7; pointer-events:none; }
  #moveStick { left:16px; }
  #lookStick { right:16px; }
  .btn { position:absolute; border-radius:50%; background:#333; border:2px solid #555;
    color:#eee; font-size:2.6vmin; display:flex; align-items:center; justify-content:center; touch-action:none; }
  /* Fire sits to the LEFT of the look stick (outside its bounding box);
     Use/Jump sit ABOVE it. Nothing shares screen space with the stick's
     own circle, in either orientation. */
  #btnFire { width:16vmin; height:16vmin; right:calc(16px + 32vmin); bottom:calc(16px + 6vmin); background:#733; }
  #btnUse  { width:12vmin; height:12vmin; right:16px; bottom:calc(16px + 30vmin + 10px); }
  #btnJump { width:12vmin; height:12vmin; right:calc(16px + 14vmin); bottom:calc(16px + 30vmin + 10px); }
</style>
</head>
<body>
<div id="status">connecting...</div>
<div id="field">
  <div class="stick" id="moveStick"><div class="knob"></div></div>
  <div class="stick" id="lookStick"><div class="knob"></div></div>
  <div class="btn" id="btnFire">FIRE</div>
  <div class="btn" id="btnUse">USE</div>
  <div class="btn" id="btnJump">JUMP</div>
</div>
<script>
'use strict';
var BT_ATTACK = 1<<0, BT_USE = 1<<1, BT_JUMP = 1<<2;

function makeStick(el) {
  var knob = el.querySelector('.knob');
  var active = false, id = null, cx = 0, cy = 0, x = 0, y = 0;
  function rect() { return el.getBoundingClientRect(); }
  function setKnob(dx, dy) { knob.style.transform = 'translate(' + (dx*30) + 'px,' + (dy*30) + 'px)'; }
  function start(px, py, pid) {
    var r = rect(); cx = r.left + r.width/2; cy = r.top + r.height/2;
    active = true; id = pid; move(px, py);
  }
  function move(px, py) {
    if (!active) return;
    var r = rect(); var radius = r.width/2;
    var dx = (px - cx) / radius, dy = (py - cy) / radius;
    var len = Math.sqrt(dx*dx + dy*dy);
    if (len > 1) { dx /= len; dy /= len; }
    x = dx; y = dy; setKnob(dx, dy);
  }
  function end() { active = false; id = null; x = 0; y = 0; setKnob(0, 0); }
  el.addEventListener('pointerdown', function(e) { el.setPointerCapture(e.pointerId); start(e.clientX, e.clientY, e.pointerId); });
  el.addEventListener('pointermove', function(e) { if (e.pointerId === id) move(e.clientX, e.clientY); });
  el.addEventListener('pointerup', function(e) { if (e.pointerId === id) end(); });
  el.addEventListener('pointercancel', function(e) { if (e.pointerId === id) end(); });
  return { get x() { return x; }, get y() { return y; } };
}

var moveStick = makeStick(document.getElementById('moveStick'));
var lookStick = makeStick(document.getElementById('lookStick'));

var buttons = 0;
function bindButton(elId, bit) {
  var el = document.getElementById(elId);
  el.addEventListener('pointerdown', function(e) { el.setPointerCapture(e.pointerId); buttons |= bit; });
  el.addEventListener('pointerup', function() { buttons &= ~bit; });
  el.addEventListener('pointercancel', function() { buttons &= ~bit; });
}
bindButton('btnFire', BT_ATTACK);
bindButton('btnUse', BT_USE);
bindButton('btnJump', BT_JUMP);

var statusEl = document.getElementById('status');
var token = new URLSearchParams(location.search).get('t') || '';
var ws = null, sendTimer = null;

function clamp16(v) { v = Math.round(v * 32767); return Math.max(-32768, Math.min(32767, v)); }

function connect() {
  var proto = location.protocol === 'https:' ? 'wss://' : 'ws://';
  ws = new WebSocket(proto + location.host + '/ws?t=' + encodeURIComponent(token));
  ws.binaryType = 'arraybuffer';
  ws.onopen = function() {
    statusEl.textContent = 'connected';
    sendTimer = setInterval(sendState, 28);
  };
  ws.onclose = function() {
    statusEl.textContent = 'disconnected -- reconnecting...';
    if (sendTimer) { clearInterval(sendTimer); sendTimer = null; }
    setTimeout(connect, 1500);
  };
  ws.onerror = function() { ws.close(); };
}

function sendState() {
  if (!ws || ws.readyState !== 1) return;
  var buf = new ArrayBuffer(12);
  var dv = new DataView(buf);
  dv.setUint32(0, buttons >>> 0, true);
  dv.setInt16(4, clamp16(moveStick.x), true);
  dv.setInt16(6, clamp16(-moveStick.y), true);
  dv.setInt16(8, clamp16(lookStick.x), true);
  dv.setInt16(10, clamp16(-lookStick.y), true);
  ws.send(buf);
}

connect();
</script>
</body></html>
)WJHTML";
}