Research:Trading and Services

From OpenMW Wiki
Jump to navigation Jump to search


Trading and services

Common mechanics

Barter function

All bartering and services use a common function to evaluate costs. Uses common formula fatigueTerm for each side of a transaction.

defining a function barterOffer :: (Actor npc, int basePrice, bool buyingSelling) -> int offerPrice

if npc is a creature: return basePrice

clampedDisposition = clamp int(npcDisposition) to [0..100]
a = min(pcMercantile, 100)
b = min(0.1 * pcLuck, 10)
c = min(0.2 * pcPersonality, 10)
d = min(npcMercantile, 100)
e = min(0.1 * npcLuck, 10)
f = min(0.2 * npcPersonality, 10)

pcTerm = (clampedDisposition - 50 + a + b + c) * pcFatigueTerm
npcTerm = (d + e + f) * npcFatigueTerm
buyTerm = 0.01 * (100 - 0.5 * (pcTerm - npcTerm))
sellTerm = 0.01 * (50 - 0.5 * (npcTerm - pcTerm))

if buying: x = buyTerm
if selling: x = min(buyTerm, sellTerm)

if x < 1: offerPrice = int(x * basePrice)
if x >= 1: offerPrice = basePrice + int((x - 1) * basePrice)
offerPrice = max(1, offerPrice)


Bartering

Actions affected On choosing/offering an item to buy/sell
Description Determining item/stack base prices and adjusted prices.
Implementation status Implemented
Analysis status Verified
basePrice = price of prototype item of the same kind, enchanted items have prices set at enchant time and saved with the object
stackSize = number of items in selected stack

if item is a weapon or armor: x = basePrice * (remainingDurability / maxDurability)
if item is lockpick, probe or repair hammer: x = basePrice * (remainingUses / maxUses)
if item is a filled soul gem: basePrice = soul points contained * empty soul gem price
otherwise: x = basePrice

if buying: merchant offer is adjusted by -barterOffer(merchant, x * stackSize, buying)
if selling: merchant offer is adjusted by +barterOffer(merchant, x * stackSize, selling)
Actions affected On finalizing a transaction.
Description Includes haggling mechanic. Uses common formula fatigueTerm.
Implementation status Implemented
Analysis status Verified
all prices are negative when player is buying, positive when player is selling

accept if playerOffer <= merchantOffer (same for buy and sell)
if npc is a creature: reject (no haggle)

a = abs(merchantOffer)
b = abs(playerOffer)
if buying: d = int(100 * (a - b) / a)
if selling: d = int(100 * (b - a) / b)

clampedDisposition = clamp int(npcDisposition) to [0..100]
dispositionTerm = fDispositionMod * (clampedDisposition - 50)
pcTerm = (dispositionTerm + pcMercantile + 0.1 * pcLuck + 0.2 * pcPersonality) * pcFatigueTerm
npcTerm = (npcMercantile + 0.1 * npcLuck + 0.2 * npcPersonality) * npcFatigueTerm
x = fBargainOfferMulti * d + fBargainOfferBase
if buying: x += abs(int(pcTerm - npcTerm))
if selling: x += abs(int(npcTerm - pcTerm))

roll 100, if roll <= x then trade is accepted
adjust npc temporary disposition by iBarterSuccessDisposition or iBarterFailDisposition

Merchant repair

Actions affected On opening the repair service window
Description
Implementation status Implemented
Analysis status Verified
p = max(1, basePrice)
r = max(1, int(maxDurability / p))

x = int((maxDurability - durability) / r)
x = int(fRepairMult * x)
cost = barterOffer(npc, x, buying)

Trainers

Actions affected On purchasing training
Description
Implementation status Implemented
Analysis status Verified, but buggy. See notes for solution.
cost of training a skill = barterOffer(npc, pcSkill * iTrainingMod, buying)

Standard Morrowind uses the current skill value, including fortifies and drains, which allows cheap training exploits with drain spells. A new implementation should use the skill's base value.

Spell merchant

Actions affected On purchasing a spell
Description
Implementation status Implemented
Analysis status Verified
cost of purchasing existing spell = barterOffer(npc, spell.magickaCost * fSpellValueMult, buying)
Actions affected On purchasing from a spellmaking service
Description
Implementation status Implemented
Analysis status Needs testing
y = 0
for each effect in spell:
    x = 0.5 * (max(1, effect.magnitudeMin) + max(1, effect.magnitudeMax))
    x *= 0.1 * effect.magicEffect.baseMagickaCost
    x *= 1 + effect.duration
    x += 0.05 * max(1, effect.area) * effect.magicEffect.baseMagickaCost

    y += x * fEffectCostMult
    y = max(1, y)
    if effect.rangeType & CAST_TARGET: y *= 1.5

magickaCost = int(y)
cost of spellmaking = barterOffer(npc, magickaCost * fSpellMakingValueMult, buying)

Enchanting merchant

Actions affected On purchasing an enchantment
Description
Implementation status Changes since last implementation
Analysis status Requires verification
y = 0
z = []
enchantPoints = 0

for each effect in enchantment:
    if enchantment is constant effect:
        if effect.magnitudeMin > 1 or effect.magnitudeMax > 1 or effect.radius > 1:
            effect.duration = int(fEnchantmentConstantDurationMult)
        else:
            effect.duration = 100

    x = 0.5 * (max(1, effect.magnitudeMin) + max(1, effect.magnitudeMax))
    x *= 0.1 * effect.magicEffect.baseMagickaCost
    x *= effect.duration     # note difference from spellmaking
    x += 0.05 * max(1, spell.radius) * effect.magicEffect.baseMagickaCost

    y += x * fEffectCostMult
    y = max(1, y)
    if effect.rangeType & CAST_TARGET: y *= 1.5

    enchantPoints += int(y)
    z[effect.order] = int(pcEnchantSkill - y * fEnchantmentChanceMult)

# note enchantPoints not used for cost
cost of enchanting service = barterOffer(npc, y * fEnchantmentValueMult, buying)

Comments

Multiple enchantments stack in a expensive manner, as both variables y and enchantPoints are accumulators. Strangely, the price is based on the equivalent to the spellmaking accumulator rather than the enchantment points used. Constant effect enchantments override the duration in an over-complicated manner. As fEnchantmentConstantDurationMult is 100 by default, it would be much cleaner to use that for duration in both cases.


Travel costs

Physical travel

Actions affected On travelling via silt strider, boat or similar travel service AI
Description
Implementation status Implemented
Analysis status Requires independent testing
dist = distance from player to destination
cost = barterOffer(npc, int(dist / fTravelMult), buying)
time = int(dist / fTravelTimeMult)

Guild guide

Actions affected On travelling via Mages' Guild teleport service
Description
Implementation status Implemented
Analysis status Verified
cost = barterOffer(npc, fMagesGuildTravel, buying)

Comments

NPCs currently following the player and currently in range when the player selects the Travel option will come with. Travel costs are actually scaled (multiplied) according to the number of passengers taken, however, there seems to be a bug where the first follower is ignored (or free): the price will only increase if the player has more than one follower. So if the player has 4 followers in range, he will pay quadruple the price he would pay alone (or with a single follower), rather than 5x the price (for 5 passengers total). The same likely applies to creatures that are following, but that needs confirmation.