forked from Mirrors/openclonk
696 lines
16 KiB
C
696 lines
16 KiB
C
/**
|
|
Control object for content menus.
|
|
This is not a regular menu, because it creates other menus to do the work for it.
|
|
|
|
@author Newton, Maikel
|
|
*/
|
|
|
|
// this array contains all the menus
|
|
// index 0 = calling object
|
|
// index >0 = first crew, then containers
|
|
local circ_menus;
|
|
local menu_object;
|
|
|
|
local crew_count;
|
|
local container_count;
|
|
|
|
static const MENU_Contents_MaxCrewDist = 20;
|
|
|
|
/** Creates a content menu for the calling object. This is supposed to be a crew member,
|
|
controlled by a player.
|
|
@return a pointer to the created menu, or \c nil if failed.
|
|
*/
|
|
global func CreateContentsMenus()
|
|
{
|
|
// Safety checks.
|
|
if (!this) return;
|
|
if (!(GetOCF() & OCF_CrewMember)) return;
|
|
if (!(this->~HasMenuControl())) return;
|
|
|
|
// Create the menu controller.
|
|
var controller = CreateObject(GUI_Contents_Controller);
|
|
controller->SetMenuObject(this);
|
|
this->SetMenu(controller);
|
|
|
|
var index = 0;
|
|
|
|
// add the clonk itself
|
|
controller->AddContentMenu(this, index, true);
|
|
|
|
// add all nearby crewmembers
|
|
var teammates = FindObjects(Find_Distance(MENU_Contents_MaxCrewDist), Find_OCF(OCF_CrewMember), Find_Owner(GetOwner()), Find_Exclude(this));
|
|
index = 1;
|
|
for(var t in teammates)
|
|
controller->AddContentMenu(t, index++, true);
|
|
|
|
// Add content menus for all containers at the position of the caller.
|
|
var containers = FindObjects(Find_AtPoint(), Find_NoContainer(), Find_OCF(OCF_Fullcon), Find_Func("IsContainer"), Sort_Func("SortInventoryObjs"));
|
|
for(var c in containers)
|
|
controller->AddContentMenu(c, index++, false);
|
|
|
|
// Show and return content menu.
|
|
controller->Show();
|
|
return controller;
|
|
}
|
|
|
|
/** Returns the object for which the menu is shown. */
|
|
public func GetMenuObject() { return menu_object; }
|
|
|
|
global func SortInventoryObjs()
|
|
{
|
|
// left: crew members
|
|
if (GetOCF() & OCF_CrewMember) return 0;
|
|
// center: vehicles
|
|
if (GetCategory() & C4D_Vehicle) return 10;
|
|
// right: buildings
|
|
if (GetCategory() & C4D_Structure) return 20;
|
|
}
|
|
|
|
public func IsContentMenu() { return true; }
|
|
|
|
func SetMenuObject(object menuObject)
|
|
{
|
|
menu_object = menuObject;
|
|
}
|
|
|
|
func Construction()
|
|
{
|
|
circ_menus = [];
|
|
container_count = 0;
|
|
crew_count = 0;
|
|
}
|
|
|
|
func Close()
|
|
{
|
|
if(menu_object)
|
|
menu_object->~MenuClosed(this);
|
|
RemoveObject();
|
|
}
|
|
|
|
func Destruction()
|
|
{
|
|
// remove all menu objects
|
|
for(var prop in circ_menus)
|
|
{
|
|
if(prop.Object)
|
|
prop.Object->~OnContentMenuClosed();
|
|
prop.Menu->RemoveObject();
|
|
}
|
|
}
|
|
|
|
func Show(bool no_update)
|
|
{
|
|
for (var prop in circ_menus)
|
|
{
|
|
if (!no_update)
|
|
prop.Object->~OnContentMenuOpened();
|
|
prop.Menu->Show();
|
|
}
|
|
}
|
|
|
|
func Hide()
|
|
{
|
|
for (var prop in circ_menus)
|
|
{
|
|
prop.Object->~OnContentMenuClosed();
|
|
prop.Menu->Hide();
|
|
}
|
|
}
|
|
|
|
func AddContentMenu(object container, int pos, bool isCrew)
|
|
{
|
|
var menu = CreateObject(GUI_CircleMenu, 0, 0, GetOwner());
|
|
|
|
menu->SetSymbol(container);
|
|
menu->SetMenuObject(menu_object);
|
|
menu->SetCommander(this);
|
|
menu->SetDragDropMenu(true);
|
|
|
|
var dist = ObjectDistance(menu_object, container);
|
|
|
|
PutContentsIntoMenu(menu, container);
|
|
circ_menus[pos] = {Object = container, Menu = menu, IsCrew = isCrew, Distance = dist};
|
|
|
|
if(isCrew)
|
|
crew_count++;
|
|
else
|
|
container_count++;
|
|
|
|
// Track external changes in containers.
|
|
AddEffect("ContainerTracker", container, 100, 1, this, nil, menu, container->GetPosition());
|
|
|
|
UpdateContentMenus();
|
|
return;
|
|
}
|
|
|
|
func RemoveContentMenu(int index)
|
|
{
|
|
var length = GetLength(circ_menus);
|
|
if(index >= length)
|
|
return;
|
|
|
|
// remove menu
|
|
if(circ_menus[index].IsCrew)
|
|
crew_count--;
|
|
else
|
|
container_count--;
|
|
|
|
circ_menus[index].Menu->RemoveObject();
|
|
|
|
// for debugging, we'll set debug information here. In case something doesn't work as intended or setLength breaks.
|
|
circ_menus[index] = {Debug="RemoveContentMenu was called", Index=index};
|
|
|
|
// close the gap
|
|
for(var i=index; i < length-1; i++)
|
|
circ_menus[i] = circ_menus[i+1];
|
|
|
|
SetLength(circ_menus, length-1);
|
|
|
|
// and update
|
|
UpdateContentMenus();
|
|
for(var prop in circ_menus)
|
|
prop.Menu->UpdateMenu();
|
|
}
|
|
|
|
// Draws the contents menus to the right positions.
|
|
private func UpdateContentMenus()
|
|
{
|
|
// for positioning to the left
|
|
var index_crew = 0;
|
|
// for positioning to the right
|
|
var index_containers = 1; // containers start with an offset of 1
|
|
|
|
// calculate left edge
|
|
var r = 160;
|
|
var d = 2*r/3;
|
|
var dx = Sqrt(r**2 - d**2);
|
|
var x_base = 800 - dx*(container_count-1);
|
|
var y_base = 320;
|
|
|
|
for(var prop in circ_menus)
|
|
{
|
|
var menu = prop.Menu;
|
|
|
|
var x,y;
|
|
|
|
if(prop.IsCrew)
|
|
{
|
|
x = x_base - dx * (2*index_crew);
|
|
y = y_base + 2*d - (-1)**index_crew * d;
|
|
|
|
index_crew++;
|
|
}
|
|
else
|
|
{
|
|
// Determine x-position.
|
|
//x = 800 + dx * (2*index_containers - container_count + 1);
|
|
x = x_base + dx*(2*index_containers);
|
|
// Alternate y-position.
|
|
y = y_base + (-1)**(index_containers) * d;
|
|
|
|
index_containers++;
|
|
}
|
|
// TODO: reduce size of menus to fit more into screen.
|
|
menu->SetPosition(x, y);
|
|
}
|
|
return;
|
|
}
|
|
|
|
private func PutContentsIntoMenu(object menu, object container)
|
|
{
|
|
if (container->~ NoStackedContentMenu())
|
|
{
|
|
var contents;
|
|
// get contents in special order if possible
|
|
if(!(contents = (container->~GetItems())))
|
|
// else just take everything we find.
|
|
contents = FindObjects(Find_Container(container));
|
|
|
|
for (var content in contents)
|
|
if (!AddContentsMenuItem(content, menu))
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
var stacked_contents = container->GetStackedContents();
|
|
for (var stack in stacked_contents)
|
|
if (!AddContentsMenuItem(stack[0], menu, stack))
|
|
return;
|
|
}
|
|
|
|
// TODO: find an extra-entry or something like that to display this.
|
|
if(container->~IsCarryingHeavy())
|
|
AddContentsMenuItem(container->GetCarryHeavy(), menu);
|
|
}
|
|
|
|
private func AddContentsMenuItem(object obj, object menu, array stack)
|
|
{
|
|
// Into the menu item, all the objects of the stack are saved as an array into it's extradata.
|
|
var item = CreateObject(GUI_MenuItem);
|
|
if (!menu->AddItem(item))
|
|
{
|
|
item->RemoveObject();
|
|
return false;
|
|
}
|
|
item->SetSymbol(obj);
|
|
if (stack == nil)
|
|
{
|
|
item->SetData([obj]);
|
|
}
|
|
else
|
|
{
|
|
item->SetCount(GetLength(stack));
|
|
item->SetData(stack);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
global func GetStackedContents()
|
|
{
|
|
if(!this) return nil;
|
|
|
|
var contents = FindObjects(Find_Container(this));
|
|
var stacked = [];
|
|
|
|
// put all contents into stackcontents
|
|
for(var content in contents)
|
|
{
|
|
// check if item of same ID already inside
|
|
var has_stacked = false;
|
|
for(var stackcontent in stacked)
|
|
{
|
|
if (!CanStackObjects(content,stackcontent[0])) continue;
|
|
stackcontent[GetLength(stackcontent)] = content;
|
|
has_stacked = true;
|
|
break;
|
|
}
|
|
// otherwise, put new inside
|
|
if (!has_stacked)
|
|
stacked[GetLength(stacked)] = [content];
|
|
}
|
|
|
|
return stacked;
|
|
}
|
|
|
|
global func CanStackObjects(object first, object second)
|
|
{
|
|
if(first->GetID() != second->GetID()) return false;
|
|
if(first->~RejectStack(second)) return false;
|
|
return true;
|
|
}
|
|
|
|
private func CanStackObjIntoMenuItem(object menu, object obj) {
|
|
for(var item in menu->GetItems()) {
|
|
if(!item) continue;
|
|
|
|
var stack = item->GetExtraData();
|
|
if(CanStackObjects(obj,stack[0])) {
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*-- Content tracking --*/
|
|
// TODO: Implement this more carefully and cover all corner cases.
|
|
// Todo: Replace this with Fx*Collection if it ever gets implemented.
|
|
public func FxContainerTrackerStart(object target, proplist effect, int temporary, object menu, array position)
|
|
{
|
|
if (temporary)
|
|
return 1;
|
|
|
|
effect.Menu = menu;
|
|
effect.Position = position;
|
|
// Initialize content list.
|
|
EffectCall(target, effect, "Update");
|
|
|
|
return 1;
|
|
}
|
|
|
|
public func FxContainerTrackerTimer(object target, proplist effect)
|
|
{
|
|
// check if the target moved
|
|
if(effect.Position[0] != target->GetX() || effect.Position[1] != target->GetY())
|
|
{
|
|
effect.CommandTarget->~OnContainerMovement(effect.Menu, target);
|
|
effect.Position = target->GetPosition();
|
|
|
|
// if it's the clonk that moved
|
|
if(target == effect.CommandTarget.menu_object)
|
|
// check all distances.
|
|
effect.CommandTarget->~OnClonkMovement();
|
|
|
|
return 1;
|
|
}
|
|
|
|
var change = false;
|
|
var index = 0;
|
|
|
|
// Match current contents to actual list, first trivial test.
|
|
if (GetLength(effect.ContentList) != target->ContentsCount())
|
|
change = true;
|
|
|
|
// Test both ways around, cause either container can be empty.
|
|
else
|
|
{
|
|
for(index=0; target->Contents(index); index++)
|
|
{
|
|
if (effect.ContentList[index] != target->Contents(index))
|
|
{
|
|
change = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(!change)
|
|
{
|
|
for (index = 0; index < GetLength(effect.ContentList); index++)
|
|
{
|
|
if (effect.ContentList[index] != target->Contents(index))
|
|
{
|
|
change = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(change)
|
|
{
|
|
EffectCall(target, effect, "Update");
|
|
effect.CommandTarget->~OnContentChange(effect.Menu, target);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
public func FxContainerTrackerStop(object target, proplist effect, int reason, bool tmp)
|
|
{
|
|
if(tmp)
|
|
return;
|
|
|
|
if(reason == 3 || reason == 4)
|
|
effect.CommandTarget->~OnContainerRemoved(effect.Menu, target);
|
|
|
|
return 1;
|
|
}
|
|
|
|
public func FxContainerTrackerUpdate(object target, proplist effect)
|
|
{
|
|
effect.ContentList = [];
|
|
var index = 0;
|
|
while (target->Contents(index))
|
|
{
|
|
effect.ContentList[index] = target->Contents(index);
|
|
index++;
|
|
}
|
|
}
|
|
|
|
/** Called when the position of the clonk that opened the menu changed.
|
|
Checks if containers are still in range, and removes menu if necessary.
|
|
*/
|
|
public func OnClonkMovement()
|
|
{
|
|
for(var prop in circ_menus)
|
|
OnContainerMovement(prop.Menu, prop.Object);
|
|
}
|
|
|
|
/** Called when the position of a container with an open menu changed.
|
|
Checks if object still is in range, and removes menu if necessary.
|
|
*/
|
|
public func OnContainerMovement(object menu, object container)
|
|
{
|
|
var index = FindMenuPos(menu);
|
|
if(index < 0)
|
|
return;
|
|
|
|
// check distance
|
|
if(menu.isCrew)
|
|
{
|
|
Log("%s - %s: %d", menu_object->GetName(), container->GetName(), ObjectDistance(menu_object, container));
|
|
if(ObjectDistance(menu_object, container) > MENU_Contents_MaxCrewDist)
|
|
RemoveContentMenu(index);
|
|
}
|
|
else
|
|
{
|
|
// todo: reverse-find_at_point or something more performant than InFrontOf.
|
|
if(!menu_object->InFrontOf(container))
|
|
RemoveContentMenu(index);
|
|
}
|
|
}
|
|
|
|
/** Called when a container with an open menu got removed.
|
|
Removes the Menu and fixes ordering.
|
|
*/
|
|
public func OnContainerRemoved(object menu, object container)
|
|
{
|
|
var index = FindMenuPos(menu);
|
|
if(index < 0)
|
|
return;
|
|
|
|
// remove menu and reorder other menus
|
|
RemoveContentMenu(index);
|
|
}
|
|
|
|
/** Called when the content of a container with an open menu changed.
|
|
Updates menu.
|
|
*/
|
|
public func OnContentChange(object menu, object container)
|
|
{
|
|
// Find changed menu and remove it.
|
|
var index = FindMenuPos(menu);
|
|
if(index < 0)
|
|
return;
|
|
|
|
menu->Clear();
|
|
PutContentsIntoMenu(menu, container);
|
|
|
|
return;
|
|
}
|
|
|
|
/** Transfers the objects contained in menu_item from the source to the target container.
|
|
@param p_source proplist containing the source menu.
|
|
@param p_target proplist containing the target menu.
|
|
@param menu_item the menu item selected by the player.
|
|
@param amount the amount of objects to be transfered.
|
|
@return the actual count of objects that have successfully been transfered.
|
|
*/
|
|
private func TransferObjects(proplist p_source, proplist p_target, object menu_item, int amount)
|
|
{
|
|
// Safety: target and source must be available.
|
|
if (!p_source || !p_target)
|
|
return 0;
|
|
|
|
// Determine actual amount that may be transfered.
|
|
var objects = menu_item->GetExtraData(); // will always be at least [object]
|
|
amount = BoundBy(amount, 0, GetLength(objects));
|
|
|
|
// Move to object from source container to target container.
|
|
// Memorize which objects have been put where in order to know which menu items to update
|
|
// how (Because the collection can be rejected by RejectCollect/AllowTransfer).
|
|
var moved_to_target = [];
|
|
var moved_length = 0;
|
|
for (var i = 0; i < amount; i++)
|
|
{
|
|
var obj = objects[i];
|
|
// Try to enter the object, but check for some collect callbacks.
|
|
if (p_target.Object->~RejectCollect(obj->GetID(), obj) && !p_target.Object->~AllowTransfer(obj))
|
|
continue;
|
|
// see if the container actually allows taking it out
|
|
if(p_source.Object->~RefuseTransfer(obj))
|
|
continue;
|
|
if (obj->Enter(p_target.Object))
|
|
{
|
|
moved_to_target[GetLength(moved_to_target)] = obj;
|
|
moved_length++;
|
|
}
|
|
}
|
|
|
|
// Update target menu, source menu is updated elsewhere.
|
|
if (moved_length > 0)
|
|
{
|
|
// in case objects may not be stacked in the contents menu of the target object, add
|
|
// each object as a new menu item
|
|
if (p_target.Object->~NoStackedContentMenu())
|
|
{
|
|
for (var mov_obj in moved_to_target)
|
|
AddContentsMenuItem(mov_obj, p_target.Menu);
|
|
}
|
|
// otherwise, add stacked
|
|
else
|
|
{
|
|
// If menu item with same objects already exists: update menu item.
|
|
var target_item = CanStackObjIntoMenuItem(p_target.Menu, moved_to_target[0]);
|
|
if (target_item)
|
|
{
|
|
var new_extra_data = Concatenate(target_item->GetExtraData(), moved_to_target);
|
|
target_item->SetData(new_extra_data);
|
|
target_item->SetCount(GetLength(new_extra_data));
|
|
}
|
|
// Otherwise add a new menu item to containing menu.
|
|
else
|
|
{
|
|
AddContentsMenuItem(moved_to_target[0], p_target.Menu, moved_to_target);
|
|
}
|
|
}
|
|
}
|
|
// Return the number of items that have been transfered.
|
|
return moved_length;
|
|
}
|
|
|
|
/** Exchanges to menu items and their contents between the source and a target menu.
|
|
|
|
*/
|
|
private func ExchangeObjects()
|
|
{
|
|
// TODO: Implement.
|
|
|
|
}
|
|
|
|
|
|
private func MoveObjects(proplist p_source, proplist p_target, object menuItem, int amount)
|
|
{
|
|
// move object to new menu
|
|
TransferObjects(p_source, p_target, menuItem, amount);
|
|
// Update menus
|
|
OnContentChange(p_source.Menu, p_source.Object);
|
|
OnContentChange(p_target.Menu, p_target.Object);
|
|
|
|
EffectCall(p_source.Object, GetEffect("ContainerTracker", p_source.Object), "Update");
|
|
EffectCall(p_target.Object, GetEffect("ContainerTracker", p_target.Object), "Update");
|
|
}
|
|
|
|
/* Interface to menu item as commander_object */
|
|
|
|
public func OnMouseClick(num x, num y, bool alt)
|
|
{
|
|
// Close menu if not clicked on one of the menus.
|
|
var menu = FindObject(Find_Distance(160, x, y), Find_ID(GUI_CircleMenu));
|
|
if (!menu || menu->GetCommander() != this)
|
|
Close();
|
|
return;
|
|
}
|
|
|
|
public func OnItemSelection(object menu, object item)
|
|
{
|
|
// Transfer item to previous menu.
|
|
var index = FindMenuPos(menu);
|
|
if(index < 0) return;
|
|
|
|
// Find this and previous menu.
|
|
var p_source_menu = circ_menus[index];
|
|
var p_target_menu = GetNextMenu(index, false);
|
|
|
|
// safety
|
|
if(!p_target_menu)
|
|
return false;
|
|
|
|
var amount = 1;
|
|
MoveObjects(p_source_menu, p_target_menu, item, amount);
|
|
return true;
|
|
}
|
|
|
|
public func OnItemSelectionAlt(object menu, object item)
|
|
{
|
|
// Transfer item to next menu.
|
|
var index = FindMenuPos(menu);
|
|
if(index < 0) return;
|
|
|
|
// Find this and next menu.
|
|
var p_source_menu = circ_menus[index];
|
|
var p_target_menu = GetNextMenu(index, true);
|
|
|
|
// safety.
|
|
if(!p_target_menu)
|
|
return false;
|
|
|
|
var amount = 1;
|
|
MoveObjects(p_source_menu, p_target_menu, item, amount);
|
|
return true;
|
|
}
|
|
|
|
private func GetNextMenu(int index, bool alt)
|
|
{
|
|
var last = GetLength(circ_menus) - 1;
|
|
|
|
// Only one menu: to nothing
|
|
if (last <= 0)
|
|
return nil;
|
|
|
|
if(alt)
|
|
{
|
|
// the clonk itself or one of the crewmembers (except leftmost)
|
|
if(index < crew_count-1)
|
|
index++;
|
|
// leftmost crewmember (could be the clonk itself too)
|
|
else if(index == crew_count-1)
|
|
{
|
|
// go to rightmost container
|
|
if(container_count == 0)
|
|
index = 0;
|
|
else
|
|
index = last;
|
|
}
|
|
// leftmost container
|
|
else if(index == crew_count)
|
|
index = 0; // go to clonk itself
|
|
// a container
|
|
else
|
|
index--;
|
|
}
|
|
else
|
|
{
|
|
// the clonk itself
|
|
if(index == 0)
|
|
index = crew_count; // go to first container
|
|
// a crewmember
|
|
else if(index < crew_count)
|
|
index--;
|
|
// a container
|
|
else
|
|
index++;
|
|
}
|
|
|
|
if (index > last)
|
|
index = crew_count-1;
|
|
|
|
return circ_menus[index];
|
|
}
|
|
|
|
private func FindMenuPos(object menu)
|
|
{
|
|
for(var i = 0; i < GetLength(circ_menus); ++i)
|
|
if (circ_menus[i].Menu == menu)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
func OnItemDropped(object menu, object dropped, object on_item)
|
|
{
|
|
var index = FindMenuPos(menu);
|
|
if (index < 0) return false;
|
|
|
|
var index2 = FindMenuPos(dropped->GetMenu());
|
|
if (index2 < 0) return false;
|
|
|
|
if (index == index2)
|
|
return false;
|
|
|
|
var p_source_menu = circ_menus[index2];
|
|
var p_target_menu = circ_menus[index];
|
|
|
|
var amount = 1;
|
|
TransferObjects(p_source_menu, p_target_menu, dropped, amount);
|
|
return true;
|
|
}
|
|
|
|
func OnItemDragDone(object menu, object dragged, object on_item)
|
|
{
|
|
var index = FindMenuPos(menu);
|
|
if(index < 0) return false;
|
|
|
|
var p_source_menu = circ_menus[index];
|
|
|
|
OnContentChange(menu, p_source_menu.Object);
|
|
EffectCall(p_source_menu.Object, GetEffect("ContainerTracker", p_source_menu.Object), "Update");
|
|
|
|
return true;
|
|
}
|