Gameplay content

Outcomes and proposals

Record how a narrative moment ended, once, so another mod can follow it up once; and ask several people something only they can all agree to.

Outcomes

A story beat that ends one of several ways (an evening that ends in a goodnight or a longer talk, a confrontation that is settled or walked out of) is an outcome: one event id, the people in it, begun before the choice and committed once.

lualocal M = "ul_AfterMidnight"
local eid = "evening_" .. a:id() .. "_" .. n

local why = Outcomes:begin(M, eid, { kind = "narrative", people = { a } })
if why then return why end                     -- "That event already has an outcome."

Game:Choice(1, nil, "Say goodnight")
Game:Choice(2, nil, "Stay and talk")
local i = Game:GetChoice()                     -- the scene yields here
local gone = Outcomes:check(M, eid)            -- still everyone here?
if gone then return gone end                   -- "Ana is no longer available." (now unavailable)

Outcomes:commit(M, eid, i == 1 and "a warm goodnight" or "private conversation")
if i == 2 then Outcomes:fade(1.5) end
  • Begin before, check after. A choice yields: while it is open, the player can block the contact, or the person can be deleted. check after the choice answers why the moment cannot go on and closes it as unavailable.
  • Committed once. commit returns true the first time and false afterwards, so a scene that runs twice (a reload, a second click) never applies its consequences twice. cancel closes it without a result (the player left it undecided).
  • A pending outcome does not survive a load. If the game is saved and reloaded in the middle of the choice, the outcome comes back cancelled ("cancelled on load"): the scene that was waiting for it is gone.
  • kind is "narrative" (the default) or "session". A narrative ending is text: it is not an animation session and applies no session consequences.
  • commit(mod, id, result, data, atHours) takes an optional table of strings (data) and a time; without one, now. Use the time when migrating an older save's events.

Follow-ups

Another mod reads completed outcomes of a source mod and marks each handled for itself, once:

lualocal SELF, SOURCE = "ul_MorningAfter", "ul_AfterMidnight"
local list = Outcomes:followUps(SELF, SOURCE, person)   -- oldest first, not handled by SELF
local o = list[1]
if o and Game:GetHoursPassed() >= o.closed + 12 then
	if Outcomes:handle(SELF, SOURCE, o.id) then         -- true the first time only
		-- the morning message, the rapport...
	end
end

Two mods can follow up the same evening, each once. outcomeCommitted (module, event id, result) fires for each commit, through Events:on.

The fade

Outcomes:fade(seconds) darkens the screen and brings it back on its own after seconds (0.2 to 5, default 1.5). Nothing has to undo it, and a script that errors in between cannot leave the screen black.

Proposals

A proposal asks something of people (exclusivity, an open arrangement, more time together) and stays open until each of them answers. The proposer (the player by default) does not answer for anyone.

lualocal M = "ul_PrivateAgreements"
local why = Proposals:propose(M, "open_1", { kind = "open", people = { ana, cleo } })
...
Proposals:accept(M, "open_1", ana)        -- still "proposed": Cleo has not answered
Proposals:accept(M, "open_1", cleo)       -- "accepted"
Proposals:decline(M, "open_1", cleo, "not now")   -- or "declined", at once
  • Everyone concerned must be an adult contact who is not blocked; propose says who is not.
  • A new proposal of the same kind, from the same mod, between the same people supersedes the open one (its reason is the new id).
  • withdraw closes it from the proposer's side. A participant who is blocked or deleted cancels it.
  • Rapport is not an answer: decide in your own script whether a person accepts, and call accept or decline for them then. Changing roles (partner, housemate) after an acceptance is your mod's job.
  • proposalChanged (module, id, state) fires on each change.
Call Does
Outcomes:begin(mod, id, { kind, people }) nil, or why not.
Outcomes:check(mod, id) nil while everyone is there, else why (now unavailable).
Outcomes:commit(mod, id, result [, data [, atHours]]) true once.
Outcomes:cancel(mod, id [, reason]) true if it was pending.
Outcomes:get(mod, id) { module, id, kind, state, result, people, data, created, closed, handledBy } or nil; a cancelled or unavailable one carries the reason in result.
Outcomes:followUps(consumer, source [, person]) Completed outcomes not handled by consumer.
Outcomes:handle(consumer, source, id) true the first time.
Outcomes:fade([seconds]) A fade out and back.
Proposals:propose(mod, id, { kind, people, proposer, terms }) nil, or why not.
Proposals:accept(mod, id, person) / decline(mod, id, person [, reason]) nil, or why not.
Proposals:withdraw(mod, id [, reason]) true if it was open.
Proposals:get(mod, id) { module, id, kind, state, reason, proposer, people, accepted, terms, created } or nil.
Proposals:list([mod [, person [, state]]]) Matching proposals.

States: an outcome is pending, completed, cancelled or unavailable; a proposal is proposed, accepted, declined, withdrawn, superseded or cancelled. Closed records are kept (up to 300 outcomes and 200 proposals, oldest closed first).