"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);