From 4d9252dd807adad6c4740b87683416dc8917cd66 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 10 Feb 2026 19:14:50 +0900 Subject: [PATCH] world control hint --- assets/locales/en/terrarum.json | 4 +- src/net/torvald/terrarum/IngameInstance.kt | 14 ++ .../torvald/terrarum/gameitems/GameItem.kt | 8 +- .../terrarum/modulebasegame/TerrarumIngame.kt | 1 + .../gameitems/FixtureItemBase.kt | 5 + .../modulebasegame/gameitems/WireCutterAll.kt | 2 + .../modulebasegame/ui/UIWorldControlHint.kt | 123 ++++++++++++++++++ 7 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 src/net/torvald/terrarum/modulebasegame/ui/UIWorldControlHint.kt diff --git a/assets/locales/en/terrarum.json b/assets/locales/en/terrarum.json index 308b5bf33..784782dbe 100644 --- a/assets/locales/en/terrarum.json +++ b/assets/locales/en/terrarum.json @@ -7,7 +7,9 @@ "CONTEXT_TIME_SECOND_PLURAL": "Seconds", "COPYRIGHT_ALL_RIGHTS_RESERVED": "All rights reserved", "COPYRIGHT_GNU_GPL_3": "Distributed under GNU GPL 3", + "GAME_ACTION_CHANGE_COLOR" : "Change color", "GAME_ACTION_MOVE_VERB" : "Move", + "GAME_ACTION_PUT_DOWN" : "Put down", "GAME_ACTION_ZOOM" : "Zoom", "GAME_ACTION_ZOOM_OUT" : "Zoom out", "MENU_IO_AUTOSAVE": "Autosave", @@ -54,7 +56,7 @@ "MENU_OPTIONS_SPEAKER_HEADPHONE": "Headphone", "MENU_OPTIONS_SPEAKER_SETUP": "Speaker Setup", "MENU_OPTIONS_SPEAKER_STEREO": "Stereo", - "MENU_OPTIONS_STREAMERS_LAYOUT": "Chat Overlay", + "MENU_OPTIONS_STREAMERS_LAYOUT": "Space for Chat Overlay", "MENU_CREDIT_GPL_DNT" : "GPL", "MENU_LABEL_JVM_DNT" : "JVM", diff --git a/src/net/torvald/terrarum/IngameInstance.kt b/src/net/torvald/terrarum/IngameInstance.kt index b0ea0e1d6..a17c2a5d4 100644 --- a/src/net/torvald/terrarum/IngameInstance.kt +++ b/src/net/torvald/terrarum/IngameInstance.kt @@ -128,6 +128,16 @@ open class IngameInstance(val batch: FlippingSpriteBatch, val isMultiplayer: Boo * Nullability of this property is believed to be unavoidable (trust me!). I'm sorry for the inconvenience. */ open var actorNowPlaying: ActorHumanoid? = null + + protected open val currentlyDisplayingControlHints = ControlHint(null, null) + + fun setControlHint(primary: String?, secondary: String?) { + currentlyDisplayingControlHints.primary = primary + currentlyDisplayingControlHints.secondary = secondary + } + + fun getCurrentControlHint() = currentlyDisplayingControlHints.copy() + /** * The actual gamer */ @@ -635,3 +645,7 @@ class ProtectedActorRemovalException(whatisit: String, caller: Throwable) : Exce val INGAME: IngameInstance get() = Terrarum.ingame!! +/** + * Control Hints are always referred against Lang for display. To not display a hint for a specific hand, use `null` + */ +data class ControlHint(var primary: String?, var secondary: String?) \ No newline at end of file diff --git a/src/net/torvald/terrarum/gameitems/GameItem.kt b/src/net/torvald/terrarum/gameitems/GameItem.kt index 001c4b201..aaa8f6fdc 100644 --- a/src/net/torvald/terrarum/gameitems/GameItem.kt +++ b/src/net/torvald/terrarum/gameitems/GameItem.kt @@ -311,12 +311,16 @@ abstract class GameItem(val originalID: ItemID) : TooltipListener(), Comparable< /** * Effects applied (continuously or not) while being equipped (drawn/pulled out) */ - open fun effectWhileEquipped(actor: ActorWithBody, delta: Float) { } + open fun effectWhileEquipped(actor: ActorWithBody, delta: Float) { + INGAME.setControlHint("GAME_INVENTORY_USE", null) // the most generic control hints + } /** * Effects applied only once when unequipped */ - open fun effectOnUnequip(actor: ActorWithBody) { } + open fun effectOnUnequip(actor: ActorWithBody) { + INGAME.setControlHint(null, null) // hide the control hints + } override fun toString(): String { diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt index 69c81ff24..0640df167 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt @@ -672,6 +672,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { uiWatchTierOne, getWearableDeviceUI, UIScreenZoom(), + UIWorldControlHint(), uiAutosaveNotifier, uiInventoryPlayer, diff --git a/src/net/torvald/terrarum/modulebasegame/gameitems/FixtureItemBase.kt b/src/net/torvald/terrarum/modulebasegame/gameitems/FixtureItemBase.kt index 907491a8c..016ed6875 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameitems/FixtureItemBase.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameitems/FixtureItemBase.kt @@ -62,6 +62,9 @@ open class FixtureItemBase(originalID: ItemID, val fixtureClassName: String) : G override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) { + INGAME.setControlHint("GAME_ACTION_PUT_DOWN", "GAME_ACTION_PICK_UP") // the most generic control hints + + // println("ghost: ${ghostItem}; ghostInit = $ghostInit; instance: $hash") if (!ghostInit.compareAndExchangeAcquire(false, true)) { ghostItem.set(makeFixture(originalID.getModuleName())) @@ -92,6 +95,8 @@ open class FixtureItemBase(originalID: ItemID, val fixtureClassName: String) : G } override fun effectOnUnequip(actor: ActorWithBody) { + super.effectOnUnequip(actor) + // ghostInit = false (INGAME as TerrarumIngame).blockMarkingActor.let { diff --git a/src/net/torvald/terrarum/modulebasegame/gameitems/WireCutterAll.kt b/src/net/torvald/terrarum/modulebasegame/gameitems/WireCutterAll.kt index 1c2d212be..892f3e5a7 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameitems/WireCutterAll.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameitems/WireCutterAll.kt @@ -135,10 +135,12 @@ class WireCutterAll(originalID: ItemID) : GameItem(originalID), FixtureInteracti } override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) { + INGAME.setControlHint("GAME_INVENTORY_USE", "GAME_ACTION_CHANGE_COLOR") // the most generic control hints (Terrarum.ingame!! as TerrarumIngame).selectedWireRenderClass = "wire_render_all" } override fun effectOnUnequip(actor: ActorWithBody) { + super.effectOnUnequip(actor) (Terrarum.ingame!! as TerrarumIngame).selectedWireRenderClass = "" selectorUI.setAsClose() } diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldControlHint.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldControlHint.kt new file mode 100644 index 000000000..878430442 --- /dev/null +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldControlHint.kt @@ -0,0 +1,123 @@ +package net.torvald.terrarum.modulebasegame.ui + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import net.torvald.terrarum.App +import net.torvald.terrarum.INGAME +import net.torvald.terrarum.Terrarum +import net.torvald.terrarum.gameitems.mouseInInteractableRange +import net.torvald.terrarum.langpack.Lang +import net.torvald.terrarum.modulebasegame.TerrarumIngame +import net.torvald.terrarum.modulebasegame.gameactors.DroppedItem +import net.torvald.terrarum.ui.Movement +import net.torvald.terrarum.ui.Toolkit +import net.torvald.terrarum.ui.UICanvas +import net.torvald.terrarum.ui.UINotControllable +import net.torvald.terrarum.modulebasegame.gameactors.FixtureBase +import net.torvald.unicode.getKeycapPC +import net.torvald.unicode.getMouseButton +import kotlin.math.roundToInt + +/** + * Created by minjaesong on 2026-02-10. + */ +@UINotControllable +class UIWorldControlHint : UICanvas() { + + init { + handler.allowESCtoClose = false + handler.setAsAlwaysVisible() + } + + private fun getHintText(): String { + val control = INGAME.getCurrentControlHint() + return if (control.primary != null && control.secondary != null) + "${getMouseButton(App.getConfigInt("control_mouse_primary"))} ${Lang[control.primary!!]}\u3000" + + "${getMouseButton(App.getConfigInt("control_mouse_secondary"))} ${Lang[control.secondary!!]}" + else if (control.primary != null) + "${getMouseButton(App.getConfigInt("control_mouse_primary"))} ${Lang[control.primary!!]}\u3000" + else if (control.secondary != null) + "${getMouseButton(App.getConfigInt("control_mouse_secondary"))} ${Lang[control.secondary!!]}" + else + "" + } + + // always use getter to accomodate a language change + private fun getFixtureHintText(): String = + "${getMouseButton(App.getConfigInt("control_mouse_primary"))} ${Lang["GAME_INVENTORY_USE"]}\u3000" + + "${getMouseButton(App.getConfigInt("control_mouse_secondary"))} ${Lang["GAME_ACTION_PICK_UP"]}" + + + private var cachedText = "" + + override var width = 480 + override var height = App.fontGame.lineHeight.toInt() + + override var openCloseTime = 0.2f + + override val mouseUp = false + + private var opacityCounter = 0f + + private fun hasFixtureUnderMouse(): Boolean { + return mouseInInteractableRange(INGAME.actorNowPlaying ?: INGAME.actorGamer) { mwx, mwy, mtx, mty -> + val actorsUnderMouse = (INGAME as TerrarumIngame).getActorsUnderMouse(mwx, mwy) + val hasPickupableActor = actorsUnderMouse.any { + (it is FixtureBase && it.canBeDespawned && System.nanoTime() - it.spawnRequestedTime > 50000000) // give freshly spawned fixtures 0.05 seconds of immunity + } + if (hasPickupableActor) 0L else -1L + } > -1L + } + + override fun updateImpl(delta: Float) { + val fixtureUnderMouse = hasFixtureUnderMouse() + val newText = if (fixtureUnderMouse) getFixtureHintText() else getHintText() + + if (newText.isNotEmpty()) { + cachedText = newText + opacityCounter = (opacityCounter + delta).coerceAtMost(openCloseTime) + } + else { + opacityCounter = (opacityCounter - delta).coerceAtLeast(0f) + if (opacityCounter <= 0f) cachedText = "" + } + + handler.opacity = opacityCounter / openCloseTime + } + + override fun renderImpl(frameDelta: Float, batch: SpriteBatch, camera: OrthographicCamera) { + batch.color = Color.WHITE + + val text = cachedText + + val offX = Toolkit.drawWidthf - (App.scr.tvSafeGraphicsWidth * 1.25f).roundToInt().toFloat() - App.fontGame.getWidth(text) + val offY = App.scr.height - height - App.scr.tvSafeGraphicsHeight - 4f + + App.fontGame.draw(batch, text, offX, offY) + } + + override fun dispose() { + } + + // overridden to not touch the tooltips + override fun doOpening(delta: Float) { + handler.opacity = handler.openCloseCounter / openCloseTime + } + + // overridden to not touch the tooltips + override fun doClosing(delta: Float) { + handler.opacity = handler.openCloseCounter / openCloseTime + } + + // overridden to not touch the tooltips + override fun endOpening(delta: Float) { + handler.opacity = 1f + } + + // overridden to not touch the tooltips + override fun endClosing(delta: Float) { + handler.opacity = 0f + } + +} \ No newline at end of file