ארכיטקטורת המשימה

אין סקריפטים. יש לוגיקה: איך נבנות הצמתים והמשתנים ב-Romergo

כשמדברים על לוגיקה בעורך המשחקים, בדרך כלל זה מהיר מגיע לסקריפטים. במקום מסוים הכותב כותב JavaScript, במקום אחר Python, ובמקום אחר לומד את שפת המנוע שלו. זה מעניק כמעט חופש בלתי מוגבל, אך יחד עם זאת מביא שגיאות תחביר, לולאות תקועות, תוספים לא תואמים וקוד שבאחר חצי שנה מפחיד אפילו עבור מחברו לפתוח אותו.

ב-Romergo הלכתי בדרך אחרת. אין סקריפטים אקראיים בתוך המשימה. אי אפשר להכניס פונקציה, לבצע בקשה רשתית או לקבל גישה ישירה לנגן. במקום זאת, הסיפור נבנה ממערך מוגבל, אך ברור ומשמעותי דיו: צמתים לוגיים, משתנים, תנאים ואפקטים.

זה לא אומר שב-Romergo אפשר לעשות רק פיצולי דרך פשוטים. ניתן לזכור את החלטות השחקן, לחשב נקודות, לפתוח ולהסתיר אפשרויות, לבחור מסלולים שונים בפרקים הבאים, ליצור אירועים אקראיים ולבנות ממשקי משתמש אינטראקטיביים. פשוט במקום הפקודה "בצע קוד זה" המחבר מתאר "במצב זה צריך לקרות מה שקרה זה".

הגרף כבר מהווה תוכנה

הלוגיקה הבסיסית ביותר Romergo נמצאת ישירות על מפת הפרק. הסצנות מחוברות בעברות, והצמתים המיוחדים מחליטים לאן ימשיך המשחק.

אם לפשט מאוד, המשימה הקטנה נראית כך:

התחלה
  ↓
שיחה עם השומר
  ↓
יש מעבר?
  ├─ כן → ארכיון סגור
  └─ לא → מסלול חלופי

זו כבר לוגיקה הניתנת לביצוע. Player נכנס לצומת ההתחלה, מציג את הסצנה, קורא את מצב ההיסטוריה ובוחר את המעבר המתאים. המחבר רואה את אותו מסלול כמפה מובנת, ולא כאוסף של if, goto וזיהויים שירותיים.

![Flow Romergo: ההאשמה הסופית עוברת דרך Switch ושלושה If / Else לכיוונים שונים לסיום, למטה פתוחה לוח Logic](../assets/logic-without-scripts-flow-nodes.webp)

*קטע של Flow אמיתי ב-Builder: סצנת Final Accusation מעבירה החלטה ל-Switch, ואז כמה If / Else בודקים את המצב ומפצלים את הסיפור לכמה סיומות שונות. הפאנל הפתוח של Logic בתחתית מראה את כל הסט הזמין: Dead End, Checkpoint, If / Else, Switch, Random ו-Teleport.*

בפאנל הלוגיקה יש כרגע שש נקודות עיקריות:

התחלה וסיום מסגרות את הפרק, סצנות רגילות מציגות את התוכן, וצמתים לוגיים הופכים אותם למסלול. כבר ברמה הזו אפשר לאסוף סיפור ליניארי, הסתעפות, כמה סיומות, אירוע אקראי ונקודת חזרה בטוחה — בלי שורת קוד אחת.

משתנה הוא זיכרון של ההיסטוריה

גרף אחד לא מספיק אם ההיסטוריה צריכה לזכור מה התרחש קודם. בשביל זה במשימה יש משתנים משלושה סוגים פשוטים:

לכל משתנה יש מפתח, תווית מובנת למחבר וערך התחלתי. ניתן לתאר מספרי משתנה גם מינימום ומקסימום, ומשתנה שירותי — להסתיר אותו מלוח המצב של השחקן או להראותו רק בביצוע תנאי מסוים.

העיקר כאן הוא שהמשתנים שייכים לכל משך ההרפתקה, ולא לסצנה אחת בלבד. אם בפרק הראשון השחקן קיבל כרטיס כניסה, בפרק הרביעי ניתן לבדוק את אותו דגל. בעת המעבר דרך סיום Chapter לפרק הבא, המצב לא מאופס. הוא נשמר יחד עם הבחירות שנעשות, האפקטים שהוחלו, נקודת הבדיקה והשלב הנוכחי של המקריות.

כך מתקבל מחזור פשוט לכותב:

הבחירה של השחקן רושמת ערך
              ↓
המשתנה שומר את המצב
              ↓
התנאי קורא אותו לאחר מכן
              ↓
הגרף, הסצנה או הממשק מגיבים

לדוגמה, בסצנה הראשונה הבחירה «להראות את התמונה שנמצאה» יכולה להגדיר את portrait_revealed = true. מאוחר יותר, הצומת **אם / אחרת** תבדוק את הדגל הזה ותפתח סצנת שיחה חדשה. עוד מאוחר יותר, ממשק הטרמינל יציג רשומה נוספת רק לשחקנים שגילו את התמונה.

משתנה אחד מקשר בין שלושה חלקים שונים של המשימה. אין צורך להעביר ידנית את הערך בין סצנות או פרקים: הם קוראים את מצב ההתקדמות המשותף.

![העמוד של המשתנה route מציג מפתח, שם, סוג וראות של המצב](../assets/logic-without-scripts-variables.webp)

*למשתנה route בכרטיס אחד מוגדר מפתח יציב, שם מובן, סוג ערך וראות של המצב. הקשרים עם בחירות וסניפים נמצאים למטה על אותו דף.*

הבחירות כותבות, הסניפים קוראים

בתרחיש רגיל, הדרך הברורה ביותר לשנות מצב היא להוסיף אפקט לאפשרות הבחירה. בעורך הוויזואלי זה נראה כמו השמה:

«להבטיח למרי» → ally = "mary"
«ללכת לבד» → ally = "none"

לאחר מכן Switch יכול להפנות את השחקן לתרחיש הרצוי לפי הערך של ally. דף המשתנים מראה בנפרד אילו בחירות רושמות את המשתנה ואילו ענפים קוראים אותו. זו פרט קטן, אבל במשימה גדולה זה מחליף את החיפוש המעיק בעשרות תרחישים.

![הגדרות Switch עם ענפים Laboratory ו-Habitat, שמבצעים השוואה של משתנה הנתיב הנבחר](../assets/logic-without-scripts-switch-rules.webp)

*הענפים Switch נבדקים מלמעלה למטה. בדוגמה זו הנתיב Laboratory מופעל ב-route = lab, ו-Habitat — ב-route = habitat.*

התנאים תומכים בהשוואות רגילות:

במקרה הפשוט, המחבר בוחר משתנה, אופרטור וערך ישירות בהגדרות של If / Else או Switch. עבור לוגיקה מורכבת יותר, runtime הכללי מבין קבוצות **כל התנאים**, **כל תנאי** ושלילה.

את הכלל הזה ניתן לתאר במבנה:

{
  "op": "all",
  "conditions": [
    { "op": "gte", "key": "evidence", "value": 3 },
    { "op": "eq", "key": "alarm_active", "value": false }
  ]
}

זה דומה לסקריפט קטן, אבל לא עקרונית לא כזה. כאן אי אפשר לקרוא לפונקציה או לשנות משהו אקראי. Romergo כבר יודע מראש את כל הפעולות המותרות, בודק את המבנה ומבצע אותו באופן זהה בתצוגה מקדימה של Builder ובפרסום של Player.

אפקטים: פקודות קטנות ללא שפת תכנות

התנאי קורא את המצב, והאפקט משנה אותו. Runtime Romergo תומך בכמה פעולות אטומיות:

בבודק הרגיל, הסצנה העיקרית נשארת פשוטה בכוונה: לבחור משתנה ולרשום ערך חדש דרך set. בלוגיקה מתקדמת של סצנות ממשק, אפשר להגדיר תנאים ואפקטים כמבנים שניתן לבדוק. לדוגמה, המעבר בין מצבי הטרמינל יכול בו זמנית לזכור החלטה, להוסיף רמז ולהפעיל אזעקה:

![עורך הסצנה Romergo: שלוש אפשרויות הבחירה רושמות את lab, habitat ו-server במשתנה route](../assets/logic-without-scripts-choice-effects.webp)

*האפקט מחובר ישירות לאופציית התשובה: שלוש בחירות רושמות ב-route את הערכים lab, habitat ו-server. שדה המשתנה והערך החדש נשארים נפרדים גם במפענח הצר.*

[
  { "op": "set", "key": "terminal_decision", "value": "cut-power" },
  { "op": "inc", "key": "evidence", "amount": 1 },
  { "op": "toggle", "key": "alarm_active" }
]

לשיטה כזו יש מגבלה חשובה: זה אינו מחשבון לנוסחאות אקראיות. לא ניתן לכתוב את evidence * trust / 2, להפעיל לולאה או ליצור פונקציה משלך. ההתנהגות המורכבת נבנית ממספר צעדים גלויים, והרחבת הלוגיקה מתבצעת באמצעות פעולות ניתנות לבדיקה חדשות runtime, ולא באמצעות הרצת קוד לא מוכר.

למחבר זה קצת פחות בלתי מוגבל, אך הסיפור נשאר צפוי. את אותם האפקטים אפשר לבדוק לפני הפרסום, לשמור, לסנכרן בין השחקנים ולשחזר לאחר טעינת ההתקדמות.

למה בתוך הצומת הלוגי עדיין אין עוד Flow

למרות כל הצפיות, לגישה הנוכחית יש חיסרון ברור: לאדם ללא ניסיון פיתוח, הצורה "משתנה — אופרטור — ערך" כבר יכולה להיראות כתכנות. וכאשר מופיעות ליד קבוצות תנאים וכמה אפקטים, המבנה הדקלרטיבי נשאר בטוח עבור Player, אבל לא בהכרח הופך לפשוט למחבר.

נראה טבעי יום אחד להגיע לצמתים ויזואליים וגם בתוך הלוגיקה עצמה. במקום רשימת תנאים אפשר לחבר בלוקים של **ו**, **או**, **לא**, השוואת משתנים ושינוי ערך. על הנייר זה נראה ידידותי יותר מטקסט או JSON.

אבל אז אני מדמיין תרחיש אמיתי: המחבר פותח צומת לוגי במפת הפרק, ובתוכה מגלה עוד Flow עם צמתים וקשרים משלו. מתקבל גרף בתוך גרף. צריך להבין איפה אתה עכשיו, איך לחזור לרמת הסיפור ובאיזה מבין שני Flow לחפש את השגיאה. עבור מתחיל, שכבות כאלה עלולות להיות מפחידות יותר מהצורה הנוכחית.

לכן בינתיים משתמשים בפשרה. מסלול הסיפור נשאר ויזואלי, בעוד שהתנאים והאפקטים בתוך הצמתים מוצגים בשדות מפורטים ודחוסים. זה עובד, אבל אני לא רואה את הממשק כפתרון סופי. יתכן שבעתיד יופיע שילוב ברור יותר של תבניות מוכנות, כיוונון שלב-אחר-שלב וחסימות ויזואליות — כזה שמפחית באמת את המורכבות, ולא רק מעביר אותה לעורך נוסף.

הלוגיקה חיה לא רק במפה

סצנות הממשק משתמשות באותו המצב. ווידג׳ט, הודעה, אפשרות תגובה, משימה, התראה או זרם מדיה יכולים להופיע רק כאשר showWhen תואם. ניתן לקשר את ערך המדד למשתנה, ואת המעבר בין מצבי הווידג׳ט — להפעיל לאחר השהיה, על פי תנאי או אחרי פעולה של השחקן.

לדוגמה, מסך מחשב הלוח יכול להתנהג כך:

1. תחילה נראה רק טרמינל נעול.

2. לאחר בחירת השחקן, האפקט מגדיר את terminal_unlocked = true.

3. תנאי פותח את התרשים של הספינה וכפתורים חדשים.

4. לחיצה על הכפתור משנה את מצב הווידג'ט ומגדילה את evidence.

5. כאשר evidence >= 3, מופיעה הודעה נסתרת ונפתחת ענף חדש של המשימה.

מפת הפרק, הבחירות הרגילות והממשק האינטראקטיבי אינם יוצרים שלוש מערכות נפרדות. הם קוראים ומשנים אובייקט מצב אחד. לכן הרמז שהתגלה בדיאלוג יכול לפתוח אלמנט בממשק, והפעולה בתוך הממשק יכולה לשנות את הסצנה שבאה לאחר מכן.

כאן בדיוק הלוגיקה ההצהרתית מתחילה להרגיש כמו תכנות אמיתי. רק שבמקום קבצים, פונקציות ואוטובוס אירועים, המחבר עובד עם ישויות ברורות של ההיסטוריה: עובדות, בחירות, תנאים ומעברים.

למה אני בכוונה לא מוסיף JavaScript שרירותי

האפשרות להכניס סקריפט נראית כדרך הקצרה ביותר לכל מנגנון חדש. אבל ברגע שהקווסט שפורסם מקבל זכות להריץ קוד שרירותי, כל מודל המוצר משתנה.

צריך להחליט אילו API של דפדפן זמינים לקוד הזה, איך להגביל את הרשת והאחסון, מה לעשות עם לולאות אינסופיות, איך להעביר משימות למצב לא מקוון, איך לסנכרן אותן במשחק משותף ואיך להבטיח שהפרויקט הישן ימשיך לעבוד לאחר עדכון Player.

חוק הצהרה הוא הרבה יותר משעמם מפונקציה שרירותית — וזו הסיבה שהוא יותר מהימן. ניתן לאמת אותו לפני ההפעלה. ניתן להבין אילו משתנים הוא קורא וכותב. ניתן לשמור את ההתקדמות בבטחה באמצע השרשרת. ניתן לבצע את המשימה בצורה זהה בדפדפן, בבנייה בלתי מקוונת וב-Player המובנה.

מילון מוגבל כאן לא מפריע להיגיון, אלא מגדיר את הגבולות שלו. אם מופיעה פעולה באמת שימושית, עדיף להוסיף אותה ל-runtime הכללי ולתת לכל המחברים התנהגות זהה ומבוקרת, מאשר לכפות על כל פרויקט להמציא מנוע משלו.

לקינוח — HTML ו-CSS מיוחדים

בכל זאת, Romergo משאיר מקום לקוד אמיתי שם שהוא הכי בטוח: בעיצוב סצנה רגילה.

בפרויקט ניתן ליצור נושא עיצוב משלך ב-HTML ו-CSS ולהשתמש בו בפרקים שונים. HTML קובע את מיקום האזורים המוכנים – חלון הדיאלוג, הפורטרט, שם הדמות, הטקסט, השאלה וכפתורי הבחירה. CSS מאפשר לשנות בצורה משמעותית אזורים אלו: מסגרות, רקע, צורת הכרטיסים, רווחים, טיפוגרפיה ותגובה לגודל המסך.

הנושאים המוכנים **Standard**, **Minimalism**, **Brutalism**, **Romance** ו-**Neon** מציגים טווח מאומת ללא עיצוב ידני. בעת יצירת נושא מותאם אישית Romergo מעתיק את התבנית הנבחרת: לאחר מכן ניתן לערוך את HTML ו-CSS מיד ולבדוק בהצגת התצוגה המקדימה האמיתית Player.

![עורך העיצוב האמיתי Romergo: CSS של נושא Brutalism ותצוגה מקדימה לנייד של הדיאלוג](../assets/logic-without-scripts-custom-html-css.webp)

*כאן אין קונספט-ארט: משמאל מוצג CSS האמיתי של נושא מובנה Brutalism, מימין — התוצאה של אותו קוד בתצוגת הטלפון. מתג Desktop / Mobile מאפשר לבדוק מיד את כללי ההתאמה, ונושא מותאם אישית מתחיל לפעול עם העתק של התבנית שנבחרה.*

אבל הגבול נשאר כפי שהיה. בנושא מותאם אישית HTML אין <script>, מטפלי אירועים, טפסים, iframe ומשאבים חיצוניים. CSS לא יכול לטעון url() או @import חיצוניים. האזורים החיוניים של הסצנה חייבים להישאר במקום; אם התבנית איבדה אותם, Player חוזר לעיצוב הבטוח והסטנדרטי.

כלומר HTML ו-CSS אחראיים ל-**איך הסיפור נראה**, ואילו הצמתים, המשתנים, התנאים והאפקטים — ל-**איך הוא פועל**.

אני אוהב בדיוק כזה פירוד. המחבר יכול להפוך את המשימה לוויזואלית שלא דומה לאחרות, אך הביצוע שלה נשאר עדיין חלק מה-runtime הכללי הנבדק. את השכבה החיצונית אפשר לצבוע מחדש ולבנות מחדש בצורה דרמטית, מבלי להפוך כל נושא לתוכנית נפרדת.

קוד ללא קוד — זה לא חוסר לוגיקה

ל-Romergo אין מטרה להחליף שפת תכנות בתמונות. גרפים אינם מתאימים למתמטיקה מסובכת, וקבוצת אפקטים אטומיים לא תהפוך למנוע אוטומציה אוניברסלי.

לעומת זאת, עבור סיפור אינטראקטיבי לרוב נדרשות דברים אחרים: לזכור עובדה, לשנות מונה, לפתוח אפשרות, לבחור מסלול, להראות מצב ממשק, לשמור נקודת חזרה ולהעביר את כל ההחלטות הללו לפרק הבא.

למטרה זו נודס ומשתנים אינם גרסה מפושטת של סקריפטים, אלא שפה ישירה יותר של הסיפור עצמו. המחבר מתאר לא פקודות למחשב, אלא קשרי סיבה ותוצאה של העולם: השחקן קיבל החלטה, המצב השתנה, הסיפור הגיב.

ואם רוצים קצת קוד אמיתי, HTML ו-CSS עדיין מחכים לקינוח.

חזרה לבלוג הפיתוח