forked from Mirrors/openclonk
511 lines
12 KiB
C
511 lines
12 KiB
C
/**
|
|
ConstructionSite
|
|
Needs material put into it, then constructs the set building.
|
|
|
|
@author boni
|
|
*/
|
|
|
|
local definition; // this definition is being built here
|
|
local direction;
|
|
local stick_to;
|
|
local full_material; // true when all needed material is in the site
|
|
local no_cancel; // if true, site cannot be cancelled
|
|
local is_constructing;
|
|
|
|
// This should be recongnized as a container by the interaction menu independent of its category.
|
|
public func IsContainer() { return !full_material; }
|
|
// Disallow site cancellation. Useful e.g. for sites that are pre-placed for a game goal
|
|
public func MakeUncancellable() { no_cancel = true; return true; }
|
|
|
|
|
|
/*-- Testing / Development --*/
|
|
|
|
// Builds the construction even if the required materials isn't there.
|
|
// Use for debugging purposes (or maybe cool scenario effects)
|
|
public func ForceConstruct()
|
|
{
|
|
full_material = true;
|
|
StartConstructing();
|
|
}
|
|
|
|
|
|
/*-- Interaction --*/
|
|
|
|
public func HasInteractionMenu() { return true; }
|
|
|
|
public func GetInteractionMenuEntries(object clonk)
|
|
{
|
|
// Default design of a control menu item
|
|
var custom_entry =
|
|
{
|
|
Right = "100%", Bottom = "2em",
|
|
BackgroundColor = {Std = 0, OnHover = 0x50ff0000},
|
|
image = {Right = "2em"},
|
|
text = {Left = "2em"}
|
|
};
|
|
|
|
return [{symbol = Icon_Cancel, extra_data = "abort",
|
|
custom =
|
|
{
|
|
Prototype = custom_entry,
|
|
Priority = 1,
|
|
text = {Prototype = custom_entry.text, Text = "$TxtAbort$"},
|
|
image = {Prototype = custom_entry.image, Symbol = Icon_Cancel}
|
|
}}];
|
|
}
|
|
|
|
public func GetMissingMaterialMenuEntries(object clonk)
|
|
{
|
|
var material = GetMissingComponents();
|
|
if (!material) return [];
|
|
|
|
var entries = [];
|
|
for (var mat in material)
|
|
{
|
|
var text = nil;
|
|
if (mat.count > 1) text = Format("x%d", mat.count);
|
|
PushBack(entries, {symbol = mat.id, text = text});
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
public func GetInteractionMenus(object clonk)
|
|
{
|
|
var menus = _inherited(clonk, ...) ?? [];
|
|
var comp_menu =
|
|
{
|
|
title = "$TxtMissingMaterial$",
|
|
entries_callback = this.GetMissingMaterialMenuEntries,
|
|
BackgroundColor = RGB(50, 0, 0),
|
|
Priority = 15
|
|
};
|
|
PushBack(menus, comp_menu);
|
|
var prod_menu =
|
|
{
|
|
title = "$TxtAbort$",
|
|
entries_callback = this.GetInteractionMenuEntries,
|
|
callback = "OnInteractionControl",
|
|
callback_hover = "OnInteractionControlHover",
|
|
callback_target = this,
|
|
BackgroundColor = RGB(0, 50, 50),
|
|
Priority = 20
|
|
};
|
|
PushBack(menus, prod_menu);
|
|
return menus;
|
|
}
|
|
|
|
public func OnInteractionControlHover(id symbol, string action, desc_menu_target, menu_id)
|
|
{
|
|
var text = "";
|
|
if (action == "abort")
|
|
{
|
|
if (no_cancel)
|
|
text = "$TxtNoAbortDesc$";
|
|
else
|
|
text = "$TxtAbortDesc$";
|
|
}
|
|
GuiUpdateText(text, menu_id, 1, desc_menu_target);
|
|
}
|
|
|
|
public func OnInteractionControl(id symbol, string action, object clonk)
|
|
{
|
|
if (action == "abort")
|
|
{
|
|
if (!Deconstruct())
|
|
Sound("UI::Click*", false, nil, clonk->GetOwner());
|
|
}
|
|
}
|
|
|
|
|
|
// Players can put materials into the construction site via space-key
|
|
private func IsInteractable(object clonk)
|
|
{
|
|
if (clonk) return !Hostile(GetOwner(), clonk->GetOwner());
|
|
return true;
|
|
}
|
|
|
|
|
|
// Adapt appearance in the interaction bar.
|
|
private func GetInteractionMetaInfo(object clonk)
|
|
{
|
|
return { Description = "$TxtMissingMaterial$", IconName = nil, IconID = Hammer };
|
|
}
|
|
|
|
|
|
// Called on player interaction.
|
|
public func Interact(object clonk)
|
|
{
|
|
if (clonk && IsInteractable(clonk))
|
|
{
|
|
TakeConstructionMaterials(clonk);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/*-- Engine callbacks --*/
|
|
|
|
public func Deconstruct()
|
|
{
|
|
// Remove site
|
|
if (no_cancel)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
for (var contained in FindObjects(Find_Container(this)))
|
|
{
|
|
contained->Exit();
|
|
}
|
|
RemoveObject();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public func Construction()
|
|
{
|
|
this.visibility = VIS_None;
|
|
definition = nil;
|
|
full_material = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Scenario saving
|
|
public func SaveScenarioObject(props)
|
|
{
|
|
if (!inherited(props, ...)) return false;
|
|
props->Remove("Name");
|
|
if (definition) props->AddCall("Definition", this, "Set", definition, direction, stick_to);
|
|
if (no_cancel) props->AddCall("NoCancel", this, "MakeUncancellable");
|
|
return true;
|
|
}
|
|
|
|
// Only allow collection if needed
|
|
public func RejectCollect(id def, object obj)
|
|
{
|
|
var max = definition->GetComponent(def);
|
|
|
|
// Not a component?
|
|
if (max == 0)
|
|
{
|
|
return true;
|
|
}
|
|
// Reject collection if full
|
|
return GetAvailableComponentCount(def) >= max;
|
|
}
|
|
|
|
// Check if full
|
|
public func Collection2(object obj)
|
|
{
|
|
UpdateStatus(obj);
|
|
}
|
|
|
|
// Component removed (e.g.: Contained wood burned down or some externel scripts went havoc)
|
|
// Make sure lists are updated
|
|
public func ContentsDestruction(object obj) { return UpdateStatus(); }
|
|
public func Ejection(object obj) { return UpdateStatus(); }
|
|
|
|
public func OnSynchronized()
|
|
{
|
|
// Reinitialize permanent message showing components.
|
|
ShowMissingComponents();
|
|
return;
|
|
}
|
|
|
|
|
|
/*-- Internals --*/
|
|
|
|
public func Set(id def, int dir, object stick)
|
|
{
|
|
definition = def;
|
|
direction = dir;
|
|
stick_to = stick;
|
|
|
|
// Set the shape of the construction site.
|
|
var w = def->~GetSiteWidth(direction, stick_to) ?? def->GetDefWidth();
|
|
var h = def->~GetSiteHeight(direction, stick_to) ?? def->GetDefHeight();
|
|
// Height of construction site needs to exceed 12 pixels for the clonk to be able to add materials.
|
|
var site_h = Max(12, h);
|
|
SetShape(-w/2, -site_h, w, site_h);
|
|
// Increase shape for below surface constructions to allow for adding materials.
|
|
if (definition->~IsBelowSurfaceConstruction())
|
|
SetShape(-w/2, -2 * site_h, w, 2 * site_h);
|
|
|
|
// Draw the building with a wired frame and large alpha unless site graphics is overloaded by definition
|
|
if (!definition->~SetConstructionSiteOverlay(this, direction, stick_to))
|
|
{
|
|
SetConstructionSiteOverlayDefault(def, direction, stick_to, w, h);
|
|
}
|
|
|
|
SetName(Format(Translate("TxtConstruction"), def->GetName()));
|
|
this.visibility = VIS_Owner | VIS_Allies;
|
|
ShowMissingComponents();
|
|
return;
|
|
}
|
|
|
|
|
|
private func SetConstructionSiteOverlayDefault(id def, int dir, object stick, int w, int h)
|
|
{
|
|
SetGraphics(nil, nil, 0);
|
|
SetGraphics(nil, def, 1, GFXOV_MODE_Base);
|
|
SetClrModulation(RGBa(255, 255, 255, 128), 1);
|
|
// If the structure is a mesh, use wire frame mode to show the site.
|
|
// TODO: use def->IsMesh() once this becomes available.
|
|
if (def->GetMeshMaterial())
|
|
{
|
|
SetClrModulation(RGBa(255, 255, 255, 50), 1);
|
|
SetGraphics(nil, def, 2, GFXOV_MODE_Base, nil, GFX_BLIT_Wireframe);
|
|
}
|
|
SetObjDrawTransform((1 - dir * 2) * 1000, 0, 0, 0, 1000, -h * 500, 1);
|
|
SetObjDrawTransform((1 - dir * 2) * 1000, 0, 0, 0, 1000, -h * 500, 2);
|
|
}
|
|
|
|
private func UpdateStatus(object item)
|
|
{
|
|
// Ignore any activity during construction
|
|
if (is_constructing) return;
|
|
|
|
// Update message
|
|
ShowMissingComponents();
|
|
|
|
// Update possibly open menus.
|
|
UpdateInteractionMenus(this.GetMissingMaterialMenuEntries);
|
|
|
|
// Update preview image
|
|
if (definition) definition->~SetConstructionSiteOverlay(this, direction, stick_to, item);
|
|
|
|
// Check if we're done?
|
|
if (full_material)
|
|
{
|
|
var controller = GetOwner();
|
|
if (item) controller = item->GetController();
|
|
StartConstructing(controller);
|
|
}
|
|
}
|
|
|
|
private func ShowMissingComponents()
|
|
{
|
|
if (definition == nil)
|
|
{
|
|
Message("");
|
|
return;
|
|
}
|
|
|
|
var stuff = GetMissingComponents();
|
|
var msg = "@";
|
|
for (var s in stuff)
|
|
if (s.count > 0)
|
|
msg = Format("%s %dx{{%i}}", msg, s.count, s.id);
|
|
// Ensure that the message is not below the bottom of the map.
|
|
var dy = 23 - Max(23 + GetY() - LandscapeHeight(), 0) / 2;
|
|
CustomMessage(msg, this, NO_OWNER, 0, dy);
|
|
}
|
|
|
|
private func GetMissingComponents()
|
|
{
|
|
if (definition == nil)
|
|
return [];
|
|
|
|
if (full_material == true)
|
|
return [];
|
|
|
|
// Set false again as soon as we find a missing component
|
|
full_material = true;
|
|
|
|
// Check for material
|
|
var component, index = 0;
|
|
var missing_material = CreateArray();
|
|
while (component = definition->GetComponent(nil, index))
|
|
{
|
|
// Find material
|
|
var max_amount = definition->GetComponent(component);
|
|
var current_amount = GetAvailableComponentCount(component);
|
|
var diff = max_amount - current_amount;
|
|
|
|
if (diff > 0)
|
|
{
|
|
PushBack(missing_material, {id=component, count=diff});
|
|
full_material = false;
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
return missing_material;
|
|
}
|
|
|
|
private func StartConstructing(int by_player)
|
|
{
|
|
if (!definition || !full_material)
|
|
return;
|
|
|
|
is_constructing = true;
|
|
|
|
// Find all objects on the bottom of the area that are not stuck
|
|
var lying_around = GetObjectsLyingAround();
|
|
|
|
// Create the site?
|
|
var site = CreateConstructionSite();
|
|
if (site)
|
|
{
|
|
StartConstructionEffect(site, by_player);
|
|
}
|
|
|
|
// Clean up stuck objects
|
|
EnsureObjectsLyingAround(lying_around);
|
|
}
|
|
|
|
|
|
// Create the construction, below surface constructions don't perform any checks.
|
|
// Uncancellable sites (for special game goals) are forced and don't do checks either
|
|
private func CreateConstructionSite()
|
|
{
|
|
var checks = !definition->~IsBelowSurfaceConstruction() && !no_cancel;
|
|
var site = CreateConstruction(definition, 0, 0, GetOwner(), 1, checks, checks);
|
|
|
|
if (!site)
|
|
{
|
|
// Spit out error message. This could happen if the landscape changed in the meantime
|
|
// a little hack: the message would immediately vanish because this object is deleted. So, instead display the
|
|
// message on one of the contents.
|
|
if (Contents(0))
|
|
{
|
|
CustomMessage("$TxtNoConstructionHere$", Contents(0), GetOwner(), nil,nil, RGB(255, 0, 0));
|
|
}
|
|
Deconstruct();
|
|
return nil;
|
|
}
|
|
|
|
// Apply direction
|
|
if (direction)
|
|
{
|
|
site->SetDir(direction);
|
|
}
|
|
// Inform about sticky building
|
|
if (stick_to)
|
|
{
|
|
site->CombineWith(stick_to);
|
|
}
|
|
|
|
return site;
|
|
}
|
|
|
|
private func StartConstructionEffect(object site, int by_player)
|
|
{
|
|
// Object provides custom construction effects?
|
|
if (!site->~DoConstructionEffects(this))
|
|
{
|
|
// If not: Autoconstruct 2.0!
|
|
Schedule(site, "DoCon(2)", 1, 50);
|
|
Schedule(this, "RemoveObject()", 1);
|
|
Global->ScheduleCall(nil, Global.GameCallEx, 51, 1, "OnConstructionFinished", site, by_player);
|
|
site->Sound("Structures::FinishBuilding");
|
|
}
|
|
}
|
|
|
|
private func TakeConstructionMaterials(object from_clonk)
|
|
{
|
|
// Check for material
|
|
var component, index = 0;
|
|
var materials;
|
|
var w = definition->GetDefWidth() + 10;
|
|
var h = definition->GetDefHeight() + 10;
|
|
|
|
while (component = definition->GetComponent(nil, index))
|
|
{
|
|
// Find material
|
|
var count_needed = definition->GetComponent(component);
|
|
index++;
|
|
|
|
materials = CreateArray();
|
|
// 1. Look for stuff in the clonk
|
|
materials[0] = FindObjects(Find_ID(component), Find_Container(from_clonk));
|
|
// 2. Look for stuff lying around
|
|
materials[1] = from_clonk->FindObjects(Find_ID(component), Find_NoContainer(), Find_InRect(-w/2, -h/2, w,h));
|
|
// 3. Look for stuff in nearby lorries/containers
|
|
var i = 2;
|
|
for (var container in from_clonk->FindObjects(Find_Or(Find_Func("IsLorry"), Find_Func("IsContainer")), Find_InRect(-w/2, -h/2, w,h)))
|
|
materials[i] = FindObjects(Find_ID(component), Find_Container(container));
|
|
// Move it
|
|
for (var material_list in materials)
|
|
{
|
|
for (var material in material_list)
|
|
{
|
|
if (count_needed <= 0)
|
|
{
|
|
break;
|
|
}
|
|
material->Exit();
|
|
material->Enter(this);
|
|
count_needed--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gets the number of available components of a type.
|
|
// This defaults to ContentsCount(), but can be overloaded
|
|
// for implementations of the construction site.
|
|
private func GetAvailableComponentCount(id component)
|
|
{
|
|
return ContentsCount(component);
|
|
}
|
|
|
|
// Find all objects on the bottom of the area that are not stuck
|
|
private func GetObjectsLyingAround()
|
|
{
|
|
var wdt = GetObjWidth();
|
|
var hgt = GetObjHeight();
|
|
return FindObjects(Find_Category(C4D_Vehicle | C4D_Object | C4D_Living), Find_AtRect(-wdt/2 - 2, -hgt, wdt + 2, hgt + 12), Find_OCF(OCF_InFree), Find_NoContainer());
|
|
}
|
|
|
|
// Clean up stuck objects
|
|
private func EnsureObjectsLyingAround(array lying_around)
|
|
{
|
|
for (var thing in lying_around)
|
|
{
|
|
if (!thing) continue;
|
|
|
|
var x, y;
|
|
var moved = 0;
|
|
|
|
x = thing->GetX();
|
|
y = thing->GetY();
|
|
|
|
// Move living creatures upwards till they stand on top.
|
|
if (thing->GetOCF() & OCF_Alive)
|
|
{
|
|
while (thing->GetContact(-1, CNAT_Bottom))
|
|
{
|
|
// Only up to 20 pixel
|
|
if (moved > 20)
|
|
{
|
|
thing->SetPosition(x, y);
|
|
break;
|
|
}
|
|
|
|
moved++;
|
|
thing->SetPosition(x, y - moved);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (thing->Stuck())
|
|
{
|
|
// Only up to 20 pixel
|
|
if (moved > 20)
|
|
{
|
|
thing->SetPosition(x, y);
|
|
break;
|
|
}
|
|
|
|
moved++;
|
|
thing->SetPosition(x, y - moved);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|