Introduced new dialogue engine compatible with spinoff revision 578 project

This commit is contained in:
Oven Bread 2024-11-22 06:59:42 +00:00 committed by Ryan
parent 79c69e3d43
commit 7749e8263f
4 changed files with 308 additions and 129 deletions

View file

@ -5055,6 +5055,10 @@
"npc_id": "2237",
"loc_data": "{3249,3266,0,1,5}-"
},
{
"npc_id": "2238",
"loc_data": "{3219,3247,0,1,6}-"
},
{
"npc_id": "2240",
"loc_data": "{3077,3259,0,1,4}-"

View file

@ -1,129 +0,0 @@
package content.region.misthalin.lumbridge.dialogue;
import core.game.dialogue.DialoguePlugin;
import core.game.dialogue.FacialExpression;
import core.game.node.entity.npc.NPC;
import core.game.node.entity.player.Player;
import core.plugin.Initializable;
import core.game.world.GameWorld;
/**
* Represents the dialogue plugin used for the donie npc.
* @author 'Vexia
* @version 1.00
*/
@Initializable
public final class DonieDialogue extends DialoguePlugin {
/**
* Constructs a new {@code DonieDialogue} {@code Object}.
*/
public DonieDialogue() {
/**
* empty.
*/
}
/**
* Constructs a new {@code DonieDialogue} {@code Object}.
* @param player the player.
*/
public DonieDialogue(Player player) {
super(player);
}
@Override
public DialoguePlugin newInstance(Player player) {
return new DonieDialogue(player);
}
@Override
public boolean open(Object... args) {
npc = (NPC) args[0];
interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Hello there, can I help you?");
stage = 0;
return true;
}
@Override
public boolean handle(int interfaceId, int buttonId) {
switch (stage) {
case 0:
interpreter.sendOptions("Select an Option", "Where am I?", "How are you today?", "Your shoe lace is untied.");
stage = 1;
break;
case 1:
switch (buttonId) {
case 1:
interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Where am I?");
stage = 10;
break;
case 2:
interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "How are you today?");
stage = 20;
break;
case 3:
interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Your shoe lace is untied.");
stage = 30;
break;
}
break;
case 10:
interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "This is the town of Lumbridge my friend.");
stage = 11;
break;
case 11:
interpreter.sendOptions("Select an Option", "Where am I?", "How are you today?", "Your shoe lace is untied.");
stage = 1;
break;
case 20:
interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Aye, not too bad thank you. Lovely weather in", "" + GameWorld.getSettings().getName() + " this fine day.");
stage = 21;
break;
case 21:
interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Weather?");
stage = 22;
break;
case 22:
interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Yes weather, you know.");
stage = 23;
break;
case 23:
interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "The state or condition of the atmosphere at a time and", "place, with respect to variables such as temperature,", "moisture, wind velocity, and barometric pressure.");
stage = 24;
break;
case 24:
interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "...");
stage = 25;
break;
case 25:
interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Not just a pretty face eh? Ha ha ha.");
stage = 26;
break;
case 26:
end();
break;
case 30:
interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "No it's not!");
stage = 31;
break;
case 31:
interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "No you're right. I have nothing to back that up.");
stage = 32;
break;
case 32:
interpreter.sendDialogues(npc, FacialExpression.HALF_GUILTY, "Fool! Leave me alone!");
stage = 33;
break;
case 33:
end();
break;
}
return true;
}
@Override
public int[] getIds() {
return new int[] { 2238 };
}
}

View file

@ -0,0 +1,53 @@
package content.region.misthalin.lumbridge.dialogue
import core.api.*
import core.game.dialogue.*
import core.game.node.entity.player.Player
import core.game.world.GameWorld
import core.game.world.GameWorld.settings
import core.plugin.Initializable
import org.rs09.consts.NPCs
@Initializable
class DonieDialogue (player: Player? = null) : DialoguePlugin(player) {
override fun newInstance(player: Player): DialoguePlugin {
return DonieDialogue(player)
}
override fun handle(interfaceId: Int, buttonId: Int): Boolean {
openDialogue(player, DonieDialogueFile(), npc)
return false
}
override fun getIds(): IntArray {
return intArrayOf(NPCs.DONIE_2238)
}
}
class DonieDialogueFile : DialogueLabeller() {
override fun addConversation() {
assignToIds(NPCs.DONIE_2238)
npc(ChatAnim.FRIENDLY, "Hello there, can I help you?")
options(
DialogueOption("whereami", "Where am I?", expression = ChatAnim.THINKING),
DialogueOption("howareyou", "How are you today?"),
DialogueOption("shoelace", "Your shoe lace is untied."),
)
label("whereami")
npc("This is the town of Lumbridge my friend.")
label("howareyou")
npc("Aye, not too bad thank you. Lovely weather in", ""+ (GameWorld.settings?.name ?: "2009Scape") +" this fine day.")
player("Weather?")
npc("Yes weather, you know.")
npc("The state or condition of the atmosphere at a time and", "place, with respect to variables such as temperature,", "moisture, wind velocity, and barometric pressure.")
player("...")
npc("Not just a pretty face eh? Ha ha ha.")
label("shoelace")
npc(ChatAnim.ANGRY, "No it's not!")
player("No you're right. I have nothing to back that up.")
npc(ChatAnim.ANGRY, "Fool! Leave me alone!")
}
}

View file

@ -0,0 +1,251 @@
package core.game.dialogue
import core.api.InputType
import core.api.face
import core.api.splitLines
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.item.Item
/** Alias FacialExpression to ChatAnim for compatibility. */
typealias ChatAnim = FacialExpression
/** Alias InputType to InputType for compatibility. */
typealias InputType = InputType
/** Create container class DialogueOption for [DialogueLabeller.options] */
class DialogueOption(
val goto: String,
val option: String,
val spokenText: String = option,
val expression: ChatAnim = ChatAnim.NEUTRAL,
val skipPlayer: Boolean = false,
val callback: ((player: Player, npc: NPC) -> Boolean)? = null
)
/**
* DialogueLabeller is another way to organize a dialogue file.
* It shares the same dialogue layout as another project for portability.
* - Uses [stage] is for the current loop and [super.stage] as the "next" stage
* - Have [dialogueCounter] assign stages and guards for each option/player/npc dialogue
* - Assign [exec] and [goto] the same [dialogueCounter] to execute all in one pass
* - Save [label] to a HashMap to refer to when using [goto]
* - Loops again when a [goto] is encountered, ends when no [stageHit] during a stage
*
* ! WARNING: DO NOT use functions in DialogueFile as it will cause unexpected behavior.
*
* Example: [content.region.misthalin.lumbridge.dialogue.RangedTutorDialogue]
*/
abstract class DialogueLabeller : DialogueFile() {
companion object {
/**
* Makes NPC stop in its tracks and look at player.
* An alternative to setting up DialoguePlugin(player) to be used in InteractionListener.
*/
fun captureNPC(player: Player, npc: NPC) {
face(npc, player.location)
npc.setDialoguePlayer(player) // This prevents random walking in [NPC.java handleTickActions()]
npc.getWalkingQueue().reset()
npc.getPulseManager().clear()
}
}
/** Maps labels to stage numbers, to make jumps. */
val labelStageMap = HashMap<String, Int> ()
/** Assigns the number to each dialogue line. */
var dialogueCounter = 0
/** Set [stage] to start off (1 when there is an initial label). */
var startingStage: Int? = null
/** Keeps the current stage for the whole cycle. */
override var stage = -1
/** Assigns the number to each stage. */
var stageHit = false
/** Jump to next stage number when hitting a goto. */
var jumpTo: Int? = null
/** ButtonID on every click */
var buttonID: Int? = null
/** ButtonID when user clicks on an option */
var optButton: Int? = null
/** Input value after a user enters a value */
var optInput: Any? = null
/** Implement this function instead of overriding handle. */
abstract fun addConversation()
/** Helper function to create an individual stage for each of the dialogue stages. */
private fun assignIndividualStage(callback: () -> Unit) {
if (startingStage == null) { startingStage = 0 }
if (stage == dialogueCounter) { // Run this stage when the stage equals to the dialogueCounter of this dialogue
callback() // CALLBACK FUNCTION
super.stage++ // Increment the stage to the next stage (only applies after a pass)
stageHit = true // Flag that the stage was hit, so that it doesn't close the dialogue
}
dialogueCounter++ // Increment the dialogueCounter to assign each line of dialogue
}
/** Formats a vararg messages or falls back to message to an array for spread function. */
private fun formatMessages(messages: Array<out String>?, message: String = ""): Array<out String> {
return if (messages == null || messages.isEmpty()) {
splitLines(message)
} else if (messages.size > 1) {
messages
} else {
splitLines(messages[0]) // A single line message is similar to a splitLine returning 1 line.
}
}
/** Does absolutely nothing but to make it look like the other API. */
fun assignToIds(npcid: Int) { /* super.npc = NPC(npcid) */ }
/** Marks the start of a series of dialogue that can be jumped to using a [goto]. */
fun label(label: String) {
if (startingStage == null) { startingStage = 1 }
dialogueCounter++
labelStageMap[label] = dialogueCounter
}
/** Jumps to a [label] after a series of dialogue. */
fun goto(label: String) {
if (stage == dialogueCounter) {
jumpTo = labelStageMap[label]
}
}
/** Jumps to a [label] inside an [exec]. */
fun loadLabel(player: Player, label: String) {
goto(label)
}
/**
* Executes the callback between stages and can be used for branching with [loadLabel].
* You can chain as many exec as you like since they read sequentially.
* You can also read [options] and [input] values here via [optButton] and [optInput].
*/
fun exec(callback: (player: Player, npc: NPC) -> Unit) {
if (startingStage == null) { startingStage = 0 }
if (stage == dialogueCounter) {
callback(player!!, npc!!)
}
}
/** Dialogue player/playerl. Shows player chathead with text. **/
fun player(chatAnim: ChatAnim = ChatAnim.NEUTRAL, vararg messages: String) {
assignIndividualStage { interpreter!!.sendDialogues(player, chatAnim, *formatMessages(messages)) }
}
/** Dialogue player/playerl. Shows player chathead with text. **/
fun player(vararg messages: String) { player(ChatAnim.NEUTRAL, *messages) }
@Deprecated("Use player() instead.", ReplaceWith("player(chatAnim, *messages)"))
fun playerl(chatAnim: ChatAnim = ChatAnim.NEUTRAL, vararg messages: String) { throw Exception("Deprecated DialogueLabel: Use player() instead.") }
@Deprecated("Use player() instead.", ReplaceWith("player(*messages)"))
fun playerl(vararg messages: String) { throw Exception("Deprecated DialogueLabel: Use player() instead.") }
/** Dialogue npc/npcl. Shows npc chathead with text. **/
fun npc(chatAnim: ChatAnim = ChatAnim.NEUTRAL, vararg messages: String) {
assignIndividualStage { interpreter!!.sendDialogues(npc, chatAnim, *formatMessages(messages)) }
}
/** Dialogue npc/npcl. Shows npcId chathead with text. **/
fun npc(chatAnim: ChatAnim = ChatAnim.NEUTRAL, npcId: Int, vararg messages: String) {
assignIndividualStage { interpreter!!.sendDialogues(NPC(npcId), chatAnim, *formatMessages(messages)) }
}
/** Dialogue npc/npcl. Shows npc chathead with text. **/
fun npc(vararg messages: String) { npc(ChatAnim.NEUTRAL, *messages) }
@Deprecated("Use npc() instead.", ReplaceWith("npc(chatAnim, *messages)"))
fun npcl(chatAnim: ChatAnim = ChatAnim.NEUTRAL, vararg messages: String) { throw Exception("Deprecated DialogueLabel: Use npc() instead.") }
@Deprecated("Use npc() instead.", ReplaceWith("npc(*messages)"))
fun npcl(vararg messages: String) { throw Exception("Deprecated DialogueLabel: Use npc() instead.") }
/** Dialogue item/iteml. Shows item with text. **/
fun item(item: Item, vararg messages: String, message: String = "") {
assignIndividualStage { interpreter!!.sendItemMessage(item, *formatMessages(messages, message)) }
}
@Deprecated("Use item() instead.", ReplaceWith("item(item, *messages)"))
fun iteml(item: Item, vararg messages: String) { throw Exception("Deprecated DialogueLabel: Use item() instead.") }
/** Dialogue overloaded doubleItem/doubleIteml. Shows two items with text. **/
fun item(item: Item, item2: Item, vararg messages: String, message: String = "") {
assignIndividualStage { interpreter!!.sendDoubleItemMessage(item, item2, formatMessages(messages, message).joinToString(" ")) }
}
/** Dialogue line/linel. Simply shows text. **/
fun line(vararg messages: String) {
assignIndividualStage { interpreter!!.sendDialogue(*messages) }
}
@Deprecated("Use line() instead.", ReplaceWith("line(*messages)"))
fun linel(vararg messages: String) { throw Exception("Deprecated DialogueLabel: Use line() instead.") }
/** Dialogue option. Shows the option dialogue with choices for the user to select. **/
fun options(vararg options: DialogueOption, title: String = "Select an Option") {
// Filter out options that aren't shown.
val filteredOptions = options.filter{ if (it.callback != null) { it.callback.invoke(player!!, npc!!) } else { true } }
// Stage Part 1: Options List Dialogue
assignIndividualStage { interpreter!!.sendOptions(title, *filteredOptions.map{ it.option }.toTypedArray()) }
// Stage Part 2: Show spoken text.
var opt = if (buttonID != null && buttonID in 1..filteredOptions.size) { filteredOptions[buttonID!! - 1] } else { null }
assignIndividualStage {
if (opt?.skipPlayer == true) {
jumpTo = stage + 1
} else {
interpreter!!.sendDialogues(player, opt?.expression ?: ChatAnim.NEUTRAL, *(splitLines(opt?.spokenText ?: " ")))
}
optButton = buttonID // transfer the buttonID to a temp memory for the next stage
}
// Stage Part 3: Jump To goto
if (stage == dialogueCounter && optButton != null && optButton in 1..filteredOptions.size) {
jumpTo = labelStageMap[filteredOptions[optButton!! - 1].goto]
}
dialogueCounter++
}
@Deprecated("Use options(DialogueOption()) and not options(string).", ReplaceWith("options(DialogueOption(options))"))
override fun options(vararg options: String?, title: String) { throw Exception("Deprecated DialogueLabel: Use options(DialogueOption()) and not options(string).") }
/** Dialogue input. Shows the input dialogue with an input box for the user to type in. Read [optInput] for the value. **/
fun input(type: InputType, prompt: String = "Enter the amount") {
assignIndividualStage {
// These are similar to calling sendInputDialogue
when (type) {
InputType.AMOUNT -> interpreter!!.sendInput(true, prompt)
InputType.NUMERIC -> interpreter!!.sendInput(false, prompt)
InputType.STRING_SHORT -> interpreter!!.sendInput(true, prompt) // Only 12 letters
InputType.STRING_LONG -> interpreter!!.sendLongInput(prompt) // Very long text, can overflow.
InputType.MESSAGE -> interpreter!!.sendMessageInput(prompt)
}
if (type == InputType.AMOUNT) {
player!!.setAttribute("parseamount", true)
}
player!!.setAttribute("runscript") { value: Any ->
optInput = value
// The next line is a hack. Because this prompt is overlays the actual chatbox, we trigger the next dialogue with a call to handle.
interpreter!!.handle(player!!.interfaceManager.chatbox.id, 2)
}
player!!.setAttribute("input-type", type)
}
}
/** Dialogue input. Shows the input dialogue with an input box for the user to type in. Read [optInput] in an [exec] function for the value. **/
fun input(numeric: Boolean, prompt: String = "Enter the amount") { input( if (numeric) { InputType.NUMERIC } else { InputType.STRING_SHORT }, prompt) }
/** Hook onto the handle function of DialogueFile. This function gets called every loop with a super.stage. */
override fun handle(componentID: Int, buttonID: Int) {
this.buttonID = buttonID
startingStage = null
/** This -1 stage is to read labels into a hashmap and to find the starting stage. */
if (stage == -1) {
dialogueCounter = 0
addConversation() // Force all labels to be recorded into hashmap.
super.stage = startingStage ?: 0
}
for (i in 0..10) { // Limit to 10 jumpTo PER DIALOGUE LINE to prevent infinite looping/ping-ponging jumps.
if (jumpTo != null) { // If jumpTo is set, set the super.stage stage as the new jumpTo stage.
super.stage = jumpTo as Int
jumpTo = null
}
stageHit = false
stage = super.stage // [stage] is for the current loop. [super.stage] is treated as the "next" stage.
dialogueCounter = 0
addConversation() // MAIN CALLBACK FUNCTION
// If there is no jumpTo set, or jumpTo is set to the same stage as this(infinite loop), exit.
if (jumpTo == null || jumpTo == stage) {
break
}
}
if (!stageHit) { end() } // If a dialogue stage is not hit, end the dialogues.
}
}