I Want To Heal 2 build v1.0.5 code
This commit is contained in:
@@ -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()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user