forked from ValkyrienSkies/Eureka
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathEurekaShipControl.kt
424 lines (342 loc) · 15.7 KB
/
EurekaShipControl.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
package org.valkyrienskies.eureka.ship
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import net.minecraft.core.Direction
import net.minecraft.network.chat.TranslatableComponent
import net.minecraft.world.entity.player.Player
import org.joml.*
import org.valkyrienskies.core.api.VSBeta
import org.valkyrienskies.core.api.ships.PhysShip
import org.valkyrienskies.core.api.ships.ServerShip
import org.valkyrienskies.core.api.ships.ServerTickListener
import org.valkyrienskies.core.api.ships.ShipForcesInducer
import org.valkyrienskies.core.api.ships.getAttachment
import org.valkyrienskies.core.api.ships.saveAttachment
import org.valkyrienskies.core.impl.game.ships.PhysShipImpl
import org.valkyrienskies.eureka.EurekaConfig
import org.valkyrienskies.mod.api.SeatedControllingPlayer
import org.valkyrienskies.mod.common.util.toJOMLD
import kotlin.math.*
@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
setterVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonIgnoreProperties(ignoreUnknown = true)
class EurekaShipControl : ShipForcesInducer, ServerTickListener {
@JsonIgnore
internal var ship: ServerShip? = null
private var extraForceLinear = 0.0
private var extraForceAngular = 0.0
var aligning = false
var disassembling = false // Disassembling also affects position
private var physConsumption = 0f
private val anchored get() = anchorsActive > 0
private var angleUntilAligned = 0.0
private var positionUntilAligned = Vector3d()
val canDisassemble
get() = ship != null &&
disassembling &&
abs(angleUntilAligned) < DISASSEMBLE_THRESHOLD &&
positionUntilAligned.distanceSquared(this.ship!!.transform.positionInWorld) < 4.0
var consumed = 0f
private set
private var wasCruisePressed = false
@JsonProperty("cruise")
var isCruising = false
private var controlData: ControlData? = null
@JsonIgnore
var seatedPlayer: Player? = null
@JsonIgnore
var oldSpeed = 0.0
private data class ControlData(
val seatInDirection: Direction,
var forwardImpulse: Float = 0.0f,
var leftImpulse: Float = 0.0f,
var upImpulse: Float = 0.0f,
var sprintOn: Boolean = false
) {
companion object {
fun create(player: SeatedControllingPlayer): ControlData {
return ControlData(
player.seatInDirection,
player.forwardImpulse,
player.leftImpulse,
player.upImpulse,
player.sprintOn
)
}
}
}
@OptIn(VSBeta::class)
override fun applyForces(physShip: PhysShip) {
if (helms < 1) {
// Enable fluid drag if all the helms have been destroyed
physShip.doFluidDrag = true
return
}
// Disable fluid drag when helms are present, because it makes ships hard to drive
physShip.doFluidDrag = EurekaConfig.SERVER.doFluidDrag
physShip as PhysShipImpl
val ship = ship ?: return
val mass = physShip.inertia.shipMass
val moiTensor = physShip.inertia.momentOfInertiaTensor
val omega: Vector3dc = physShip.poseVel.omega
val vel: Vector3dc = physShip.poseVel.vel
val balloonForceProvided = balloons * forcePerBalloon
val buoyantFactorPerFloater = min(
EurekaConfig.SERVER.floaterBuoyantFactorPerKg / 15.0 / mass,
EurekaConfig.SERVER.maxFloaterBuoyantFactor
)
physShip.buoyantFactor = 1.0 + floaters * buoyantFactorPerFloater
// Revisiting eureka control code.
// [x] Move torque stabilization code
// [x] Move linear stabilization code
// [x] Revisit player controlled torque
// [x] Revisit player controlled linear force
// [x] Anchor freezing
// [x] Rewrite Alignment code
// [x] Revisit Elevation code
// [x] Balloon limiter
// [x] Add Cruise code
// [x] Rotation based of ship size
// [x] Engine consumption
// [x] Fix elevation sensitivity
// region Aligning
val invRotation = physShip.poseVel.rot.invert(Quaterniond())
val invRotationAxisAngle = AxisAngle4d(invRotation)
// Floor makes a number 0 to 3, which corresponds to direction
val alignTarget = floor((invRotationAxisAngle.angle / (PI * 0.5)) + 4.5).toInt() % 4
angleUntilAligned = (alignTarget.toDouble() * (0.5 * PI)) - invRotationAxisAngle.angle
if (disassembling) {
val pos = ship.transform.positionInWorld
positionUntilAligned = pos.floor(Vector3d())
val direction = pos.sub(positionUntilAligned, Vector3d())
physShip.applyInvariantForce(direction)
}
if ((aligning) && abs(angleUntilAligned) > ALIGN_THRESHOLD) {
if (angleUntilAligned < 0.3 && angleUntilAligned > 0.0) angleUntilAligned = 0.3
if (angleUntilAligned > -0.3 && angleUntilAligned < 0.0) angleUntilAligned = -0.3
val idealOmega = Vector3d(invRotationAxisAngle.x, invRotationAxisAngle.y, invRotationAxisAngle.z)
.mul(-angleUntilAligned)
.mul(EurekaConfig.SERVER.stabilizationSpeed)
val idealTorque = moiTensor.transform(idealOmega)
physShip.applyInvariantTorque(idealTorque)
}
// endregion
val controllingPlayer = ship.getAttachment(SeatedControllingPlayer::class.java)
val validPlayer = controllingPlayer != null && !anchored
if (isCruising && anchored) {
isCruising = false
showCruiseStatus()
}
stabilize(
physShip,
omega,
vel,
physShip,
!validPlayer && !aligning,
!validPlayer
)
var idealUpwardVel = Vector3d(0.0, 0.0, 0.0)
if (validPlayer) {
val player = controllingPlayer!!
val currentControlData = getControlData(player)
if (!isCruising) {
// only take the latest control data if the player is not cruising
controlData = currentControlData
}
wasCruisePressed = player.cruise
} else {
if (!isCruising) {
// If the player isn't controlling the ship, and not cruising, reset the control data
controlData = null
oldSpeed = 0.0
}
}
controlData?.let { control ->
applyPlayerControl(control, physShip)
idealUpwardVel = getPlayerUpwardVel(control, mass)
}
// region Elevation
val idealUpwardForce = (idealUpwardVel.y() - vel.y() - (GRAVITY / EurekaConfig.SERVER.elevationSnappiness)) *
mass * EurekaConfig.SERVER.elevationSnappiness
physShip.applyInvariantForce(Vector3d(0.0,
min(balloonForceProvided, max(idealUpwardForce, 0.0)) +
// Add drag to the y-component
vel.y() * -mass,
0.0)
)
// endregion
physShip.isStatic = anchored
}
private fun getControlData(player: SeatedControllingPlayer): ControlData {
val currentControlData = ControlData.create(player)
if (!wasCruisePressed && player.cruise) {
// the player pressed the cruise button
isCruising = !isCruising
showCruiseStatus()
} else if (!player.cruise && isCruising &&
(player.leftImpulse != 0.0f || player.sprintOn || player.upImpulse != 0.0f || player.forwardImpulse != 0.0f) &&
currentControlData != controlData
) {
// The player pressed another button
isCruising = false
showCruiseStatus()
}
return currentControlData
}
private fun applyPlayerControl(control: ControlData, physShip: PhysShipImpl) {
val ship = ship ?: return
val transform = physShip.transform
val aabb = ship.worldAABB
val center = transform.positionInWorld
// region Player controlled rotation
val moiTensor = physShip.inertia.momentOfInertiaTensor
val omega: Vector3dc = physShip.poseVel.omega
val largestDistance = run {
var dist = center.distance(aabb.minX(), center.y(), aabb.minZ())
dist = max(dist, center.distance(aabb.minX(), center.y(), aabb.maxZ()))
dist = max(dist, center.distance(aabb.maxX(), center.y(), aabb.minZ()))
dist = max(dist, center.distance(aabb.maxX(), center.y(), aabb.maxZ()))
dist
}.coerceIn(0.5, EurekaConfig.SERVER.maxSizeForTurnSpeedPenalty)
val maxLinearAcceleration = EurekaConfig.SERVER.turnAcceleration
val maxLinearSpeed = EurekaConfig.SERVER.turnSpeed + extraForceAngular
// acceleration = alpha * r
// therefore: maxAlpha = maxAcceleration / r
val maxOmegaY = maxLinearSpeed / largestDistance
val maxAlphaY = maxLinearAcceleration / largestDistance
val isBelowMaxTurnSpeed = abs(omega.y()) < maxOmegaY
val normalizedAlphaYMultiplier =
if (isBelowMaxTurnSpeed && control.leftImpulse != 0.0f) control.leftImpulse.toDouble()
else -omega.y().coerceIn(-1.0, 1.0)
val idealAlphaY = normalizedAlphaYMultiplier * maxAlphaY
physShip.applyInvariantTorque(moiTensor.transform(Vector3d(0.0, idealAlphaY, 0.0)))
// endregion
physShip.applyInvariantTorque(getPlayerControlledBanking(control, physShip, moiTensor, -idealAlphaY))
physShip.applyInvariantForce(getPlayerForwardVel(control, physShip))
}
private fun getPlayerControlledBanking(control: ControlData, physShip: PhysShipImpl, moiTensor: Matrix3dc, strength: Double): Vector3d {
val rotationVector = control.seatInDirection.normal.toJOMLD()
physShip.poseVel.transformDirection(rotationVector)
rotationVector.y = 0.0
rotationVector.mul(strength * 1.5)
physShip.poseVel.rot.transform(
moiTensor.transform(
physShip.poseVel.rot.transformInverse(rotationVector)
)
)
return rotationVector
}
// Player controlled forward and backward thrust
private fun getPlayerForwardVel(control: ControlData, physShip: PhysShipImpl): Vector3d {
val scaledMass = physShip.inertia.shipMass * EurekaConfig.SERVER.speedMassScale
val vel: Vector3dc = physShip.poseVel.vel
// region Player controlled forward and backward thrust
val forwardVector = control.seatInDirection.normal.toJOMLD()
physShip.poseVel.rot.transform(forwardVector)
forwardVector.normalize()
val s = 1 / smoothingATanMax(
EurekaConfig.SERVER.linearMaxMass,
physShip.inertia.shipMass * EurekaConfig.SERVER.linearMassScaling + EurekaConfig.SERVER.linearBaseMass
)
val maxSpeed = EurekaConfig.SERVER.linearMaxSpeed / 15
oldSpeed = max(min(oldSpeed * (1 - s) + control.forwardImpulse.toDouble() * s, maxSpeed), -maxSpeed)
forwardVector.mul(oldSpeed)
val playerUpDirection = physShip.poseVel.transformDirection(Vector3d(0.0, 1.0, 0.0))
val velOrthogonalToPlayerUp = vel.sub(playerUpDirection.mul(playerUpDirection.dot(vel)), Vector3d())
// This is the speed that the ship is always allowed to go out, without engines
val baseForwardVel = Vector3d(forwardVector).mul(EurekaConfig.SERVER.baseSpeed)
val forwardForce = Vector3d(baseForwardVel).sub(velOrthogonalToPlayerUp).mul(scaledMass)
if (extraForceLinear != 0.0) {
// engine boost
val boost = max((extraForceLinear - EurekaConfig.SERVER.enginePowerLinear * EurekaConfig.SERVER.engineBoostOffset) * EurekaConfig.SERVER.engineBoost, 0.0);
extraForceLinear += boost + boost * boost * EurekaConfig.SERVER.engineBoostExponentialPower;
// This is the maximum speed we want to go in any scenario (when not sprinting)
val idealForwardVel = Vector3d(forwardVector).mul(EurekaConfig.SERVER.maxCasualSpeed)
val idealForwardForce = Vector3d(idealForwardVel).sub(velOrthogonalToPlayerUp).mul(scaledMass)
val extraForceNeeded = Vector3d(idealForwardForce).sub(forwardForce)
forwardForce.fma(min(extraForceLinear / extraForceNeeded.length(), 1.0), extraForceNeeded)
}
return forwardForce
}
// Player controlled elevation
private fun getPlayerUpwardVel(control: ControlData, mass: Double): Vector3d {
if (control.upImpulse != 0.0f) {
val balloonForceProvided = balloons * forcePerBalloon
return Vector3d(0.0, 1.0, 0.0)
.mul(control.upImpulse.toDouble())
.mul(
if (control.upImpulse < 0.0f) {
EurekaConfig.SERVER.baseImpulseDescendRate
}
else {
EurekaConfig.SERVER.baseImpulseElevationRate +
// Smoothing for how the elevation scales as you approaches the balloonElevationMaxSpeed
smoothing(2.0, EurekaConfig.SERVER.balloonElevationMaxSpeed, balloonForceProvided / mass)
}
)
}
return Vector3d(0.0, 0.0, 0.0)
}
private fun showCruiseStatus() {
val cruiseKey = if (isCruising) "hud.vs_eureka.start_cruising" else "hud.vs_eureka.stop_cruising"
seatedPlayer?.displayClientMessage(TranslatableComponent(cruiseKey), true)
}
var powerLinear = 0.0
var powerAngular = 0.0
var anchors = 0 // Amount of anchors
set(v) {
field = v; deleteIfEmpty()
}
var anchorsActive = 0 // Anchors that are active
var balloons = 0 // Amount of balloons
set(v) {
field = v; deleteIfEmpty()
}
var helms = 0 // Amount of helms
set(v) {
field = v; deleteIfEmpty()
}
var floaters = 0 // Amount of floaters * 15
set(v) {
field = v; deleteIfEmpty()
}
private fun deleteIfEmpty() {
if (helms <= 0 && floaters <= 0 && anchors <= 0 && balloons <= 0) {
ship?.saveAttachment<EurekaShipControl>(null)
}
}
/**
* f(x) = max - smoothing / (x + (smoothing / max))
*/
private fun smoothing(smoothing: Double, max: Double, x: Double): Double = max - smoothing / (x + (smoothing / max))
/**
* g(x) = (tan^(-1)(x * smoothing)) / smoothing
*/
private fun smoothingATan(smoothing: Double, x: Double): Double = atan(x * smoothing) / smoothing
// limit x to max using ATan
private fun smoothingATanMax(max: Double, x: Double): Double = smoothingATan(1 / (max * 0.638), x)
companion object {
fun getOrCreate(ship: ServerShip): EurekaShipControl {
return ship.getAttachment<EurekaShipControl>()
?: EurekaShipControl().also { ship.saveAttachment(it) }
}
private const val ALIGN_THRESHOLD = 0.01
private const val DISASSEMBLE_THRESHOLD = 0.02
private val forcePerBalloon get() = EurekaConfig.SERVER.massPerBalloon * -GRAVITY
private const val GRAVITY = -10.0
}
override fun onServerTick() {
extraForceLinear = powerLinear
powerLinear = 0.0
extraForceAngular = powerAngular
powerAngular = 0.0
consumed = physConsumption * /* should be physics ticks based*/ 0.1f
physConsumption = 0.0f
}
}