I Want To Heal 2 build v1.0.5 code

This commit is contained in:
Warren H
2026-06-28 13:29:39 -04:00
parent 8abb44ea02
commit 257c34fc8d
53 changed files with 18187 additions and 1199 deletions
+413
View File
@@ -0,0 +1,413 @@
import * as THREE from 'three'
import type { BossInput } from './actionCombatTypes'
import type { SpellSlot } from './actionCombatCore'
export const ACTION_ARENA_SCALE = 48
export const ACTION_PLAYER_KEYS = new Set(['w', 'a', 's', 'd', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'])
export const ACTION_RENDER_QUALITY = {
antialias: false,
maxPixelRatio: 1.1,
shadows: false,
}
export const CLAUDECRAFT_CAMERA_FOV = 60
const CLAUDECRAFT_CAMERA_PITCH = 0.32
const CLAUDECRAFT_CAMERA_DISTANCE = 12
const CLAUDECRAFT_CAMERA_EYE_HEIGHT = 2
const CLAUDECRAFT_CAMERA_DRAG_SENSITIVITY = 0.006
export type ActionWorldPoint = { x: number, z: number }
export type ActionCameraTarget = { x: number, y: number }
export type ActionArenaProjection = { height: number, width: number }
export type ActionWorldProjector = (x: number, y: number) => ActionWorldPoint
export type ActionCombatCameraController = {
bindDrag: (element: HTMLElement, options?: {
isPaused?: () => boolean,
onPointerDown?: (event: PointerEvent) => void,
}) => () => void
reset: (yaw?: number) => void
yaw: number
}
export type CombatActionQueue = {
reset: boolean
}
export function readCombatInput(
keys: Set<string>,
cameraYaw: number,
targetId: string | null,
castSpell: SpellSlot | null,
actions: CombatActionQueue,
options?: { fallbackAim?: { x: number, y: number } },
): BossInput {
const strafe = Number(keys.has('d') || keys.has('arrowright')) - Number(keys.has('a') || keys.has('arrowleft'))
const forwardInput = Number(keys.has('w') || keys.has('arrowup')) - Number(keys.has('s') || keys.has('arrowdown'))
const forwardX = Math.sin(cameraYaw)
const forwardY = Math.cos(cameraYaw)
const rightX = -Math.cos(cameraYaw)
const rightY = Math.sin(cameraYaw)
const xAxis = rightX * strafe + forwardX * forwardInput
const yAxis = rightY * strafe + forwardY * forwardInput
const length = Math.hypot(xAxis, yAxis)
const fallbackAim = options?.fallbackAim ?? { x: 0, y: -1 }
return {
xAxis,
yAxis,
reset: actions.reset,
targetDelta: 0,
targetId,
castSpell,
aimX: length > 0.01 ? xAxis / length : fallbackAim.x,
aimY: length > 0.01 ? yAxis / length : fallbackAim.y,
}
}
export function clearCombatActions(actions: CombatActionQueue) {
actions.reset = false
}
export function setupActionSceneEnvironment(scene: THREE.Scene, options?: { divider?: boolean, worldHeight?: number, worldWidth?: number }) {
scene.background = new THREE.Color(0x10151a)
scene.fog = new THREE.Fog(0x10151a, 14, 30)
const worldWidth = options?.worldWidth ?? 20
const worldHeight = options?.worldHeight ?? 11.25
const ambient = new THREE.HemisphereLight(0xf8f0d8, 0x1c2530, 1.7)
scene.add(ambient)
const sun = new THREE.DirectionalLight(0xfff1c2, 2.8)
sun.position.set(-6, 11, 8)
sun.castShadow = true
sun.shadow.camera.left = -13
sun.shadow.camera.right = 13
sun.shadow.camera.top = 9
sun.shadow.camera.bottom = -9
scene.add(sun)
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(worldWidth, worldHeight, 18, 10),
new THREE.MeshStandardMaterial({ color: 0x243126, roughness: 0.88 }),
)
ground.rotation.x = -Math.PI / 2
ground.receiveShadow = true
scene.add(ground)
const grid = new THREE.GridHelper(worldWidth, Math.max(20, Math.round(worldWidth)), 0x566044, 0x3b4436)
grid.position.y = 0.012
scene.add(grid)
addActionArenaBounds(scene, { divider: options?.divider, worldHeight, worldWidth })
}
export function addActionArenaBounds(scene: THREE.Scene, options?: { divider?: boolean, worldHeight?: number, worldWidth?: number }) {
const wallMaterial = new THREE.MeshStandardMaterial({ color: 0x32323a, roughness: 0.7 })
const worldWidth = options?.worldWidth ?? 20
const worldHeight = options?.worldHeight ?? 11.25
const halfW = worldWidth / 2
const halfH = worldHeight / 2
for (const wall of [
{ x: 0, z: -halfH - 0.15, sx: worldWidth + 0.4, sz: 0.28 },
{ x: 0, z: halfH + 0.15, sx: worldWidth + 0.4, sz: 0.28 },
{ x: -halfW - 0.15, z: 0, sx: 0.28, sz: worldHeight + 0.3 },
{ x: halfW + 0.15, z: 0, sx: 0.28, sz: worldHeight + 0.3 },
]) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(wall.sx, 0.62, wall.sz), wallMaterial)
mesh.position.set(wall.x, 0.31, wall.z)
mesh.castShadow = true
mesh.receiveShadow = true
scene.add(mesh)
}
if (!options?.divider) return
const dividerHeight = 3.45
const divider = new THREE.Mesh(
new THREE.BoxGeometry(0.22, dividerHeight, worldHeight - 0.85),
new THREE.MeshStandardMaterial({ color: 0x5d6470, roughness: 0.72, transparent: true, opacity: 0.48 }),
)
divider.position.set(0, dividerHeight / 2, 0)
divider.castShadow = true
divider.receiveShadow = true
scene.add(divider)
}
export function createActionCombatCamera(options?: { aspect?: number, far?: number, near?: number }) {
return new THREE.PerspectiveCamera(
CLAUDECRAFT_CAMERA_FOV,
options?.aspect ?? 16 / 9,
options?.near ?? 0.1,
options?.far ?? 120,
)
}
export function createActionCombatCameraController(initialYaw = Math.PI): ActionCombatCameraController {
let dragX: number | null = null
const controller: ActionCombatCameraController = {
yaw: initialYaw,
reset: (yaw = initialYaw) => {
controller.yaw = yaw
dragX = null
},
bindDrag: (element, options) => {
const isPaused = options?.isPaused ?? (() => false)
const handlePointerDown = (event: PointerEvent) => {
if (isPaused()) return
element.setPointerCapture(event.pointerId)
dragX = event.clientX
options?.onPointerDown?.(event)
}
const handlePointerMove = (event: PointerEvent) => {
if (isPaused() || dragX === null) return
controller.yaw -= (event.clientX - dragX) * CLAUDECRAFT_CAMERA_DRAG_SENSITIVITY
dragX = event.clientX
}
const handlePointerUp = (event: PointerEvent) => {
if (isPaused()) return
if (element.hasPointerCapture(event.pointerId)) element.releasePointerCapture(event.pointerId)
dragX = null
}
element.addEventListener('pointerdown', handlePointerDown)
element.addEventListener('pointermove', handlePointerMove)
element.addEventListener('pointerup', handlePointerUp)
element.addEventListener('pointercancel', handlePointerUp)
return () => {
element.removeEventListener('pointerdown', handlePointerDown)
element.removeEventListener('pointermove', handlePointerMove)
element.removeEventListener('pointerup', handlePointerUp)
element.removeEventListener('pointercancel', handlePointerUp)
}
},
}
return controller
}
export function resizeActionCombatCamera(camera: THREE.PerspectiveCamera, width: number, height: number) {
camera.aspect = Math.max(1, width) / Math.max(1, height)
camera.updateProjectionMatrix()
}
export function updateActionCombatCamera(
camera: THREE.PerspectiveCamera,
yaw: number,
target: ActionCameraTarget,
project: ActionWorldProjector = toActionWorld,
) {
const point = project(target.x, target.y)
applyClaudeCraftCamera(camera, yaw, point)
}
export function updateFollowCamera(camera: THREE.PerspectiveCamera, yaw: number, target: ActionCameraTarget) {
updateActionCombatCamera(camera, yaw, target)
}
export function applyClaudeCraftCamera(camera: THREE.PerspectiveCamera, yaw: number, point: ActionWorldPoint) {
const eyeY = CLAUDECRAFT_CAMERA_EYE_HEIGHT
camera.position.x = point.x - Math.sin(yaw) * Math.cos(CLAUDECRAFT_CAMERA_PITCH) * CLAUDECRAFT_CAMERA_DISTANCE
camera.position.y = eyeY + Math.sin(CLAUDECRAFT_CAMERA_PITCH) * CLAUDECRAFT_CAMERA_DISTANCE
camera.position.z = point.z - Math.cos(yaw) * Math.cos(CLAUDECRAFT_CAMERA_PITCH) * CLAUDECRAFT_CAMERA_DISTANCE
if (camera.fov !== CLAUDECRAFT_CAMERA_FOV) {
camera.fov = CLAUDECRAFT_CAMERA_FOV
camera.updateProjectionMatrix()
}
camera.lookAt(point.x, eyeY, point.z)
}
export function toActionWorld(x: number, y: number, arena: ActionArenaProjection = { height: 540, width: 960 }) {
return {
x: (x - arena.width / 2) / ACTION_ARENA_SCALE,
z: (y - arena.height / 2) / ACTION_ARENA_SCALE,
}
}
export function getLineTelegraph(scene: THREE.Scene, map: Map<string, THREE.Mesh>, id: string) {
let mesh = map.get(id)
if (!mesh) {
mesh = new THREE.Mesh(
new THREE.BufferGeometry(),
new THREE.MeshBasicMaterial({ color: 0xff5d43, transparent: true, opacity: 0.28, side: THREE.DoubleSide }),
)
scene.add(mesh)
map.set(id, mesh)
}
return mesh
}
export function syncLineTelegraphGeometry(
mesh: THREE.Mesh,
startPoint: { x: number; y: number },
endPoint: { x: number; y: number },
width: number,
project: (x: number, y: number) => { x: number, z: number } = toActionWorld,
) {
const start = project(startPoint.x, startPoint.y)
const end = project(endPoint.x, endPoint.y)
const dx = end.x - start.x
const dz = end.z - start.z
const length = Math.max(0.001, Math.hypot(dx, dz))
const normalX = -dz / length
const normalZ = dx / length
const halfWidth = width / ACTION_ARENA_SCALE / 2
const y = 0.035
const positions = new Float32Array([
start.x + normalX * halfWidth, y, start.z + normalZ * halfWidth,
start.x - normalX * halfWidth, y, start.z - normalZ * halfWidth,
end.x + normalX * halfWidth, y, end.z + normalZ * halfWidth,
end.x - normalX * halfWidth, y, end.z - normalZ * halfWidth,
])
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.setIndex([0, 2, 1, 2, 3, 1])
geometry.computeVertexNormals()
mesh.geometry.dispose()
mesh.geometry = geometry
}
export function getCircleTelegraph(scene: THREE.Scene, map: Map<string, THREE.Mesh>, id: string) {
let mesh = map.get(id)
if (!mesh) {
mesh = new THREE.Mesh(
new THREE.CircleGeometry(1, 42),
new THREE.MeshBasicMaterial({ color: 0xffd36d, transparent: true, opacity: 0.24, side: THREE.DoubleSide }),
)
mesh.rotation.x = -Math.PI / 2
scene.add(mesh)
map.set(id, mesh)
}
return mesh
}
export function getArcTelegraph(scene: THREE.Scene, map: Map<string, THREE.Mesh>, id: string) {
let mesh = map.get(id)
if (!mesh) {
mesh = new THREE.Mesh(
new THREE.BufferGeometry(),
new THREE.MeshBasicMaterial({ color: 0xff8a43, transparent: true, opacity: 0.3, side: THREE.DoubleSide }),
)
scene.add(mesh)
map.set(id, mesh)
}
return mesh
}
export function syncArcTelegraphGeometry(
mesh: THREE.Mesh,
centerPoint: { x: number; y: number },
radius: number,
startAngle: number,
endAngle: number,
width: number,
project: (x: number, y: number) => { x: number, z: number } = toActionWorld,
) {
const center = project(centerPoint.x, centerPoint.y)
const innerRadius = Math.max(0, radius - width / 2) / ACTION_ARENA_SCALE
const outerRadius = Math.max(width, radius + width / 2) / ACTION_ARENA_SCALE
const segments = 32
const positions: number[] = []
const indices: number[] = []
const y = 0.045
for (let i = 0; i <= segments; i += 1) {
const t = i / segments
const angle = startAngle + (endAngle - startAngle) * t
positions.push(
center.x + Math.cos(angle) * innerRadius, y, center.z + Math.sin(angle) * innerRadius,
center.x + Math.cos(angle) * outerRadius, y, center.z + Math.sin(angle) * outerRadius,
)
if (i < segments) {
const base = i * 2
indices.push(base, base + 1, base + 2, base + 1, base + 3, base + 2)
}
}
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3))
geometry.setIndex(indices)
geometry.computeVertexNormals()
mesh.geometry.dispose()
mesh.geometry = geometry
}
export function createHealBurstEffect(options?: { simple?: boolean, color?: number }) {
const color = options?.color ?? 0x7dff9d
const group = new THREE.Group()
const ring = new THREE.Mesh(
new THREE.TorusGeometry(options?.simple ? 0.34 : 0.38, options?.simple ? 0.026 : 0.028, 8, options?.simple ? 32 : 36),
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.9 }),
)
ring.rotation.x = Math.PI / 2
group.add(ring)
if (options?.simple) return group
const pillar = new THREE.Mesh(
new THREE.CylinderGeometry(0.18, 0.34, 0.9, 18, 1, true),
new THREE.MeshBasicMaterial({ color: 0xb8ffd0, transparent: true, opacity: 0.34, side: THREE.DoubleSide }),
)
pillar.position.y = 0.45
group.add(pillar)
for (let i = 0; i < 5; i += 1) {
const mote = new THREE.Mesh(
new THREE.SphereGeometry(0.045, 8, 6),
new THREE.MeshBasicMaterial({ color: 0xf4fff2, transparent: true, opacity: 0.9 }),
)
const angle = (Math.PI * 2 * i) / 5
mote.position.set(Math.cos(angle) * 0.34, 0.25 + i * 0.12, Math.sin(angle) * 0.34)
group.add(mote)
}
return group
}
export function getOrCreateStunEffect(scene: THREE.Scene, effects: Map<string, THREE.Group>, id: string) {
let group = effects.get(id)
if (!group) {
group = new THREE.Group()
const starMaterial = new THREE.MeshBasicMaterial({ color: 0xffd36d, transparent: true, opacity: 0.95 })
for (let i = 0; i < 3; i += 1) {
const star = new THREE.Mesh(new THREE.OctahedronGeometry(0.09), starMaterial.clone())
const angle = (Math.PI * 2 * i) / 3
star.position.set(Math.cos(angle) * 0.28, Math.sin(i * 1.7) * 0.05, Math.sin(angle) * 0.28)
star.rotation.set(i * 0.7, i * 0.4, i * 0.9)
group.add(star)
}
const ring = new THREE.Mesh(
new THREE.TorusGeometry(0.28, 0.012, 6, 24),
new THREE.MeshBasicMaterial({ color: 0xfff0a8, transparent: true, opacity: 0.62 }),
)
ring.rotation.x = Math.PI / 2
group.add(ring)
scene.add(group)
effects.set(id, group)
}
return group
}
export function setObjectOpacity(group: THREE.Object3D, opacity: number) {
group.traverse((child) => {
if (!(child instanceof THREE.Mesh)) return
const materials = Array.isArray(child.material) ? child.material : [child.material]
for (const material of materials) {
material.transparent = true
material.opacity = opacity
}
})
}
export function pruneObjectMap<T extends THREE.Object3D>(map: Map<string, T>, active: Set<string>) {
for (const [id, object] of map) {
if (active.has(id)) continue
disposeObject3d(object)
map.delete(id)
}
}
export function disposeObject3d(object: THREE.Object3D) {
object.removeFromParent()
object.traverse((child) => {
if (!(child instanceof THREE.Mesh)) return
child.geometry.dispose()
const material = child.material
if (Array.isArray(material)) material.forEach((item) => item.dispose())
else material.dispose()
})
}