forked from Mirrors/openclonk
513 lines
14 KiB
C
513 lines
14 KiB
C
/**
|
|
ClonkInventoryControl
|
|
Handles the Clonk's interaction with the inventory.
|
|
|
|
*/
|
|
|
|
|
|
/*
|
|
used properties:
|
|
this.inventory.is_picking_up: whether currently picking up
|
|
this.inventory.quick_slot: slot that is currently selected for quick switching
|
|
this.inventory.hotkey_down: the number of a hotkey being held down
|
|
this.inventory.quick_slot_switched: true if the quick slot was switched during pressing down of a hotkey, the hotkey release will do nothing
|
|
this.inventory.slots_switched: true if two inventory were switched (pressed a hotkey while another one was pressed); the hotkey release will do nothing
|
|
|
|
other used properties of "this.inventory" might have been declared in Inventory.ocd
|
|
*/
|
|
|
|
|
|
func Construction()
|
|
{
|
|
if(this.inventory == nil)
|
|
this.inventory = {};
|
|
this.inventory.quick_slot = 1;
|
|
this.inventory.hotkey_down = nil;
|
|
return _inherited(...);
|
|
}
|
|
|
|
public func OnShiftCursor(object new_cursor)
|
|
{
|
|
if (this.control.is_interacting)
|
|
AbortPickingUp();
|
|
return _inherited(new_cursor, ...);
|
|
}
|
|
|
|
public func GetQuickSwitchSlot()
|
|
{
|
|
return this.inventory.quick_slot;
|
|
}
|
|
|
|
// Called by other libraries and objects when the Clonk has forcefully dropped (not thrown) an object.
|
|
func OnDropped(object obj)
|
|
{
|
|
return _inherited(obj, ...);
|
|
}
|
|
|
|
func RejectCollect(id objid, object obj)
|
|
{
|
|
var rejected = _inherited(objid, obj, ...);
|
|
if(rejected) return rejected;
|
|
|
|
// Allow collection only if called via clonk->Collect, to prevent picking up stuff on the ground.
|
|
// Make an exception for containers, though.
|
|
if (!this.inventory.force_collection && !obj->Contained()) return true;
|
|
return false;
|
|
}
|
|
|
|
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, ...);
|
|
|
|
// Quickswitch changes the current active inventory slot
|
|
if (ctrl == CON_QuickSwitch && status == CONS_Down)
|
|
{
|
|
// but ignore quickswitch if we have more than 1 hand-slot
|
|
if(this.HandObjects > 1)
|
|
return inherited(plr, ctrl, x, y, strength, repeat, status, ...);;
|
|
|
|
// A number key (hotkey) is pressed, change quick switch slot
|
|
/*if (this.inventory.hotkey_down != nil)
|
|
{
|
|
if (SetQuickSwitchSlot(this.inventory.hotkey_down-1))
|
|
this.inventory.quick_slot_switched = true;
|
|
return true;
|
|
}*/
|
|
// Otherwise select slot
|
|
SetHandItemPos(0, this.inventory.quick_slot); // quick_slot is updated in SetHandItemPos
|
|
return true;
|
|
}
|
|
if (ctrl == CON_QuickSwitch && status == CONS_Up) // Do nothing for now but will be used in the future
|
|
{
|
|
return true;
|
|
}
|
|
// Collection and dropping is only allowed when the Clonk is not contained.
|
|
if (!Contained())
|
|
{
|
|
// Quick-pickup item via click? Note that this relies on being executed after the normal Clonk controls
|
|
if (ctrl == CON_Use && !this->GetHandItem(0) && status == CONS_Down)
|
|
{
|
|
var sort = Sort_Distance(x, y);
|
|
var items = FindAllPickupItems(sort);
|
|
for (var item in items)
|
|
{
|
|
if (item && TryToCollect(item)) return true;
|
|
}
|
|
}
|
|
|
|
// Begin picking up objects.
|
|
if (ctrl == CON_PickUp && status == CONS_Down)
|
|
{
|
|
this->CancelUse();
|
|
BeginPickingUp();
|
|
return true;
|
|
}
|
|
|
|
// Drop the mouse item?
|
|
if (ctrl == CON_Drop && status == CONS_Down)
|
|
{
|
|
// Do not immediately collect another thing unless chosen with left/right.
|
|
if (this.inventory.is_picking_up)
|
|
{
|
|
SetNextPickupItem(nil);
|
|
}
|
|
|
|
var item = this->GetHandItem(0);
|
|
if (item)
|
|
this->DropInventoryItem(this->GetHandItemPos(0));
|
|
return true;
|
|
}
|
|
|
|
|
|
// Switching pickup object or finish pickup?
|
|
if (this.inventory.is_picking_up)
|
|
{
|
|
// Stop picking up.
|
|
if (ctrl == CON_PickUpNext_Stop)
|
|
{
|
|
AbortPickingUp();
|
|
return true;
|
|
}
|
|
|
|
// Quickly pick up everything possible.
|
|
if (ctrl == CON_PickUpNext_All)
|
|
{
|
|
PickUpAll();
|
|
AbortPickingUp();
|
|
return true;
|
|
}
|
|
|
|
// Finish picking up (aka "collect").
|
|
if (ctrl == CON_PickUp && status == CONS_Up)
|
|
{
|
|
EndPickingUp();
|
|
return true;
|
|
}
|
|
|
|
// Switch left/right through objects.
|
|
var dir = nil;
|
|
if (ctrl == CON_PickUpNext_Left) dir = -1;
|
|
else if (ctrl == CON_PickUpNext_Right) dir = 1;
|
|
|
|
if (dir != nil)
|
|
{
|
|
var item = FindNextPickupObject(this.inventory.pickup_item, dir);
|
|
if (item)
|
|
SetNextPickupItem(item);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else // Contained
|
|
{
|
|
// If we are contained, have a picking up process running, and issue another command we first stop the selection.
|
|
if (this.inventory.is_picking_up) AbortPickingUp();
|
|
}
|
|
|
|
// shift inventory
|
|
var inventory_shift = 0;
|
|
if (ctrl == CON_InventoryShiftForward) inventory_shift = 1;
|
|
else if (ctrl == CON_InventoryShiftBackward) inventory_shift = -1;
|
|
|
|
if (inventory_shift)
|
|
{
|
|
var current = (this->GetHandItemPos(0) + inventory_shift) % this.MaxContentsCount;
|
|
if (current < 0) current = this.MaxContentsCount + current;
|
|
this->SetHandItemPos(0, current);
|
|
return true;
|
|
}
|
|
|
|
var hot;
|
|
|
|
// dropping items via hotkey
|
|
hot = 0;
|
|
if (ctrl == CON_DropHotkey0) hot = 10;
|
|
if (ctrl == CON_DropHotkey1) hot = 1;
|
|
if (ctrl == CON_DropHotkey2) hot = 2;
|
|
if (ctrl == CON_DropHotkey3) hot = 3;
|
|
if (ctrl == CON_DropHotkey4) hot = 4;
|
|
if (ctrl == CON_DropHotkey5) hot = 5;
|
|
if (ctrl == CON_DropHotkey6) hot = 6;
|
|
if (ctrl == CON_DropHotkey7) hot = 7;
|
|
if (ctrl == CON_DropHotkey8) hot = 8;
|
|
if (ctrl == CON_DropHotkey9) hot = 9;
|
|
|
|
if (hot > 0)
|
|
{
|
|
// Do not stop picking-up but nullify object selection.
|
|
// That way, you can still drop during pickup, but you won't automatically pickup stuff when you try to drop.
|
|
if (this.inventory.is_picking_up)
|
|
{
|
|
SetNextPickupItem(nil);
|
|
}
|
|
|
|
this->~DropInventoryItem(hot-1);
|
|
return true;
|
|
}
|
|
|
|
// inventory
|
|
hot = 0;
|
|
if (ctrl == CON_Hotkey0) hot = 10;
|
|
if (ctrl == CON_Hotkey1) hot = 1;
|
|
if (ctrl == CON_Hotkey2) hot = 2;
|
|
if (ctrl == CON_Hotkey3) hot = 3;
|
|
if (ctrl == CON_Hotkey4) hot = 4;
|
|
if (ctrl == CON_Hotkey5) hot = 5;
|
|
if (ctrl == CON_Hotkey6) hot = 6;
|
|
if (ctrl == CON_Hotkey7) hot = 7;
|
|
if (ctrl == CON_Hotkey8) hot = 8;
|
|
if (ctrl == CON_Hotkey9) hot = 9;
|
|
|
|
// another hotkey is already pressed
|
|
if (this.inventory.hotkey_down != nil && hot > 0 && hot <= this.MaxContentsCount && this.inventory.hotkey_down != hot)
|
|
{
|
|
// do nothing if this is just key down
|
|
if (status == CONS_Down)
|
|
return true;
|
|
// switch the two slots
|
|
this->~Switch2Items(this.inventory.hotkey_down-1, hot-1);
|
|
//this.inventory.slots_switched = true;
|
|
// This needs some explanation:
|
|
// In the event of the Clonk window ever losing focus, a hotkey might still be registered as being held down.
|
|
// If this was ever the case, the inventory would constantly switch around unless the exact same hotkey is pressed
|
|
// again to trigger a release. This could very well confuse players as it is not obvious which key needs to be
|
|
// pressed. With this, there will only be one switch and afterwards the inventory works just fine.
|
|
// The downside is that after one switch a hotkey has be pressed again for another switch.
|
|
this.inventory.hotkey_down = nil;
|
|
|
|
return true;
|
|
}
|
|
|
|
// hotkey up: perform slot selection
|
|
if (hot > 0 && hot <= this.MaxContentsCount && status == CONS_Up)
|
|
{
|
|
// This wasn't liked by many players, so slot selection is back to key down.
|
|
|
|
// Only perform slot selection if nothing happened in the meantime
|
|
/*if (!this.inventory.quick_slot_switched)
|
|
if (!this.inventory.slots_switched)
|
|
SetHandItemPos(0, hot-1);*/
|
|
|
|
this.inventory.hotkey_down = nil;
|
|
//this.inventory.quick_slot_switched = false;
|
|
//this.inventory.slots_switched = false;
|
|
|
|
return true;
|
|
}
|
|
// a hotkey is pressed, save it for now
|
|
if (hot > 0 && hot <= this.MaxContentsCount && status == CONS_Down)
|
|
{
|
|
this.inventory.hotkey_down = hot;
|
|
// For safety
|
|
//this.inventory.quick_slot_switched = false;
|
|
//this.inventory.slots_switched = false;
|
|
|
|
SetHandItemPos(0, hot-1);
|
|
return true;
|
|
}
|
|
|
|
return inherited(plr, ctrl, x, y, strength, repeat, status, ...);
|
|
}
|
|
|
|
private func FxIntHighlightItemStart(object target, proplist fx, temp, object item)
|
|
{
|
|
if (temp) return;
|
|
fx.item = item;
|
|
|
|
fx.dummy = CreateObject(Dummy, item->GetX() - GetX(), item->GetY() - GetY(), GetOwner());
|
|
fx.dummy.ActMap =
|
|
{
|
|
Attach =
|
|
{
|
|
Name = "Attach",
|
|
Procedure = DFA_ATTACH,
|
|
FacetBase = 1
|
|
}
|
|
};
|
|
fx.dummy.Visibility = VIS_Owner;
|
|
fx.dummy.Plane = 1000;
|
|
fx.dummy->Message("@%s", item->GetName());
|
|
|
|
// Center dummy!
|
|
fx.dummy->SetVertexXY(0, item->GetVertex(0, VTX_X), item->GetVertex(0, VTX_Y));
|
|
fx.dummy->SetAction("Attach", item);
|
|
|
|
fx.width = item->GetDefWidth();
|
|
fx.height = item->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, item);
|
|
|
|
// Custom selector particle if object says so
|
|
if (item->~PickupHighlight(fx.dummy, fx.width, fx.height, GetOwner()))
|
|
return;
|
|
|
|
// Draw a nice selector particle on item change.
|
|
var selector =
|
|
{
|
|
Size = PV_Step(3, 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 FxIntHighlightItemTimer(object target, proplist fx, int time)
|
|
{
|
|
if (!fx.dummy) return -1;
|
|
if (!fx.item) return -1;
|
|
if (ObjectDistance(this, fx.item) > 20) return -1;
|
|
if (fx.item->Contained()) return -1;
|
|
}
|
|
|
|
private func FxIntHighlightItemStop(object target, proplist fx, int reason, temp)
|
|
{
|
|
if (temp) return;
|
|
if (fx.dummy) fx.dummy->RemoveObject();
|
|
if (!this) return;
|
|
if (fx.item == this.inventory.pickup_item)
|
|
this.inventory.pickup_item = nil;
|
|
}
|
|
|
|
private func SetNextPickupItem(object to)
|
|
{
|
|
// Clear all old markers.
|
|
var e = nil;
|
|
while (e = GetEffect("IntHighlightItem", this))
|
|
RemoveEffect(nil, this, e);
|
|
// And set & mark new one.
|
|
this.inventory.pickup_item = to;
|
|
if (to)
|
|
AddEffect("IntHighlightItem", this, 1, 2, this, nil, to);
|
|
}
|
|
|
|
private func FindAllPickupItems(sorting_criterion)
|
|
{
|
|
return FindObjects(Find_Distance(20), Find_NoContainer(), Find_Property("Collectible"), Find_Layer(this->GetObjectLayer()), Find_Not(Find_OCF(OCF_HitSpeed1)), sorting_criterion);
|
|
}
|
|
|
|
private func FindNextPickupObject(object start_from, int x_dir)
|
|
{
|
|
if (!start_from) start_from = this;
|
|
// Returns objects sorted by ascending x-position, with all objects right of the clonk being sorted before objects left of the clonk
|
|
var sort = Sort_Func("Library_ClonkInventoryControl_Sort_Priority", start_from->GetX());
|
|
var objects = FindAllPickupItems(sort);
|
|
var len = GetLength(objects);
|
|
if (!len) return nil;
|
|
// Find object next to the current one.
|
|
// (note that index==-1 accesses the last element)
|
|
var index = GetIndexOf(objects, start_from);
|
|
if (index != -1)
|
|
{
|
|
// Previous item was found in the list.
|
|
// Cycle through list to the right (x_dir==1) or left (x_dir==-1)
|
|
index = (index + x_dir) % len;
|
|
}
|
|
else
|
|
{
|
|
// Previous item is no longer in range or there was no previous item - start with item closest to clonk position
|
|
// Going only in view direction may sound intuitive, but is weird in practice when you're standing just on top
|
|
// of an object that is 1px behind you and it selects an item very far away instead
|
|
// The most intuitive seems to pre-select the item closest to the clonk hands (also e.g. when scaling),
|
|
// so use distance from object center.
|
|
index += (ObjectDistance(objects[-1]) > ObjectDistance(objects[0]));
|
|
}
|
|
var next = objects[index];
|
|
if (next == start_from) return nil;
|
|
return next;
|
|
}
|
|
|
|
private func BeginPickingUp()
|
|
{
|
|
this.inventory.is_picking_up = true;
|
|
|
|
var obj = FindNextPickupObject(this, 0);
|
|
if (obj)
|
|
SetNextPickupItem(obj);
|
|
}
|
|
|
|
// Ends the pickup process without actually doing anything.
|
|
private func AbortPickingUp()
|
|
{
|
|
this.inventory.pickup_item = nil;
|
|
EndPickingUp();
|
|
}
|
|
|
|
private func EndPickingUp()
|
|
{
|
|
this.inventory.is_picking_up = false;
|
|
|
|
if (this.inventory.pickup_item)
|
|
{
|
|
TryToCollect(this.inventory.pickup_item);
|
|
}
|
|
|
|
var e = nil;
|
|
while (e = GetEffect("IntHighlightItem", this))
|
|
{
|
|
RemoveEffect(nil, this, e);
|
|
}
|
|
|
|
this.inventory.pickup_item = nil;
|
|
}
|
|
|
|
// Shows an effect when you picked up an item.
|
|
private func TryToCollect(object item)
|
|
{
|
|
// Remember stuff for a possible message - the item might have removed itself later.
|
|
var x = item->GetX();
|
|
var y = item->GetY();
|
|
var name = item->GetName();
|
|
|
|
// When pushing a lorry, try to directly collect it into the lorry first.
|
|
var vehicle = GetActionTarget();
|
|
if (vehicle && vehicle->~IsContainer() && GetProcedure() == "PUSH")
|
|
{
|
|
vehicle->Collect(item);
|
|
}
|
|
|
|
// Otherwise, try to collect the item myself.
|
|
if (item && !item->Contained())
|
|
Collect(item);
|
|
|
|
// If anything happened, assume collection.
|
|
if (!item || item->Contained())
|
|
{
|
|
var message = CreateObject(FloatingMessage, AbsX(x), AbsY(y), GetOwner());
|
|
message.Visibility = VIS_Owner;
|
|
message->SetMessage(name);
|
|
message->SetYDir(-10);
|
|
message->FadeOut(1, 20);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Pick up all objects in the vicinity.
|
|
private func PickUpAll()
|
|
{
|
|
// First try to find objects with the ID of the currently selected object.
|
|
var preferred_id = nil;
|
|
if (this.inventory.pickup_item != nil)
|
|
preferred_id = this.inventory.pickup_item->GetID();
|
|
|
|
var sort = Sort_Distance();
|
|
|
|
var all_objects = FindAllPickupItems(sort);
|
|
// Try the preferred id first.
|
|
if (preferred_id)
|
|
for (var obj in all_objects)
|
|
{
|
|
if (!obj || obj->Contained()) continue;
|
|
if (obj->GetID() != preferred_id) continue;
|
|
TryToCollect(obj);
|
|
}
|
|
// And try the others now..
|
|
for (var obj in all_objects)
|
|
{
|
|
if (!obj || obj->Contained()) continue;
|
|
TryToCollect(obj);
|
|
}
|
|
}
|
|
|
|
// Used in Inventory.ocd
|
|
public func SetHandItemPos(int hand, int inv)
|
|
{
|
|
// Save the current slot as the last slot only for the first hand
|
|
// and if the inventory slot actually changes.
|
|
if (hand == 0 && this->GetHandItemPos(0) != inv)
|
|
this.inventory.quick_slot = this->GetHandItemPos(0);
|
|
|
|
return _inherited(hand, inv, ...);
|
|
}
|
|
|
|
public func SetQuickSwitchSlot(int slot)
|
|
{
|
|
// Do not set if the quick switch slot doesn't change
|
|
if (slot == this.inventory.quick_slot) return false;
|
|
// Do not set if slot is currently selected
|
|
if (slot == this->GetHandItemPos(0)) return false;
|
|
|
|
this.inventory.quick_slot = slot;
|
|
// Notify HUD
|
|
this->~OnInventoryChange();
|
|
this->~UpdateAttach();
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Backpack control */
|
|
func Selected(object mnu, object mnu_item)
|
|
{
|
|
var backpack_index = mnu_item->GetExtraData();
|
|
var hands_index = 0;
|
|
// Update menu
|
|
var show_new_item = this->GetItem(hands_index);
|
|
mnu_item->SetSymbol(show_new_item);
|
|
// swap index with backpack index
|
|
this->Switch2Items(hands_index, backpack_index);
|
|
}
|