Playtest #3 fixes: lost models, picking, live LP, temp overflow, 蕾缪安 S3, 回环射手, FX, detail card, spinners
- #1 lost operator models: the Spine store no longer hands out a skeleton whose unload is in flight (PIXI.Assets.unload is async); eviction runs 1 s after a release and the empty-scene budget only after 3 s - #7 picking: one shared rule (render/pick.js, pixel probe where bodies overlap) for prep/battle clicks, hover, the enemy pen and equipment drops; the drag ghost stands on its drop tile; hover probes capped at one per frame - #2 the top-bar LP drops live as the own battle's enemies enter the blue gate (−N, 联防中), teammates' pending LP in the team panel - #3 temp overflow pieces that arrive after a prep deadline survive through the next prep (tempDue); temp-row warning and ready-button reason - #4 蕾缪安 S3: locks every 0.5 s, then one shell every 0.3 s in lock order (PRTS notes), bombardShell fx - #5 回环射手: real boomerang (out 15, back 3.75 tiles/s, attack only when every boomerang is caught) - #6 visible projectiles, muzzle flashes, slashes, skill bursts and auras at unchanged frame cost - #8 特质 right under the detail card header, with its official type icon - #9 no spinning busy cursors / spinners next to texts; game data warmed before the match - docs, recordings and tests updated (2414 unit tests, all browser suites green)
This commit is contained in:
1 parent
01a44cd288
commit
0aba602ecf
72 files changed
+5132
-571
No files matched your search
+27
-2
@@ -13,7 +13,7 @@
|
||||
// ATTACK_PAUSE seconds after each attack. Reaching the final leg's end = leak. An `attract` (诱导) status suspends the
|
||||
// route: the enemy walks to the status point instead (moveAttracted) and re-plans its route when released.
|
||||
|
||||
import { ATTACK_PAUSE, MOVE_SCALE, PROJECTILE_SPEEDS, PROJECTILE_SPEED, COLS } from './constants.js';
|
||||
import { ATTACK_PAUSE, MOVE_SCALE, PROJECTILE_SPEEDS, PROJECTILE_SPEED, BOOMERANG_RETURN_SPEED, COLS } from './constants.js';
|
||||
import { sortEnemyTargets, sortAllyTargets, canTargetEnemy, canTargetAlly, tileKeyOf } from './targeting.js';
|
||||
import { reduceElement } from './damage.js';
|
||||
import { OB_CRATE } from './grid.js';
|
||||
@@ -140,7 +140,9 @@ export function performAttack(b, u, prof, targets, opts = null) {
|
||||
b._ev(['atk', u.id, t.id, vis]);
|
||||
if (isHeal) { doHeal(b, u, prof, t); continue; }
|
||||
const info = { isSkill, index: i, attackId };
|
||||
if (ranged && t.side === 'enemy') {
|
||||
if (ranged && t.side === 'enemy' && prof.projectile === 'boomerang') {
|
||||
throwBoomerang(b, u, prof, t, info);
|
||||
} else if (ranged && t.side === 'enemy') {
|
||||
const speed = PROJECTILE_SPEEDS[prof.projectile] ?? PROJECTILE_SPEED;
|
||||
// projectiles land even if the shooter died meanwhile (damage is credited to it)
|
||||
b.addProjectile({ from: u, target: t, speed, visual: prof.projectile, source: u, hitDead: prof.splashRadius > 0,
|
||||
@@ -154,6 +156,29 @@ export function performAttack(b, u, prof, targets, opts = null) {
|
||||
if (prof.afterAttack) b._safe(() => prof.afterAttack(b, u, targets), 'profile.afterAttack', u);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回环射手 boomerang (projectile 'boomerang', professions.js loopshooter): it flies out to the target at
|
||||
* PROJECTILE_SPEEDS.boomerang and the attack hits on arrival; a target that died / vanished meanwhile is not hit (the
|
||||
* boomerang still flies to its last position — a splash profile would burst there), then it flies back to the thrower's
|
||||
* current position at BOOMERANG_RETURN_SPEED, dealing nothing on the way back, and is caught (u.trait.boomerangsOut −1:
|
||||
* the thrower attacks again once every boomerang is back). A thrower knocked out / withdrawn meanwhile loses it — nothing
|
||||
* returns to a unit off the field or to a later deployment of it (the deploy hook hands it a fresh one).
|
||||
*/
|
||||
function throwBoomerang(b, u, prof, t, info) {
|
||||
const seq = u.deploySeq;
|
||||
u.trait.boomerangsOut = (u.trait.boomerangsOut || 0) + 1;
|
||||
const home = () => u.alive && u.deployed && u.deploySeq === seq;
|
||||
b.addProjectile({ from: u, target: t, speed: PROJECTILE_SPEEDS.boomerang, visual: 'boomerang', source: u, hitDead: true,
|
||||
onHit: (c) => {
|
||||
// (guarded on its own: a content error in the hit must not cost the thrower its boomerang for the battle)
|
||||
if (c.target || prof.splashRadius > 0) b._safe(() => resolveHit(b, u, prof, c.target, info, c.x, c.y), 'boomerang.hit', u);
|
||||
if (!home()) return;
|
||||
// hitDead: flies on to the thrower's last position even while it is hidden, caught there when it is still home
|
||||
b.addProjectile({ from: { x: c.x, y: c.y }, target: u, speed: BOOMERANG_RETURN_SPEED, visual: 'boomerangReturn', source: u, hitDead: true,
|
||||
onHit: () => { if (home() && u.trait.boomerangsOut > 0) u.trait.boomerangsOut--; } });
|
||||
} });
|
||||
}
|
||||
|
||||
/** Apply one attack hit (called on impact for projectiles). `target` may be null (splash on a dead target's spot). */
|
||||
export function resolveHit(b, u, prof, target, info, x, y) {
|
||||
const atk = u.s.atk;
|
||||
|
||||
Reference in new issue
Block a user