noise = { freq = 20, duration = 3.0, samples = {} } function Range(min, max) return {min = min, max = max} end function Color(r, g, b, a) return {r, g, b, a} end function clamp(value, min, max) return math.max(min, math.min(max, value)) end function remap(value, x1, y1, x2, y2) local l = x2 - x1 local a = (clamp(value, x1, x2) - x1)/l return y1 + (y2 - y1) * a end function getNoise(t) local s = t * noise.freq local s0 = math.floor(s) local a = s - s0; a = math.sin(a * math.pi * 0.5)^0.5 s0 = (s0 % #noise.samples) + 1 local s1 = (s0 % #noise.samples) + 1 return noise.samples[s0] + a * (noise.samples[s1] - noise.samples[s0]) end MAT_NONE = "-" POSITION = Vec(0.102, -0.108, -1.397) ROTATION = Vec(27.8, -7.54, 58.85) function init() --RegisterTool("jackhammer", GetTranslatedStringByKey("JACKHAMMER"), "MOD/vox/jackhammer.vox", 1) --SetBool("game.tool.jackhammer.enabled", true) --SetString("game.tool.jackhammer.ammo.display","") maxRange = 3.0 -- Also grab distance surfaceDistance = 1.0 motorPower = 0.0 time = 0.0 body = 0 indicatorShapes = {} chiselShape = 0 -- For chisel animation chiselShapeInitialTransform = Transform() vibrationFreq = 2.0 * math.pi * 5.0 -- Frequency for hit count and sounds animationVibrationFreq = 2.0 * math.pi * 10.0 -- Frequency for animation hits = 0 period = 0 currMaterial = MAT_NONE hardness = {} hardness.wanted = 0.0 hardness.actual = hardness.wanted distanceAnimation = {} distanceAnimation.wanted = 1.0 distanceAnimation.actual = distanceAnimation.wanted materialProperties = {} materialProperties["dirt"] = { sound = LoadSound("dirt/hit-m0.ogg"), strength = 2, hardness = 0.1 } materialProperties["foliage"] = { sound = LoadSound("foliage/hit-m0.ogg"), strength = 2, hardness = 0.1 } materialProperties["glass"] = { sound = LoadSound("glass/hit-m0.ogg"), strength = 1, hardness = 0.0 } materialProperties["hardmasonry"] = { sound = LoadSound("masonry/hit-m0.ogg"), strength = 14, hardness = 1.0 } -- will not break materialProperties["hardmetal"] = { sound = LoadSound("metal/hit-m0.ogg"), strength = 14, hardness = 1.0 } -- will not break materialProperties["heavymetal"] = { sound = LoadSound("metal/hit-m0.ogg"), strength = 14, hardness = 1.0 } -- will not break materialProperties["ice"] = { sound = LoadSound("ice/hit-m0.ogg"), strength = 1, hardness = 0.3 } materialProperties["masonry"] = { sound = LoadSound("masonry/hit-m0.ogg"), strength = 6, hardness = 0.8 } materialProperties["metal"] = { sound = LoadSound("metal/hit-m0.ogg"), strength = 14, hardness = 1.0 } materialProperties["plaster"] = { sound = LoadSound("masonry/hit-m0.ogg"), strength = 4, hardness = 0.5 } materialProperties["plastic"] = { sound = LoadSound("plastic/hit-m0.ogg"), strength = 4, hardness = 0.1 } materialProperties["rock"] = { sound = LoadSound("masonry/hit-m0.ogg"), strength = 14, hardness = 1.0 } materialProperties["wood"] = { sound = LoadSound("wood/hit-m0.ogg"), strength = 3, hardness = 0.2 } noContactSoundLoop = LoadLoop("MOD/sound/jackhammer-no-contact.ogg") contactSoundLoop = LoadLoop("MOD/sound/jackhammer-contact.ogg") hapticLoop = LoadHaptic("MOD/haptic/jackhammer_fire.xml") local toolHaptic = LoadHaptic("MOD/haptic/jackhammer_tool.xml") SetToolHaptic("jackhammer", toolHaptic); noise.freq = 20 noise.duration = 3.0 noise.samples = {} local numSamples = noise.freq * noise.duration for i = 1, numSamples do noise.samples[i] = math.random() * 2.0 - 1.0 end end function tick(dt) local cameraTransform = GetCameraTransform() local b = GetToolBody() if b ~= body then body = b local shapes = GetBodyShapes(body) chiselShape = shapes[2] chiselShapeInitialTransform = GetShapeLocalTransform(chiselShape) indicatorShapes[1] = shapes[3] indicatorShapes[2] = shapes[4] indicatorShapes[3] = shapes[5] end if GetString("game.player.tool") == "jackhammer" then if GetBool("game.player.canusetool") and InputDown("usetool") then motorPower = math.min(motorPower + dt * 2.0, 1.0) else local power = math.max(motorPower - dt * 2.0, 0.0) if motorPower ~= 0.0 and power == 0.0 then local periods = math.floor(time/animationVibrationFreq) time = time - (periods * animationVibrationFreq) local wt = time * vibrationFreq period = math.floor(wt) end motorPower = power end time = time + dt * motorPower local fwd = TransformToParentVec(cameraTransform,Vec(0,0,-1)) QueryRequire("visible") local hit, d, normal, shape = QueryRaycast(cameraTransform.pos, fwd, maxRange, 0.2) local hitPoint = VecAdd(cameraTransform.pos, VecScale(fwd, d)) hit, hitPoint, normal = GetShapeClosestPoint(shape, hitPoint) if hit then -- Get material properties at hit position local matType, matR, matG, matB, matA = GetShapeMaterialAtPosition(shape, hitPoint) if matType ~= currMaterial then if materialProperties[matType] == nil then currMaterial = "rock" else currMaterial = matType end end local properties = materialProperties[currMaterial] local wt = time * vibrationFreq local p = math.floor(wt) if period ~= p then hits = hits + 1 PlaySound(properties.sound, hitPoint, 0.5) local color = VecLerp(Vec(0.8, 0.85, 0.9), Vec(matR, matG, matB), 0.7) spawnDust(hitPoint, normal, color, Range(0.1, 0.2), Range(0.8, 1.2), 1.0, 5) spawnDust(GetBodyTransform(body).pos, normal, color, Range(0.1, 0.2), Range(0.8, 1.2), 1.0, 5) -- Make hole if hits > properties.strength then local count = MakeHole(hitPoint, 0.3 + math.random() * 0.2, 0.2 + math.random() * 0.2, 0.0) -- hardmetal will not break if count > 0 then color = VecLerp(Vec(0.8, 0.85, 0.9), Vec(matR, matG, matB), 0.7) spawnDust(hitPoint, normal, color, Range(0.1, 0.3), Range(0.8, 1.4), 1.5, 8) local incident = QuatRotateVec(GetBodyTransform(body).rot, Vec(0,0,-1)) spawnFragments(hitPoint, normal, incident, Color(matR, matG, matB, matA), 20) end if properties.hardness > 0.9 then spawnSparks(hitPoint, normal, 4) end hits = math.random(0,5) -- reset hits end period = p end surfaceDistance = d hardness.wanted = properties.hardness PlayLoop(contactSoundLoop, cameraTransform.pos, motorPower) if not HapticIsPlaying(hapticLoop) then PlayHaptic(hapticLoop, motorPower) end else hits = 0 surfaceDistance = -POSITION[3] hardness.wanted = 0.0 PlayLoop(noContactSoundLoop, cameraTransform.pos, motorPower) if not HapticIsPlaying(hapticLoop) then PlayHaptic(hapticLoop, motorPower * 0.5) end end hardness.actual = hardness.actual + (hardness.wanted - hardness.actual) * 0.3 distanceAnimation.wanted = surfaceDistance distanceAnimation.actual = distanceAnimation.actual + (distanceAnimation.wanted - distanceAnimation.actual) * 0.3; local vibrationAnimation = math.sin(time * animationVibrationFreq) * 0.015 local headbob = GetInt("options.input.headbob") / 100.0 -- Vibration a 1.0 full tool only a 0.0 chisel only local a = 0.8 if not hit then a = 0.2 end local chiselTransform = TransformCopy(chiselShapeInitialTransform) chiselTransform.pos[3] = 0.1 + chiselTransform.pos[3] + vibrationAnimation * (1.0 - a) SetShapeLocalTransform(chiselShape, chiselTransform) local rotationSpeed = 0.1 local rotationMagnitude = 5.0 * hardness.actual * motorPower local rotation = QuatEuler(ROTATION[1] + getNoise((time + 0.1) * rotationSpeed) * rotationMagnitude, ROTATION[2] + getNoise((time + 0.4) * rotationSpeed) * rotationMagnitude, ROTATION[3] + getNoise((time + 0.7) * rotationSpeed) * rotationMagnitude) local direction = QuatRotateVec(rotation, Vec(0.0, 0.0, -1.0)) local offsetSpeed = 4.0 local offsetMagnitude = 0.01 * hardness.actual * motorPower * (0.5 + headbob * 0.5) local offset = VecScale(direction, vibrationAnimation * a) offset = VecAdd(offset, Vec(getNoise(time * offsetSpeed) * offsetMagnitude, getNoise((time + 0.2) * offsetSpeed) * offsetMagnitude, getNoise((time + 0.4) * offsetSpeed) * offsetMagnitude)) local z = 0.0 if distanceAnimation.actual < -POSITION[3] then z = distanceAnimation.actual else z = -POSITION[3] + remap(distanceAnimation.actual, -POSITION[3], 0.0, maxRange, 0.25) end -- z and f used to prevent clipping local f = 1.0 - clamp(z, 0.0, 1.0) local position = VecAdd(Vec(POSITION[1], POSITION[2] - f*0.5, -z), offset) rotation = QuatRotateQuat(QuatEuler(f * 45.0, -f*45.0, 0.0), rotation) local t = Transform(position, rotation) SetToolTransform(t, 1.0 - motorPower) -- Lights for i = 1, 3, 1 do SetShapeEmissiveScale(indicatorShapes[i], 0.0) end if motorPower > 0.25 then SetShapeEmissiveScale(indicatorShapes[1], 1.0) end if motorPower > 0.5 then SetShapeEmissiveScale(indicatorShapes[2], 1.0) end if motorPower > 0.75 then SetShapeEmissiveScale(indicatorShapes[3], 1.0) end -- Camera shake local mag = (0.1 + hardness.actual) * motorPower * headbob local shakeRotation = QuatAxisAngle(Vec(1.0,0.0,0.0), getNoise(time * 3.0) * mag) shakeRotation = QuatRotateQuat(shakeRotation, QuatAxisAngle(Vec(0.0,1.0,0.0), getNoise((time+0.5) * 3.0) * mag * 0.2)) shakeRotation = QuatRotateQuat(shakeRotation, QuatAxisAngle(Vec(0.0,0.0,1.0), getNoise((time+1.0) * 3.0) * mag * 0.1)) SetPlayerCameraOffsetTransform(Transform(Vec(), shakeRotation)) -- Camera dof local dofAmount = hardness.actual * motorPower * 0.2 local dofDistance = surfaceDistance if GetInt("options.gfx.dof") ~= 0 then if hit and motorPower > 0.0 then dofAmount = dofAmount + 0.2 SetCameraDof(dofDistance, dofAmount) end else SetCameraDof(dofDistance, dofAmount) end else motorPower = 0.0 time = 0.0 distanceAnimation.wanted = -POSITION[3] distanceAnimation.actual = distanceAnimation.wanted end end function spawnDust(position, normal, color, radiusRange, lifeRange, speedFactor, numParticles) ParticleReset() ParticleColor(color[1], color[2], color[3]) ParticleRadius(radiusRange.min, radiusRange.max) ParticleCollide(0.0) ParticleAlpha(0.5, 0.0) for i = 1, numParticles, 1 do ParticleGravity(-2.0 + math.random() * 3.0) local dir = Vec(math.random() * 2.0 - 1.0, math.random() * 2.0 - 1.0, math.random() * 2.0 - 1.0) local offset = VecScale(dir, math.random() * 0.2) offset = VecAdd(offset, VecScale(normal, 0.05)) local pos = VecAdd(position, offset) local speed = math.random() speed = speed * speed local vel = VecScale(dir, speed * speedFactor) SpawnParticle(pos, vel, lifeRange.min + (lifeRange.max - lifeRange.min) * math.random()) end end function spawnFragments(position, normal, incident, color, numParticles) ParticleReset() ParticleColor(color[1], color[2], color[3]) ParticleAlpha(color[4]) ParticleTile(6) ParticleSticky(0.1) ParticleStretch(0.0) ParticleGravity(-10.0) ParticleCollide(1) for i = 1, numParticles, 1 do local r = math.random() r = r * r ParticleRadius(0.01 + r * 0.05, 0.001) local dir = Vec(math.random() * 2.0 - 1.0, math.random() * 2.0 - 1.0, math.random() * 2.0 - 1.0) local offset = VecScale(dir, math.random() * 0.2) offset = VecAdd(offset, VecScale(normal, 0.05)) local pos = VecAdd(position, offset) local speed = math.random() speed = speed * speed local reflect = VecSub(incident, VecScale(normal, 2.0 * VecDot(incident, normal))) dir = VecAdd(reflect, VecScale(dir, 0.5)) local vel = VecScale(dir, speed * 2.5) SpawnParticle(pos, vel, 1.0 + math.random()) end end function spawnSparks(position, normal, numParticles) ParticleReset() ParticleColor(1.0, 0.6, 0.4, 1.0, 0.3, 0.2) ParticleDrag(0.05) ParticleRadius(0.008, 0.0) ParticleEmissive(8.0, 0.0, "easeout") ParticleTile(4) ParticleGravity(-8.0) ParticleCollide(1) for i = 1, numParticles, 1 do local a = math.random() local life = 0.3 * a*a*a*4.0 local dir = Vec(math.random() * 2.0 - 1.0, math.random() * 2.0 - 1.0, math.random() * 2.0 - 1.0) local vel = VecAdd(normal, VecScale(dir, 0.5)) vel = VecScale(vel, a * 3.0) SpawnParticle(position, vel, life) end end