tarinai/js/pin_attachment_system.js
2026-07-01 14:41:05 +09:00

139 lines
5.6 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 resetLooseState(item) {
if (!item) return item;
item.pinState = "loose";
item.pinTargetId = "";
item.pinAttachAngle = 0;
item.pinAttachDistance = 0;
item.pinOffsetY = 0;
item.pinDamageTick = 0;
item.pinFallCheckTimer = 0;
item.pinLogAt = -999;
return item;
}
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,
resetLooseState,
drawAttachedForEntity,
});
})(typeof window !== "undefined" ? window : globalThis);