feedback1 integrate: WN 灼热/炽焰源石虫 models (local overlay, tinted alias); 暴鸰 bomb drop
# Conflicts: # docs/SIM.md # public/js/render/app.js # public/js/render/spine.js # public/js/render/units.js # server/sim/content/enemies.js # server/sim/snapshot.js
This commit is contained in:
commit
4b15919185
26 files changed
+1383
-90
No files matched your search
+10
-5
@@ -79,8 +79,10 @@
|
||||
// three.js PerspectiveCamera is synced from it every change. Missing art / three / WebGL2, a failed init or a lost
|
||||
// context fall back to the 2D atlas board. `opts.board`: 'auto' (default) | '3d' | '2d'; `?board=2d|3d` in the URL
|
||||
// overrides (dev; '3d' also accepts a slow / software GPU). stats().board3d → { on, calls, triangles, cpuMs }.
|
||||
// three.js is only downloaded when the local-art manifest lists the board atlas. The built 3D area, the drawn 2D rows
|
||||
// and the lit rect follow `viewKind` (a 'prep' camera on the boss rows = the Final Assault prep = the boss field).
|
||||
// three.js is only downloaded when the local-art manifest lists the board atlas. That manifest is awaited with the asset
|
||||
// manifest (≤ 4 s), so an enemy whose model only the local client has draws it (assets.js spineEntry). The built 3D
|
||||
// area, the drawn 2D rows and the lit rect follow `viewKind` (a 'prep' camera on the boss rows = the Final Assault
|
||||
// prep = the boss field).
|
||||
// Battle device boxes (`ctx.createBox`) follow the board layer (switchableBox), so a 3D ⇄ 2D switch keeps crates.
|
||||
// Crowds and clipped Spine skeletons render through the shared impostor atlas (render/impostor.js), flushed once per
|
||||
// frame before the main pass.
|
||||
@@ -388,7 +390,10 @@ export async function createFieldView(host, options = {}) {
|
||||
const artListed = want3d ? boardArtListed(assets).catch(() => false) : Promise.resolve(false);
|
||||
const threePromise = artListed.then((ok) => (ok ? loadThree() : null));
|
||||
const packPromise = artListed.then((ok) => (ok ? Promise.resolve(assets.ready ? assets.ready() : null).catch(() => null).then(() => loadBoardPack(assets)) : null));
|
||||
await withTimeout(Promise.resolve(assets.ready ? assets.ready() : null).catch(() => {}), 4000);
|
||||
// the manifest, and the optional local-art manifest in parallel: unit views pick an enemy's local-client model by it
|
||||
// (assets.js spineEntry, DESIGN §13 — 灼热源石虫 / 炽焰源石虫); absent or slow, they draw the web models
|
||||
await withTimeout(Promise.all([assets.ready ? assets.ready() : null, assets.local ? assets.local() : null]
|
||||
.map((p) => Promise.resolve(p).catch(() => {}))), 4000);
|
||||
// web fonts for the bitmap damage numbers / tier chips (never block long)
|
||||
try { if (document.fonts?.load) await withTimeout(Promise.all([document.fonts.load('700 40px Bender'), document.fonts.load('700 40px Oxanium')]), 1500); } catch { /* ignore */ }
|
||||
|
||||
@@ -1367,7 +1372,7 @@ export async function createFieldView(host, options = {}) {
|
||||
if (e[0] !== 'atk' || woundUp.has(e) || CHAIN_KINDS.has(e[3])) return;
|
||||
const v = views.get(e[1]);
|
||||
if (!v || !v.windUp) return;
|
||||
if (v.windUp(t - upcomingT)) woundUp.add(e);
|
||||
if (v.windUp(t - upcomingT, e[3])) woundUp.add(e);
|
||||
}
|
||||
|
||||
const EVS = [];
|
||||
@@ -1399,7 +1404,7 @@ export async function createFieldView(host, options = {}) {
|
||||
const src = views.get(e[1]) || battleView(e[1]);
|
||||
const tgt = views.get(e[2]) || battleView(e[2]);
|
||||
// chain / chainHeal bounces: the "source" is the previous target of the bounce, not an attacker
|
||||
if (src && !CHAIN_KINDS.has(e[3])) src.onAttack?.(tgt, now);
|
||||
if (src && !CHAIN_KINDS.has(e[3])) src.onAttack?.(tgt, now, e[3]);
|
||||
if (e[3] === 'none' || !e[3]) { if (tgt && src) meleePending.set(tgt.id, { src, t: now }); }
|
||||
fx.attack(src, tgt, e[3]);
|
||||
break;
|
||||
|
||||
@@ -237,9 +237,16 @@ const FX_GUESS = [
|
||||
[/stealth|hide|vanish|cloak/i, 'vanish'], [/pull|push|knock|dash|leap/i, 'move'], [/pulse|wave|ring|sonic/i, 'wave'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 'phase' (an enemy's mode change, render/units.js FORMS) kinds the model shows on its own: 暴鸰 'bombed' — its bomb is
|
||||
* the 'droneBomb' projectile of the same moment, a puff on the drone would read as something else (feedback D4).
|
||||
*/
|
||||
const SILENT_PHASES = new Set(['bombed']);
|
||||
|
||||
/** Visual spec of an fx kind (see FX_KINDS); `extra.kind` / `extra.element` may pick a better colour. */
|
||||
export function fxSpec(kind, extra = {}) {
|
||||
const k = typeof kind === 'string' ? kind : '';
|
||||
if (k === 'phase' && SILENT_PHASES.has(extra && extra.kind)) return { a: 'none', c: 0xffffff };
|
||||
let spec = FX_KINDS[k];
|
||||
if (!spec) {
|
||||
const g = FX_GUESS.find(([re]) => re.test(k));
|
||||
|
||||
+24
-14
@@ -6,8 +6,10 @@
|
||||
//
|
||||
// Driving (units.js calls these; the sim is authoritative, the actor only visualises):
|
||||
// setBase('idle'|'move'|'stun') the resting state from the snapshot anim code
|
||||
// attack(interval) one attack happened now (b.ev 'atk'): plays begin→loop, re-phases the loop
|
||||
// so its OnAttack frame lands now, timeScale = loopDuration / interval
|
||||
// attack(interval, once) one attack happened now (b.ev 'atk'): plays begin→loop, re-phases the loop
|
||||
// so its OnAttack frame lands now, timeScale = loopDuration / interval;
|
||||
// `once` (a one-off cast, style.js PROJ[kind].once — 暴鸰's bomb drop): the
|
||||
// clip plays once at its own speed, then base
|
||||
// setSkill(on) skill begin→loop while active (skill idle replaces idle), end on stop
|
||||
// deploy() 'Start' once, then base
|
||||
// die() die clip once (callers fade out afterwards)
|
||||
@@ -17,7 +19,8 @@
|
||||
// closing clip timed to end `in` s from now (a 重生's last clip ends with the
|
||||
// 重生), landing in `roles`
|
||||
// update(dt) advances the skeleton (autoUpdate is off: one clock for everything)
|
||||
// Attack mode lasts until ~1.4 attack intervals without a new attack, then the end clip (if any) and base.
|
||||
// Attack mode lasts until ~1.4 attack intervals without a new attack (a `once` cast: to the end of its clip), then the
|
||||
// end clip (if any) and base.
|
||||
|
||||
const clampN = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
|
||||
@@ -229,42 +232,49 @@ export class SpineActor {
|
||||
* clip from its wind-up so the strike frame lands when the attack event is rendered. Returns true once started;
|
||||
* false when it is still too early (call again next frame) or there is nothing to wind up.
|
||||
*/
|
||||
windUp(interval, lead) {
|
||||
windUp(interval, lead, once = false) {
|
||||
if (this.dead || this.mode === 'stun' || this.mode === 'die' || this.mode === 'change' || !(lead >= 0)) return false;
|
||||
const clip = this._attackClip();
|
||||
if (!clip) return false;
|
||||
if (this.mode === 'attack' && this.current === clip.loop) return false; // in rhythm: attack() re-phases
|
||||
const iv = clampN(Number.isFinite(interval) && interval > 0 ? interval : this.interval, 0.08, 8);
|
||||
const loopDur = this.dur(clip.loop);
|
||||
// a one-off cast plays at the clip's own speed (one loop per clip length), whatever the attack rhythm
|
||||
const iv = once ? loopDur : clampN(Number.isFinite(interval) && interval > 0 ? interval : this.interval, 0.08, 8);
|
||||
const hit = this._hitTime(clip.loop, loopDur);
|
||||
const plan = windUpPlan(loopDur, hit, iv, lead);
|
||||
if (!(hit > 0) || lead * plan.ts > hit + 1e-6) return false;
|
||||
this.interval = iv;
|
||||
if (!once) this.interval = iv;
|
||||
this.mode = 'attack';
|
||||
this.attackUntil = this.clock + lead + Math.max(0.45, iv * 1.4);
|
||||
this._play(clip.loop, true, { timeScale: plan.tsWind, start: plan.start, mix: 0.06 });
|
||||
this.attackUntil = this.clock + lead + (once ? Math.max(0, loopDur - hit) / plan.ts : Math.max(0.45, iv * 1.4));
|
||||
this._play(clip.loop, !once, { timeScale: plan.tsWind, start: plan.start, mix: 0.06 });
|
||||
this.windTs = plan.ts;
|
||||
this.windUntil = this.clock + lead;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** An attack happened now. `interval` = seconds between attacks (game time already scaled to real). */
|
||||
attack(interval) {
|
||||
/**
|
||||
* An attack happened now. `interval` = seconds between attacks (game time already scaled to real); `once` = a one-off
|
||||
* cast (no rhythm): the clip plays once at its own speed from its strike frame, then the resting state.
|
||||
*/
|
||||
attack(interval, once = false) {
|
||||
if (this.dead || this.mode === 'stun' || this.mode === 'die' || this.mode === 'change') return; // a form change plays out
|
||||
this.interval = clampN(Number.isFinite(interval) && interval > 0 ? interval : this.interval, 0.08, 8);
|
||||
if (!once) this.interval = clampN(Number.isFinite(interval) && interval > 0 ? interval : this.interval, 0.08, 8);
|
||||
const clip = this._attackClip();
|
||||
if (!clip) return;
|
||||
const loopDur = this.dur(clip.loop);
|
||||
const ts = clampN(loopDur / this.interval, 0.35, 4);
|
||||
const ts = once ? 1 : clampN(loopDur / this.interval, 0.35, 4);
|
||||
const hit = this._hitTime(clip.loop, loopDur);
|
||||
const wasAttacking = this.mode === 'attack' && this.current === clip.loop;
|
||||
this.mode = 'attack';
|
||||
this.attackUntil = this.clock + Math.max(0.45, this.interval * 1.4);
|
||||
this.attackUntil = this.clock + (once ? Math.max(0, loopDur - hit) / ts : Math.max(0.45, this.interval * 1.4));
|
||||
this.windUntil = null;
|
||||
if (!wasAttacking) {
|
||||
// not wound up (no look-ahead, e.g. a batch that arrived late): the sim already resolved the hit, so show
|
||||
// the strike frame now
|
||||
this._play(clip.loop, true, { timeScale: ts, start: hit, mix: 0.06 });
|
||||
this._play(clip.loop, !once, { timeScale: ts, start: hit, mix: 0.06 });
|
||||
} else if (once) {
|
||||
const e = this.spine.state.tracks[0];
|
||||
if (e) e.timeScale = ts;
|
||||
} else {
|
||||
const e = this.spine.state.tracks[0];
|
||||
if (e) {
|
||||
|
||||
@@ -89,7 +89,7 @@ export function dmgStyleKey(type) {
|
||||
export const HIT_TINT = Object.freeze({ phys: 0xffd9a0, arts: 0xc77dff, true: 0xffffff, heal: 0x62f08a, elem: 0xff7b3a });
|
||||
|
||||
/**
|
||||
* Projectile visuals per b.ev 'atk' projKind (sim: none | arrow | bolt | bomb | lob | orb | drone | enemy | boomerang;
|
||||
* Projectile visuals per b.ev 'atk' projKind (sim: none | arrow | bolt | bomb | lob | orb | drone | enemy | boomerang | droneBomb;
|
||||
* chain / chainHeal / beam are beams). Drawn by render/fx.js:
|
||||
* look tracer (bullet streak + muzzle flash) | orb (glowing ball, particle trail) | shell (lobbed on an arc with a
|
||||
* ground shadow, smoke trail) | dart (small fast bolt) | boomerang (spins to the target and back to the thrower)
|
||||
@@ -101,6 +101,8 @@ export const HIT_TINT = Object.freeze({ phys: 0xffd9a0, arts: 0xc77dff, true: 0x
|
||||
* diameter; `arc` peak height of a lob over a 3-tile throw (scales with the range)
|
||||
* colours `tint` hot core, `glow` halo, `trail` trail particles, `muzzle` flash at the shooter
|
||||
* hit arrival burst (fx.js _impact): spark | arts | heal | boom | splash | zap | enemy
|
||||
* once a one-off cast, not an attack rhythm: the shooter's attack clip plays once at its own speed (render/spine.js
|
||||
* SpineActor.attack / windUp) — 暴鸰's bomb drop
|
||||
*/
|
||||
export const PROJ = Object.freeze({
|
||||
arrow: { look: 'tracer', speed: 14, tint: 0xfff6dc, glow: 0xffc45a, len: 1.05, width: 0.2, head: 0.36, muzzle: 0xffd27a, hit: 'spark' },
|
||||
@@ -111,6 +113,9 @@ export const PROJ = Object.freeze({
|
||||
drone: { look: 'dart', speed: 16, tint: 0xe4fbff, glow: 0x57c9ff, len: 0.7, width: 0.16, head: 0.3, trail: 0x57c9ff, hit: 'zap' },
|
||||
enemy: { look: 'orb', speed: 10, tint: 0xffe2da, glow: 0xff3b30, len: 0.55, width: 0.28, head: 0.44, trail: 0xff4a3a, muzzle: 0xff6a5a, hit: 'enemy' },
|
||||
boomerang: { look: 'boomerang', speed: 15, back: 3.75, tint: 0xfff4d6, glow: 0x9ff0dc, len: 0.4, width: 0.3, head: 0.5, trail: 0x9ff0dc, hit: 'spark' },
|
||||
// 暴鸰's bomb (sim content/enemies.js kitBombd; official projectile_bombd, speed 5): dropped from the drone, it falls
|
||||
// onto its target with a low arc and bursts where the sim's 'explode' blast goes off
|
||||
droneBomb: { look: 'shell', speed: 5, once: true, tint: 0xffe2c8, glow: 0xff5a3a, len: 0.6, width: 0.3, head: 0.5, trail: 0xff7a4a, arc: 0.35, smoke: 0x2e2824, hit: 'boom' },
|
||||
});
|
||||
|
||||
/** Status keys (b.ev 'status' + UF flags) → icon atlas key (render/textures.js) and colour. */
|
||||
|
||||
+61
-19
@@ -4,7 +4,10 @@
|
||||
// the avatar-in-rarity-diamond fallback) + HUD container (bar layer: HP bar with delayed "ghost" damage, SP bar
|
||||
// with ready glow / draining skill bar, tier chip, status icons, blocked marker). The fallback shows at once and
|
||||
// cross-fades to the Spine model when it has loaded; missing or failed models keep the fallback forever (the
|
||||
// game never blocks on Spine). Every bar is a tinted Texture.WHITE sprite, so HUDs batch into few draw calls.
|
||||
// game never blocks on Spine) — except an optional local-client model (assets.js spineEntry `fallback`, DESIGN §13:
|
||||
// 灼热源石虫 / 炽焰源石虫), which falls back to the web model first; that web alias (the plain 源石虫) is drawn tinted
|
||||
// toward the slug's own colours (ALIAS_TINT). Every bar is a tinted Texture.WHITE sprite, so HUDs batch into few draw
|
||||
// calls.
|
||||
//
|
||||
// Placement: feet anchored at world (x, y, z); scale = camera px-per-tile at the feet × UNIT.modelScale, so
|
||||
// chibis shrink with distance like the original. An enemy's model is also scaled by its official prefab factor
|
||||
@@ -25,9 +28,10 @@
|
||||
// An operator that enters a battle already knocked out (联防, user playtest #5 item 2: sim 'die' reason 'forcedExit')
|
||||
// goes down with `die(true)`: straight to the held end of the clip, no fall.
|
||||
// Enemy modes (sim fx 'phase' { id, kind } or another fx's `form` → `setForm(kind, fx)`): 掠海漂移体 dropping to 爬行模式
|
||||
// (user playtest #5 item 1) plays its skeleton's 'Change' clip once, then the crawl set (*_02); 转译基底's forms, the 逐火
|
||||
// embers, 再生's puppet, the leaders' 重生 and 守墓石像 likewise (user report after 0.1.0) — FORMS. A view built later
|
||||
// (`info.form` = UnitInfo `form`, the sim's current form, through render/app.js renderInfo) starts in the mode.
|
||||
// (user playtest #5 item 1) plays its skeleton's 'Change' clip once, then the crawl set (*_02); 暴鸰 flies on without its
|
||||
// bomb (*_2) after the drop (feedback D4); 转译基底's forms, the 逐火 embers, 再生's puppet, the leaders' 重生 and 守墓石像
|
||||
// likewise (user report after 0.1.0) — FORMS. A view built later (`info.form` = UnitInfo `form`, the sim's current form,
|
||||
// through render/app.js renderInfo) starts in the mode.
|
||||
// Element gauges (b.snap `elem` → sample `el` / `elFill` / `elUntil` / `elDur`), the official form (PRTS 元素: "模型
|
||||
// 下部会显示对应的元素图标,并以白条显示剩余的元素值"; enemies "小尺寸图标(不显示元素图标,仅根据元素种类改变背景色)"): a row
|
||||
// right under the unit's own HP / SP bars and inside their span — the element's disc at the left (operators with its
|
||||
@@ -55,7 +59,7 @@
|
||||
import { UF, ANIM } from '../../../shared/constants.js';
|
||||
import { SpineActor } from './spine.js';
|
||||
import { diamondTexture, shadowTexture, fxAtlas, tierChip, statusTexture, itemTexture, hudRings, ringArc, HUD_DISC, ELEMENT_RING } from './textures.js';
|
||||
import { COLORS, TIER_COLORS, ENEMY_FRAME, UNIT, statusIconKey } from './style.js';
|
||||
import { COLORS, TIER_COLORS, ENEMY_FRAME, UNIT, PROJ, statusIconKey } from './style.js';
|
||||
import { drawCrate, rowDepthKey, ROW_KEY, deviceBoxOf, DEVICE_BOX } from './tiles.js';
|
||||
|
||||
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
@@ -93,6 +97,13 @@ export const DOWN_LOOK = Object.freeze({
|
||||
ring: Object.freeze({ [DOWN_STATE.COUNTING]: 0x4ed8af, [DOWN_STATE.WAIT_DP]: 0xffc600, [DOWN_STATE.WAIT_TILE]: 0xff4b3e }),
|
||||
size: 0.42, height: 1.02,
|
||||
});
|
||||
/**
|
||||
* An enemy drawn with another enemy's web model because its own is only in the local client (manifest `spineAliasOf`
|
||||
* + `spineLocal`: 灼热 / 炽焰源石虫 → the plain 源石虫's skeleton, feedback D3 after 0.1.0): a multiply tint toward its own
|
||||
* lava colours (orange / red-orange), so it reads apart from the plain slug where the local art was not extracted
|
||||
* (research 07 §5.6 "a hue shift") [ASSUMED look]. Never on its official (local) model; status tints win over it.
|
||||
*/
|
||||
export const ALIAS_TINT = Object.freeze({ enemy_1305_mhslim: 0xffc48a, enemy_1305_mhslim_2: 0xff9070 });
|
||||
/**
|
||||
* Element gauge row under the bars (see header): the disc's diameter in tiles and its pixel clamp (enemies × `enemy`),
|
||||
* the gap under the bars (px). The white bar is as tall as the SP bar and fills the rest of the bars' width. During a
|
||||
@@ -106,6 +117,9 @@ export const EL_BAR = Object.freeze({ icon: 0.15, min: 8, max: 15, enemy: 0.8, g
|
||||
* → UnitView.setForm), per Spine id; the mode's roles override the manifest's (data/assets.json anims), `change` plays
|
||||
* once first:
|
||||
* - 掠海漂移体 (PRTS: 受晕眩/沉睡/冻结影响后进入爬行模式 — for good) crawls on its *_02 clips after 'Change';
|
||||
* - 暴鸰 flies on its bomb-less *_2 clips once its one bomb left (the official prefab's mode S1: Move→Move_2, Idle→Idle_2,
|
||||
* Die→Die_2; user feedback after 0.1.0, D4: the bomb used to stay under the drone — no change clip, the drop is its
|
||||
* Attack clip);
|
||||
* - 转译基底·α (user report after 0.1.0, #5): its 2 s change clip A_Die_B / _C / _D, then 寻仇者 B_*, 幽灵 C_* (no attack
|
||||
* clip: it never attacks) or 特战术师 D_* — its manifest roles are the original form's A_Idle / A_Move;
|
||||
* - 深池逐火战士 / 精锐战士 / 护卫 (#8): knocked out → 'Die' (the 1 s 重生), the 余烬 on Idle_2 / Move_2 and its death on
|
||||
@@ -144,6 +158,14 @@ const STATUE = Object.freeze({
|
||||
});
|
||||
const JAKILL2 = clipSet('C2_Idle', 'C2_Move', 'C2_Die', 'C2_Attack');
|
||||
export const FORMS = Object.freeze({
|
||||
enemy_1040_bombd: Object.freeze({
|
||||
bombed: Object.freeze({
|
||||
roles: Object.freeze({
|
||||
idle: 'Idle_2', deploy: 'Idle_2', die: 'Die_2',
|
||||
move: Object.freeze({ begin: 'Move_Begin_2', loop: 'Move_Loop_2', end: 'Move_End_2' }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
enemy_2025_syufo: Object.freeze({
|
||||
crawl: Object.freeze({ change: 'Change', roles: clipSet('Idle_02', 'Move_02', 'Die_02', 'Attack_02') }),
|
||||
}),
|
||||
@@ -294,6 +316,7 @@ export class UnitView {
|
||||
this.body.addChild(this.fallback);
|
||||
this.actor = null;
|
||||
this.spineReady = false;
|
||||
this.baseTint = 0xffffff; // the drawn model's own tint (ALIAS_TINT), under the status tints
|
||||
|
||||
this.hud = new P.Container();
|
||||
ctx.layers.bars.addChild(this.hud);
|
||||
@@ -349,10 +372,19 @@ export class UnitView {
|
||||
const back = this._wantsBack();
|
||||
const entry = id ? a.spineEntry(id, { back }) : null;
|
||||
if (!entry || this.ctx.settings?.quality === 'low' && this.isEnemy && !this.isBoss && this.ctx.crowded?.()) return;
|
||||
// every acquire is paired with exactly one release: a superseded / failed / post-destroy load releases its own
|
||||
// entry; the displayed model's entry (`_actorEntry`) is released when that model is replaced or destroyed
|
||||
this.entry = entry;
|
||||
this.entryBack = back;
|
||||
this._acquireSpine(entry, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load `entry` and show it. Every acquire is paired with exactly one release: a superseded / failed / post-destroy
|
||||
* load releases its own entry; the displayed model's entry (`_actorEntry`) is released when that model is replaced or
|
||||
* destroyed. A model that fails to load keeps the fallback diamond — unless the entry names a `fallback` (an optional
|
||||
* local-client model, assets.js spineEntry: DESIGN §13), which is loaded in its place.
|
||||
*/
|
||||
_acquireSpine(entry, id) {
|
||||
const a = this.ctx.assets;
|
||||
this.entry = entry;
|
||||
const req = this._spineReq = (this._spineReq || 0) + 1;
|
||||
a.spine.acquire(entry).then((data) => {
|
||||
if (this.destroyed || req !== this._spineReq) { this._releaseEntry(entry); return; }
|
||||
@@ -370,6 +402,7 @@ export class UnitView {
|
||||
if (swap) this._dropActor();
|
||||
this.actor = actor;
|
||||
this._actorEntry = entry;
|
||||
this.baseTint = (!entry.local && ALIAS_TINT[id]) || 0xffffff; // the web alias of a local-only model
|
||||
this.body.addChild(this.actor.spine);
|
||||
this.spineReady = true;
|
||||
this.swapT = swap ? 1 : 0;
|
||||
@@ -386,7 +419,11 @@ export class UnitView {
|
||||
if (this.flags & UF.SKILL) this.actor.setSkill(true);
|
||||
this.actor.setBase(this._baseFromAnim());
|
||||
}
|
||||
}, () => { this._releaseEntry(entry); /* keep the fallback */ });
|
||||
}, () => {
|
||||
this._releaseEntry(entry);
|
||||
if (entry.fallback && !this.destroyed && req === this._spineReq) this._acquireSpine(entry.fallback, id);
|
||||
// else keep the fallback diamond
|
||||
});
|
||||
}
|
||||
|
||||
_formSpec() {
|
||||
@@ -573,13 +610,17 @@ export class UnitView {
|
||||
}
|
||||
|
||||
/** An attack was made (b.ev 'atk'). `target` = view or null. */
|
||||
onAttack(target, now) {
|
||||
onAttack(target, now, kind) {
|
||||
if (!this.alive) return;
|
||||
if (this.lastAtk >= 0) {
|
||||
const d = now - this.lastAtk;
|
||||
if (d > 0.05 && d < 6) this.atkInterval = this.atkInterval * 0.6 + d * 0.4;
|
||||
// a one-off cast (PROJ[kind].once: 暴鸰's bomb drop) is no attack rhythm: its clip plays once at its own speed
|
||||
const once = !!PROJ[kind]?.once;
|
||||
if (!once) {
|
||||
if (this.lastAtk >= 0) {
|
||||
const d = now - this.lastAtk;
|
||||
if (d > 0.05 && d < 6) this.atkInterval = this.atkInterval * 0.6 + d * 0.4;
|
||||
}
|
||||
this.lastAtk = now;
|
||||
}
|
||||
this.lastAtk = now;
|
||||
if (target && !this.isEnemy && this.info.kind !== 'device') {
|
||||
// operators keep their deploy direction (research 09 §1.2); enemies may turn towards their target
|
||||
} else if (target && this.isEnemy) {
|
||||
@@ -591,17 +632,18 @@ export class UnitView {
|
||||
this.lungeDir.x = dx / len; this.lungeDir.y = dy / len;
|
||||
}
|
||||
this.lunge = 1;
|
||||
if (this.actor) this.actor.attack(this.atkInterval); // game seconds: the actor's clock runs in game time
|
||||
if (this.actor) this.actor.attack(this.atkInterval, once); // game seconds: the actor's clock runs in game time
|
||||
if (this.imp) this.imp.dirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* An attack by this unit is `lead` game seconds ahead in the snapshot buffer: start the Spine attack wind-up now
|
||||
* so the strike frame lines up with the attack. True once started (then stop calling for that attack).
|
||||
* so the strike frame lines up with the attack. True once started (then stop calling for that attack). `kind` = the
|
||||
* 'atk' projKind (a one-off cast winds up at the clip's own speed).
|
||||
*/
|
||||
windUp(lead) {
|
||||
windUp(lead, kind) {
|
||||
if (!this.alive || !this.actor || !this.spineReady) return false;
|
||||
const ok = this.actor.windUp(this.atkInterval, lead);
|
||||
const ok = this.actor.windUp(this.atkInterval, lead, !!PROJ[kind]?.once);
|
||||
if (ok && this.imp) this.imp.dirty = true;
|
||||
return ok;
|
||||
}
|
||||
@@ -732,7 +774,7 @@ export class UnitView {
|
||||
this.fallback.visible = this.swapT < 1;
|
||||
const sc = s * UNIT.modelScale * this.modelK;
|
||||
const flashK = this.flash > 0 ? this.flash : 0;
|
||||
let tint = 0xffffff;
|
||||
let tint = this.baseTint;
|
||||
if (this.down) tint = DOWN_LOOK.tint;
|
||||
else if (this.flags & UF.FROZEN) tint = 0x9fd4ff;
|
||||
else if (this.flags & UF.COLD) tint = 0xcfe6ff;
|
||||
|
||||
Reference in new issue
Block a user