openclonk/planet/Objects.ocd/Libraries.ocd/ClonkUseControl.ocd/Script.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;
}