plotcoder-board 0.1.34 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/plotcoder-mcp-server.mjs +34 -13
- package/src/board/agents.js +1 -1
- package/src/board/fountain.js +1 -0
- package/src/board/project.d.ts +4 -0
- package/src/board/project.js +13 -2
- package/src/board/readWall.d.ts +1 -1
- package/src/board/readWall.js +3 -1
- package/src/board/reducer.d.ts +4 -1
- package/src/board/reducer.js +22 -5
- package/src/board/words.js +1 -1
package/README.md
CHANGED
|
@@ -105,4 +105,4 @@ next round into a test of `claim_account` instead of the door it meant to test.
|
|
|
105
105
|
|
|
106
106
|
## Status
|
|
107
107
|
|
|
108
|
-
Version 0.1.
|
|
108
|
+
Version 0.1.35. A project of boards; sign in with your email and a password from the PlotCoder mark and your projects follow you to every device, share one with another writer by email and write it together live, or stay signed out and work on this device as before. Pages sit beside the wall: a scene's text lives on its card, measures it, paginates to the industry's rules, prints, goes out and comes in as Fountain or Final Draft, and goes out as Markdown or plain text for a collaborator in Google Docs. It installs as a progressive web app and opens offline; plotcoder.com serves over HTTPS. The wall, beats, card length, groups, arrows, pan and zoom, save and open, and the agent surface are in use.
|
package/package.json
CHANGED
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
setPremise,
|
|
72
72
|
setPremiseOpen,
|
|
73
73
|
setBoardNameOpen,
|
|
74
|
+
setProjectNameOpen,
|
|
74
75
|
structureBeats,
|
|
75
76
|
reidentifyProject,
|
|
76
77
|
renameProject,
|
|
@@ -1065,7 +1066,7 @@ function summarize(state) {
|
|
|
1065
1066
|
const pays = paidByHere.filter((item) => item.id === note.id).map((item) => `, pays off "${item.fromHeadline}" from "${item.fromBoardName}" (${episodeLabel(lastHeld.project, lastHeld.boards, item.fromBoardId, item.fromNoteId)})`).join("");
|
|
1066
1067
|
const snap = state.revision?.snapshot?.[note.id];
|
|
1067
1068
|
const revised = snap && (snap.headline !== note.headline || snap.change !== note.change || (snap.text ?? "") !== (note.text ?? "") || (snap.location ?? "") !== (note.location ?? "")) ? `, changed in ${state.revision.color}` : "";
|
|
1068
|
-
const place = note.location ? `, at: ${note.location}` : "";
|
|
1069
|
+
const place = note.location ? `, at: ${note.location}` : note.locationOpen ? `, at: open, by the writer's word — "${note.locationOpen}"` : "";
|
|
1069
1070
|
const when = note.when ? `, when: ${note.when}` : note.whenOpen ? `, when: open, by the writer's word — "${note.whenOpen}"` : "";
|
|
1070
1071
|
const openWord = note.open ? `, open (the writer's words): "${note.open}"` : "";
|
|
1071
1072
|
const count = formatPages(noteEighths(note));
|
|
@@ -1576,9 +1577,11 @@ server.registerTool(
|
|
|
1576
1577
|
const beatEighths = reading.beats.reduce((sum, beat) => sum + noteEighths(state.notes.find((note) => note.id === beat.id) ?? {}), 0);
|
|
1577
1578
|
// Fields left open by the writer's word (R61): the logline and the whens are the reading's; the premise and the board's name are the project's.
|
|
1578
1579
|
const openFieldLines = [
|
|
1580
|
+
...(projectForRead.nameOpen ? [` - the project's name — ${projectForRead.nameOpen}`] : []),
|
|
1579
1581
|
...reading.openFields.filter((field) => field.field === "logline").map((field) => ` - the logline — ${field.words}`),
|
|
1580
1582
|
...(projectForRead.premiseOpen ? [` - the premise — ${projectForRead.premiseOpen}`] : []),
|
|
1581
1583
|
...(readBoardMeta?.nameOpen ? [` - this board's name — ${readBoardMeta.nameOpen}`] : []),
|
|
1584
|
+
...reading.openFields.filter((field) => field.field === "location").map((field) => ` - "${state.notes.find((note) => note.id === field.id)?.headline ?? field.id}" — where: ${field.words}`),
|
|
1582
1585
|
// Whens left open in the same words are one line, not one per card (round twenty, entry 22).
|
|
1583
1586
|
...(() => {
|
|
1584
1587
|
const byWords = new Map();
|
|
@@ -1599,6 +1602,7 @@ server.registerTool(
|
|
|
1599
1602
|
`PlotCoder wall (${door(live, base)})`,
|
|
1600
1603
|
...(state.lock ? [`numbers: locked since ${String(state.lock.at).slice(0, 10)}; read_pages shows each scene's number`] : []),
|
|
1601
1604
|
`board: "${readBoardMeta?.name ?? "?"}"${readBoardMeta?.nameOpen ? ` — its name is open, by the writer's word: "${readBoardMeta.nameOpen}"` : ""}${projectForRead.boards.length > 1 ? ` — board ${projectForRead.boards.findIndex((meta) => meta.id === readBoardMeta?.id) + 1} of ${projectForRead.boards.length} in the project "${projectForRead.name}"; open_board reads another` : ""}`,
|
|
1605
|
+
...(projectForRead.nameOpen ? [`project: "${projectForRead.name}" — its name is open, by the writer's word: "${projectForRead.nameOpen}"`] : []),
|
|
1602
1606
|
...(projectForRead.premiseOpen ? [`premise: open, by the writer's word — "${projectForRead.premiseOpen}"`] : []),
|
|
1603
1607
|
`logline: ${state.loglineOpen ? `open, by the writer's word — "${state.loglineOpen}"` : state.logline ? `"${state.logline}"` : "(none yet)"}`,
|
|
1604
1608
|
"the cast and the places are list_board's, not the reading's",
|
|
@@ -3010,7 +3014,7 @@ server.registerTool(
|
|
|
3010
3014
|
});
|
|
3011
3015
|
const total = parts.reduce((sum, part) => sum + part.on.length, 0);
|
|
3012
3016
|
// An open card reads as open on a person's page too (round eighteen, entry 53).
|
|
3013
|
-
const where_ = (note) => [note.location ? `at ${note.location}` : "", note.when ? note.when : note.whenOpen ? `when open: "${note.whenOpen}"` : "", note.rank === "beat" ? "beat" : "", note.open ? `open: "${note.open}"` : ""].filter(Boolean).join(" · ");
|
|
3017
|
+
const where_ = (note) => [note.location ? `at ${note.location}` : note.locationOpen ? `where open: "${note.locationOpen}"` : "", note.when ? note.when : note.whenOpen ? `when open: "${note.whenOpen}"` : "", note.rank === "beat" ? "beat" : "", note.open ? `open: "${note.open}"` : ""].filter(Boolean).join(" · ");
|
|
3014
3018
|
// A read opens with the door it came through, like every reading (round sixteen, entry 28); one scene a line (29).
|
|
3015
3019
|
const lines = [
|
|
3016
3020
|
`PlotCoder cast (${door(live, base)})`,
|
|
@@ -3087,14 +3091,16 @@ server.registerTool(
|
|
|
3087
3091
|
{
|
|
3088
3092
|
title: "Set where scenes happen",
|
|
3089
3093
|
description:
|
|
3090
|
-
"Set the place of one or more cards: where the scene happens, as the writer would say it ('the piano shop', 'the flat, kitchen') — a phrase, not a slugline. The same phrase on several cards is one place in the Cast panel; an empty string clears it. list_board shows each card's place as 'at: …'.",
|
|
3091
|
-
inputSchema: { ids: z.array(z.string()).min(1), location: z.string() },
|
|
3094
|
+
"Set the place of one or more cards: where the scene happens, as the writer would say it ('the piano shop', 'the flat, kitchen') — a phrase, not a slugline. The same phrase on several cards is one place in the Cast panel; an empty string clears it. Or leave the place open: pass open with the writer's words for why it is not decided — \"where it happens\" — and the reading lists it under open, by the writer's word, and stops asking where, while the card's other questions stand; a place decides it, open \"\" leaves it blank. list_board shows each card's place as 'at: …'.",
|
|
3095
|
+
inputSchema: { ids: z.array(z.string()).min(1), location: z.string().optional(), open: z.string().optional() },
|
|
3092
3096
|
},
|
|
3093
3097
|
async (args) => {
|
|
3098
|
+
if (args.location === undefined && args.open === undefined) return ok("Say which: location (the place, or \"\" to clear it), or open (the writer's words for why it is not decided).");
|
|
3094
3099
|
const { state, changed, result, live } = await commit({
|
|
3095
3100
|
type: "set_location",
|
|
3096
3101
|
ids: args.ids,
|
|
3097
|
-
location: args.location,
|
|
3102
|
+
...(args.location !== undefined ? { location: args.location } : {}),
|
|
3103
|
+
...(args.open !== undefined ? { open: args.open } : {}),
|
|
3098
3104
|
});
|
|
3099
3105
|
if (!changed) {
|
|
3100
3106
|
const known = args.ids.filter((id) => state.notes.some((note) => note.id === id));
|
|
@@ -3102,6 +3108,10 @@ server.registerTool(
|
|
|
3102
3108
|
return ok("No place changed: those cards already read that way.");
|
|
3103
3109
|
}
|
|
3104
3110
|
const place = result[0].location;
|
|
3111
|
+
const placeOpen = result[0].locationOpen ?? "";
|
|
3112
|
+
if (placeOpen) {
|
|
3113
|
+
return ok(`${result.length} card(s) have their place left open, by the writer's word: "${placeOpen}"${where(live)}. The reading lists it and stops asking where; the heading prints the headline in its stead; set_location with a place decides it, open "" leaves it blank. The card is still asked about everything else.${stillOpen(result)}`, result);
|
|
3114
|
+
}
|
|
3105
3115
|
return ok(
|
|
3106
3116
|
`${result.length} card(s) now ${place ? `at ${place}` : "nowhere"}${where(live)}.${nearPlaces(state, place)}${stillOpen(result)}`,
|
|
3107
3117
|
result,
|
|
@@ -3507,7 +3517,7 @@ server.registerTool(
|
|
|
3507
3517
|
const { project, boards, live, base, changedAt } = await readProject();
|
|
3508
3518
|
return ok(
|
|
3509
3519
|
[
|
|
3510
|
-
`Project "${project.name}" (${door(live, base)})`,
|
|
3520
|
+
`Project "${project.name}"${project.nameOpen ? ` — its name is open, by the writer's word: "${project.nameOpen}"` : ""} (${door(live, base)})`,
|
|
3511
3521
|
`premise: ${project.premiseOpen ? `open, by the writer's word — "${project.premiseOpen}"` : project.premise ? `"${project.premise}"` : "(not set)"}`,
|
|
3512
3522
|
`boards: ${project.boards.length}`,
|
|
3513
3523
|
describeBoards(project, boards, changedAt),
|
|
@@ -3550,11 +3560,18 @@ server.registerTool(
|
|
|
3550
3560
|
"rename_project",
|
|
3551
3561
|
{
|
|
3552
3562
|
title: "Rename the project",
|
|
3553
|
-
description: "Rename the project — the name at the top of the wall, over every board.",
|
|
3554
|
-
inputSchema: { name: z.string().min(1) },
|
|
3563
|
+
description: "Rename the project — the name at the top of the wall, over every board. Or leave its name open: pass open with the writer's words for why the title is not decided — \"The Allotments, or Plot 14\" — and the name stands as it is while the reading lists the words; a name decides it, open \"\" takes the words back.",
|
|
3564
|
+
inputSchema: { name: z.string().min(1).optional(), open: z.string().optional() },
|
|
3555
3565
|
},
|
|
3556
3566
|
async (args) => {
|
|
3567
|
+
if (args.name === undefined && args.open === undefined) return ok("Say which: name, or open (the writer's words for why the title is not decided).");
|
|
3557
3568
|
const { project, boards, rev, base, live } = await readProject();
|
|
3569
|
+
if (args.name === undefined) {
|
|
3570
|
+
const opened = setProjectNameOpen(project, args.open);
|
|
3571
|
+
if (opened === project) return ok("Project name unchanged: it already reads that way.");
|
|
3572
|
+
await writeProject(opened, boards, rev, base);
|
|
3573
|
+
return ok(opened.nameOpen ? `The project keeps its name, "${opened.name}", and its name is left open, by the writer's word: "${opened.nameOpen}"${where(live)}. The reading lists it; rename_project with a name decides it.` : `The project's name, "${opened.name}", is no longer open${where(live)}.`, { name: opened.name, nameOpen: opened.nameOpen });
|
|
3574
|
+
}
|
|
3558
3575
|
const next = renameProject(project, args.name);
|
|
3559
3576
|
if (next === project) return ok("Project name unchanged.");
|
|
3560
3577
|
await writeProject(next, boards, rev, base);
|
|
@@ -3562,7 +3579,7 @@ server.registerTool(
|
|
|
3562
3579
|
// follow the rename (round fifteen, entry 40).
|
|
3563
3580
|
if (accountDoor) workingProject(next.id, next.name);
|
|
3564
3581
|
const several = (next.boards ?? []).length > 1;
|
|
3565
|
-
return ok(`Project renamed to "${next.name}"${where(live)}. It shows at the head of every reply, in list_boards and read_wall, and as the title of every script out${several ? `, where each board follows it as an episode line (Episode 1 of ${next.boards.length} · ${next.boards[0].name})` : ""}. The boards keep their names.`, next);
|
|
3582
|
+
return ok(`Project renamed to "${next.name}"${where(live)}.${project.nameOpen ? " Its name is decided; the open words are gone." : ""} It shows at the head of every reply, in list_boards and read_wall, and as the title of every script out${several ? `, where each board follows it as an episode line (Episode 1 of ${next.boards.length} · ${next.boards[0].name})` : ""}. The boards keep their names.`, next);
|
|
3566
3583
|
},
|
|
3567
3584
|
);
|
|
3568
3585
|
|
|
@@ -3740,13 +3757,17 @@ server.registerTool(
|
|
|
3740
3757
|
{
|
|
3741
3758
|
title: "Start a project",
|
|
3742
3759
|
description:
|
|
3743
|
-
"Through the account door: start a new project of the writer's with this name — one empty board, nothing on it — and work it from now on. The writer sees it under Projects on every device. board names the first board; boardOpen leaves its name open in the writer's words instead (\"the pilot, or the film\"), so a board born from a maybe is not silently \"Board 1\".",
|
|
3744
|
-
inputSchema: { name: z.string().min(1), board: z.string().optional(), boardOpen: z.string().optional(), pages: pagesSchema.optional(), minutes: z.number().positive().optional() },
|
|
3760
|
+
"Through the account door: start a new project of the writer's with this name — one empty board, nothing on it — and work it from now on. The writer sees it under Projects on every device. A title not decided: open with the writer's words (\"The Allotments, or Plot 14\") instead of a name, and the project starts as Untitled project with those words beside it. board names the first board; boardOpen leaves its name open in the writer's words instead (\"the pilot, or the film\"), so a board born from a maybe is not silently \"Board 1\".",
|
|
3761
|
+
inputSchema: { name: z.string().min(1).optional(), open: z.string().optional(), board: z.string().optional(), boardOpen: z.string().optional(), pages: pagesSchema.optional(), minutes: z.number().positive().optional() },
|
|
3745
3762
|
},
|
|
3746
3763
|
async (args) => {
|
|
3747
3764
|
const account = await findAccount();
|
|
3748
3765
|
if (!account) return shut("No account door: there is one project here, the open one. Set PLOTCODER_EMAIL and PLOTCODER_PASSWORD to start another on the writer's account.");
|
|
3749
|
-
|
|
3766
|
+
if (!args.name?.trim() && !args.open?.trim()) return ok("Say which: name (the title), or open (the writer's words for why the title is not decided — the project starts as \"Untitled project\" with those words beside it).");
|
|
3767
|
+
let record = emptyProject();
|
|
3768
|
+
if (args.name?.trim()) record = renameProject(record, args.name.trim());
|
|
3769
|
+
// A project born from a maybe is born open on its name (R61's edge): open holds the writer's words.
|
|
3770
|
+
if (args.open?.trim()) record = setProjectNameOpen(record, args.open);
|
|
3750
3771
|
if (args.board?.trim()) record = renameBoard(record, record.activeBoardId, args.board.trim());
|
|
3751
3772
|
// A board born from a maybe is born open on its name (R61): boardOpen holds the writer's words.
|
|
3752
3773
|
if (args.boardOpen?.trim()) record = setBoardNameOpen(record, record.activeBoardId, args.boardOpen);
|
|
@@ -3760,7 +3781,7 @@ server.registerTool(
|
|
|
3760
3781
|
joinPresence(record.id);
|
|
3761
3782
|
const targetLine = target === undefined ? ` Its target is ${formatPages(state.targetEighths)} pages, the default for a feature; set_target for a pilot or a half-hour, or pass pages or minutes here.` : ` Its target is ${formatPages(state.targetEighths)} pages.`;
|
|
3762
3783
|
const first = record.boards[0];
|
|
3763
|
-
const nameOpenLine = first.nameOpen ? ` The board's name is left open, by the writer's word: "${first.nameOpen}"; rename_board decides it.` : ""
|
|
3784
|
+
const nameOpenLine = `${record.nameOpen ? ` The project's name is left open, by the writer's word: "${record.nameOpen}"; rename_project decides it.` : ""}${first.nameOpen ? ` The board's name is left open, by the writer's word: "${first.nameOpen}"; rename_board decides it.` : ""}`;
|
|
3764
3785
|
return ok(`Started "${record.name}" (${record.id}) with its first board "${first.name}" (${first.id}), and working it now, as ${account.email}.${targetLine}${nameOpenLine}${oneCallHint(record)}`, { id: record.id, name: record.name, boardId: first.id, boardName: first.name, boardNameOpen: first.nameOpen ?? "", targetEighths: state.targetEighths });
|
|
3765
3786
|
},
|
|
3766
3787
|
);
|
package/src/board/agents.js
CHANGED
|
@@ -55,7 +55,7 @@ export const AGENTS = {
|
|
|
55
55
|
"No opinions about how many beats there should be. Marking the turns a treatment plainly makes is reading it, not an opinion: mark them, say which, and let the writer strike or add.",
|
|
56
56
|
"Page counts are estimates.",
|
|
57
57
|
"Ask before delete_board, delete_project, empty_account, delete_account, unlock_numbers, remove_file, an import_project that replaces, or claim_account — the writer gives the email and the password; never invent one. export_project first, when something might be wanted back.",
|
|
58
|
-
"Do not invent people or a logline. What the treatment states — an age, a job, a bad knee — is not invented: it goes in the person's notes. An unnamed person is named by their role — Dana's mother, the dispatcher — which is a name until the writer gives one. A scene is one place and one stretch of time; a new place or time is a new card. A beat is a whole card; a setup arrow lands on the scene's card, so a payoff never needs a card of its own. Acts are groups titled Act one, Act two, when the treatment has them; the wall never asks whether an act is a sequence. Paper colour means nothing to the app. Under target is a fact to report plainly, like over; neither is a verdict. A thing the writer has not decided is an open card in their words (set_open), or an open field — the logline, the premise, a when, a board's name take open beside the value — never a guess to fill the field; a thing whose far end the writer knows and not where it is first seen — the letter, the ring — is a thread (create_thread) with an open start, and the wall asks from that end.",
|
|
58
|
+
"Do not invent people or a logline. What the treatment states — an age, a job, a bad knee — is not invented: it goes in the person's notes. An unnamed person is named by their role — Dana's mother, the dispatcher — which is a name until the writer gives one. A scene is one place and one stretch of time; a new place or time is a new card. A beat is a whole card; a setup arrow lands on the scene's card, so a payoff never needs a card of its own. Acts are groups titled Act one, Act two, when the treatment has them; the wall never asks whether an act is a sequence. Paper colour means nothing to the app. Under target is a fact to report plainly, like over; neither is a verdict. A thing the writer has not decided is an open card in their words (set_open), or an open field — the logline, the premise, a card's place or when, a board's or the project's name take open beside the value — never a guess to fill the field; a thing whose far end the writer knows and not where it is first seen — the letter, the ring — is a thread (create_thread) with an open start, and the wall asks from that end.",
|
|
59
59
|
],
|
|
60
60
|
person:
|
|
61
61
|
"Give your agent the account door only on a machine you trust; it signs in as you and shows under People as “an agent, as you” while it runs. Wire the server before you start the agent's session, with the two sign-in lines beside it, and the agent has every tool from its first message; wired from inside a session, the server connects only on the next one. Your agent can also make your account: give it your email and a password of your choosing.",
|
package/src/board/fountain.js
CHANGED
|
@@ -124,6 +124,7 @@ export function toFountain(state, options = {}) {
|
|
|
124
124
|
if (cast.length) marks.push(`with ${cast.join(", ")}`);
|
|
125
125
|
if (note.plants) marks.push(note.plantsWhat ? `plants ${note.plantsWhat}` : "plants something to pay off later");
|
|
126
126
|
if (note.open) marks.push(`open: ${note.open}`);
|
|
127
|
+
if (note.locationOpen) marks.push(`place open: ${note.locationOpen}`);
|
|
127
128
|
if (note.whenOpen) marks.push(`when open: ${note.whenOpen}`);
|
|
128
129
|
const onThreads = (state.threads ?? []).filter((thread) => thread.noteIds.includes(note.id)).map((thread) => thread.name);
|
|
129
130
|
if (onThreads.length) marks.push(`thread: ${onThreads.join(", ")}`);
|
package/src/board/project.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type ProjectRecord = {
|
|
|
19
19
|
version: number;
|
|
20
20
|
id: string;
|
|
21
21
|
name: string;
|
|
22
|
+
/** The writer's words for why the project's name is not decided (R61's edge), or empty; the name stands meanwhile. */
|
|
23
|
+
nameOpen: string;
|
|
22
24
|
premise: string;
|
|
23
25
|
/** The writer's words for why there is no premise yet (R61), or empty. */
|
|
24
26
|
premiseOpen: string;
|
|
@@ -75,6 +77,8 @@ export declare function setPremise(project: ProjectRecord, premise: string, now?
|
|
|
75
77
|
export declare function setPremiseOpen(project: ProjectRecord, words: string, now?: string): ProjectRecord;
|
|
76
78
|
/** The writer's words for why a board's name is not decided (R61); the name stands meanwhile. */
|
|
77
79
|
export declare function setBoardNameOpen(project: ProjectRecord, id: string, words: string, now?: string): ProjectRecord;
|
|
80
|
+
/** The writer's words for why the project's name is not decided (R61's edge); the name stands meanwhile. */
|
|
81
|
+
export declare function setProjectNameOpen(project: ProjectRecord, words: string, now?: string): ProjectRecord;
|
|
78
82
|
export declare function boardById(project: ProjectRecord, id: string): BoardMeta | null;
|
|
79
83
|
/** What a script going out is called: a named project is the title, its board beside it only when the project has several. */
|
|
80
84
|
export declare function scriptTitles(project: ProjectRecord, board: BoardMeta | null | undefined): { title: string; episode?: string };
|
package/src/board/project.js
CHANGED
|
@@ -33,6 +33,7 @@ export function emptyProject(now = nowIso()) {
|
|
|
33
33
|
version: PROJECT_VERSION,
|
|
34
34
|
id: newId(),
|
|
35
35
|
name: DEFAULT_PROJECT_NAME,
|
|
36
|
+
nameOpen: "",
|
|
36
37
|
premise: "",
|
|
37
38
|
premiseOpen: "",
|
|
38
39
|
boards: [board],
|
|
@@ -87,6 +88,8 @@ export function normalizeProject(value, now = nowIso()) {
|
|
|
87
88
|
...rest,
|
|
88
89
|
version: PROJECT_VERSION,
|
|
89
90
|
name: trimmed(value.name, DEFAULT_PROJECT_NAME),
|
|
91
|
+
// A project named before R61's edge has no open name; the name stands until the writer says it is not decided.
|
|
92
|
+
nameOpen: openWords(value.nameOpen),
|
|
90
93
|
premise: typeof value.premise === "string" ? value.premise.trim() : "",
|
|
91
94
|
// A project written before R61 has no open premise (R61).
|
|
92
95
|
premiseOpen: openWords(value.premiseOpen),
|
|
@@ -342,8 +345,16 @@ export function setActiveBoard(project, id, now = nowIso()) {
|
|
|
342
345
|
|
|
343
346
|
export function renameProject(project, name, now = nowIso()) {
|
|
344
347
|
const next = trimmed(name, "");
|
|
345
|
-
if (!next || next === project.name) return project;
|
|
346
|
-
|
|
348
|
+
if (!next || (next === project.name && !(project.nameOpen ?? ""))) return project;
|
|
349
|
+
// A name decides the field: the open words go (R61's edge).
|
|
350
|
+
return touch(project, { name: next, nameOpen: "" }, now);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The writer's words for why the project's name is not decided (R61's edge), or "" to take them back; the name stands meanwhile. */
|
|
354
|
+
export function setProjectNameOpen(project, words, now = nowIso()) {
|
|
355
|
+
const next = openWords(words);
|
|
356
|
+
if (next === (project.nameOpen ?? "")) return project;
|
|
357
|
+
return touch(project, { nameOpen: next }, now);
|
|
347
358
|
}
|
|
348
359
|
|
|
349
360
|
export function setPremise(project, premise, now = nowIso()) {
|
package/src/board/readWall.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export type WallReading = {
|
|
|
72
72
|
/** Open cards (R59): the writer's words for what is not decided, in story order; not asked about while they stand. */
|
|
73
73
|
open: Array<{ id: string; words: string; hides: FindingKind[] }>;
|
|
74
74
|
/** Fields left open by the writer's word (R61): the board's logline, and each card's when, in story order. Listed, not asked. */
|
|
75
|
-
openFields: Array<{ field: "logline"; words: string } | { field: "when"; id: string; words: string }>;
|
|
75
|
+
openFields: Array<{ field: "logline"; words: string } | { field: "location"; id: string; words: string } | { field: "when"; id: string; words: string }>;
|
|
76
76
|
/** Threads (R60): each named string with its cards in story order and which ends are open. */
|
|
77
77
|
threads: Array<{ id: string; name: string; ids: string[]; startOpen: boolean; endOpen: boolean; /** Eighths from the first card's start to the last card's start; 0 with fewer than two cards. */ apart: number }>;
|
|
78
78
|
/** Cards here that pay off a fold of another board (R58), composed by the door from the project. */
|
package/src/board/readWall.js
CHANGED
|
@@ -241,7 +241,8 @@ export function readWall(state, options = {}) {
|
|
|
241
241
|
// A card that says no place, once the writer has started placing cards.
|
|
242
242
|
// One question however many there are; a wall with no places at all is a
|
|
243
243
|
// wall the writer has not placed yet, and is not asked.
|
|
244
|
-
|
|
244
|
+
// A place left open in the writer's words (R61's edge) is listed, not asked.
|
|
245
|
+
const unplaced = order.filter((note) => askable(note) && !(note.location ?? "").trim() && !(note.locationOpen ?? "").trim());
|
|
245
246
|
if (unplaced.length > 0 && unplaced.length < order.length) {
|
|
246
247
|
findings.push({
|
|
247
248
|
kind: "unplaced",
|
|
@@ -535,6 +536,7 @@ export function readWall(state, options = {}) {
|
|
|
535
536
|
function describeOpenFields(state, order) {
|
|
536
537
|
const fields = [];
|
|
537
538
|
if ((state.loglineOpen ?? "").trim()) fields.push({ field: "logline", words: state.loglineOpen.trim() });
|
|
539
|
+
for (const note of order) if ((note.locationOpen ?? "").trim()) fields.push({ field: "location", id: note.id, words: note.locationOpen.trim() });
|
|
538
540
|
for (const note of order) if ((note.whenOpen ?? "").trim()) fields.push({ field: "when", id: note.id, words: note.whenOpen.trim() });
|
|
539
541
|
return fields;
|
|
540
542
|
}
|
package/src/board/reducer.d.ts
CHANGED
|
@@ -93,6 +93,8 @@ export type BoardNote = {
|
|
|
93
93
|
when: string;
|
|
94
94
|
/** The writer's words for why the when is not decided (R61), or empty; while they stand the when is blank and the reading lists them. */
|
|
95
95
|
whenOpen: string;
|
|
96
|
+
/** The writer's words for why the place is not decided (R61's edge), or empty; while they stand the place is blank, listed, and not asked. */
|
|
97
|
+
locationOpen: string;
|
|
96
98
|
/** The scene's text in Fountain (R23 b): action, cues, dialogue; empty until written. */
|
|
97
99
|
text: string;
|
|
98
100
|
createdAt: string;
|
|
@@ -186,6 +188,7 @@ export type Command =
|
|
|
186
188
|
location?: string;
|
|
187
189
|
when?: string;
|
|
188
190
|
whenOpen?: string;
|
|
191
|
+
locationOpen?: string;
|
|
189
192
|
open?: string;
|
|
190
193
|
text?: string;
|
|
191
194
|
}
|
|
@@ -216,7 +219,7 @@ export type Command =
|
|
|
216
219
|
| { type: "set_plant"; ids: string[]; plants?: boolean; what?: string }
|
|
217
220
|
| { type: "set_open"; ids: string[]; open: string }
|
|
218
221
|
| { type: "set_payoff_board"; ids: string[]; boardId: string | null; noteId?: string | null }
|
|
219
|
-
| { type: "set_location"; ids: string[]; location
|
|
222
|
+
| { type: "set_location"; ids: string[]; location?: string; open?: string }
|
|
220
223
|
| { type: "set_when"; ids: string[]; when?: string; open?: string }
|
|
221
224
|
| { type: "apply_template"; template: string; beats?: Array<{ name: string; prompt: string; at: number }> }
|
|
222
225
|
| { type: "set_text"; id: string; text: string }
|
package/src/board/reducer.js
CHANGED
|
@@ -247,6 +247,8 @@ export function seedState(now = nowIso()) {
|
|
|
247
247
|
lengthEighths: null,
|
|
248
248
|
characterIds,
|
|
249
249
|
location: "",
|
|
250
|
+
// The writer's words for why the place is not decided (R61), or nothing.
|
|
251
|
+
locationOpen: "",
|
|
250
252
|
when: "",
|
|
251
253
|
// The writer's words for why the when is not decided (R61), or nothing.
|
|
252
254
|
whenOpen: "",
|
|
@@ -368,6 +370,8 @@ export function normalizeState(value) {
|
|
|
368
370
|
const open = typeof note?.open === "string" ? note.open : "";
|
|
369
371
|
// Cards written before R37 have no place; a scene is nowhere until it is.
|
|
370
372
|
const location = typeof note?.location === "string" ? note.location : "";
|
|
373
|
+
// Cards written before R61's edge have no open place; a place is decided or blank until the writer says otherwise.
|
|
374
|
+
const locationOpen = typeof note?.locationOpen === "string" ? note.locationOpen : "";
|
|
371
375
|
// Cards written before R55 have no when; a scene is at no time until it is.
|
|
372
376
|
const when = typeof note?.when === "string" ? note.when : "";
|
|
373
377
|
// Cards written before R61 have no open when; a when is decided or blank until the writer says otherwise.
|
|
@@ -386,6 +390,7 @@ export function normalizeState(value) {
|
|
|
386
390
|
note.payoffNoteId === payoffNoteId &&
|
|
387
391
|
note.open === open &&
|
|
388
392
|
note.location === location &&
|
|
393
|
+
note.locationOpen === locationOpen &&
|
|
389
394
|
note.when === when &&
|
|
390
395
|
note.whenOpen === whenOpen &&
|
|
391
396
|
note.text === text
|
|
@@ -393,7 +398,7 @@ export function normalizeState(value) {
|
|
|
393
398
|
return note;
|
|
394
399
|
}
|
|
395
400
|
patched = true;
|
|
396
|
-
return { ...note, rank, lengthEighths, characterIds, plants, plantsWhat, payoffBoardId, payoffNoteId, open, location, when, whenOpen, text };
|
|
401
|
+
return { ...note, rank, lengthEighths, characterIds, plants, plantsWhat, payoffBoardId, payoffNoteId, open, location, locationOpen, when, whenOpen, text };
|
|
397
402
|
});
|
|
398
403
|
|
|
399
404
|
// Boards written before the production half (Roadmap 2, item 8) have no
|
|
@@ -636,7 +641,8 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
636
641
|
payoffBoardId: null,
|
|
637
642
|
payoffNoteId: null,
|
|
638
643
|
open: cleanOpen(command.open),
|
|
639
|
-
location: cleanPlace(command.location),
|
|
644
|
+
location: cleanOpen(command.locationOpen) ? "" : cleanPlace(command.location),
|
|
645
|
+
locationOpen: cleanOpen(command.locationOpen),
|
|
640
646
|
when: cleanOpen(command.whenOpen) ? "" : cleanWhen(command.when),
|
|
641
647
|
whenOpen: cleanOpen(command.whenOpen),
|
|
642
648
|
text: typeof command.text === "string" ? command.text : "",
|
|
@@ -1165,6 +1171,7 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
1165
1171
|
payoffNoteId: null,
|
|
1166
1172
|
open: "",
|
|
1167
1173
|
location: "",
|
|
1174
|
+
locationOpen: "",
|
|
1168
1175
|
when: "",
|
|
1169
1176
|
whenOpen: "",
|
|
1170
1177
|
text: "",
|
|
@@ -1256,11 +1263,21 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
1256
1263
|
case "set_location": {
|
|
1257
1264
|
const ids = new Set(command.ids);
|
|
1258
1265
|
if (ids.size === 0) return { state, changed: false };
|
|
1259
|
-
|
|
1266
|
+
// The place, or the writer's words for why there is none yet (R61's
|
|
1267
|
+
// edge, round twenty entry 16): words clear the place, a place clears
|
|
1268
|
+
// the words, open "" with a place does both in one command.
|
|
1269
|
+
const hasOpen = typeof command.open === "string";
|
|
1270
|
+
const locationOpen = hasOpen ? cleanOpen(command.open) : null;
|
|
1271
|
+
const location = hasOpen ? (locationOpen ? "" : typeof command.location === "string" ? cleanPlace(command.location) : null) : cleanPlace(command.location);
|
|
1260
1272
|
const touched = [];
|
|
1261
1273
|
const notes = state.notes.map((note) => {
|
|
1262
|
-
if (!ids.has(note.id)
|
|
1263
|
-
const
|
|
1274
|
+
if (!ids.has(note.id)) return note;
|
|
1275
|
+
const patch = {
|
|
1276
|
+
location: location === null ? (note.location ?? "") : location,
|
|
1277
|
+
locationOpen: locationOpen === null ? (location ? "" : (note.locationOpen ?? "")) : locationOpen,
|
|
1278
|
+
};
|
|
1279
|
+
if (patch.location === (note.location ?? "") && patch.locationOpen === (note.locationOpen ?? "")) return note;
|
|
1280
|
+
const next = bump(note, patch, now);
|
|
1264
1281
|
touched.push(next);
|
|
1265
1282
|
return next;
|
|
1266
1283
|
});
|
package/src/board/words.js
CHANGED
|
@@ -66,7 +66,7 @@ export const WORD_GROUPS = [
|
|
|
66
66
|
{
|
|
67
67
|
id: "open",
|
|
68
68
|
name: "Open",
|
|
69
|
-
sentence: "A card the writer has not decided, in their words on its edge: the reading lists it and asks nothing else of it until the words are cleared. A field can be open the same way — the logline, the premise, a card's when, a board's name — the words where the value would be, listed and not asked.",
|
|
69
|
+
sentence: "A card the writer has not decided, in their words on its edge: the reading lists it and asks nothing else of it until the words are cleared. A field can be open the same way — the logline, the premise, a card's place or when, a board's or the project's name — the words where the value would be, listed and not asked.",
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
id: "thread",
|