-
-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
350 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
...icExtension/src/main/kotlin/com/typewritermc/basic/entries/dialogue/InputDialogueEntry.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package com.typewritermc.basic.entries.dialogue | ||
|
||
import com.typewritermc.basic.entries.dialogue.messengers.input.BedrockInputDialogueDialogueMessenger | ||
import com.typewritermc.basic.entries.dialogue.messengers.input.JavaInputDialogueDialogueMessenger | ||
import com.typewritermc.core.books.pages.Colors | ||
import com.typewritermc.core.entries.Ref | ||
import com.typewritermc.core.entries.emptyRef | ||
import com.typewritermc.core.extension.annotations.* | ||
import com.typewritermc.core.interaction.EntryContextKey | ||
import com.typewritermc.core.interaction.InteractionContext | ||
import com.typewritermc.core.utils.failure | ||
import com.typewritermc.core.utils.ok | ||
import com.typewritermc.core.utils.replaceAll | ||
import com.typewritermc.engine.paper.entry.Criteria | ||
import com.typewritermc.engine.paper.entry.Modifier | ||
import com.typewritermc.engine.paper.entry.TriggerableEntry | ||
import com.typewritermc.engine.paper.entry.dialogue.DialogueMessenger | ||
import com.typewritermc.engine.paper.entry.entries.ConstVar | ||
import com.typewritermc.engine.paper.entry.entries.DialogueEntry | ||
import com.typewritermc.engine.paper.entry.entries.SpeakerEntry | ||
import com.typewritermc.engine.paper.entry.entries.Var | ||
import com.typewritermc.engine.paper.snippets.snippet | ||
import com.typewritermc.engine.paper.utils.isFloodgate | ||
import org.bukkit.entity.Player | ||
import java.time.Duration | ||
import java.util.* | ||
import kotlin.reflect.KClass | ||
|
||
interface InputDialogueEntry : DialogueEntry { | ||
@MultiLine | ||
@Placeholder | ||
@Colored | ||
val text: Var<String> | ||
val duration: Var<Duration> | ||
} | ||
|
||
@Entry("text_input_dialogue", "An input dialogue which excepts any text", Colors.CYAN, "fa6-solid:keyboard") | ||
@ContextKeys(TextInputContextKeys::class) | ||
class TextInputDialogueEntry( | ||
override val id: String = "", | ||
override val name: String = "", | ||
override val criteria: List<Criteria> = emptyList(), | ||
override val modifiers: List<Modifier> = emptyList(), | ||
override val triggers: List<Ref<TriggerableEntry>> = emptyList(), | ||
override val speaker: Ref<SpeakerEntry> = emptyRef(), | ||
override val text: Var<String> = ConstVar(""), | ||
override val duration: Var<Duration> = ConstVar(Duration.ZERO), | ||
) : InputDialogueEntry { | ||
override fun messenger(player: Player, context: InteractionContext): DialogueMessenger<*> { | ||
return messenger(player, context, this, TextInputContextKeys.TEXT) { ok(it) } | ||
} | ||
} | ||
|
||
enum class TextInputContextKeys(override val klass: KClass<*>) : EntryContextKey { | ||
@KeyType(String::class) | ||
TEXT(String::class), | ||
} | ||
|
||
val integerInputNotAIntegerMessage: String by snippet( | ||
"dialogue.input.error.integer.not_a_integer", | ||
"<red>Input must be an integer" | ||
) | ||
val numberInputOutOfRangeMessage: String by snippet( | ||
"dialogue.input.error.number.out_of_range", | ||
"<red>Input must be between <min> and <max>" | ||
) | ||
|
||
@Entry("integer_input_dialogue", "An input dialogue which excepts an integer", Colors.CYAN, "fa6-solid:keyboard") | ||
@ContextKeys(IntegerInputContextKeys::class) | ||
class IntegerInputDialogueEntry( | ||
override val id: String = "", | ||
override val name: String = "", | ||
override val criteria: List<Criteria> = emptyList(), | ||
override val modifiers: List<Modifier> = emptyList(), | ||
override val triggers: List<Ref<TriggerableEntry>> = emptyList(), | ||
override val speaker: Ref<SpeakerEntry> = emptyRef(), | ||
override val text: Var<String> = ConstVar(""), | ||
override val duration: Var<Duration> = ConstVar(Duration.ZERO), | ||
val range: Optional<ClosedRange<Int>> = Optional.empty(), | ||
) : InputDialogueEntry { | ||
override fun messenger(player: Player, context: InteractionContext): DialogueMessenger<*> { | ||
return messenger(player, context, this, IntegerInputContextKeys.NUMBER) parser@{ | ||
val number = it.toIntOrNull() ?: return@parser failure(integerInputNotAIntegerMessage) | ||
if (range.isPresent && number !in range.get()) { | ||
return@parser failure( | ||
numberInputOutOfRangeMessage | ||
.replaceAll( | ||
"<min>" to range.get().start.toString(), | ||
"<max>" to range.get().endInclusive.toString() | ||
) | ||
) | ||
} | ||
|
||
ok(number) | ||
} | ||
} | ||
} | ||
|
||
enum class IntegerInputContextKeys(override val klass: KClass<*>) : EntryContextKey { | ||
@KeyType(Int::class) | ||
NUMBER(Int::class), | ||
} | ||
|
||
val doubleInputNotADoubleMessage: String by snippet( | ||
"dialogue.input.error.double.not_a_double", | ||
"<red>Input must be a double" | ||
) | ||
|
||
@Entry("double_input_dialogue", "An input dialogue which excepts a double", Colors.CYAN, "fa6-solid:keyboard") | ||
@ContextKeys(DoubleInputContextKeys::class) | ||
class DoubleInputDialogueEntry( | ||
override val id: String = "", | ||
override val name: String = "", | ||
override val criteria: List<Criteria> = emptyList(), | ||
override val modifiers: List<Modifier> = emptyList(), | ||
override val triggers: List<Ref<TriggerableEntry>> = emptyList(), | ||
override val speaker: Ref<SpeakerEntry> = emptyRef(), | ||
override val text: Var<String> = ConstVar(""), | ||
override val duration: Var<Duration> = ConstVar(Duration.ZERO), | ||
val range: Optional<ClosedRange<Double>> = Optional.empty(), | ||
) : InputDialogueEntry { | ||
override fun messenger(player: Player, context: InteractionContext): DialogueMessenger<*> { | ||
return messenger(player, context, this, DoubleInputContextKeys.NUMBER) parser@{ | ||
val number = it.toDoubleOrNull() ?: return@parser failure(doubleInputNotADoubleMessage) | ||
if (range.isPresent && number !in range.get()) { | ||
return@parser failure( | ||
numberInputOutOfRangeMessage.replaceAll( | ||
"<min>" to range.get().start.toString(), | ||
"<max>" to range.get().endInclusive.toString() | ||
) | ||
) | ||
} | ||
|
||
ok(number) | ||
} | ||
} | ||
} | ||
|
||
enum class DoubleInputContextKeys(override val klass: KClass<*>) : EntryContextKey { | ||
@KeyType(Double::class) | ||
NUMBER(Double::class), | ||
} | ||
|
||
private fun <T : Any> messenger( | ||
player: Player, | ||
context: InteractionContext, | ||
entry: InputDialogueEntry, | ||
key: EntryContextKey, | ||
parser: (String) -> Result<T>, | ||
): DialogueMessenger<InputDialogueEntry> { | ||
return if (player.isFloodgate) { | ||
BedrockInputDialogueDialogueMessenger(player, context, entry, key, parser) | ||
} else { | ||
JavaInputDialogueDialogueMessenger(player, context, entry, key, parser) | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
...writermc/basic/entries/dialogue/messengers/input/BedrockInputDialogueDialogueMessenger.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package com.typewritermc.basic.entries.dialogue.messengers.input | ||
|
||
import com.typewritermc.basic.entries.dialogue.InputDialogueEntry | ||
import com.typewritermc.core.interaction.EntryContextKey | ||
import com.typewritermc.core.interaction.InteractionContext | ||
import com.typewritermc.engine.paper.entry.dialogue.DialogueMessenger | ||
import com.typewritermc.engine.paper.entry.dialogue.MessengerState | ||
import com.typewritermc.engine.paper.extensions.placeholderapi.parsePlaceholders | ||
import com.typewritermc.engine.paper.utils.legacy | ||
import org.bukkit.entity.Player | ||
import org.geysermc.cumulus.form.CustomForm | ||
|
||
class BedrockInputDialogueDialogueMessenger<T : Any>( | ||
player: Player, | ||
context: InteractionContext, | ||
entry: InputDialogueEntry, | ||
private val key: EntryContextKey, | ||
private val parser: (String) -> Result<T>, | ||
) : DialogueMessenger<InputDialogueEntry>(player, context, entry) { | ||
|
||
override fun init() { | ||
super.init() | ||
sendForm() | ||
} | ||
|
||
private fun sendForm() { | ||
org.geysermc.floodgate.api.FloodgateApi.getInstance().sendForm( | ||
player.uniqueId, | ||
CustomForm.builder() | ||
.title("<bold>${entry.speakerDisplayName.get(player).parsePlaceholders(player)}</bold>".legacy()) | ||
.label("${entry.text.get(player).parsePlaceholders(player).legacy()}\n\n") | ||
.input("Value") | ||
.closedOrInvalidResultHandler { _, _ -> | ||
state = MessengerState.CANCELLED | ||
} | ||
.validResultHandler { _, response -> | ||
val input = response.asInput() | ||
if (input == null) { | ||
sendForm() | ||
return@validResultHandler | ||
} | ||
val value = parser(input) | ||
if (value.isFailure) { | ||
sendForm() | ||
return@validResultHandler | ||
} | ||
val data = value.getOrNull() ?: return@validResultHandler | ||
context[entry, key] = data | ||
state = MessengerState.FINISHED | ||
} | ||
) | ||
} | ||
} |
138 changes: 138 additions & 0 deletions
138
...ypewritermc/basic/entries/dialogue/messengers/input/JavaInputDialogueDialogueMessenger.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package com.typewritermc.basic.entries.dialogue.messengers.input | ||
|
||
import com.typewritermc.basic.entries.dialogue.InputDialogueEntry | ||
import com.typewritermc.core.interaction.EntryContextKey | ||
import com.typewritermc.core.interaction.InteractionContext | ||
import com.typewritermc.engine.paper.entry.dialogue.DialogueMessenger | ||
import com.typewritermc.engine.paper.entry.dialogue.MessengerState | ||
import com.typewritermc.engine.paper.entry.dialogue.TickContext | ||
import com.typewritermc.engine.paper.entry.dialogue.typingDurationType | ||
import com.typewritermc.engine.paper.extensions.placeholderapi.parsePlaceholders | ||
import com.typewritermc.engine.paper.interaction.chatHistory | ||
import com.typewritermc.engine.paper.snippets.snippet | ||
import com.typewritermc.engine.paper.utils.* | ||
import io.papermc.paper.event.player.AsyncChatEvent | ||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder | ||
import org.bukkit.entity.Player | ||
import org.bukkit.event.EventHandler | ||
import java.time.Duration | ||
|
||
val inputFormat: String by snippet( | ||
"dialogue.input.format", """ | ||
|<gray><st>${" ".repeat(60)}</st> | ||
| | ||
|<gray><padding>[ <bold><speaker></bold><reset><gray> ] | ||
| | ||
|<message> | ||
| | ||
|<gray><info_padding><info_text> | ||
|<gray><st>${" ".repeat(60)}</st> | ||
""".trimMargin() | ||
) | ||
|
||
val inputInfoText: String by snippet("dialogue.input.info", "Type answer in chat") | ||
|
||
val inputPadding: String by snippet("dialogue.input.padding", " ") | ||
val inputMinLength: Int by snippet("dialogue.input.minLength", 1) | ||
val inputMaxLineLength: Int by snippet("dialogue.input.maxLineLength", 40) | ||
|
||
class JavaInputDialogueDialogueMessenger<T : Any>( | ||
player: Player, | ||
context: InteractionContext, | ||
entry: InputDialogueEntry, | ||
private val key: EntryContextKey, | ||
private val parser: (String) -> Result<T>, | ||
) : | ||
DialogueMessenger<InputDialogueEntry>(player, context, entry) { | ||
|
||
private var speakerDisplayName = "" | ||
private var text = "" | ||
private var infoText = "" | ||
private var typingDuration = Duration.ZERO | ||
private var playedTime = Duration.ZERO | ||
|
||
override var isCompleted: Boolean | ||
// We don't want to be able to complete the dialogue if the player clicks on a npc. | ||
get() { | ||
if (!infoText.startsWith("<red>")) { | ||
infoText = "<red>$infoText" | ||
} | ||
return state == MessengerState.FINISHED | ||
} | ||
set(value) { | ||
playedTime = if (!value) Duration.ZERO | ||
else typingDuration | ||
} | ||
|
||
override fun init() { | ||
super.init() | ||
|
||
speakerDisplayName = entry.speakerDisplayName.get(player).parsePlaceholders(player) | ||
text = entry.text.get(player).parsePlaceholders(player) | ||
infoText = inputInfoText | ||
typingDuration = typingDurationType.totalDuration(text.stripped(), entry.duration.get(player)) | ||
} | ||
|
||
@EventHandler | ||
private fun onPlayerChat(event: AsyncChatEvent) { | ||
if (event.player.uniqueId != player.uniqueId) return | ||
event.isCancelled = true | ||
val message = event.message().plainText() | ||
val result = parser(message) | ||
if (result.isFailure) { | ||
infoText = result.exceptionOrNull()?.message ?: "<red>Invalid input" | ||
player.sendInputDialogue() | ||
return | ||
} | ||
|
||
val value = result.getOrNull() ?: return | ||
context[entry, key] = value | ||
state = MessengerState.FINISHED | ||
} | ||
|
||
private val percentage: Double | ||
get() = if (!typingDuration.isZero) playedTime.toMillis().toDouble() / typingDuration.toMillis() else 1.2 | ||
|
||
override fun tick(context: TickContext) { | ||
if (state != MessengerState.RUNNING) return | ||
playedTime += context.deltaTime | ||
|
||
val playedTicks = playedTime.toTicks() | ||
|
||
// When the messages is send we don't want to keep sending the message every tick. | ||
// However, we only start reducing after the message has been displayed fully. | ||
if (percentage > 1.1) { | ||
// Change in highlight color | ||
val shouldDisplay = playedTicks % 50 == 0L | ||
if (!shouldDisplay) return | ||
} | ||
|
||
player.sendInputDialogue() | ||
} | ||
|
||
private fun Player.sendInputDialogue() { | ||
val rawText = text.stripped() | ||
val resultingLines = rawText.limitLineLength(inputMaxLineLength).lineCount | ||
|
||
val message = text.asPartialFormattedMini( | ||
percentage, | ||
padding = inputPadding, | ||
minLines = inputMinLength.coerceAtLeast(resultingLines), | ||
maxLineLength = inputMaxLineLength | ||
) | ||
|
||
val charactersLeft = (50 - infoText.stripped().length).coerceAtLeast(0) | ||
val charactersPadding = charactersLeft / 2 | ||
|
||
val component = inputFormat.asMiniWithResolvers( | ||
Placeholder.parsed("speaker", speakerDisplayName), | ||
Placeholder.component("message", message), | ||
Placeholder.parsed("info_text", infoText), | ||
Placeholder.unparsed("info_padding", " ".repeat(charactersPadding)), | ||
Placeholder.parsed("padding", inputPadding) | ||
) | ||
|
||
val componentWithDarkMessages = chatHistory.composeDarkMessage(component) | ||
sendMessage(componentWithDarkMessages) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters