Spaces:
Running
Running
Rebuild engine+comboBattler: dodge roll i-frames
Browse filesCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- web/comboBattler.js +62 -3
- web/engine.js +4 -0
web/comboBattler.js
CHANGED
|
@@ -5220,6 +5220,10 @@ function healActor(b, a, amount, empowered) {
|
|
| 5220 |
}
|
| 5221 |
function dealDamage(b, src, tgt, amount, label, opts = {}) {
|
| 5222 |
if (!tgt.alive) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5223 |
const { damageType = "physical", delivery = "spell", armorIgnoring = false, empowered = false } = opts;
|
| 5224 |
const physical = delivery === "melee" || delivery === "projectile";
|
| 5225 |
if (physical) {
|
|
@@ -6077,7 +6081,29 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6077 |
let offTick = null, keyHandlers = null, tapHandlers = null, controlsEl = null, alive = true;
|
| 6078 |
const keys = { x: 0, y: 0 };
|
| 6079 |
const joy = { x: 0, y: 0, active: false };
|
| 6080 |
-
const req = { attack: false, skill: 0 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6081 |
let moveTarget = null;
|
| 6082 |
let navTarget = null, navRoute = null;
|
| 6083 |
let routePath = null, routeIdx = 0;
|
|
@@ -6116,6 +6142,11 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6116 |
e.preventDefault();
|
| 6117 |
return;
|
| 6118 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6119 |
if (e.key >= "1" && e.key <= "9") {
|
| 6120 |
req.skill = +e.key;
|
| 6121 |
e.preventDefault();
|
|
@@ -6218,6 +6249,7 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6218 |
sb.el.style.borderColor = ok ? "#c9a227" : "#20262e";
|
| 6219 |
}
|
| 6220 |
if (threatBadge) threatBadge.textContent = `\u2694 ${runState.kills} THREAT ${runState.diff.toFixed(1)}\xD7`;
|
|
|
|
| 6221 |
}
|
| 6222 |
function setHeroSkills(skills) {
|
| 6223 |
const p = pa();
|
|
@@ -6300,9 +6332,16 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6300 |
adrWrap.append(adrLab, adrTrack);
|
| 6301 |
pad.appendChild(adrWrap);
|
| 6302 |
pad.appendChild(skills);
|
| 6303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6304 |
req.attack = true;
|
| 6305 |
}, true));
|
|
|
|
| 6306 |
wrap.appendChild(pad);
|
| 6307 |
threatBadge = document.createElement("div");
|
| 6308 |
threatBadge.style.cssText = "position:absolute;top:14px;left:50%;transform:translateX(-50%);pointer-events:none;padding:5px 12px;border-radius:999px;background:rgba(20,24,33,.8);border:1px solid #2a3340;color:#e8e8e8;font:700 12px monospace;letter-spacing:.04em";
|
|
@@ -6326,12 +6365,29 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6326 |
if (!battle || !R) return;
|
| 6327 |
const dtMS = ticker.deltaMS, dt = dtMS / 1e3;
|
| 6328 |
const p = pa();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6329 |
const clearNav = () => {
|
| 6330 |
moveTarget = null;
|
| 6331 |
navTarget = navRoute = routePath = null;
|
| 6332 |
routeIdx = 0;
|
| 6333 |
};
|
| 6334 |
-
if (
|
|
|
|
|
|
|
| 6335 |
clearNav();
|
| 6336 |
setInput(battle, "P0", { moveX: joy.x, moveY: joy.y });
|
| 6337 |
} else if (keys.x || keys.y) {
|
|
@@ -6541,7 +6597,10 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6541 |
skillRowEl = null;
|
| 6542 |
adrFill = null;
|
| 6543 |
threatBadge = null;
|
|
|
|
| 6544 |
skillBtns.length = 0;
|
|
|
|
|
|
|
| 6545 |
try {
|
| 6546 |
markers?.destroy();
|
| 6547 |
} catch {
|
|
|
|
| 5220 |
}
|
| 5221 |
function dealDamage(b, src, tgt, amount, label, opts = {}) {
|
| 5222 |
if (!tgt.alive) return 0;
|
| 5223 |
+
if (tgt.invulnUntil && b.t < tgt.invulnUntil) {
|
| 5224 |
+
log(b, "miss", tgt, { name: label });
|
| 5225 |
+
return 0;
|
| 5226 |
+
}
|
| 5227 |
const { damageType = "physical", delivery = "spell", armorIgnoring = false, empowered = false } = opts;
|
| 5228 |
const physical = delivery === "melee" || delivery === "projectile";
|
| 5229 |
if (physical) {
|
|
|
|
| 6081 |
let offTick = null, keyHandlers = null, tapHandlers = null, controlsEl = null, alive = true;
|
| 6082 |
const keys = { x: 0, y: 0 };
|
| 6083 |
const joy = { x: 0, y: 0, active: false };
|
| 6084 |
+
const req = { attack: false, skill: 0, dodge: false };
|
| 6085 |
+
let dash = null, dodgeCdUntil = 0, dodgeBtn = null;
|
| 6086 |
+
const DODGE_IFRAMES = 0.4, DODGE_DASH = 0.16, DODGE_CD = 1.1, DODGE_BOOST = 3.4;
|
| 6087 |
+
const norm = (x, y) => {
|
| 6088 |
+
const m = Math.hypot(x, y) || 1;
|
| 6089 |
+
return { x: x / m, y: y / m };
|
| 6090 |
+
};
|
| 6091 |
+
const moveDir = () => {
|
| 6092 |
+
if (joy.active && (joy.x || joy.y)) return norm(joy.x, joy.y);
|
| 6093 |
+
if (keys.x || keys.y) return norm(keys.x, keys.y);
|
| 6094 |
+
const p = pa();
|
| 6095 |
+
return p ? norm(p.faceX || 1, p.faceY || 0) : { x: 1, y: 0 };
|
| 6096 |
+
};
|
| 6097 |
+
function tryDodge() {
|
| 6098 |
+
const p = pa();
|
| 6099 |
+
if (!p || !p.alive || !battle || battle.t < dodgeCdUntil) return;
|
| 6100 |
+
const dir = moveDir();
|
| 6101 |
+
p._dashBase = p.moveSpeed;
|
| 6102 |
+
p.moveSpeed = p._dashBase * DODGE_BOOST;
|
| 6103 |
+
p.invulnUntil = battle.t + DODGE_IFRAMES;
|
| 6104 |
+
dash = { until: battle.t + DODGE_DASH, x: dir.x, y: dir.y };
|
| 6105 |
+
dodgeCdUntil = battle.t + DODGE_CD;
|
| 6106 |
+
}
|
| 6107 |
let moveTarget = null;
|
| 6108 |
let navTarget = null, navRoute = null;
|
| 6109 |
let routePath = null, routeIdx = 0;
|
|
|
|
| 6142 |
e.preventDefault();
|
| 6143 |
return;
|
| 6144 |
}
|
| 6145 |
+
if (e.key === "Shift" || e.code === "ShiftLeft" || e.code === "ShiftRight") {
|
| 6146 |
+
req.dodge = true;
|
| 6147 |
+
e.preventDefault();
|
| 6148 |
+
return;
|
| 6149 |
+
}
|
| 6150 |
if (e.key >= "1" && e.key <= "9") {
|
| 6151 |
req.skill = +e.key;
|
| 6152 |
e.preventDefault();
|
|
|
|
| 6249 |
sb.el.style.borderColor = ok ? "#c9a227" : "#20262e";
|
| 6250 |
}
|
| 6251 |
if (threatBadge) threatBadge.textContent = `\u2694 ${runState.kills} THREAT ${runState.diff.toFixed(1)}\xD7`;
|
| 6252 |
+
if (dodgeBtn) dodgeBtn.style.opacity = battle && battle.t >= dodgeCdUntil ? "1" : "0.4";
|
| 6253 |
}
|
| 6254 |
function setHeroSkills(skills) {
|
| 6255 |
const p = pa();
|
|
|
|
| 6332 |
adrWrap.append(adrLab, adrTrack);
|
| 6333 |
pad.appendChild(adrWrap);
|
| 6334 |
pad.appendChild(skills);
|
| 6335 |
+
const actionRow = document.createElement("div");
|
| 6336 |
+
actionRow.style.cssText = "display:flex;gap:10px;align-items:flex-end";
|
| 6337 |
+
dodgeBtn = roundBtn("\u27F2", () => {
|
| 6338 |
+
req.dodge = true;
|
| 6339 |
+
}, false);
|
| 6340 |
+
dodgeBtn.title = "Dodge roll (Shift)";
|
| 6341 |
+
actionRow.append(dodgeBtn, roundBtn("\u2694", () => {
|
| 6342 |
req.attack = true;
|
| 6343 |
}, true));
|
| 6344 |
+
pad.appendChild(actionRow);
|
| 6345 |
wrap.appendChild(pad);
|
| 6346 |
threatBadge = document.createElement("div");
|
| 6347 |
threatBadge.style.cssText = "position:absolute;top:14px;left:50%;transform:translateX(-50%);pointer-events:none;padding:5px 12px;border-radius:999px;background:rgba(20,24,33,.8);border:1px solid #2a3340;color:#e8e8e8;font:700 12px monospace;letter-spacing:.04em";
|
|
|
|
| 6365 |
if (!battle || !R) return;
|
| 6366 |
const dtMS = ticker.deltaMS, dt = dtMS / 1e3;
|
| 6367 |
const p = pa();
|
| 6368 |
+
if (req.dodge) {
|
| 6369 |
+
req.dodge = false;
|
| 6370 |
+
tryDodge();
|
| 6371 |
+
}
|
| 6372 |
+
let dashing = false;
|
| 6373 |
+
if (dash) {
|
| 6374 |
+
if (p && p.alive && battle.t < dash.until) dashing = true;
|
| 6375 |
+
else {
|
| 6376 |
+
if (p && p._dashBase) {
|
| 6377 |
+
p.moveSpeed = p._dashBase;
|
| 6378 |
+
p._dashBase = 0;
|
| 6379 |
+
}
|
| 6380 |
+
dash = null;
|
| 6381 |
+
}
|
| 6382 |
+
}
|
| 6383 |
const clearNav = () => {
|
| 6384 |
moveTarget = null;
|
| 6385 |
navTarget = navRoute = routePath = null;
|
| 6386 |
routeIdx = 0;
|
| 6387 |
};
|
| 6388 |
+
if (dashing) {
|
| 6389 |
+
setInput(battle, "P0", { moveX: dash.x, moveY: dash.y });
|
| 6390 |
+
} else if (joy.active) {
|
| 6391 |
clearNav();
|
| 6392 |
setInput(battle, "P0", { moveX: joy.x, moveY: joy.y });
|
| 6393 |
} else if (keys.x || keys.y) {
|
|
|
|
| 6597 |
skillRowEl = null;
|
| 6598 |
adrFill = null;
|
| 6599 |
threatBadge = null;
|
| 6600 |
+
dodgeBtn = null;
|
| 6601 |
skillBtns.length = 0;
|
| 6602 |
+
dash = null;
|
| 6603 |
+
dodgeCdUntil = 0;
|
| 6604 |
try {
|
| 6605 |
markers?.destroy();
|
| 6606 |
} catch {
|
web/engine.js
CHANGED
|
@@ -943,6 +943,10 @@ function healActor(b, a, amount, empowered) {
|
|
| 943 |
}
|
| 944 |
function dealDamage(b, src, tgt, amount, label, opts = {}) {
|
| 945 |
if (!tgt.alive) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 946 |
const { damageType = "physical", delivery = "spell", armorIgnoring = false, empowered = false } = opts;
|
| 947 |
const physical = delivery === "melee" || delivery === "projectile";
|
| 948 |
if (physical) {
|
|
|
|
| 943 |
}
|
| 944 |
function dealDamage(b, src, tgt, amount, label, opts = {}) {
|
| 945 |
if (!tgt.alive) return 0;
|
| 946 |
+
if (tgt.invulnUntil && b.t < tgt.invulnUntil) {
|
| 947 |
+
log(b, "miss", tgt, { name: label });
|
| 948 |
+
return 0;
|
| 949 |
+
}
|
| 950 |
const { damageType = "physical", delivery = "spell", armorIgnoring = false, empowered = false } = opts;
|
| 951 |
const physical = delivery === "melee" || delivery === "projectile";
|
| 952 |
if (physical) {
|