125 lines
5.3 KiB
JavaScript
125 lines
5.3 KiB
JavaScript
"use strict";
|
|
|
|
// Layer: entity-runtime/items/pin-attachment
|
|
// Centralizes pushpin/oshibyo attachment geometry, target classification, and
|
|
// render attachment placement. Motion/effects still live in dynamic systems.
|
|
(function (global) {
|
|
function isPin(itemOrType) {
|
|
const type = typeof itemOrType === "string" ? itemOrType : itemOrType?.type;
|
|
return Boolean(type && typeof global.isPinType === "function" && global.isPinType(type));
|
|
}
|
|
function behaviorFor(itemOrType) {
|
|
const type = typeof itemOrType === "string" ? itemOrType : itemOrType?.type;
|
|
return typeof global.pinBehaviorFor === "function" ? global.pinBehaviorFor(type) : null;
|
|
}
|
|
function isOshibyo(itemOrType) {
|
|
return Boolean(behaviorFor(itemOrType)?.blocksZunchi);
|
|
}
|
|
function tarinaiVisualSide(item, tarinai) {
|
|
const faceDir = tarinai?.facingDir ? tarinai.facingDir() : (tarinai?.facing || 1);
|
|
const faceSide = faceDir >= 0 ? 1 : -1;
|
|
return isOshibyo(item) ? -faceSide : faceSide;
|
|
}
|
|
function tarinaiAnchor(item, tarinai, opts = {}) {
|
|
const r = tarinai?.radius || 16;
|
|
const visualSide = Number.isFinite(opts.visualSide) ? opts.visualSide : tarinaiVisualSide(item, tarinai);
|
|
if (isOshibyo(item)) {
|
|
const sprite = String(tarinai?.visibleSpriteId || (tarinai?.rawSpriteId ? tarinai.rawSpriteId() : "") || "");
|
|
const backSprite = sprite === "back" || sprite === "tarinai12_back" || /(^|_)back$/i.test(sprite);
|
|
const xFactor = backSprite ? 0.62 : 0.96;
|
|
const yFactor = backSprite ? 0.28 : 0.50;
|
|
const angleFactor = backSprite ? 0.32 : 0.48;
|
|
const distFactor = backSprite ? 0.70 : 0.96;
|
|
return {
|
|
visualSide,
|
|
x: (tarinai?.x || 0) + visualSide * r * xFactor,
|
|
y: (tarinai?.y || 0) + r * yFactor,
|
|
angle: visualSide * angleFactor,
|
|
distance: r * distFactor,
|
|
offsetY: r * yFactor,
|
|
};
|
|
}
|
|
const dx = Number.isFinite(opts.dx) ? opts.dx : 0;
|
|
const dy = Number.isFinite(opts.dy) ? opts.dy : -1;
|
|
const dist = Number.isFinite(opts.distance) ? opts.distance : Math.hypot(dx, dy);
|
|
const clampFn = global.clamp || ((v, lo, hi) => Math.max(lo, Math.min(hi, v)));
|
|
return {
|
|
visualSide,
|
|
x: (tarinai?.x || 0) + visualSide * r * 0.36,
|
|
y: (tarinai?.y || 0) - r * 0.04,
|
|
angle: Math.atan2(dy || -1, dx || visualSide),
|
|
poseAngle: visualSide * 0.28,
|
|
distance: clampFn(dist || r * 0.58, r * 0.20, r * 0.74),
|
|
offsetY: clampFn(dy, -r * 0.74, r * 0.38),
|
|
};
|
|
}
|
|
function applyTarinaiPose(item, tarinai, worldRef = null) {
|
|
if (!item || !tarinai) return null;
|
|
const anchor = tarinaiAnchor(item, tarinai);
|
|
item.pinVisualSide = anchor.visualSide;
|
|
item.x = anchor.x;
|
|
item.y = anchor.y;
|
|
item.prevX = item.x;
|
|
item.prevY = item.y;
|
|
item.spin = isOshibyo(item) ? anchor.angle : (anchor.poseAngle ?? anchor.angle);
|
|
item.vx = tarinai.vx || 0;
|
|
item.vy = tarinai.vy || 0;
|
|
item.deletable = false;
|
|
if (worldRef) worldRef.drawListDirty = true;
|
|
return anchor;
|
|
}
|
|
function ballAnchor(item, ball) {
|
|
const angle = (ball?.spin || 0) + (item?.pinBallOffsetAngle || 0);
|
|
const distance = Math.max(4, Number(item?.pinBallOffsetDistance || (ball?.r || 18) * 0.55) || (ball?.r || 18) * 0.55);
|
|
return {
|
|
angle,
|
|
distance,
|
|
visualSide: Math.cos(angle) >= 0 ? 1 : -1,
|
|
x: (ball?.x || 0) + Math.cos(angle) * distance,
|
|
y: (ball?.y || 0) + Math.sin(angle) * distance,
|
|
spin: angle + (isOshibyo(item) ? 0.18 : Math.PI * 0.5),
|
|
};
|
|
}
|
|
function applyBallPose(item, ball, worldRef = null) {
|
|
if (!item || !ball) return null;
|
|
const anchor = ballAnchor(item, ball);
|
|
item.prevX = item.x;
|
|
item.prevY = item.y;
|
|
item.x = anchor.x;
|
|
item.y = anchor.y;
|
|
item.vx = ball.vx || 0;
|
|
item.vy = ball.vy || 0;
|
|
item.pinVisualSide = anchor.visualSide;
|
|
item.spin = anchor.spin;
|
|
item.deletable = false;
|
|
if (Number.isFinite(ball.spinVelocity)) item.spinVelocity = ball.spinVelocity;
|
|
if (worldRef) worldRef.drawListDirty = true;
|
|
return anchor;
|
|
}
|
|
function isAttachedToEntity(pin, entity) {
|
|
if (!pin || !entity || !isPin(pin)) return false;
|
|
if (entity.type === "ball") return pin.pinState === "ball_lodged" && pin.pinBallId === entity.id;
|
|
const isTarinaiEntity = typeof global.Tarinai !== "undefined" && entity instanceof global.Tarinai;
|
|
return isTarinaiEntity && pin.pinState === "lodged" && pin.pinTargetId === entity.id;
|
|
}
|
|
function drawAttachedForEntity(args = {}) {
|
|
const { renderStack, entity, visibleRect, worldRef, time, drawnIds, isVisible } = args;
|
|
if (!renderStack || !entity || !drawnIds || typeof isVisible !== "function") return 0;
|
|
const source = entity.type === "ball" ? (renderStack.ballLodgedPins || []) : (renderStack.lodgedPins || []);
|
|
let drawn = 0;
|
|
for (const it of source) {
|
|
if (!it || drawnIds.has(it.id) || !isAttachedToEntity(it, entity)) continue;
|
|
if (isVisible(it, visibleRect)) it.draw?.(global.ctx || args.ctx, time ?? worldRef?.time ?? 0, args.lighting || null);
|
|
drawnIds.add(it.id);
|
|
drawn += 1;
|
|
}
|
|
return drawn;
|
|
}
|
|
global.TarinaiPinAttachmentSystem = Object.freeze({
|
|
isPin,
|
|
tarinaiAnchor,
|
|
applyTarinaiPose,
|
|
applyBallPose,
|
|
drawAttachedForEntity,
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|