forked from Mirrors/openclonk
482 lines
12 KiB
C
482 lines
12 KiB
C
/**
|
|
Clonk use controls
|
|
Author: Newton
|
|
|
|
Objects that inherit this object need to return _inherited(...) in the
|
|
following callbacks (if defined):
|
|
Construction, Departure,
|
|
Entrance, AttachTargetLost, CrewSelection, Death,
|
|
Destruction
|
|
|
|
The following callbacks are made to other objects:
|
|
*Use, *UseStop, *UseStart, *UseHolding, *UseCancel
|
|
|
|
Used properties
|
|
|
|
this.control.current_object: object that is being used at the moment
|
|
this.control.using_type: way of usage
|
|
this.control.mlastx: last x position of the cursor
|
|
this.control.mlasty: last y position of the cursor
|
|
this.control.noholdingcallbacks: whether to do HoldingUseControl callbacks
|
|
this.control.shelved_command: command (function) with condition that will be executed when the condition is met
|
|
used for example to re-call *Use/Throw commands when the Clonk finished scaling
|
|
|
|
|
|
wheras * is 'Contained' if the clonk is contained and otherwise (riding,
|
|
pushing, to self) it is 'Control'. The item in the inventory only gets
|
|
the Use*-calls. If the callback is handled, you should return true.
|
|
Currently, this is explained more in detail here:
|
|
http://forum.openclonk.org/topic_show.pl?tid=337
|
|
*/
|
|
|
|
/* ++++++++++++++++++++++++ Callbacks ++++++++++++++++++++++++ */
|
|
|
|
protected func Construction()
|
|
{
|
|
if (this.control == nil)
|
|
this.control = {};
|
|
|
|
this.control.current_object = nil;
|
|
this.control.using_type = nil;
|
|
this.control.shelved_command = nil;
|
|
|
|
return _inherited(...);
|
|
}
|
|
|
|
public func GetUsedObject()
|
|
{
|
|
return this.control.current_object;
|
|
}
|
|
|
|
// The using-command hast to be canceled if the clonk is entered into
|
|
// or exited from a building.
|
|
protected func Entrance()
|
|
{
|
|
CancelUse();
|
|
return _inherited(...);
|
|
}
|
|
|
|
|
|
protected func Departure()
|
|
{
|
|
CancelUse();
|
|
return _inherited(...);
|
|
}
|
|
|
|
// The same for vehicles
|
|
protected func AttachTargetLost()
|
|
{
|
|
CancelUse();
|
|
return _inherited(...);
|
|
}
|
|
|
|
// ...aaand the same for when the clonk is deselected
|
|
protected func CrewSelection(bool unselect)
|
|
{
|
|
if (unselect)
|
|
{
|
|
// cancel usage on unselect first...
|
|
CancelUse();
|
|
}
|
|
|
|
return _inherited(unselect, ...);
|
|
}
|
|
|
|
protected func Destruction()
|
|
{
|
|
// cancel usage...
|
|
CancelUse();
|
|
return _inherited(...);
|
|
}
|
|
|
|
protected func Death()
|
|
{
|
|
// cancel usage...
|
|
CancelUse();
|
|
return _inherited(...);
|
|
}
|
|
|
|
|
|
/* ++++++++++++++++++++++++ Shelved command ++++++++++++++++++++++++ */
|
|
|
|
public func ShelveCommand(object condition_obj, string condition, object callback_obj, string callback, proplist data)
|
|
{
|
|
this.control.shelved_command =
|
|
{
|
|
cond = condition,
|
|
cond_obj = condition_obj,
|
|
callback = callback,
|
|
callback_obj = callback_obj,
|
|
data = data
|
|
};
|
|
AddEffect("ShelvedCommand", this, 1, 5, this);
|
|
}
|
|
|
|
public func StopShelvedCommand()
|
|
{
|
|
this.control.shelved_command = nil;
|
|
if (GetEffect("ShelvedCommand", this))
|
|
RemoveEffect("ShelvedCommand", this);
|
|
}
|
|
|
|
func FxShelvedCommandTimer(_, effect, time)
|
|
{
|
|
if (!this.control.shelved_command) return -1;
|
|
if (!this.control.shelved_command.callback_obj) return -1;
|
|
if (!this.control.shelved_command.cond_obj) return -1;
|
|
if (!this.control.shelved_command.cond_obj->Call(this.control.shelved_command.cond, this.control.shelved_command.data)) return 1;
|
|
this.control.shelved_command.callback_obj->Call(this.control.shelved_command.callback, this.control.shelved_command.data);
|
|
return -1;
|
|
}
|
|
|
|
func FxShelvedCommandStop(target, effect, reason, temp)
|
|
{
|
|
if (temp)
|
|
return;
|
|
this.control.shelved_command = nil;
|
|
}
|
|
|
|
|
|
/* ++++++++++++++++++++++++ Use Controls ++++++++++++++++++++++++ */
|
|
|
|
public func CancelUse()
|
|
{
|
|
if (!GetUsedObject())
|
|
{
|
|
// just forget any possibly stored actions
|
|
StopShelvedCommand();
|
|
return;
|
|
}
|
|
|
|
// use the saved x,y coordinates for canceling
|
|
CancelUseControl(this.control.mlastx, this.control.mlasty);
|
|
}
|
|
|
|
// to be called during usage of an object to re-start usage as soon as possible
|
|
func PauseUse(object obj, string custom_condition, proplist data)
|
|
{
|
|
// cancel use first, since it removes old shelved commands
|
|
if (this.control.started_use)
|
|
{
|
|
CancelUse();
|
|
this.control.started_use = false;
|
|
}
|
|
|
|
var callback_obj = this;
|
|
|
|
if (custom_condition != nil)
|
|
{
|
|
callback_obj = obj;
|
|
}
|
|
else
|
|
{
|
|
custom_condition = "CanReIssueCommand";
|
|
}
|
|
|
|
data = data ?? {};
|
|
data.obj = obj;
|
|
data.ctrl = CON_Use;
|
|
ShelveCommand(callback_obj, custom_condition, this, "ReIssueCommand", data);
|
|
}
|
|
|
|
func DetermineUsageType(object obj)
|
|
{
|
|
if (!obj)
|
|
return nil;
|
|
// house
|
|
if (obj == Contained())
|
|
return C4D_Structure;
|
|
// object
|
|
if (obj->Contained() == this)
|
|
return C4D_Object;
|
|
// vehicle
|
|
var proc = GetProcedure();
|
|
if (obj == GetActionTarget())
|
|
if (proc == "ATTACH" && proc == "PUSH")
|
|
return C4D_Vehicle;
|
|
// unknown
|
|
return nil;
|
|
}
|
|
|
|
func GetUseCallString(string action)
|
|
{
|
|
// Control... or Contained...
|
|
var control_string = "Control";
|
|
if (this.control.using_type == C4D_Structure)
|
|
control_string = "Contained";
|
|
// ..Use.. or ..UseAlt...
|
|
var estr = "";
|
|
if (this.control.alt && this.control.using_type != C4D_Object)
|
|
estr = "Alt";
|
|
// Action
|
|
if (!action)
|
|
action = "";
|
|
return Format("~%sUse%s%s", control_string, estr, action);
|
|
}
|
|
|
|
func CanReIssueCommand(proplist data)
|
|
{
|
|
if (!data.obj)
|
|
return false;
|
|
|
|
if (data.ctrl == CON_Use)
|
|
return !data.obj->~RejectUse(this);
|
|
}
|
|
|
|
func ReIssueCommand(proplist data)
|
|
{
|
|
if (data.ctrl == CON_Use)
|
|
return StartUseControl(data.ctrl, this.control.mlastx, this.control.mlasty, data.obj);
|
|
}
|
|
|
|
func StartUseControl(int ctrl, int x, int y, object obj)
|
|
{
|
|
this.control.started_use = false;
|
|
|
|
if (obj->~RejectUse(this))
|
|
{
|
|
// remember for later:
|
|
PauseUse(obj);
|
|
// but still catch command
|
|
return true;
|
|
}
|
|
|
|
// Disable climb/hangle actions for the duration of this use
|
|
if (obj.ForceFreeHands && !GetEffect("IntControlFreeHands", this))
|
|
AddEffect("IntControlFreeHands", this, 130, 0, this);
|
|
|
|
obj->SetController(GetController());
|
|
this.control.current_object = obj;
|
|
this.control.using_type = DetermineUsageType(obj);
|
|
this.control.alt = ctrl != CON_Use;
|
|
|
|
if (this->~HasVirtualCursor())
|
|
{
|
|
var cursor = this->~VirtualCursor(), angle;
|
|
if (!cursor->IsActive() && (angle = obj->~DefaultCrosshairAngle(this, GetDir() * 2 - 1)))
|
|
{
|
|
x = +Sin(angle, CURSOR_Radius, 10);
|
|
y = -Cos(angle, CURSOR_Radius, 10);
|
|
}
|
|
cursor->StartAim(this, angle);
|
|
}
|
|
|
|
var hold_enabled = obj->Call("~HoldingEnabled");
|
|
|
|
if (hold_enabled)
|
|
SetPlayerControlEnabled(GetOwner(), CON_Aim, true);
|
|
|
|
// first call UseStart. If unhandled, call Use (mousecontrol)
|
|
var handled = obj->Call(GetUseCallString("Start"), this, x, y);
|
|
if (!handled)
|
|
{
|
|
handled = obj->Call(GetUseCallString(), this, x, y);
|
|
this.control.noholdingcallbacks = handled;
|
|
}
|
|
else
|
|
{
|
|
// *Start was handled. So clean up possible old noholdingcallbacks-values.
|
|
this.control.noholdingcallbacks = false;
|
|
}
|
|
|
|
if (!handled)
|
|
{
|
|
this.control.current_object = nil;
|
|
this.control.using_type = nil;
|
|
if (hold_enabled)
|
|
SetPlayerControlEnabled(GetOwner(), CON_Aim, false);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
this.control.started_use = true;
|
|
// add helper effect that prevents errors when objects are suddenly deleted by quickly cancelling their use beforehand
|
|
AddEffect("ItemRemovalCheck", GetUsedObject(), 1, 100, this, nil); // the slow timer is arbitrary and will just clean up the effect if necessary
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
func CancelUseControl(int x, int y)
|
|
{
|
|
// forget possibly stored commands
|
|
StopShelvedCommand();
|
|
|
|
// to horse first (if there is one)
|
|
var horse = GetActionTarget();
|
|
if (horse && GetProcedure() == "ATTACH" && GetUsedObject() != horse)
|
|
StopUseControl(x, y, horse, true);
|
|
|
|
return StopUseControl(x, y, GetUsedObject(), true);
|
|
}
|
|
|
|
func StopUseControl(int x, int y, object obj, bool cancel)
|
|
{
|
|
var stop = "Stop";
|
|
if (cancel)
|
|
stop = "Cancel";
|
|
|
|
// ControlUseStop, ControlUseAltStop, ContainedUseAltStop, ContainedUseCancel, etc...
|
|
var handled = false;
|
|
if (obj)
|
|
{
|
|
handled = obj->Call(GetUseCallString(stop), this, x, y);
|
|
}
|
|
|
|
if (obj == GetUsedObject())
|
|
{
|
|
// if ControlUseStop returned -1, the current object is kept as "used object"
|
|
// but no more callbacks except for ControlUseCancel are made. The usage of this
|
|
// object is finally cancelled on ControlUseCancel.
|
|
if (cancel || handled != -1)
|
|
{
|
|
// look for correct removal helper effect and remove it
|
|
var effect_index = 0;
|
|
var removal_helper = nil;
|
|
do
|
|
{
|
|
removal_helper = GetEffect("ItemRemovalCheck", GetUsedObject(), effect_index++);
|
|
if (!removal_helper)
|
|
break;
|
|
if (removal_helper.CommandTarget != this)
|
|
continue;
|
|
break;
|
|
} while (true);
|
|
|
|
RemoveEffect("IntControlFreeHands", this); // make sure we can climb again
|
|
|
|
this.control.current_object = nil;
|
|
this.control.using_type = nil;
|
|
this.control.alt = false;
|
|
|
|
if (removal_helper)
|
|
{
|
|
RemoveEffect(nil, nil, removal_helper, true);
|
|
}
|
|
}
|
|
this.control.noholdingcallbacks = false;
|
|
|
|
SetPlayerControlEnabled(GetOwner(), CON_Aim, false);
|
|
|
|
if (this->~HasVirtualCursor())
|
|
this->~VirtualCursor()->StopAim();
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
func HoldingUseControl(int ctrl, int x, int y, object obj)
|
|
{
|
|
var mex = x;
|
|
var mey = y;
|
|
|
|
//Message("%d,%d",this,mex,mey);
|
|
|
|
// automatic adjustment of the direction
|
|
// --------------------
|
|
// if this is desired for ALL objects is the question, we will find out.
|
|
// For now, this is done for items and vehicles, not for buildings and
|
|
// mounts (naturally). If turning vehicles just like that without issuing
|
|
// a turning animation is the question. But after all, the clonk can turn
|
|
// by setting the dir too.
|
|
|
|
|
|
// not riding and not in building not while scaling
|
|
if (GetProcedure() != "ATTACH" && !Contained() && GetProcedure() != "SCALE")
|
|
{
|
|
// pushing vehicle: object to turn is the vehicle
|
|
var dir_obj = GetActionTarget();
|
|
if (GetProcedure() != "PUSH")
|
|
dir_obj = nil;
|
|
|
|
// otherwise, turn the clonk
|
|
if (!dir_obj)
|
|
dir_obj = this;
|
|
|
|
if ((dir_obj->GetComDir() == COMD_Stop && dir_obj->GetXDir() == 0) || dir_obj->GetProcedure() == "FLIGHT")
|
|
{
|
|
if (dir_obj->GetDir() == DIR_Left)
|
|
{
|
|
if (mex > 0)
|
|
dir_obj->SetDir(DIR_Right);
|
|
}
|
|
else
|
|
{
|
|
if (mex < 0)
|
|
dir_obj->SetDir(DIR_Left);
|
|
}
|
|
}
|
|
}
|
|
|
|
var handled = obj->Call(GetUseCallString("Holding"), this, mex, mey);
|
|
|
|
return handled;
|
|
}
|
|
|
|
// very infrequent timer to prevent dangling effects, this is not necessary for correct functioning
|
|
func FxItemRemovalCheckTimer(object target, proplist effect, int time)
|
|
{
|
|
if (!effect.CommandTarget)
|
|
return -1;
|
|
if (effect.CommandTarget->~GetUsedObject() != target)
|
|
return -1;
|
|
return 1;
|
|
}
|
|
|
|
// this will be called when an inventory object (that is in use) is suddenly removed
|
|
func FxItemRemovalCheckStop(object target, proplist effect, int reason, bool temporary)
|
|
{
|
|
if (temporary)
|
|
return;
|
|
// only trigger when the object has been removed
|
|
if (reason != FX_Call_RemoveClear)
|
|
return;
|
|
// safety
|
|
if (!effect.CommandTarget)
|
|
return;
|
|
if (effect.CommandTarget->~GetUsedObject() != target)
|
|
return;
|
|
// quickly cancel use in a clean way while the object is still available
|
|
effect.CommandTarget->CancelUse();
|
|
return;
|
|
}
|
|
|
|
|
|
// Control use redirected to script
|
|
func ControlUse2Script(int ctrl, int x, int y, int strength, bool repeat, int status, object obj)
|
|
{
|
|
if (ctrl == CON_Use || ctrl == CON_UseAlt)
|
|
{
|
|
// cancel usage if a menu pops up
|
|
if (this->~GetMenu())
|
|
{
|
|
CancelUse();
|
|
return true;
|
|
}
|
|
|
|
// standard use
|
|
if (status == CONS_Down && !repeat)
|
|
{
|
|
return StartUseControl(ctrl, x, y, obj);
|
|
}
|
|
else if (status == CONS_Up && (obj == GetUsedObject() || obj == GetActionTarget()))
|
|
{
|
|
return StopUseControl(x, y, obj);
|
|
}
|
|
|
|
// more use (holding)
|
|
if (status == CONS_Up)
|
|
{
|
|
// leftover use release
|
|
CancelUse();
|
|
return true;
|
|
}
|
|
else if (status == CONS_Down && repeat && !this.control.noholdingcallbacks)
|
|
{
|
|
return HoldingUseControl(ctrl, x, y, obj);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|