openclonk/planet/Objects.ocd/Environment.ocd/Bricks.ocd/MovingBrick.ocd/Script.c

515 lines
15 KiB
C

/*--
Moving Bricks
--*/
local size; // Length of the brick.
// Constants
local MovementType = { // enum
None = nil,
Horizontal = 1,
Vertical = 2,
Graph = 3
};
local EdgeSpeed_Infinite = -1;
local EdgeAdvancement = { // enum: How to select the next edge when advancing on the graph
Random = nil,
Clockwise = 1,
Counterclockwise = 2
};
// Movement properties (for editor)
local movement;
local movement_speed = 100;
// Movement on graph
local current_edge_index, current_vertex_index, target_vertex_index;
local vertex_wait_time;
local last_vx, last_vy, target_pos_x, target_pos_y;
protected func Initialize()
{
// No movement by default
movement = { Type=MovementType.None };
// Size defaults to four.
SetSize(4);
// Allow for dynamically changing speeds.
ActMap = { Prototype = this.Prototype.ActMap };
ActMap.Moving = { Prototype = ActMap.Moving };
movement_speed = ActMap.Moving.Speed;
// Set floating action.
SetAction("Moving");
SetComDir(COMD_None);
return;
}
public func SetSize(int to_size)
{
size = BoundBy(to_size, 1, 4);
// Update graphics.
var graph = Format("Size%dN%d", size, 1 + Random(1));
SetGraphics(graph);
SetShape(-20,-4,size*10,8);
// Update solid
SetSolidMask(0,size*8-8,10*size,8);
return;
}
public func SetMoveSpeed(int speed)
{
movement_speed = ActMap.Moving.Speed = Max(0, speed);
return;
}
public func ClearMovement()
{
RemoveEffect("MoveHorizontal", this);
RemoveEffect("MoveVertical", this);
RemoveEffect("MoveOnGraph", this);
movement = { Type=MovementType.None };
current_edge_index = current_vertex_index = -1;
SetComDir(COMD_None);
SetXDir(); SetYDir();
return true;
}
/*-- Horizontal movement --*/
public func MoveHorizontal(int left, int right, int speed)
{
ClearMovement();
var effect = AddEffect("MoveHorizontal", this, 100, 1, this);
effect.Left = left;
effect.Right = right;
if (speed != nil)
SetMoveSpeed(10 * speed);
if (GetComDir() != COMD_Right) SetComDir(COMD_Left);
// Props for editor display of movement range
movement = { Type = MovementType.Horizontal, Graph = [{X=left, Y=GetY()}, {X=right, Y=GetY()}] };
return;
}
private func FxMoveHorizontalTimer(object target, proplist effect)
{
if (target->GetX() > effect.Right - 23 + 10*(4 - size))
if (target->GetComDir() == COMD_Right)
SetComDir(COMD_Left);
if (target->GetX() < effect.Left + 23)
if (target->GetComDir() == COMD_Left)
SetComDir(COMD_Right);
return 1;
}
/*-- Vertical movement --*/
public func MoveVertical(int top, int bottom, int speed)
{
ClearMovement();
var effect = AddEffect("MoveVertical", this, 100, 1, this);
effect.Top = top;
effect.Bottom = bottom;
if (speed != nil)
SetMoveSpeed(10 * speed);
if (GetComDir() != COMD_Down) SetComDir(COMD_Up);
// Props for editor display of movement range
movement = { Type=MovementType.Vertical, Graph = [{X=GetX(), Y=top}, {X=GetX(), Y=bottom}] };
return;
}
private func FxMoveVerticalTimer(object target, proplist effect)
{
if (target->GetY() > effect.Bottom - 7)
if (target->GetComDir() == COMD_Down)
SetComDir(COMD_Up);
if (target->GetY() < effect.Top + 7)
if (target->GetComDir() == COMD_Up)
SetComDir(COMD_Down);
return 1;
}
/*-- Movement on graph --*/
public func MoveOnGraph(proplist graph)
{
ClearMovement();
movement = { Type = MovementType.Graph, Graph = graph };
OnGraphUpdate(movement.Graph);
var effect = AddEffect("MoveOnGraph", this, 100, 1, this);
effect.movement_graph = graph;
return;
}
private func FxMoveOnGraphTimer(object target, proplist effect)
{
// Vertex wait time?
if (vertex_wait_time)
{
if (!--vertex_wait_time)
{
OnVertexReached(current_vertex_index, true);
}
return;
}
// Check if the target vertex has been reached
var dx = target_pos_x - GetX();
var dy = target_pos_y - GetY();
if (dx * last_vx + dy * last_vy <= 0)
{
OnVertexReached(target_vertex_index);
}
return 1;
}
private func StartEdgeMovement(int edge_index, int edge_direction)
{
// Initiate movement towards target vertex
//Log("%v Start edge movement %v (%v)", this, edge_index, edge_direction);
current_edge_index = edge_index;
current_vertex_index = -1;
var edge = movement.Graph.Edges[edge_index];
target_vertex_index = edge.Vertices[edge_direction];
var target_vertex = movement.Graph.Vertices[target_vertex_index];
target_pos_x = target_vertex.X;
target_pos_y = target_vertex.Y;
var speed = edge.Speed ?? movement_speed;
if (speed == EdgeSpeed_Infinite)
{
SetPosition(target_pos_x, target_pos_y);
speed = 100; // For trigger
}
else
{
var dx = target_pos_x - GetX();
var dy = target_pos_y - GetY();
var d = Max(1, Distance(dx, dy));
SetXDir((last_vx = speed * dx / d), 100);
SetYDir((last_vy = speed * dy / d), 100);
ActMap.Moving.Speed = Max(0, speed);
}
}
private func OnVertexReached(int vertex_index, bool after_wait)
{
//Log("Vertex reached %v (%v)", vertex_index, after_wait);
// Safety
if (vertex_index < 0 || vertex_index >= GetLength(movement.Graph.Vertices))
{
vertex_index = 0;
}
// Called when a vertex has been reached. Perform vertex action & continue movement
current_vertex_index = vertex_index;
target_vertex_index = -1;
var vertex = movement.Graph.Vertices[vertex_index];
if (!after_wait)
{
// Stop for now (befure UserAction because UserAction may do movement)
SetXDir(); SetYDir();
// Vertex action
if (vertex.ArrivalAction)
{
UserAction->EvaluateAction(vertex.ArrivalAction, this);
}
// Vertex wait time? Finish for now; function Will be called again later.
if (vertex.WaitTime)
{
vertex_wait_time = vertex.WaitTime;
return;
}
}
// Find next vertex to continue to
var vertex = movement.Graph.Vertices[vertex_index];
var n_edges = GetLength(vertex._edges);
if (!n_edges) return; // Stuck on an unconnected vertex
// First figure out where we came from
var last_edge_index = -1; // index in vertex._edges array
if (current_edge_index >= 0)
{
last_edge_index = GetIndexOf(vertex._edges, movement.Graph.Edges[current_edge_index]);
}
// Advance in specified order
var advancement = vertex.Advancement;
var next_edge_index = 0; // index in vertex._edges array
if (advancement == EdgeAdvancement.Random)
{
var exclude_current = (last_edge_index >= 0 && n_edges > 1);
next_edge_index = Random(n_edges - exclude_current);
if (next_edge_index == last_edge_index) next_edge_index += exclude_current;
}
else if (advancement == EdgeAdvancement.Clockwise)
{
next_edge_index = (last_edge_index + 1) % n_edges;
}
else if (advancement == EdgeAdvancement.Counterclockwise)
{
next_edge_index = last_edge_index - 1;
if (next_edge_index < 0) next_edge_index = n_edges - 1;
}
var next_edge = vertex._edges[next_edge_index];
StartEdgeMovement(next_edge._index, next_edge.Vertices[0] == current_vertex_index);
}
/* Scenario saving */
func SaveScenarioObject(props)
{
if (!inherited(props, ...)) return false;
if (size != 4) props->AddCall("Size", this, "SetSize", size);
if (movement_speed != GetID().ActMap.Moving.Speed) props->AddCall("MoveSpeed", this, "SetMoveSpeed", movement_speed);
if (GetComDir() == COMD_None) props->Remove("ComDir");
return true;
}
func FxMoveHorizontalSaveScen(obj, fx, props)
{
props->AddCall("Move", obj, "MoveHorizontal", fx.Left, fx.Right);
return true;
}
func FxMoveVerticalSaveScen(obj, fx, props)
{
props->AddCall("Move", obj, "MoveVertical", fx.Top, fx.Bottom);
return true;
}
func FxMoveOnGraphSaveScen(obj, fx, props)
{
props->AddCall("Move", obj, "MoveOnGraph", fx.movement_graph);
return true;
}
/* Editor */
public func SetMoveType(new_movement)
{
var movement_type = new_movement.Type;
// Set movement in editor: Set default graphs
if (movement_type == MovementType.Horizontal)
{
MoveHorizontal(Max(GetX()-50, 10), Min(GetX()+50, LandscapeWidth()-10));
}
else if (movement_type == MovementType.Vertical)
{
MoveVertical(Max(GetY()-50, 10), Min(GetY()+50, LandscapeHeight()-10));
}
else if (movement_type == MovementType.Graph)
{
var x = BoundBy(GetX(), 30, LandscapeWidth()-30), y = BoundBy(GetY(), 50, LandscapeHeight()-50);
MoveOnGraph({ Vertices=[{X=x, Y=y}, {X=x-20, Y=y-40}, {X=x+20, Y=y-40}, {X=x, Y=y+40}], Edges=[{Vertices=[0, 1]}, {Vertices=[0, 2]}, {Vertices=[0, 3]}] });
}
else
{
ClearMovement();
}
return true;
}
public func Definition(def)
{
if (!def.EditorProps) def.EditorProps = {};
def.EditorProps.movement_speed = { Name="$Speed$", EditorHelp="$SpeedHelp$", Type="int", Min=5, Set="SetMoveSpeed" };
def.EditorProps.movement = { Name="$Movement$", EditorHelp="$MovementHelp$", Type="enum", Set="SetMoveType", OptionKey="Type", ValueKey="Graph", Options=[
{ Name="$NoMovement$", EditorHelp="$NoMovementHelp$", Value={Type=MovementType.None} },
{ Name="$Horizontal$", EditorHelp="$HorizontalHelp$", Value={Type=MovementType.Horizontal}, Delegate={ Type="polyline", VerticalFix=true, StructureFix=true, OnUpdate="OnHorizontalGraphUpdate", Relative=false, Color=0xff2010 } },
{ Name="$Vertical$", EditorHelp="$VerticalHelp$", Value={Type=MovementType.Vertical}, Delegate={ Type="polyline", HorizontalFix=true, StructureFix=true, OnUpdate="OnVerticalGraphUpdate", Relative=false, Color=0x20ff10 } },
{ Name="$Graph$", EditorHelp="$GraphHelp$", Value={Type=MovementType.Graph}, Delegate={ Type="graph", OnUpdate="OnGraphUpdate", Relative=false, Color=0xef8000,
EdgeDelegate = { Name="$Edge$", EditorHelp="$EdgeHelp$", EditorProps={
Speed = { Name="$Speed$", EditorHelp="$EdgeSpeedHelp$", Type="enum", Options=[
{Name="$Default$", EditorHelp="$DefaultHelp$"},
{Name="$Instant$", EditorHelp="$InstantSpeedHelp$", Value=EdgeSpeed_Infinite },
{Name="$CustomSpeed$", EditorHelp="$CustomSpeedHelp$", Type=C4V_Int, Delegate={Type="int", Min=5} }
] },
} },
VertexDelegate = { Name="$Vertex$", EditorHelp="$VertexHelp$", EditorProps={
ArrivalAction = new UserAction.Prop { Name="$ArrivalAction$", EditorHelp="$ArrivalActionHelp$" },
WaitTime = {Name="$VertexWaitTime$", EditorHelp="$VertexWaitTimeHelp$", Type="int", Min=0},
Advancement = { Name="$Advancement$", EditorHelp="$AdvancementHelp$", Type="enum", Options=[
{ Name="$Random$", EditorHelp="$RandomAdvancementHelp$", Value=EdgeAdvancement.Random },
{ Name="$Clockwise$", EditorHelp="$CWAdvancementHelp$", Value=EdgeAdvancement.Clockwise },
{ Name="$Counterclockwise$", EditorHelp="$CCWAdvancementHelp$", Value=EdgeAdvancement.Counterclockwise }
] }
} },
} },
]};
// TODO: Button to flip direction
}
public func EditCursorMoved(int old_x, int old_y, bool movement_finished)
{
// Move horizontal/vertical graph elements
if (movement.Type == MovementType.Horizontal)
{
movement.Graph = [{X=movement.Graph[0].X, Y=GetY()}, {X=movement.Graph[1].X, Y=GetY()}];
}
else if (movement.Type == MovementType.Vertical)
{
movement.Graph = [{Y=movement.Graph[0].Y, X=GetX()}, {Y=movement.Graph[1].Y, X=GetX()}];
}
else if (movement.Type == MovementType.Graph)
{
// Snap to nearest edge if mouse is let go
if (movement_finished)
{
SetPositionToNearestEdge();
}
else
{
// Stop movment during drag (will be reactivated in SetPositionToNearestEdge)
ActMap.Moving.Speed = 0;
}
}
// return true to signal engine to update shapes
return true;
}
private func OnHorizontalGraphUpdate(array vertices)
{
var fx = GetEffect("MoveHorizontal", this);
if (fx)
{
fx.Left = Min(vertices[0].X, vertices[1].X);
fx.Right = Max(vertices[0].X, vertices[1].X);
}
}
private func OnVerticalGraphUpdate(array vertices)
{
var fx = GetEffect("MoveVertical", this);
if (fx)
{
fx.Top = Min(vertices[0].Y, vertices[1].Y);
fx.Bottom = Max(vertices[0].Y, vertices[1].Y);
}
}
private func OnGraphUpdate(proplist graph)
{
// Graph update
// Update vertex indices
var vertex, ivertex = 0, n, edge, iedge = 0;
for (vertex in graph.Vertices)
{
vertex._index = ivertex++;
}
for (edge in graph.Edges)
{
edge._index = iedge++;
}
// Update all connectivities
for (vertex in graph.Vertices)
{
// Order connected edges by angle
n = 0;
vertex._edges = [];
for (var edge in graph.Edges)
{
var evtxidx = GetIndexOf(edge.Vertices, vertex._index);
if (evtxidx > -1)
{
var connected_vertex = graph.Vertices[edge.Vertices[1 - evtxidx]];
vertex._edges[n++] = edge;
edge._angle = Angle(vertex.X, vertex.Y, connected_vertex.X, connected_vertex.Y);
}
}
// Order them by angle to allow clockwise/counter-clockwise graph traversal
SortArrayByProperty(vertex._edges, "_angle");
}
// Reset position to closest edge
SetPositionToNearestEdge();
// Done
return true;
}
private func SetPositionToNearestEdge()
{
// First check if we're close to a current vertex
var x = GetX(), y = GetY();
//Log("x=%d, y=%d", x, y);
if (current_vertex_index >= 0 && current_vertex_index < GetLength(movement.Graph.Vertices))
{
var current_vertex = movement.Graph.Vertices[current_vertex_index];
if (Distance(current_vertex.X, current_vertex.Y, x, y) < 8)
{
// no reset necessery. But do restart edge movement if e.g. stuck on an unconnected vertex that just got connected.
if (!vertex_wait_time) OnVertexReached(current_vertex_index, true);
}
}
current_vertex_index = target_vertex_index = -1;
vertex_wait_time = 0;
// Reset position to closest edge
var best_distance, edge_index = 0, best_edge;
var proj_x, proj_y, edge_direction;
for (var edge in movement.Graph.Edges)
{
var v0 = movement.Graph.Vertices[edge.Vertices[0]], v1 = movement.Graph.Vertices[edge.Vertices[1]];
var dx0 = x - v0.X, dy0 = y - v0.Y;
var dx1 = v1.X - v0.X, dy1 = v1.Y - v0.Y;
var d = dx1*dx1 + dy1*dy1;
// Perpendicular distance
var dp = Abs(dx0 * dy1 - dx1 * dy0);
// Penalty if outside edge along the edge axis
var da = dx0 * dx1 + dy0 * dy1;
// Total distance (multiplied by d1 length)
var distance = dp + Max(-da) + Max(da - d);
//Log("%d (%v, %v) dist = %d", edge_index, {X=v0.X, Y=v0.Y, idx=v0._index}, {X=v1.X, Y=v1.Y, idx=v1._index}, distance);
//Log("dx0=%d, dx1=%d, dy0=%d, dy1=%d, d=%d, dp=%d, da=%d, distance=%d", dx0, dx1, dy0, dy1, d, dp, da, distance);
if (!edge_index || distance < best_distance)
{
best_edge = edge_index;
best_distance = distance;
edge_direction = (last_vx * dx1 + last_vy * dy1 >= 0);
if (best_edge == current_edge_index && best_distance/Distance(dx1, dy1) < 6)
{
// Stay on current edge
StartEdgeMovement(best_edge, edge_direction);
return;
}
da = BoundBy(da, 0, d); // Da da da! Stay inside edge range!
proj_x = x - dx0 + dx1 * da / d;
proj_y = y - dy0 + dy1 * da / d;
}
++edge_index;
}
current_edge_index = -1;
if (!edge_index) return; // No edges. Nothing to do.
//Log("Snap to edge %d", best_edge);
// Project position onto closest edge
SetPosition(proj_x, proj_y);
// Start movement along this edge
StartEdgeMovement(best_edge, edge_direction);
}
/* Properties */
local ActMap = {
Moving = {
Prototype = Action,
Name = "Moving",
Procedure = DFA_FLOAT,
Length = 1,
Delay = 1,
X = 0,
Y = 0,
Accel = 20,
Decel = 20,
Speed = 100,
Wdt = 40,
Hgt = 8,
NextAction = "Moving",
},
};
local Name = "MovingBrick";
local Plane = 200;
local SolidMaskPlane = 150; // move almost everything so background stuff can be put onto moving bricks