[Skript] Tauchbalken zu universellem Ausdauersystem erweitern
Mit LeGo lassen sich sehr bequem Bars erstellen. In den meisten Fällen ist die Absicht aber nur ein einzelner Balken: ein Ausdauersystem. Meist ist dann die Frage, ob der Tauchbalken ersetzt werden soll oder wie ein neuer Balken in das Schema passt, usw.
Eine vertane Chance, denn der Tauchbalken beschreibt bereits nichts anderes als verbleibenden Atem und wird außer fürs Tauchen im Spiel nicht genutzt. Außerdem ist der Balken schon vollständig integriert und regeneriert bereits automatisch. Es reicht vollkommen, den Wert der Variable "divectr" des oCNpc-Objekts des Helden zu ändern - fertig ist das Ausauersystem inkl. Balken und Regeneration.
Um diese Ausdauer (oder besser den Atem) für Modder besser offen zu legen, habe ich ein Skript geschrieben, dass eine komfortable Schnittstelle darstellt.
Neben Breath_Decrease { Ms / Percent / Permille }, um den Atem manipulieren, gibt es eine Vielzahl von Funktionen, z.B. um die Regenerierung anzupassen, den maximalen Atem zu vergrößern oder um sich den verbleibenden Atem eines NPC zu holen. Dabei wird der verbleibende Atem des Helden mitgespeichert, sowie dessen maximal verfügbarer Atem. Entsprechende Werte von anderen NPCs werden nicht mitgespeichert. Der Regenerierungsfaktor bleibt ebenfalls über Spielstände hinweg erhalten. Dieser Faktor ist global für alle NPC. Da NPC normalerweise nicht tauchen (sondern maximal schwimmen) stellt das kein Problem da. Der Tauchbalken kann dazu angezeigt werden - entweder solange er nicht voll ist, oder durchgängig.
Mehr gibt es kaum hinzuzufügen. Besser ist die Auflistung der Funktion am Anfang des Skripts zusammen mit den Kommentaren über den jeweiligen Funktionen.
Code:
/*
* breath.d
* Source: https://forum.worldofplayers.de/forum/threads/?p=26441844
*
* Collection of functions that expose the swim bar for generalized use (e.g. stamina, breath).
*
* - Requires Ikarus 1.2.2
* - Compatible with Gothic 1 and Gothic 2
*
* Instructions
* - Initialize from Init_Global with these two lines
* Breath_Init();
* Breath_SetBarVisibleNonEmpty(); - OR - Breath_SetBarVisibleAlways();
*
*
* Initialize the regeneration and bar visibility (call from Init_Global)
* func void Breath_Init()
*
* Set visibility of swim bar (to be called optionally AFTER Breath_Init)
* func void Breath_SetBarVisibleAlways()
* func void Breath_SetBarVisibleNonEmpty()
*
* Get/set maximum breath, load/save-persistent for player only. Default is 30000 ms (30 sec)
* func intf Breath_TotalF (C_Npc slf)
* func int Breath_Total (C_Npc slf)
* func void Breath_SetTotalF(C_Npc slf, intf ms)
* func void Breath_SetTotal (C_Npc slf, int ms)
*
* Get remaining breath
* func intf Breath_RemainingMsF (C_Npc slf)
* func int Breath_RemainingMs (C_Npc slf)
* func int Breath_RemainingPercent (C_Npc slf)
* func int Breath_RemainingPermille(C_Npc slf)
*
* Check if enough breath is available
* func bool Breath_AvailableMsF (C_Npc slf, intf ms)
* func bool Breath_AvailableMs (C_Npc slf, int ms)
* func bool Breath_AvailablePercent (C_Npc slf, int percent)
* func bool Breath_AvailablePermille(C_Npc slf, int permille)
*
* Set remaining breath
* func void Breath_SetMsF (C_Npc slf, intf ms)
* func void Breath_SetMs (C_Npc slf, int ms)
* func void Breath_SetPercent (C_Npc slf, int percent)
* func void Breath_SetPermille(C_Npc slf, int permille)
*
* Decrease breath
* func void Breath_DecreaseMsF (C_Npc slf, intf ms)
* func void Breath_DecreaseMs (C_Npc slf, int ms)
* func void Breath_DecreasePercent (C_Npc slf, int percent)
* func void Breath_DecreasePermille(C_Npc slf, int permille)
*
* Decrease breath if available, return TRUE if successful
* func bool Breath_RequestMsF (C_Npc slf, intf ms, intf required)
* func bool Breath_RequestMs (C_Npc slf, int ms, int required)
* func bool Breath_RequestPercent (C_Npc slf, int percent, int required)
* func bool Breath_RequestPermille(C_Npc slf, int permille, int required)
*
* Set the regeneration: how many ms of breath are recovered within one sec. Default is 2000 ms
* func void Breath_SetRegeneration(int ms)
*//* Internal constants/variables */
const int _Breath_DontRegen = 0;
var int _Breath_player_max;
var int _Breath_RegenFactor;
/*
* Obtain the corresponding number of milliseconds for a fraction of the total breath
*/
func int Breath_ToMs(var C_Npc slf, var int amount, var int frac){
if (frac <= 1){ return amount; };
if (!Hlp_IsValidNpc(slf)){ return FLOATNULL; };
var oCNpc npc; npc = Hlp_GetNpc(slf);
var int unit; unit = divf(npc.divetime, mkf(frac));
return mulf(amount, unit);
};
/*
* Obtain the corresponding fraction of the total breath for a number of milliseconds
*/
func int Breath_FromMs(var C_Npc slf, var int ms, var int frac){
if (frac <= 1){ return ms; };
if (!Hlp_IsValidNpc(slf)){ return FLOATNULL; };
var oCNpc npc; npc = Hlp_GetNpc(slf);
var int unit; unit = divf(mkf(frac), npc.divetime);
return mulf(ms, unit);
};
/*
* Return the total breath in milliseconds (integer float)
*/
func int Breath_TotalF(var C_Npc slf){
if (!Hlp_IsValidNpc(slf)){ return FLOATNULL; };
var oCNpc npc; npc = Hlp_GetNpc(slf);
return npc.divetime;
};
func int Breath_Total(var C_Npc slf){
return roundf(Breath_TotalF(slf));
};
/*
* Set the total breath in milliseconds (integer float)
* The remaining breath value is not altered and remains as before
*/
func void Breath_SetTotalF(var C_Npc slf, var int ms){
if (!Hlp_IsValidNpc(slf)){ return; };
var oCNpc npc; npc = Hlp_GetNpc(slf);
// If negative, decrease current total
if (lf(ms, FLOATNULL)){
ms = addf(npc.divetime, ms);
if (lf(ms, FLOATNULL)){
ms = FLOATNULL;
};
};
npc.divetime = ms;
// Save for loading/level change
if (Npc_IsPlayer(slf)){
_Breath_player_max = npc.divetime;
};
};
func void Breath_SetTotal(var C_Npc slf, var int ms){
Breath_SetTotalF(slf, mkf(ms));
};
/*
* Return the remaining breath in milliseconds or fraction (integer float)
*/
func int Breath_Remaining(var C_Npc slf, var int frac){
if (!Hlp_IsValidNpc(slf)){ return FLOATNULL; };
var oCNpc npc; npc = Hlp_GetNpc(slf);
return Breath_FromMs(slf, npc.divectr, frac);
};
func int Breath_RemainingMsF(var C_Npc slf){
return Breath_Remaining(slf, 1);
};
func int Breath_RemainingMs(var C_Npc slf){
return roundf(Breath_Remaining(slf, 1));
};
func int Breath_RemainingPercent(var C_Npc slf){
return roundf(Breath_Remaining(slf, 100));
};
func int Breath_RemainingPermille(var C_Npc slf){
return roundf(Breath_Remaining(slf, 1000));
};
/*
* Check if enough breath (milliseconds or fraction) is available
*/
func int Breath_Available(var C_Npc slf, var int amount, var int frac){
var int ms; ms = Breath_ToMs(slf, amount, frac);
var int avail; avail = Breath_Remaining(slf, 1);
return gef(avail, ms);
};
func int Breath_AvailableMsF(var C_Npc slf, var int ms){
return Breath_Available(slf, ms, 1);
};
func int Breath_AvailableMs(var C_Npc slf, var int ms){
return Breath_Available(slf, mkf(ms), 1);
};
func int Breath_AvailablePercent(var C_Npc slf, var int percent){
return Breath_Available(slf, mkf(percent), 100);
};
func int Breath_AvailablePermille(var C_Npc slf, var int permille){
return Breath_Available(slf, mkf(permille), 1000);
};
/*
* Set the remaining breath in milliseconds or fraction (integer float)
*/
func void Breath_Set(var C_Npc slf, var int amount, var int frac){
if (!Hlp_IsValidNpc(slf)){ return; };
var int ms; ms = Breath_ToMs(slf, amount, frac);
var oCNpc npc; npc = Hlp_GetNpc(slf);
// If negative, decrease current remaining
if (lf(ms, FLOATNULL)){
ms = addf(npc.divectr, ms);
if (lf(ms, FLOATNULL)){
ms = FLOATNULL;
};
} else if (gf(ms, npc.divetime)){
ms = npc.divetime;
};
npc.divectr = ms;
// Do not regenerate for this frame
if (!_Breath_DontRegen){ _Breath_DontRegen = MEM_ArrayCreate(); };
MEM_ArrayInsert(_Breath_DontRegen, _@(npc));
};
func void Breath_SetMsF(var C_Npc slf, var int ms){
Breath_Set(slf, ms, 1);
};
func void Breath_SetMs(var C_Npc slf, var int ms){
Breath_Set(slf, mkf(ms), 1);
};
func void Breath_SetPercent(var C_Npc slf, var int percent){
Breath_Set(slf, mkf(percent), 100);
};
func void Breath_SetPermille(var C_Npc slf, var int permille){
Breath_Set(slf, mkf(permille), 1000);
};
/*
* Decrease the breath (by milliseconds or fraction)
*/
func void Breath_Decrease(var C_Npc slf, var int amount, var int frac){
var int ms; ms = Breath_ToMs(slf, amount, frac);
Breath_Set(slf, negf(ms), 1);
};
func void Breath_DecreaseMsF(var C_Npc slf, var int ms){
Breath_Decrease(slf, ms, 1);
};
func void Breath_DecreaseMs(var C_Npc slf, var int ms){
Breath_Decrease(slf, mkf(ms), 1);
};
func void Breath_DecreasePercent(var C_Npc slf, var int percent){
Breath_Decrease(slf, mkf(percent), 100);
};
func void Breath_DecreasePermille(var C_Npc slf, var int permille){
Breath_Decrease(slf, mkf(permille), 1000);
};
/*
* Decrease the breath (by milliseconds or fraction) only if a required minimum is available
*/
func int Breath_Request(var C_Npc slf, var int amount, var int required, var int frac){
if (Breath_Available(slf, required, frac)){
Breath_Decrease(slf, amount, frac);
return TRUE;
} else {
return FALSE;
};
};
func int Breath_RequestMsF(var C_Npc slf, var int ms, var int required){
return Breath_Request(slf, ms, required, 1);
};
func int Breath_RequestMs(var C_Npc slf, var int ms, var int required){
return Breath_Request(slf, mkf(ms), mkf(required), 1);
};
func int Breath_RequestPercent(var C_Npc slf, var int percent, var int required){
return Breath_Request(slf, mkf(percent), mkf(required), 100);
};
func int Breath_RequestPermille(var C_Npc slf, var int permille, var int required){
return Breath_Request(slf, mkf(permille), mkf(required), 1000);
};
/*
* Set how many milliseconds it takes to regenerate one second of breath (for all NPC). Default is 2000 ms per second
*/
func void Breath_SetRegeneration(var int ms){
_Breath_RegenFactor = fracf(ms, 1000);
};
/*
* Change the visibility of the swim bar
*/
func void Breath_SetBarVisible(var int always){
const int oCGame__UpdatePlayerStatus_swimBarJmp1_G1 = 6525142; //0x6390D6
const int oCGame__UpdatePlayerStatus_swimBarJmp1_G2 = 7090988; //0x6C332C
const int oCGame__UpdatePlayerStatus_swimBarJmp2_G1 = 6525154; //0x6390E2
const int oCGame__UpdatePlayerStatus_swimBarJmp2_G2 = 7091000; //0x6C3338
var int addr1; var int addr2;
addr1 = MEMINT_SwitchG1G2(oCGame__UpdatePlayerStatus_swimBarJmp1_G1, oCGame__UpdatePlayerStatus_swimBarJmp1_G2);
addr2 = MEMINT_SwitchG1G2(oCGame__UpdatePlayerStatus_swimBarJmp2_G1, oCGame__UpdatePlayerStatus_swimBarJmp2_G2);
const int once = 0;
if (!once){
MemoryProtectionOverride(addr1, 2);
MemoryProtectionOverride(addr2, 14);
once = 1;
};
if (always){// Overwrite first conditional jump to unconditional jump
if (MEM_ReadByte( addr1) == /*0F*/ 15){
MEM_WriteByte(addr1, /*EB*/235); // jmp
MEM_WriteByte(addr1+1, /*1C*/ 28); // 0x28
};
} else {// Revert the above change
if (MEM_ReadInt( addr1) == /*EB 1C A4 00*/10755307){// Compare 4 bytes, because of third-party changes
MEM_WriteByte(addr1, /*0F*/ 15); // jz
MEM_WriteByte(addr1+1, /*84*/132);
};
// Overwrite second condition to be true if breath is non-full
if (MEM_ReadByte( addr2) == /*E8*/232){
MEM_WriteByte(addr2, /*8B*/139); // mov
MEM_WriteByte(addr2+1, /*81*/129); // eax,
MEM_WriteInt( addr2+2, MEMINT_SwitchG1G2(2064, 2000)); // [offset oCNpc.divetime]
MEM_WriteByte(addr2+6, /*3B*/ 59); // cmp
MEM_WriteByte(addr2+7, /*81*/129); // eax,
MEM_WriteInt( addr2+8, MEMINT_SwitchG1G2(2068, 2004)); // [offset oCNpc.divectr]
MEM_WriteByte(addr2+12, /*0F*/ 15); // jz
MEM_WriteByte(addr2+13, /*84*/132);
};
};
};
func void Breath_SetBarVisibleAlways(){
Breath_SetBarVisible(TRUE);
};
func void Breath_SetBarVisibleNonEmpty(){
Breath_SetBarVisible(FALSE);
};
/*
This is the Daedalus-based (very slow) equivalent to the assembly-based initialization below.
It's kept here for better understanding of the purpose and method.
/*
* Internal hook for regenerating breath
*
var int _Breath_player;
func void _Breath_RegenHook() {
var oCNpc npc; npc = _^(ESI);
if (Npc_IsPlayer(npc)) {
_Breath_player = npc.divectr;
};
var int factor; factor = _Breath_RegenFactor;
if (_Breath_DontRegen) {
var int i; i = MEM_ArrayIndexOf(_Breath_DontRegen, ESI);
if (i != -1) {
factor = 0;
MEM_ArrayRemoveIndex(_Breath_DontRegen, i);
};
};
// Additive term to divectr
var int incr; incr = mulf(MEM_Timer.frameTimeFloat, factor);
EAX = _@(incr);
};
/*
* Modify the breath regeneration
*
func void Breath_Init_slow() {
const int oCNpc_Process_diveRegen_G1 = 6926517; //0x69B0B5
const int oCNpc_Process_diveRegen_G2 = 7595752; //0x73E6E8
var int address; address = MEMINT_SwitchG1G2(oCNpc_Process_diveRegen_G1, oCNpc_Process_diveRegen_G2);
// Only perform changes if bytes are not already modified
if (MEM_ReadInt(address + 2) == _@(MEM_Timer.frameTimeFloat)) {
const int ASMINT_OP_floatLoadFromEAX = 217; //0x00D9
MemoryProtectionOverride(address, 8);
MEM_WriteInt(address, ASMINT_OP_nop + (ASMINT_OP_nop<<8) + (ASMINT_OP_nop<<16) + (ASMINT_OP_nop<<24));
MEM_WriteInt(address + 4, ASMINT_OP_nop + (ASMINT_OP_nop<<8) + (ASMINT_OP_floatLoadFromEAX<<16));
HookEngineF(address, 5, _Breath_RegenHook);
};
// Reset regenerate list
if (_Breath_DontRegen) {
MEM_ArrayClear(_Breath_DontRegen);
};
// Set default regeneration first time: regenerate 2 seconds of breath every second
var int oncePerSave;
if (!oncePerSave) {
Breath_SetRegeneration(2000);
_Breath_player = mkf(30000); // NPC defaults
_Breath_player_max = mkf(30000); // NPC defaults
oncePerSave = TRUE;
} else if (Hlp_IsValidNpc(hero)) {
// Hero only exists at this point if a game is being loaded or the world is changed: exactly what we want
var oCNpc her; her = Hlp_GetNpc(hero);
if (her.divetime == mkf(30000)) {
// Double check, in case it was changed elsewhere already (e.g. Init_Global)
her.divetime = _Breath_player_max;
};
her.divectr = _Breath_player;
};
// Set the swim bar to visible when non-empty
Breath_SetBarVisibleNonEmpty();
};*//*
* Modify the breath regeneration
* Written in assembly for performance, because it is executed every frame for all NPC with non-full breath
*/
func void Breath_Init(){
const int oCNpc_Process_diveRegen_G1 = 6926517; //0x69B0B5
const int oCNpc_Process_diveRegen_G2 = 7595752; //0x73E6E8
var int address; address = MEMINT_SwitchG1G2(oCNpc_Process_diveRegen_G1, oCNpc_Process_diveRegen_G2);
var int _Breath_player;
// Only perform the following changes if the bytes are not already modified
if (MEM_ReadInt(address + 2) == _@(MEM_Timer.frameTimeFloat)){// Assembly instructions
const int ASMINT_OP_pushESI = 86; //0x56
const int ASMINT_OP_jzShort = 116; //0x74
const int ASMINT_OP_jnzShort = 117; //0x75
// 2 bytes
const int ASMINT_OP_testECXtoECX = 51589; //0xC985
const int ASMINT_OP_testEAXtoEAX = 49285; //0xC085
const int ASMINT_OP_floatLoadFromMem = 1497; //0x05D9
const int ASMINT_OP_subSTfromST = 57560; //0xE0D8
const int ASMINT_OP_mulST0byST1pop = 51678; //0xC9DE
const int ASMINT_OP_cmpESItoMem = 13627; //0x353B
const int ASMINT_OP_movECXtoMem = 3465; //0x0D89
const int ASMINT_OP_movESIplusToECX = 36491; //0x8E8B
// Re-write regeneration (assembly for performance: called every frame for all NPC!)
var int oCNpc_divectr_offset;
var int oCNpc_player;
var int zCArray_zCVob__IsInList;
var int zCArray_zCVob__Remove;
oCNpc_divectr_offset = MEMINT_SwitchG1G2(2068, 2004);
oCNpc_player = MEMINT_SwitchG1G2(/*0x8DBBB0*/9288624, /*0xAB2684*/11216516);
zCArray_zCVob__IsInList = MEMINT_SwitchG1G2(/*0x648EB0*/6590128, /*0x6D3D80*/ 7159168);
zCArray_zCVob__Remove = MEMINT_SwitchG1G2(/*0x648E60*/6590048, /*0x6D3D30*/ 7159088);
ASM_Open(83);
MemoryProtectionOverride(address, 8);
MEM_WriteByte(address, ASMINT_OP_jmp);
MEM_WriteInt (address + 1, ASM_Here() - address - 5);
// Save breath value of player
ASM_2(ASMINT_OP_cmpESItoMem); ASM_4(oCNpc_player);
ASM_1(ASMINT_OP_jnzShort); ASM_1(12);
ASM_2(ASMINT_OP_movESIplusToECX); ASM_4(oCNpc_divectr_offset);
ASM_2(ASMINT_OP_movECXtoMem); ASM_4(_@(_Breath_player));
// Load regeneration factors
ASM_2(ASMINT_OP_floatLoadFromMem); ASM_4(_@(MEM_Timer.frameTimeFloat));
ASM_2(ASMINT_OP_floatLoadFromMem); ASM_4(_@(_Breath_RegenFactor));
// Check if array exists
ASM_2(ASMINT_OP_movMemToECX); ASM_4(_@(_Breath_DontRegen));
ASM_2(ASMINT_OP_testECXtoECX);
ASM_1(ASMINT_OP_jzShort); ASM_1(14+18);
// Check if NPC is in array
ASM_1(ASMINT_OP_pushESI);
ASM_2(ASMINT_OP_movESPtoEAX);
ASM_1(ASMINT_OP_pushEAX);
ASM_1(ASMINT_OP_call); ASM_4(zCArray_zCVob__IsInList - (ASM_Here()+4));
ASM_1(ASMINT_OP_popECX);
ASM_2(ASMINT_OP_testEAXtoEAX);
ASM_1(ASMINT_OP_jzShort); ASM_1(18);
// Set one factor to zero
ASM_2(ASMINT_OP_subSTfromST);
ASM_1(ASMINT_OP_pushESI);
ASM_2(ASMINT_OP_movESPtoEAX);
ASM_1(ASMINT_OP_pushEAX);
ASM_2(ASMINT_OP_movMemToECX); ASM_4(_@(_Breath_DontRegen));
ASM_1(ASMINT_OP_call); ASM_4(zCArray_zCVob__Remove - (ASM_Here()+4));
ASM_1(ASMINT_OP_popECX);
// Multiply regeneration factors
ASM_2(ASMINT_OP_mulST0byST1pop);
ASM_1(ASMINT_OP_pushIm); ASM_4(address+8);
ASM_1(ASMINT_OP_retn);
var int i; i = ASM_Close();
};
// Reset regenerate list
if (_Breath_DontRegen){
MEM_ArrayClear(_Breath_DontRegen);
};
// Set default regeneration first time: regenerate 2 seconds of breath every second
var int oncePerSave;
if (!oncePerSave){
Breath_SetRegeneration(2000);
_Breath_player = mkf(30000); // NPC defaults
_Breath_player_max = mkf(30000); // NPC defaults
oncePerSave = TRUE;
} else if (Hlp_IsValidNpc(hero)){// Restore breath values for player
// Hero only exists at this point if a game is being loaded or the world is changed: exactly what we want
var oCNpc her; her = Hlp_GetNpc(hero);
if (her.divetime == mkf(30000)){// Double check, in case it was changed elsewhere already (e.g. Init_Global)
her.divetime = _Breath_player_max;
};
her.divectr = _Breath_player;
};
// Set the swim bar to visible when non-empty
Breath_SetBarVisibleNonEmpty();
};
Diese Schnittschnelle ist so einfach zu nutzen, dass ich in diesem Post zwei Beispiele zeige.
(a) Ein komplettes Sprint System
(b) Ein komplettes Stamina-System für den Nahkampf
Beide Beispiele betten sich sehr gut in Gothic ein - mit nur wenigen Zeilen Code. Erstaunlicherweise bestehen die Beispiele jeweils aus nur einer zentralen sehr kurzen Funktion (neben jeweils zwei Hilfsfunktionen und einer Initialisierungsfunktion).
Diese Beispiele sind sofort nutzbar und ohne Codeänderungen in jeder Mod einsatzbereit, da die Einstellungsmöglichkeiten über Ausdauer(oder Atem)-Verbrauch in Variablen zugänglich sind.
Den Code lasse ich ansonsten ganz unkommentiert hier. Stattdessen möchte ich auf einen Patch verweisen, den ich damit erstellt habe. Er nutzt die drei Skripte wie hier vorgestellt ohne große Änderungen.
Da der Code hier frei zugänglich ist und evtl. seinen Weg in eine Mod findet, habe ich im Patch darauf geachtet, das System unangetastet zu lassen, falls es bereits in einer Mod so enthalten ist.
/*
* integratedSprint.d
* Source: https://forum.worldofplayers.de/forum/threads/?p=26441848
*
* Add sprint integrated into the internal breath (diving) system
*
* - Requires Ikarus 1.2.2, LeGo 2.6.0 (FrameFunctions), breath.d
* - Compatible with Gothic 1 and Gothic 2
*
* Instructions
* - Initialize from Init_Global with
* IntegratedSprint_Init();
* - Set the variable IntegratedSprint_DurationMS from the scripts. This allows to dynamically change the duration. E.g.
* IntegratedSprint_DurationMS = 15000; // 15 seconds of total sprint
*/
const string IntegratedSprint_Mds = "HUMANS_SPRINT_CLEAN.MDS"; // MDS overlay name
var int IntegratedSprint_DurationMS; // Duration of sprint in milliseconds: Set in scripts
/*
* Check if an MDS overlay of given name is active
*/
func int Mdl_OverlayMdsIsActive(var C_Npc slf, var string mdsName){
var oCNpc npc; npc = Hlp_GetNpc(slf);
mdsName = STR_Upper(mdsName);
repeat(i, npc.activeOverlays_numInArray); var int i;
if (Hlp_StrCmp(mdsName, MEM_ReadStringArray(npc.activeOverlays_array, i))){
return TRUE;
};
end;
return FALSE;
};
/*
* Check if an NPC is running
*/
func int Npc_IsRunning(var C_Npc slf){
var oCNpc npc; npc = Hlp_GetNpc(slf);
var int aiPtr; aiPtr = npc.human_ai;
if (GOTHIC_BASE_VERSION == 2){
const int oCAniCtrl_Human__IsRunning = 7004672; //0x6AE200
const int call = 0;
if (CALL_Begin(call)){
CALL_PutRetValTo(_@(ret));
CALL__thiscall(_@(aiPtr), oCAniCtrl_Human__IsRunning);
call = CALL_End();
};
var int ret;
return +ret;
} else {// Gothic 1 does not have the function oCAniCtrl_Human::IsRunning. Re-implement it here
// These lines require at lease Ikarus 1.2.2
var oCAniCtrl_Human ai; ai = _^(aiPtr);
var int modelPtr; modelPtr = ai._zCAIPlayer_model;
if (!modelPtr){
return FALSE;
};
// Modified from auxillary.d in GothicFreeAim
// https://github.com/szapp/GothicFreeAim/blob/v1.2.0/_work/data/Scripts/Content/GFA/_intern/auxiliary.d#L169
const int zCModel_numActAnis_offset = 52; //0x34
const int zCModel_actAniList_offset = 56; //0x37
const int zCModelAni_aniID_offset = 76; //0x4C
var int actAniOffset; actAniOffset = modelPtr+zCModel_actAniList_offset;
repeat(i, MEM_ReadInt(modelPtr+zCModel_numActAnis_offset)); var int i;
var int aniID; aniID = MEM_ReadInt(MEM_ReadInt(MEM_ReadInt(actAniOffset))+zCModelAni_aniID_offset);
if (aniID == MEM_ReadStatArr(ai.s_runl, npc.fmode))
|| (aniID == MEM_ReadStatArr(ai.s_runr, npc.fmode))
|| (aniID == MEM_ReadStatArr(ai.t_run_2_runl, npc.fmode))
|| (aniID == MEM_ReadStatArr(ai.t_runl_2_run, npc.fmode))
|| (aniID == MEM_ReadStatArr(ai.t_runl_2_runr, npc.fmode))
|| (aniID == MEM_ReadStatArr(ai.t_runr_2_runl, npc.fmode))
|| (aniID == MEM_ReadStatArr(ai.t_runr_2_run, npc.fmode)){
return TRUE;
};
actAniOffset += 4;
end;
};
return FALSE;
};
/*
* Breath based sprinting. This function has to be called every single frame
*/
func void IntegratedSprint(){
const int THRESHOLD_MS = 1161527296; // 3000.0f
const int ACTION_WATERWALK = 4; // oCAniCtrl_Human.actionMode
const int oCNpc__SetWeaponMode2_G1 = 6904416; //0x695A60
const int oCNpc__SetWeaponMode2_G2 = 7573120; //0x738E80
var oCNpc her; her = Hlp_GetNpc(hero);
var oCAniCtrl_Human ai; ai = _^(her.human_ai);
// Calculate cost per this frame (dynamically, because frame length varies and divetime might be updated)
var int factor; factor = divf(her.divetime, mkf(IntegratedSprint_DurationMS));
var int cost; cost = mulf(MEM_Timer.frameTimeFloat, factor);
if ((MEM_KeyPressed(MEM_GetKey("keyIntSprint"))) || (MEM_KeyPressed(MEM_GetSecondaryKey("keyIntSprint"))))
&& (Breath_AvailableMsF(hero, cost))
&& (ai.actionMode < ACTION_WATERWALK){// Decrease only while running
if (Npc_IsRunning(hero)){
Breath_DecreaseMsF(hero, cost);
// Apply overlay after refractory period
if (!Mdl_OverlayMdsIsActive(hero, IntegratedSprint_Mds))
&& (Breath_AvailableMsF(hero, THRESHOLD_MS)){Mdl_ApplyOverlayMds(her, IntegratedSprint_Mds);
};
};
} else if (Mdl_OverlayMdsIsActive(hero, IntegratedSprint_Mds)){// Check if mid-air, copied from GothicFreeAim <http://github.com/szapp/GothicFreeAim>
if (lf(ai._zCAIPlayer_aboveFloor, mkf(50))){Mdl_RemoveOverlayMds(her, IntegratedSprint_Mds);
// Fix ranged combat by re-initializing weapon mode
if (Npc_HasReadiedRangedWeapon(hero)){
var int herPtr; herPtr = _@(her);
var int fmode; fmode = her.fmode;
var int zero;
const int call = 0;
if (CALL_Begin(call)){
CALL_IntParam(_@(zero));
CALL__thiscall(_@(herPtr), MEMINT_SwitchG1G2(oCNpc__SetWeaponMode2_G1, oCNpc__SetWeaponMode2_G2));
CALL_IntParam(_@(fmode));
CALL__thiscall(_@(herPtr), MEMINT_SwitchG1G2(oCNpc__SetWeaponMode2_G1, oCNpc__SetWeaponMode2_G2));
call = CALL_End();
};
};
};
};
};
/*
* Initialization function to be called from Init_Global
*/
func void IntegratedSprint_Init(){// Requires LeGo FrameFunctions
if (_LeGo_Flags & LeGo_FrameFunctions){
FF_ApplyOnceExtGT(IntegratedSprint, 0, -1);
};
};
/*
* integratedStamina.d
* Source: https://forum.worldofplayers.de/forum/threads/?p=26441848
*
* Add stamina regulation to melee fighting integrated into the internal breath (diving) system
*
* - Requires Ikarus 1.2.2, LeGo (HookEngine), breath.d
* - Compatible with Gothic 1 and Gothic 2
*
* Instructions
* - Initialize from Init_Global with
* IntegratedStamina_Init();
* - Set the below variables somewhere from the scripts. This allows to dynamically change the percentage costs. E.g.
* IntegratedStamina_FIST_PARADE = 10;
* IntegratedStamina_FIST_FIRSTHIT = 7;
* IntegratedStamina_FIST_COMBO = 4;
* IntegratedStamina_2H_PARADE = 18;
* IntegratedStamina_2H_FIRSTHIT = 10;
* IntegratedStamina_2H_COMBO = 8;
* IntegratedStamina_1H_PARADE = 25;
* IntegratedStamina_1H_FIRSTHIT = 15;
* IntegratedStamina_1H_COMBO = 10;
*/
const string IntegratedStamina_Mds = "HUMANS_DISABLE_MELEE.MDS"; // MDS overlay name
var int IntegratedStamina_FIST_PARADE; // Breath cost in percent
var int IntegratedStamina_FIST_FIRSTHIT; // Breath cost in percent
var int IntegratedStamina_FIST_COMBO; // Breath cost in percent
var int IntegratedStamina_2H_PARADE; // Breath cost in percent
var int IntegratedStamina_2H_FIRSTHIT; // Breath cost in percent
var int IntegratedStamina_2H_COMBO; // Breath cost in percent
var int IntegratedStamina_1H_PARADE; // Breath cost in percent
var int IntegratedStamina_1H_FIRSTHIT; // Breath cost in percent
var int IntegratedStamina_1H_COMBO; // Breath cost in percent
/*
* Check if an MDS overlay of given name is active
*/
func int Mdl_OverlayMdsIsActive(var C_Npc slf, var string mdsName){
var oCNpc npc; npc = Hlp_GetNpc(slf);
mdsName = STR_Upper(mdsName);
repeat(i, npc.activeOverlays_numInArray); var int i;
if (Hlp_StrCmp(mdsName, MEM_ReadStringArray(npc.activeOverlays_array, i))){
return TRUE;
};
end;
return FALSE;
};
/*
* Unfortunately Gothic 1 does not have the option bShowWeaponTrails, skip the entire function call here
*/
func void HideWeaponTrails(var int hide){
const int oCAniCtrl_Human__ShowWeaponTrail_G1 = 6452016; //0x627330
const int oCAniCtrl_Human__ShowWeaponTrail_G2 = 7011952; //0x6AFE70
var int addr; addr = MEMINT_SwitchG1G2(oCAniCtrl_Human__ShowWeaponTrail_G1, oCAniCtrl_Human__ShowWeaponTrail_G2);
var int byte; byte = MEM_ReadByte(addr);
if (hide){
if (byte == /*64*/100){
MemoryProtectionOverride(addr, 1);
MEM_WriteByte(addr, /*C3*/195);
};
} else if (byte == /*C3*/195){
MEM_WriteByte(addr, /*64*/100);
};
};
/*
* Decrease stamina on melee fight actions
*/
func void IntegratedStamina(){
var oCAniCtrl_Human ai; ai = _^(ESI);
var C_Npc slf; slf = _^(ai.npc);
// For player only
if (!Npc_IsPlayer(slf)){
return;
};
// Collect information about current action
var oCNpc npc; npc = _^(ai.npc);
var int fists; fists = (npc.fmode == FMODE_FIST);
var int twoHanded; twoHanded = (npc.fmode == 4);
var int start; start = ((ai.bitfield & /*endCombo*/2) != 0);
var int firstHit; firstHit = (start) && (truncf(ai.lastHitAniFrame) < 6); // Tolerance for non-combo animations
var int parade; parade = FALSE;
if (start){
ai.hitAniID = EAX;
if (ai.hitAniID != ai._t_hitl) && (ai.hitAniID != ai._t_hitr)
&& (ai.hitAniID != ai._t_hitf) && (ai.hitAniID != ai._t_hitfrun){
parade = TRUE;
// Return if already running (only the case for long jump back parades)
const int oCAniCtrl_Human__IsParadeRunning_G1 = 6456432; //0x628470
const int oCAniCtrl_Human__IsParadeRunning_G2 = 7017808; //0x6B1550
const int call = 0;
if (CALL_Begin(call)){
CALL__thiscall(_@(ESI), MEMINT_SwitchG1G2(oCAniCtrl_Human__IsParadeRunning_G1,
oCAniCtrl_Human__IsParadeRunning_G2));
call = CALL_End();
};
if (CALL_RetValAsInt()){
return;
};
};
};
// Determine decrement/cost
var int decr;
if (fists){
if (parade){ decr = IntegratedStamina_FIST_PARADE; }
else if (firstHit){ decr = IntegratedStamina_FIST_FIRSTHIT; }
else /*followHit*/{ decr = IntegratedStamina_FIST_COMBO; };
} else if (twoHanded){
if (parade){ decr = IntegratedStamina_2H_PARADE; }
else if (firstHit){ decr = IntegratedStamina_2H_FIRSTHIT; }
else /*followHit*/{ decr = IntegratedStamina_2H_COMBO; };
} else /*oneHanded*/{
if (parade){ decr = IntegratedStamina_1H_PARADE; }
else if (firstHit){ decr = IntegratedStamina_1H_FIRSTHIT; }
else /*followHit*/{ decr = IntegratedStamina_1H_COMBO; };
};
// Decrease breath or disable actions by animation
if (!Breath_RequestPercent(slf, decr, decr)){// End combo or disable melee
if (!(ai.bitfield & /*endCombo*/2)){
ai.bitfield = ai.bitfield | /*endCombo*/2;
} else if (!Mdl_OverlayMdsIsActive(slf, IntegratedStamina_Mds)){Mdl_ApplyOverlayMds(slf, IntegratedStamina_Mds);
};
// Temporarily disable all(!) weapon trails
HideWeaponTrails(TRUE);
} else if (Mdl_OverlayMdsIsActive(slf, IntegratedStamina_Mds)){// Back to normal
Mdl_RemoveOverlayMds(slf, IntegratedStamina_Mds);
HideWeaponTrails(FALSE);
};
};
/*
* Initialization function to be called from Init_Global
*/
func void IntegratedStamina_Init(){
const int oCAniCtrl_Human__StartHitCombo_G1 = 6452587; //0x62756B
const int oCAniCtrl_Human__StartHitCombo_G2 = 7012555; //0x6B00CB
const int oCAniCtrl_Human__HitCombo_next_G1 = 6453169; //0x6277B1
const int oCAniCtrl_Human__HitCombo_next_G2 = 7013146; //0x6B031A
HookEngineF(MEMINT_SwitchG1G2(oCAniCtrl_Human__StartHitCombo_G1,
oCAniCtrl_Human__StartHitCombo_G2), 8, IntegratedStamina);
HookEngineF(MEMINT_SwitchG1G2(oCAniCtrl_Human__HitCombo_next_G1,
oCAniCtrl_Human__HitCombo_next_G2), 8, IntegratedStamina);
};
Wäre es eigentlich auch möglich, den Verbrauch der Ausdauer beim Tauchen zu manipulieren? Das ist soweit ich weiß ja in der Engine festgelegt. Ich möchte beispielsweise gerne die gesamte Ausdauer durch zwei teilen (ist mit deinem Skript ja bereits problemlos möglich), das soll dann die Zeit in Millisekunden ergeben, die man unter Wasser bleiben kann, ohne Schaden zu erleiden. Also ähnlich wie bei deinem Sprint-Skript.
Wäre es eigentlich auch möglich, den Verbrauch der Ausdauer beim Tauchen zu manipulieren? Das ist soweit ich weiß ja in der Engine festgelegt. Ich möchte beispielsweise gerne die gesamte Ausdauer durch zwei teilen (ist mit deinem Skript ja bereits problemlos möglich), das soll dann die Zeit in Millisekunden ergeben, die man unter Wasser bleiben kann, ohne Schaden zu erleiden. Also ähnlich wie bei deinem Sprint-Skript.
Ich verstehe deine Absicht nicht ganz. Das hört sich an als hättest du gerade deine eigene Frage beantwortet. Wenn du den maximalen Atem halbierst (von 30 zu 15 Sekunden), dann kann der Spieler nur noch 15 Sekunden tauchen. Da der Spieler diese Zahl nie zu Gesicht bekommt (es sei denn du hast vor sie z.B. im Charaktermenü anzeigen zu lassen), kannst du alles andere einfach entsprechend skalieren.
Wahrscheinlich habe ich dich nicht vollständig verstanden. Könntest du das bitte noch etwas näher beschreiben mit konkreten Zahlen-Beispielen usw.?
Ich verstehe deine Absicht nicht ganz. Das hört sich an als hättest du gerade deine eigene Frage beantwortet. Wenn du den maximalen Atem halbierst (von 30 zu 15 Sekunden), dann kann der Spieler nur noch 15 Sekunden tauchen. Da der Spieler diese Zahl nie zu Gesicht bekommt (es sei denn du hast vor sie z.B. im Charaktermenü anzeigen zu lassen), kannst du alles andere einfach entsprechend skalieren.
Wahrscheinlich habe ich dich nicht vollständig verstanden. Könntest du das bitte noch etwas näher beschreiben mit konkreten Zahlen-Beispielen usw.?
Ja, du hast natürlich völlig recht. Ich hätte mir die Funktionsweise des Skripts erst mal genau anschauen sollen...
Das darstellen im Charaktermenü war auch überhaupt kein Problem.
Kleine Notiz: Das Integrated Stamina Skript funktioniert momentan nicht richtig für Gothic 1. Dort wird der "erste" Schlag nicht richtig ermittelt und das Animationsoverlay ist fehlerhaft. Das werde ich bei Zeiten beheben. Allerdings weiß ich noch nicht wann ich dazu komme. Das nur als Hinweis, falls jemand dieses Skript verwendet.
Ich habe alle drei hier geposteten Skripte überarbeitet.
In breath.d gab es nur Kleinigkeiten, in integratedSprint.d habe ich das Problem behoben, dass man mit gezogener Fernkampfwaffe nach dem Sprinten nicht schießen kann (dabei habe ich mich an dem Fix aus DirtySwamp von Draxes und Cryp18Struct orientiert - ich weiß aber nicht wo der ursprünglich herkommt), und die integratedStamina.d inkl. der Ressourcen habe ich aktualisiert, damit es nun auch in Gothic 1 vernünftig funktioniert.
Ich habe das Skript vom integratedSprint aktualisiert.
Nun wird eine gezogene Fernkampfwaffe nicht automatisch nach dem Sprinten weggesteckt, sondern ist direkt weiterhin nutzbar. Außerdem wird Springen und Fallen nicht mehr durch die Sprinttaste unterbrochen.
Falls jemand dieses Skript in seiner Mod eingebunden hat, empfehle ich es zu aktualisieren.
da ich die selben Bugs in meinem Patch hatte, habe ich direkt mal die gelegenheit genutzt und sie bei mir mit eingebaut + ein paar schönheits Operationen
Einen Fehler bekomme ich aber nicht behoben, wenn der Spieler tot ist und man Sprinten will, wird die Animation kurz gestartet, gleich darauf legt er sich aber wieder tot hin. Genau das will ich verhindern, bloß habe ich keine Ahnung welche Funktion ich dafür abfragen muss. Ich habe bis heute keine Funktions Referenz oder der gleichen finden können.
Weißt du wo es sowas gibt? Das würde mir das Leben 1000x vereinfachen
Ansonsten schau mal bei dir selber auch nach ob du den Bug hast, ich konnte jetzt nix eindeutiges in deinem Code finden.
Ich habe eine kleinen Flüchtigkeitsfehler im integratedSprint-Skript behoben. Nun kann man auch nach Sprinten mit gezogenenr Armbrust diese weiterhin nutzen.
Eigentlich hatte ich das dort wohl tatsächlich abgefragt. Jetzt habe ich dort aber einen kleinen Zahlendreher entdeckt. Der sollte unproblematisch sein und würde nur dafür sorgen, dass der Tauchbalken weiterhin dauerhaft angezeigt bliebe, würde man die Sichtbarkeit des Balkens während des Spiels ändern. Das werde ich korrigieren.
Das Skript im Einleitungspost (breath.d) ist aktualisiert.
die Abfrage bei func int Mdl_OverlayMdsIsActive greift leider nicht auf Mdl_ApplyOverlayMdsTimed zu, hast du dafür vielleicht bitte eine Lösung? Ich brauche diese Abfrage nämlich, weil ich Mdl_ApplyOverlayMdsTimed bei alkoholischen Getränken verwende, wo ich die Humans_Drunken.mds für bestimmte Zeit aktiviere und während diese aktiv ist, soll der Spieler nicht sprinten können.