forked from Mirrors/openclonk
514 lines
15 KiB
C
514 lines
15 KiB
C
/**
|
|
ClonkInteractionControl
|
|
Handles the Clonk's interaction with other objects.
|
|
|
|
*/
|
|
|
|
|
|
/*
|
|
used properties:
|
|
this.control.is_interacting: whether interaction is in progress (user is holding [space])
|
|
this.control.interaction_start_time: frame counter at the time of the selection process
|
|
this.control.interaction_hud_controller: hud object that takes the callbacks. Updated when starting interaction.
|
|
*/
|
|
|
|
|
|
public func Construction()
|
|
{
|
|
this.control.is_interacting = false;
|
|
return _inherited(...);
|
|
}
|
|
|
|
public func OnShiftCursor(object new_cursor)
|
|
{
|
|
if (this.control.is_interacting)
|
|
AbortInteract();
|
|
return _inherited(new_cursor, ...);
|
|
}
|
|
|
|
public func ObjectControl(int plr, int ctrl, int x, int y, int strength, bool repeat, int status)
|
|
{
|
|
if (!this)
|
|
return inherited(plr, ctrl, x, y, strength, repeat, status, ...);
|
|
|
|
// Begin interaction.
|
|
if (ctrl == CON_Interact && status == CONS_Down)
|
|
{
|
|
this->CancelUse();
|
|
BeginInteract();
|
|
return true;
|
|
}
|
|
|
|
// Switch object or finish interaction?
|
|
if (this.control.is_interacting)
|
|
{
|
|
// Stop picking up.
|
|
if (ctrl == CON_InteractNext_Stop)
|
|
{
|
|
AbortInteract();
|
|
return true;
|
|
}
|
|
|
|
// Finish picking up (aka "collect").
|
|
if (ctrl == CON_Interact && status == CONS_Up)
|
|
{
|
|
EndInteract();
|
|
return true;
|
|
}
|
|
|
|
// Switch left/right through objects.
|
|
var dir = nil;
|
|
if (ctrl == CON_InteractNext_Left) dir = -1;
|
|
else if (ctrl == CON_InteractNext_Right) dir = 1;
|
|
else if (ctrl == CON_InteractNext_CycleObject) dir = 0;
|
|
|
|
if (dir != nil)
|
|
{
|
|
var item = FindNextInteraction(this.control.interaction_hud_controller->GetCurrentInteraction(), dir);
|
|
if (item)
|
|
SetNextInteraction(item);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return inherited(plr, ctrl, x, y, strength, repeat, status, ...);
|
|
}
|
|
|
|
private func FxIntHighlightInteractionStart(object target, proplist fx, temp, proplist interaction, proplist interaction_help)
|
|
{
|
|
if (temp) return;
|
|
fx.obj = interaction.interaction_object;
|
|
fx.interaction = interaction;
|
|
fx.interaction_help = target.control.interaction_hud_controller->GetInteractionHelp(interaction, target);
|
|
|
|
fx.dummy = CreateObject(Dummy, fx.obj->GetX() - GetX(), fx.obj->GetY() - GetY(), GetOwner());
|
|
fx.dummy.ActMap =
|
|
{
|
|
Attach =
|
|
{
|
|
Name = "Attach",
|
|
Procedure = DFA_ATTACH,
|
|
FacetBase = 1
|
|
}
|
|
};
|
|
fx.dummy.Visibility = VIS_Owner;
|
|
// The selector's plane should be just behind the Clonk for stuff that usually is behind the Clonk.
|
|
// Otherwise, it looks rather odd when the catapult shines through the Clonk.
|
|
if (fx.obj.Plane < this.Plane)
|
|
{
|
|
fx.dummy.Plane = this.Plane - 1;
|
|
}
|
|
else
|
|
{
|
|
fx.dummy.Plane = 1000;
|
|
}
|
|
var multiple_interactions_hint = "";
|
|
if (fx.interaction.has_multiple_interactions)
|
|
multiple_interactions_hint = Format("<c 999999>[%s] $More$..</c>", GetPlayerControlAssignment(GetOwner(), CON_Up, true, false));
|
|
fx.dummy->Message("@<c eeffee>%s</c>|%s|", fx.interaction_help.help_text, multiple_interactions_hint);
|
|
|
|
// Center dummy!
|
|
fx.dummy->SetVertexXY(0, fx.obj->GetVertex(0, VTX_X), fx.obj->GetVertex(0, VTX_Y));
|
|
fx.dummy->SetAction("Attach", fx.obj);
|
|
|
|
fx.width = fx.obj->GetDefWidth();
|
|
fx.height = fx.obj->GetDefHeight();
|
|
|
|
// Draw the item's graphics in front of it again to achieve a highlighting effect.
|
|
fx.dummy->SetGraphics(nil, nil, 1, GFXOV_MODE_Object, nil, GFX_BLIT_Additive, fx.obj);
|
|
|
|
var custom_selector = nil;
|
|
if (fx.obj) custom_selector = fx.obj->~DrawCustomInteractionSelector(fx.dummy, target, fx.interaction.interaction_index, fx.interaction.extra_data);
|
|
|
|
if (!custom_selector)
|
|
{
|
|
fx.scheduled_selection_particle = (FrameCounter() - this.control.interaction_start_time) < 10;
|
|
if (!fx.scheduled_selection_particle)
|
|
EffectCall(nil, fx, "CreateSelectorParticle");
|
|
}
|
|
else
|
|
{
|
|
// Note that custom selectors are displayed immediately - particle because they might e.g. move the dummy.
|
|
fx.scheduled_selection_particle = false;
|
|
}
|
|
}
|
|
|
|
private func FxIntHighlightInteractionCreateSelectorParticle(object target, effect fx)
|
|
{
|
|
// Failsafe.
|
|
if (!fx.dummy) return;
|
|
|
|
// Draw a nice selector particle on item change.
|
|
var selector =
|
|
{
|
|
Size = PV_Step(5, 2, 1, Max(fx.width, fx.height)),
|
|
Attach = ATTACH_Front,
|
|
Rotation = PV_Step(1, PV_Random(0, 360), 1),
|
|
Alpha = 200
|
|
};
|
|
|
|
fx.dummy->CreateParticle("Selector", 0, 0, 0, 0, 0, Particles_Colored(selector, GetPlayerColor(GetOwner())), 1);
|
|
}
|
|
|
|
private func FxIntHighlightInteractionTimer(object target, proplist fx, int time)
|
|
{
|
|
if (!fx.dummy) return -1;
|
|
if (!fx.obj) return -1;
|
|
|
|
if (fx.scheduled_selection_particle && time > 10)
|
|
{
|
|
EffectCall(nil, fx, "CreateSelectorParticle");
|
|
fx.scheduled_selection_particle = false;
|
|
}
|
|
}
|
|
|
|
private func FxIntHighlightInteractionStop(object target, proplist fx, int reason, temp)
|
|
{
|
|
if (temp) return;
|
|
if (fx.dummy) fx.dummy->RemoveObject();
|
|
if (!this) return;
|
|
}
|
|
|
|
private func FxIntHighlightInteractionOnExecute(object target, proplist fx)
|
|
{
|
|
if (!fx.obj || !fx.dummy) return;
|
|
var message = fx.dummy->CreateObject(FloatingMessage, 0, 0, GetOwner());
|
|
message.Visibility = VIS_Owner;
|
|
message->SetMessage(Format("%s||", fx.interaction_help.help_text));
|
|
message->SetYDir(-10);
|
|
message->FadeOut(1, 20);
|
|
}
|
|
|
|
private func SetNextInteraction(proplist to)
|
|
{
|
|
// Clear all old markers.
|
|
var e = nil;
|
|
while (e = GetEffect("IntHighlightInteraction", this))
|
|
RemoveEffect(nil, this, e);
|
|
// And set & mark new one.
|
|
this.control.interaction_hud_controller->SetCurrentInteraction(to);
|
|
if (to)
|
|
AddEffect("IntHighlightInteraction", this, 1, 2, this, nil, to);
|
|
}
|
|
|
|
private func FindNextInteraction(proplist start_from, int x_dir)
|
|
{
|
|
var starting_object = this;
|
|
if (start_from && start_from.interaction_object)
|
|
starting_object = start_from.interaction_object;
|
|
var sort = Sort_Func("Library_ClonkInventoryControl_Sort_Priority", starting_object->GetX());
|
|
var interactions = GetInteractableObjects(sort);
|
|
var len = GetLength(interactions);
|
|
if (!len) return nil;
|
|
// Find object next to the current one.
|
|
// (note that index==-1 accesses the last element)
|
|
var index = -1;
|
|
// GetIndexOf does not use DeepEqual, so work around that here.
|
|
for (var i = 0; i < len; ++i)
|
|
{
|
|
if (!DeepEqual(start_from, interactions[i])) continue;
|
|
index = i;
|
|
break;
|
|
}
|
|
|
|
if (index != -1) // Previous item was found in the list.
|
|
{
|
|
var previous_interaction = interactions[index];
|
|
// Cycle interactions of same object (dir == 0).
|
|
// Or cycle through objects to the right (x_dir==1) or left (x_dir==-1).
|
|
var cycle_dir = x_dir;
|
|
var do_cycle_object = x_dir == 0;
|
|
if (do_cycle_object) cycle_dir = 1;
|
|
|
|
var found = false;
|
|
for (var i = 1; i < len; ++i)
|
|
{
|
|
index = (index + cycle_dir) % len;
|
|
if (index < 0) index += len;
|
|
var is_same_object = interactions[index].interaction_object == previous_interaction.interaction_object;
|
|
if (do_cycle_object == is_same_object)
|
|
{
|
|
found = true;
|
|
|
|
// When cycling to the left, make sure to arrive at the first interaction for that object (and not the last).
|
|
// Otherwise it's pretty unintuitive, why you sometimes grab and sometimes enter the catapult as the first interaction.
|
|
if (x_dir == -1)
|
|
{
|
|
// Fast forward to first interaction.
|
|
var target_object = interactions[index].interaction_object;
|
|
// It's guaranteed that the interactions are not split over the array borders. So we can just search until the index is 0.
|
|
for (var current_index = index - 1; current_index >= 0; --current_index)
|
|
{
|
|
if (interactions[current_index].interaction_object == target_object)
|
|
{
|
|
index = current_index;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) index = -1;
|
|
}
|
|
else
|
|
{
|
|
// Find highest priority item.
|
|
var high_prio = nil;
|
|
for (var i = 0; i < len; ++i)
|
|
{
|
|
var interaction = interactions[i];
|
|
if (high_prio != nil && interaction.priority <= high_prio.priority) continue;
|
|
high_prio = interaction;
|
|
index = i;
|
|
}
|
|
}
|
|
|
|
if (index == -1) return nil;
|
|
var next = interactions[index];
|
|
if (DeepEqual(next, start_from)) return nil;
|
|
return next;
|
|
}
|
|
|
|
private func BeginInteract()
|
|
{
|
|
this.control.interaction_hud_controller = this->GetHUDController();
|
|
this.control.is_interacting = true;
|
|
this.control.interaction_start_time = FrameCounter();
|
|
|
|
// Force update the HUD controller, which is responsible for pre-selecting the "best" object.
|
|
this.control.interaction_hud_controller->UpdateInteractionObject();
|
|
// Then, iff the HUD shows an object, pre-select one.
|
|
var interaction = this.control.interaction_hud_controller->GetCurrentInteraction();
|
|
if (interaction)
|
|
SetNextInteraction(interaction);
|
|
this.control.interaction_hud_controller->EnableInteractionUpdating(false);
|
|
}
|
|
|
|
// Stops interaction selection without executing the current selection.
|
|
private func AbortInteract()
|
|
{
|
|
this.control.interaction_hud_controller->SetCurrentInteraction(nil);
|
|
EndInteract();
|
|
}
|
|
|
|
private func EndInteract()
|
|
{
|
|
this.control.is_interacting = false;
|
|
|
|
var executed = false;
|
|
if (this.control.interaction_hud_controller->GetCurrentInteraction())
|
|
{
|
|
ExecuteInteraction(this.control.interaction_hud_controller->GetCurrentInteraction());
|
|
executed = true;
|
|
}
|
|
|
|
var e = nil;
|
|
while (e = GetEffect("IntHighlightInteraction", this))
|
|
{
|
|
if (executed)
|
|
EffectCall(this, e, "OnExecute");
|
|
RemoveEffect(nil, this, e);
|
|
}
|
|
|
|
this.control.interaction_hud_controller->SetCurrentInteraction(nil);
|
|
this.control.interaction_hud_controller->EnableInteractionUpdating(true);
|
|
}
|
|
|
|
/*
|
|
Wraps "PushBack", but also sets a flag when two interactions of the same object exist.
|
|
*/
|
|
private func PushBackInteraction(array to, proplist interaction)
|
|
{
|
|
PushBack(to, interaction);
|
|
var count = 0;
|
|
for (var other in to)
|
|
{
|
|
if (other.interaction_object && (other.interaction_object == interaction.interaction_object))
|
|
{
|
|
count += 1;
|
|
if (count > 1 || other != interaction)
|
|
other.has_multiple_interactions = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
returns an array containing proplists with informations about the interactable actions.
|
|
The proplist properties are:
|
|
interaction_object
|
|
priority: used for sorting the objects in the action bar. Note that the returned objects are not yet sorted
|
|
interaction_index: when an object has multiple defined interactions, this is the index
|
|
extra_data: custom extra_data for an interaction specified by the object
|
|
actiontype: the kind of interaction. One of the ACTIONTYPE_* constants
|
|
*/
|
|
func GetInteractableObjects(array sort)
|
|
{
|
|
var possible_interactions = [];
|
|
// find vehicles & structures & script interactables
|
|
// Get custom interactions from the clonk
|
|
// extra interactions are an array of proplists. proplists have to contain at least a function pointer "f", a description "desc" and an "icon" definition/object. Optional "front"-boolean for sorting in before/after other interactions.
|
|
var extra_interactions = this->~GetExtraInteractions() ?? []; // if not present, just use []. Less error prone than having multiple if(!foo).
|
|
|
|
// all except structures only if outside
|
|
var can_only_use_container = !!Contained();
|
|
|
|
// add extra-interactions
|
|
if (!can_only_use_container)
|
|
for(var interaction in extra_interactions)
|
|
{
|
|
PushBackInteraction(possible_interactions,
|
|
{
|
|
interaction_object = interaction.Object,
|
|
priority = interaction.Priority,
|
|
interaction_index = nil,
|
|
extra_data = interaction,
|
|
actiontype = ACTIONTYPE_EXTRA
|
|
});
|
|
}
|
|
// Make sure that the Clonk's action target is always shown.
|
|
// You can push a lorry out of your bounding box and would, otherwise, then be unable to release it.
|
|
var main_criterion = Find_AtRect(-5, -10, 10, 20);
|
|
var action_target = nil;
|
|
if (action_target = GetActionTarget())
|
|
{
|
|
main_criterion = Find_Or(main_criterion, Find_InArray([action_target]));
|
|
}
|
|
// add interactables (script interface)
|
|
var interactables = FindObjects(
|
|
main_criterion,
|
|
Find_Or(Find_OCF(OCF_Grab), Find_Func("IsInteractable", this), Find_OCF(OCF_Entrance)),
|
|
Find_NoContainer(), Find_Layer(GetObjectLayer()),
|
|
sort);
|
|
for(var interactable in interactables)
|
|
{
|
|
var icnt = interactable->~GetInteractionCount() ?? 1;
|
|
|
|
if (!can_only_use_container)
|
|
{
|
|
// first the script
|
|
// one object could have a scripted interaction AND be a vehicle
|
|
if (interactable->~IsInteractable(this))
|
|
for(var j = 0; j < icnt; j++)
|
|
{
|
|
PushBackInteraction(possible_interactions,
|
|
{
|
|
interaction_object = interactable,
|
|
priority = 9,
|
|
interaction_index = j,
|
|
extra_data = nil,
|
|
actiontype = ACTIONTYPE_SCRIPT
|
|
});
|
|
}
|
|
// check whether further interactions are possible
|
|
|
|
// can be grabbed? (vehicles/chests..)
|
|
if (interactable->GetOCF() & OCF_Grab)
|
|
{
|
|
var priority = 19;
|
|
// not if swimming because the grab command cannot really fix that (unlike e.g. scale/hangle)
|
|
if (GetProcedure() == "SWIM")
|
|
if (!this->~CanGrabUnderwater(interactable)) // unless it's a special clonk that can grab underwater. It needs to define a callback then.
|
|
continue;
|
|
// high priority if already grabbed
|
|
if (GetActionTarget() == interactable) priority = 0;
|
|
|
|
PushBackInteraction(possible_interactions,
|
|
{
|
|
interaction_object = interactable,
|
|
priority = priority,
|
|
interaction_index = nil,
|
|
extra_data = nil,
|
|
actiontype = ACTIONTYPE_VEHICLE
|
|
});
|
|
}
|
|
}
|
|
|
|
// can be entered?
|
|
if (interactable->GetOCF() & OCF_Entrance && (!can_only_use_container || interactable == Contained()))
|
|
{
|
|
var priority = 29;
|
|
if (Contained() == interactable) priority = 0;
|
|
PushBackInteraction(possible_interactions,
|
|
{
|
|
interaction_object = interactable,
|
|
priority = priority,
|
|
interaction_index = nil,
|
|
extra_data = nil,
|
|
actiontype = ACTIONTYPE_STRUCTURE
|
|
});
|
|
}
|
|
}
|
|
|
|
return possible_interactions;
|
|
}
|
|
|
|
// executes interaction with an object. /action_info/ is a proplist as returned by GetInteractableObjects
|
|
func ExecuteInteraction(proplist action_info)
|
|
{
|
|
if (!action_info.interaction_object)
|
|
return;
|
|
|
|
// object is a pushable vehicle
|
|
if(action_info.actiontype == ACTIONTYPE_VEHICLE)
|
|
{
|
|
var proc = GetProcedure();
|
|
// object is inside building -> activate
|
|
if(Contained() && action_info.interaction_object->Contained() == Contained())
|
|
{
|
|
SetCommand("Activate", action_info.interaction_object);
|
|
return true;
|
|
}
|
|
// crew is currently pushing vehicle
|
|
else if(proc == "PUSH")
|
|
{
|
|
// which is mine -> let go
|
|
if(GetActionTarget() == action_info.interaction_object)
|
|
ObjectCommand("UnGrab");
|
|
else
|
|
ObjectCommand("Grab", action_info.interaction_object);
|
|
|
|
return true;
|
|
}
|
|
// grab
|
|
else if(proc == "WALK")
|
|
{
|
|
ObjectCommand("Grab", action_info.interaction_object);
|
|
return true;
|
|
}
|
|
}
|
|
// object is a building
|
|
else if (action_info.actiontype == ACTIONTYPE_STRUCTURE)
|
|
{
|
|
// inside? -> exit
|
|
if(Contained() == action_info.interaction_object)
|
|
{
|
|
ObjectCommand("Exit");
|
|
return true;
|
|
}
|
|
// outside? -> enter
|
|
else if(this->CanEnter())
|
|
{
|
|
ObjectCommand("Enter", action_info.interaction_object);
|
|
return true;
|
|
}
|
|
}
|
|
else if (action_info.actiontype == ACTIONTYPE_SCRIPT)
|
|
{
|
|
if(action_info.interaction_object->~IsInteractable(this))
|
|
{
|
|
action_info.interaction_object->Interact(this, action_info.interaction_index);
|
|
return true;
|
|
}
|
|
}
|
|
else if (action_info.actiontype == ACTIONTYPE_EXTRA)
|
|
{
|
|
if(action_info.extra_data)
|
|
action_info.extra_data.Object->Call(action_info.extra_data.Fn, this);
|
|
}
|
|
}
|