openclonk/planet/Objects.ocd/Helpers.ocd/Dialogue.ocd/Script.c

485 lines
13 KiB
C

/**
Dialogue
Attach to a non player charachter to provide a message interface.
@author Sven
*/
local dlg_target;
local dlg_name;
local dlg_info;
local dlg_progress;
local dlg_section; // if set, this string is included in progress callbacks (i.e., func Dlg_[Name]_[Section][Progress]() is called)
local dlg_status;
local dlg_interact; // default true. can be set to false to deactivate the dialogue
local dlg_attention; // if set, a red attention mark is put above the clonk
local dlg_broadcast; // if set, all non-message (i.e. menu) MessageBox calls are called as MessageBoxBroadcast.
static const DLG_Status_Active = 0; // next interaction calls progress function
static const DLG_Status_Stop = 1; // dialogue is done and menu closed on next interaction
static const DLG_Status_Remove = 2; // dialogue is removed on next interaction
static const DLG_Status_Wait = 3; // dialogue is deactivated temporarily to prevent accidental restart after dialogue end
/*-- Dialogue creation --*/
// Sets a new dialogue for a npc.
global func SetDialogue(string name, bool attention)
{
if (!this)
return;
var dialogue = CreateObjectAbove(Dialogue);
dialogue->InitDialogue(name, this, attention);
dialogue->SetObjectLayer(nil);
dialogue.Plane = this.Plane+1; // for proper placement of the attention symbol
return dialogue;
}
// Removes the existing dialogue of an object.
global func RemoveDialogue()
{
var dialogue = Dialogue->FindByTarget(this);
if (dialogue) return dialogue->RemoveObject();
return false;
}
// Find dialogue attached to a target (definition call, e.g. var dlg = Dialogue->FindByTarget(foo))
func FindByTarget(object target)
{
if (!target) return nil;
return FindObject(Find_ID(Dialogue), Find_ActionTarget(target));
}
/*-- Dialogue properties --*/
protected func Initialize()
{
// Dialogue progress to one.
dlg_progress = 1;
// Dialogue allows interaction by default.
dlg_interact = true;
// Dialogue is active by default.
dlg_status = DLG_Status_Active;
return;
}
public func InitDialogue(string name, object target, bool attention)
{
dlg_target = target;
dlg_name = name;
// Attach dialogue object to target.
if (attention)
{
// Attention: Show exclamation mark and glitter effect every five seconds
AddAttention();
}
else
{
// No attention: Set invisible action
SetAction("Dialogue", target);
RemoveAttention();
}
// Update dialogue to target.
UpdateDialogue();
// Effect on targets to remove the dialogue when target dies or is removed
AddEffect("IntDialogue", target, 1, 0, this);
// Custom dialogue initialization
if (!Call(Format("~Dlg_%s_Init", dlg_name), dlg_target))
GameCall(Format("~Dlg_%s_Init", dlg_name), this, dlg_target);
return true;
}
private func FxIntDialogueStop(object target, proplist fx, int reason, bool temp)
{
// Target removed or died: Remove dialogue
if (!temp) RemoveObject();
return FX_OK;
}
public func AddAttention()
{
// Attention: Show exclamation mark and glitter effect every five seconds
if (!dlg_attention)
{
SetAction("DialogueAttention", dlg_target);
RemoveTimer("AttentionEffect"); AddTimer("AttentionEffect", 36*5);
dlg_attention = true;
}
return true;
}
public func RemoveAttention()
{
// No longer show exclamation mark and glitter effects
if (dlg_attention)
{
RemoveTimer("AttentionEffect");
if (dlg_target) SetAction("Dialogue", dlg_target);
dlg_attention = false;
}
return true;
}
private func AttentionEffect() { return SetAction("DialogueAttentionEffect", dlg_target); }
private func UpdateDialogue()
{
// Adapt size to target. Updateing to direction does not work well for NPCs that walk around
// It's also not very intuitive if the player just walks to the attention symbol anyway.
var wdt = dlg_target->GetID()->GetDefWidth() + 10;
var hgt = dlg_target->GetID()->GetDefHeight();
//var dir = dlg_target->GetDir();
SetShape(-wdt/2, -hgt/2, wdt, hgt);
// Transfer target name.
//SetName(Format("$MsgSpeak$", dlg_target->GetName()));
return;
}
public func SetDialogueInfo()
{
return;
}
public func SetInteraction(bool allow)
{
dlg_interact = allow;
return;
}
public func SetDialogueProgress(int progress, string section, bool add_attention)
{
dlg_progress = Max(1, progress);
dlg_section = section;
if (add_attention) AddAttention();
return;
}
public func SetDialogueStatus(int status)
{
dlg_status = status;
return;
}
// to be called from within dialogue after the last message
public func StopDialogue()
{
// put on wait for a while; then reenable
SetDialogueStatus(DLG_Status_Wait);
ScheduleCall(this, this.SetDialogueStatus, 30, 1, DLG_Status_Stop);
return true;
}
/*-- Interaction --*/
// Players can talk to NPC via the interaction bar.
public func IsInteractable() { return dlg_interact; }
// Adapt appearance in the interaction bar.
public func GetInteractionMetaInfo(object clonk)
{
if (InDialogue(clonk))
return { Description = Format("$MsgSpeak$", dlg_target->GetName()) , IconName = nil, IconID = Clonk, Selected = true };
return { Description = Format("$MsgSpeak$", dlg_target->GetName()) , IconName = nil, IconID = Clonk, Selected = false };
}
// Advance dialogue from script
public func CallDialogue(object clonk, progress, string section)
{
if (GetType(progress)) SetDialogueProgress(progress, section);
return Interact(clonk);
}
// Called on player interaction.
public func Interact(object clonk)
{
// Should not happen: not active -> stop interaction
if (!dlg_interact)
return true;
// Currently in a dialogue: abort that dialogue.
if (InDialogue(clonk))
clonk->CloseMenu();
// No conversation context: abort.
if (!dlg_name)
return true;
// Dialogue still waiting? Do nothing then
// (A sound might be nice here)
if (dlg_status == DLG_Status_Wait)
{
return true;
}
// Stop dialogue?
if (dlg_status == DLG_Status_Stop)
{
clonk->CloseMenu();
dlg_status = DLG_Status_Active;
return true;
}
// Remove dialogue?
if (dlg_status == DLG_Status_Remove)
{
clonk->CloseMenu();
RemoveObject();
return true;
}
// Remove attention mark on first interaction
RemoveAttention();
// Have speakers face each other
SetSpeakerDirs(dlg_target, clonk);
// Start conversation context.
// Update dialogue progress first.
var progress = dlg_progress;
dlg_progress++;
// Then call relevant functions.
// Call generic function first, then progress function
var fn_generic = Format("~Dlg_%s", dlg_name);
var fn_progress = Format("~Dlg_%s_%s%d", dlg_name, dlg_section ?? "", progress);
if (!Call(fn_generic, clonk))
if (!GameCall(fn_generic, this, clonk, dlg_target))
if (!Call(fn_progress, clonk))
GameCall(fn_progress, this, clonk, dlg_target);
return true;
}
private func InDialogue(object clonk)
{
return clonk->GetMenu() == Dialogue;
}
public func MessageBoxAll(string message, object talker, bool as_message)
{
for(var i = 0; i < GetPlayerCount(C4PT_User); ++i)
{
var plr = GetPlayerByIndex(i, C4PT_User);
MessageBox(message, GetCursor(plr), talker, plr, as_message);
}
}
// Message box as dialog to player with a message copy to all other players
public func MessageBoxBroadcast(string message, object clonk, object talker, array options)
{
// message copy to other players
for(var i = 0; i < GetPlayerCount(C4PT_User); ++i)
{
var plr = GetPlayerByIndex(i, C4PT_User);
if (GetCursor(plr) != clonk)
MessageBox(message, GetCursor(plr), talker, plr, true);
}
// main message as dialog box
return MessageBox(message, clonk, talker, nil, false, options);
}
static MessageBox_last_talker, MessageBox_last_pos;
private func MessageBox(string message, object clonk, object talker, int for_player, bool as_message, array options)
{
// broadcast enabled: message copy to other players
if (dlg_broadcast && !as_message)
{
for(var i = 0; i < GetPlayerCount(C4PT_User); ++i)
{
var other_plr = GetPlayerByIndex(i, C4PT_User);
if (GetCursor(other_plr) != clonk)
MessageBox(message, GetCursor(other_plr), talker, other_plr, true);
}
}
// Use current NPC as talker if unspecified.
// On definition call or without talker, just show the message without a source
if (!talker && this != Dialogue) talker = dlg_target;
if (talker) message = Format("<c %x>%s:</c> %s", talker->GetColor(), talker->GetName(), message);
var portrait;
if (talker) portrait = talker->~GetPortrait();
// A target Clonk is given: Use a menu for this dialogue.
if (clonk && !as_message)
{
var menu_target, cmd;
if (this != Dialogue)
{
menu_target = this;
cmd = "MenuOK";
}
clonk->CreateMenu(Dialogue, menu_target, C4MN_Extra_None, nil, nil, C4MN_Style_Dialog, false, Dialogue);
// Add NPC portrait.
//var portrait = Format("%i", talker->GetID()); //, Dialogue, talker->GetColor(), "1");
if (talker)
if (portrait)
clonk->AddMenuItem("", nil, Dialogue, nil, clonk, nil, C4MN_Add_ImgPropListSpec, portrait);
else
clonk->AddMenuItem("", nil, Dialogue, nil, clonk, nil, C4MN_Add_ImgObject, talker);
// Add NPC message.
clonk->AddMenuItem(message, nil, nil, nil, clonk, nil, C4MN_Add_ForceNoDesc);
// Add answers.
if (options) for (var option in options)
{
var option_text, option_command;
if (GetType(option) == C4V_Array)
{
// Text+Command given
option_text = option[0];
option_command = option[1];
if (GetChar(option_command) == GetChar("#"))
{
// Command given as section name: Remove leading # and call section change
var ichar=1, ocmd = "", c;
while (c = GetChar(option_command, ichar++)) ocmd = Format("%s%c", ocmd, c);
option_command = Format("CallDialogue(Object(%d), 1, \"%s\")", clonk->ObjectNumber(), ocmd);
}
else
{
// if only a command is given, the standard parameter is just the clonk
if (!WildcardMatch(option_command, "*(*")) option_command = Format("%s(Object(%d))", option_command, clonk->ObjectNumber());
}
}
else
{
// Only text given - command means regular dialogue advance
option_text = option;
option_command = cmd;
}
clonk->AddMenuItem(option_text, option_command, nil, nil, clonk, nil, C4MN_Add_ForceNoDesc);
}
// If there are no answers, add a next entry
if (cmd && !options) clonk->AddMenuItem("$Next$", cmd, nil, nil, clonk, nil, C4MN_Add_ForceNoDesc);
// Set menu decoration.
clonk->SetMenuDecoration(GUI_MenuDeco);
// Set text progress to NPC name.
if (talker)
{
var name = talker->GetName();
var n_length;
while (GetChar(name, n_length))
n_length++;
clonk->SetMenuTextProgress(n_length + 1);
}
}
else
{
// No target is given: Global (player) message
if (!GetType(for_player)) for_player = NO_OWNER;
// altenate left/right position as speakers change
if (talker != MessageBox_last_talker) MessageBox_last_pos = !MessageBox_last_pos;
MessageBox_last_talker = talker;
var flags = 0, xoff = 150;
if (!MessageBox_last_pos)
{
flags = MSG_Right;
xoff *= -1;
CustomMessage("", nil, for_player); // clear prev msg
}
else
{
CustomMessage("", nil, for_player, 0,0, nil, nil, nil, MSG_Right); // clear prev msg
}
CustomMessage(message, nil, for_player, xoff,150, nil, GUI_MenuDeco, portrait ?? talker, flags);
}
return;
}
public func MenuOK(proplist menu_id, object clonk)
{
// prevent the menu from closing when pressing MenuOK
if (dlg_interact)
Interact(clonk);
}
// Enable or disable message broadcasting to all players for important dialogues
public func SetBroadcast(bool to_val)
{
dlg_broadcast = to_val;
return true;
}
public func SetSpeakerDirs(object speaker1, object speaker2)
{
// Force direction of two clonks to ace each other for dialogue
if (!speaker1 || !speaker2) return false;
speaker1->SetDir(speaker1->GetX() < speaker2->GetX());
speaker2->SetDir(speaker1->GetX() > speaker2->GetX());
return true;
}
/* Scenario saving */
// Scenario saving
func SaveScenarioObject(props)
{
if (!inherited(props, ...)) return false;
if (!dlg_target) return false; // don't save dead dialogue object
// Dialog has its own creation procedure
props->RemoveCreation();
props->Remove("Plane"); // updated when setting dialogue
props->Add(SAVEOBJ_Creation, "%s->SetDialogue(%v,%v)", dlg_target->MakeScenarioSaveName(), dlg_name, !!dlg_attention);
// Force dependency on all contained objects, so dialogue initialization procedure can access them
var i=0, obj;
while (obj = dlg_target->Contents(i++)) obj->MakeScenarioSaveName();
return true;
}
/* Properties */
local ActMap = {
Dialogue = {
Prototype = Action,
Name = "Dialogue",
Procedure = DFA_ATTACH,
Delay = 0,
NextAction = "Dialogue",
},
DialogueAttention = {
Prototype = Action,
Name = "DialogueAttention",
Procedure = DFA_ATTACH,
X = 0, Y = 0, Wdt = 8, Hgt = 24, OffX = 0, OffY = -30,
Delay = 0,
NextAction = "DialogueAttention",
},
DialogueAttentionEffect = {
Prototype = Action,
Name = "DialogueAttentionEffect",
Procedure = DFA_ATTACH,
X = 0, Y = 0, Wdt = 8, Hgt = 24, OffX = 0, OffY = -30,
Delay = 2,
Length = 4,
NextAction = "DialogueAttentionREffect",
},
DialogueAttentionREffect = {
Prototype = Action,
Name = "DialogueAttentionREffect",
Procedure = DFA_ATTACH,
X = 0, Y = 0, Wdt = 8, Hgt = 24, OffX = 0, OffY = -30,
Delay = 2,
Length = 4,
Reverse = 1,
NextAction = "DialogueAttention",
}
};
local Name = "$Name$";