Skip to content
Draft
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
116 changes: 116 additions & 0 deletions docs/TEMPLE_SERVICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Temple recovery pricing

Temple recovery uses an explicit service rank and a deterministic quote. NPC
combat level is inventory evidence, not priest proficiency: the 27 providers
range from effective combat level 1 to 115 and include guards, an ogre, a
vampire, a raas, witches, and other non-priest archetypes.

## Affordability evidence and newcomer policy

The Incuna start gives no currency. An early quest outcome gives 50 copper and
its combat outcome gives 15 silver. Before this policy, level-1 recovery cost
5 silver 90 copper for depletion, 5 silver for poison, 10 silver for disease,
30 silver for curse removal, and 50 silver for damnation removal. Those fixed
fees could therefore turn an early condition into a progression deadlock.

Historically, depletion, disease, and poison treatment were free below level
3. Classic now suppresses normal death depletion through character level 3.
The policy aligns those safeguards: patients at or below level 3 receive free
depletion, disease, and poison treatment when condition difficulty is at most
5. Curse and damnation removal remain paid because the historical grace
deliberately excluded inventory curses. A harder condition is never subsidized
solely because its carrier is new; it receives the normal quote and still
requires a capable provider.

## Provider capability

The central registry identifies each provider by its stable authored map path
and NPC name, and maps that pair to one reviewed service rank:

| Rank | Role | Providers |
| ---: | --- | --- |
| 20 | Community | Brelend Lee, Manzom, Telath, Marcus Stephen, Thumron, Helga, Eshorem, Oxa, Jania, Merithax, Ugthar, Morvarm, Cthisss, Alcrom, Murfar |
| 40 | Regional | Hivro Holygauntlet, Sakura, Ami, Saruthar, Celach Dawson, Filbreena Ulass |
| 60 | Senior | Conuld Burch, Friar Marcus, Talrain, Traba Jainkoaren, Gwenty |
| 100 | High priest | Archbishop Theodorus III |

The content validator discovers standard, special, and quest-gated temple
interfaces and requires those 27 authored identities to match the registry exactly.
Combat-level outliers such as level-1 Manzom (service rank 20), level-115
Talrain (rank 60), and level-68 Archbishop Theodorus III (rank 100) prove that
the capability input is independent.

Condition difficulty is the highest disease, poison, cursed-item, or
damned-item level within that native spell's existing scope. Remove damnation
also sees ordinary curses because Classic's spell already handles both; this
policy does not expand it. Depletion difficulty is the greater of its object level and
five times the total depleted stat points. A provider refuses a condition when
difficulty exceeds service rank. During the synchronous spell call, the NPC's
effective level is temporarily set to its service rank and restored in a
`finally` block, so Classic's disease and curse capability checks use the same
reviewed input as the quote.

## Full-price curve

For paid care, the unrounded copper price is:

```text
base + patient_level * patient_factor
+ condition_count * condition_factor
+ difficulty * difficulty_factor
+ depletion_severity * severity_factor
```

An easy case treated by a provider above the required rank receives a
capability discount of one percent per two surplus ranks, capped at 30%. The
discounted result is rounded down to 10 copper, then clamped to the service
minimum and maximum.

| Service | Base | Patient | Condition | Difficulty | Severity | Minimum | Maximum |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Remove depletion | 200 | 45 | 75 | 10 | 100 | 100 | 12,000 |
| Cure disease | 300 | 50 | 100 | 20 | 0 | 100 | 15,000 |
| Cure poison | 150 | 35 | 75 | 15 | 0 | 100 | 8,000 |
| Remove curse | 750 | 60 | 150 | 20 | 0 | 100 | 18,000 |
| Remove damnation | 1,250 | 80 | 200 | 30 | 0 | 100 | 25,000 |

Examples, all in copper:

| Case | Rank | Quote |
| --- | ---: | ---: |
| Level-3 patient, difficulty-5 poison | 20 | free |
| Level-4 patient, difficulty-4 poison | 20 | 390 |
| Level-4 patient, one-point depletion (difficulty 5) | 20 | 560 |
| Level-1 patient, difficulty-1 curse | 20 | 890 |
| Level-20 patient, difficulty-20 disease | 20 | 1,800 |
| Same disease and patient | 100 | 1,260 |
| Level-100 patient, difficulty-100 disease | 100 | 7,400 |

Patient level, condition count, difficulty, and depletion severity all make an
otherwise equivalent quote nondecreasing. Greater surplus provider capability
can only reduce the quote. Integer arithmetic, explicit rounding, and bounds
make the result reproducible.

## Confirmation and settlement

Preview and confirmation both use the same quote function. The confirmation
link carries the service, price, count, difficulty, severity, provider rank,
and a digest of condition evidence. Confirmation re-snapshots and requotes; a
changed token produces a new preview and no cast or payment.

The transaction checks funds before casting, then compares synchronous pre-
and postconditions before collecting payment:

- no condition: refuse, do not cast, and do not charge;
- insufficient funds: do not cast and do not charge;
- total failure: report failure and do not charge;
- full success: charge exactly the displayed quote;
- partial success: identify the remaining condition and charge exactly the
displayed quote, because the provider was prequalified for the complete
snapshot and delivered beneficial treatment;
- repeated confirmation: the cured condition no longer exists, so do not cast
or charge.

The individual `remove depletion`, `cure disease`, `cure poison`, `remove
curse`, and `remove damnation` spells remain separate. This policy does not
change `restoration` or broaden any spell's scope.
246 changes: 170 additions & 76 deletions maps/python/Temple.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,23 @@
"""Temple.py: Implements temple-related functions."""

from collections import OrderedDict

from Atrinik import *
from Interface import InterfaceBuilder


temple_services = OrderedDict((
("remove depletion", [
lambda activator: activator.level / 2 * 130 + 525,
"Removal of depletion",
"Depletion most commonly occurs when you die, and it drains some of your stat attributes. In order to get them back, [green]remove depletion[/green] prayer can be used. I can cast this prayer on you, if you wish.",
]),
("remove curse", [
3000,
"Removal of curse from all cursed items",
"I will remove any curse from all cursed items in your inventory. Note that the curse may still come back, if the item is permanently cursed...",
]),
("remove damnation", [
5000,
"Removal of damnation from all damned items",
"I will remove any damnation from all damned items in your inventory. Note that the damnation may still come back, if the item is permanently damned...",
]),
("cure disease", [
1000,
"Curing of disease",
"I can attempt to cure any disease that is troubling you.",
]),
("cure poison", [
500,
"Curing of poison",
"I can heal your poisoning, if you wish.",
]),
("food", [
0,
"Spare bit of food",
]),
))
from TempleServices import (
SERVICES,
execute_treatment,
provider_for,
quote as service_quote,
snapshot as service_snapshot,
)


service_descriptions = {
"remove depletion": "Depletion most commonly occurs when you die, and it drains some of your stat attributes. I can restore them with a remove depletion prayer.",
"remove curse": "I will remove ordinary curses from cursed items in your inventory. A permanent curse may return.",
"remove damnation": "I will remove damnation from damned items in your inventory. A permanent damnation may return.",
"cure disease": "I can cure diseases that are within my service training.",
"cure poison": "I can cleanse poison that is within my service training.",
}

class Temple(InterfaceBuilder):
"""Temple interface builder."""
Expand All @@ -49,69 +30,182 @@ class Temple(InterfaceBuilder):
def subdialog_services(self):
"""Show the available temple services."""

for service in temple_services:
self.add_link(temple_services[service][1], dest = service)
for service in SERVICES.values():
self.add_link(service.title, dest=service.spell)

self.add_link("Tell me about {}.".format(self.temple_name), dest = self.temple_name)
self.add_link("Spare bit of food", dest="food")
self.add_link("Tell me about {}.".format(self.temple_name), dest=self.temple_name)

if self.enemy_temple_name:
self.add_link("Tell me about {}.".format(self.enemy_temple_name), dest = self.enemy_temple_name)
self.add_link(
"Tell me about {}.".format(self.enemy_temple_name),
dest=self.enemy_temple_name,
)

def dialog_hello(self):
"""Default hello dialog handler."""

self.add_msg("Welcome to the church of {god}. I am {npc.name}, a devoted servant of {god}.", god = self.temple_name)
self.add_msg(
"Welcome to the church of {god}. I am {npc.name}, a devoted servant of {god}.",
god=self.temple_name,
)

if self.enemy_temple_name:
self.add_msg("Beware that followers of {enemy_god} are not welcome here.", enemy_god = self.enemy_temple_name)
self.add_msg(
"Beware that followers of {enemy_god} are not welcome here.",
enemy_god=self.enemy_temple_name,
)

self.add_msg("I can offer you the following services.")
self.subdialog_services()

def _provider(self):
return provider_for(self._npc.name, self._npc.map.path)

def _snapshot(self, service_name):
return service_snapshot(service_name, self._activator.inv, Type.DISEASE)

def _current_quote(self, service_name):
provider = self._provider()
if provider is None:
self.add_msg(
"My temple-service training is not configured, so I cannot safely offer this treatment."
)
return None

condition = self._snapshot(service_name)
if condition.count == 0:
self.add_msg("You do not have a condition this service can treat.")
return None

try:
current = service_quote(
service_name,
self._activator.level,
provider.service_rank,
condition,
)
except ValueError:
self.add_msg(
"This condition has difficulty {difficulty}, but my service rank is only {rank}. I cannot reliably treat it, so I will not cast or charge you.",
difficulty=condition.difficulty,
rank=provider.service_rank,
)
return None

return provider, condition, current

def _show_quote(self, current):
if current.free:
self.add_msg(
"Newcomer care covers this difficulty-{difficulty} treatment. There is no charge.",
difficulty=current.difficulty,
)
else:
self.add_msg(
"This difficulty-{difficulty} treatment will cost {cost}.",
difficulty=current.difficulty,
cost=CostString(current.cost),
)
self.add_link(
"Confirm treatment",
dest="buy {}|{}".format(current.service, current.token),
)

def _cast(self, provider, service_name):
old_level = self._npc.level
try:
self._npc.level = provider.service_rank
spell = GetArchetype(
"spell_" + service_name.replace(" ", "_")
).clone.sp
self._npc.Cast(spell, self._activator)
finally:
self._npc.level = old_level

def _confirm(self, service_name, presented_token, provider, before, current):
preview = current._replace(token=presented_token)
result = execute_treatment(
preview,
current,
self._activator.GetMoney(),
before,
lambda: self._cast(provider, service_name),
lambda: self._snapshot(service_name),
self._activator.PayAmount,
)
if result.outcome == "drift":
self.add_msg(
"Your condition or quote changed before confirmation. I have not cast or charged you; please review the new quote."
)
self._show_quote(current)
elif result.outcome == "insufficient-funds":
self.add_msg(
"You do not have enough money. I have not cast or charged you."
)
elif result.outcome == "failure":
self.add_msg("The prayer had no effect. You are not charged.")
elif result.outcome == "payment-failure":
self.add_msg(
"The treatment had an effect, but payment could not be collected. You are not charged."
)
else:
if result.outcome == "partial":
self.add_msg(
"The prayer helped, but some of the condition remains. The confirmed partial-success policy applies."
)
else:
self.add_msg("The treatment succeeds.")
if result.charged:
self.add_msg(
"You pay {cost}.",
cost=CostString(result.charged),
color=COLOR_YELLOW,
)
else:
self.add_msg("No payment is due under newcomer care.")

def dialog(self, msg):
"""Handle services and speaking about particular gods."""

if msg == self.temple_name:
self.add_msg(self.temple_desc)
elif msg == self.enemy_temple_name:
return
if msg == self.enemy_temple_name:
self.add_msg(self.enemy_temple_desc)
else:
if msg.startswith("buy "):
msg = msg[4:]
is_buy = True
return
if msg == "food":
if self._activator.food < 500:
self._activator.food = 500
self.add_msg("Your stomach is filled again.")
else:
is_buy = False
self.add_msg("You don't look very hungry to me...")
return

service = temple_services.get(msg)

if not service:
presented_token = None
if msg.startswith("buy "):
service_name, separator, presented_token = msg[4:].partition("|")
if not separator:
return

if msg == "food":
if self._activator.food < 500:
self._activator.food = 500
self.add_msg("Your stomach is filled again.")
else:
self.add_msg("You don't look very hungry to me...")
else:
self.add_msg("[title]{service[1]}[/title]", service = service)
cost = service[0]
if not isinstance(cost, int):
cost = int(cost(self._activator))

if not is_buy:
self.add_msg(service[2])
self.add_msg("This will cost you {cost}.", cost = CostString(cost))
self.add_link("Confirm", dest = "buy " + msg)
else:
if self._activator.PayAmount(cost):
if cost:
self.add_msg("You pay {cost}.", cost = CostString(cost), color = COLOR_YELLOW)

self.add_msg("Okay, I will cast [green]{origmsg}[/green] on you now.", origmsg = msg)
self._npc.Cast(GetArchetype("spell_" + msg.replace(" ", "_")).clone.sp, self._activator)
else:
self.add_msg("You do not have enough money...")
else:
service_name = msg

service = SERVICES.get(service_name)
if service is None:
return

self.add_msg("[title]{title}[/title]", title=service.title)
self.add_msg(service_descriptions[service_name])
quoted = self._current_quote(service_name)
if quoted is None:
return
provider, before, current = quoted
if presented_token is None:
self._show_quote(current)
else:
self._confirm(
service_name, presented_token, provider, before, current
)

class TempleGrunhilde(Temple):
"""Grunhilde temple."""
Expand Down
Loading
Loading