mirror of
https://github.com/dhewm/dhewm3-sdk.git
synced 2024-11-30 08:21:07 +00:00
366 lines
17 KiB
Text
366 lines
17 KiB
Text
The following read me file contains various notes by Ivan_the_B about the new melee attack system, combo system, dynamic spread spread system and the dynamic pain system.
|
|
notes compiles together on 10.14.08 by Revility. Any changes or additions to the systems will be added as needed.
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Melee System
|
|
//--------------------------------------------------------------------------------
|
|
|
|
- How it works (quick overview):
|
|
If the weapon worldmodel has the joint "melee", a tracer is fired from that joint in its direction, otherwise is fired from the player vieworigin, as the default D3 melee.
|
|
The width of the tracer is determined by the "melee_tracerWidth" key in the weapon def. A value of "0" means that the tracer is nothing more than a line.
|
|
The lenght of the tracer is determined by the "melee_distance" key in the weapon def.
|
|
The damagedef is determined by the "def_melee" key in the weapon def.
|
|
|
|
|
|
|
|
|
|
|
|
- New weapon scriptevents:
|
|
the old "melee()" scriptevent could be used, but what makes all this so easy to use are these new scriptevents:
|
|
|
|
Code:
|
|
void startAutoMelee( float dmgMult ); //specify the damage multiplier of this attack...very useful for combo ;D
|
|
void stopAutoMelee(); //stop if active!
|
|
|
|
Once "startAutoMelee" is started, the game automatically performs something very close to "melee()" every tick, until "stopAutoMelee()" is called.
|
|
Plus, it automatically ignores the last entity hit since "startAutoMelee" was started, so that it cannot happen that the same entity is constantly hit.
|
|
Starting again "startAutoMelee()" if it's already active resets the last entity hit to null, so this is a way to say "I can hit you again from now".
|
|
|
|
- How to use it:
|
|
Sure, you could call those scriptevents from your weapons, but what would allow us a far greater flexibility is calling them at a certain frame of the player animation.
|
|
That's why I've added the following funtions in the player scriptobject
|
|
|
|
Code:
|
|
startWeaponAutoMeleeX1(); //standard damage
|
|
startWeaponAutoMeleeX2(); //double damage
|
|
//...there are also X3 X4 X5 X10 X100, and you can easily add your own.
|
|
stopWeaponAutoMeleeAny(); //stop if active!
|
|
They simply call the "startAutoMelee" and "stopAutoMelee" scriptobject on the current weapon, specifing different damage multiplyers.
|
|
|
|
Example of the first attack animation for the ruinblade, from player.def.
|
|
|
|
Code:
|
|
anim ruinblade_punch_first models/md5/characters/npcs/playermoves/sword_swing1.md5anim{
|
|
frame 1 object_call startWeaponAutoMeleeX1 //start attack, standard damage
|
|
frame 9 object_call stopWeaponAutoMeleeAny //end attack
|
|
}
|
|
Note that I can choose to stop the attack at the frame 9 out of 15 because it fits the sword movement in the game.
|
|
Ruinblade scriptobject has been modified accordingly (it simply wait for the attack anim to end).
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Movements based on animations for the player
|
|
//--------------------------------------------------------------------------------
|
|
- How it works (quick overview):
|
|
I had to add a lot of stuff in Physics_Player.cpp to add a new movementType ( PM_ANIM ) based on animations delta.
|
|
Player scriptevents can enable/disable this new movementType.
|
|
|
|
While it's active: dodge, dash ,weapon change, ability to attack, player model turning and other movements are disabled.
|
|
The scriptobject of the current weapon is set in a new state called "WeaponWaiting" that should do nothing more than wait (could also stop sounds/effects if needed).
|
|
|
|
When it is disabled: all is reactivated and the scriptobject of the current weapon is set in it "idle" state.
|
|
|
|
- New player scriptevents:
|
|
|
|
Code:
|
|
void setAnimMoveOn();
|
|
void setAnimMoveOff();
|
|
|
|
- How to use it:
|
|
Don't use it
|
|
It should only be started/stopped by the following player function.
|
|
|
|
Quote
|
|
playCustomAnimMove(string animname, float blendIn, float blendOut, float gravityMult)
|
|
It plays the specified animation on full-body with the movementType based on animations delta.
|
|
Use this inside the function for comboes, decribed next.
|
|
"gravityMult" specifiles the gravity fot this animation. 1 means default gravity, 0 means no gravity, 0.5 half gravity, and so on...
|
|
You can also change it from the animation def thanks to the player functions
|
|
|
|
Code:
|
|
void enableGravityInAnimMoveX1();
|
|
void enableGravityInAnimMoveX05();
|
|
void disableGravityInAnimMoveAny();
|
|
|
|
example (assuming this anim has been started with gravity > 0):
|
|
|
|
|
|
Quote
|
|
anim ruinblade_combo_1 models/md5/characters/npcs/playermoves/comboanim.md5anim{ //gravity multiplier only works for combos
|
|
frame 1 sound_body player_sword_fire
|
|
frame 2 object_call startWeaponAutoMeleeX10 //start attack
|
|
frame 5 object_call disableGravityInAnimMoveAny //disable gravity
|
|
frame 13 object_call stopWeaponAutoMeleeAny //end attack
|
|
}
|
|
Note: No need to re-enable the gravity because the multiplier is only used while a combo is executed, not for normal movements.
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Adding a combo animation
|
|
//--------------------------------------------------------------------------------
|
|
Easy. Simply add the new animation in the player.def and start/stop sounds and attacks from there.
|
|
No need to add a corresponding anim in the weapon def.
|
|
Example:
|
|
|
|
|
|
Code:
|
|
anim ruinblade_combo_1 models/md5/characters/npcs/playermoves/sword_swing3.md5anim{ //to do: create a proper anim
|
|
frame 1 sound_body player_sword_fire
|
|
frame 2 object_call startWeaponAutoMeleeX10 //start attack
|
|
frame 13 object_call stopWeaponAutoMeleeAny //end attack
|
|
}
|
|
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Combo system
|
|
//--------------------------------------------------------------------------------
|
|
|
|
- How it works (quick overview):
|
|
Near the beginning of the player .script file, there's a new function.
|
|
|
|
Code:
|
|
void player::check_inputs()
|
|
This baby checks every frame the player inputs and store them, if needed, as flags in a float value (like the attack-choose system in every doom3 AI).
|
|
This means that you can store all the inputs states in a single float and check them later. What could be more handy and efficient?
|
|
|
|
The flags are:
|
|
FLAG_NULL //nothing is pressed
|
|
FLAG_RIGHT
|
|
FLAG_LEFT
|
|
FLAG_FORWARD
|
|
FLAG_BACKWARD
|
|
FLAG_FIREPRI
|
|
FLAG_FIRESEC
|
|
FLAG_JUMP
|
|
FLAG_CROUCH
|
|
|
|
- How to use it:
|
|
Hum.
|
|
Cold seems complex at first sight,but in reality it allows us to specify any sort of combo in a very simple way.
|
|
|
|
We have 7 variables
|
|
|
|
Code:
|
|
float flags_combo_now;
|
|
float flags_combo_1ago;
|
|
float flags_combo_2ago;
|
|
float flags_combo_3ago;
|
|
float flags_combo_4ago;
|
|
float flags_combo_5ago;
|
|
float flags_combo_6ago;
|
|
Each of them represents all the input states in different time instants.
|
|
I won't exaplain which are these instants (from here I'll call them "relevant instants" ), but just know that they are the useful ones.
|
|
|
|
"flags_combo_now" represents the actual input states.
|
|
//...
|
|
"flags_combo_6ago" represents the oldest input states we can remember about.
|
|
|
|
Now...
|
|
Let's start with an example.
|
|
We want to start a certain combo if we quickly press "crouch, crouch, forward".
|
|
First of all, you have to think it as "crouch, nothing, crouch, nothing, forward".
|
|
That is, if we use our flags: FLAG_CROUCH, FLAG_NULL, FLAG_CROUCH, FLAG_NULL, FLAG_FORWARD
|
|
Right?
|
|
|
|
Well, what we want is that
|
|
if(
|
|
the input states registered 4 "relevant instants" ago are == FLAG_CROUCH <--> (flags_combo_4ago == FLAG_CROUCH)
|
|
and
|
|
the input states registered 3 "relevant instants" ago are == FLAG_NULL <--> (flags_combo_3ago == FLAG_NULL)
|
|
and
|
|
the input states registered 2 "relevant instants" ago are == FLAG_CROUCH <--> (flags_combo_2ago == FLAG_CROUCH)
|
|
and
|
|
the input states registered 1 "relevant instants" ago are == FLAG_NULL <--> (flags_combo_1ago == FLAG_NULL)
|
|
and
|
|
the current input states are == FLAG_FORWARD <--> (flags_combo_now == FLAG_FORWARD)
|
|
)then
|
|
performs our combo <--> playCustomAnimMove("our_combo_anim_name", 2, 2);
|
|
|
|
That is, in DoomScript:
|
|
|
|
Code:
|
|
if (( flags_combo_4ago == FLAG_CROUCH ) //crouch
|
|
&& ( flags_combo_3ago == FLAG_NULL ) //nothing
|
|
&& ( flags_combo_2ago == FLAG_CROUCH ) //crouch
|
|
&& ( flags_combo_1ago == FLAG_NULL ) //nothing
|
|
&& ( flags_combo_now == FLAG_FORWARD ) ) { //forward
|
|
sys.println("combo1");
|
|
playCustomAnimMove("ruinblade_combo_1", 2, 2);
|
|
}
|
|
|
|
This is strictly correct, but we can improve it and do much more with this combo system...
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Designing combos is an art...
|
|
//--------------------------------------------------------------------------------
|
|
If you understood everything as far as here, you can go on reading.
|
|
|
|
Now we know that (flags_combo_1ago == FLAG_CROUCH) means that 1 "relevant instant" ago, only CROUCH was pressed
|
|
But remember that (flags_combo_1ago & FLAG_CROUCH) means that 1 "relevant instant" ago, at least CROUCH was pressed
|
|
|
|
Back to out example...
|
|
The problem with the previous solution is that it's too rigid: we have to be careful to do not press FORWARD until we've released FLAG_CROUCH again.
|
|
Since all this has to be executed very quickly, this could not always happen (try to believe).
|
|
That's why the following code is better:
|
|
|
|
|
|
Quote
|
|
if (( flags_combo_4ago == FLAG_CROUCH )
|
|
&& ( flags_combo_3ago == FLAG_NULL )
|
|
&& ( flags_combo_2ago == FLAG_CROUCH )
|
|
&& (( flags_combo_1ago == FLAG_NULL ) || (( flags_combo_1ago & FLAG_CROUCH ) && ( flags_combo_1ago & FLAG_FORWARD )) )
|
|
&& ( flags_combo_now == FLAG_FORWARD ) ) {
|
|
sys.println("combo1");
|
|
playCustomAnimMove("ruinblade_combo_1", 2, 2);
|
|
}
|
|
|
|
In fact, it also allows that both CROUCH and FORWARD were pressed 1 "relevant instant" ago.
|
|
|
|
A lot more complex combos can be done, even if limited to 7 "relevant instants" (but can be increased if we need it).
|
|
See the file for more examples.
|
|
|
|
Also, dodge and dash are now checked this way inside the same function.
|
|
No more 2 extra threads running.
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Ruinblade script rewritten
|
|
//--------------------------------------------------------------------------------
|
|
As I said at the beginning, now every attack really hit enemies that touch our sword (also more than one).
|
|
This means animations become really important, because it only depends on them how useful an attack is.
|
|
|
|
That's why I've rewritten the ruinblade script allowing us to enter in the "cycle" of animation from different points. That is, when we start attacking, instead of playng always the same animation as first one, now it depends:
|
|
- If we are pressing the strafe right button, the first animation will be the attack from right to left.
|
|
- If we are pressing the strafe left button, the first animation will be the attack from left to right.
|
|
- Otherwise the first animation will be the the old one.
|
|
|
|
It is very intuitive and the player feels to have more control over what is happening. And of course more strategy is needed.
|
|
|
|
For example if we have a monster to our right, we can simply hold right before attacking and what we get is that the best animation for this situation is played.
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Conclusions
|
|
//--------------------------------------------------------------------------------
|
|
In conclusion, you only need to edit player.def and player.script
|
|
- In player.def you can add animations and start/stop melee attacks
|
|
- In player.script you can edit check_inputs() to define new combos and start their animations.
|
|
|
|
|
|
I've added, as a test, 4 combos checks for the ruinblade. They only use this system and nothing more.
|
|
You can look at/edit them in void player::check_inputs():
|
|
1) crouch - crouch - fw //damage x10
|
|
2) bw - fw - firesec //damage x2
|
|
3) crouch and without releasing it press ( firepri + firesec) //damage x2
|
|
4) hold firesec about for a second and release it //damage x2
|
|
|
|
I've also added this one to the chainsaw (note that it's the same as the first one of the ruinblade, but this is not a problem)
|
|
1) crouch - crouch - fw //damage x100
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Dynamic spread
|
|
//--------------------------------------------------------------------------------
|
|
Scripted only solution.
|
|
Spread is calculated before firing the in "Fire" weapon state based on player speed and posture.
|
|
|
|
Code:
|
|
spread = 5 + sys.vecLength(mypl.getLinearVelocity()) / 30 ; //spread based on player velocity
|
|
|
|
if(mov_z < 0){ spread = spread/3; }; //spread based on player posture
|
|
|
|
|
|
|
|
Dynamic walkspeed:
|
|
You can set how much the player speed should be multiplied while firing a specific weapon.
|
|
In the weapon .def you can add the key
|
|
|
|
Code:
|
|
"firingWalkSpeedMult" "0.7"
|
|
In this example player walk speed will be muliplied by 0.7. That is, player be 30% slower.
|
|
|
|
|
|
//--------------------------------------------------------------------------------
|
|
//Enemy Pain reaction Changes
|
|
//--------------------------------------------------------------------------------
|
|
|
|
- The idea:
|
|
there are 2 sets of pain anims identified by their name.
|
|
1) "pain_zonename" are meant for normal pain (monster stands in place in pain)
|
|
2) "highpain_zonename" are meant for high pain (monster falls back/down in pain)
|
|
|
|
Every damage zone has a current "damage value" that automatically slowly decreases to 0.
|
|
When the damage on a zone is greater than the maximum value specified for that zone, the right highpain anim is played and all the damage valuea are reset to 0.
|
|
|
|
|
|
- How to add a new pain animation
|
|
Simply add it in the modeldef.
|
|
Just make sure overrideLegs is called in order to play it also on legs.
|
|
|
|
Code:
|
|
anim highpain_head models/md5/monsters/vagary/chestpain.md5anim { //this is for head only
|
|
frame 1 call overrideLegs //<---!
|
|
frame 1 sound_voice
|
|
}
|
|
|
|
NOTE: overrideLegs is not called for maggots, fat and running zombies because their pain anims make them to walk. There's a fix for this in their AIs that force idle anim on legs, but are just exceptions.
|
|
|
|
In case of missing pain anim for a particular zone:
|
|
- standard pain: default anim is "pain"
|
|
- high pain: default anim is "highpain" (if even "highpain" is not present, "pain" is played)
|
|
|
|
|
|
- How AIs works
|
|
Main changes are in Torso_Pain(), where timeouts and states are set based on the values of boolean flags like AI_HIGHPAIN, AI_COMBOPAIN, AI_MELEEPAIN.
|
|
|
|
Those timeouts allow check_attacks() to choose only certain type of attacks (usually dodge) for a while after a pain animation.
|
|
|
|
If AI_HIGHPAIN is true, the monster is set in a state called state_HighPain() that wait for the pain anim to end. This will allow monsters to fall/down back with long animations without the risk that the anim could be interrupted by other attack anims.
|
|
|
|
- Define values for each monster
|
|
|
|
example for head zone:
|
|
"highpain_threshold head" "180" //180 damage points is the threshold for head zone
|
|
"highpain_resettime head" "12" //it takes 12 seconds to go from 180 to 0.
|
|
|
|
|
|
- Minimum time between pain anim
|
|
|
|
Every monster inherits from monster_default the following keys:
|
|
|
|
Code:
|
|
"pain_threshold" "1" //play pain anims if damaged more than 1hp (that is, always)
|
|
"pain_delay" "1" //minimum time between 2 pain anims and pain sounds.
|
|
|
|
Note that those values are NOT used if pain is inflicted by a melee damage (the ones with the key "melee" "1") because monsters should be always ready to react to combo or simple sword attacks.
|
|
|
|
What is really the minimum time between the beginning of 2 pain anims is the value
|
|
|
|
Code:
|
|
nextpain = sys.getTime() + 0.1;
|
|
|
|
that is set in Torso_Pain() animstate.
|
|
|
|
It is usually 0.1 seconds. But in the case of archvile, hellknight and vagary this value is much higher and is checked also in other animstates in order to make them harder to fight.
|
|
|
|
- Force pain type with combo
|
|
|
|
You can call the following function from the combo animation itself
|
|
|
|
Code:
|
|
void comboForceHighPain();
|
|
void comboAllowHighPain();
|
|
void comboDenyHighPain();
|
|
|
|
example:
|
|
|
|
|
|
Code:
|
|
anim ruinblade_combo_1 todo.md5anim{ //to do: create a proper anim
|
|
frame 1 sound_body player_sword_fire
|
|
frame 2 object_call startWeaponAutoMeleeX1 //start attack 1
|
|
frame 2 object_call comboDenyHighPain//every actor hit will choose standard pain anims
|
|
|
|
frame 9 object_call stopWeaponAutoMeleeAny //end attack 1
|
|
frame 10 object_call startWeaponAutoMeleeX2 //start attack 2
|
|
frame 10 object_call comboForceHighPain //every actor hit will choose highpain anims
|
|
|
|
frame 20 object_call stopWeaponAutoMeleeAny //end attack 2
|
|
}
|
|
|
|
No need to reset damage multiplier or pain anim type because they work only while a combo is performed.
|