Spaces:
Running
Running
Fix hero spinning while walking: joystick dead zone with hysteresis, vector smoothing, floating stick origin
Browse files- index.html +24 -8
- sw.js +1 -1
index.html
CHANGED
|
@@ -228,6 +228,7 @@ button.big.ghost { background:rgba(20,16,10,.8); color:var(--gold); }
|
|
| 228 |
// ---------------------------------------------------------------- constants
|
| 229 |
const HW = 96, HH = 48; // half tile size (tile 192x96)
|
| 230 |
const HERO_SPEED = 4.2; // tiles / second
|
|
|
|
| 231 |
const ENEMY_SPEED = 0.7; // multiplier on enemy file speed
|
| 232 |
const ENEMY_DMG_SCALE = 0.3; // balance factor on file damage (web port)
|
| 233 |
const HERO_R = 0.28, ENEMY_R = 0.28;
|
|
@@ -270,7 +271,7 @@ button.big.ghost { background:rgba(20,16,10,.8); color:var(--gold); }
|
|
| 270 |
enemies: [], drops: [], projs: [], texts: [], marks: [],
|
| 271 |
canvas: null, ctx: null, W: 0, H: 0, zoom: 1,
|
| 272 |
auto: false, lock: true, atkOn: true, eid: 0,
|
| 273 |
-
joy: { on: false, ox: 0, oy: 0, vx: 0, vy: 0 },
|
| 274 |
keyDir: null, ptr: null, cls: 'brute', running: false,
|
| 275 |
noHotUntil: 0, changing: false, lastHud: 0, lastSave: 0, kills: 0, skillBtns: [],
|
| 276 |
};
|
|
@@ -899,14 +900,26 @@ button.big.ghost { background:rgba(20,16,10,.8); color:var(--gold); }
|
|
| 899 |
// while an attack animation is playing, keep the attack facing and do not replace the swing with run/stance
|
| 900 |
const attacking = t < H.lockUntil;
|
| 901 |
const inReach = H.target && !H.target.dead && dist(H.x, H.y, H.target.x, H.target.y) <= S.world.classes[H.cls].range + 0.25;
|
| 902 |
-
|
| 903 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
const len = Math.hypot(vx, vy);
|
| 905 |
-
if (len > 0.01) {
|
| 906 |
// screen vector -> world (iso inverse on direction vector)
|
| 907 |
const wx = (vx / HW + vy / HH) / 2, wy = (vy / HH - vx / HW) / 2;
|
| 908 |
const wl = Math.hypot(wx, wy) || 1;
|
| 909 |
-
const k = sp * dt *
|
| 910 |
let mx = wx / wl * k, my = wy / wl * k;
|
| 911 |
// already in reach of the locked target: do not walk into it (that made the hero overshoot back and forth)
|
| 912 |
if (inReach && mx * (H.target.x - H.x) + my * (H.target.y - H.y) > 0) { mx = 0; my = 0; }
|
|
@@ -1088,15 +1101,18 @@ button.big.ghost { background:rgba(20,16,10,.8); color:var(--gold); }
|
|
| 1088 |
}
|
| 1089 |
if (S.joy.on) {
|
| 1090 |
let vx = e.clientX - S.joy.ox, vy = e.clientY - S.joy.oy;
|
| 1091 |
-
const L = Math.hypot(vx, vy), R =
|
| 1092 |
-
if (L > R) {
|
|
|
|
|
|
|
|
|
|
| 1093 |
S.joy.vx = vx; S.joy.vy = vy; S.joy.cx = S.joy.ox + vx; S.joy.cy = S.joy.oy + vy;
|
| 1094 |
}
|
| 1095 |
});
|
| 1096 |
const up = (e) => {
|
| 1097 |
if (!S.ptr || e.pointerId !== S.ptr.id) return;
|
| 1098 |
if (!S.joy.on && now() - S.ptr.t0 < 0.45) onTap(e.clientX, e.clientY);
|
| 1099 |
-
S.joy.on = false; S.joy.vx = S.joy.vy = 0; S.ptr = null;
|
| 1100 |
};
|
| 1101 |
cv.addEventListener('pointerup', up);
|
| 1102 |
cv.addEventListener('pointercancel', up);
|
|
|
|
| 228 |
// ---------------------------------------------------------------- constants
|
| 229 |
const HW = 96, HH = 48; // half tile size (tile 192x96)
|
| 230 |
const HERO_SPEED = 4.2; // tiles / second
|
| 231 |
+
const JOY_R = 70, JOY_DEAD = 10; // joystick radius / dead zone (css px); released again below 60% of the dead zone
|
| 232 |
const ENEMY_SPEED = 0.7; // multiplier on enemy file speed
|
| 233 |
const ENEMY_DMG_SCALE = 0.3; // balance factor on file damage (web port)
|
| 234 |
const HERO_R = 0.28, ENEMY_R = 0.28;
|
|
|
|
| 271 |
enemies: [], drops: [], projs: [], texts: [], marks: [],
|
| 272 |
canvas: null, ctx: null, W: 0, H: 0, zoom: 1,
|
| 273 |
auto: false, lock: true, atkOn: true, eid: 0,
|
| 274 |
+
joy: { on: false, act: false, ox: 0, oy: 0, vx: 0, vy: 0, sx: 0, sy: 0 },
|
| 275 |
keyDir: null, ptr: null, cls: 'brute', running: false,
|
| 276 |
noHotUntil: 0, changing: false, lastHud: 0, lastSave: 0, kills: 0, skillBtns: [],
|
| 277 |
};
|
|
|
|
| 900 |
// while an attack animation is playing, keep the attack facing and do not replace the swing with run/stance
|
| 901 |
const attacking = t < H.lockUntil;
|
| 902 |
const inReach = H.target && !H.target.dead && dist(H.x, H.y, H.target.x, H.target.y) <= S.world.classes[H.cls].range + 0.25;
|
| 903 |
+
// movement input: stick with dead zone (+ smoothing against thumb tremor) or keyboard
|
| 904 |
+
let vx = 0, vy = 0, power = 0;
|
| 905 |
+
if (S.joy.on) {
|
| 906 |
+
const L = Math.hypot(S.joy.vx, S.joy.vy);
|
| 907 |
+
if (!S.joy.act && L > JOY_DEAD) { S.joy.act = true; S.joy.sx = S.joy.vx; S.joy.sy = S.joy.vy; }
|
| 908 |
+
else if (S.joy.act && L < JOY_DEAD * 0.6) S.joy.act = false;
|
| 909 |
+
if (S.joy.act) {
|
| 910 |
+
const a = Math.min(1, dt * 14);
|
| 911 |
+
S.joy.sx += (S.joy.vx - S.joy.sx) * a; S.joy.sy += (S.joy.vy - S.joy.sy) * a;
|
| 912 |
+
vx = S.joy.sx; vy = S.joy.sy;
|
| 913 |
+
power = Math.min(1, 0.4 + (Math.hypot(vx, vy) - JOY_DEAD) / 30);
|
| 914 |
+
}
|
| 915 |
+
} else if (S.keyDir) { [vx, vy] = S.keyDir; power = 1; }
|
| 916 |
+
{
|
| 917 |
const len = Math.hypot(vx, vy);
|
| 918 |
+
if (power > 0 && len > 0.01) {
|
| 919 |
// screen vector -> world (iso inverse on direction vector)
|
| 920 |
const wx = (vx / HW + vy / HH) / 2, wy = (vy / HH - vx / HW) / 2;
|
| 921 |
const wl = Math.hypot(wx, wy) || 1;
|
| 922 |
+
const k = sp * dt * power;
|
| 923 |
let mx = wx / wl * k, my = wy / wl * k;
|
| 924 |
// already in reach of the locked target: do not walk into it (that made the hero overshoot back and forth)
|
| 925 |
if (inReach && mx * (H.target.x - H.x) + my * (H.target.y - H.y) > 0) { mx = 0; my = 0; }
|
|
|
|
| 1101 |
}
|
| 1102 |
if (S.joy.on) {
|
| 1103 |
let vx = e.clientX - S.joy.ox, vy = e.clientY - S.joy.oy;
|
| 1104 |
+
const L = Math.hypot(vx, vy), R = JOY_R;
|
| 1105 |
+
if (L > R) { // the stick follows the thumb, so reversing direction never needs a long drag back
|
| 1106 |
+
S.joy.ox = e.clientX - vx / L * R; S.joy.oy = e.clientY - vy / L * R;
|
| 1107 |
+
vx = vx / L * R; vy = vy / L * R;
|
| 1108 |
+
}
|
| 1109 |
S.joy.vx = vx; S.joy.vy = vy; S.joy.cx = S.joy.ox + vx; S.joy.cy = S.joy.oy + vy;
|
| 1110 |
}
|
| 1111 |
});
|
| 1112 |
const up = (e) => {
|
| 1113 |
if (!S.ptr || e.pointerId !== S.ptr.id) return;
|
| 1114 |
if (!S.joy.on && now() - S.ptr.t0 < 0.45) onTap(e.clientX, e.clientY);
|
| 1115 |
+
S.joy.on = false; S.joy.act = false; S.joy.vx = S.joy.vy = S.joy.sx = S.joy.sy = 0; S.ptr = null;
|
| 1116 |
};
|
| 1117 |
cv.addEventListener('pointerup', up);
|
| 1118 |
cv.addEventListener('pointercancel', up);
|
sw.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
// The Space serves assets with "Cache-Control: no-store", so the browser HTTP cache never keeps them.
|
| 3 |
// Cache Storage (used here) is not affected by that header, so images are downloaded once per build.
|
| 4 |
// The two placeholders below (build id and asset list) are filled in by tools/inline_build.py.
|
| 5 |
-
const BUILD = '
|
| 6 |
const SHELL = 'flare-shell-' + BUILD; // index.html (network first, cache fallback)
|
| 7 |
const ASSETS = 'flare-assets-' + BUILD; // sprites, tiles, icons (cache-first)
|
| 8 |
const KEEP = [SHELL, ASSETS];
|
|
|
|
| 2 |
// The Space serves assets with "Cache-Control: no-store", so the browser HTTP cache never keeps them.
|
| 3 |
// Cache Storage (used here) is not affected by that header, so images are downloaded once per build.
|
| 4 |
// The two placeholders below (build id and asset list) are filled in by tools/inline_build.py.
|
| 5 |
+
const BUILD = '0c47d0b01e9d';
|
| 6 |
const SHELL = 'flare-shell-' + BUILD; // index.html (network first, cache fallback)
|
| 7 |
const ASSETS = 'flare-assets-' + BUILD; // sprites, tiles, icons (cache-first)
|
| 8 |
const KEEP = [SHELL, ASSETS];
|