/** Sequence Cutscene to be watched by all players. Start calling global func StartSequence, stop using StopSequence Can also be used as a trigger object for UserActions. @author Sven */ local seq_name; local seq_progress; local started; /* Start and stop */ public func Start(string name, int progress, ...) { if (started) Stop(); // Force global coordinates for the script execution. SetPosition(0, 0); // Store sequence name and progress. this.seq_name = name; this.seq_progress = progress; // Call init function of this scene - difference to start function is that it is called before any player joins. var fn_init = Format("~%s_Init", seq_name); if (!Call(fn_init, ...)) GameCall(fn_init, this, ...); // Join all players: disable player controls and call join player of this scene. for (var i = 0; i < GetPlayerCount(C4PT_User); ++i) { var plr = GetPlayerByIndex(i, C4PT_User); JoinPlayer(plr); } started = true; // Sound effect. Sound("UI::Ding", true); // Call start function of this scene. var fn_start = Format("%s_Start", seq_name); if (!Call(fn_start, ...)) GameCall(fn_start, this, ...); return true; } protected func InitializePlayer(int plr) { if (seq_name) { // Scripted sequence JoinPlayer(plr); } else { // Editor-made sequence if (trigger && trigger.Trigger == "player_join") OnTrigger(nil, plr); } return true; } protected func InitializePlayers() { if (!seq_name) { // Editor-made sequence if (trigger && trigger.Trigger == "game_start") OnTrigger(nil, GetPlayerByIndex(0, C4PT_User)); } return true; } public func RemovePlayer(int plr) { if (seq_name) { // Scripted sequence // Called by sequence if it ends and by engine if player leaves. var fn_remove = Format("~%s_RemovePlayer", seq_name); if (!Call(fn_remove, plr)) GameCall(fn_remove, this, plr); } else { // Editor-made sequence if (trigger && trigger.Trigger == "player_remove") OnTrigger(nil, plr); } return true; } public func JoinPlayer(int plr) { DeactivatePlayerControls(plr, true); // Per-player sequence callback. var fn_join = Format("~%s_JoinPlayer", seq_name); if (!Call(fn_join, plr)) GameCall(fn_join, this, plr); return true; } public func DeactivatePlayerControls(int plr, bool make_invincible) { var j = 0, crew; while (crew = GetCrew(plr, j++)) { //if (crew == GetCursor(plr)) crew.Sequence_was_cursor = true; else crew.Sequence_was_cursor = nil; crew->SetCrewEnabled(false); crew->CancelUse(); if (crew->GetMenu()) if (!crew->GetMenu()->~Uncloseable()) crew->CancelMenu(); if (make_invincible) { crew->MakeInvincible(); crew.Sequence_stored_breath = crew->GetBreath(); crew.Sequence_made_invincible = true; } crew->SetCommand("None"); crew->SetComDir(COMD_Stop); } return true; } public func ReactivatePlayerControls(int plr) { var j = 0, crew; while (crew = GetCrew(plr, j++)) { crew->SetCrewEnabled(true); if (crew.Sequence_made_invincible) { crew->ClearInvincible(); // just in case clonk was underwater var breath_diff = crew.Sequence_stored_breath - crew->GetBreath(); crew.Sequence_stored_breath = nil; if (breath_diff) crew->DoBreath(breath_diff + 100); // give some bonus breath for the distraction crew.Sequence_made_invincible = nil; } } // Ensure proper cursor. if (!GetCursor(plr)) SetCursor(plr, GetCrew(plr)); crew = GetCursor(plr); if (crew) SetPlrView(plr, crew); return true; } public func Stop(bool no_remove) { if (started) { SetViewTarget(nil); // Reenable crew and reset cursor. for (var i = 0; iMessageBoxAll(message, talker, as_message, ...); } private func MessageBox(string message, object clonk, object talker, int for_player, bool as_message, ...) { return Dialogue->MessageBox(message, clonk, talker, for_player, as_message, ...); } /*-- Helper Functions --*/ // Helper function to get a speaker in sequences. public func GetHero(object nearest_obj) { // Prefer object stored as hero - if not assigned, find someone close to specified object. if (!this.hero) { if (nearest_obj) this.hero = nearest_obj->FindObject(Find_ID(Clonk), Find_Not(Find_Owner(NO_OWNER)), nearest_obj->Sort_Distance()); else this.hero = FindObject(Find_ID(Clonk), Find_Not(Find_Owner(NO_OWNER))); } // If there is still no hero, take any clonk. Let the NPCs do the sequence among themselves. // (to prevent null pointer exceptions if all players left during the sequence) if (!this.hero) this.hero = FindObject(Find_ID(Clonk)); // Might return nil if all players are gone and there are no NPCs. Well, there was noone to listen anyway. return this.hero; } // Scenario section overload: automatically transfers all player clonks. public func LoadScenarioSection(name, ...) { // Store objects: All clonks and sequence object this.save_objs = []; AddSectSaveObj(this); for (var iplr = 0; iplr < GetPlayerCount(C4PT_User); ++iplr) { var plr = GetPlayerByIndex(iplr, C4PT_User); for (var icrew = 0; icrew < GetCrewCount(iplr); ++icrew) { var crew = GetCrew(plr, icrew); if (crew) AddSectSaveObj(crew); } } var save_objs = this.save_objs; // Load new section var result = inherited(name, ...); // Restore objects for (var obj in save_objs) if (obj) obj->SetObjectStatus(C4OS_NORMAL); if (this) this.save_objs = nil; // Recover HUD for (var iplr = 0; iplr < GetPlayerCount(C4PT_User); ++iplr) { var plr = GetPlayerByIndex(iplr, C4PT_User); var controllerDef = Library_HUDController->GetGUIControllerID(); var HUDcontroller = FindObject(Find_ID(controllerDef), Find_Owner(plr)); if (!HUDcontroller) HUDcontroller = CreateObjectAbove(controllerDef, 10, 10, plr); } return result; } // Flag obj and any contained stuff for scenario saving. public func AddSectSaveObj(object obj) { if (!obj) return false; this.save_objs[GetLength(this.save_objs)] = obj; var cont, i = 0; while (cont = obj->Contents(i++)) AddSectSaveObj(cont); return obj->SetObjectStatus(C4OS_INACTIVE); } /*-- Global helper functions --*/ // Starts the specified sequence at indicated progress. global func StartSequence(string name, int progress, ...) { var seq = CreateObject(Sequence, 0, 0, NO_OWNER); seq->Start(name, progress, ...); return seq; } // Stops the currently active sequence. global func StopSequence() { var seq = FindObject(Find_ID(Sequence)); if (!seq) return false; return seq->Stop(); } // Returns the currently active sequence. global func GetActiveSequence() { var seq = FindObject(Find_ID(Sequence)); return seq; } /* User-made sequences from the editor */ local Name="$Name$"; local Description="$Description$"; local trigger, condition, action, action_progress_mode, action_allow_parallel; local active=true; local check_interval=12; local deactivate_after_action; // If true, finished is set to true after the first execution and the trigger deactivated local Visibility=VIS_Editor; local trigger_started; local trigger_offset; // Timer offset of trigger to allow non-synced triggers public func IsSequence() { return true; } // finished: Disables the trigger. true if trigger has run and deactivate_after_action is set to true. // Note that this flag is not saved in scenarios, so saving as scenario and reloading will re-enable all triggers (for editor mode) local finished; public func Initialize() { // The default action is an empty sequence, because that's usually what the user wants // Must init this in initialize to force a writable array action = { Function="sequence", Actions=[] }; } public func Definition(def) { // EditorActions if (!def.EditorActions) def.EditorActions = {}; def.EditorActions.Test = { Name="$Test$", EditorHelp = "$TestHelp$", Command="OnTrigger(nil, %player%, true)" }; // UserActions UserAction->AddEvaluator("Action", "$Name$", "$SetActive$", "$SetActiveDesc$", "sequence_set_active", [def, def.EvalAct_SetActive], { Target = { Function="action_object" }, Status = { Function="bool_constant", Value=true } }, { Type="proplist", Display="{{Target}}: {{Status}}", EditorProps = { Target = UserAction->GetObjectEvaluator("IsSequence", "$Name$"), Status = new UserAction.Evaluator.Boolean { Name="$Status$", EditorHelp="$SetActiveStatusHelp$" } } } ); UserAction->AddEvaluator("Action", "$Name$", "$ActTrigger$", "$ActTriggerDesc$", "sequence_trigger", [def, def.EvalAct_Trigger], { Target = { Function="action_object" }, TriggeringObject = { Function="triggering_object" } }, { Type="proplist", Display="{{Target}}({{TriggeringObject}})", EditorProps = { Target = new UserAction->GetObjectEvaluator("IsSequence", "$Name$") { Priority=60 }, TriggeringObject = new UserAction.Evaluator.Object { Name="$TriggeringObject$", EditorHelp="$TriggeringObjectHelp$" } } } ); UserAction->AddEvaluator("Action", "$Name$", "$DisablePlayerControls$", "$DisablePlayerControlsHelp$", "sequence_disable_player_controls", [def, def.EvalAct_DisablePlayerControls], { Players = { Function="all_players" }, MakeInvincible = { Function="bool_constant", Value=true } }, { Type="proplist", Display="{{Players}}", EditorProps = { Players = new UserAction.Evaluator.PlayerList { Name="$Players$", EditorHelp="$PlayerControlsPlayersHelp$" }, MakeInvincible = new UserAction.Evaluator.Boolean { Name="$MakeInvincible$", EditorHelp="$MakeInvincibleHelp$" } } } ); UserAction->AddEvaluator("Action", "$Name$", "$EnablePlayerControls$", "$EnablePlayerControlsHelp$", "sequence_enable_player_controls", [def, def.EvalAct_EnablePlayerControls], { Players = { Function="all_players" } }, new UserAction.Evaluator.PlayerList { Name="$Players$", EditorHelp="$PlayerControlsPlayersHelp$" }, "Players"); // EditorProps if (!def.EditorProps) def.EditorProps = {}; def.EditorProps.active = { Name="$Active$", Type="bool", Set="SetActive" }; def.EditorProps.finished = { Name="$Finished$", Type="bool", Set="SetFinished" }; def.EditorProps.trigger = { Name="$Trigger$", Type="enum", OptionKey="Trigger", Set="SetTrigger", Priority=110, Options = [ { Name="$None$" }, { Name="$PlayerEnterRegionRect$", EditorHelp="$PlayerEnterRegionHelp$", Value={ Trigger="player_enter_region_rect", Rect=[-20, -20, 40, 40] }, ValueKey="Rect", Delegate={ Type="rect", Color=0xff8000, Relative=true, Set="SetTriggerRect", SetRoot=true } }, { Name="$PlayerEnterRegionCircle$", EditorHelp="$PlayerEnterRegionHelp$", Value={ Trigger="player_enter_region_circle", Radius=25 }, ValueKey="Radius", Delegate={ Type="circle", Color=0xff8000, Relative=true, Set="SetTriggerRadius", SetRoot=true } }, { Name="$ObjectEnterRegionRect$", EditorHelp="$ObjectEnterRegionHelp$", Value={ Trigger="object_enter_region_rect", Rect=[-20, -20, 40, 40] }, Delegate={ Name="$ObjectEnterRegionRect$", EditorHelp="$ObjectEnterRegionHelp$", Type="proplist", EditorProps = { ID = { Name="$ID$", EditorHelp="$IDHelp$", Type="def", Set="SetTriggerID", SetRoot=true }, Rect = { Type="rect", Color=0xff8000, Relative=true, Set="SetTriggerRect", SetRoot=true } } } }, { Name="$ObjectEnterRegionCircle$", EditorHelp="$ObjectEnterRegionHelp$", Value={ Trigger="object_enter_region_circle", Radius=25 }, Delegate={ Name="$ObjectEnterRegionCircle$", EditorHelp="$ObjectEnterRegionHelp$", Type="proplist", EditorProps = { ID = { Name="$ID$", EditorHelp="$IDHelp$", Type="def", Set="SetTriggerID", SetRoot=true }, Radius = { Type="circle", Color=0xff8000, Relative=true, Set="SetTriggerRadius", SetRoot=true } } } }, { Name="$ObjectCountInContainer$", EditorHelp="$ObjectCountInContainerHelp$", Value={ Trigger="contained_object_count", Count=1 }, Delegate={ Name="$ObjectCountInContainer$", EditorHelp="$ObjectCountInContainerHelp$", Type="proplist", EditorProps = { Container = { Name="$Container$", EditorHelp="$CountContainerHelp$", Type="object" }, ID = { Name="$ID$", EditorHelp="$CountIDHelp$", Type="def", EmptyName="$AnyID$" }, Count = { Name="$Count$", Type="int", Min=1 }, Operation = { Name="$Operation$", EditorHelp="$CountOperationHelp$", Type="enum", Options = [ { Name="$GreaterEqual$", EditorHelp="$GreaterEqualHelp$" }, { Name="$LessThan$", EditorHelp="$LessThanHelp$", Value="lt" } ] } } } }, { Name="$Interval$", EditorHelp="$IntervalHelp$", Value={ Trigger="interval", Interval=60 }, ValueKey="Interval", Delegate={ Name="$IntervalTime$", Type="int", Min=1, Set="SetIntervalTimer", SetRoot=true } }, { Name="$GameStart$", Value={ Trigger="game_start" } }, { Name="$PlayerJoin$", Value={ Trigger="player_join" } }, { Name="$PlayerRemove$", Value={ Trigger="player_remove" } }, { Name="$GoalsFulfilled$", Value={ Trigger="goals_fulfilled" } }, { Group="$ClonkDeath$", Name="$AnyClonkDeath$", Value={ Trigger="any_clonk_death" } }, { Group="$ClonkDeath$", Name="$PlayerClonkDeath$", Value={ Trigger="player_clonk_death" } }, { Group="$ClonkDeath$", Name="$NeutralClonkDeath$", Value={ Trigger="neutral_clonk_death" } }, { Group="$ClonkDeath$", Name="$SpecificClonkDeath$", Value={ Trigger="specific_clonk_death" }, ValueKey="Object", Delegate={ Type="object", Filter="IsClonk" } }, { Name="$Construction$", Value={ Trigger="construction" }, ValueKey="ID", Delegate={ Type="def", Filter="IsStructure", EmptyName="$Anything$" } }, { Name="$Production$", Value={ Trigger="production" }, ValueKey="ID", Delegate={ Type="def", EmptyName="$Anything$" } }, ] }; def.EditorProps.condition = new UserAction.Evaluator.Boolean { Name="$Condition$" }; def.EditorProps.action = new UserAction.Prop { Priority=105 }; def.EditorProps.action_progress_mode = UserAction.PropProgressMode; def.EditorProps.action_allow_parallel = UserAction.PropParallel; def.EditorProps.deactivate_after_action = { Name="$DeactivateAfterAction$", Type="bool" }; def.EditorProps.check_interval = { Name="$CheckInterval$", EditorHelp="$CheckIntervalHelp$", Type="int", Set="SetCheckInterval", Save="Interval" }; def.EditorProps.trigger_offset = { Name="$CheckOffset$", EditorHelp="$CheckOffsetHelp$", Type="int", Set="SetTriggerOffset" }; } public func SetTrigger(proplist new_trigger, int check_offset) { trigger = new_trigger; // Compute actual trigger time offset based on current frame counter if (GetType(check_offset) && check_interval > 0) { check_offset -= FrameCounter(); if (check_offset < 0) check_offset -= ((check_offset/check_interval)-1) * check_interval; check_offset %= check_interval; } // Set trigger: Restart any specific trigger timers if (active && !finished) StartTrigger(check_offset); return true; } public func SetTriggerRect(array new_trigger_rect) { if (trigger && trigger.Rect) { trigger.Rect = new_trigger_rect; SetTrigger(trigger); // restart trigger } return true; } public func SetTriggerRadius(int new_trigger_radius) { if (trigger) { trigger.Radius = new_trigger_radius; SetTrigger(trigger); // restart trigger } return true; } public func SetTriggerID(id new_id) { if (trigger) { trigger.ID = new_id; SetTrigger(trigger); // restart trigger } return true; } public func GetCheckOffset() { // Get timer offset of check function } public func SetAction(new_action, new_action_progress_mode, new_action_allow_parallel) { action = new_action; action_progress_mode = new_action_progress_mode; action_allow_parallel = new_action_allow_parallel; return true; } public func SetCondition(new_condition) { condition = new_condition; return true; } public func SetActive(bool new_active, bool force_triggers) { if (active == new_active && !force_triggers) return true; active = new_active; if (active && !finished) { // Activated: Start trigger StartTrigger(); } else { // Inactive or inactive by editor run: Stop trigger StopTrigger(); } return true; } public func SetFinished(bool new_finished) { finished = new_finished; return SetActive(active, true); } public func SetDeactivateAfterAction(bool new_val) { deactivate_after_action = new_val; return true; } public func StartTrigger(int start_delay) { if (!trigger) return false; if (trigger_started) StopTrigger(); trigger_started = true; SetGraphics("Active"); var fn = trigger.Trigger; var id_search, timer_fn; if (trigger.ID) id_search = Find_ID(trigger.ID); if (fn == "player_enter_region_rect") { this.search_mask = Find_And(Find_InRect(trigger.Rect[0], trigger.Rect[1], trigger.Rect[2], trigger.Rect[3]), Find_OCF(OCF_Alive), Find_Func("IsClonk"), Find_Not(Find_Owner(NO_OWNER))); timer_fn = this.EnterRegionTimer; } else if (fn == "player_enter_region_circle") { this.search_mask = Find_And(Find_Distance(trigger.Radius), Find_OCF(OCF_Alive), Find_Func("IsClonk"), Find_Not(Find_Owner(NO_OWNER))); timer_fn = this.EnterRegionTimer; } else if (fn == "object_enter_region_rect") { this.search_mask = Find_And(Find_InRect(trigger.Rect[0], trigger.Rect[1], trigger.Rect[2], trigger.Rect[3]), id_search); timer_fn = this.EnterRegionTimer; } else if (fn == "object_enter_region_circle") { this.search_mask = Find_And(Find_Distance(trigger.Radius), Find_OCF(OCF_Alive), Find_Func("IsClonk"), id_search); timer_fn = this.EnterRegionTimer; } else if (fn == "contained_object_count") { timer_fn = this.CountContainedObjectsTimer; } else if (fn == "interval") { check_interval = trigger.Interval; timer_fn = this.OnTrigger; } else { trigger_offset = 0; return false; } // If a timer was started, remember its offset trigger_offset = (FrameCounter() + start_delay) % Max(1, check_interval); // Start directly or delayed if (start_delay > 0) { ScheduleCall(this, Global.AddTimer, start_delay, 1, timer_fn, check_interval); } else { AddTimer(timer_fn, check_interval); } return true; } public func SetTriggerOffset(int new_trigger_offset) { if (trigger_offset != new_trigger_offset) { // Schedule trigger restart to set correct offset SetTrigger(trigger, (trigger_offset = new_trigger_offset)); } return true; } public func StopTrigger() { SetGraphics(); // Remove any timers that may have been added RemoveTimer(this.EnterRegionTimer); RemoveTimer(this.CountContainedObjectsTimer); RemoveTimer(this.OnTrigger); ClearScheduleCall(this, Global.AddTimer); trigger_started = false; return true; } public func SetCheckInterval(new_interval) { check_interval = Max(1, new_interval); return SetTrigger(trigger); // restart trigger } public func SetIntervalTimer(int new_interval) { if (trigger) trigger.Interval = new_interval; return SetTrigger(trigger); // restart trigger } private func EnterRegionTimer() { for (var clonk in FindObjects(this.search_mask)) { if (!clonk) continue; // deleted by previous execution OnTrigger(clonk, clonk->GetOwner()); if (active != true) break; // deactivated by trigger } } private func CountContainedObjectsTimer() { if (trigger.Container) { var n = trigger.Container->ContentsCount(trigger.ID), f; if (!trigger.Operation) f = (n >= trigger.Count); // Operation == nil: greater than else f = (n < trigger.Count); // Operation == "lt": less than if (f) OnTrigger(nil, NO_OWNER); } } public func OnTrigger(object triggering_clonk, int triggering_player, bool is_editor_test) { // Check condition if (condition && !UserAction->EvaluateCondition(condition, this, triggering_clonk, triggering_player)) return false; // Only one action at the time if (!action_allow_parallel && !action_progress_mode) StopTrigger(); // Execute action return UserAction->EvaluateAction(action, this, triggering_clonk, triggering_player, action_progress_mode, action_allow_parallel, this.OnActionFinished); } private func OnActionFinished(context) { // Callback from EvaluateAction: Action finished. Deactivate action if desired. if (deactivate_after_action) SetFinished(true); else if (active && !finished && !trigger_started) StartTrigger(); return true; } public func OnClonkDeath(object clonk, int killer) { // Is this a clonk death trigger? if (!trigger || !clonk) return false; var t = trigger.Trigger; if (!WildcardMatch(t, "*_clonk_death")) return false; // Specific trigger check if (t == "player_clonk_death") { if (clonk->GetOwner() == NO_OWNER) return false; } else if (t == "neutral_clonk_death") { if (clonk->GetOwner() != NO_OWNER) return false; } else if (t == "specific_clonk_death") { if (trigger.Object != clonk) return false; } // OK, trigger it! return OnTrigger(clonk, killer); } public func OnConstructionFinished(object structure, int constructing_player) { // Is this a structure finished trigger? if (!trigger || !structure) return false; if (trigger.Trigger != "construction") return false; if (trigger.ID) if (structure->GetID() != trigger.ID) return false; // OK, trigger it! return OnTrigger(structure, constructing_player); } public func OnProductionFinished(object product, int producing_player) { // Is this a structure finished trigger? if (!trigger || !product) return false; if (trigger.Trigger != "production") return false; if (trigger.ID) if (product->GetID() != trigger.ID) return false; // OK, trigger it! return OnTrigger(product, producing_player); } public func OnGoalsFulfilled() { // All goals fulfilled: Return true if any action is executed (stops regular GameOver) if (!trigger) return false; if (trigger.Trigger != "goals_fulfilled") return false; return OnTrigger(); } public func SetName(string new_name, ...) { if (new_name == GetID()->GetName()) { Message(""); } else { if (trigger) Message(Format("@%s", new_name)); else Message(Format("@%s", new_name)); } return inherited(new_name, ...); } private func EvalAct_SetActive(proplist props, proplist context) { // User action: Enable/disable sequence var target = UserAction->EvaluateValue("Object", props.Target, context); var status = UserAction->EvaluateValue("Boolean", props.Status, context); if (!target) return; if (status && target.finished) target->~SetFinished(false); target->~SetActive(status); } private func EvalAct_Trigger(proplist props, proplist context) { var target = UserAction->EvaluateValue("Object", props.Target, context); var triggering_object = UserAction->EvaluateValue("Object", props.TriggeringObject, context); if (target && target->~IsSequence()) target->OnTrigger(triggering_object, nil, false); } private func EvalAct_DisablePlayerControls(proplist props, proplist context) { var players = UserAction->EvaluateValue("PlayerList", props.Players, context) ?? []; var is_invincible = UserAction->EvaluateValue("Boolean", props.MakeInvincible, context); for (var player in players) DeactivatePlayerControls(player, is_invincible); } private func EvalAct_EnablePlayerControls(proplist props, proplist context) { var players = UserAction->EvaluateValue("PlayerList", props.Players, context) ?? []; for (var player in players) ReactivatePlayerControls(player); } /*-- Saving --*/ // No scenario saving. public func SaveScenarioObject(props, ...) { if (!_inherited(props, ...)) return false; // Do not save script-created sequences if (this.seq_name) return false; // Save editor-made sequences if (save_scenario_dup_objects && finished) // finished flag only copied for object duplication; not saved in savegames props->AddCall("Active", this, "SetFinished", finished); if (!active) props->AddCall("Active", this, "SetActive", active); if (trigger) props->AddCall("Trigger", this, "SetTrigger", trigger, trigger_offset); if (condition) props->AddCall("Condition", this, "SetCondition", condition); if ((action && !DeepEqual(action, { Function="sequence", Actions=[] })) || action_progress_mode || action_allow_parallel) props->AddCall("Action", this, "SetAction", action, Format("%v", action_progress_mode), action_allow_parallel); if (deactivate_after_action) props->AddCall("DeactivateAfterAction", this, "SetDeactivateAfterAction", deactivate_after_action); return true; }