openclonk/planet/Objects.ocd/Libraries.ocd/Structures.ocd/Structure.ocd/Script.c

482 lines
12 KiB
C

/**
Structure Library
Basic library for structures, handles:
* Damage
* Info dialogue
* Energy bar if rule active
* Basements
* Value
@author Maikel
*/
// All structure related local variables are stored in a single proplist.
// This reduces the chances of clashing local variables. See Initialize
// for which variables are being used.
local lib_structure;
protected func Construction()
{
// Initialize the single proplist for the structure library.
if (lib_structure == nil)
lib_structure = {};
lib_structure.basement = nil;
// This contains a list of materials that are needed for repairing damage. (proplist with {id, count})
lib_structure.repair_materials = [];
// Total value of the components - used to calculate the required material for repairing.
lib_structure.total_component_value = nil;
// Make a writable copy of the editor action.
if (this.EditorActions == GetID().EditorActions)
MakePropertyWritable("EditorActions");
return _inherited(...);
}
// This object is a structure.
public func IsStructure() { return true; }
/*-- Damage Handling --*/
public func GetHitPoints()
{
return this.HitPoints;
}
public func GetRemainingHitPoints()
{
return this.HitPoints - GetDamage();
}
public func Damage(int change, int cause, int cause_plr)
{
// Only do stuff if the object has the HitPoints property.
if (this && this.HitPoints != nil)
{
if (GetDamage() >= this.HitPoints)
{
// Remove contents from the building depending on the type of damage.
EjectContentsOnDestruction(cause, cause_plr);
// Handle detruction with a custom callback? If not, remove the object.
if (!this->~OnNoHitPointsRemaining(cause, cause_plr))
{
// Destruction callback is made by the engine.
return RemoveObject();
}
}
// Update all interaction menus with the new hitpoints.
UpdateInteractionMenus(this.GetDamageMenuEntries);
}
return _inherited(change, cause, cause_plr, ...);
}
private func EjectContentsOnDestruction(int cause, int by_player)
{
// Exit all objects in this structure.
for (var obj in FindObjects(Find_Container(this)))
{
// For a non-blast destruction just place the objects at the bottom of the structure.
var angle = Random(360);
var x = RandomX(GetLeft(), GetRight());
var y = GetBottom();
var dx = 0;
var dy = 0;
var dr = 0;
// Scatter objects around if the destruction is caused by a blast.
if (cause == FX_Call_DmgBlast)
{
var speed = RandomX(3, 4);
x = RandomX(-4, 4);
y = RandomX(-4, 4);
dx = Cos(angle, speed);
dy = Sin(angle, speed);
dr = RandomX(-20, 20);
}
obj->Exit(x, y, Random(360), dx, dy, dr);
obj->SetController(by_player);
}
return;
}
/*-- Basement Handling --*/
public func SetBasement(object to_basement)
{
lib_structure.basement = to_basement;
if (this.EditorActions)
{
if (lib_structure.basement)
this.EditorActions.basement = nil;
else
this.EditorActions.basement = new GetID().EditorActions.basement {};
}
return;
}
public func GetBasement()
{
if (lib_structure) return lib_structure.basement;
return nil;
}
public func IsStructureWithoutBasement()
{
return IsStructure() && !(lib_structure && lib_structure.basement);
}
public func AddBasement()
{
var offset = this->~GetBasementOffset() ?? [0, 0];
var basement = CreateObject(this->~GetBasementID() ?? Basement, offset[0], GetBottom() + 4 + offset[1]);
basement->SetParent(this);
return;
}
/*-- SolidMask --*/
// Move objects out of the solid mask of a structure: it assumes the structure's shape is fully covered by it solid mask (e.g. basement, bridge).
private func MoveOutOfSolidMask()
{
// Find all objects inside the basement which are stuck.
var wdt = GetObjWidth();
var hgt = GetObjHeight();
var lying_around = FindObjects(Find_Or(Find_Category(C4D_Vehicle), Find_Category(C4D_Object), Find_Category(C4D_Living)), Find_AtRect(-wdt / 2, -hgt / 2, wdt, hgt), Find_NoContainer());
// Move up these objects.
for (var obj in lying_around)
{
var x = obj->GetX();
var y = obj->GetY();
var delta_y = 0;
var max_delta = obj->GetObjHeight() + hgt;
// Move up object until it is not stuck any more.
while (obj->Stuck() || obj->GetContact(-1, CNAT_Bottom))
{
// Only move up the object by at most its height plus the basements height.
if (delta_y > max_delta)
{
obj->SetPosition(x, y);
break;
}
delta_y++;
obj->SetPosition(x, y - delta_y);
}
}
return;
}
/*-- Value --*/
public func CalcDefValue()
{
var value = 0;
var comp, index = 0;
while (comp = this->GetComponent(nil, index++))
{
var comp_value = comp->GetValue();
var comp_amount = this->GetComponent(comp);
value += comp_value * comp_amount;
}
return value;
}
/* Repair Materials */
// Adds one unit of a material to the internal repair list.
public func AddRepairMaterial(id material_id)
{
// Sort into list of already contained
for (var old_material in lib_structure.repair_materials)
{
if (old_material.id == material_id)
{
old_material.count += 1;
return true;
}
}
// New material?
PushBack(lib_structure.repair_materials, {id = material_id, count = 1});
return true;
}
// Calculates and caches the total component value.
private func GetTotalComponentValue()
{
if (lib_structure.total_component_value == nil)
{
lib_structure.total_component_value = 0;
var component, i = 0;
while (component = GetComponent(nil, i++))
{
var count = GetComponent(component);
lib_structure.total_component_value += count * component->GetValue();
}
}
return lib_structure.total_component_value;
}
// Returns all materials that are currently needed to repair the structure fully.
public func GetRepairMaterials()
{
// Safety.
if (!GetComponent()) return [];
var damage = GetDamage();
// Reset repair materials if structure was magically repaired to 100%.
if (damage <= 0)
{
lib_structure.repair_materials = [];
return [];
}
// Otherwise, figure out materials that could be used to repair the structure.
// Initialize total component value (calculate only once for speed).
var total_component_value = GetTotalComponentValue();
// Check if the material list already contains enough materials to repair all the damage.
var material_value = 0;
for (var material in lib_structure.repair_materials)
{
material_value += material.count * material.id->GetValue();
}
// Multiply by 1000 to prevent issues with rounding.
var remaining_damage_value = 1000 * total_component_value * GetDamage() / GetHitPoints() - 1000 * material_value;
if (remaining_damage_value <= 0)
{
return lib_structure.repair_materials;
}
// Otherwise, we need to fill the remaining space with additional materials.
while (remaining_damage_value > 0)
{
// Do a random sample of components weighted by value and amount.
var random_sample = Random(total_component_value);
var current_offset = 0;
var component, i = 0;
var found = false;
while (component = GetComponent(nil, i++))
{
var count = GetComponent(component);
var value = component->GetValue();
var weight = count * value;
current_offset += weight;
if (random_sample <= current_offset)
{
remaining_damage_value -= 1000 * value;
AddRepairMaterial(component);
found = true;
break;
}
}
// Failsafe. No components?
if (!found)
break;
}
return lib_structure.repair_materials;
}
/*-- Interaction --*/
// Always show an interaction menu with at least the damage entry.
public func HasInteractionMenu() { return true; }
public func RejectInteractionMenu(object clonk)
{
if (GetCon() < 100) return Format("$MsgNotFullyConstructed$", GetName());
return _inherited(clonk, ...);
}
// Show damage and allow a player to repair the building when damaged.
public func GetInteractionMenus(object clonk)
{
var menus = _inherited(clonk, ...) ?? [];
var damage_menu =
{
title = "$Damage$",
entries_callback = this.GetDamageMenuEntries,
callback = "OnRepairSelected",
callback_hover = "OnRepairMenuHover",
callback_target = this,
BackgroundColor = RGB(75, 50, 0),
Priority = 90
};
PushBack(menus, damage_menu);
return menus;
}
// Returns the contents of the "damage" section in the interaction menu.
public func GetDamageMenuEntries()
{
var is_invincible = this.HitPoints == nil || this->IsInvincible();
var damage_text = "$Invincible$";
var color = RGB(0, 150, 0);
if (!is_invincible)
{
if (GetDamage() == 0) damage_text = "$NotDamaged$";
else if (GetDamage() < this.HitPoints / 2)
{
damage_text = "$SlightlyDamaged$";
color = RGB(200, 150, 0);
}
else
{
damage_text = "<c ff0000>$HeavilyDamaged$</c>";
color = RGB(150, 0, 0);
}
}
var menu =
{
Bottom = "2em",
right =
{
Left = "2em + 0.2em",
Right = "100% - 0.2em",
bottom = {Top = "50%", Margin = "0.1em", BackgroundColor = RGB(0, 0, 0)},
top = {Text = damage_text, Style = GUI_TextHCenter}
},
symbol =
{
Right = "2em",
Symbol = Hammer
}
};
// Show hit points.
var percent = "100%";
if (!is_invincible)
percent = Format("%d%%", 100 * GetRemainingHitPoints() / GetHitPoints());
menu.right.bottom.fill = {BackgroundColor = color, Right = percent, Margin = "0.1em"};
// Cross out hammer symbol.
if (is_invincible || GetDamage() == 0)
{
menu.symbol.overlay = {Margin = "0.25em", Symbol = Icon_Cancel};
}
return [{symbol = Hammer, extra_data = "repair", custom = menu}];
}
public func OnRepairSelected(id symbol, string action, object cursor)
{
// Repairing requires a hammer or something alike.
var hammer = FindObject(Find_Container(cursor), Find_Func("IsConstructor"));
if (!hammer)
{
PlayerMessage(cursor->GetOwner(), "$YouNeedAHammer$");
Sound("UI::Click2", {player = cursor->GetOwner()});
return;
}
// Check whether some of the required material can be found in the Clonk or the structure.
var materials = GetRepairMaterials();
var total_repair_value = 0;
for (var material in materials)
{
while (material.count > 0)
{
var real_material = FindObject(Find_Or(Find_Container(this), Find_Container(cursor)), Find_ID(material.id));
if (!real_material) break;
var repair_value = real_material->GetValue();
total_repair_value += repair_value;
real_material->RemoveObject();
material.count -= 1;
}
}
if (total_repair_value == 0)
{
PlayerMessage(cursor->GetOwner(), "$YouNeedMaterials$");
Sound("UI::Click2", {player = cursor->GetOwner()});
return;
}
// Now repair based on the value fraction. Round up - rather be nice to the player.
var total_component_value = GetTotalComponentValue();
var total_damage_repaired = GetHitPoints() * total_repair_value / total_component_value + 1;
DoDamage(-total_damage_repaired);
UpdateInteractionMenus(this.GetDamageMenuEntries);
// Clean up the material list and remove nil entries.
var new_list = [];
for (var material in lib_structure.repair_materials)
{
if (material.count > 0)
PushBack(new_list, material);
}
lib_structure.repair_materials = new_list;
Sound("Structures::Repair");
}
// On hovering, show a list of materials that are needed for repairing the structure.
public func OnRepairMenuHover(id symbol, string action, desc_menu_target, menu_id)
{
var text = "$NoRepairNecessary$";
var is_invincible = this.HitPoints == nil || this->IsInvincible();
if (!is_invincible && GetDamage() > 0)
{
var materials = GetRepairMaterials();
text = "$RepairRequires$";
var first = true;
for (var material in materials)
{
if (first)
{
text = Format("%s %dx%s ({{%i}})", text, material.count, material.id->GetName(), material.id);
first = false;
}
else
{
text = Format("%s, %dx%s ({{%i}})", text, material.count, material.id->GetName(), material.id);
}
}
}
GuiUpdateText(text, menu_id, 1, desc_menu_target);
}
public func Flip()
{
// Mirror structure
if (this->~NoConstructionFlip())
return false;
return SetDir(1 - GetDir());
}
private func FlipVertices()
{
// Flips all vertices around the Y = 0 axis, this can be used to flip the vertices of asymmetric structures.
for (var cnt = 0; cnt < GetVertexNum(); cnt++)
{
SetVertex(cnt, VTX_X, -GetVertex(cnt, VTX_X));
SetVertex(cnt, VTX_Y, GetVertex(cnt, VTX_Y));
}
return;
}
public func Definition(def, ...)
{
if (!def.EditorProps) def.EditorProps = {};
if (!def.EditorActions) def.EditorActions = {};
def.EditorActions.basement = { Name = "$Basement$", EditorHelp = "$BasementHelp$", Command = "AddBasement()" };
if (!def->~NoConstructionFlip())
def.EditorActions.flip = { Name = "$Flip$", EditorHelp = "$FlipHelp$", Command = "Flip()" };
return _inherited(def, ...);
}