Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .data/raw-cache/server/loc/doors.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,38 @@ contentGroup = "content.closed_left_picketgate"
[object.params]
"param.next_loc_stage" = "loc.rustic_openfencegate_l"
"param.opensound" = "synth.picketgate_open"
# ── Al-Kharid palace double doors (content/areas/city/al-kharid, M5.ALKHARID.1) ──
# Ported from the pre-sync AlKharidLocEditor. Tagging the closed/open pair with the canonical
# door content groups is what makes the generic DoubleDoorScript bind Open + Close on both
# halves with the standard revert timer; `next_loc_stage` wires each half to its counterpart.
[[object]]
id = "loc.palacedoor_l"
inherit = "loc.palacedoor_l"
contentGroup = "content.closed_left_door"

[object.params]
"param.next_loc_stage" = "loc.openpalacedoor_l"

[[object]]
id = "loc.openpalacedoor_l"
inherit = "loc.openpalacedoor_l"
contentGroup = "content.opened_left_door"

[object.params]
"param.next_loc_stage" = "loc.palacedoor_l"

[[object]]
id = "loc.palacedoor_r"
inherit = "loc.palacedoor_r"
contentGroup = "content.closed_right_door"

[object.params]
"param.next_loc_stage" = "loc.openpalacedoor_r"

[[object]]
id = "loc.openpalacedoor_r"
inherit = "loc.openpalacedoor_r"
contentGroup = "content.opened_right_door"

[object.params]
"param.next_loc_stage" = "loc.palacedoor_r"
125 changes: 125 additions & 0 deletions .data/raw-cache/server/npcs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4496,3 +4496,128 @@ contentGroup = "content.dark_crab_fishing_spot"
moveRestrict = "NoMove"
wanderRange = 0

# ══════════════════════════════════════════════════════════════════════════════
# Al-Kharid module (content/areas/city/al-kharid, M5.F1)
# Ported from the pre-sync AlKharidNpcEditor.
# ══════════════════════════════════════════════════════════════════════════════

# Bankers: the rev240 cache does not tag the Al-Kharid bank-booth NPCs with `content.banker`,
# so the generic Banker script (Talk-to / Bank / Collect) would never bind without this overlay.
[[npc]]
id = "npc.kharidbanker1"
inherit = "npc.kharidbanker1"
contentGroup = "content.banker"
wanderRange = 0

[[npc]]
id = "npc.kharidbanker2"
inherit = "npc.kharidbanker2"
contentGroup = "content.banker"
wanderRange = 0

# Toll-gate border guards — anchored so they cannot wander off the gate.
[[npc]]
id = "npc.borderguard1"
inherit = "npc.borderguard1"
wanderRange = 0

[[npc]]
id = "npc.borderguard2"
inherit = "npc.borderguard2"
wanderRange = 0

# Named shopkeepers. Sell/buy/change percentages match each OSRS wiki shop page; without them
# `Shops.open` falls back to the cache param defaults (1300 / 400 / 30), which are wrong for
# every one of these stores. Stock itself comes from the upstream `.data/raw-cache/server/shops`
# corpus (inv.gemshop / inv.scimitarshop / inv.legsshop / inv.skirtshop / inv.craftingshop).
[[npc]]
id = "npc.gem_trader"
inherit = "npc.gem_trader"
wanderRange = 1

[npc.params]
"param.shop_sell_percentage" = 1000
"param.shop_buy_percentage" = 700
"param.shop_change_percentage" = 30

[[npc]]
id = "npc.zeke"
inherit = "npc.zeke"
moveRestrict = "Indoors"

[npc.params]
"param.shop_sell_percentage" = 1000
"param.shop_buy_percentage" = 550
"param.shop_change_percentage" = 20

[[npc]]
id = "npc.louie_legs"
inherit = "npc.louie_legs"
moveRestrict = "Indoors"

[npc.params]
"param.shop_sell_percentage" = 1000
"param.shop_buy_percentage" = 650
"param.shop_change_percentage" = 10

[[npc]]
id = "npc.ranael"
inherit = "npc.ranael"
moveRestrict = "Indoors"

[npc.params]
"param.shop_sell_percentage" = 1000
"param.shop_buy_percentage" = 650
"param.shop_change_percentage" = 10

[[npc]]
id = "npc.dommik"
inherit = "npc.dommik"
moveRestrict = "Indoors"

[npc.params]
"param.shop_sell_percentage" = 1000
"param.shop_buy_percentage" = 650
"param.shop_change_percentage" = 20

# Silk trader — Talk-to-only haggle, no shop interface (OSRS canon).
[[npc]]
id = "npc.silk_trader"
inherit = "npc.silk_trader"
wanderRange = 1

# Prince Ali Rescue quest NPCs — anchored, indoors where appropriate.
[[npc]]
id = "npc.hassan"
inherit = "npc.hassan"
moveRestrict = "Indoors"
wanderRange = 0

[[npc]]
id = "npc.osman"
inherit = "npc.osman"
wanderRange = 0

[[npc]]
id = "npc.lady_keli"
inherit = "npc.lady_keli"
wanderRange = 0

[[npc]]
id = "npc.joe"
inherit = "npc.joe"
wanderRange = 0

[[npc]]
id = "npc.prince_ali_prison"
inherit = "npc.prince_ali_prison"
moveRestrict = "Indoors"
wanderRange = 0

# Ali Morrisane slot. The rev240 cache ships no dedicated `ali_morrisane` symbol; the Al-Kharid
# populace symbol stands in, and needs an explicit Talk-to op slot so the AliMorrisane dialogue
# script is reachable from the right-click menu.
[[npc]]
id = "npc.al_kharid_man"
inherit = "npc.al_kharid_man"
option1 = "Talk-to"
9 changes: 9 additions & 0 deletions content/areas/city/al-kharid/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
id("base-conventions")
}

dependencies {
implementation(projects.api.pluginCommons)
implementation(projects.api.scriptAdvanced)
implementation(projects.content.quest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.rsmod.content.areas.city.alkharid

import org.rsmod.plugin.scripts.PluginScript
import org.rsmod.plugin.scripts.ScriptContext

/**
* Top-level Al-Kharid plugin script (M5.F1).
*
* Interactive behaviour lives in dedicated per-NPC + per-loc + generic scripts:
* - [AlKharidTollGate] — toll-gate Open/Pay, free after Prince Ali Rescue.
* - [org.rsmod.content.areas.city.alkharid.quests.PrinceAliRescue] — the quest itself (M7.F5).
* - [org.rsmod.content.areas.city.alkharid.npcs.Ranael] — Ranael's Super Skirt Store.
* - [org.rsmod.content.areas.city.alkharid.npcs.LouieLegs] — Louie's Armoured Legs Bazaar.
* - [org.rsmod.content.areas.city.alkharid.npcs.Dommik] — Dommik's Crafting Store.
* - [org.rsmod.content.areas.city.alkharid.npcs.Zeke] — Zeke's Superior Scimitars.
* - [org.rsmod.content.areas.city.alkharid.npcs.GemTrader] — Gem Trader shop.
* - [org.rsmod.content.areas.city.alkharid.npcs.SilkTrader] — Talk-to-only silk haggle (no shop
* interface, per OSRS canon).
* - [org.rsmod.content.areas.city.alkharid.npcs.AliMorrisane] — F2P-portion Ali dialogue.
* - [org.rsmod.content.areas.city.alkharid.npcs.Karim] — Talk-to-only kebab sale, 1gp each.
* - [org.rsmod.content.areas.city.alkharid.npcs.AlKharidGeneralStore] — the two general-store
* clerks opening `inv.generalshop1`.
*
* Everything the pre-sync build expressed as Kotlin config now lives as data:
* - Bank booths use the canonical generic-loc binding in `generic-locs`; the Al-Kharid bankers are
* tagged `content.banker` by an inherit-self stanza in `.data/raw-cache/server/npcs.toml` so the
* generic Banker NPC script binds Talk-to / Bank / Collect.
* - The palace double doors are tagged with the canonical `content.closed_*_door` /
* `content.opened_*_door` groups (plus a `param.next_loc_stage` round-trip) in
* `.data/raw-cache/server/loc/doors.toml`, so the generic `DoubleDoorScript` binds Open + Close.
* - Shop stock comes from upstream's `.data/raw-cache/server/shops` toml corpus; each shopkeeper
* script simply names the matching `inv.x`. The per-shop sell/buy/change percentages are npc
* params in `npcs.toml`.
* - NPC spawns come from upstream's canonical `.data/raw-cache/map/npcs/kharid_desert_region.toml`
* and `draynor.toml`, so this module ships no spawn file of its own.
*/
class AlKharidScript : PluginScript() {
override fun ScriptContext.startup() {
// Intentionally empty: interactive behaviour lives in per-NPC + per-loc + generic scripts.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
@file:Suppress("SpellCheckingInspection")

package org.rsmod.content.areas.city.alkharid

import jakarta.inject.Inject
import org.rsmod.api.invtx.invTakeFee
import org.rsmod.api.player.protect.ProtectedAccess
import org.rsmod.api.script.onOpLoc1
import org.rsmod.api.script.onOpLoc4
import org.rsmod.api.script.onOpNpc1
import org.rsmod.content.areas.city.alkharid.quests.PrinceAliRescue
import org.rsmod.game.entity.Npc
import org.rsmod.map.CoordGrid
import org.rsmod.plugin.scripts.PluginScript
import org.rsmod.plugin.scripts.ScriptContext

/**
* Al-Kharid toll gate — Open / Pay-toll(10gp) on the Lumbridge↔Al-Kharid gate pair.
*
* Free passage is a Prince Ali Rescue completion reward, read through the quest engine
* ([PrinceAliRescue.quest]) rather than off the raw `varp.princequest` value.
*/
class AlKharidTollGate @Inject constructor(private val princeAliRescue: PrinceAliRescue) :
PluginScript() {
override fun ScriptContext.startup() {
// Default gate (from map data): Open=op1, Pay-toll(10gp)=op4.
onOpLoc1(GATE_DEFAULT_LEFT) { openGate() }
onOpLoc1(GATE_DEFAULT_RIGHT) { openGate() }
onOpLoc4(GATE_DEFAULT_LEFT) { payToll() }
onOpLoc4(GATE_DEFAULT_RIGHT) { payToll() }

// 2op variant: Open=op1, Pay-toll(10gp)=op2.
onOpLoc1(GATE_LEFT_2OP) { openGate() }
onOpLoc1(GATE_RIGHT_2OP) { openGate() }

// 1op variant (after quest): Open=op1.
onOpLoc1(GATE_LEFT_1OP) { openGateAfterQuest() }
onOpLoc1(GATE_RIGHT_1OP) { openGateAfterQuest() }

// Border guard dialogue.
onOpNpc1(BORDER_GUARD_1) { talkToGuard(it.npc) }
onOpNpc1(BORDER_GUARD_2) { talkToGuard(it.npc) }
}

private fun ProtectedAccess.isQuestComplete(): Boolean =
princeAliRescue.quest.isQuestCompleted(player)

private fun ProtectedAccess.getDestination(): CoordGrid =
if (player.coords.x >= GATE_PIVOT_X) EAST_SIDE_LANDING else WEST_SIDE_LANDING

private suspend fun ProtectedAccess.openGate() {
if (isQuestComplete()) {
openGateAfterQuest()
return
}
mes("You need to pay 10 gold to pass through the gate.")
val option = choice2("Pay 10 gold.", OPT_PAY, "No thanks.", OPT_DECLINE)
when (option) {
OPT_PAY -> payToll()
OPT_DECLINE -> mes("You decide not to pass through the gate.")
}
}

private suspend fun ProtectedAccess.payToll() {
if (isQuestComplete()) {
openGateAfterQuest()
return
}
if (!player.invTakeFee(fee = TOLL_FEE)) {
mes("You do not have enough coins to pay the toll.")
return
}
mes("You pay the guard 10 gold.")
telejump(getDestination())
}

private fun ProtectedAccess.openGateAfterQuest() {
telejump(getDestination())
}

private suspend fun ProtectedAccess.talkToGuard(npc: Npc) {
if (isQuestComplete()) {
startDialogue(npc) {
chatNpc(happy, "You are a friend of Al Kharid. Pass through freely.")
}
return
}
startDialogue(npc) {
chatNpc(
neutral,
"Halt! You must pay a toll of 10 gold coins to pass through this gate.",
)
val option =
choice2(
"Okay, I will pay.",
OPT_PAY,
"No thanks, I will find another way.",
OPT_DECLINE,
)
when (option) {
OPT_PAY -> {
if (!access.player.invTakeFee(fee = TOLL_FEE)) {
chatPlayer(sad, "Oh dear, I do not seem to have enough money.")
} else {
chatPlayer(happy, "Here you go.")
chatNpc(happy, "Thank you, you may pass.")
access.telejump(access.getDestination())
}
}
OPT_DECLINE -> {
chatPlayer(neutral, "No thanks, I will find another way.")
chatNpc(neutral, "Suit yourself.")
}
}
}
}

public companion object {
/** OSRS canon: the Al-Kharid gate toll is 10gp. */
public const val TOLL_FEE: Int = 10

public const val GATE_DEFAULT_LEFT: String = "loc.kharidmetalgateclosedl"
public const val GATE_DEFAULT_RIGHT: String = "loc.kharidmetalgateclosedr"
public const val GATE_LEFT_2OP: String = "loc.kharidmetalgateclosedl_2op"
public const val GATE_RIGHT_2OP: String = "loc.kharidmetalgateclosedr_2op"
public const val GATE_LEFT_1OP: String = "loc.kharidmetalgateclosedl_1op"
public const val GATE_RIGHT_1OP: String = "loc.kharidmetalgateclosedr_1op"
public const val BORDER_GUARD_1: String = "npc.borderguard1"
public const val BORDER_GUARD_2: String = "npc.borderguard2"

/** Absolute x of the gate line: east of it means the player is on the Al-Kharid side. */
private const val GATE_PIVOT_X: Int = 3268

private val WEST_SIDE_LANDING: CoordGrid = CoordGrid(0, 51, 50, 5, 28)
private val EAST_SIDE_LANDING: CoordGrid = CoordGrid(0, 51, 50, 2, 28)

private const val OPT_PAY: Int = 1
private const val OPT_DECLINE: Int = 2
}
}
Loading