Fellowship

How people stand with each other: regard between any two people, attraction, sentiments with reasons, hidden compatibility, groups and households, and standing with groups as fame and infamy kept apart. Named for the Fellowship of the Ring.

The ideas come from Merryfolk’s hearts (regard grown by time together), The Sims (attraction as its own track; The Sims 4’s sentiments), RimWorld (hidden compatibility) and Fallout: New Vegas (fame and infamy kept apart, so the same people can like you and fear you).

Regard

Regard is one person’s feeling toward another, -1..1, one way: Ruth can like Kofi more than he likes her. It’s a base value plus the person’s active sentiments about the other, clamped.

It moves:

A passing acquaintance doesn’t last: once two people haven’t met for forget (14) days (divided by fading), and the one’s regard and attraction for the other are both under forgetBelow (0.05) with no sentiments, the record goes, and they’re strangers again (knows is false). A big town keeps only the relationships that matter.

Social actions (tagged 'social') aimed at someone weigh clamp(1 + regard × 0.8 + compat × 0.2, 0.1, 2) (socialRegard, socialCompat, socialMin, socialMax): people seek out the people they like.

Attraction and compatibility

Compatibility is hidden and the same both ways: a seeded value in ±compatSpread (0.6) for each pair, plus up to 0.3 (compatValues) for shared values (PersonDef.values), clamped to -1..1. Some people just get on.

Attraction is a second track for romance, -1..1. It grows by half the regard gain when they meet (attractGain, times the headline knob romance), but only for pairs with compatibility over 0.3 (attractAt), and only while the person has no partner. The game can change it with fellowship.attract(a, b, by) (an input). Cradle can pair people when regard and attraction are both high.

Sentiments

A sentiment is a labeled reason that adds to regard: “helped me out” +0.15, “snubbed me” -0.5 for an hour.

world.fellowship.sentiment('kofi', 'player', 'snubbed me', -0.5, 60);   // for an hour
world.fellowship.sentiments('kofi', 'player');   // [{ label: 'snubbed me', by: -0.5, until: ... }]

A relationship keeps at most sentiments (4); past that, the weakest (by absolute value) goes. A new sentiment with the same label replaces the old one. Sentiments with a time run out. Deeds with sentiment add one (worth the deed’s regard, or 0.1); Cradle adds ‘my partner’ and ‘my child’.

Groups and households

world.fellowship.group({ id: 'h_okafor', kind: 'household', label: 'the Okafors', purse: true });
world.fellowship.group({ id: 'watch', kind: 'authority', label: 'the Watch' });
world.person('mara', { home: 'apt_1', groups: ['h_okafor'] });
world.fellowship.join('regulars', 'kofi');

GroupDef:

Field Meaning
id The group’s id
label A readable name
kind 'household' (a person’s household: Ceres shares money in it), 'authority' (Wormtongue reports crimes to it), or anything else ('family', 'faction', 'club')
members Members listed here, on top of people whose definitions name the group
purse Households: money and goods are shared (Ceres)

A person’s groups are those their definition names, those that list them, and those they’ve joined at runtime. A person’s household is their first group of kind 'household'.

Standing

Standing is how a group sees someone (usually the player): fame and infamy, each 0..1, kept apart, with a label from the pair.

world.fellowship.changeStanding('watch', 'player', 0.5, 0);
world.fellowship.standing('watch', 'player');   // { fame: 0.5, infamy: 0, label: 'liked' }

Each value falls in a band (under 0.15, under 0.4, under 0.7, above: bands), and the label is config.labels[fameBand][infamyBand]:

Fame \ Infamy low some high very high
low unknown suspect trouble menace
some familiar mixed rough edges feared
high liked complicated loved and feared dangerous hero
very high beloved tolerated wild card legend

Deeds with fame or infamy change the doer’s standing with every group the receiver belongs to. Wormtongue adds infamy when crimes are reported.

Imitation

When two people meet and one likes the other (regard over 0.3, imitateAt), what the liked person is doing becomes a little more appealing: their habit for that action rises by imitation (0.03).

State

In world.s.mod.fellowship:

Key Holds
rel Relationships by 'a>b': { r, a, s, met, last }: regard base, attraction, sentiments, meetings, last met
standing By group, by subject: [fame, infamy]
partners Partner by person (both ways)
joined Members added at runtime, by group

API

world.fellowship. Input methods are recorded and safe from the game; engine methods change state directly.

Method Returns  
group(def) this Declares a group
members(group) id[] Everyone in it (not gone)
groupsOf(id) id[] The groups a person belongs to
household(id) id or undefined Their household group
join(group, id, on = true) void Input. Joins (or leaves, with false) a group
regard(a, b) number a’s regard for b, sentiments included
attraction(a, b) number a’s attraction to b
compat(a, b) number The pair’s compatibility
knows(a, b) boolean a has a relationship record with b (they’ve met, or regard was set)
relations(a) array Everyone a has a record with: { id, regard, attraction, met }, best first
friends(a, n = 3) id[] The people a likes most (regard over 0.2)
adjust(a, b, by, why?) void Input. Changes a’s regard for b (logged when why is given)
sentiment(a, b, label, by, minutes?) void Input. Adds a sentiment
sentiments(a, b) Sentiment[] Active sentiments
partner(a) id or undefined a’s partner
setPartner(a, b) void Input. Makes them partners (or ends a’s partnership with null)
standing(group, subject) { fame, infamy, label } Standing
changeStanding(group, subject, fame, infamy) void Input. Adds to fame and infamy
attract(a, b, by) void Input. Changes a’s attraction to b
rel(a, b) Rel or undefined A copy of the record a -> b, or undefined if there’s none
applyRegard, applyAttraction, applySentiment, applyStanding, applyJoin, applyPartners void Engine. The direct versions of the inputs
config, groups   The config and the declared groups

Terms and effects

Terms: regard, attraction, compat, friend, foe (each toward the target, or :<id>), member:<group>, partner / partner:<id>, fame:<group>.<subject>, infamy:<group>.<subject>.

Effects: regard+=n / regard:<id>+=n / regard=n, fame:<group>.<subject>+=n, infamy:<group>.<subject>+=n. Without a subject, fame and infamy mean the context’s person (the player when there’s none), in effects and conditions alike: an action’s then: ['fame:watch+=0.1'] raises the doer’s standing. Name the player to change theirs: fame:watch.player+=0.1.

See Conditions.

Events

Fellowship emits no events of its own. It writes log entries: met ({ target, minutes }), regard (when a reason is given), sentiment.

Config

Tyrell.create({ moduleConfig: { fellowship: { ... } } }):

Field Default Meaning
meetGain 0.03 Regard gained per half hour together, before compatibility and mood
compatSpread 0.6 How much compatibility varies between pairs
decay 0.004 Regard drifts back toward zero by this much a day
sentiments 4 Most sentiments kept per relationship
imitation 0.03 How much a liked person’s habits rub off
forget 14 Days after their last meeting that a passing acquaintance is forgotten: a record with regard and attraction both under forgetBelow, and no sentiments. Keeps a big town’s relationships to the ones that matter. 0 forgets nobody
forgetBelow 0.05 How faint a relationship has to be to be forgotten
labels the table above Standing labels, rows by fame band, columns by infamy band
friendship 1 Headline knob: how fast friendships and enmities form (the regard each meeting adds or takes away); 0: time together changes nothing
romance 1 Headline knob: how fast attraction grows from time together
fading 1 Headline knob: how fast relationships fade (decay times it, forget divided by it); 0: never

A person’s own tune: { fellowship: { friendship, romance, fading } } multiplies the knobs for their regard (and attraction) for others. The finer numbers, each explained in Tuning: friendAt (0.5) and foeAt (-0.4) for the friend and foe terms, likeAbove (0.2) for friends(), compatValues (0.3), meetMin (-0.05), meetMax (0.08), meetCompat (1), meetMood (1), attractAt (0.3), attractGain (0.5), imitateAt (0.3), socialRegard (0.8), socialCompat (0.2), socialMin (0.1), socialMax (2), deedSentiment (0.1), witnessRegard (0.4), bands ([0.15, 0.4, 0.7]). During play, world.tune('fellowship', { ... }) changes any of them as an input.

With other modules

Example

const fs = world.fellowship;
fs.group({ id: 'regulars', kind: 'club', label: "the Belt's regulars" });
world.deed('stood_a_round', { regard: 0.1, sentiment: 'bought me a drink' });

world.did('player', 'stood_a_round', { for: 'ines', seenBy: ['kofi'] });
fs.regard('ines', 'player');        // 0.1 base + 0.1 sentiment
fs.regard('kofi', 'player');        // 0.04: he saw it
fs.changeStanding('regulars', 'player', 0.2, 0);
fs.standing('regulars', 'player').label;   // 'familiar'
world.lang.test('friend:player', world.ctx(world.get('ines')));   // false: regard is under 0.5