openclonk/planet/Objects.ocd/HUD.ocd/CircleMenu.ocd/ContentsMenuController.ocd/Script.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;
}