-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
rangedShooting.d
765 lines (645 loc) · 31.6 KB
/
rangedShooting.d
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
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
/*
* Free aiming mechanics for ranged combat shooting
*
* Gothic Free Aim (GFA) v1.0.0-beta.18 - Free aiming for the video games Gothic 1 and Gothic 2 by Piranha Bytes
* Copyright (C) 2016-2017 mud-freak (@szapp)
*
* This file is part of Gothic Free Aim.
* <http://github.com/szapp/GothicFreeAim>
*
* Gothic Free Aim is free software: you can redistribute it and/or
* modify it under the terms of the MIT License.
* On redistribution this notice must remain intact and all copies must
* identify the original author.
*
* Gothic Free Aim is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* MIT License for more details.
*
* You should have received a copy of the MIT License along with
* Gothic Free Aim. If not, see <http://opensource.org/licenses/MIT>.
*/
/*
* Wrapper function for the config function GFA_GetDrawForce(). It is called from GFA_SetupProjectile().
* This function is necessary for error handling and to supply the readied weapon and respective talent value.
*/
func int GFA_GetDrawForce_() {
// Get readied/equipped ranged weapon
var int talent; var int weaponPtr;
if (!GFA_GetWeaponAndTalent(hero, _@(weaponPtr), _@(talent))) {
// On error return 50% draw force
return 50;
};
var C_Item weapon; weapon = _^(weaponPtr);
// Retrieve draw force value from config
var int drawForce; drawForce = GFA_GetDrawForce(weapon, talent);
// Must be a percentage in range of [0, 100]
if (drawForce > 100) {
drawForce = 100;
} else if (drawForce < 0) {
drawForce = 0;
};
return drawForce;
};
/*
* Wrapper function for the config function GFA_GetAccuracy(). It is called from GFA_SetupProjectile().
* This function is necessary for error handling and to supply the readied weapon and respective talent value.
*/
func int GFA_GetAccuracy_() {
// Get readied/equipped ranged weapon
var int talent; var int weaponPtr;
if (!GFA_GetWeaponAndTalent(hero, _@(weaponPtr), _@(talent))) {
// On error return 50% accuracy
return 50;
};
var C_Item weapon; weapon = _^(weaponPtr);
// Retrieve accuracy value from config
var int accuracy; accuracy = GFA_GetAccuracy(weapon, talent);
// Must be a percentage in range of [1, 100], division by 0!
if (accuracy > 100) {
accuracy = 100;
} else if (accuracy < 1) {
// Prevent devision by zero later
accuracy = 1;
};
return accuracy;
};
/*
* Wrapper function for the config function GFA_GetInitialBaseDamage(). It is called from GFA_SetupProjectile().
* This function is necessary for error handling and to supply the readied weapon and respective talent value.
*/
func int GFA_GetInitialBaseDamage_(var int baseDamage, var int damageType, var int aimingDistance) {
// Get readied/equipped ranged weapon
var int talent; var int weaponPtr;
if (!GFA_GetWeaponAndTalent(hero, _@(weaponPtr), _@(talent))) {
// On error return the base damage unaltered
return baseDamage;
};
var C_Item weapon; weapon = _^(weaponPtr);
// Scale distance between [0, 100] for [RANGED_CHANCE_MINDIST, RANGED_CHANCE_MAXDIST], see AI_Constants.d
// For readability: 100*(aimingDistance-RANGED_CHANCE_MINDIST)/(RANGED_CHANCE_MAXDIST-RANGED_CHANCE_MINDIST)
aimingDistance = roundf(divf(mulf(FLOAT1C, subf(aimingDistance, castToIntf(RANGED_CHANCE_MINDIST))),
subf(castToIntf(RANGED_CHANCE_MAXDIST), castToIntf(RANGED_CHANCE_MINDIST))));
// Clip to range [0, 100]
if (aimingDistance > 100) {
aimingDistance = 100;
} else if (aimingDistance < 0) {
aimingDistance = 0;
};
// Retrieve adjusted damage value from config
baseDamage = GFA_GetInitialBaseDamage(baseDamage, damageType, weapon, talent, aimingDistance);
// No negative damage
if (baseDamage < 0) {
baseDamage = 0;
};
return baseDamage;
};
/*
* Wrapper function for the config function GFA_GetRecoil(). It is called from GFA_SetupProjectile().
* This function is necessary for error handling and to supply the readied weapon and respective talent value.
*/
func int GFA_GetRecoil_() {
// Get readied/equipped ranged weapon
var int talent; var int weaponPtr;
if (!GFA_GetWeaponAndTalent(hero, _@(weaponPtr), _@(talent))) {
// On error return 0% recoil
return 0;
};
var C_Item weapon; weapon = _^(weaponPtr);
// Retrieve recoil value from config
var int recoil; recoil = GFA_GetRecoil(weapon, talent);
// Must be a percentage in range of [0, 100]
if (recoil > 100) {
recoil = 100;
} else if (recoil < 0) {
recoil = 0;
};
return recoil;
};
/*
* Set the projectile direction. This function hooks oCAIArrow::SetupAIVob() to overwrite the target vob with the aim
* vob that is placed in front of the camera at the nearest intersection with the world or an object.
* Setting up the projectile involves several parts:
* 1st: Set base damage of projectile: GFA_GetInitialBaseDamage()
* 2nd: Manipulate aiming accuracy (scatter): GFA_GetAccuracy()
* 3rd: Add recoil to mouse movement: GFA_GetRecoil()
* 4th: Set projectile drop-off (by draw force): GFA_GetDrawForce()
* 5th: Add trail strip FX for better visibility
* 6th: Setup the aim vob and overwrite the target
*/
func void GFA_SetupProjectile() {
// Only if shooter is the player and if free aiming is enabled
var C_Npc shooter; shooter = _^(MEM_ReadInt(ESP+8)); // Second function argument is the shooter
if (!GFA_ACTIVE) || (!Npc_IsPlayer(shooter)) {
return;
};
var int projectilePtr; projectilePtr = MEM_ReadInt(ESP+4); // First function argument is the projectile
if (!projectilePtr) {
return;
};
if (!Hlp_Is_oCItem(projectilePtr)) {
return;
};
var oCItem projectile; projectile = _^(projectilePtr);
// Before anything: Create target position vector for shot by taking the nearest ray intersection with world/objects
var int pos[3]; // Position of the target shot
var int distance; // Distance to camera (used for calculating position of target shot in local space)
var int distPlayer; // Distance to player (used for debugging output in zSpy)
GFA_AimRay(GFA_MAX_DIST, TARGET_TYPE_NPCS, 0, _@(pos), _@(distPlayer), _@(distance));
// When the target is too close, shots go vertically up, because the reticle is targeted. To solve this problem,
// restrict the minimum distance
var int focusDist;
var oCNpc her; her = Hlp_GetNpc(hero);
if (Hlp_Is_oCNpc(her.focus_vob)) {
var C_Npc focusNpc; focusNpc = _^(her.focus_vob);
focusDist = Npc_GetDistToPlayer(focusNpc);
} else {
focusDist = GFA_MAX_DIST;
};
if (lf(distPlayer, mkf(GFA_MIN_AIM_DIST))) || (focusDist < GFA_MIN_AIM_DIST) {
distance = addf(distance, mkf(GFA_MIN_AIM_DIST));
};
// Remove projectile from any saves that might be written while the projectile is mid-air
projectile._zCVob_bitfield[4] = projectile._zCVob_bitfield[4] | zCVob_bitfield4_dontWriteIntoArchive;
// 1st: Modify the base damage of the projectile
// This allows for dynamical adjustment of damage (e.g. based on draw force).
var int baseDamage;
var int newBaseDamage;
// Do this for one damage type only. It gets too complicated for multiple damage types
var int iterator; iterator = projectile.damageType;
var int damageIndex; damageIndex = 0;
// Find damage index from bit field
while((iterator > 0) && ((iterator & 1) != 1)); // Check lower bit
damageIndex += 1;
// Cut off lower bit
iterator = iterator >> 1;
end;
if (iterator > 1) || (damageIndex == DAM_INDEX_MAX) {
if (GFA_DEBUG_PRINT) {
MEM_Info("GFA_SetupProjectile (initial damage): Ignoring projectile due to multiple/invalid damage types.");
};
// Keep damage as is (the class variable damageTotal might be zero)
baseDamage = projectile.damageTotal;
newBaseDamage = baseDamage;
} else {
// Retrieve and update damage
baseDamage = MEM_ReadStatArr(_@(projectile.damage), damageIndex);
newBaseDamage = GFA_GetInitialBaseDamage_(baseDamage, damageIndex, distPlayer);
// Apply new damage to projectile
projectile.damageTotal = newBaseDamage;
MEM_WriteStatArr(_@(projectile.damage), damageIndex, newBaseDamage);
};
// 2nd: Manipulate aiming accuracy (scatter)
// The scattering is optional: If disabled, the default hit chance from Gothic is used, where shots are always
// accurate, but register damage in a fraction of shots only, depending on skill and distance
if (GFA_TRUE_HITCHANCE) {
// The accuracy is first used as a probability to decide whether a projectile should hit or not. Depending on
// this, the minimum (rmin) and maximum (rmax) scattering angles (half the visual angle) are designed by which
// the shot is deviated.
// Not-a-hit results in rmin=GFA_SCATTER_MISS and rmax=GFA_SCATTER_MAX.
// A positive hit results in rmin=0 and rmax=GFA_SCATTER_HIT*(-accuracy+100).
var int rmin;
var int rmax;
// Retrieve accuracy percentage
var int accuracy; accuracy = GFA_GetAccuracy_(); // Change the accuracy in that function, not here!
// Determine whether it is considered accurate enough for a positive hit
if (r_MinMax(0, 99) < accuracy) {
// The projectile will land inside the hit radius scaled by the accuracy
rmin = FLOATNULL;
// The circle area from the radius scales better with accuracy
var int hitRadius; hitRadius = castToIntf(GFA_SCATTER_HIT);
var int hitArea; hitArea = mulf(PI, sqrf(hitRadius)); // Area of circle from radius
// Scale the maximum area with minimum acurracy
// (hitArea - 1) * (accuracy - 100)
// -------------------------------- + 1
// -100
var int maxArea;
maxArea = addf(divf(mulf(subf(hitArea, FLOATONE), mkf(accuracy-100)), negf(FLOAT1C)), FLOATONE);
// Convert back to a radius
rmax = sqrtf(divf(maxArea, PI));
if (rmax > hitRadius) {
rmax = hitRadius;
};
} else {
// The projectile will land outside of the hit radius
rmin = castToIntf(GFA_SCATTER_MISS);
rmax = castToIntf(GFA_SCATTER_MAX);
};
// r_MinMax works with integers: scale up
var int rmaxI; rmaxI = roundf(mulf(rmax, FLOAT1K));
// Azimiuth scatter (horizontal deviation from a perfect shot in degrees)
var int angleX; angleX = fracf(r_MinMax(FLOATNULL, rmaxI), 1000); // Here the 1000 are scaled down again
// For a circular scattering pattern the range of possible values (rmin and rmax) for angleY is decreased:
// r^2 - x^2 = y^2 => y = sqrt(r^2 - x^2), where r is the radius to stay within the maximum radius
// Adjust rmin
if (lf(angleX, rmin)) {
rmin = sqrtf(subf(sqrf(rmin), sqrf(angleX)));
} else {
rmin = FLOATNULL;
};
// r_MinMax works with integers: scale up
var int rminI; rminI = roundf(mulf(rmin, FLOAT1K));
// Adjust rmax
if (lf(angleX, rmax)) {
rmax = sqrtf(subf(sqrf(rmax), sqrf(angleX)));
} else {
rmax = FLOATNULL;
};
// r_MinMax works with integers: scale up
rmaxI = roundf(mulf(rmax, FLOAT1K));
// Elevation scatter (vertical deviation from a perfect shot in degrees)
var int angleY; angleY = fracf(r_MinMax(rminI, rmaxI), 1000); // Here the 1000 are scaled down again
// Randomize the sign of scatter
if (r_Max(1)) { // 0 or 1, approx. 50-50 chance
angleX = negf(angleX);
};
if (r_Max(1)) {
angleY = negf(angleY);
};
// Create vector in local space from distance. The angles calculated above will be applied to this vector
var int localPos[3];
localPos[0] = FLOATNULL;
localPos[1] = FLOATNULL;
localPos[2] = distance; // Distance into outVec (facing direction)
// Rotate around x-axis by angleX (elevation scatter). Rotation equations are simplified, because x and y are 0
SinCosApprox(Print_ToRadian(angleX));
localPos[1] = mulf(negf(localPos[2]), sinApprox); // y*cos - z*sin = y'
localPos[2] = mulf(localPos[2], cosApprox); // y*sin + z*cos = z'
// Rotate around y-axis by angleY (azimuth scatter)
SinCosApprox(Print_ToRadian(angleY));
localPos[0] = mulf(localPos[2], sinApprox); // x*cos + z*sin = x'
localPos[2] = mulf(localPos[2], cosApprox); // -x*sin + z*cos = z'
// Get camera vob
var zCVob camVob; camVob = _^(MEM_Game._zCSession_camVob);
var zMAT4 camPos; camPos = _^(_@(camVob.trafoObjToWorld[0]));
// Translation into local coordinate system of camera (rotation): rightVec*x + upVec*y + outVec*z
// rightVec*x
pos[0] = mulf(camPos.v0[zMAT4_rightVec], localPos[0]);
pos[1] = mulf(camPos.v1[zMAT4_rightVec], localPos[0]);
pos[2] = mulf(camPos.v2[zMAT4_rightVec], localPos[0]);
// rightVec*x + upVec*y
pos[0] = addf(pos[0], mulf(camPos.v0[zMAT4_upVec], localPos[1]));
pos[1] = addf(pos[1], mulf(camPos.v1[zMAT4_upVec], localPos[1]));
pos[2] = addf(pos[2], mulf(camPos.v2[zMAT4_upVec], localPos[1]));
// rightVec*x + upVec*y + outVec*z
pos[0] = addf(pos[0], mulf(camPos.v0[zMAT4_outVec], localPos[2]));
pos[1] = addf(pos[1], mulf(camPos.v1[zMAT4_outVec], localPos[2]));
pos[2] = addf(pos[2], mulf(camPos.v2[zMAT4_outVec], localPos[2]));
// Add the translated coordinates to the camera position (final target position expressed in world coordinates)
pos[0] = addf(camPos.v0[zMAT4_position], pos[0]);
pos[1] = addf(camPos.v1[zMAT4_position], pos[1]);
pos[2] = addf(camPos.v2[zMAT4_position], pos[2]);
};
// 3rd: Add recoil
// Add recoil to camera angle
var int recoil; recoil = GFA_GetRecoil_(); // Modify the recoil in that function, not here!
if (recoil) {
var int recoilAngle; recoilAngle = fracf(GFA_MAX_RECOIL*recoil, 100);
// Get game camera
var int camAI; camAI = MEM_ReadInt(zCAICamera__current);
// Vertical recoil: Classical upwards movement of the camera scaled by GFA_Recoil
var int camYAngle; camYAngle = MEM_ReadInt(camAI+zCAICamera_elevation_offset);
MEM_WriteInt(camAI+zCAICamera_elevation_offset, subf(camYAngle, recoilAngle));
};
// 4th: Set projectile drop-off (by draw force)
// The curved trajectory of the projectile is achieved by setting a fixed gravity, but applying it only after a
// certain air time. This air time is adjustable and depends on draw force: GFA_GetDrawForce().
// First get the rigid body of the projectile which is responsible for gravity. The rigid body object does not exist
// yet at this point, so it has to be retrieved/created by calling this function:
const int call = 0;
if (CALL_Begin(call)) {
CALL__thiscall(_@(projectilePtr), zCVob__GetRigidBody);
call = CALL_End();
};
var int rBody; rBody = CALL_RetValAsInt(); // zCRigidBody*
// Retrieve draw force percentage from which to calculate the drop time (time at which the gravity is applied)
var int drawForce; drawForce = GFA_GetDrawForce_(); // Modify the draw force in that function, not here!
// The gravity is a fixed value. An exception are very short draw times. There, the gravity is higher
var int gravityMod;
if (drawForce < 25) {
// Draw force below 25% (very short draw time) increases gravity
gravityMod = castToIntf(3.0);
} else {
gravityMod = FLOATONE;
};
// Calculate the air time at which to apply the gravity, by the maximum air time GFA_TRAJECTORY_ARC_MAX. Because
// drawForce is a percentage, GFA_TRAJECTORY_ARC_MAX is first multiplied by 100 and later divided by 10000
var int dropTime; dropTime = (drawForce*(GFA_TRAJECTORY_ARC_MAX*100))/10000;
// Create a timed frame function to apply the gravity to the projectile after the calculated air time
FF_ApplyOnceExtData(GFA_EnableProjectileGravity, dropTime, 1, rBody);
// Set the gravity to the projectile. Again: The gravity does not take effect until it is activated
MEM_WriteInt(rBody+zCRigidBody_gravity_offset, mulf(castToIntf(GFA_PROJECTILE_GRAVITY), gravityMod));
// Reset draw timer
GFA_BowDrawOnset = MEM_Timer.totalTime + GFA_DRAWTIME_RELOAD;
// 5th: Add trail strip FX for better visibility
// The horizontal position of the camera is aligned with the arrow trajectory, to counter the parallax effect and to
// allow reasonable aiming. Unfortunately, when the projectile flies along the out vector of the camera (exactly
// away from the camera), it is barely to not at all visible. To aid visibility, an additional trail strip FX is
// applied. This is only necessary when the projectile does not have an FX anyway (e.g. magic arrows). The trail
// strip FX will be removed later once the projectile stops moving.
if (GOTHIC_BASE_VERSION == 2) {
// Gothic 1 does not offer effects on items
if (Hlp_StrCmp(MEM_ReadString(projectilePtr+oCItem_effect_offset), "")) { // Projectile has no FX
MEM_WriteString(projectilePtr+oCItem_effect_offset, GFA_TRAIL_FX);
const int call2 = 0;
if (CALL_Begin(call2)) {
CALL__thiscall(_@(projectilePtr), oCItem__InsertEffect);
call2 = CALL_End();
};
};
} else {
// Simplified mechanics for Gothic 1
Wld_PlayEffect(GFA_TRAIL_FX_SIMPLE, projectile, projectile, 0, 0, 0, FALSE);
};
// 6th: Reposition the aim vob and overwrite the target vob
var int vobPtr; vobPtr = GFA_SetupAimVob(_@(pos));
MEM_WriteInt(ESP+12, vobPtr); // Overwrite the third argument (target vob) passed to oCAIArrow::SetupAIVob()
// Update the shooting statistics
GFA_StatsShots += 1;
if (GFA_DEBUG_PRINT) {
MEM_Info("GFA_SetupProjectile:");
var int s; s = SB_New();
SB(" aiming distance: ");
SB(STR_Prefix(toStringf(divf(distPlayer, FLOAT1C)), 4));
SB("m");
MEM_Info(SB_ToString());
SB_Clear();
SB(" draw force: ");
SBi(drawForce);
SB("%");
MEM_Info(SB_ToString());
SB_Clear();
if (GFA_TRUE_HITCHANCE) {
SB(" accuracy: ");
SBi(accuracy);
SB("%");
MEM_Info(SB_ToString());
SB_Clear();
SB(" scatter: (");
SB(STR_Prefix(toStringf(angleX), 5));
SBc(176 /* deg */);
SB(", ");
SB(STR_Prefix(toStringf(angleY), 5));
SBc(176 /* deg */);
SB(") visual angles");
MEM_Info(SB_ToString());
SB_Clear();
} else {
var int hitchance;
if (GOTHIC_BASE_VERSION == 1) {
// In Gothic 1, the hit chance is determined by dexterity (for both bows and crossbows)
hitchance = hero.attribute[ATR_DEXTERITY];
} else {
// In Gothic 2, the hit chance is the learned skill value (talent)
GFA_GetWeaponAndTalent(hero, 0, _@(hitchance));
};
SB(" hit chance: ");
SBi(hitchance);
SB("% (standard hit chance, scattering disabled)");
MEM_Info(SB_ToString());
SB_Clear();
};
SB(" recoil: ");
SBi(recoil);
SB("%");
MEM_Info(SB_ToString());
SB_Clear();
SB(" base damage: ");
SBi(newBaseDamage);
SB(" (of ");
SBi(baseDamage);
SB(" normal base damage)");
MEM_Info(SB_ToString());
SB_Destroy();
};
};
/*
* This is a frame function timed by draw force and is responsible for applying gravity to a projectile after a certain
* air time as determined in GFA_SetupProjectile(). The gravity is merely turned on, the gravity strength itself is set
* in GFA_SetupProjectile().
*/
func void GFA_EnableProjectileGravity(var int rigidBody) {
if (!rigidBody) {
return;
};
// Check validity of the zCRigidBody pointer by its first class variable (value is always 10.0). This is necessary
// for loading a saved game, as the pointer will not point to a zCRigidBody address anymore.
if (roundf(MEM_ReadInt(rigidBody+zCRigidBody_mass_offset)) != 10) {
return;
};
// Do not add gravity if projectile already stopped moving
if (MEM_ReadInt(rigidBody+zCRigidBody_velocity_offset) == FLOATNULL) // zCRigidBody.velocity[3]
&& (MEM_ReadInt(rigidBody+zCRigidBody_velocity_offset+4) == FLOATNULL)
&& (MEM_ReadInt(rigidBody+zCRigidBody_velocity_offset+8) == FLOATNULL) {
return;
};
// Turn on gravity
var int bitfield; bitfield = MEM_ReadByte(rigidBody+zCRigidBody_bitfield_offset);
MEM_WriteByte(rigidBody+zCRigidBody_bitfield_offset, bitfield | zCRigidBody_bitfield_gravityActive);
};
/*
* This function resets the gravity back to its default value, after any collision occurred. The function hooks
* oCAIArrow::ReportCollisionToAI() at an offset where a valid collision was detected.
* It is important to reset the gravity, because the projectile may bounce off of walls (etc.), after which it would
* float around with the previously set drop-off gravity (GFA_PROJECTILE_GRAVITY).
*/
func void GFA_ResetProjectileGravity() {
var int arrowAI; arrowAI = MEMINT_SwitchG1G2(ESI, ECX);
var oCItem projectile; projectile = _^(MEM_ReadInt(arrowAI+oCAIArrowBase_hostVob_offset));
if (!projectile._zCVob_rigidBody) {
return;
};
var int rigidBody; rigidBody = projectile._zCVob_rigidBody;
// Better safe than writing to an invalid address
if (FF_ActiveData(GFA_EnableProjectileGravity, rigidBody)) {
FF_RemoveData(GFA_EnableProjectileGravity, rigidBody);
};
// Reset projectile gravity (zCRigidBody.gravity) after collision (oCAIArrow.collision) to default
MEM_WriteInt(rigidBody+zCRigidBody_gravity_offset, FLOATONE);
// Remove trail strip FX
if (GOTHIC_BASE_VERSION == 1) {
Wld_StopEffect_Ext(GFA_TRAIL_FX_SIMPLE, projectile, projectile, 0);
};
};
/*
* Manipulate the hit chance when shooting NPCs. This function hooks oCAIArrow::ReportCollisionToAI() at the offset
* where the hit chance of the NPC is checked.
* Depending on GFA_TRUE_HITCHANCE, the resulting hit chance is either the Gothic default hit chance or always 100%.
* For the latter (GFA_TRUE_HITCHANCE == true) the hit chance is instead determined earlier by scattering in
* GFA_SetupProjectile().
* Additionally, the shooting statistics are updated.
* This function is only making changes if the shooter is the player.
*/
func void GFA_OverwriteHitChance() {
var int arrowAI; arrowAI = MEMINT_SwitchG1G2(ESI, EBP);
var C_Npc shooter; shooter = _^(MEM_ReadInt(arrowAI+oCAIArrow_origin_offset));
// Only if shooter is the player and if free aiming is enabled for ranged combat
if (!GFA_ACTIVE) || (!Npc_IsPlayer(shooter)) {
return;
};
var oCItem projectile; projectile = _^(MEM_ReadInt(arrowAI+oCAIArrowBase_hostVob_offset));
// Hit chance, calculated from skill (or dexterity in Gothic 1) and distance
var int hitChancePtr; hitChancePtr = MEMINT_SwitchG1G2(/*esp+3Ch-28h*/ ESP+20, /*esp+1ACh-194h*/ ESP+24);
var int hit;
if (!GFA_TRUE_HITCHANCE) {
// If accuracy/scattering is disabled, stick to the hit chance calculation from Gothic
// G1: float, G2: integer
var int hitChance; hitChance = MEMINT_SwitchG1G2(MEM_ReadInt(hitChancePtr), mkf(MEM_ReadInt(hitChancePtr)));
// The random number by which a hit is determined (integer)
var int rand; rand = EAX % 100;
// Determine if positive hit
hit = lf(mkf(rand), hitChance); // rand < hitChance
} else {
// If accuracy/scattering is enabled, all shots that hit the target are always positive hits
hit = TRUE;
MEM_WriteInt(hitChancePtr, MEMINT_SwitchG1G2(FLOAT1C, 100)); // Overwrite to hit always
};
// Update the shooting statistics
GFA_StatsHits += hit;
};
/*
* For hit registration of projectiles with NPCs, Gothic only checks the bounding box of the collision vob. This results
* in a large number of false positive hits when using free aiming with scattering. This function performs a refined
* collision check once the bounding box collision was determined. This function is called from
* GFA_ExtendCollisionCheck() only if GFA_TRUE_HITCHANCE is true.
*/
func int GFA_RefinedProjectileCollisionCheck(var int vobPtr, var int arrowAI) {
if (!GFA_ACTIVE) {
return TRUE;
};
// Retrieve projectile and rigid body
var int projectilePtr; projectilePtr = MEM_ReadInt(arrowAI+oCAIArrowBase_hostVob_offset);
if (!projectilePtr) {
return TRUE;
};
var oCItem projectile; projectile = _^(projectilePtr);
var int rBody; rBody = projectile._zCVob_rigidBody;
if (!rBody) {
return TRUE;
};
// Direction of collision line: projectile position subtracted from the last predicted position of the rigid body
GFA_DebugCollTrj[0] = projectile._zCVob_trafoObjToWorld[ 3];
GFA_DebugCollTrj[1] = projectile._zCVob_trafoObjToWorld[ 7];
GFA_DebugCollTrj[2] = projectile._zCVob_trafoObjToWorld[11];
GFA_DebugCollTrj[3] = subf(MEM_ReadInt(rBody+zCRigidBody_xPos_offset), GFA_DebugCollTrj[0]);
GFA_DebugCollTrj[4] = subf(MEM_ReadInt(rBody+zCRigidBody_xPos_offset+4), GFA_DebugCollTrj[1]);
GFA_DebugCollTrj[5] = subf(MEM_ReadInt(rBody+zCRigidBody_xPos_offset+8), GFA_DebugCollTrj[2]);
var int fromPosPtr; fromPosPtr = _@(GFA_DebugCollTrj);
var int dirPosPtr; dirPosPtr = _@(GFA_DebugCollTrj)+sizeof_zVEC3;
// Direction vector needs to be normalized
const int call = 0;
if (CALL_Begin(call)) {
CALL__thiscall(_@(dirPosPtr), zVEC3__NormalizeSafe);
call = CALL_End();
};
MEM_CopyBytes(CALL_RetValAsPtr(), dirPosPtr, sizeof_zVEC3);
// Get maximum required length of trajectory inside the bounding box (diagonal of bounding box)
var int bbox[6];
MEM_CopyBytes(vobPtr+zCVob_bbox3D_offset, _@(bbox), sizeof_zTBBox3D);
var int dist; // Distance from bbox.mins to bbox.max
dist = sqrtf(addf(addf(sqrf(subf(bbox[3], bbox[0])), sqrf(subf(bbox[4], bbox[1]))), sqrf(subf(bbox[5], bbox[2]))));
dist = addf(dist, FLOAT3C); // Add the 3m-shift of the start (see below)
// Adjust length of ray (large models have huge bounding boxes)
GFA_DebugCollTrj[0] = subf(GFA_DebugCollTrj[0], mulf(GFA_DebugCollTrj[3], FLOAT3C)); // Start 3m behind projectile
GFA_DebugCollTrj[1] = subf(GFA_DebugCollTrj[1], mulf(GFA_DebugCollTrj[4], FLOAT3C));
GFA_DebugCollTrj[2] = subf(GFA_DebugCollTrj[2], mulf(GFA_DebugCollTrj[5], FLOAT3C));
GFA_DebugCollTrj[3] = mulf(GFA_DebugCollTrj[3], dist); // Trace trajectory from the edge through the bounding box
GFA_DebugCollTrj[4] = mulf(GFA_DebugCollTrj[4], dist);
GFA_DebugCollTrj[5] = mulf(GFA_DebugCollTrj[5], dist);
// Perform refined collision check
GFA_AllowSoftSkinTraceRay(1);
var int hit;
var int flags; flags = zTraceRay_poly_normal | zTraceRay_poly_ignore_transp;
var int trRep; trRep = MEM_Alloc(sizeof_zTTraceRayReport);
const int call2 = 0;
if (CALL_Begin(call2)) {
CALL_PtrParam(_@(trRep)); // zTTraceRayReport (not needed)
CALL_IntParam(_@(flags)); // Trace ray flags
CALL_PtrParam(_@(dirPosPtr)); // Trace ray direction
CALL_PtrParam(_@(fromPosPtr)); // Start vector
CALL_PutRetValTo(_@(hit)); // Did the trace ray hit
CALL__thiscall(_@(vobPtr), zCVob__TraceRay); // This is a vob specific trace ray
call2 = CALL_End();
};
MEM_Free(trRep); // Free the report
GFA_AllowSoftSkinTraceRay(0);
// Also check dedicated head visual if present (not detected by model trace ray)
if (!hit) {
hit = GFA_AimRayHead(vobPtr, fromPosPtr, dirPosPtr, 0);
};
// Add direction vector to position vector to form a line (for debug visualization)
GFA_DebugCollTrj[3] = addf(GFA_DebugCollTrj[0], GFA_DebugCollTrj[3]);
GFA_DebugCollTrj[4] = addf(GFA_DebugCollTrj[1], GFA_DebugCollTrj[4]);
GFA_DebugCollTrj[5] = addf(GFA_DebugCollTrj[2], GFA_DebugCollTrj[5]);
return +hit;
};
/*
* Enlarge the bounding box of human NPCs, because it does not include the head by default. Without the head inside
* the bounding box, shots to the head would not be detected. This function hooks zCModel::CalcModelBBox3DWorld() just
* before exiting the function. This hook might impact performance, since the bounding boxes of models is calculated
* every frame for all unshrunk models.
*/
func void GFA_EnlargeHumanModelBBox() {
// Prevent crash on startup
if (!Hlp_IsValidNpc(hero)) {
return;
};
// Exit for non-NPC models
var int model; model = EBX;
var int vobPtr; vobPtr = MEM_ReadInt(model+zCModel_hostVob_offset);
if (!Hlp_Is_oCNpc(vobPtr)) {
return;
};
// Exit if NPC is shrunk
var oCNpc slf; slf = _^(vobPtr);
if (!Hlp_IsValidNpc(slf)) {
return;
};
// Exit if NPC is not fully initialized yet
if (!slf.anictrl) {
return;
};
// Exit if AI is not fully initialized yet
var zCAIPlayer playerAI; playerAI = _^(slf.anictrl);
if (!playerAI.modelHeadNode) {
return;
};
// Only consider NPCs with a dedicated head visual
var int headNode; headNode = playerAI.modelHeadNode;
if (!MEM_ReadInt(headNode+zCModelNodeInst_visual_offset)) {
return;
};
// Backup frame counter
var int frameCtr; frameCtr = MEM_ReadInt(model+zCModel_masterFrameCtr_offset);
// Calculate the head node bounding box. This will transform the local bounding box to world coordinates
const int call = 0;
if (CALL_Begin(call)) {
CALL__thiscall(_@(model), zCModel__CalcNodeListBBoxWorld);
call = CALL_End();
};
// Reset frame counter to allow reassessing the model again. Important for GFA_CH_DetectIntersectionWithNode()
MEM_WriteInt(model+zCModel_masterFrameCtr_offset, frameCtr);
// Copy the bounding box of the head
var int headBBox[6]; // sizeof_zTBBox3D/4
var int headBBoxPtr; headBBoxPtr = _@(headBBox);
MEM_CopyBytes(headNode+zCModelNodeInst_bbox3D_offset, headBBoxPtr, sizeof_zTBBox3D);
// Subtract the world coordinates from the world-transformed bounding box.
// There does not seem to be an easier way. It is not clear if the local offset of the head node is accessible
// somewhere directly
var zMAT4 trafo; trafo = _^(vobPtr+zCVob_trafoObjToWorld_offset);
headBBox[0] = subf(headBBox[0], trafo.v0[zMAT4_position]);
headBBox[1] = subf(headBBox[1], trafo.v1[zMAT4_position]);
headBBox[2] = subf(headBBox[2], trafo.v2[zMAT4_position]);
headBBox[3] = subf(headBBox[3], trafo.v0[zMAT4_position]);
headBBox[4] = subf(headBBox[4], trafo.v1[zMAT4_position]);
headBBox[5] = subf(headBBox[5], trafo.v2[zMAT4_position]);
// Enlarge the model bounding box by including the head bounding box
var int modelBBoxPtr; modelBBoxPtr = model+zCModel_bbox3d_offset;
const int call2 = 0;
if (CALL_Begin(call2)) {
CALL_PtrParam(_@(headBBoxPtr));
CALL__thiscall(_@(modelBBoxPtr), zTBBox3D__CalcGreaterBBox3D);
call2 = CALL_End();
};
};