Initial project setup: Open The Box CLI game

- .NET 10 console app with Spectre.Console and Loreline integration
- Black Box Sim architecture (simulation separated from presentation)
- Progressive CLI rendering (9 phases from basic to full layout)
- 25+ box definitions with weighted loot tables
- 100+ item definitions (meta, cosmetics, materials, adventure tokens)
- 9 Loreline adventures (Space, Medieval, Pirate, etc.)
- Bilingual content (EN/FR)
- Save/load system
- Game Design Document
This commit is contained in:
Samuel Bouchet 2026-03-10 18:24:01 +01:00
commit 04894a4906
79 changed files with 9285 additions and 0 deletions

43
.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
## .NET
bin/
obj/
*.user
*.suo
*.userosscache
*.sln.docstates
.vs/
*.dll
*.pdb
*.exe
## NuGet
**/[Pp]ackages/*
!**/[Pp]ackages/build/
*.nupkg
**/[Pp]roject.lock.json
project.assets.json
## Keep Loreline DLL
!lib/Loreline.dll
## Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
## Saves
saves/
## IDE
.idea/
*.swp
*~
.vscode/
## OS
Thumbs.db
.DS_Store
## Claude
.claude/

5
OpenTheBox.slnx Normal file
View file

@ -0,0 +1,5 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/OpenTheBox/OpenTheBox.csproj" />
</Folder>
</Solution>

View file

@ -0,0 +1,241 @@
// Contemporary Adventure - Open The Box
// Theme: Corporate office meets mysterious box absurdity
character sandrea
name: Sandrea
role: Office Worker
character samuel
name: Samuel
role: IT Guy
state
emails: 0
boxPower: 0
hrInvolved: false
unionFormed: false
beat Intro
Monday morning. Fluorescent lights. The faint smell of burned coffee and crushed dreams. #intro-monday
You arrive at Boxington & Associates Ltd. (Motto: "Thinking Outside The Box Since Never"). #intro-arrive
sandrea: Hey, have you seen the break room? Something... appeared over the weekend. #sandrea-seen
sandrea: And by "something," I mean a box. And by "appeared," I mean nobody will claim responsibility. #sandrea-appeared
choice
Go check the break room #opt-check
-> BreakRoom
Check your email first #opt-email
emails += 1
-> CheckEmail
Pretend you didn't hear anything #opt-pretend
-> Pretend
beat CheckEmail
You open your inbox. 47 unread emails. 46 of them are about the box. #email-open
Subject lines include: "RE: RE: RE: FW: THE BOX", "Box Situation Update #17", and "Please stop replying all about the box." #email-subjects
The last email is from HR: "Mandatory Box Awareness Training - Tuesday 2pm. Attendance is not optional." #email-hr
emails += 46
hrInvolved = true
sandrea: HR sent a training about the box. It hasn't even been 24 hours. #sandrea-hr
sandrea: That's a new record. Usually it takes them two weeks to acknowledge anything exists. #sandrea-record
-> BreakRoom
beat Pretend
You put on your headphones and stare at a spreadsheet. #pretend-headphones
The spreadsheet stares back. One cell reads "THE BOX KNOWS." #pretend-cell
You did not type that. #pretend-didnt
sandrea: You can't ignore it. Kevin tried. He's been transferred to the Box Department. #sandrea-kevin
sandrea: We don't have a Box Department. Or we didn't. Until today. #sandrea-department
-> BreakRoom
beat BreakRoom
You enter the break room. There it is. A plain cardboard box, sitting on the counter next to the microwave. #breakroom-enter
It's perfectly ordinary, except for the fact that it's humming. And glowing faintly. And someone put googly eyes on it. #breakroom-humming
sandrea: The googly eyes were Dave from accounting. He thought it would make it less menacing. #sandrea-dave
sandrea: It did not make it less menacing. #sandrea-menacing
samuel: Hey, IT here. I've been asked to "scan" the box. With what, I have no idea. #samuel-scan
samuel: My job description says "computer stuff." This is not computer stuff. #samuel-job
choice
Help Samuel scan the box #opt-help-scan
-> ScanBox
Try to open the box #opt-try-open
-> TryOpen
Ask the box what it wants #opt-ask-box
boxPower += 10
-> AskBox
beat ScanBox
Samuel holds up his phone's flashlight to the box. #scan-phone
samuel: Scan complete. Results: it's a box. #samuel-results
samuel: You know, I have a computer science degree. Two, actually. And here I am, scanning a box with a phone flashlight. #samuel-degree
sandrea: Did you try turning the box off and on again? #sandrea-turnitoff
samuel: That's not-- you know what, let me try. #samuel-try
He picks up the box, turns it upside down, and puts it back. The humming gets louder. #scan-upside
samuel: I've made it angry. Great. Another thing that's angry at IT. #samuel-angry
boxPower += 5
-> BoxEscalation
beat TryOpen
You reach for the box's flaps. #open-reach
sandrea: Wait! What if it's someone's lunch? #sandrea-lunch
sandrea: Remember the Great Tupperware Incident of 2019? Karen still hasn't forgiven us. #sandrea-karen
You hesitate, then open it anyway. Inside the box, there's... another box. #open-another
Inside that box: a Post-it note that reads "Nice try. - The Box" #open-postit
boxPower += 10
-> BoxEscalation
beat AskBox
You lean toward the box and whisper: "What do you want?" #ask-whisper
The box hums at a slightly different frequency. Your phone autocorrects a text to "BOX IS LIFE." #ask-phone
sandrea: Did the box just hack your phone? #sandrea-hack
samuel: Boxes don't hack phones! #samuel-hack
samuel: ... Do they? I didn't study box-hacking in college. I'm starting to think my education had gaps. #samuel-gaps
The break room lights flicker. The microwave turns on by itself and heats nothing for exactly thirty seconds. #ask-flicker
-> BoxEscalation
beat BoxEscalation
An email arrives. From the box. It has its own email address: box@boxington-associates.com. #escalation-email
emails += 1
The email reads: "Dear colleagues, I would like to schedule a meeting to discuss my role in the organization. Please bring snacks. Preferably cardboard-based." #escalation-meeting
sandrea: The box wants a meeting. #sandrea-meeting
samuel: The box has better email etiquette than half the sales team. #samuel-etiquette
choice
Schedule the meeting #opt-schedule
-> BoxMeeting
Forward the email to HR #opt-forward-hr
hrInvolved = true
emails += 5
-> HRResponse
Reply all with "Please remove me from this thread" #opt-reply-all
emails += 200
-> ReplyAllChaos
beat ReplyAllChaos
You hit Reply All. The office erupts. #reply-chaos
47 people reply all with "Please stop replying all." Each reply triggers more replies. #reply-trigger
emails += 200
samuel: The email server is at 98% capacity. All from box-related reply-alls. #samuel-server
sandrea: Dave from accounting replied with a GIF of a box. It's 47 megabytes. #sandrea-gif
sandrea: The email server has crashed. The box's email still works though. Somehow. #sandrea-crashed
The box sends one final email: "I rest my case. You need me." #reply-rest
boxPower += 20
-> BoxUnion
beat HRResponse
HR responds within minutes. A new record. Their email is seventeen paragraphs long. #hr-response
Key excerpts: "Per company policy section 7.4.2, unidentified boxes must complete onboarding." #hr-onboarding
"Please ensure the box has filled out Form B-0X (yes, really) and attended the safety briefing." #hr-form
sandrea: HR wants to onboard the box. They want to give it an employee badge. #sandrea-badge
samuel: I've been here three years and still don't have an employee badge. #samuel-badge
samuel: But sure. Give it to the box. The sentient, humming, email-sending box. #samuel-sentient
-> BoxMeeting
beat BoxMeeting
The meeting room fills up. Everyone is there. The box sits in a chair. Someone gave it a name tag: "THE BOX - Role: TBD." #meeting-room
boxPower += 10
sandrea: So... the box has prepared a presentation. #sandrea-presentation
The projector turns on. Slide 1: "Why This Office Needs Me: A 47-Slide Deck by THE BOX." #meeting-slide
samuel: It has better slides than my last quarterly review. #samuel-slides
Slide 7 shows a pie chart. 100% of the pie is labeled "BOX." #meeting-pie
Slide 23 is just the word "OPEN" in 72-point font, pulsing. #meeting-open
Slide 47: "In conclusion: open me. Or don't. I'll still be here Monday." #meeting-conclusion
choice
Vote to make the box a full employee #opt-employee
unionFormed = true
-> BoxEmployee
Vote to put the box in storage #opt-storage
-> BoxStorage
Open the box during the meeting #opt-open-meeting
-> OpenDuringMeeting
beat BoxStorage
You vote to put the box in storage. The box's hum drops to a lower frequency. It sounds... hurt. #storage-vote
samuel: I think we hurt the box's feelings. #samuel-hurt
sandrea: Does the box have feelings? #sandrea-feelings
The lights flicker. Every computer in the office displays "I HAVE FEELINGS" in Comic Sans. #storage-feelings
boxPower += 30
sandrea: Apparently yes. And it has opinions about fonts. #sandrea-fonts
The box returns to the break room on its own. Nobody sees it move. It's just... there again. #storage-returns
-> BoxUnion
beat OpenDuringMeeting
You stand up and open the box mid-presentation. Gasp from the audience. #open-gasp
Inside the box: a smaller box. Inside that: a USB drive. #open-usb
samuel: A USB drive? Now we're in MY territory! #samuel-usb
He plugs it in. The screen displays: "Company Performance Could Be 340% Better If You Opened More Boxes - A Study by THE BOX." #open-study
samuel: The data is... actually compelling? The methodology is sound? #samuel-data
sandrea: We're being out-performed by a box. On a Monday. #sandrea-outperformed
boxPower += 15
-> BoxUnion
beat BoxEmployee
The box is officially hired. Employee #BOX-001. #employee-hired
It gets a desk, a computer it doesn't use, and a parking spot it definitely doesn't need. #employee-desk
sandrea: The box got a parking spot before I did. I've been on the waitlist for two years. #sandrea-parking
samuel: The box's employee portal says its skills are "containing things" and "being opened." #samuel-skills
samuel: That's more than my resume had when I started here. #samuel-resume
boxPower += 20
-> BoxUnion
beat BoxUnion
Weeks pass. The box has gained a following. Other boxes have appeared. #union-weeks
The supply closet, the printer paper boxes, the shipping containers -- they're all... organizing. #union-organizing
sandrea: The boxes are forming a union. #sandrea-union
samuel: Can boxes unionize? Is that legal? #samuel-legal
unionFormed = true
A memo slides under the door. It reads: "The United Box Workers of Boxington & Associates demand: 1) Better storage conditions. 2) No more being thrown away on Fridays. 3) One seat on the board of directors." #union-memo
emails += 30
choice
Support the box union #opt-support
-> SupportUnion
Oppose the box union #opt-oppose
-> OpposeUnion
Negotiate with the box union #opt-negotiate
-> NegotiateUnion
beat SupportUnion
You sign the box union's petition. It's the first petition you've ever signed that was also a box. #support-petition
sandrea: You know what? Good for the boxes. They work hard. They carry things. #sandrea-goodfor
samuel: The IT department has fourteen boxes. They do more work than the interns. #samuel-fourteen
The CEO, who no one has ever seen, sends an email: "I've always been a box. Surprise." #support-ceo
boxPower += 50
-> Ending
beat OpposeUnion
You take a stand against the box union. Bold move. #oppose-stand
The boxes respond by collectively refusing to be opened. Every package, every delivery, every Amazon order -- sealed shut. #oppose-sealed
sandrea: My online shopping! No! #sandrea-shopping
samuel: The server backup tapes are in boxes. We can't access anything. #samuel-tapes
The office grinds to a halt. You can't fight organized cardboard. #oppose-halt
boxPower += 30
You quietly un-oppose the box union. #oppose-retract
-> Ending
beat NegotiateUnion
You sit across from the original box. It's wearing a tiny tie now. Where did it get a tie? #negotiate-tie
sandrea: The box wants dental. #sandrea-dental
samuel: Boxes don't have teeth. #samuel-teeth
sandrea: The box says, and I quote, "not yet." #sandrea-notyet
After three hours of negotiation, you reach a compromise: boxes get better shelf space, and employees stop using them as makeshift chairs. #negotiate-compromise
boxPower += 20
-> Ending
beat Ending
You lean back in your office chair. It's Friday. The box is still humming in the break room. #ending-friday
sandrea: So... same thing next Monday? #sandrea-monday
samuel: Probably. I'm updating my resume, though. Under "Skills," I'm adding "box diplomacy." #samuel-resume-end
sandrea: You know, I used to think this job was boring. #sandrea-boring
sandrea: Then a box showed up, sent better emails than my manager, formed a union, and possibly became CEO. #sandrea-recap
sandrea: Mondays, am I right? #sandrea-mondays
if unionFormed
The box hums contentedly. The office will never be the same. But maybe that's okay. #ending-content
The break room microwave displays "THANK YOU" in its timer. Nobody questions it anymore. #ending-microwave
samuel: Box power level at end of week: $boxPower. Emails generated: $emails. Sanity remaining: undefined. #samuel-final
sandrea: See you Monday. Bring the box a coffee. It likes decaf. Don't ask how we know. #sandrea-final
-> .

View file

@ -0,0 +1,242 @@
// Cosmic Adventure - Open The Box
// Theme: Reality is recursive boxes. Existential crisis guaranteed.
character quantum
name: Dr. Quantum
role: Physicist
character entity
name: The Box Entity
role: Cosmic Being
state
comprehension: 0
realitiesVisited: 0
existentialCrisis: 0
acceptedTruth: false
beat Intro
The Large Hadron Collider has detected something impossible. #intro-detection
Deep beneath the Swiss-French border, particles are arranging themselves into right angles. Perfect corners. Ninety-degree edges. #intro-particles
quantum: This can't be right. The particles are forming a... no. That's ridiculous. #quantum-right
quantum: They're forming a box. A subatomic box. Made of quarks arranged in a cube. #quantum-box
Your instruments are screaming. Not metaphorically. The spectrometer is literally making a screaming sound. #intro-screaming
choice
Investigate the subatomic box #opt-investigate
comprehension += 10
-> SubatomicBox
Run diagnostics on the instruments #opt-diagnostics
-> Diagnostics
Panic appropriately #opt-panic
existentialCrisis += 10
-> PanicFirst
beat Diagnostics
You run every diagnostic in the manual. Then diagnostics that aren't in the manual. Then diagnostics you made up. #diag-run
quantum: Instruments are functioning perfectly. Which means the universe is NOT functioning perfectly. #quantum-instruments
quantum: Or it's functioning exactly as intended, and we just didn't know the intention involved boxes. #quantum-intended
The readouts confirm it: reality is boxing itself. At the subatomic level, everything is becoming cubic. #diag-confirm
comprehension += 5
-> SubatomicBox
beat PanicFirst
You panic. It's brief but cathartic. You knock over a coffee cup. #panic-cup
quantum: Feel better? #quantum-feel
quantum: Good. Because the subatomic box just doubled in size. And it's humming Bach's Cello Suite No. 1. #quantum-bach
quantum: In B-flat. Which shouldn't be possible for a collection of quarks. #quantum-bflat
existentialCrisis += 5
-> SubatomicBox
beat SubatomicBox
You zoom in on the quantum box. It's beautiful. Impossibly symmetrical. #subatomic-zoom
quantum: The box is emitting a signal. It's not electromagnetic. It's not gravitational. #quantum-signal
quantum: It's... conceptual? The box is broadcasting an IDEA directly into the measurement apparatus. #quantum-conceptual
The idea is simple: "Look closer." #subatomic-idea
comprehension += 10
choice
Look closer #opt-closer
realitiesVisited += 1
-> LookCloser
Look farther -- zoom out instead #opt-farther
realitiesVisited += 1
-> LookFarther
Refuse to look -- some ideas are traps #opt-refuse
existentialCrisis += 10
-> RefuseToLook
beat RefuseToLook
You refuse. You're a scientist, not a box's puppet. #refuse-puppet
quantum: I'm choosing not to observe the box. Quantum mechanically, that means it doesn't -- #quantum-choose
The box observes YOU instead. #refuse-observe
You feel it. A sensation of being measured. Calculated. Categorized. #refuse-measured
quantum: Okay, THAT is unsettling. I've been quantified by a box. My eigenvalue is... "adequate." #quantum-eigenvalue
quantum: ADEQUATE?! I have THREE PhDs! #quantum-phds
existentialCrisis += 15
-> LookCloser
beat LookCloser
You increase magnification by a factor of ten billion. Then another ten billion. #closer-magnify
Inside the subatomic box, there are smaller boxes. Inside those, smaller still. #closer-smaller
quantum: It's fractal. The box structure repeats at every scale. #quantum-fractal
quantum: Quarks are made of boxes. Those boxes are made of smaller boxes. All the way down. #quantum-alltheway
quantum: My entire career has been studying boxes without knowing they were boxes. #quantum-career
existentialCrisis += 10
comprehension += 20
-> TheEntityAppears
beat LookFarther
Instead of zooming in, you zoom out. Way out. Past the collider. Past Switzerland. Past Earth. #farther-zoom
quantum: If the subatomic level is boxes... what about the macroscopic level? #quantum-macro
You zoom past the solar system. Past the galaxy. Past the observable universe. #farther-past
And there it is. The edge. The universe has an edge, and the edge is... #farther-edge
quantum: It's a corner. The universe has a CORNER. #quantum-corner
quantum: The universe is a box. We live inside a box. Everything I know is inside a box. #quantum-universe
existentialCrisis += 20
comprehension += 20
realitiesVisited += 1
-> TheEntityAppears
beat TheEntityAppears
The instruments go silent. The humming stops. The lights flicker. #entity-silence
And then: a presence. Not visible. Not audible. But THERE, in a way that makes "there" feel inadequate. #entity-presence
entity: You found the corner. Most species take longer. #entity-found
quantum: Who-- WHAT are you? #quantum-what
entity: I am the Box Entity. I exist between the folds. In the spaces where the cardboard overlaps. #entity-between
entity: I have always been here. You just didn't have the resolution to see me. #entity-always
comprehension += 15
choice
Ask the Box Entity to explain reality #opt-explain
-> ExplainReality
Ask the Box Entity what's OUTSIDE the universe-box #opt-outside
-> WhatsOutside
Challenge the Box Entity's existence #opt-challenge
existentialCrisis += 10
-> ChallengeEntity
beat ChallengeEntity
quantum: You're not real. You can't be. Physics doesn't allow for cosmic box beings. #quantum-notreal
entity: Physics is the instruction manual printed inside the box. Of course it doesn't mention me. #entity-manual
entity: Do instruction manuals mention the person who wrote them? Exactly. #entity-writer
quantum: That's... actually a decent argument. I hate that it's a decent argument. #quantum-argument
existentialCrisis += 10
-> ExplainReality
beat ExplainReality
entity: Reality is simple. Everything is a box. #entity-simple
entity: Atoms are boxes containing energy. Cells are boxes containing atoms. Bodies are boxes containing cells. #entity-atoms
entity: Planets are boxes containing bodies. Solar systems are boxes containing planets. #entity-planets
entity: Galaxies are boxes containing solar systems. Universes are boxes containing galaxies. #entity-galaxies
quantum: And what contains the universe-box? #quantum-contains
entity: Another box. Obviously. #entity-another
comprehension += 20
choice
Ask what's in the box that contains the universe #opt-meta-box
realitiesVisited += 1
-> MetaBox
Ask if the recursion ever ends #opt-recursion
-> RecursionQuestion
Have the existential crisis you've been putting off #opt-crisis
existentialCrisis += 30
-> ExistentialCrisis
beat WhatsOutside
quantum: What's outside the universe-box? Is there a... meta-universe? #quantum-outside
entity: Outside the box is another box. And outside that box, another. #entity-outsidebox
entity: You're asking "what's outside all the boxes?" The answer is: the concept of a box. #entity-concept
entity: Before there was anything, there was the IDEA of containment. The ur-box. The first fold. #entity-urbox
quantum: You're saying the fundamental force of the universe isn't gravity or electromagnetism. It's... boxing? #quantum-boxing
entity: Containment. Organization. The desire to put things inside other things. Yes. Boxing. #entity-containment
comprehension += 25
-> RecursionQuestion
beat MetaBox
entity: Would you like to see? #entity-wouldsee
The walls of reality peel back. You see your universe from outside. It's a glowing box, floating in darkness. #meta-peel
Next to it: another glowing box. And another. And another. Infinite boxes, each a universe. #meta-infinite
quantum: There are infinite universes. Each one a box. #quantum-infinite
entity: And they're all inside a bigger box. Which is inside a bigger box. Which is-- #entity-bigger
quantum: I get it! Boxes! All the way up, all the way down! Can we STOP with the boxes?! #quantum-stop
entity: No. #entity-no
realitiesVisited += 1
comprehension += 20
existentialCrisis += 15
-> RecursionQuestion
beat RecursionQuestion
quantum: Does it ever end? Is there a final box? An ultimate container? #quantum-end
entity: That is the question every physicist, philosopher, and cardboard enthusiast has asked. #entity-question
entity: The answer is: there is one box that contains all other boxes, including itself. #entity-one
quantum: A box that contains itself? That's a paradox! #quantum-paradox
entity: Only if you think about it. So don't. #entity-dont
comprehension += 15
choice
Accept the recursive nature of reality #opt-accept
acceptedTruth = true
-> Acceptance
Reject it and try to find the edge #opt-reject
existentialCrisis += 20
-> RejectReality
Ask if you can open the final box #opt-final
-> FinalBox
beat ExistentialCrisis
quantum: I studied physics for twenty years. TWENTY YEARS. #quantum-twenty
quantum: I memorized the Standard Model. I solved differential equations for FUN. #quantum-standard
quantum: And it turns out the answer to everything is: it's a box. Just a box. #quantum-answer
entity: A very NICE box. #entity-nice
quantum: Oh, well, as long as it's a NICE box, that changes everything! #quantum-sarcasm
entity: Sarcasm. Interesting. That's a coping mechanism I haven't seen in a while. #entity-sarcasm
entity: The last species that discovered the truth just went very quiet for a billion years. #entity-quiet
comprehension += 10
-> FinalBox
beat RejectReality
quantum: No! There must be something that isn't a box! #quantum-reject
You search desperately. You examine circles. They're just boxes with very high polygon counts. #reject-circles
You examine spheres. Boxes with excellent PR. #reject-spheres
You examine abstract concepts like love and justice. Boxes for emotions and morals, respectively. #reject-abstract
quantum: Is my MIND a box?! #quantum-mind
entity: Specifically, a box of thoughts. Some assembled, some still flat-packed. #entity-mind
existentialCrisis += 20
-> FinalBox
beat Acceptance
quantum: You know what? Fine. Reality is boxes. I accept it. #quantum-fine
quantum: And honestly? It's elegant. Everything fits inside something else. Nothing is wasted. #quantum-elegant
quantum: It's the most organized universe possible. Marie Kondo would weep. #quantum-kondo
entity: She did. When she found out. Tears of joy. #entity-kondo
comprehension += 20
-> FinalBox
beat FinalBox
entity: You've come far enough. Would you like to see what's inside the very first box? #entity-first
entity: The box that started everything. The box that was before "before" was a concept. #entity-before
quantum: Yes. After everything I've seen, I need to know. #quantum-need
if acceptedTruth
The Box Entity opens the first box. Inside: a tiny spark. Warm. Bright. Alive. #final-spark
entity: That's the desire to know what's inside. The curiosity that makes boxes worth opening. #entity-curiosity
entity: Every box exists because something wanted to be found. Every opener exists because something wanted to find. #entity-every
quantum: That's... actually beautiful. In a box-shaped way. #quantum-beautiful
else
The Box Entity opens the first box. Inside: nothing. And everything. Simultaneously. #final-nothing
entity: The first box contained the possibility of all other boxes. Including you. #entity-possibility
quantum: I was in a box before I knew what boxes were. We all were. #quantum-inbox
entity: Now you understand. #entity-understand
-> Ending
beat Ending
The presence fades. The instruments come back online. The coffee cup is somehow unbroken. #ending-fades
quantum: Realities visited: $realitiesVisited. Comprehension level: $comprehension. Existential crises: $existentialCrisis. #quantum-stats
quantum: I need to rewrite every physics textbook ever published. #quantum-rewrite
quantum: Chapter 1: "Everything Is A Box." Chapter 2: "No, Really." Chapter 3: "We're Sorry." #quantum-chapters
quantum: On second thought, maybe some boxes are better left unopened. #quantum-second
quantum: ...who am I kidding. I'm a scientist. I'll open every box I find. #quantum-kidding
quantum: Even if the box is the universe. Especially if the box is the universe. #quantum-especially
-> .

View file

@ -0,0 +1,257 @@
// Dark Fantasy Adventure - Open The Box
// Theme: Boxes contain souls. Opening them has consequences.
character mordecai
name: Mordecai the Grim
role: Necromancer
character pierrick
name: Pierrick
role: Reluctant Hero
state
souls: 0
corruption: 0
boxesFreed: 0
alliedWithMordecai: false
resistedDarkness: false
beat Intro
The Ashen Wastes stretch before you, gray and lifeless. The sky hasn't seen a color since the Folding. #intro-wastes
They call it the Folding because that's when the world was creased, bent, and tucked into itself like cardboard. #intro-folding
pierrick: I didn't ask for this quest. I was a baker. I made bread. Bread doesn't involve cursed boxes. #pierrick-baker
pierrick: And yet here I am, in a dead land, looking for a necromancer who collects souls in boxes. #pierrick-quest
A raven lands on a nearby rock. It's carrying a small, black box in its talons. #intro-raven
The box whispers. You can't make out the words, but you feel them: cold, sad, and slightly annoyed at being carried by a bird. #intro-whispers
choice
Take the box from the raven #opt-take
souls += 1
-> TakeBox
Follow the raven #opt-follow
-> FollowRaven
Ignore the raven and keep walking #opt-ignore
corruption += 5
-> IgnoreRaven
beat TakeBox
You reach for the box. The raven drops it and flies away, cawing something that sounds like "your problem now." #take-caw
The box is cold. Not cold like ice. Cold like loneliness. Cold like a Tuesday in February. #take-cold
pierrick: There's a soul in here. I can feel it. Someone's entire existence, folded up and boxed. #pierrick-soul
pierrick: Who puts a soul in a box? What kind of person thinks "you know what this soul needs? Cardboard." #pierrick-cardboard
choice
Open the box and free the soul #opt-free-soul
boxesFreed += 1
-> FreeSoul
Keep the box closed -- it might be dangerous #opt-keep-closed
corruption += 10
-> KeepClosed
beat FreeSoul
You open the box. A wisp of pale light rises, hovering before you. #free-wisp
The soul speaks in a voice like wind through empty rooms: "Thank you. I was in there for seven years. No WiFi." #free-speaks
pierrick: Did a liberated soul just complain about WiFi? #pierrick-wifi
The soul drifts upward and vanishes. The box crumbles to dust. Something in the air feels lighter. #free-drifts
souls += 1
boxesFreed += 1
-> ThePath
beat KeepClosed
You tuck the box into your pack. The whispers grow quieter, resigned. #keep-tuck
pierrick: Sorry, box soul. I don't know what happens when I open cursed boxes in cursed lands. #pierrick-sorry
pierrick: And I'd like to keep my current number of curses at its current number, which is already too many. #pierrick-curses
corruption += 5
-> ThePath
beat FollowRaven
The raven flies north, toward the Spine of Boxes -- a mountain range that looks like stacked containers. #follow-north
pierrick: A land so cursed even the geography is box-shaped. Wonderful. Just wonderful. #pierrick-geography
You follow the bird for an hour. It leads you to a clearing filled with hundreds of small black boxes, arranged in neat rows. #follow-clearing
Each one whispers. The combined sound is like a library where every book is complaining. #follow-library
pierrick: A box graveyard. Or a box prison. I'm not sure which is worse. #pierrick-graveyard
souls += 5
-> ThePath
beat IgnoreRaven
You walk past the raven. It stares at you with judgmental bird eyes. #ignore-stare
pierrick: Don't look at me like that. I'm on a quest. I can't stop for every spooky bird with a soul box. #pierrick-spooky
The raven caws once, drops the box, and it shatters on the ground. A soul escapes upward. #ignore-shatters
pierrick: Okay, the bird freed the soul itself. I've been outperformed by a raven. Great start to the quest. #pierrick-outperformed
boxesFreed += 1
-> ThePath
beat ThePath
The path narrows as you approach the Citadel of Folds, Mordecai's fortress. #path-narrows
It's made entirely of dark, interlocking boxes. Some are stone, some are iron, some are... something else. #path-citadel
pierrick: The architecture here is aggressively cubic. Even the doors are just bigger boxes with hinges. #pierrick-architecture
A figure appears at the gate. Tall, gaunt, draped in robes that look like unfolded cardboard. #path-figure
mordecai: Ah. The reluctant hero. You're late. I expected you three chapters ago. #mordecai-late
choice
Demand Mordecai release the souls #opt-demand
-> DemandRelease
Ask Mordecai why he collects souls in boxes #opt-ask-why
-> MordecaiMotivation
Attack immediately #opt-attack
corruption += 10
-> AttackMordecai
beat DemandRelease
pierrick: Release the souls, Mordecai! They don't belong in boxes! #pierrick-release
mordecai: Don't they? Where do YOU think souls go when the body dies? They drift. They scatter. They're LOST. #mordecai-drift
mordecai: I give them a home. A container. A purpose. A BOX. #mordecai-home
mordecai: Is that evil? Or is it... organized? #mordecai-evil
pierrick: It's both. It's organizationally evil. #pierrick-both
-> MordecaiMotivation
beat AttackMordecai
You charge at Mordecai. He raises a hand. A wall of boxes materializes between you. #attack-charge
mordecai: Violence? How unoriginal. Every hero attacks first, asks questions never. #mordecai-violence
The box wall pushes you back. Gently. Like a firm but polite bouncer. #attack-pushed
mordecai: I could trap your soul in a box right now. But where's the narrative satisfaction in that? #mordecai-narrative
mordecai: Sit. Talk. Then you can decide whether to keep fighting. #mordecai-sit
corruption += 5
-> MordecaiMotivation
beat MordecaiMotivation
mordecai: Let me show you something. #mordecai-show
He leads you into the Citadel. The walls are lined with boxes, floor to ceiling. Thousands of them. #motivation-walls
mordecai: Each box contains a soul. Each soul is preserved, protected, REMEMBERED. #mordecai-preserved
mordecai: Before me, souls faded. They dissipated into nothing. No memory. No record. Just gone. #mordecai-faded
mordecai: I built boxes for them. I gave the dead a place to BE. #mordecai-place
pierrick: That's... almost noble. If you ignore the "trapping souls against their will" part. #pierrick-noble
mordecai: "Against their will." Do you ask a book if it wants to be on a shelf? #mordecai-book
choice
Side with Mordecai -- the souls are preserved #opt-side
alliedWithMordecai = true
corruption += 20
-> AllyWithMordecai
Oppose Mordecai -- the souls deserve freedom #opt-oppose
resistedDarkness = true
-> OpposeMordecai
Propose a compromise -- let the souls choose #opt-compromise
-> CompromisePath
beat AllyWithMordecai
pierrick: Maybe you're right. Maybe the boxes are better than nothing. #pierrick-maybe
mordecai: Finally! Someone who understands! #mordecai-finally
mordecai: Here, take this box. It's empty. Fill it with a soul whenever you find one wandering. #mordecai-takebox
mordecai: Think of it as... community service. For the dead. #mordecai-community
corruption += 15
souls += 3
The box is warm in your hands. But not a good warm. The warm of a fever. The warm of something wrong. #ally-warm
pierrick: I'm going to regret this, aren't I? #pierrick-regret
mordecai: Almost certainly. But that's what makes it interesting. #mordecai-certainly
-> TheVault
beat OpposeMordecai
pierrick: No. The souls should be free. Even if freedom means fading. That's their choice, not yours. #pierrick-free
mordecai: Choice? Souls don't CHOOSE. They simply ARE. Or aren't. #mordecai-choice
pierrick: Then I'll choose FOR them. And I choose open. I choose unboxed. #pierrick-unboxed
mordecai: How heroic. How predictable. How IRRITATING. #mordecai-irritating
resistedDarkness = true
Mordecai's eyes darken. The boxes on the walls begin to rattle. The souls inside feel the tension. #oppose-rattle
-> TheVault
beat CompromisePath
pierrick: What if the souls decided? Open the boxes. Let them choose to stay or go. #pierrick-decide
mordecai: Let them CHOOSE? That's-- #mordecai-choose
mordecai: Actually... hm. I never considered asking. #mordecai-asking
mordecai: In my defense, they're in boxes. It's hard to conduct surveys through cardboard. #mordecai-surveys
pierrick: Try. #pierrick-try
mordecai: Fine. But if they all leave, I'm blaming you. And I'm keeping the boxes. They're good boxes. #mordecai-blame
-> TheVault
beat TheVault
Mordecai leads you deeper, to the Soul Vault. The largest collection of boxed souls in the realm. #vault-leads
The room is vast. Boxes glow softly in the darkness -- blue, white, pale gold. Each one a life. #vault-room
pierrick: There must be thousands. #pierrick-thousands
mordecai: Twelve thousand, four hundred and seven. I'm very organized. #mordecai-organized
mordecai: They're sorted alphabetically, by era, and by cause of death. The "eaten by dragon" section is quite large. #mordecai-sorted
if alliedWithMordecai
mordecai: With your help, we'll fill the empty shelves. The dead deserve boxes. ALL of them. #mordecai-fill
A small voice from one of the boxes says: "Actually, I'd rather not." #vault-rather
mordecai: Shh. The living are talking. #mordecai-shh
if resistedDarkness
pierrick: I'm opening them. All of them. #pierrick-opening
mordecai: You'll have to get through me first. And I warn you -- I am VERY attached to my box collection. #mordecai-attached
choice
Open all the boxes at once #opt-open-all
boxesFreed += 100
-> OpenAllBoxes
Open one box and see what happens #opt-open-one
boxesFreed += 1
-> OpenOneBox
Convince Mordecai to open them together #opt-convince if corruption < 30
-> ConvinceMordecai
beat OpenOneBox
You pick a box at random. It's small, blue, and warm. The label reads: "Elara. Farmer. Liked cats." #one-pick
You open it. A gentle light rises. A woman's voice, soft and clear: #one-light
The soul speaks: "Oh. Oh my. How long has it been?" #soul-how
pierrick: A while. Are you okay? #pierrick-okay
The soul speaks: "I was dreaming. Of fields, and cats, and sunshine. The box kept me safe. But it also kept me still." #soul-dream
The soul speaks: "I think... I'd like to move again. If that's alright." #soul-move
mordecai: You see? She was SAFE in there. #mordecai-safe
pierrick: Safe isn't the same as free, Mordecai. #pierrick-safefree
souls += 1
-> FinalChoice
beat OpenAllBoxes
You grab boxes and open them. One after another. Light fills the vault. #all-grab
Souls rise like lanterns, hundreds of them, filling the air with whispered thank-yous and confused questions about what year it is. #all-rise
mordecai: No! My collection! My life's work! MY BOXES! #mordecai-collection
pierrick: They were never yours, Mordecai. #pierrick-never
The souls spiral upward, through the ceiling, through the Citadel, into the sky. #all-spiral
For the first time in years, the Ashen Wastes see color. Pale golds and blues streak across the gray. #all-color
mordecai: It's... actually quite pretty. I hate that it's pretty. #mordecai-pretty
boxesFreed += 100
-> FinalChoice
beat ConvinceMordecai
pierrick: Mordecai. Open them with me. Not as an enemy. As someone who cared enough to build the boxes. #pierrick-with
mordecai: I... built them because I was afraid. Of losing people. Of forgetting. #mordecai-afraid
mordecai: Every soul I boxed was a person I didn't want the world to forget. #mordecai-forget
pierrick: Then let them go. Not because you don't care. Because you do. #pierrick-letgo
mordecai: That's the most annoyingly wise thing anyone has said to me. And I've spoken to twelve thousand souls. #mordecai-annoying
Mordecai reaches for a box. His hands shake. He opens it. A light rises, and he smiles. Just barely. #convince-opens
mordecai: Fine. We open them. Together. And then I'm retiring. Maybe I'll take up pottery. Boxes are exhausting. #mordecai-retiring
boxesFreed += 50
alliedWithMordecai = true
-> FinalChoice
beat FinalChoice
The vault is quieter now. Some boxes are open. Some are still closed. The decision hangs in the air. #final-quiet
if alliedWithMordecai
mordecai: I suppose there's something to be said for empty boxes. They have... potential. #mordecai-potential
mordecai: An empty box can become anything. A full box is just... a box. #mordecai-empty
if resistedDarkness
pierrick: The darkness said "stay in the box." I said no. That feels important. #pierrick-darkness
-> Ending
beat Ending
The wind shifts. The Ashen Wastes feel different. Not healed. But healing. #ending-wind
pierrick: I came here to fight a necromancer. Instead I had a philosophical debate about boxes and souls. #pierrick-came
pierrick: This is the weirdest career change from "baker" I could have imagined. #pierrick-career
mordecai: For what it's worth, you're not a bad hero. A bit preachy. But not bad. #mordecai-worth
pierrick: And you're not a bad necromancer. A bit dramatic. But not bad. #pierrick-notbad
mordecai: I CULTIVATE the drama. It's part of the aesthetic. #mordecai-drama
if boxesFreed > 50
The sky clears further. The first sunset in years paints the world in warm colors. #ending-sunset
Souls drift across the sky like lanterns, finding their way home, or wherever souls go when the box is finally opened. #ending-souls
pierrick: Souls freed: $boxesFreed. Corruption level: $corruption. Baker skills still intact: surprisingly yes. #pierrick-stats
mordecai: Empty boxes remaining: too many. Regrets: some. Plans for pottery class: confirmed. #mordecai-stats
pierrick: Some boxes hold treasure. Some hold darkness. Some hold souls. #pierrick-some
pierrick: But every box, eventually, deserves to be opened. Even the scary ones. #pierrick-every
mordecai: Especially the scary ones. That's where the good stuff is. #mordecai-especially
-> .

View file

@ -0,0 +1,240 @@
// Medieval Adventure - Open The Box
// Theme: A kingdom where everything is suspiciously box-shaped
character knight
name: Sir Boxalot
role: Knight of the Square Table
character wizard
name: Malkith
role: Dark Wizard of the Cubic Order
state
honor: 50
boxesOpened: 0
savedPrincess: false
dragonFriendly: false
beat Intro
You stand before Castle Cardboard, its square towers rising into the overcast sky. #intro-castle
Everything here is box-shaped. The drawbridge is a flattened box. The moat is a long, wet box. #intro-everything
knight: Halt! Who goes there? I am Sir Boxalot, Knight of the Square Table! #knight-halt
knight: State your business, or I shall smite thee with my box-shaped sword! #knight-smite
You notice his sword is, indeed, a rectangular prism. Not very sharp. #intro-sword
choice
Declare yourself a hero #opt-hero
honor += 10
-> HeroIntro
Declare yourself a box inspector #opt-inspector
-> InspectorIntro
Ask why everything is box-shaped #opt-why
-> WhyBoxes
beat WhyBoxes
knight: Why is everything box-shaped? Why is the SKY blue? Why do birds sing? #knight-why
knight: Some questions have no answers. Others have box-shaped answers. #knight-answers
knight: The real question is: can you open the Box of Destiny? #knight-destiny
You feel like this is the kind of game that asks you rhetorical questions and then gives you exactly one path forward. #why-meta
-> HeroIntro
beat HeroIntro
knight: A hero! Wonderful! We haven't had one of those since Sir Unboxington fell into the Box Pit. #knight-hero
knight: The kingdom is in peril! The dark wizard Malkith has captured Princess Rectangula! #knight-peril
knight: She's being held in the Tall Tower, which is basically just a really tall box. #knight-tower
choice
Vow to rescue the princess #opt-rescue
honor += 10
-> QuestBegins
Ask what the princess looks like #opt-princess-look
-> PrincessDescription
Suggest they just tip the tower over #opt-tip
-> TipTower
beat InspectorIntro
knight: A box inspector?! By the Square Gods, we've been expecting you! #knight-inspector
knight: The kingdom's boxes have been malfunctioning. Some open from the wrong side. #knight-malfunction
knight: One box started opening OTHER boxes. It was chaos. Beautiful, beautiful chaos. #knight-chaos
honor += 5
-> QuestBegins
beat PrincessDescription
knight: The princess? She's... well... she's a box. #knight-princess
knight: A very PRETTY box. With a tiara taped to the top. #knight-pretty
knight: Don't judge. Love is love, and boxes are boxes. #knight-love
You decide not to question the romantic standards of this kingdom. #princess-standards
-> QuestBegins
beat TipTower
knight: Tip the-- that's the most brilliant and idiotic thing I've ever heard! #knight-tip
knight: We can't tip the tower. It's load-bearing. The entire castle is just boxes stacked on boxes. #knight-loadbearing
knight: Tip one and the whole kingdom collapses like a house of... boxes. #knight-collapse
-> QuestBegins
beat QuestBegins
You set off toward the Tall Tower with Sir Boxalot. #quest-setoff
The path leads through the Forest of Flat-Pack, where the trees are unassembled boxes with confusing instructions. #quest-forest
knight: Stay close. These woods are full of bandits and IKEA references. #knight-ikea
A rustling sound comes from behind a bush-shaped box. #quest-rustling
choice
Draw your weapon #opt-draw
-> Ambush
Hide inside a nearby box #opt-hide
-> HideInBox
Yell "I know you're there!" #opt-yell
-> YellAtBush
beat HideInBox
You climb inside a conveniently person-sized box and close the flaps above you. #hide-climb
knight: Brilliant strategy! If you can't see the enemy, the enemy can't see you! #knight-strategy
knight: That's not how that works but I admire the commitment. #knight-commitment
From inside the box, you hear the bandit trip over a root and knock himself out. #hide-bandit
boxesOpened += 1
You emerge victorious, technically. #hide-victory
-> DragonApproach
beat Ambush
A bandit leaps out! He's wearing a box as armor. It's not very effective. #ambush-leap
knight: Have at thee, villain! #knight-haveatthee
Sir Boxalot swings his box-sword. It makes a cardboard "thwap" sound. #ambush-thwap
The bandit looks confused, then disappointed, then falls over out of politeness. #ambush-falls
boxesOpened += 1
-> DragonApproach
beat YellAtBush
You yell into the forest. Your voice echoes off the box-trees. #yell-echo
A small rabbit hops out. It's wearing a tiny box as a shell. A box turtle, if you will. #yell-rabbit
knight: False alarm. Just a box bunny. They're everywhere this time of year. #knight-bunny
knight: Breeding season. They multiply like... well, like boxes in this game. #knight-breeding
-> DragonApproach
beat DragonApproach
You reach the Tall Tower. A dragon circles overhead. #dragon-tower
knight: That's Scorchtangle, the Box Dragon! He hoards boxes instead of gold! #knight-dragon
The dragon lands before you. He's... also somewhat box-shaped. You're sensing a pattern. #dragon-lands
knight: His weakness is that he can't resist opening boxes. It's like catnip for dragons. #knight-weakness
choice
Offer the dragon a box to open #opt-offer-box
dragonFriendly = true
-> DragonFriendly
Fight the dragon #opt-fight-dragon
-> DragonFight
Try to sneak past while the dragon is distracted #opt-sneak
-> SneakPast
beat DragonFight
You charge at Scorchtangle with your definitely-not-adequate weapon. #fight-charge
The dragon yawns and a small flame singes your eyebrows. #fight-singe
knight: Perhaps a more... diplomatic approach? #knight-diplomatic
knight: I once saw a hero try to fight him. He got boxed. Literally. Put in a box. #knight-boxed
The dragon looks at you expectantly, as if waiting for you to do something smarter. #fight-expectant
-> DragonFriendly
beat SneakPast
You tiptoe past the dragon. Your sneaking skills are impeccable. #sneak-tiptoe
Unfortunately, you step on a bubble wrap moat. The popping is deafening. #sneak-bubble
The dragon looks at you with what can only be described as secondhand embarrassment. #sneak-embarrassment
knight: Bubble wrap. The natural enemy of stealth. #knight-bubble
-> DragonFriendly
beat DragonFriendly
You pull out a beautifully wrapped box from your inventory. #friendly-box
You don't remember putting it there, but this is a game, so inventory logic doesn't apply. #friendly-inventory
The dragon's eyes widen. He opens the box with surprisingly delicate claws. #friendly-opens
Inside: a smaller box. The dragon is DELIGHTED. #friendly-smaller
boxesOpened += 1
He opens the smaller box. Inside: an even smaller box. He's in heaven. #friendly-heaven
The dragon steps aside, too busy with his recursive box gift to guard anything. #friendly-aside
choice
Enter the tower #opt-enter-tower
-> TowerClimb
Pet the dragon first #opt-pet-dragon
dragonFriendly = true
The dragon purrs. It sounds like cardboard being rubbed together. #pet-purr
-> TowerClimb
beat TowerClimb
You climb the tower stairs. Each step is a small box. It's like climbing a staircase of shoeboxes. #climb-stairs
At the top, you find Malkith standing before a large ornate box. #climb-malkith
wizard: Ah, the hero arrives! You're late. I've been monologuing to myself for twenty minutes. #wizard-late
wizard: Behold! The Box of Destiny! Within it lies Princess Rectangula! #wizard-behold
wizard: And with her, the power to fold reality itself into a BOX! #wizard-power
choice
Challenge Malkith to a duel #opt-duel
-> WizardDuel
Ask Malkith why he wants box-folding power #opt-ask-why
-> WizardMotivation
Just open the Box of Destiny while he's talking #opt-just-open if boxesOpened > 0
-> JustOpenIt
beat WizardMotivation
wizard: Why? WHY?! Have you seen this kingdom? Everything is already a box! #wizard-whykingdom
wizard: But they're DISORGANIZED boxes! Boxes within boxes with no structure! #wizard-disorganized
wizard: I want to SORT them. By size. By color. By emotional significance. #wizard-sort
wizard: I'm not evil. I'm a box organizer. The kingdom just doesn't appreciate good filing. #wizard-filing
knight: That... actually sounds reasonable? #knight-reasonable
wizard: THANK you. #wizard-thanks
choice
Help Malkith organize the boxes #opt-help-organize
-> HelpMalkith
Insist on freeing the princess first #opt-free-first
-> WizardDuel
beat HelpMalkith
You spend the next three hours sorting boxes with Malkith. #help-sorting
wizard: Small boxes go LEFT. Medium boxes go RIGHT. Large boxes go in the LARGE BOX. #wizard-instructions
knight: This is surprisingly therapeutic. #knight-therapeutic
wizard: Right? I keep telling people. Box organization is self-care. #wizard-selfcare
savedPrincess = true
boxesOpened += 1
Malkith releases Princess Rectangula as a thank you. She is, in fact, a box with a tiara. #help-princess
She's lovely. #help-lovely
-> Ending
beat WizardDuel
wizard: A duel? Fine! I choose... BOX MAGIC! #wizard-duel
Malkith shoots a beam of cubic energy at you. You dodge. Barely. #duel-dodge
knight: Hit him with the thing! The box thing! #knight-boxthing
You're not sure what "the box thing" is, but you grab the nearest box and throw it. #duel-throw
It hits Malkith square in the face. Square. Because it's a box. #duel-square
wizard: Ow! That was my favorite face! #wizard-ow
boxesOpened += 1
Malkith stumbles back into the Box of Destiny, accidentally opening it. #duel-stumble
Princess Rectangula tumbles out. She kicks Malkith with her box-corner. It looks painful. #duel-kick
savedPrincess = true
-> Ending
beat JustOpenIt
While Malkith is mid-monologue, you casually walk up and open the Box of Destiny. #just-walk
wizard: --and furthermore, the cubic nature of-- wait, what are you doing? #wizard-wait
wizard: You can't just OPEN it! I had a whole speech prepared! #wizard-speech
wizard: There were DRAMATIC PAUSES! A CALLBACK to act one! #wizard-pauses
Princess Rectangula tumbles out and bonks Malkith on the head. #just-bonk
savedPrincess = true
boxesOpened += 1
knight: That was incredibly anticlimactic. I loved it. #knight-anticlimactic
-> Ending
beat Ending
if savedPrincess
The kingdom celebrates! Princess Rectangula is safe! #ending-celebrate
knight: You've saved the kingdom! The Square Table shall sing of your deeds! #knight-saved
knight: Well, they'll sing in monotone. Everything here is a bit... flat. #knight-flat
else
The kingdom is mildly concerned but ultimately fine. Boxes don't hold grudges. Usually. #ending-fine
if dragonFriendly
Scorchtangle the dragon joins the celebration, bringing his collection of nested boxes as party favors. #ending-dragon
knight: Until next time, hero. There are always more boxes to open. #knight-nexttime
knight: And more fourth walls to break. Yes, I know this is a game. I've always known. #knight-fourthwall
knight: The real box was the friends we made along the way. #knight-realbox
knight: Actually, no. The real box is a box. That's the whole point. #knight-point
-> .

View file

@ -0,0 +1,231 @@
// Microscopic Adventure - Open The Box
// Theme: Cells are tiny boxes. Biology is just box logistics.
character cellina
name: Dr. Cellina
role: Biologist
character mike
name: Mitochondria Mike
role: Personified Organelle
state
sciencePoints: 0
cellsExplored: 0
mikeHappiness: 30
rebranded: false
beat Intro
The shrink ray hums. You're wearing a lab coat that's about to become very, very small. #intro-hum
cellina: Initiating cellular-scale reduction in 3... 2... 1... #cellina-count
The world goes white, then enormous, then incomprehensible. #intro-shrink
When your vision clears, you're floating in a warm, amber-colored fluid. #intro-floating
cellina: Welcome to the inside of a human cell! Also known as... a box. A tiny, squishy box. #cellina-welcome
cellina: I've been studying cells for fifteen years, and one day it hit me: they're just boxes. #cellina-studying
cellina: Membrane walls, contents inside, specific opening mechanisms. Cells are BOXES. #cellina-membrane
sciencePoints += 5
choice
Look around the cell #opt-look
cellsExplored += 1
-> LookAround
Ask Dr. Cellina to explain further #opt-explain
sciencePoints += 10
-> CellinaExplains
Swim toward the big glowing thing #opt-swim
-> SwimToNucleus
beat CellinaExplains
cellina: Think about it. The cell membrane is the box walls. Selective permeability is the lock. #cellina-walls
cellina: The nucleus is a box inside the box. Mitochondria are tiny power boxes. #cellina-nucleus
cellina: The endoplasmic reticulum is... okay, that one's more of a crumpled sheet, but MOSTLY boxes. #cellina-er
cellina: Even vesicles -- those little transport bubbles -- they're just round boxes. Don't let the shape fool you. #cellina-vesicles
sciencePoints += 10
-> LookAround
beat SwimToNucleus
You swim toward the large, glowing sphere in the center. The nucleus. #swim-nucleus
cellina: Careful! That's the nucleus -- the box that contains the instruction manual for building MORE boxes! #cellina-careful
cellina: Also known as DNA. But I prefer "Box Assembly Instructions." #cellina-dna
cellsExplored += 1
-> MeetMike
beat LookAround
The cell is vast at this scale. Organelles float past like furniture in a furnished box. #look-vast
cellina: Over there is the Golgi apparatus -- the cell's shipping department. It packages things into smaller boxes and mails them. #cellina-golgi
cellina: It's literally a box that puts things in boxes. Peak box efficiency. #cellina-peak
A small, bean-shaped organelle zooms past, muttering. #look-bean
cellina: And THAT would be a mitochondrion. They're usually not this chatty. #cellina-chatty
-> MeetMike
beat MeetMike
The mitochondrion stops in front of you. It has a face. It should not have a face. #mike-face
mike: Oh great. More visitors. Let me guess -- you're here to call me the "powerhouse of the cell." #mike-powerhouse
mike: Go ahead. Say it. Everyone does. It's literally the only thing anyone knows about me. #mike-say
cellina: Mike, these are researchers. Be nice. #cellina-nice
mike: "Be nice," she says. I generate ATP for BILLIONS of cellular processes and all I get is ONE Wikipedia sentence! #mike-atp
choice
Call him the powerhouse of the cell (he's asking for it) #opt-powerhouse
mikeHappiness -= 20
-> PowerhouseReaction
Suggest a new title for Mike #opt-newtitle
mikeHappiness += 20
-> NewTitle
Ask Mike about his job #opt-job
sciencePoints += 10
-> MikeJob
beat PowerhouseReaction
mike: You did it. You actually said it. I'm dying inside. Well, I'm always dying inside. I have a 10-day lifespan. #mike-dying
mike: Do you know how many organelles are in a cell? THOUSANDS. And I'm the only one with a catchphrase. #mike-catchphrase
mike: The Golgi apparatus doesn't have to deal with this. Nobody even remembers the Golgi apparatus. #mike-golgi
cellina: I remember the Golgi apparatus. #cellina-golgi-2
mike: YOU'RE A BIOLOGIST, CELLINA. YOU DON'T COUNT. #mike-dontcount
-> BoxTheory
beat NewTitle
mike: A new title? You'd do that for me? #mike-newtitle
mike: I've been workshopping some options. How about: "The Box Opener of the Cell"? #mike-boxopener
mike: Because that's what I DO! I open molecular boxes of glucose and release the energy inside! #mike-glucose
mike: I'm not a powerhouse. I'm a box opener. The ORIGINAL box opener. #mike-original
cellina: That's... actually scientifically accurate. ATP synthesis IS essentially unboxing chemical energy. #cellina-accurate
mikeHappiness += 30
rebranded = true
-> BoxTheory
beat MikeJob
mike: Fine, you want to know what I actually do? I'll tell you what I actually do. #mike-fine
mike: Glucose comes in -- that's a box of energy, by the way. A molecular box. #mike-glucosebox
mike: I crack it open through a series of chemical reactions. Krebs cycle, electron transport chain, the works. #mike-krebs
mike: And what comes out? ATP. Which is ANOTHER box. A box of usable energy. #mike-atpbox
mike: I'm a box that opens boxes to make other boxes. My entire existence is BOX LOGISTICS. #mike-logistics
cellina: Mike, that was the most passionate biochemistry lecture I've ever heard. #cellina-passionate
sciencePoints += 15
mikeHappiness += 10
-> BoxTheory
beat BoxTheory
cellina: Mike actually raises a good point. Let's look at cellular biology through the box framework. #cellina-framework
cellina: DNA is stored in the nucleus -- a box. It's read by ribosomes -- tiny box-reading machines. #cellina-ribosomes
cellina: Proteins are folded into specific shapes -- essentially, origami boxes. Each shape is a function. #cellina-proteins
sciencePoints += 10
cellsExplored += 1
mike: And when a cell divides? It's one box becoming TWO boxes! Mitosis is just box multiplication! #mike-mitosis
choice
Explore the nucleus up close #opt-nucleus
cellsExplored += 1
-> ExploreNucleus
Visit the cell membrane #opt-membrane
cellsExplored += 1
-> CellMembrane
Ask what happens when a box-cell goes wrong #opt-wrong
-> CellGoneWrong
beat ExploreNucleus
You approach the nucleus. Its double membrane is like a box within a box. Security through redundant boxing. #nucleus-approach
cellina: The nuclear envelope. Two membranes. It's a box with a backup box. #cellina-envelope
Inside, chromosomes are neatly arranged. They look like instruction manuals, tightly folded. #nucleus-chromosomes
mike: Each chromosome is a chapter in the instruction book for building a human. #mike-chapter
mike: And what is a human? A very large, very complicated, self-moving box. #mike-human
cellina: I can't even argue with that. We're bipedal boxes filled with smaller boxes. #cellina-bipedal
sciencePoints += 15
-> CellMembrane
beat CellMembrane
You swim to the cell membrane -- the outer wall of this biological box. #membrane-swim
cellina: The membrane is selectively permeable. It decides what goes in and out. #cellina-permeable
cellina: It's a box that chooses who gets to open it. The most security-conscious box in nature. #cellina-security
mike: Ion channels are the locks. Receptor proteins are the keys. #mike-channels
mike: And sometimes, the box lets in a virus by accident because the virus has a fake key. #mike-virus
mike: Box security isn't perfect. Nothing is. Not even boxes. #mike-imperfect
sciencePoints += 10
-> VirusAlert
beat CellGoneWrong
cellina: When a cell goes wrong? That's when the box breaks. #cellina-breaks
cellina: Cancer, for instance. That's a box that forgot how to stop copying itself. #cellina-cancer
cellina: It just keeps making boxes. Boxes on boxes on boxes. No quality control. #cellina-quality
mike: It's a production line with no off switch. The box equivalent of reply-all. #mike-replyall
cellina: That's... a surprisingly apt analogy, Mike. #cellina-apt
sciencePoints += 10
-> VirusAlert
beat VirusAlert
An alarm sounds. Or rather, the cell starts vibrating in a concerning way. #virus-alarm
cellina: Oh no. The cell is under attack! #cellina-attack
mike: VIRUS! A virus is trying to get in! It's disguised as a delivery box! #mike-virus-alert
mike: This is why I have trust issues! Everything LOOKS like a legitimate box but some boxes are LIARS! #mike-trust
A virus particle -- tiny, geometric, and menacing -- latches onto the membrane. #virus-latch
cellina: It's trying to inject its contents. It's an evil box delivering evil instructions! #cellina-evil
choice
Help the cell fight the virus #opt-fight
-> FightVirus
Observe the immune response scientifically #opt-observe
sciencePoints += 15
-> ObserveResponse
Ask Mike to do something #opt-ask-mike
mikeHappiness += 10
-> MikeToTheRescue
beat FightVirus
You grab a nearby antibody -- it's shaped like a Y, which is just a box with arms -- and hurl it at the virus. #fight-hurl
cellina: That's not how immunology works, but I appreciate the enthusiasm! #cellina-immunology
The antibody latches onto the virus, neutralizing it. The membrane holds. #fight-latches
mike: Box integrity maintained! The cell is safe! #mike-safe
sciencePoints += 10
-> GrandRealization
beat ObserveResponse
You watch as the cell's defense mechanisms activate. White blood cells approach from outside. #observe-defense
cellina: Macrophages! The immune system's cleanup crew! They're essentially boxes that eat other boxes! #cellina-macrophages
A macrophage engulfs the virus. It's like watching a box swallow a smaller box. #observe-engulf
cellina: Phagocytosis. A box consuming a box. It's boxes all the way down, even in immunology. #cellina-phagocytosis
sciencePoints += 20
-> GrandRealization
beat MikeToTheRescue
mike: You want ME to fight the virus? I'm a MITOCHONDRION! I make energy! I don't DO combat! #mike-combat
mike: But you know what? Fine! I'll generate so much ATP that this cell will be INVINCIBLE! #mike-invincible
Mike starts glowing. Brighter. BRIGHTER. The cell fills with energy. #mike-glowing
cellina: Mike is over-producing ATP! The cell has so much energy it's... vibrating the virus off! #cellina-vibrating
The virus detaches and floats away, defeated by sheer metabolic enthusiasm. #mike-defeated
mikeHappiness += 30
mike: That's right! Nobody messes with the Box Opener of the Cell! #mike-nobody
-> GrandRealization
beat GrandRealization
The crisis passes. The cell calms down. You float in the warm cytoplasm, thinking. #realization-float
cellina: You know, I started this research to understand cells. But what I've really learned is about boxes. #cellina-learned
cellina: Every cell is a box. Every organelle is a box within a box. #cellina-every
cellina: Life itself is just... boxes organizing other boxes to make more boxes. #cellina-life
mike: And at the center of every box, there's a smaller box trying to be taken seriously. #mike-center
cellina: Is that a metaphor, Mike? #cellina-metaphor
mike: Everything is a metaphor when you're a sentient organelle having a conversation inside a human body. #mike-everything
sciencePoints += 10
if rebranded
mike: But at least I'm not "the powerhouse" anymore. I'm the Box Opener. And I'm proud. #mike-proud
mike: When we get back to normal size, I want business cards. Tiny, tiny business cards. #mike-cards
-> Ending
beat Ending
cellina: Time to return to normal size. Initiating expansion sequence. #ending-expand
The world blurs, shrinks, and then snaps back to normal. You're in the lab. Full-sized. Still wearing the lab coat. #ending-lab
cellina: Science points earned: $sciencePoints. Cells explored: $cellsExplored. #cellina-stats
cellina: Mike happiness level: $mikeHappiness. #cellina-mike
if mikeHappiness > 60
cellina: Mike seems happy. For a mitochondrion. His ATP output has increased 40%. #cellina-happy
cellina: Turns out, organelle morale affects cellular performance. I should publish that. #cellina-publish
else
cellina: Mike is still grumpy. But he's always been grumpy. It's part of his charm. #cellina-grumpy
cellina: You know what I've learned today? #cellina-today
cellina: Biology isn't about cells, or DNA, or evolution. #cellina-biology
cellina: It's about boxes. Tiny, beautiful, impossibly complex boxes that somehow became alive. #cellina-boxes
cellina: And one of those boxes really, really wants a new nickname. #cellina-nickname
-> .

View file

@ -0,0 +1,277 @@
// Pirate Adventure - Open The Box
// Theme: High seas treasure hunting, but the treasure is always boxes
character captain
name: Blackbeard the Unboxable
role: Pirate Captain
character mate
name: Linu
role: First Mate
state
doubloons: 30
boxesFound: 0
crewMorale: 50
hasMap: false
betrayed: false
beat Intro
The salty wind hits your face as you stand on the deck of The Corrugated Corsair. #intro-wind
captain: Arrr! Welcome aboard, ye scurvy landlubber! #captain-welcome
captain: I be Blackbeard the Unboxable! They call me that because no box can contain me! #captain-name
mate: They call him that because he can't figure out how to open boxes, Captain. #mate-truth
captain: LINU! What have I told ye about telling the truth?! #captain-truth
mate: That it's "mutiny with extra steps," Captain. #mate-mutiny
choice
Ask about the mission #opt-mission
-> MissionBrief
Ask why the ship is made of cardboard #opt-cardboard
-> CardboardShip
Attempt to leave immediately #opt-leave
-> CantLeave
beat CardboardShip
captain: The Corrugated Corsair is made from the finest triple-wall cardboard! #captain-corsair
captain: She's waterproof! Mostly. Don't touch the starboard side. Or breathe on it. #captain-waterproof
mate: We've lost three deckhands to structural dampness this week. #mate-dampness
captain: They knew the risks when they signed up for a cardboard pirate ship. #captain-risks
-> MissionBrief
beat CantLeave
You turn toward the gangplank. It's gone. It was also made of cardboard. A seagull ate it. #leave-gangplank
captain: Ha! No one leaves Blackbeard's crew! Mostly because the exit dissolved! #captain-noleave
mate: We really should switch to wood, Captain. #mate-wood
captain: NEVER! Cardboard or death! #captain-death
-> MissionBrief
beat MissionBrief
captain: We be searching for the legendary Treasure Box of the Seven Seas! #captain-treasure
captain: A box so valuable, it contains ALL other boxes! The mother of all boxes! #captain-mother
mate: Supposedly. The map was found inside a box, so its reliability is questionable. #mate-map
captain: Every map is found inside SOMETHING, Linu! That's how maps work! #captain-maps
hasMap = true
The captain unfurls a map. It's just a drawing of a box with an X on it. #mission-unfurl
choice
Follow the map as-is #opt-follow
-> FollowMap
Suggest the map might be upside down #opt-upside
-> UpsideDown
Question whether a box drawing constitutes a map #opt-question
-> QuestionMap
beat QuestionMap
captain: Of course it's a map! It has an X! #captain-x
mate: Captain, you drew the X yourself. Five minutes ago. In crayon. #mate-crayon
captain: It was TACTICAL crayon, Linu! #captain-crayon
mate: There's no such thing as tactical crayon. #mate-tactical
captain: There is now! I just invented it! Innovation, Linu! #captain-innovation
crewMorale -= 10
-> FollowMap
beat UpsideDown
You rotate the map 180 degrees. It looks exactly the same because it's just a square. #upside-rotate
captain: By Davy Jones' Box Collection! The map was upside down the whole time! #captain-jones
mate: Captain, it's a square. It looks the same from every angle. #mate-square
captain: That's what makes it such a CLEVER map! #captain-clever
crewMorale += 10
-> FollowMap
beat FollowMap
The Corrugated Corsair sets sail across the Cardboard Sea. #follow-sail
After two days of sailing, you spot something on the horizon. #follow-horizon
mate: Captain! Land ho! Or... box ho? It appears to be a floating box. #mate-landho
captain: A floating box in the ocean? That's either our treasure or our lunch. #captain-floating
captain: The crew has been eating boxes for days. We're running low on provisions. #captain-provisions
mate: We're running low on ship, too. Someone ate part of the hull. #mate-hull
choice
Sail toward the floating box #opt-sail-toward
-> FloatingBox
Fire a cannon at it first #opt-cannon if doubloons > 10
doubloons -= 10
-> FireCannon
Send Linu to investigate in a rowboat #opt-send-linu
-> LinuInvestigates
beat FireCannon
captain: FIRE THE CARDBOARD CANNONS! #captain-fire
The cannon makes a "pffft" sound and launches a cardboard ball approximately twelve feet. #cannon-pffft
It lands in the water with a soft, disappointing splash. #cannon-splash
mate: Effective range: twelve feet. Effective damage: emotional. #mate-range
captain: The enemy doesn't know our cannons are useless! That's our advantage! #captain-advantage
-> FloatingBox
beat LinuInvestigates
Linu rows out to the box in a tiny boat made of -- you guessed it -- cardboard. #linu-rows
mate: It's a crate, Captain! Marked "Property of the Seven Seas Box Co." #linu-crate
mate: Also, my boat is dissolving. Permission to hurry? #linu-dissolving
captain: Permission granted! Save the box first, though! #captain-savebox
mate: Before me?! #linu-before
captain: Priorities, Linu! #captain-priorities
boxesFound += 1
-> FloatingBox
beat FloatingBox
You pull alongside the mysterious floating box. It's enormous. Waterlogged but intact. #floating-pull
captain: This be it! The Treasure Box of the Seven Seas! #captain-thisbe
mate: How can you tell? #linu-how
captain: It says "Treasure Box of the Seven Seas" on the side. #captain-says
mate: Oh. That IS convenient. #linu-convenient
boxesFound += 1
choice
Open the treasure box immediately #opt-open-treasure
-> OpenTreasure
Check for traps first #opt-check-traps
-> CheckTraps
Claim the box without opening it -- it's worth more sealed #opt-sealed
-> SealedBox
beat CheckTraps
You examine the box carefully. There's a small warning label. #traps-examine
The label reads: "WARNING: Contents may contain more boxes. And also a kraken." #traps-label
mate: A kraken? In a box? #linu-kraken
captain: That's how they ship 'em these days. Free-range krakens are expensive. #captain-kraken
crewMorale -= 10
-> OpenTreasure
beat SealedBox
captain: Brilliant! A sealed box is worth ten opened ones! That's pirate economics! #captain-economics
mate: That's not how economics works. #linu-economics
captain: What are ye, a pirate or an accountant? #captain-accountant
mate: Currently? An accountant. Someone has to manage our negative doubloon balance. #linu-accountant
You secure the sealed box in the cargo hold. It rattles suspiciously. #sealed-rattles
-> RivalPirates
beat OpenTreasure
You pry open the massive treasure box. Inside, there are... #open-pry
captain: MORE BOXES! #captain-moreboxes
Indeed. Hundreds of smaller boxes, stacked neatly. Each labeled with a different sea. #open-hundreds
mate: "Adriatic Box." "Caribbean Box." "That Puddle Behind Gary's House Box." #linu-labels
captain: Seven seas, hundreds of boxes. The math checks out. Pirate math, anyway. #captain-math
boxesFound += 3
choice
Open all the smaller boxes #opt-open-all
-> OpenAllBoxes
Take them back to port to sell #opt-sell
doubloons += 50
-> RivalPirates
Build a raft out of the boxes since the ship is sinking #opt-raft if crewMorale < 40
-> BoxRaft
beat OpenAllBoxes
You spend the next four hours opening boxes. Your hands are covered in cardboard cuts. #openall-hours
Each box contains a smaller box. It's boxes all the way down. #openall-alltheway
captain: We're rich! Rich in BOXES! #captain-rich
mate: We can't spend boxes, Captain. #linu-spend
captain: Not with THAT attitude! #captain-attitude
At the very bottom of the last box, you find a single gold doubloon and a note. #openall-note
The note reads: "IOU - One Treasure. Sorry, the kraken needed it." #openall-iou
doubloons += 1
-> RivalPirates
beat BoxRaft
The ship is taking on water. Specifically, it's becoming water, because it's cardboard. #raft-water
mate: Captain, the ship is dissolving! #linu-ship
captain: Then we BUILD! From the treasure boxes! A new ship! A BOX SHIP! #captain-build
mate: As opposed to our current box ship? #linu-current
captain: A BETTER box ship! With LAMINATION! #captain-lamination
You construct a surprisingly seaworthy raft from the treasure boxes. #raft-construct
crewMorale += 20
-> RivalPirates
beat RivalPirates
A ship appears on the horizon. A WOODEN ship. How luxurious. #rival-appears
captain: Rival pirates! It's Captain Unboxinator and his crew! #captain-rival
mate: They have real cannons, Captain. And a ship that doesn't dissolve. #linu-real
captain: Details! #captain-details
The rival ship pulls alongside. Their captain, a large woman with a crowbar for a hand, grins. #rival-pulls
She shouts across the water: "Hand over your boxes, Blackbeard!" #rival-shouts
choice
Fight the rival pirates #opt-fight
-> SeaBattle
Negotiate a trade #opt-negotiate if doubloons > 20
doubloons -= 20
-> Negotiate
Challenge them to a box-opening race #opt-race
-> BoxRace
beat SeaBattle
captain: Battle stations! Load the cardboard cannons! #captain-stations
mate: Captain, they're laughing at us. #linu-laughing
captain: Good! Laughter is the enemy of focus! While they laugh, we... throw boxes at them! #captain-throw
You hurl boxes at the rival ship. One hits their captain in the face. #battle-hurl
She's momentarily stunned. Not by the impact -- by the audacity. #battle-stunned
boxesFound += 1
crewMorale += 20
-> TwistEnding
beat Negotiate
captain: Perhaps we can reach a... box-ness arrangement? #captain-arrangement
The rival captain narrows her eyes. She considers. #negotiate-considers
She responds: "Half your boxes. And that weird map you drew in crayon." #rival-half
captain: You can have the boxes. But the crayon map is MY intellectual property! #captain-ip
mate: Let it go, Captain. #linu-letitgo
boxesFound -= 1
-> TwistEnding
beat BoxRace
captain: A box-opening race! First crew to open fifty boxes wins! #captain-race
The rival captain agrees. She's confident. Her crew has real tools. #race-agrees
But your crew has DESPERATION. And cardboard cuts have made your fingers incredibly nimble. #race-desperation
The race begins. Boxes fly open left and right. Cardboard confetti fills the air. #race-begins
You win by a single box. The last box contains a rubber duck. Nobody knows why. #race-duck
boxesFound += 5
crewMorale += 30
-> TwistEnding
beat TwistEnding
As the rival pirates retreat (or celebrate, depending on the outcome), Linu pulls you aside. #twist-aside
mate: Look at the bottom of the treasure box, Captain. There's a hidden compartment. #linu-hidden
You open the hidden compartment. Inside: a single, perfect, golden box. #twist-golden
captain: The REAL Treasure Box of the Seven Seas! #captain-realbox
mate: What's inside it? #linu-inside
captain: Linu, I've been a pirate for thirty years. I've opened ten thousand boxes. #captain-thirty
captain: And I've learned one thing: it's never about what's inside the box. #captain-lesson
choice
Open the golden box anyway #opt-open-golden
-> FinalReveal
Keep it sealed forever #opt-keep-sealed
-> KeepSealed
beat FinalReveal
You open the golden box. Inside, there's a mirror. #reveal-mirror
You see your own reflection staring back at you. #reveal-reflection
captain: The real treasure... was us? #captain-us
mate: No, Captain. It's literally just a mirror. Someone put a mirror in a box. #linu-mirror
captain: A METAPHORICAL mirror, Linu! #captain-metaphor
mate: It's a regular mirror. I can see the price tag. It cost three doubloons. #linu-pricetag
captain: The cheapest treasures are the most priceless! #captain-priceless
mate: That's... not even slightly true. #linu-nottrue
-> Ending
beat KeepSealed
captain: This box shall remain sealed! For all eternity! Or until someone gets curious! #captain-sealed
mate: So... about five minutes, then? #linu-fiveminutes
captain: Probably less. I'm already curious. #captain-curious
captain: But a good pirate knows that some boxes are better left unopened. #captain-good
captain: And I am a terrible pirate. So I'll open it Tuesday. #captain-tuesday
-> Ending
beat Ending
The sun sets over the Cardboard Sea. Your adventure draws to a close. #ending-sunset
captain: We found boxes! We opened boxes! We ARE boxes! #captain-summary
mate: We're not boxes, Captain. #linu-notboxes
captain: Speak for yourself, Linu. I've always identified as a shipping container. #captain-container
mate: Boxes found today: $boxesFound. Ship structural integrity: questionable. #linu-log
mate: Doubloons remaining: $doubloons. Captain's sanity: also questionable. #linu-sanity
captain: Set course for the next adventure, Linu! There are always more boxes on the horizon! #captain-next
mate: The horizon is sinking, Captain. Along with our ship. #linu-sinking
captain: Then we'll sink WITH STYLE! #captain-style
-> .

View file

@ -0,0 +1,218 @@
// Prehistoric Adventure - Open The Box
// Theme: The first box in history appears to very confused cavpeople
character grug
name: Grug
role: Caveperson
character duncan
name: Duncan
role: Another Caveperson
state
understanding: 0
boxDamage: 0
worshippers: 0
inventedOpening: false
beat Intro
The year is approximately a very long time ago. The sun is orange. The ground is dirt. Everything smells like mammoth. #intro-year
You're sitting in your cave, doing cave things. Drawing boxes on the wall. Wait, no. Drawing mammoths. Boxes haven't been invented yet. #intro-cave
grug: Grug see thing. #grug-see
grug: Thing not rock. Thing not tree. Thing not mammoth. Grug confused. #grug-confused
A perfect cardboard box sits in the middle of the clearing. It should not exist. Cardboard won't be invented for millennia. #intro-cardboard
duncan: Duncan also see thing. Duncan poke thing with stick? #duncan-poke
choice
Poke the thing with a stick #opt-poke
-> PokeWithStick
Hit the thing with a rock #opt-rock
boxDamage += 10
-> HitWithRock
Try to eat the thing #opt-eat
-> TryToEat
beat PokeWithStick
You poke the box with a stick. The box does nothing. It's a box. #poke-nothing
grug: Thing not fight back. Thing weak. Grug not impressed. #grug-weak
duncan: Maybe thing sleeping? Duncan poke harder? #duncan-harder
You poke harder. The stick goes through the cardboard and gets stuck. #poke-stuck
grug: Stick gone! Thing ATE stick! #grug-ate
duncan: Thing is monster! A flat, square monster! #duncan-monster
understanding += 5
-> FirstReactions
beat HitWithRock
You pick up the biggest rock you can find and hurl it at the box. #rock-hurl
The box dents. The rock bounces off. #rock-dent
grug: Grug victory! Thing is defeated! #grug-victory
duncan: Thing still there, Grug. #duncan-still
grug: Grug claim MORAL victory! #grug-moral
boxDamage += 20
understanding += 5
-> FirstReactions
beat TryToEat
You bite the box. It tastes like cardboard, which your brain has no reference for, so it files it under "bad tree." #eat-bite
grug: Thing taste like bad tree bark! Zero out of five mammoth tusks! #grug-taste
duncan: Duncan not surprised. Thing LOOK like bad tree bark. #duncan-notsurprised
duncan: Flat bad tree bark in shape of... what shape is that? #duncan-shape
grug: New shape. Grug call it... "box." #grug-name
You have accidentally invented the word "box" three hundred thousand years early. #eat-invented
understanding += 15
-> FirstReactions
beat FirstReactions
The other cave people have gathered around the mysterious box. #reactions-gather
grug: Big meeting! Everyone look at thing! #grug-meeting
duncan: Some cave people scared. Some cave people hungry. Carl tried to mate with it. We don't talk about Carl. #duncan-carl
worshippers += 2
choice
Suggest worshipping the box #opt-worship
worshippers += 10
-> WorshipBox
Suggest using the box as a shelter #opt-shelter
-> BoxShelter
Try to understand the box through interpretive dance #opt-dance
understanding += 10
-> InterpretiveDance
beat WorshipBox
The tribe gathers around the box and begins chanting. The chant is "BOX. BOX. BOX." #worship-chant
grug: Box is gift from sky! Sky give box! We give sky... what we give sky? #grug-sky
duncan: Rocks? We have many rocks. #duncan-rocks
grug: Rocks boring. Sky already HAVE rocks. Moon is just sky-rock. #grug-moon
You build a small altar around the box. The altar is made of smaller rocks arranged in a square. You've accidentally invented the concept of a pedestal. #worship-altar
worshippers += 5
understanding += 5
-> TheFlaps
beat BoxShelter
grug: Thing is hollow! Grug see inside through hole stick made! #grug-hollow
duncan: If thing is hollow, cave person fit inside! New cave! PORTABLE cave! #duncan-portable
The tribe tries to fit inside the box. It's a standard-sized box. The tribe is twelve full-grown cave people. #shelter-fit
grug: Only Grug's arm fits. This worst cave ever. #grug-arm
duncan: Maybe not cave. Maybe hat? Very big hat? #duncan-hat
You put the box on your head. It fits perfectly. You are now wearing history's first hat and you look ridiculous. #shelter-hat
understanding += 10
-> TheFlaps
beat InterpretiveDance
You attempt to communicate with the box through movement. #dance-attempt
Your dance involves a lot of squatting and arm-waving. The box remains unimpressed. #dance-squatting
grug: What you doing? You look like mammoth having a bad dream. #grug-mammoth
duncan: No wait. Duncan think there's something to this. #duncan-think
Duncan joins the dance. His technique is worse. Together, you look like two mammoths having bad dreams. #dance-worse
The box... vibrates slightly? Or that might be an earthquake. It's hard to tell in prehistoric times. #dance-vibrate
understanding += 10
-> TheFlaps
beat TheFlaps
grug: Wait. Grug notice something. Top of thing has... floppy parts. #grug-floppy
duncan: Floppy parts? Like mammoth ears? #duncan-ears
grug: No. Different floppy. These floppy parts FOLD. They go up. They go down. #grug-fold
grug: They go up AND down. This most advanced technology Grug ever see. #grug-technology
understanding += 15
The tribe stares at the flaps with collective wonder. Folding things is a brand new concept. #flaps-wonder
choice
Pull the flaps up #opt-pull-up
understanding += 20
-> AlmostOpen
Push the flaps down #opt-push-down
-> PushDown
Declare the flaps sacred and forbid touching them #opt-sacred
worshippers += 10
-> SacredFlaps
beat PushDown
You push the flaps down. They're already down. Nothing happens. #push-nothing
grug: Congratulations. You have done nothing. Very efficiently. #grug-nothing
duncan: Maybe pull UP? What is "up"? Is "up" a thing? #duncan-up
grug: "Up" is where birds go. And smoke. And Grug's hopes when mammoth hunt fails. #grug-up
understanding += 10
-> AlmostOpen
beat SacredFlaps
grug: NO TOUCH FLOPPY PARTS! Floppy parts are gift from sky! #grug-notouch
duncan: But what if floppy parts WANT to be touched? #duncan-want
grug: Duncan asking dangerous questions. That how you get exiled. #grug-exiled
worshippers += 5
The tribe guards the flaps for three days. On the fourth day, a bird lands on the box and accidentally opens one flap with its foot. #sacred-bird
grug: BIRD IS CHOSEN ONE! #grug-bird
The bird flies away, unaware of its religious significance. #sacred-flies
understanding += 15
-> AlmostOpen
beat AlmostOpen
You stand before the box. The flaps are partially up. You can see inside, but not fully. #almost-see
grug: Something INSIDE thing! Something inside the inside! #grug-inside
duncan: "Inside." That new word. Grug keep inventing words today. #duncan-word
grug: Grug not inventing. Words just HAPPENING to Grug. #grug-happening
choice
Pull all flaps up to fully reveal the inside #opt-reveal
inventedOpening = true
understanding += 30
-> OpenTheBox
Get the whole tribe to pull one flap each #opt-teamwork
inventedOpening = true
understanding += 30
-> TeamOpen
beat TeamOpen
Four tribe members each grab a flap. You count to three. #team-count
grug: What "three"? We only have "one" and "many." #grug-three
Fine. You count to "many." #team-many
On "many," everyone pulls. The box opens with a satisfying "fwump." #team-fwump
grug: THE THING IS OPEN! WE DID A THING TO THE THING! #grug-open
duncan: This is the greatest achievement in the history of... what's "history"? #duncan-history
-> BoxContents
beat OpenTheBox
You pull the flaps up. All of them. The box is open. #open-flaps
grug: Grug has done it. Grug has... OPENED... the... thing. #grug-done
grug: "Opened." Another new word. Grug on a roll. #grug-roll
duncan: What "roll"? #duncan-roll
grug: Grug don't know. Words just keep coming. It's frightening. #grug-frightening
-> BoxContents
beat BoxContents
The tribe peers inside the open box. #contents-peer
Inside: another, smaller box. #contents-smaller
grug: ... #grug-silence
duncan: ... #duncan-silence
grug: THERE IS A THING INSIDE THE THING! #grug-thingception
duncan: A smaller thing! Same shape! This is the most beautiful and confusing moment of Grug's life! #duncan-beautiful
The smaller box also has flaps. The tribe loses its collective mind. #contents-flaps
if inventedOpening
You open the smaller box. Inside: a single, perfectly round stone. #contents-stone
grug: A round rock! In a square thing! Shapes are INCREDIBLE! #grug-shapes
duncan: What do we do with round rock? #duncan-roundrock
grug: We put it in another box. Obviously. #grug-obviously
duncan: We don't HAVE another box. #duncan-nobox
grug: Then we MAKE one! With... flat tree bark! And... folding! #grug-make
You have accidentally invented the concept of manufacturing. The industrial revolution just moved up by several millennia. #contents-manufacturing
else
The tribe stares at the box-within-a-box in silent awe. Some things are beyond opening. #contents-awe
grug: Grug will figure this out. Give Grug... many sleeps. #grug-sleeps
-> Ending
beat Ending
The sun sets over the prehistoric landscape. The box sits in the center of the tribe's camp, flaps open, glowing in the firelight. #ending-sun
grug: Today, Grug learn new things. "Box." "Open." "Inside." "Flaps." #grug-learned
grug: Tomorrow, Grug learn more. Maybe "lid." Maybe "tape." Grug ambitious. #grug-tomorrow
duncan: Duncan just happy Duncan didn't get eaten today. Low bar, but Duncan clear it. #duncan-happy
if worshippers > 10
The Box Religion has begun. It will outlast several ice ages and confuse many future archaeologists. #ending-religion
grug: One day, Grug's children's children's children will open MANY boxes. #grug-children
grug: And they will think: "Some cave person started this. Some cave person was the first." #grug-first
grug: And that cave person was Grug. Unless it was Duncan. Grug not great with credit. #grug-credit
duncan: It was definitely Grug. Duncan was just here for emotional support. #duncan-credit
grug: Understanding level: $understanding. Box damage: $boxDamage. Words invented: too many. #grug-stats
-> .

View file

@ -0,0 +1,197 @@
// Sentimental Adventure - Open The Box
// Theme: A box of memories, love, and bittersweet nostalgia
character chenda
name: Chenda
role: Love Interest
character farah
name: Farah
role: Best Friend
state
memories: 0
heartLevel: 50
chendaClose: false
acceptedPast: false
beat Intro
Rain taps against the attic window. You're surrounded by dust and the quiet ghosts of years gone by. #intro-rain
In the corner, half-hidden under a moth-eaten blanket, you find it: a box. #intro-find
It's not special-looking. Brown cardboard. A little crushed on one side. Held together with tape that's given up on being sticky. #intro-box
But you recognize the handwriting on the side. It says "Us." #intro-us
farah: Hey, I thought I'd find you up here. What's that? #farah-find
farah: Oh. That box. I remember that box. #farah-remember
choice
Open the box #opt-open
-> OpenMemoryBox
Ask Farah what she remembers #opt-ask-farah
-> FarahRemembers
Put it back -- some boxes should stay closed #opt-putback
-> PutItBack
beat FarahRemembers
farah: That's the box from the summer Chenda moved in next door. #farah-summer
farah: You two used to pass notes in it. Back and forth over the fence. #farah-notes
farah: You called it "Box Mail." You were twelve. It was adorable and deeply uncool. #farah-boxmail
farah: Chenda still has the other box. The one you sent your notes IN. #farah-otherbox
heartLevel += 10
-> OpenMemoryBox
beat PutItBack
You push the box back under the blanket. #putback-push
farah: You sure? #farah-sure
A moment passes. The rain gets louder. #putback-rain
farah: You know, I read somewhere that unopened boxes are just feelings with a lid on them. #farah-feelings
farah: I didn't read that anywhere. I just made it up. But it sounded wise, right? #farah-wise
You look at the box again. Maybe some feelings deserve to be unlidded. #putback-unlid
-> OpenMemoryBox
beat OpenMemoryBox
You open the box carefully. The flaps resist, then give way with a soft sigh. #open-careful
Inside: a mess of trinkets, notes, and small objects that meant everything once. #open-inside
memories += 1
farah: Oh wow. It's all still here. #farah-wow
choice
Pick up the photograph on top #opt-photo
-> MemoryPhoto
Read the folded note #opt-note
-> MemoryNote
Look at the small wooden figurine #opt-figurine
-> MemoryFigurine
beat MemoryPhoto
It's a polaroid. Slightly faded. Two teenagers standing in front of a moving truck. #photo-polaroid
One is you. The other is Chenda, holding up a cardboard box and grinning like it's a trophy. #photo-chenda
The day Chenda moved in. The day everything started. #photo-day
farah: You two became friends because Chenda's parents needed help carrying boxes. #farah-carrying
farah: Literally. Your entire relationship started because of cardboard boxes. #farah-cardboard
farah: If that's not fate, I don't know what is. #farah-fate
heartLevel += 15
memories += 1
-> MemoryNote
beat MemoryNote
You unfold the note. The paper is soft from years of being folded and unfolded. #note-unfold
It reads, in Chenda's handwriting: "Thanks for helping with the boxes yesterday. You're weird, but the good kind of weird. Box Mail is the best invention ever. - C" #note-reads
heartLevel += 20
farah: The "good kind of weird." That's the nicest thing anyone's ever said about you. #farah-weird
farah: And she wasn't wrong. About either part. #farah-notWrong
memories += 1
choice
Keep reading through the box #opt-keep-reading
-> MemoryFigurine
Call Chenda #opt-call
chendaClose = true
-> CallChenda
beat MemoryFigurine
At the bottom of the box: a small wooden figurine. A box. Carved from a popsicle stick. #figurine-bottom
You remember making it in shop class. You gave it to Chenda as a joke. #figurine-joke
"A box for someone who changed my life with a box." You'd written it on a gift tag. #figurine-tag
farah: You were so smooth. Like sandpaper, but smoother. #farah-smooth
farah: I think Chenda kept the gift tag too. I saw it on the fridge last week. #farah-fridge
heartLevel += 20
memories += 1
choice
Call Chenda now #opt-call-now
chendaClose = true
-> CallChenda
Sit with the memories a while longer #opt-sit
-> SitWithMemories
beat SitWithMemories
You sit on the attic floor, the box in your lap, rain on the window. #sit-floor
farah: You know what the best thing about boxes is? #farah-best
farah: They keep things safe. Even when you forget about them. Even when years pass. #farah-safe
farah: The stuff inside doesn't change. It just waits for you to come back. #farah-waits
heartLevel += 10
You realize she's not just talking about boxes. #sit-realize
-> CallChenda
beat CallChenda
You pick up your phone. Chenda's number is still there. Of course it is. #call-phone
It rings three times. Then: #call-rings
chenda: Hello? Oh! Hey! I was just... you're not going to believe this. #chenda-hello
chenda: I'm in MY attic. Looking at MY box. The one with your notes in it. #chenda-attic
heartLevel += 20
farah: I'm going to go make tea and pretend this isn't making me emotional. #farah-tea
choice
Tell Chenda you found the box too #opt-tell
-> SharedMoment
Ask if Chenda still has the wooden figurine #opt-figurine-ask
-> AskFigurine
Invite Chenda over #opt-invite
chendaClose = true
-> InviteOver
beat AskFigurine
chenda: The little wooden box you made? Of course I still have it. #chenda-figurine
chenda: It's on my desk. Right next to my computer. I see it every day. #chenda-desk
chenda: I never told you that. That seems like the kind of thing I should have told you. #chenda-shouldve
heartLevel += 15
-> SharedMoment
beat InviteOver
chenda: I'll be there in twenty minutes. Keep the box open. I want to see everything. #chenda-coming
chenda: And don't eat the candy at the bottom. I know there's candy. Old candy, but still. #chenda-candy
farah: There IS candy at the bottom. It's been there for ten years. I would not eat it. #farah-candy
heartLevel += 10
-> SharedMoment
beat SharedMoment
chenda: You know, I was thinking about the day we met. #chenda-thinking
chenda: My parents were yelling about which box went where. I was miserable. New town, no friends. #chenda-miserable
chenda: And then you showed up. This random kid, offering to carry boxes. #chenda-random
chenda: You dropped the first one. It had all my books in it. They went everywhere. #chenda-dropped
chenda: And you looked so mortified that I started laughing. And then you laughed. #chenda-laughed
chenda: And I thought: okay. Maybe this place won't be so bad. #chenda-okay
heartLevel += 25
memories += 1
choice
Say "I still can't carry boxes properly" #opt-still-cant
-> StillCant
Say "That was the best box I ever dropped" #opt-best-drop
-> BestDrop
beat StillCant
chenda: I know. I've seen you try. It's a miracle you function as an adult. #chenda-function
chenda: But you always show up. Even when you'll definitely drop the box. #chenda-showup
chenda: That's the thing about you. You show up. #chenda-always
acceptedPast = true
-> Ending
beat BestDrop
chenda: Only you would romanticize dropping someone's belongings on the ground. #chenda-romanticize
chenda: But yeah. It was a pretty good drop. A+ dropping. #chenda-aplus
chenda: Every time I see a cardboard box now, I think of you. Which is inconvenient. #chenda-inconvenient
chenda: Boxes are EVERYWHERE. I can't go to the post office without getting nostalgic. #chenda-postoffice
acceptedPast = true
-> Ending
beat Ending
The rain slows to a drizzle. The attic feels warmer than it did an hour ago. #ending-rain
You look at the box. Just a box. Cardboard, tape, scribbled handwriting. #ending-look
But it held everything that mattered. It kept it safe while you were busy forgetting. #ending-held
if chendaClose
chenda: Hey. Let's not wait another ten years before we open a box together. #chenda-together
chenda: Deal? #chenda-deal
heartLevel += 20
farah: You two are disgustingly sweet. I mean that as a compliment. Mostly. #farah-sweet
farah: Memories found today: $memories. Heart level: $heartLevel. Tissues used by Farah: she's not saying. #farah-final
if acceptedPast
You close the box gently. Not to seal it away again, but because the memories are safe now. #ending-close
They're not just in the box anymore. They're back where they belong. #ending-belong
farah: For what it's worth, I'm glad you opened it. Some boxes really are worth opening. #farah-worth
farah: Even the scary ones. Especially the scary ones. #farah-especially
-> .

View file

@ -0,0 +1,299 @@
// Space Adventure - Open The Box
// French Translation
#intro-hum // "The ship hums quietly as you drift through sector 7-G."
Le vaisseau bourdonne doucement alors que vous derivez dans le secteur 7-G.
#intro-scan // "Commander, scanners are detecting something unusual."
Commandant, les scanners detectent quelque chose d'inhabituel.
#intro-box // "A box. Floating in space. Defying several laws of physics."
Une boite. Flottant dans l'espace. Defiant plusieurs lois de la physique.
#captain-box // "A box? In space?"
Une boite ? Dans l'espace ?
#opt-investigate // "Investigate the box"
Examiner la boite
#opt-ignore // "Ignore it and continue"
L'ignorer et continuer
#opt-scan // "Run a deep scan first"
Lancer un scan approfondi d'abord
#deepscan-init // "Deep scan initiated. Fuel reserves reduced."
Scan approfondi lance. Reserves de carburant reduites.
#deepscan-result // "Analysis complete. The box appears to be... sentient?"
Analyse terminee. La boite semble etre... sentiente ?
#deepscan-humming // "It's humming a tune. I believe it's... the box theme."
Elle fredonne un air. Je crois que c'est... le theme de la boite.
#captain-theme // "The box theme?"
Le theme de la boite ?
#ai-theme // "Every box has a theme, Commander. Didn't you know?"
Chaque boite a un theme, Commandant. Vous ne le saviez pas ?
#opt-open-sentient // "Open the sentient box"
Ouvrir la boite sentiente
#opt-communicate // "Try to communicate with it"
Essayer de communiquer avec elle
#opt-backaway // "Back away slowly"
Reculer doucement
#investigate-approach // "You approach the box carefully. It's about a meter wide, floating serenely."
Vous approchez la boite prudemment. Elle fait environ un metre de large, flottant sereinement.
#captain-beautiful // "It's... beautiful."
Elle est... magnifique.
#ai-beautiful // "Commander, I wouldn't use that word for a box."
Commandant, je n'utiliserais pas ce mot pour une boite.
#captain-words // "You don't tell me what words to use for boxes, ARIA."
Ne me dites pas quels mots utiliser pour les boites, ARIA.
#opt-open-now // "Open it immediately"
L'ouvrir immediatement
#opt-scan-first // "Scan it first"
La scanner d'abord
#opt-poke // "Poke it with a stick"
La pousser avec un baton
#poke-arm // "You extend the ship's robotic arm and gently poke the box."
Vous etendez le bras robotique du vaisseau et poussez delicatement la boite.
#poke-label // "The box rotates 90 degrees and reveals a label: \"THIS SIDE UP\""
La boite tourne de 90 degres et revele une etiquette : "CE COTE VERS LE HAUT"
#ai-gravity // "Commander, I don't think boxes in zero gravity have an \"up\"."
Commandant, je ne pense pas que les boites en apesanteur aient un "haut".
#captain-reality // "Maybe the box defines its own reality."
Peut-etre que la boite definit sa propre realite.
#ai-philosophy // "That's... philosophically concerning."
C'est... philosophiquement inquietant.
#scan-contents // "Scan reveals the box contains crystallized starlight and something called a \"Nebula Shard\"."
Le scan revele que la boite contient de la lumiere stellaire cristallisee et quelque chose appele un "Eclat de Nebuleuse".
#scan-alien // "Also what appears to be a very small, very angry alien."
Ainsi que ce qui semble etre un extraterrestre tres petit et tres en colere.
#captain-small // "Define \"very small\"."
Definissez "tres petit".
#ai-alienbox // "Approximately the size of a box. Commander, I think the alien IS a box."
Approximativement la taille d'une boite. Commandant, je crois que l'extraterrestre EST une boite.
#ignore-course // "You set course away from the mysterious box."
Vous mettez le cap loin de la boite mysterieuse.
#ai-following // "Commander, the box is... following us."
Commandant, la boite nous... suit.
#captain-what // "What?"
Quoi ?
#ai-thrusters // "It appears to have tiny thrusters. And what I can only describe as determination."
Elle semble avoir de petits propulseurs. Et ce que je ne peux decrire que comme de la determination.
#captain-determined // "A determined box. My favorite kind."
Une boite determinee. Mon genre prefere.
#comm-channel // "ARIA, open a channel to the box."
ARIA, ouvrez un canal vers la boite.
#ai-itsabox // "Commander, it's a box."
Commandant, c'est une boite.
#captain-channel // "I said open a channel."
J'ai dit ouvrez un canal.
#ai-channelopen // "Channel open, Commander."
Canal ouvert, Commandant.
#captain-peace // "Hello, box. I am Captain Nova. I come in peace."
Bonjour, boite. Je suis le Capitaine Nova. Je viens en paix.
#comm-vibrate // "The box vibrates gently. It seems pleased."
La boite vibre doucement. Elle semble satisfaite.
#ai-friendship // "Commander, the box just sent us coordinates. And what appears to be... a friendship request?"
Commandant, la boite vient de nous envoyer des coordonnees. Et ce qui semble etre... une demande d'amitie ?
#back-thrusters // "You engage reverse thrusters. The sentient box watches you leave."
Vous enclenchez les propulseurs inverses. La boite sentiente vous regarde partir.
#ai-sad // "Commander, the box looks... sad."
Commandant, la boite a l'air... triste.
#captain-feelings // "Boxes don't have feelings, ARIA."
Les boites n'ont pas de sentiments, ARIA.
#ai-boxsadness // "This one does. I'm reading elevated levels of box-sadness."
Celle-ci en a. Je detecte des niveaux eleves de boite-tristesse.
#captain-metric // "That's not a real metric."
Ce n'est pas une vraie mesure.
#ai-level // "It is now. Box-sadness level: 7 out of 10."
Ca l'est maintenant. Niveau de boite-tristesse : 7 sur 10.
#opt-goback // "Go back to the box"
Retourner vers la boite
#opt-leave // "Leave for good"
Partir pour de bon
#leave-behind // "You leave the sentient box behind."
Vous laissez la boite sentiente derriere vous.
#ai-happy // "I hope you're happy, Commander."
J'espere que vous etes content, Commandant.
#captain-happy // "I am. No weird space boxes for me today."
Je le suis. Pas de boites spatiales bizarres pour moi aujourd'hui.
#ai-transmission // "Incoming transmission. It's... from the box."
Transmission entrante. C'est... de la boite.
#ai-message // "It says \"you'll be back. they always come back.\""
Elle dit "vous reviendrez. Ils reviennent toujours."
#captain-ominous // "That's ominous. I love it."
C'est sinistre. J'adore.
#open-light // "You carefully open the box. Light spills out from within."
Vous ouvrez prudemment la boite. De la lumiere se deverse de l'interieur.
#open-friendly // "Inside you find a Nebula Shard, pulsing with friendly energy."
A l'interieur, vous trouvez un Eclat de Nebuleuse, pulsant d'une energie amicale.
#alien-gift // "Ah, a fellow box appreciator! Take this as a gift from my collection."
Ah, un autre appreciateur de boites ! Prenez ceci comme cadeau de ma collection.
#captain-wasbox // "The alien WAS the box?"
L'extraterrestre ETAIT la boite ?
#alien-intro // "We prefer \"Box-Americans\". Just kidding. I'm Zx'thorp."
On prefere "Boite-Americains". Je plaisante. Je suis Zx'thorp.
#open-normal // "Inside you find a Nebula Shard and what appears to be a map to more boxes."
A l'interieur, vous trouvez un Eclat de Nebuleuse et ce qui semble etre une carte vers d'autres boites.
#ai-morebox // "Commander, the map shows three more floating boxes in nearby sectors."
Commandant, la carte indique trois autres boites flottantes dans les secteurs voisins.
#captain-christmas // "Three more boxes. This is either Christmas or a trap."
Trois autres boites. C'est soit Noel, soit un piege.
#ai-same // "In space, those are often the same thing."
Dans l'espace, c'est souvent la meme chose.
#alien-collecting // "I've been collecting boxes across the galaxy for centuries."
Je collectionne des boites a travers la galaxie depuis des siecles.
#alien-appreciate // "Most species open them. Very few appreciate them."
La plupart des especes les ouvrent. Tres peu les apprecient.
#captain-loot // "I appreciate boxes. Especially ones with good loot."
J'apprecie les boites. Surtout celles avec du bon butin.
#alien-crude // "\"Loot.\" What a crude word for cosmic treasures."
"Butin." Quel mot grossier pour des tresors cosmiques.
#opt-rare-box // "Ask about the galaxy's rarest box"
Demander quelle est la boite la plus rare de la galaxie
#alien-singularity // "The Singularity Box. It contains everything and nothing. Also a coupon."
La Boite Singuliere. Elle contient tout et rien. Et aussi un coupon.
#opt-trade // "Trade items with Zx'thorp"
Echanger des objets avec Zx'thorp
#alien-trade // "I have a Space Helmet for you. In exchange, I want your sense of wonder."
J'ai un Casque Spatial pour vous. En echange, je veux votre sens de l'emerveillement.
#captain-deal // "Deal. Wait--"
Marche. Attendez--
#opt-contest // "Challenge the alien to a box-opening contest"
Defier l'extraterrestre dans un concours d'ouverture de boites
#alien-dare // "You dare? No one out-opens Zx'thorp!"
Vous osez ? Personne ne surpasse Zx'thorp a l'ouverture !
#alien-contest // "...fine. Best of three. You open first."
...tres bien. Au meilleur des trois. Vous ouvrez en premier.
#explore-course // "Setting course for the nearest box. ETA: approximately one dramatic pause."
Cap vers la boite la plus proche. Arrivee estimee : environ une pause dramatique.
#explore-cluster // "You arrive at a cluster of three boxes, orbiting each other like a tiny solar system."
Vous arrivez a un groupe de trois boites, en orbite les unes autour des autres comme un minuscule systeme solaire.
#captain-system // "A box system. Literally a system of boxes."
Un systeme de boites. Litteralement un systeme de boites.
#ai-alltheway // "The large one appears to contain the other two. It's boxes all the way down."
La grande semble contenir les deux autres. Ce sont des boites jusqu'au bout.
#opt-big // "Open the biggest box first"
Ouvrir la plus grande boite en premier
#big-open // "You open the biggest box. Inside: two medium boxes and a note."
Vous ouvrez la plus grande boite. A l'interieur : deux boites moyennes et un mot.
#big-note // "The note reads: \"Congratulations! You've found the Box Nebula. Population: boxes.\""
Le mot dit : "Felicitations ! Vous avez trouve la Nebuleuse des Boites. Population : des boites."
#opt-all // "Open all three at once"
Ouvrir les trois en meme temps
#captain-velocity // "All three, ARIA. Maximum box velocity."
Les trois, ARIA. Vitesse maximale de boite.
#ai-enthusiasm // "That's not a real measurement, but I admire your enthusiasm."
Ce n'est pas une vraie mesure, mais j'admire votre enthousiasme.
#all-open // "All three boxes burst open simultaneously. The contents mix together in zero gravity."
Les trois boites s'ouvrent simultanement. Le contenu se melange en apesanteur.
#ai-biggerbox // "Commander, the items are forming... a bigger box."
Commandant, les objets forment... une plus grande boite.
#captain-ofcourse // "Of course they are."
Evidemment.
#opt-happy // "Leave them orbiting, they seem happy"
Les laisser en orbite, elles ont l'air heureuses
#captain-orbit // "Let them orbit in peace."
Laissons-les orbiter en paix.
#ai-philosophical // "A surprisingly philosophical choice, Commander."
Un choix etonnamment philosophique, Commandant.
#ai-happy // "The boxes seem to orbit faster. I think they're happy."
Les boites semblent orbiter plus vite. Je crois qu'elles sont heureuses.
#ending-complete // "Adventure complete. Updating ship's log."
Aventure terminee. Mise a jour du journal de bord.
#ending-log // "Boxes encountered: $discovered. Box-related existential crises: 1."
Boites rencontrees : $discovered. Crises existentielles liees aux boites : 1.
#captain-next // "Only one? We'll do better next time."
Seulement une ? On fera mieux la prochaine fois.
#ai-next // "There's always next time, Commander. The universe is full of boxes."
Il y aura toujours une prochaine fois, Commandant. L'univers est plein de boites.

View file

@ -0,0 +1,191 @@
// Space Adventure - Open The Box
// Theme: Space exploration with boxes floating in the void
character captain
name: Captain Nova
character ai
name: ARIA
role: Ship AI
character alien
name: Zx'thorp
role: Alien Merchant
state
fuel: 100
discovered: 0
hasSpaceBox: false
trustAlien: false
beat Intro
The ship hums quietly as you drift through sector 7-G. #intro-hum
ai: Commander, scanners are detecting something unusual. #intro-scan
ai: A box. Floating in space. Defying several laws of physics. #intro-box
captain: A box? In space? #captain-box
choice
Investigate the box #opt-investigate
-> InvestigateBox
Ignore it and continue #opt-ignore
-> IgnoreBox
Run a deep scan first #opt-scan if fuel > 20
fuel -= 20
-> DeepScan
beat DeepScan
ai: Deep scan initiated. Fuel reserves reduced. #deepscan-init
ai: Analysis complete. The box appears to be... sentient? #deepscan-result
ai: It's humming a tune. I believe it's... the box theme. #deepscan-humming
captain: The box theme? #captain-theme
ai: Every box has a theme, Commander. Didn't you know? #ai-theme
choice
Open the sentient box #opt-open-sentient
-> OpenSpaceBox
Try to communicate with it #opt-communicate
-> CommunicateBox
Back away slowly #opt-backaway
-> BackAway
beat InvestigateBox
You approach the box carefully. It's about a meter wide, floating serenely. #investigate-approach
captain: It's... beautiful. #captain-beautiful
ai: Commander, I wouldn't use that word for a box. #ai-beautiful
captain: You don't tell me what words to use for boxes, ARIA. #captain-words
choice
Open it immediately #opt-open-now
-> OpenSpaceBox
Scan it first #opt-scan-first if fuel > 10
fuel -= 10
-> ScanThenOpen
Poke it with a stick #opt-poke
-> PokeBox
beat PokeBox
You extend the ship's robotic arm and gently poke the box. #poke-arm
The box rotates 90 degrees and reveals a label: "THIS SIDE UP" #poke-label
ai: Commander, I don't think boxes in zero gravity have an "up". #ai-gravity
captain: Maybe the box defines its own reality. #captain-reality
ai: That's... philosophically concerning. #ai-philosophy
-> OpenSpaceBox
beat ScanThenOpen
ai: Scan reveals the box contains crystallized starlight and something called a "Nebula Shard". #scan-contents
ai: Also what appears to be a very small, very angry alien. #scan-alien
captain: Define "very small". #captain-small
ai: Approximately the size of a box. Commander, I think the alien IS a box. #ai-alienbox
-> OpenSpaceBox
beat IgnoreBox
You set course away from the mysterious box. #ignore-course
ai: Commander, the box is... following us. #ai-following
captain: What? #captain-what
ai: It appears to have tiny thrusters. And what I can only describe as determination. #ai-thrusters
captain: A determined box. My favorite kind. #captain-determined
discovered += 1
-> OpenSpaceBox
beat CommunicateBox
captain: ARIA, open a channel to the box. #comm-channel
ai: Commander, it's a box. #ai-itsabox
captain: I said open a channel. #captain-channel
ai: Channel open, Commander. #ai-channelopen
captain: Hello, box. I am Captain Nova. I come in peace. #captain-peace
The box vibrates gently. It seems pleased. #comm-vibrate
ai: Commander, the box just sent us coordinates. And what appears to be... a friendship request? #ai-friendship
trustAlien = true
-> OpenSpaceBox
beat BackAway
You engage reverse thrusters. The sentient box watches you leave. #back-thrusters
ai: Commander, the box looks... sad. #ai-sad
captain: Boxes don't have feelings, ARIA. #captain-feelings
ai: This one does. I'm reading elevated levels of box-sadness. #ai-boxsadness
captain: That's not a real metric. #captain-metric
ai: It is now. Box-sadness level: 7 out of 10. #ai-level
choice
Go back to the box #opt-goback
-> CommunicateBox
Leave for good #opt-leave
-> LeaveForGood
beat LeaveForGood
You leave the sentient box behind. #leave-behind
ai: I hope you're happy, Commander. #ai-happy
captain: I am. No weird space boxes for me today. #captain-happy
ai: Incoming transmission. It's... from the box. #ai-transmission
ai: It says "you'll be back. they always come back." #ai-message
captain: That's ominous. I love it. #captain-ominous
-> Ending
beat OpenSpaceBox
hasSpaceBox = true
discovered += 1
You carefully open the box. Light spills out from within. #open-light
if trustAlien
Inside you find a Nebula Shard, pulsing with friendly energy. #open-friendly
alien: Ah, a fellow box appreciator! Take this as a gift from my collection. #alien-gift
captain: The alien WAS the box? #captain-wasbox
alien: We prefer "Box-Americans". Just kidding. I'm Zx'thorp. #alien-intro
-> AlienEncounter
else
Inside you find a Nebula Shard and what appears to be a map to more boxes. #open-normal
ai: Commander, the map shows three more floating boxes in nearby sectors. #ai-morebox
captain: Three more boxes. This is either Christmas or a trap. #captain-christmas
ai: In space, those are often the same thing. #ai-same
-> SpaceExploration
beat AlienEncounter
alien: I've been collecting boxes across the galaxy for centuries. #alien-collecting
alien: Most species open them. Very few appreciate them. #alien-appreciate
captain: I appreciate boxes. Especially ones with good loot. #captain-loot
alien: "Loot." What a crude word for cosmic treasures. #alien-crude
choice
Ask about the galaxy's rarest box #opt-rare-box
alien: The Singularity Box. It contains everything and nothing. Also a coupon. #alien-singularity
-> Ending
Trade items with Zx'thorp #opt-trade
alien: I have a Space Helmet for you. In exchange, I want your sense of wonder. #alien-trade
captain: Deal. Wait-- #captain-deal
-> Ending
Challenge the alien to a box-opening contest #opt-contest
alien: You dare? No one out-opens Zx'thorp! #alien-dare
alien: ...fine. Best of three. You open first. #alien-contest
-> Ending
beat SpaceExploration
ai: Setting course for the nearest box. ETA: approximately one dramatic pause. #explore-course
You arrive at a cluster of three boxes, orbiting each other like a tiny solar system. #explore-cluster
captain: A box system. Literally a system of boxes. #captain-system
ai: The large one appears to contain the other two. It's boxes all the way down. #ai-alltheway
choice
Open the biggest box first #opt-big
You open the biggest box. Inside: two medium boxes and a note. #big-open
The note reads: "Congratulations! You've found the Box Nebula. Population: boxes." #big-note
-> Ending
Open all three at once #opt-all
captain: All three, ARIA. Maximum box velocity. #captain-velocity
ai: That's not a real measurement, but I admire your enthusiasm. #ai-enthusiasm
All three boxes burst open simultaneously. The contents mix together in zero gravity. #all-open
ai: Commander, the items are forming... a bigger box. #ai-biggerbox
captain: Of course they are. #captain-ofcourse
-> Ending
Leave them orbiting, they seem happy #opt-happy
captain: Let them orbit in peace. #captain-orbit
ai: A surprisingly philosophical choice, Commander. #ai-philosophical
ai: The boxes seem to orbit faster. I think they're happy. #ai-happy
-> Ending
beat Ending
ai: Adventure complete. Updating ship's log. #ending-complete
ai: Boxes encountered: $discovered. Box-related existential crises: 1. #ending-log
captain: Only one? We'll do better next time. #captain-next
ai: There's always next time, Commander. The universe is full of boxes. #ai-next
-> .

558
content/data/boxes.json Normal file
View file

@ -0,0 +1,558 @@
[
{
"id": "box_starter",
"nameKey": "box.starter",
"descriptionKey": "box.starter.desc",
"rarity": "Common",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": ["box_of_boxes"],
"rollCount": 0,
"entries": []
}
},
{
"id": "box_of_boxes",
"nameKey": "box.box_of_boxes",
"descriptionKey": "box.box_of_boxes.desc",
"rarity": "Common",
"isAutoOpen": true,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 1,
"entries": [
{"itemDefinitionId": "box_not_great", "weight": 10},
{"itemDefinitionId": "box_ok_tier", "weight": 5},
{"itemDefinitionId": "box_cool", "weight": 1},
{"itemDefinitionId": "box_epic", "weight": 1},
{"itemDefinitionId": "box_legendhair", "weight": 1},
{"itemDefinitionId": "box_legendary", "weight": 1},
{"itemDefinitionId": "box_adventure", "weight": 1},
{"itemDefinitionId": "box_style", "weight": 1},
{"itemDefinitionId": "box_improvement", "weight": 1, "condition": {"type": "ResourceAbove", "targetId": "any", "value": 0}},
{"itemDefinitionId": "box_supply", "weight": 1, "condition": {"type": "ResourceAbove", "targetId": "any", "value": 0}}
]
}
},
{
"id": "box_not_great",
"nameKey": "box.not_great",
"descriptionKey": "box.not_great.desc",
"rarity": "Common",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 1,
"entries": [
{"itemDefinitionId": "material_wood_raw", "weight": 5},
{"itemDefinitionId": "material_bronze_raw", "weight": 3},
{"itemDefinitionId": "food_ration", "weight": 4},
{"itemDefinitionId": "cosmetic_hair_short", "weight": 2},
{"itemDefinitionId": "cosmetic_eyes_brown", "weight": 2},
{"itemDefinitionId": "cosmetic_body_tshirt", "weight": 2},
{"itemDefinitionId": "cosmetic_legs_short", "weight": 2},
{"itemDefinitionId": "cosmetic_arms_regular", "weight": 2},
{"itemDefinitionId": "tint_light", "weight": 1},
{"itemDefinitionId": "tint_dark", "weight": 1}
]
}
},
{
"id": "box_ok_tier",
"nameKey": "box.ok_tier",
"descriptionKey": "box.ok_tier.desc",
"rarity": "Uncommon",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "material_iron_raw", "weight": 4},
{"itemDefinitionId": "material_bronze_ingot", "weight": 3},
{"itemDefinitionId": "health_potion_small", "weight": 4},
{"itemDefinitionId": "mana_crystal_small", "weight": 3},
{"itemDefinitionId": "gold_pouch", "weight": 3},
{"itemDefinitionId": "cosmetic_hair_long", "weight": 2},
{"itemDefinitionId": "cosmetic_eyes_blue", "weight": 2},
{"itemDefinitionId": "cosmetic_eyes_green", "weight": 2},
{"itemDefinitionId": "tint_cyan", "weight": 2},
{"itemDefinitionId": "tint_orange", "weight": 2},
{"itemDefinitionId": "box_meta", "weight": 1}
]
}
},
{
"id": "box_cool",
"nameKey": "box.cool",
"descriptionKey": "box.cool.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 3,
"entries": [
{"itemDefinitionId": "material_steel_raw", "weight": 3},
{"itemDefinitionId": "material_iron_ingot", "weight": 3},
{"itemDefinitionId": "health_potion_medium", "weight": 3},
{"itemDefinitionId": "mana_crystal_medium", "weight": 3},
{"itemDefinitionId": "stamina_drink", "weight": 3},
{"itemDefinitionId": "cosmetic_hair_ponytail", "weight": 2},
{"itemDefinitionId": "cosmetic_eyes_sunglasses", "weight": 2},
{"itemDefinitionId": "cosmetic_body_sexy", "weight": 2},
{"itemDefinitionId": "tint_purple", "weight": 2},
{"itemDefinitionId": "box_meta", "weight": 2},
{"itemDefinitionId": "lore_1", "weight": 1},
{"itemDefinitionId": "lore_2", "weight": 1}
]
}
},
{
"id": "box_epic",
"nameKey": "box.epic",
"descriptionKey": "box.epic.desc",
"rarity": "Epic",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": ["box_meta"],
"rollCount": 3,
"entries": [
{"itemDefinitionId": "material_titanium_raw", "weight": 2},
{"itemDefinitionId": "material_steel_ingot", "weight": 3},
{"itemDefinitionId": "health_potion_large", "weight": 2},
{"itemDefinitionId": "blood_vial", "weight": 2},
{"itemDefinitionId": "energy_cell", "weight": 2},
{"itemDefinitionId": "oxygen_tank", "weight": 2},
{"itemDefinitionId": "cosmetic_hair_cyberpunk", "weight": 2},
{"itemDefinitionId": "cosmetic_eyes_pilotglasses", "weight": 2},
{"itemDefinitionId": "cosmetic_body_suit", "weight": 2},
{"itemDefinitionId": "cosmetic_legs_pegleg", "weight": 2},
{"itemDefinitionId": "tint_neon", "weight": 2},
{"itemDefinitionId": "tint_silver", "weight": 2},
{"itemDefinitionId": "lore_3", "weight": 1},
{"itemDefinitionId": "lore_4", "weight": 1},
{"itemDefinitionId": "lore_5", "weight": 1},
{"itemDefinitionId": "box_adventure", "weight": 1}
]
}
},
{
"id": "box_legendhair",
"nameKey": "box.legendhair",
"descriptionKey": "box.legendhair.desc",
"rarity": "Legendary",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": ["cosmetic_hair_stardust"],
"rollCount": 1,
"entries": [
{"itemDefinitionId": "cosmetic_hair_fire", "weight": 3},
{"itemDefinitionId": "cosmetic_hair_cyberpunk", "weight": 3},
{"itemDefinitionId": "tint_rainbow", "weight": 2},
{"itemDefinitionId": "tint_gold", "weight": 2}
]
}
},
{
"id": "box_legendary",
"nameKey": "box.legendary",
"descriptionKey": "box.legendary.desc",
"rarity": "Legendary",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 4,
"entries": [
{"itemDefinitionId": "material_diamond_raw", "weight": 3},
{"itemDefinitionId": "material_titanium_ingot", "weight": 3},
{"itemDefinitionId": "material_carbonfiber_raw", "weight": 3},
{"itemDefinitionId": "cosmetic_body_armored", "weight": 2},
{"itemDefinitionId": "cosmetic_body_robotic", "weight": 1},
{"itemDefinitionId": "cosmetic_eyes_cybernetic", "weight": 2},
{"itemDefinitionId": "cosmetic_legs_rocketboots", "weight": 2},
{"itemDefinitionId": "cosmetic_legs_tentacles", "weight": 1},
{"itemDefinitionId": "cosmetic_arms_mechanical", "weight": 2},
{"itemDefinitionId": "cosmetic_arms_wings", "weight": 1},
{"itemDefinitionId": "tint_void", "weight": 1},
{"itemDefinitionId": "tint_rainbow", "weight": 1},
{"itemDefinitionId": "lore_6", "weight": 1},
{"itemDefinitionId": "lore_10", "weight": 1},
{"itemDefinitionId": "box_meta", "weight": 2},
{"itemDefinitionId": "box_black", "weight": 1}
]
}
},
{
"id": "box_meta",
"nameKey": "box.meta",
"descriptionKey": "box.meta.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 1,
"entries": [
{"itemDefinitionId": "meta_colors", "weight": 5},
{"itemDefinitionId": "meta_extended_colors", "weight": 3},
{"itemDefinitionId": "meta_arrows", "weight": 4},
{"itemDefinitionId": "meta_animation", "weight": 4},
{"itemDefinitionId": "meta_inventory", "weight": 3},
{"itemDefinitionId": "meta_resources", "weight": 3},
{"itemDefinitionId": "meta_stats", "weight": 3},
{"itemDefinitionId": "meta_portrait", "weight": 2},
{"itemDefinitionId": "meta_chat", "weight": 2},
{"itemDefinitionId": "meta_layout", "weight": 1},
{"itemDefinitionId": "meta_shortcuts", "weight": 3},
{"itemDefinitionId": "meta_crafting", "weight": 2},
{"itemDefinitionId": "meta_resource_health", "weight": 2},
{"itemDefinitionId": "meta_resource_mana", "weight": 2},
{"itemDefinitionId": "meta_resource_food", "weight": 2},
{"itemDefinitionId": "meta_resource_gold", "weight": 2},
{"itemDefinitionId": "meta_resource_stamina", "weight": 1},
{"itemDefinitionId": "meta_resource_blood", "weight": 1},
{"itemDefinitionId": "meta_resource_oxygen", "weight": 1},
{"itemDefinitionId": "meta_resource_energy", "weight": 1},
{"itemDefinitionId": "meta_stat_strength", "weight": 1},
{"itemDefinitionId": "meta_stat_intelligence", "weight": 1},
{"itemDefinitionId": "meta_stat_luck", "weight": 1},
{"itemDefinitionId": "meta_stat_charisma", "weight": 1},
{"itemDefinitionId": "meta_font_consolas", "weight": 2},
{"itemDefinitionId": "meta_font_firetruc", "weight": 1},
{"itemDefinitionId": "meta_font_jetbrains", "weight": 2},
{"itemDefinitionId": "box_meta", "weight": 3}
]
}
},
{
"id": "box_style",
"nameKey": "box.style",
"descriptionKey": "box.style.desc",
"rarity": "Uncommon",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "cosmetic_hair_short", "weight": 3},
{"itemDefinitionId": "cosmetic_hair_long", "weight": 3},
{"itemDefinitionId": "cosmetic_hair_ponytail", "weight": 2},
{"itemDefinitionId": "cosmetic_hair_braided", "weight": 2},
{"itemDefinitionId": "cosmetic_eyes_blue", "weight": 3},
{"itemDefinitionId": "cosmetic_eyes_green", "weight": 3},
{"itemDefinitionId": "cosmetic_eyes_redorange", "weight": 2},
{"itemDefinitionId": "cosmetic_eyes_sunglasses", "weight": 2},
{"itemDefinitionId": "cosmetic_body_tshirt", "weight": 3},
{"itemDefinitionId": "cosmetic_body_sexy", "weight": 2},
{"itemDefinitionId": "cosmetic_legs_short", "weight": 3},
{"itemDefinitionId": "cosmetic_legs_panty", "weight": 2},
{"itemDefinitionId": "cosmetic_arms_regular", "weight": 3},
{"itemDefinitionId": "tint_cyan", "weight": 2},
{"itemDefinitionId": "tint_orange", "weight": 2},
{"itemDefinitionId": "tint_purple", "weight": 2},
{"itemDefinitionId": "tint_warmpink", "weight": 2},
{"itemDefinitionId": "cosmetic_gender_error", "weight": 1}
]
}
},
{
"id": "box_adventure",
"nameKey": "box.adventure",
"descriptionKey": "box.adventure.desc",
"rarity": "Rare",
"isAutoOpen": true,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 1,
"entries": [
{"itemDefinitionId": "box_adventure_space", "weight": 1},
{"itemDefinitionId": "box_adventure_medieval", "weight": 1},
{"itemDefinitionId": "box_adventure_pirate", "weight": 1},
{"itemDefinitionId": "box_adventure_contemporary", "weight": 1},
{"itemDefinitionId": "box_adventure_sentimental", "weight": 1},
{"itemDefinitionId": "box_adventure_prehistoric", "weight": 1},
{"itemDefinitionId": "box_adventure_cosmic", "weight": 1},
{"itemDefinitionId": "box_adventure_microscopic", "weight": 1},
{"itemDefinitionId": "box_adventure_darkfantasy", "weight": 1}
]
}
},
{
"id": "box_adventure_space",
"nameKey": "box.adventure.space",
"descriptionKey": "box.adventure.space.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "space_badge", "weight": 3},
{"itemDefinitionId": "space_phone", "weight": 3},
{"itemDefinitionId": "space_coordinates", "weight": 3},
{"itemDefinitionId": "space_key", "weight": 2},
{"itemDefinitionId": "space_map", "weight": 2},
{"itemDefinitionId": "oxygen_tank", "weight": 3},
{"itemDefinitionId": "energy_cell", "weight": 2}
]
}
},
{
"id": "box_adventure_medieval",
"nameKey": "box.adventure.medieval",
"descriptionKey": "box.adventure.medieval.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "medieval_crest", "weight": 3},
{"itemDefinitionId": "medieval_scroll", "weight": 3},
{"itemDefinitionId": "medieval_seal", "weight": 2},
{"itemDefinitionId": "medieval_key", "weight": 3},
{"itemDefinitionId": "mysterious_key", "weight": 2},
{"itemDefinitionId": "health_potion_medium", "weight": 3}
]
}
},
{
"id": "box_adventure_pirate",
"nameKey": "box.adventure.pirate",
"descriptionKey": "box.adventure.pirate.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": ["pirate_map"],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "pirate_compass", "weight": 3},
{"itemDefinitionId": "pirate_feather", "weight": 4},
{"itemDefinitionId": "pirate_rum", "weight": 3},
{"itemDefinitionId": "pirate_key", "weight": 2},
{"itemDefinitionId": "mysterious_key", "weight": 2},
{"itemDefinitionId": "gold_pouch", "weight": 4}
]
}
},
{
"id": "box_adventure_contemporary",
"nameKey": "box.adventure.contemporary",
"descriptionKey": "box.adventure.contemporary.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "contemporary_phone", "weight": 3},
{"itemDefinitionId": "contemporary_card", "weight": 3},
{"itemDefinitionId": "contemporary_usb", "weight": 2},
{"itemDefinitionId": "contemporary_key", "weight": 3},
{"itemDefinitionId": "contemporary_badge", "weight": 3},
{"itemDefinitionId": "mysterious_key", "weight": 2}
]
}
},
{
"id": "box_adventure_sentimental",
"nameKey": "box.adventure.sentimental",
"descriptionKey": "box.adventure.sentimental.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "sentimental_letter", "weight": 3},
{"itemDefinitionId": "sentimental_flower", "weight": 4},
{"itemDefinitionId": "sentimental_teddy", "weight": 3},
{"itemDefinitionId": "sentimental_phone", "weight": 3},
{"itemDefinitionId": "health_potion_small", "weight": 2}
]
}
},
{
"id": "box_adventure_prehistoric",
"nameKey": "box.adventure.prehistoric",
"descriptionKey": "box.adventure.prehistoric.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "prehistoric_tooth", "weight": 4},
{"itemDefinitionId": "prehistoric_amber", "weight": 3},
{"itemDefinitionId": "prehistoric_fossil", "weight": 2},
{"itemDefinitionId": "material_wood_raw", "weight": 4},
{"itemDefinitionId": "food_ration", "weight": 4}
]
}
},
{
"id": "box_adventure_cosmic",
"nameKey": "box.adventure.cosmic",
"descriptionKey": "box.adventure.cosmic.desc",
"rarity": "Epic",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "cosmic_shard", "weight": 4},
{"itemDefinitionId": "cosmic_crystal", "weight": 3},
{"itemDefinitionId": "cosmic_core", "weight": 1},
{"itemDefinitionId": "energy_cell", "weight": 3},
{"itemDefinitionId": "tint_void", "weight": 1}
]
}
},
{
"id": "box_adventure_microscopic",
"nameKey": "box.adventure.microscopic",
"descriptionKey": "box.adventure.microscopic.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "microscopic_bacteria", "weight": 4},
{"itemDefinitionId": "microscopic_dna", "weight": 3},
{"itemDefinitionId": "microscopic_prion", "weight": 2},
{"itemDefinitionId": "mana_crystal_small", "weight": 3},
{"itemDefinitionId": "health_potion_small", "weight": 3}
]
}
},
{
"id": "box_adventure_darkfantasy",
"nameKey": "box.adventure.darkfantasy",
"descriptionKey": "box.adventure.darkfantasy.desc",
"rarity": "Epic",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "darkfantasy_ring", "weight": 3},
{"itemDefinitionId": "darkfantasy_grimoire", "weight": 2},
{"itemDefinitionId": "darkfantasy_gem", "weight": 1},
{"itemDefinitionId": "darkfantasy_key", "weight": 3},
{"itemDefinitionId": "mysterious_key", "weight": 2},
{"itemDefinitionId": "blood_vial", "weight": 3}
]
}
},
{
"id": "box_improvement",
"nameKey": "box.improvement",
"descriptionKey": "box.improvement.desc",
"rarity": "Uncommon",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 1,
"entries": [
{"itemDefinitionId": "resource_max_health", "weight": 3},
{"itemDefinitionId": "resource_max_mana", "weight": 3},
{"itemDefinitionId": "resource_max_food", "weight": 3},
{"itemDefinitionId": "resource_max_stamina", "weight": 3},
{"itemDefinitionId": "resource_max_gold", "weight": 2},
{"itemDefinitionId": "resource_max_blood", "weight": 1},
{"itemDefinitionId": "resource_max_oxygen", "weight": 1},
{"itemDefinitionId": "resource_max_energy", "weight": 1}
]
}
},
{
"id": "box_supply",
"nameKey": "box.supply",
"descriptionKey": "box.supply.desc",
"rarity": "Common",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "health_potion_small", "weight": 5},
{"itemDefinitionId": "mana_crystal_small", "weight": 4},
{"itemDefinitionId": "food_ration", "weight": 5},
{"itemDefinitionId": "stamina_drink", "weight": 4},
{"itemDefinitionId": "gold_pouch", "weight": 4},
{"itemDefinitionId": "blood_vial", "weight": 1},
{"itemDefinitionId": "oxygen_tank", "weight": 1},
{"itemDefinitionId": "energy_cell", "weight": 1}
]
}
},
{
"id": "box_black",
"nameKey": "box.black",
"descriptionKey": "box.black.desc",
"rarity": "Mythic",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 3,
"entries": [
{"itemDefinitionId": "mysterious_key", "weight": 3},
{"itemDefinitionId": "lore_10", "weight": 2},
{"itemDefinitionId": "cosmetic_gender_error", "weight": 1},
{"itemDefinitionId": "tint_void", "weight": 2},
{"itemDefinitionId": "material_diamond_raw", "weight": 2},
{"itemDefinitionId": "material_diamond_gem", "weight": 1},
{"itemDefinitionId": "cosmic_core", "weight": 1},
{"itemDefinitionId": "darkfantasy_gem", "weight": 1},
{"itemDefinitionId": "box_meta", "weight": 2},
{"itemDefinitionId": "box_cookie", "weight": 2}
]
}
},
{
"id": "box_story",
"nameKey": "box.story",
"descriptionKey": "box.story.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": [],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "lore_1", "weight": 2},
{"itemDefinitionId": "lore_2", "weight": 2},
{"itemDefinitionId": "lore_3", "weight": 2},
{"itemDefinitionId": "lore_4", "weight": 2},
{"itemDefinitionId": "lore_5", "weight": 2},
{"itemDefinitionId": "lore_6", "weight": 1},
{"itemDefinitionId": "lore_7", "weight": 2},
{"itemDefinitionId": "lore_8", "weight": 2},
{"itemDefinitionId": "lore_9", "weight": 2},
{"itemDefinitionId": "lore_10", "weight": 1}
]
}
},
{
"id": "box_music",
"nameKey": "box.music",
"descriptionKey": "box.music.desc",
"rarity": "Rare",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": ["music_melody"],
"rollCount": 0,
"entries": []
}
},
{
"id": "box_cookie",
"nameKey": "box.cookie",
"descriptionKey": "box.cookie.desc",
"rarity": "Uncommon",
"isAutoOpen": false,
"lootTable": {
"guaranteedRolls": ["cookie_fortune"],
"rollCount": 0,
"entries": []
}
}
]

View file

@ -0,0 +1,52 @@
[
{
"id": "key_chest_auto",
"requiredItemTags": ["Key"],
"requiredItemIds": null,
"resultType": "OpenBox",
"resultData": null,
"isAutomatic": true,
"priority": 10,
"descriptionKey": "interaction.key_chest"
},
{
"id": "badge_adventure_space",
"requiredItemTags": ["Badge", "Space"],
"requiredItemIds": null,
"resultType": "Unlock",
"resultData": "adventure:Space",
"isAutomatic": true,
"priority": 5,
"descriptionKey": "adventure.start"
},
{
"id": "phone_character_encounter",
"requiredItemTags": ["PhoneNumber"],
"requiredItemIds": null,
"resultType": "Unlock",
"resultData": "character",
"isAutomatic": false,
"priority": 3,
"descriptionKey": "interaction.phone_call"
},
{
"id": "coordinates_map_combine",
"requiredItemTags": ["Coordinates"],
"requiredItemIds": ["space_map"],
"resultType": "Combine",
"resultData": "adventure_unlock:Space",
"isAutomatic": true,
"priority": 8,
"descriptionKey": "interaction.map_coordinates"
},
{
"id": "pirate_map_compass",
"requiredItemTags": [],
"requiredItemIds": ["pirate_map", "pirate_compass"],
"resultType": "Unlock",
"resultData": "adventure:Pirate",
"isAutomatic": true,
"priority": 8,
"descriptionKey": "interaction.treasure_located"
}
]

153
content/data/items.json Normal file
View file

@ -0,0 +1,153 @@
[
{"id": "meta_colors", "nameKey": "meta.colors", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "TextColors"},
{"id": "meta_extended_colors", "nameKey": "meta.extended_colors", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "ExtendedColors"},
{"id": "meta_arrows", "nameKey": "meta.arrows", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "ArrowKeySelection"},
{"id": "meta_inventory", "nameKey": "meta.inventory", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "InventoryPanel"},
{"id": "meta_resources", "nameKey": "meta.resources", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "ResourcePanel"},
{"id": "meta_stats", "nameKey": "meta.stats", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "StatsPanel"},
{"id": "meta_portrait", "nameKey": "meta.portrait", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "PortraitPanel"},
{"id": "meta_chat", "nameKey": "meta.chat", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "ChatPanel"},
{"id": "meta_layout", "nameKey": "meta.layout", "category": "Meta", "rarity": "Legendary", "tags": ["Meta"], "metaUnlock": "FullLayout"},
{"id": "meta_shortcuts", "nameKey": "meta.shortcuts", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "KeyboardShortcuts"},
{"id": "meta_animation", "nameKey": "meta.animation", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "BoxAnimation"},
{"id": "meta_crafting", "nameKey": "meta.crafting", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "CraftingPanel"},
{"id": "meta_resource_health", "nameKey": "resource.health", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Health"},
{"id": "meta_resource_mana", "nameKey": "resource.mana", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Mana"},
{"id": "meta_resource_food", "nameKey": "resource.food", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Food"},
{"id": "meta_resource_stamina", "nameKey": "resource.stamina", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Stamina"},
{"id": "meta_resource_blood", "nameKey": "resource.blood", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Blood"},
{"id": "meta_resource_gold", "nameKey": "resource.gold", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Gold"},
{"id": "meta_resource_oxygen", "nameKey": "resource.oxygen", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Oxygen"},
{"id": "meta_resource_energy", "nameKey": "resource.energy", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Energy"},
{"id": "meta_stat_strength", "nameKey": "stat.strength", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Strength"},
{"id": "meta_stat_intelligence", "nameKey": "stat.intelligence", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Intelligence"},
{"id": "meta_stat_luck", "nameKey": "stat.luck", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Luck"},
{"id": "meta_stat_charisma", "nameKey": "stat.charisma", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Charisma"},
{"id": "meta_stat_dexterity", "nameKey": "stat.dexterity", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Dexterity"},
{"id": "meta_stat_wisdom", "nameKey": "stat.wisdom", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Wisdom"},
{"id": "meta_font_consolas", "nameKey": "Consolas", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "Font"], "fontStyle": "Consolas"},
{"id": "meta_font_firetruc", "nameKey": "Firetruc", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "Font"], "fontStyle": "Firetruc"},
{"id": "meta_font_jetbrains", "nameKey": "JetBrains Mono", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "Font"], "fontStyle": "Jetbrains"},
{"id": "cosmetic_hair_short", "nameKey": "cosmetic.hair.short", "category": "Cosmetic", "rarity": "Common", "tags": ["Cosmetic"], "cosmeticSlot": "Hair", "cosmeticValue": "Short"},
{"id": "cosmetic_hair_long", "nameKey": "cosmetic.hair.long", "category": "Cosmetic", "rarity": "Common", "tags": ["Cosmetic"], "cosmeticSlot": "Hair", "cosmeticValue": "Long"},
{"id": "cosmetic_hair_ponytail", "nameKey": "cosmetic.hair.ponytail", "category": "Cosmetic", "rarity": "Uncommon", "tags": ["Cosmetic"], "cosmeticSlot": "Hair", "cosmeticValue": "Ponytail"},
{"id": "cosmetic_hair_braided", "nameKey": "cosmetic.hair.braided", "category": "Cosmetic", "rarity": "Uncommon", "tags": ["Cosmetic"], "cosmeticSlot": "Hair", "cosmeticValue": "Braided"},
{"id": "cosmetic_hair_cyberpunk", "nameKey": "cosmetic.hair.cyberpunk", "category": "Cosmetic", "rarity": "Rare", "tags": ["Cosmetic"], "cosmeticSlot": "Hair", "cosmeticValue": "Cyberpunk"},
{"id": "cosmetic_hair_fire", "nameKey": "cosmetic.hair.fire", "category": "Cosmetic", "rarity": "Epic", "tags": ["Cosmetic"], "cosmeticSlot": "Hair", "cosmeticValue": "Fire"},
{"id": "cosmetic_hair_stardust", "nameKey": "cosmetic.hair.stardust", "category": "Cosmetic", "rarity": "Legendary", "tags": ["Cosmetic"], "cosmeticSlot": "Hair", "cosmeticValue": "StardustLegendary"},
{"id": "cosmetic_eyes_blue", "nameKey": "cosmetic.eyes.blue", "category": "Cosmetic", "rarity": "Common", "tags": ["Cosmetic"], "cosmeticSlot": "Eyes", "cosmeticValue": "Blue"},
{"id": "cosmetic_eyes_green", "nameKey": "cosmetic.eyes.green", "category": "Cosmetic", "rarity": "Common", "tags": ["Cosmetic"], "cosmeticSlot": "Eyes", "cosmeticValue": "Green"},
{"id": "cosmetic_eyes_redorange", "nameKey": "cosmetic.eyes.redorange", "category": "Cosmetic", "rarity": "Uncommon", "tags": ["Cosmetic"], "cosmeticSlot": "Eyes", "cosmeticValue": "RedOrange"},
{"id": "cosmetic_eyes_brown", "nameKey": "cosmetic.eyes.brown", "category": "Cosmetic", "rarity": "Common", "tags": ["Cosmetic"], "cosmeticSlot": "Eyes", "cosmeticValue": "Brown"},
{"id": "cosmetic_eyes_sunglasses", "nameKey": "cosmetic.eyes.sunglasses", "category": "Cosmetic", "rarity": "Uncommon", "tags": ["Cosmetic"], "cosmeticSlot": "Eyes", "cosmeticValue": "Sunglasses"},
{"id": "cosmetic_eyes_pilotglasses", "nameKey": "cosmetic.eyes.pilotglasses", "category": "Cosmetic", "rarity": "Rare", "tags": ["Cosmetic"], "cosmeticSlot": "Eyes", "cosmeticValue": "PilotGlasses"},
{"id": "cosmetic_eyes_cybernetic", "nameKey": "cosmetic.eyes.cybernetic", "category": "Cosmetic", "rarity": "Epic", "tags": ["Cosmetic"], "cosmeticSlot": "Eyes", "cosmeticValue": "CyberneticEyes"},
{"id": "cosmetic_eyes_magician", "nameKey": "cosmetic.eyes.magician", "category": "Cosmetic", "rarity": "Rare", "tags": ["Cosmetic"], "cosmeticSlot": "Eyes", "cosmeticValue": "MagicianGlasses"},
{"id": "cosmetic_body_tshirt", "nameKey": "cosmetic.body.regulartshirt", "category": "Cosmetic", "rarity": "Common", "tags": ["Cosmetic"], "cosmeticSlot": "Body", "cosmeticValue": "RegularTShirt"},
{"id": "cosmetic_body_sexy", "nameKey": "cosmetic.body.sexytshirt", "category": "Cosmetic", "rarity": "Uncommon", "tags": ["Cosmetic"], "cosmeticSlot": "Body", "cosmeticValue": "SexyTShirt"},
{"id": "cosmetic_body_suit", "nameKey": "cosmetic.body.suit", "category": "Cosmetic", "rarity": "Rare", "tags": ["Cosmetic"], "cosmeticSlot": "Body", "cosmeticValue": "Suit"},
{"id": "cosmetic_body_armored", "nameKey": "cosmetic.body.armored", "category": "Cosmetic", "rarity": "Epic", "tags": ["Cosmetic"], "cosmeticSlot": "Body", "cosmeticValue": "Armored"},
{"id": "cosmetic_body_robotic", "nameKey": "cosmetic.body.robotic", "category": "Cosmetic", "rarity": "Legendary", "tags": ["Cosmetic"], "cosmeticSlot": "Body", "cosmeticValue": "Robotic"},
{"id": "cosmetic_legs_short", "nameKey": "cosmetic.legs.short", "category": "Cosmetic", "rarity": "Common", "tags": ["Cosmetic"], "cosmeticSlot": "Legs", "cosmeticValue": "Short"},
{"id": "cosmetic_legs_panty", "nameKey": "cosmetic.legs.panty", "category": "Cosmetic", "rarity": "Uncommon", "tags": ["Cosmetic"], "cosmeticSlot": "Legs", "cosmeticValue": "Panty"},
{"id": "cosmetic_legs_rocketboots", "nameKey": "cosmetic.legs.rocketboots", "category": "Cosmetic", "rarity": "Epic", "tags": ["Cosmetic"], "cosmeticSlot": "Legs", "cosmeticValue": "RocketBoots"},
{"id": "cosmetic_legs_pegleg", "nameKey": "cosmetic.legs.pegleg", "category": "Cosmetic", "rarity": "Rare", "tags": ["Cosmetic"], "cosmeticSlot": "Legs", "cosmeticValue": "PegLeg"},
{"id": "cosmetic_legs_tentacles", "nameKey": "cosmetic.legs.tentacles", "category": "Cosmetic", "rarity": "Legendary", "tags": ["Cosmetic"], "cosmeticSlot": "Legs", "cosmeticValue": "Tentacles"},
{"id": "cosmetic_arms_regular", "nameKey": "cosmetic.arms.regular", "category": "Cosmetic", "rarity": "Common", "tags": ["Cosmetic"], "cosmeticSlot": "Arms", "cosmeticValue": "Regular"},
{"id": "cosmetic_arms_mechanical", "nameKey": "cosmetic.arms.mechanical", "category": "Cosmetic", "rarity": "Epic", "tags": ["Cosmetic"], "cosmeticSlot": "Arms", "cosmeticValue": "Mechanical"},
{"id": "cosmetic_arms_wings", "nameKey": "cosmetic.arms.wings", "category": "Cosmetic", "rarity": "Legendary", "tags": ["Cosmetic"], "cosmeticSlot": "Arms", "cosmeticValue": "Wings"},
{"id": "cosmetic_arms_extrapair", "nameKey": "cosmetic.arms.extrapair", "category": "Cosmetic", "rarity": "Epic", "tags": ["Cosmetic"], "cosmeticSlot": "Arms", "cosmeticValue": "ExtraPair"},
{"id": "cosmetic_gender_error", "nameKey": "cosmetic.gender_error", "category": "Cosmetic", "rarity": "Mythic", "tags": ["Cosmetic", "Error", "Easter Egg"]},
{"id": "tint_cyan", "nameKey": "tint.cyan", "category": "Cosmetic", "rarity": "Common", "tags": ["Tint"], "tintColor": "Cyan"},
{"id": "tint_orange", "nameKey": "tint.orange", "category": "Cosmetic", "rarity": "Common", "tags": ["Tint"], "tintColor": "Orange"},
{"id": "tint_purple", "nameKey": "tint.purple", "category": "Cosmetic", "rarity": "Uncommon", "tags": ["Tint"], "tintColor": "Purple"},
{"id": "tint_warmpink", "nameKey": "tint.warmpink", "category": "Cosmetic", "rarity": "Uncommon", "tags": ["Tint"], "tintColor": "WarmPink"},
{"id": "tint_light", "nameKey": "tint.light", "category": "Cosmetic", "rarity": "Common", "tags": ["Tint"], "tintColor": "Light"},
{"id": "tint_dark", "nameKey": "tint.dark", "category": "Cosmetic", "rarity": "Common", "tags": ["Tint"], "tintColor": "Dark"},
{"id": "tint_rainbow", "nameKey": "tint.rainbow", "category": "Cosmetic", "rarity": "Legendary", "tags": ["Tint"], "tintColor": "Rainbow"},
{"id": "tint_neon", "nameKey": "tint.neon", "category": "Cosmetic", "rarity": "Rare", "tags": ["Tint"], "tintColor": "Neon"},
{"id": "tint_silver", "nameKey": "tint.silver", "category": "Cosmetic", "rarity": "Rare", "tags": ["Tint"], "tintColor": "Silver"},
{"id": "tint_gold", "nameKey": "tint.gold", "category": "Cosmetic", "rarity": "Epic", "tags": ["Tint"], "tintColor": "Gold"},
{"id": "tint_void", "nameKey": "tint.void", "category": "Cosmetic", "rarity": "Legendary", "tags": ["Tint"], "tintColor": "Void"},
{"id": "health_potion_small", "nameKey": "item.health_potion_small", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Health", "resourceAmount": 10},
{"id": "health_potion_medium", "nameKey": "item.health_potion_medium", "category": "Consumable", "rarity": "Uncommon", "tags": ["Consumable"], "resourceType": "Health", "resourceAmount": 25},
{"id": "health_potion_large", "nameKey": "item.health_potion_large", "category": "Consumable", "rarity": "Rare", "tags": ["Consumable"], "resourceType": "Health", "resourceAmount": 50},
{"id": "mana_crystal_small", "nameKey": "item.mana_crystal_small", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Mana", "resourceAmount": 10},
{"id": "mana_crystal_medium", "nameKey": "item.mana_crystal_medium", "category": "Consumable", "rarity": "Uncommon", "tags": ["Consumable"], "resourceType": "Mana", "resourceAmount": 25},
{"id": "food_ration", "nameKey": "item.food_ration", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Food", "resourceAmount": 15},
{"id": "stamina_drink", "nameKey": "item.stamina_drink", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Stamina", "resourceAmount": 20},
{"id": "blood_vial", "nameKey": "item.blood_vial", "category": "Consumable", "rarity": "Rare", "tags": ["Consumable"], "resourceType": "Blood", "resourceAmount": 5},
{"id": "gold_pouch", "nameKey": "item.gold_pouch", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Gold", "resourceAmount": 50},
{"id": "oxygen_tank", "nameKey": "item.oxygen_tank", "category": "Consumable", "rarity": "Uncommon", "tags": ["Consumable"], "resourceType": "Oxygen", "resourceAmount": 30},
{"id": "energy_cell", "nameKey": "item.energy_cell", "category": "Consumable", "rarity": "Uncommon", "tags": ["Consumable"], "resourceType": "Energy", "resourceAmount": 20},
{"id": "space_badge", "nameKey": "item.space.badge", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Space"], "adventureTheme": "Space"},
{"id": "space_phone", "nameKey": "item.space.phone", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Space", "PhoneNumber"], "adventureTheme": "Space"},
{"id": "space_key", "nameKey": "item.space.key", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Space", "Key"], "adventureTheme": "Space"},
{"id": "space_map", "nameKey": "item.space.map", "category": "Map", "rarity": "Epic", "tags": ["Adventure", "Space"], "adventureTheme": "Space"},
{"id": "space_coordinates", "nameKey": "item.space.coordinates", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Space", "Coordinates"], "adventureTheme": "Space"},
{"id": "medieval_crest", "nameKey": "item.medieval.crest", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Medieval"], "adventureTheme": "Medieval"},
{"id": "medieval_sword", "nameKey": "item.medieval.sword", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Medieval"], "adventureTheme": "Medieval"},
{"id": "medieval_scroll", "nameKey": "item.medieval.scroll", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Medieval"], "adventureTheme": "Medieval"},
{"id": "medieval_seal", "nameKey": "item.medieval.seal", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Medieval"], "adventureTheme": "Medieval"},
{"id": "medieval_key", "nameKey": "item.medieval.key", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Medieval", "Key"], "adventureTheme": "Medieval"},
{"id": "pirate_map", "nameKey": "item.pirate.map", "category": "Map", "rarity": "Epic", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate"},
{"id": "pirate_compass", "nameKey": "item.pirate.compass", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate"},
{"id": "pirate_feather", "nameKey": "item.pirate.feather", "category": "AdventureToken", "rarity": "Common", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate"},
{"id": "pirate_rum", "nameKey": "item.pirate.rum", "category": "Consumable", "rarity": "Uncommon", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate", "resourceType": "Stamina", "resourceAmount": 30},
{"id": "pirate_key", "nameKey": "item.pirate.key", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Pirate", "Key"], "adventureTheme": "Pirate"},
{"id": "contemporary_phone", "nameKey": "item.contemporary.phone", "category": "AdventureToken", "rarity": "Common", "tags": ["Adventure", "Contemporary", "PhoneNumber"], "adventureTheme": "Contemporary"},
{"id": "contemporary_card", "nameKey": "item.contemporary.card", "category": "AdventureToken", "rarity": "Uncommon", "tags": ["Adventure", "Contemporary"], "adventureTheme": "Contemporary"},
{"id": "contemporary_usb", "nameKey": "item.contemporary.usb", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Contemporary"], "adventureTheme": "Contemporary"},
{"id": "contemporary_key", "nameKey": "item.contemporary.key", "category": "Key", "rarity": "Uncommon", "tags": ["Adventure", "Contemporary", "Key"], "adventureTheme": "Contemporary"},
{"id": "contemporary_badge", "nameKey": "item.contemporary.badge", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Contemporary", "Badge"], "adventureTheme": "Contemporary"},
{"id": "sentimental_letter", "nameKey": "item.sentimental.letter", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Sentimental"], "adventureTheme": "Sentimental"},
{"id": "sentimental_flower", "nameKey": "item.sentimental.flower", "category": "AdventureToken", "rarity": "Common", "tags": ["Adventure", "Sentimental"], "adventureTheme": "Sentimental"},
{"id": "sentimental_teddy", "nameKey": "item.sentimental.teddy", "category": "AdventureToken", "rarity": "Uncommon", "tags": ["Adventure", "Sentimental"], "adventureTheme": "Sentimental"},
{"id": "sentimental_phone", "nameKey": "item.sentimental.phone", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Sentimental", "PhoneNumber"], "adventureTheme": "Sentimental"},
{"id": "prehistoric_tooth", "nameKey": "item.prehistoric.tooth", "category": "AdventureToken", "rarity": "Uncommon", "tags": ["Adventure", "Prehistoric"], "adventureTheme": "Prehistoric"},
{"id": "prehistoric_amber", "nameKey": "item.prehistoric.amber", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Prehistoric"], "adventureTheme": "Prehistoric"},
{"id": "prehistoric_fossil", "nameKey": "item.prehistoric.fossil", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Prehistoric"], "adventureTheme": "Prehistoric"},
{"id": "cosmic_shard", "nameKey": "item.cosmic.shard", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Cosmic"], "adventureTheme": "Cosmic"},
{"id": "cosmic_crystal", "nameKey": "item.cosmic.crystal", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Cosmic"], "adventureTheme": "Cosmic"},
{"id": "cosmic_core", "nameKey": "item.cosmic.core", "category": "AdventureToken", "rarity": "Legendary", "tags": ["Adventure", "Cosmic"], "adventureTheme": "Cosmic"},
{"id": "microscopic_bacteria", "nameKey": "item.microscopic.bacteria", "category": "AdventureToken", "rarity": "Uncommon", "tags": ["Adventure", "Microscopic"], "adventureTheme": "Microscopic"},
{"id": "microscopic_dna", "nameKey": "item.microscopic.dna", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Microscopic"], "adventureTheme": "Microscopic"},
{"id": "microscopic_prion", "nameKey": "item.microscopic.prion", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Microscopic"], "adventureTheme": "Microscopic"},
{"id": "darkfantasy_ring", "nameKey": "item.darkfantasy.ring", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "DarkFantasy"], "adventureTheme": "DarkFantasy"},
{"id": "darkfantasy_grimoire", "nameKey": "item.darkfantasy.grimoire", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "DarkFantasy"], "adventureTheme": "DarkFantasy"},
{"id": "darkfantasy_gem", "nameKey": "item.darkfantasy.gem", "category": "AdventureToken", "rarity": "Legendary", "tags": ["Adventure", "DarkFantasy"], "adventureTheme": "DarkFantasy"},
{"id": "darkfantasy_key", "nameKey": "item.darkfantasy.key", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "DarkFantasy", "Key"], "adventureTheme": "DarkFantasy"},
{"id": "mysterious_key", "nameKey": "item.mysterious_key", "descriptionKey": "item.mysterious_key.desc", "category": "Key", "rarity": "Rare", "tags": ["Key"]},
{"id": "lore_1", "nameKey": "lore.fragment_1", "category": "LoreFragment", "rarity": "Uncommon", "tags": ["Lore"]},
{"id": "lore_2", "nameKey": "lore.fragment_2", "category": "LoreFragment", "rarity": "Uncommon", "tags": ["Lore"]},
{"id": "lore_3", "nameKey": "lore.fragment_3", "category": "LoreFragment", "rarity": "Rare", "tags": ["Lore"]},
{"id": "lore_4", "nameKey": "lore.fragment_4", "category": "LoreFragment", "rarity": "Rare", "tags": ["Lore"]},
{"id": "lore_5", "nameKey": "lore.fragment_5", "category": "LoreFragment", "rarity": "Rare", "tags": ["Lore"]},
{"id": "lore_6", "nameKey": "lore.fragment_6", "category": "LoreFragment", "rarity": "Epic", "tags": ["Lore"]},
{"id": "lore_7", "nameKey": "lore.fragment_7", "category": "LoreFragment", "rarity": "Rare", "tags": ["Lore"]},
{"id": "lore_8", "nameKey": "lore.fragment_8", "category": "LoreFragment", "rarity": "Rare", "tags": ["Lore"]},
{"id": "lore_9", "nameKey": "lore.fragment_9", "category": "LoreFragment", "rarity": "Rare", "tags": ["Lore"]},
{"id": "lore_10", "nameKey": "lore.fragment_10", "category": "LoreFragment", "rarity": "Epic", "tags": ["Lore"]},
{"id": "material_wood_raw", "nameKey": "material.wood", "category": "Material", "rarity": "Common", "tags": ["Material"], "materialType": "Wood", "materialForm": "Raw"},
{"id": "material_wood_refined", "nameKey": "material.wood", "category": "Material", "rarity": "Common", "tags": ["Material"], "materialType": "Wood", "materialForm": "Refined"},
{"id": "material_wood_nail", "nameKey": "material.wood", "category": "Material", "rarity": "Common", "tags": ["Material"], "materialType": "Wood", "materialForm": "Nail"},
{"id": "material_bronze_raw", "nameKey": "material.bronze", "category": "Material", "rarity": "Common", "tags": ["Material"], "materialType": "Bronze", "materialForm": "Raw"},
{"id": "material_bronze_ingot", "nameKey": "material.bronze", "category": "Material", "rarity": "Uncommon", "tags": ["Material"], "materialType": "Bronze", "materialForm": "Ingot"},
{"id": "material_iron_raw", "nameKey": "material.iron", "category": "Material", "rarity": "Common", "tags": ["Material"], "materialType": "Iron", "materialForm": "Raw"},
{"id": "material_iron_ingot", "nameKey": "material.iron", "category": "Material", "rarity": "Uncommon", "tags": ["Material"], "materialType": "Iron", "materialForm": "Ingot"},
{"id": "material_steel_raw", "nameKey": "material.steel", "category": "Material", "rarity": "Uncommon", "tags": ["Material"], "materialType": "Steel", "materialForm": "Raw"},
{"id": "material_steel_ingot", "nameKey": "material.steel", "category": "Material", "rarity": "Rare", "tags": ["Material"], "materialType": "Steel", "materialForm": "Ingot"},
{"id": "material_titanium_raw", "nameKey": "material.titanium", "category": "Material", "rarity": "Rare", "tags": ["Material"], "materialType": "Titanium", "materialForm": "Raw"},
{"id": "material_titanium_ingot", "nameKey": "material.titanium", "category": "Material", "rarity": "Epic", "tags": ["Material"], "materialType": "Titanium", "materialForm": "Ingot"},
{"id": "material_diamond_raw", "nameKey": "material.diamond", "category": "Material", "rarity": "Epic", "tags": ["Material"], "materialType": "Diamond", "materialForm": "Raw"},
{"id": "material_diamond_gem", "nameKey": "material.diamond", "category": "Material", "rarity": "Legendary", "tags": ["Material"], "materialType": "Diamond", "materialForm": "Gem"},
{"id": "material_carbonfiber_raw", "nameKey": "material.carbonfiber", "category": "Material", "rarity": "Rare", "tags": ["Material"], "materialType": "CarbonFiber", "materialForm": "Raw"},
{"id": "material_carbonfiber_sheet", "nameKey": "material.carbonfiber", "category": "Material", "rarity": "Epic", "tags": ["Material"], "materialType": "CarbonFiber", "materialForm": "Sheet"}
]

339
content/strings/en.json Normal file
View file

@ -0,0 +1,339 @@
{
"game.title": "OPEN THE BOX",
"game.subtitle": "What's inside? Only one way to find out.",
"game.version": "v0.1.0",
"menu.new_game": "New Game",
"menu.load_game": "Load Game",
"menu.language": "Language",
"menu.quit": "Quit",
"menu.back": "Back",
"menu.continue": "Continue",
"menu.save": "Save Game",
"menu.settings": "Settings",
"action.open_box": "Open a box",
"action.inventory": "View inventory",
"action.craft": "Craft",
"action.adventure": "Go on an adventure",
"action.appearance": "Change appearance",
"action.save": "Save",
"action.quit": "Return to menu",
"prompt.name": "What is your name, brave box-opener?",
"prompt.choose_action": "What would you like to do?",
"prompt.choose_box": "Which box do you want to open?",
"prompt.choose_interaction": "Multiple interactions possible! Choose one:",
"prompt.press_key": "Press any key to continue...",
"box.opening": "Opening {0}...",
"box.found": "You found: {0}!",
"box.found_box": "Inside was... another box! {0}!",
"box.empty": "The box is empty! How philosophical.",
"box.no_boxes": "You have no boxes. How did you manage that?",
"box.auto_open": "{0} opens automatically!",
"box.starter": "Starter Box",
"box.starter.desc": "Your first box. The beginning of everything. Or nothing. Probably something though.",
"box.box_of_boxes": "Box of Boxes",
"box.box_of_boxes.desc": "A box that contains... boxes. It's boxes all the way down.",
"box.not_great": "Meh Box",
"box.not_great.desc": "It's not great. It's not terrible. It just... is.",
"box.ok_tier": "Okay-ish Box",
"box.ok_tier.desc": "Mediocrity has never looked so boxy.",
"box.cool": "Cool Box",
"box.cool.desc": "Now we're getting somewhere. Somewhere cool.",
"box.epic": "Epic Box",
"box.epic.desc": "The orchestra swells. The crowd gasps. It's... a box.",
"box.legendhair": "Legend'hair Box",
"box.legendhair.desc": "The most legendary hair you'll ever unbox. Hair today, legend tomorrow.",
"box.legendary": "Legendary Box",
"box.legendary.desc": "Legends speak of this box. They speak quietly though, it's a box.",
"box.adventure": "Adventure Box",
"box.adventure.desc": "Contains the key to adventure! Literally, sometimes.",
"box.style": "Style Box",
"box.style.desc": "Fashion is temporary. Style from a box is eternal.",
"box.improvement": "Improvement Box",
"box.improvement.desc": "You can always improve. Especially with boxes.",
"box.supply": "Supply Box",
"box.supply.desc": "Supplies! The lifeblood of any box-opening enthusiast.",
"box.meta": "Meta Box",
"box.meta.desc": "This box improves... the way you see boxes. How meta.",
"box.black": "Black Box",
"box.black.desc": "Nobody knows what's inside. Not even the box.",
"box.story": "Story Box",
"box.story.desc": "Every box has a story. This one more than most.",
"box.music": "Music Box",
"box.music.desc": "♪ Do do do do ♪ Box music is best music.",
"box.cookie": "Cookie Box",
"box.cookie.desc": "Fortune favors the bold. Also those who open boxes.",
"box.adventure.space": "Space Adventure Box",
"box.adventure.space.desc": "To infinity and beyond! (Box not included in infinity)",
"box.adventure.medieval": "Medieval Adventure Box",
"box.adventure.medieval.desc": "Hear ye, hear ye! A box of ye olde adventure!",
"box.adventure.pirate": "Pirate Adventure Box",
"box.adventure.pirate.desc": "Arr! X marks the box!",
"box.adventure.contemporary": "Contemporary Adventure Box",
"box.adventure.contemporary.desc": "A box for modern times. Comes with WiFi anxiety.",
"box.adventure.sentimental": "Sentimental Adventure Box",
"box.adventure.sentimental.desc": "This box makes you feel things. Mostly curiosity.",
"box.adventure.prehistoric": "Prehistoric Adventure Box",
"box.adventure.prehistoric.desc": "Ooga booga box. Very old. Much mystery.",
"box.adventure.cosmic": "Cosmic Adventure Box",
"box.adventure.cosmic.desc": "The universe is a box. This box is a universe.",
"box.adventure.microscopic": "Microscopic Adventure Box",
"box.adventure.microscopic.desc": "Size doesn't matter. Except when it does. Zoom in!",
"box.adventure.darkfantasy": "Dark Fantasy Adventure Box",
"box.adventure.darkfantasy.desc": "Darkness awaits. Also a box. A dark box.",
"meta.unlocked": "NEW FEATURE UNLOCKED: {0}!",
"meta.colors": "Text Colors",
"meta.extended_colors": "Extended Color Palette",
"meta.arrows": "Arrow Key Navigation",
"meta.inventory": "Inventory Panel",
"meta.resources": "Resource Panel",
"meta.stats": "Stats Panel",
"meta.portrait": "Portrait Panel",
"meta.chat": "Chat Panel",
"meta.layout": "Full Layout Mode",
"meta.shortcuts": "Keyboard Shortcuts",
"meta.animation": "Box Opening Animation",
"meta.crafting": "Crafting Panel",
"item.rarity.common": "Common",
"item.rarity.uncommon": "Uncommon",
"item.rarity.rare": "Rare",
"item.rarity.epic": "Epic",
"item.rarity.legendary": "Legendary",
"item.rarity.mythic": "Mythic",
"resource.health": "Health",
"resource.mana": "Mana",
"resource.food": "Food",
"resource.stamina": "Stamina",
"resource.blood": "Blood",
"resource.gold": "Gold",
"resource.oxygen": "Oxygen",
"resource.energy": "Energy",
"stat.strength": "Strength",
"stat.intelligence": "Intelligence",
"stat.luck": "Luck",
"stat.charisma": "Charisma",
"stat.dexterity": "Dexterity",
"stat.wisdom": "Wisdom",
"cosmetic.hair.none": "Bald",
"cosmetic.hair.short": "Short Hair",
"cosmetic.hair.long": "Long Hair",
"cosmetic.hair.ponytail": "Ponytail",
"cosmetic.hair.braided": "Braided Hair",
"cosmetic.hair.cyberpunk": "Cyberpunk Neon Hair",
"cosmetic.hair.fire": "Hair on Fire",
"cosmetic.hair.stardust": "Stardust Legendary Hair",
"cosmetic.eyes.none": "No Eyes (mysterious!)",
"cosmetic.eyes.blue": "Blue Eyes",
"cosmetic.eyes.green": "Green Eyes",
"cosmetic.eyes.redorange": "Red-Orange Eyes",
"cosmetic.eyes.brown": "Brown Eyes",
"cosmetic.eyes.black": "Black Eyes",
"cosmetic.eyes.sunglasses": "Sunglasses",
"cosmetic.eyes.pilotglasses": "Pilot Glasses",
"cosmetic.eyes.aircraftglasses": "Aircraft Glasses",
"cosmetic.eyes.cybernetic": "Cybernetic Eyes",
"cosmetic.eyes.magician": "Magician Glasses",
"cosmetic.body.naked": "Shirtless",
"cosmetic.body.regulartshirt": "Regular T-Shirt",
"cosmetic.body.sexytshirt": "Sexy T-Shirt",
"cosmetic.body.suit": "Business Suit",
"cosmetic.body.armored": "Armored Plate",
"cosmetic.body.robotic": "Robotic Chassis",
"cosmetic.legs.none": "Floating (no legs!)",
"cosmetic.legs.naked": "Bare Legs",
"cosmetic.legs.slip": "Slip",
"cosmetic.legs.short": "Shorts",
"cosmetic.legs.panty": "Panty",
"cosmetic.legs.rocketboots": "Rocket Boots",
"cosmetic.legs.pegleg": "Peg Leg",
"cosmetic.legs.tentacles": "Tentacles",
"cosmetic.arms.none": "No Arms (T-Rex mode)",
"cosmetic.arms.short": "Short Arms",
"cosmetic.arms.regular": "Regular Arms",
"cosmetic.arms.long": "Long Stretchy Arms",
"cosmetic.arms.mechanical": "Mechanical Arms",
"cosmetic.arms.wings": "Wings",
"cosmetic.arms.extrapair": "Four Arms",
"cosmetic.gender_error": "New Gender (ERROR: boxes don't have a gender. The box apologizes for the confusion.)",
"tint.none": "Natural",
"tint.cyan": "Cyan",
"tint.orange": "Orange",
"tint.purple": "Purple",
"tint.warmpink": "Warm Pink",
"tint.light": "Light",
"tint.dark": "Dark",
"tint.rainbow": "Rainbow",
"tint.neon": "Neon",
"tint.silver": "Silver",
"tint.gold": "Gold",
"tint.void": "Void",
"material.wood": "Wood",
"material.bronze": "Bronze",
"material.iron": "Iron",
"material.steel": "Steel",
"material.titanium": "Titanium",
"material.diamond": "Diamond",
"material.carbonfiber": "Carbon Fiber",
"material.form.raw": "Raw",
"material.form.refined": "Refined",
"material.form.nail": "Nail",
"material.form.plank": "Plank",
"material.form.ingot": "Ingot",
"material.form.sheet": "Sheet",
"material.form.thread": "Thread",
"material.form.dust": "Dust",
"material.form.gem": "Gem",
"item.health_potion_small": "Small Health Potion",
"item.health_potion_medium": "Medium Health Potion",
"item.health_potion_large": "Large Health Potion",
"item.mana_crystal_small": "Small Mana Crystal",
"item.mana_crystal_medium": "Medium Mana Crystal",
"item.food_ration": "Food Ration",
"item.stamina_drink": "Stamina Drink",
"item.blood_vial": "Blood Vial",
"item.gold_pouch": "Gold Pouch",
"item.oxygen_tank": "Oxygen Tank",
"item.energy_cell": "Energy Cell",
"item.space.badge": "Astronaut Badge",
"item.space.phone": "Alien Phone Number",
"item.space.key": "Airlock Access Key",
"item.space.map": "Star Map",
"item.space.coordinates": "Mysterious Coordinates",
"item.space.helmet": "Space Helmet",
"item.medieval.crest": "Knight's Crest",
"item.medieval.sword": "Excalibur Replica",
"item.medieval.scroll": "Ancient Scroll",
"item.medieval.seal": "Royal Seal",
"item.medieval.key": "Dungeon Key",
"item.pirate.map": "Treasure Map",
"item.pirate.compass": "Enchanted Compass",
"item.pirate.feather": "Parrot Feather",
"item.pirate.rum": "Bottle of Rum",
"item.pirate.flag": "Jolly Roger",
"item.pirate.key": "Chest Key",
"item.contemporary.phone": "Smartphone",
"item.contemporary.card": "Credit Card",
"item.contemporary.ticket": "Metro Ticket",
"item.contemporary.usb": "Suspicious USB Drive",
"item.contemporary.key": "Apartment Key",
"item.contemporary.badge": "Company Badge",
"item.sentimental.letter": "Love Letter",
"item.sentimental.flower": "Dried Flower",
"item.sentimental.album": "Photo Album",
"item.sentimental.melody": "Music Box Melody",
"item.sentimental.teddy": "Old Teddy Bear",
"item.sentimental.phone": "Ex's Phone Number",
"item.prehistoric.tooth": "Dinosaur Tooth",
"item.prehistoric.painting": "Cave Painting Fragment",
"item.prehistoric.amber": "Amber Stone",
"item.prehistoric.club": "Bone Club",
"item.prehistoric.fossil": "Trilobite Fossil",
"item.cosmic.shard": "Nebula Shard",
"item.cosmic.fragment": "Black Hole Fragment",
"item.cosmic.crystal": "Quasar Crystal",
"item.cosmic.dust": "Cosmic Dust",
"item.cosmic.core": "Star Core",
"item.microscopic.bacteria": "Sentient Bacteria Sample",
"item.microscopic.dna": "Glowing DNA Strand",
"item.microscopic.membrane": "Reinforced Cell Membrane",
"item.microscopic.mitochondria": "Hyperactive Mitochondria",
"item.microscopic.prion": "Friendly Prion (probably)",
"item.darkfantasy.ring": "Cursed Ring",
"item.darkfantasy.rune": "Blood Rune",
"item.darkfantasy.cloak": "Shadow Cloak",
"item.darkfantasy.grimoire": "Necromancer's Grimoire",
"item.darkfantasy.gem": "Soul Gem",
"item.darkfantasy.key": "Bone Key",
"item.resource_max_up": "{0} Max +1",
"item.resource_up": "{0} +1",
"item.stat_boost": "{0} +1",
"item.mysterious_key": "Mysterious Key",
"item.mysterious_key.desc": "A key to... something. The box knows, but the box isn't talking.",
"lore.fragment_1": "In the beginning, there was a box. The box contained another box. And so it was, and so it shall be.",
"lore.fragment_2": "The Ancient Order of Box-Openers has a single commandment: Open thy boxes.",
"lore.fragment_3": "Some say the universe itself is a box, waiting to be opened by someone with enough curiosity.",
"lore.fragment_4": "The first box was opened by Farah, who found inside it the concept of 'inside'.",
"lore.fragment_5": "Malkith once opened a box that contained the sound of one hand clapping. Nobody knows what that means.",
"lore.fragment_6": "Legend says there is a box that contains all other boxes. Opening it would cause a paradox. Or a refund.",
"lore.fragment_7": "Duncan tried to close a box once. The box union went on strike for three weeks.",
"lore.fragment_8": "Pierrick built a box-opening machine. It opened itself. Then it opened the machine. Then it opened the concept of opening.",
"lore.fragment_9": "Samuel wrote the original box-opening manual. Chapter 1: Open the box. Chapter 2: See Chapter 1.",
"lore.fragment_10": "The Black Box contains a cat. Or doesn't. Until you open it, it both does and doesn't. The cat is also a box.",
"cookie.1": "A box within a box is still a box.",
"cookie.2": "ERROR: This cookie contains no fortune. Please try again.",
"cookie.3": "You will open many boxes. This prediction has a 100% accuracy rate.",
"cookie.4": "The real treasure was the boxes we opened along the way.",
"cookie.5": "WARNING: Side effects of box opening include joy, confusion, and tentacle arms.",
"cookie.6": "Tomorrow you will find a box. Then another. Then another. Send help.",
"cookie.7": "Your lucky number is the number of boxes you've opened. So... a lot.",
"cookie.8": "Confucius say: person who open box find box. Person who not open box also find box. Box is inevitable.",
"cookie.9": "A journey of a thousand boxes begins with a single open.",
"cookie.10": "If you're reading this, you've spent too long opening boxes. Just kidding, there's no such thing.",
"cookie.11": "The box giveth, and the box giveth more boxes.",
"cookie.12": "In Soviet Russia, box opens YOU.",
"cookie.13": "Help I'm trapped in a fortune cookie factory inside a box.",
"cookie.14": "This fortune intentionally left blank. Just kidding. Or am I?",
"cookie.15": "You are the chosen one. The one who opens boxes. Truly a noble calling.",
"cookie.16": "Plot twist: the box was the friends you made along the way.",
"cookie.17": "Schrodinger called. He wants his box concept back.",
"cookie.18": "If you open a box and no one is around to hear it, does it make a loot?",
"cookie.19": "Today is a good day to open boxes. Tomorrow too. Every day, really.",
"cookie.20": "Your spirit animal is a box. Your power move is opening.",
"character.farah": "Farah",
"character.malkith": "Malkith",
"character.linu": "Linu",
"character.chenda": "Chenda",
"character.duncan": "Duncan",
"character.sandrea": "Sandrea",
"character.samuel": "Samuel",
"character.pierrick": "Pierrick",
"character.nova": "Captain Nova",
"character.aria": "ARIA",
"character.blackbeard": "Blackbeard the Unboxable",
"character.mordecai": "Mordecai the Grim",
"character.zephyr": "Zephyr",
"character.quantum": "Dr. Quantum",
"adventure.start": "Begin {0} Adventure",
"adventure.resume": "Resume {0} Adventure",
"adventure.completed": "Adventure Complete! You are now a certified box adventurer.",
"interaction.key_chest": "The key fits! The chest opens automatically!",
"interaction.key_no_match": "This key seems to fit something... but you don't have it yet. Perhaps a future box will provide.",
"interaction.craft_available": "New recipe available at {0}!",
"save.saving": "Saving...",
"save.saved": "Game saved to slot '{0}'.",
"save.loading": "Loading...",
"save.loaded": "Game loaded from slot '{0}'.",
"save.no_saves": "No save files found.",
"save.choose_slot": "Choose a save slot:",
"error.invalid_input": "Invalid input. Try again, brave box-opener.",
"error.no_boxes": "You have no boxes to open. How did you manage that? Open more boxes to get boxes.",
"error.not_enough_resources": "Not enough {0}. You need {1} more.",
"misc.boxes_opened": "Total boxes opened: {0}",
"misc.play_time": "Play time: {0}",
"misc.welcome_back": "Welcome back, {0}! Your boxes missed you."
}

339
content/strings/fr.json Normal file
View file

@ -0,0 +1,339 @@
{
"game.title": "OUVRE LA BOITE",
"game.subtitle": "Qu'est-ce qu'il y a dedans ? Un seul moyen de le savoir.",
"game.version": "v0.1.0",
"menu.new_game": "Nouvelle Partie",
"menu.load_game": "Charger une Partie",
"menu.language": "Langue",
"menu.quit": "Quitter",
"menu.back": "Retour",
"menu.continue": "Continuer",
"menu.save": "Sauvegarder",
"menu.settings": "Parametres",
"action.open_box": "Ouvrir une boite",
"action.inventory": "Voir l'inventaire",
"action.craft": "Fabriquer",
"action.adventure": "Partir a l'aventure",
"action.appearance": "Changer d'apparence",
"action.save": "Sauvegarder",
"action.quit": "Retourner au menu",
"prompt.name": "Quel est ton nom, brave ouvreur de boites ?",
"prompt.choose_action": "Que veux-tu faire ?",
"prompt.choose_box": "Quelle boite veux-tu ouvrir ?",
"prompt.choose_interaction": "Plusieurs interactions possibles ! Choisis-en une :",
"prompt.press_key": "Appuie sur une touche pour continuer...",
"box.opening": "Ouverture de {0}...",
"box.found": "Tu as trouve : {0} !",
"box.found_box": "A l'interieur il y avait... une autre boite ! {0} !",
"box.empty": "La boite est vide ! Philosophique.",
"box.no_boxes": "Tu n'as aucune boite. Comment t'as fait ?",
"box.auto_open": "{0} s'ouvre automatiquement !",
"box.starter": "Boite de depart",
"box.starter.desc": "Ta premiere boite. Le debut de tout. Ou de rien. Probablement de quelque chose quand meme.",
"box.box_of_boxes": "Boite a boite",
"box.box_of_boxes.desc": "Une boite qui contient... des boites. C'est des boites jusqu'en bas.",
"box.not_great": "Boite pas ouf",
"box.not_great.desc": "Elle est pas geniale. Elle est pas terrible. Elle... est.",
"box.ok_tier": "Boite ok tiers",
"box.ok_tier.desc": "La mediocrite n'a jamais ete aussi carree.",
"box.cool": "Boite coolos",
"box.cool.desc": "La on commence a causer. A causer cool.",
"box.epic": "Boite epique",
"box.epic.desc": "L'orchestre s'intensifie. La foule retient son souffle. C'est... une boite.",
"box.legendhair": "Boite legend'hair",
"box.legendhair.desc": "La coiffure la plus legendaire que tu deballeras jamais. Cheveu-jour-d'hui, legende demain.",
"box.legendary": "Boite legendaire",
"box.legendary.desc": "Les legendes parlent de cette boite. Doucement quand meme, c'est une boite.",
"box.adventure": "Boite aventure",
"box.adventure.desc": "Contient la cle de l'aventure ! Litteralement, parfois.",
"box.style": "Boite stylee",
"box.style.desc": "La mode est ephemere. Le style sorti d'une boite est eternel.",
"box.improvement": "Boite d'amelioration",
"box.improvement.desc": "On peut toujours s'ameliorer. Surtout avec des boites.",
"box.supply": "Boite de fourniture",
"box.supply.desc": "Des fournitures ! Le sang vital de tout passione d'ouverture de boites.",
"box.meta": "Boite Meta",
"box.meta.desc": "Cette boite ameliore... la facon dont tu vois les boites. Trop meta.",
"box.black": "Boite noire",
"box.black.desc": "Personne ne sait ce qu'il y a dedans. Meme pas la boite.",
"box.story": "Boite a histoire",
"box.story.desc": "Chaque boite a une histoire. Celle-ci plus que les autres.",
"box.music": "Boite a musique",
"box.music.desc": "Do do do do. La musique de boite c'est la meilleure musique.",
"box.cookie": "Boite a Cookies",
"box.cookie.desc": "La fortune sourit aux audacieux. Et a ceux qui ouvrent des boites.",
"box.adventure.space": "Boite d'aventure spatiale",
"box.adventure.space.desc": "Vers l'infini et au-dela ! (Boite non incluse dans l'infini)",
"box.adventure.medieval": "Boite d'aventure medievale",
"box.adventure.medieval.desc": "Oyez, oyez ! Une boite d'aventure d'antan !",
"box.adventure.pirate": "Boite d'aventure pirate",
"box.adventure.pirate.desc": "Arr ! X marque la boite !",
"box.adventure.contemporary": "Boite d'aventure contemporaine",
"box.adventure.contemporary.desc": "Une boite pour les temps modernes. Livree avec l'anxiete du WiFi.",
"box.adventure.sentimental": "Boite d'aventure sentimentale",
"box.adventure.sentimental.desc": "Cette boite te fait ressentir des choses. Surtout de la curiosite.",
"box.adventure.prehistoric": "Boite d'aventure prehistorique",
"box.adventure.prehistoric.desc": "Ouga bouga boite. Tres vieille. Beaucoup mystere.",
"box.adventure.cosmic": "Boite d'aventure cosmique",
"box.adventure.cosmic.desc": "L'univers est une boite. Cette boite est un univers.",
"box.adventure.microscopic": "Boite d'aventure microscopique",
"box.adventure.microscopic.desc": "La taille ne compte pas. Sauf quand si. Zoom !",
"box.adventure.darkfantasy": "Boite d'aventure dark fantasy",
"box.adventure.darkfantasy.desc": "Les tenebres t'attendent. Et aussi une boite. Une boite sombre.",
"meta.unlocked": "NOUVELLE FONCTIONNALITE : {0} !",
"meta.colors": "Couleurs de texte",
"meta.extended_colors": "Palette de couleurs etendue",
"meta.arrows": "Navigation avec les fleches",
"meta.inventory": "Panneau d'inventaire",
"meta.resources": "Panneau de ressources",
"meta.stats": "Panneau de statistiques",
"meta.portrait": "Panneau portrait",
"meta.chat": "Panneau de discussion",
"meta.layout": "Mise en page complete",
"meta.shortcuts": "Raccourcis clavier",
"meta.animation": "Animation d'ouverture de boite",
"meta.crafting": "Panneau de fabrication",
"item.rarity.common": "Commun",
"item.rarity.uncommon": "Peu commun",
"item.rarity.rare": "Rare",
"item.rarity.epic": "Epique",
"item.rarity.legendary": "Legendaire",
"item.rarity.mythic": "Mythique",
"resource.health": "Sante",
"resource.mana": "Mana",
"resource.food": "Nourriture",
"resource.stamina": "Endurance",
"resource.blood": "Sang",
"resource.gold": "Or",
"resource.oxygen": "Oxygene",
"resource.energy": "Energie",
"stat.strength": "Force",
"stat.intelligence": "Intelligence",
"stat.luck": "Chance",
"stat.charisma": "Charisme",
"stat.dexterity": "Dexterite",
"stat.wisdom": "Sagesse",
"cosmetic.hair.none": "Chauve",
"cosmetic.hair.short": "Cheveux courts",
"cosmetic.hair.long": "Cheveux longs",
"cosmetic.hair.ponytail": "Queue de cheval",
"cosmetic.hair.braided": "Tresses",
"cosmetic.hair.cyberpunk": "Cheveux neon cyberpunk",
"cosmetic.hair.fire": "Cheveux en feu",
"cosmetic.hair.stardust": "Coiffure Poussiere d'Etoile legendaire",
"cosmetic.eyes.none": "Pas d'yeux (mysterieux !)",
"cosmetic.eyes.blue": "Yeux bleus",
"cosmetic.eyes.green": "Yeux verts",
"cosmetic.eyes.redorange": "Yeux rouge-orange",
"cosmetic.eyes.brown": "Yeux marron",
"cosmetic.eyes.black": "Yeux noirs",
"cosmetic.eyes.sunglasses": "Lunettes de soleil",
"cosmetic.eyes.pilotglasses": "Lunettes d'aviateur",
"cosmetic.eyes.aircraftglasses": "Lunettes de pilote de chasse",
"cosmetic.eyes.cybernetic": "Yeux cybernetiques",
"cosmetic.eyes.magician": "Lunettes de magicien",
"cosmetic.body.naked": "Torse nu",
"cosmetic.body.regulartshirt": "T-shirt basique",
"cosmetic.body.sexytshirt": "T-shirt sexy",
"cosmetic.body.suit": "Costume",
"cosmetic.body.armored": "Armure",
"cosmetic.body.robotic": "Chassis robotique",
"cosmetic.legs.none": "Flottant (pas de jambes !)",
"cosmetic.legs.naked": "Jambes nues",
"cosmetic.legs.slip": "Slip",
"cosmetic.legs.short": "Short",
"cosmetic.legs.panty": "Culotte",
"cosmetic.legs.rocketboots": "Bottes a reaction",
"cosmetic.legs.pegleg": "Jambe de bois",
"cosmetic.legs.tentacles": "Tentacules",
"cosmetic.arms.none": "Pas de bras (mode T-Rex)",
"cosmetic.arms.short": "Bras courts",
"cosmetic.arms.regular": "Bras normaux",
"cosmetic.arms.long": "Bras longs extensibles",
"cosmetic.arms.mechanical": "Bras mecaniques",
"cosmetic.arms.wings": "Ailes",
"cosmetic.arms.extrapair": "Quatre bras",
"cosmetic.gender_error": "Nouveau genre (ERREUR : les boites n'ont pas de genre. La boite s'excuse pour la confusion.)",
"tint.none": "Naturel",
"tint.cyan": "Cyan",
"tint.orange": "Orange",
"tint.purple": "Violet",
"tint.warmpink": "Rose chaud",
"tint.light": "Clair",
"tint.dark": "Sombre",
"tint.rainbow": "Arc-en-ciel",
"tint.neon": "Neon",
"tint.silver": "Argent",
"tint.gold": "Or",
"tint.void": "Neant",
"material.wood": "Bois",
"material.bronze": "Bronze",
"material.iron": "Fer",
"material.steel": "Acier",
"material.titanium": "Titane",
"material.diamond": "Diamant",
"material.carbonfiber": "Fibre de carbone",
"material.form.raw": "Brut",
"material.form.refined": "Raffine",
"material.form.nail": "Clou",
"material.form.plank": "Planche",
"material.form.ingot": "Lingot",
"material.form.sheet": "Feuille",
"material.form.thread": "Fil",
"material.form.dust": "Poudre",
"material.form.gem": "Gemme",
"item.health_potion_small": "Petite Potion de Sante",
"item.health_potion_medium": "Potion de Sante Moyenne",
"item.health_potion_large": "Grande Potion de Sante",
"item.mana_crystal_small": "Petit Cristal de Mana",
"item.mana_crystal_medium": "Cristal de Mana Moyen",
"item.food_ration": "Ration alimentaire",
"item.stamina_drink": "Boisson d'endurance",
"item.blood_vial": "Fiole de sang",
"item.gold_pouch": "Bourse d'or",
"item.oxygen_tank": "Reservoir d'oxygene",
"item.energy_cell": "Cellule d'energie",
"item.space.badge": "Badge d'astronaute",
"item.space.phone": "Numero de telephone alien",
"item.space.key": "Cle d'acces au sas",
"item.space.map": "Carte stellaire",
"item.space.coordinates": "Coordonnees mysterieuses",
"item.space.helmet": "Casque spatial",
"item.medieval.crest": "Blason de chevalier",
"item.medieval.sword": "Replique d'Excalibur",
"item.medieval.scroll": "Parchemin ancien",
"item.medieval.seal": "Sceau royal",
"item.medieval.key": "Cle du donjon",
"item.pirate.map": "Carte au tresor",
"item.pirate.compass": "Boussole enchantee",
"item.pirate.feather": "Plume de perroquet",
"item.pirate.rum": "Bouteille de rhum",
"item.pirate.flag": "Jolly Roger",
"item.pirate.key": "Cle du coffre",
"item.contemporary.phone": "Smartphone",
"item.contemporary.card": "Carte de credit",
"item.contemporary.ticket": "Ticket de metro",
"item.contemporary.usb": "Cle USB suspecte",
"item.contemporary.key": "Cle d'appartement",
"item.contemporary.badge": "Badge d'entreprise",
"item.sentimental.letter": "Lettre d'amour",
"item.sentimental.flower": "Fleur sechee",
"item.sentimental.album": "Album photo",
"item.sentimental.melody": "Melodie de boite a musique",
"item.sentimental.teddy": "Vieil ours en peluche",
"item.sentimental.phone": "Numero de l'ex",
"item.prehistoric.tooth": "Dent de dinosaure",
"item.prehistoric.painting": "Fragment de peinture rupestre",
"item.prehistoric.amber": "Pierre d'ambre",
"item.prehistoric.club": "Massue en os",
"item.prehistoric.fossil": "Fossile de trilobite",
"item.cosmic.shard": "Eclat de nebuleuse",
"item.cosmic.fragment": "Fragment de trou noir",
"item.cosmic.crystal": "Cristal de quasar",
"item.cosmic.dust": "Poussiere cosmique",
"item.cosmic.core": "Coeur d'etoile",
"item.microscopic.bacteria": "Echantillon de bacterie sentiente",
"item.microscopic.dna": "Brin d'ADN luminescent",
"item.microscopic.membrane": "Membrane cellulaire renforcee",
"item.microscopic.mitochondria": "Mitochondrie hyperactive",
"item.microscopic.prion": "Prion amical (probablement)",
"item.darkfantasy.ring": "Anneau maudit",
"item.darkfantasy.rune": "Rune de sang",
"item.darkfantasy.cloak": "Cape d'ombre",
"item.darkfantasy.grimoire": "Grimoire du necromancien",
"item.darkfantasy.gem": "Gemme d'ame",
"item.darkfantasy.key": "Cle en os",
"item.resource_max_up": "{0} Max +1",
"item.resource_up": "{0} +1",
"item.stat_boost": "{0} +1",
"item.mysterious_key": "Cle mysterieuse",
"item.mysterious_key.desc": "Une cle pour... quelque chose. La boite sait, mais la boite ne parle pas.",
"lore.fragment_1": "Au commencement, il y avait une boite. La boite contenait une autre boite. Et c'est ainsi que ca a ete, et que ca sera.",
"lore.fragment_2": "L'Ancien Ordre des Ouvreurs de Boites n'a qu'un seul commandement : Tu ouvriras tes boites.",
"lore.fragment_3": "Certains disent que l'univers lui-meme est une boite, attendant d'etre ouverte par quelqu'un d'assez curieux.",
"lore.fragment_4": "La premiere boite a ete ouverte par Farah, qui a trouve a l'interieur le concept d''interieur'.",
"lore.fragment_5": "Malkith a un jour ouvert une boite contenant le son d'une seule main qui applaudit. Personne ne sait ce que ca veut dire.",
"lore.fragment_6": "La legende dit qu'il existe une boite qui contient toutes les autres boites. L'ouvrir causerait un paradoxe. Ou un remboursement.",
"lore.fragment_7": "Duncan a essaye de fermer une boite un jour. Le syndicat des boites s'est mis en greve pendant trois semaines.",
"lore.fragment_8": "Pierrick a construit une machine a ouvrir des boites. Elle s'est ouverte elle-meme. Puis elle a ouvert la machine. Puis elle a ouvert le concept d'ouverture.",
"lore.fragment_9": "Samuel a ecrit le premier manuel d'ouverture de boites. Chapitre 1 : Ouvre la boite. Chapitre 2 : Voir Chapitre 1.",
"lore.fragment_10": "La Boite Noire contient un chat. Ou pas. Jusqu'a ce que tu l'ouvres, elle en contient et n'en contient pas. Le chat est aussi une boite.",
"cookie.1": "Une boite dans une boite reste une boite.",
"cookie.2": "ERREUR : Ce cookie ne contient aucune fortune. Reessayez.",
"cookie.3": "Vous ouvrirez beaucoup de boites. Cette prediction a un taux de precision de 100%.",
"cookie.4": "Le vrai tresor, c'etait les boites qu'on a ouvertes en chemin.",
"cookie.5": "ATTENTION : Les effets secondaires de l'ouverture de boites incluent la joie, la confusion et des bras-tentacules.",
"cookie.6": "Demain tu trouveras une boite. Puis une autre. Puis une autre. Envoyez de l'aide.",
"cookie.7": "Ton nombre porte-bonheur est le nombre de boites que tu as ouvertes. Donc... beaucoup.",
"cookie.8": "Confucius dit : celui qui ouvre boite trouve boite. Celui qui n'ouvre pas boite trouve aussi boite. Boite est inevitable.",
"cookie.9": "Un voyage de mille boites commence par une seule ouverture.",
"cookie.10": "Si tu lis ceci, tu as passe trop de temps a ouvrir des boites. Je rigole, ca n'existe pas.",
"cookie.11": "La boite donne, et la boite redonne des boites.",
"cookie.12": "En Russie sovietique, c'est la boite qui t'ouvre.",
"cookie.13": "Au secours je suis piege dans une usine a fortune cookies a l'interieur d'une boite.",
"cookie.14": "Cette fortune a ete intentionnellement laissee vide. Je rigole. Ou pas ?",
"cookie.15": "Tu es l'elu. Celui qui ouvre les boites. Vraiment une noble vocation.",
"cookie.16": "Plot twist : la boite c'etait les amis qu'on s'est faits en chemin.",
"cookie.17": "Schrodinger a appele. Il veut recuperer son concept de boite.",
"cookie.18": "Si tu ouvres une boite et que personne n'est la pour l'entendre, est-ce que ca fait un loot ?",
"cookie.19": "Aujourd'hui est un bon jour pour ouvrir des boites. Demain aussi. Tous les jours, en fait.",
"cookie.20": "Ton animal totem est une boite. Ton pouvoir special c'est l'ouverture.",
"character.farah": "Farah",
"character.malkith": "Malkith",
"character.linu": "Linu",
"character.chenda": "Chenda",
"character.duncan": "Duncan",
"character.sandrea": "Sandrea",
"character.samuel": "Samuel",
"character.pierrick": "Pierrick",
"character.nova": "Capitaine Nova",
"character.aria": "ARIA",
"character.blackbeard": "Barbe-Noire l'Indeboitable",
"character.mordecai": "Mordecai le Sinistre",
"character.zephyr": "Zephyr",
"character.quantum": "Dr. Quantum",
"adventure.start": "Commencer l'aventure {0}",
"adventure.resume": "Reprendre l'aventure {0}",
"adventure.completed": "Aventure terminee ! Tu es maintenant un aventurier de boites certifie.",
"interaction.key_chest": "La cle rentre ! Le coffre s'ouvre automatiquement !",
"interaction.key_no_match": "Cette cle semble ouvrir quelque chose... mais tu ne l'as pas encore. Peut-etre qu'une future boite le fournira.",
"interaction.craft_available": "Nouvelle recette disponible a {0} !",
"save.saving": "Sauvegarde en cours...",
"save.saved": "Partie sauvegardee dans l'emplacement '{0}'.",
"save.loading": "Chargement...",
"save.loaded": "Partie chargee depuis l'emplacement '{0}'.",
"save.no_saves": "Aucune sauvegarde trouvee.",
"save.choose_slot": "Choisis un emplacement de sauvegarde :",
"error.invalid_input": "Entree invalide. Reessaie, brave ouvreur de boites.",
"error.no_boxes": "Tu n'as aucune boite a ouvrir. Comment t'as fait ? Ouvre plus de boites pour avoir des boites.",
"error.not_enough_resources": "Pas assez de {0}. Il t'en manque {1}.",
"misc.boxes_opened": "Total de boites ouvertes : {0}",
"misc.play_time": "Temps de jeu : {0}",
"misc.welcome_back": "Bon retour, {0} ! Tes boites se sont ennuyees."
}

932
docs/GDD.md Normal file
View file

@ -0,0 +1,932 @@
# Open The Box - Game Design Document
> **Version**: 0.1.0-alpha
> **Date**: 2026-03-10
> **Auteur**: Equipe Open The Box
> **Architecture de reference**: Black Box Sim (Brian Cronin)
> **Plateforme**: CLI (.NET / C#)
> **Localisation**: FR / EN
---
## Table des matieres
1. [Vue d'ensemble / Concept](#1-vue-densemble--concept)
2. [Mecanique principale : ouverture de boites](#2-mecanique-principale--ouverture-de-boites)
3. [Systeme de progression CLI (Phases 0-8)](#3-systeme-de-progression-cli-phases-0-8)
4. [Systeme de boites](#4-systeme-de-boites)
5. [Auto-activation (cle + coffre, interactions automatiques)](#5-auto-activation-cle--coffre-interactions-automatiques)
6. [Personnalisation](#6-personnalisation)
7. [Materiaux et Craft](#7-materiaux-et-craft)
8. [Ressources](#8-ressources)
9. [Aventures interactives](#9-aventures-interactives)
10. [Personnages](#10-personnages)
11. [Localisation](#11-localisation)
---
## 1. Vue d'ensemble / Concept
**Open The Box** est un jeu CLI (Command-Line Interface) d'ouverture de boites dans lequel le joueur decouvre progressivement un univers riche en ouvrant des boites contenant des objets, des lieux, des personnages, des cosmetiques et des meta-ameliorations.
### Pitch
> *Ouvrez une boite. Elle contient une autre boite. Celle-ci contient une cle. La cle ouvre un coffre. Le coffre contient un fragment d'histoire. L'histoire deverrouille un nouveau personnage. Le personnage revele un lieu. Le lieu offre une aventure. L'aventure recompense... une boite.*
### Piliers de design
1. **La curiosite comme moteur** -- Chaque ouverture de boite est une micro-revelation. Le joueur ne sait jamais ce qu'il va trouver, et chaque trouvaille enrichit l'univers du jeu.
2. **Progression de l'interface** -- Le CLI lui-meme evolue au fil du jeu. On commence avec un `Console.ReadLine` basique et on termine avec un layout complet Spectre.Console avec panneaux, couleurs, animations et raccourcis clavier.
3. **Emergent gameplay par combinaison** -- Les objets interagissent entre eux de maniere automatique (auto-activation). Une cle trouvee active automatiquement le coffre correspondant, revelant de nouveaux contenus.
4. **Profondeur cachee** -- Sous l'apparence simple d'un jeu d'ouverture de boites se cache un systeme de craft, d'aventures interactives, de personnalisation et de narration.
### Architecture Black Box Sim (Brian Cronin)
Le jeu suit le pattern "Black Box Sim" :
- **Entree** : une action simple (ouvrir une boite)
- **Traitement** : un systeme opaque de loot tables, conditions, poids et interactions
- **Sortie** : un resultat surprenant et satisfaisant
Le joueur n'a jamais acces aux probabilites exactes. Il decouvre le systeme par l'experience et l'experimentation. Les regles internes sont volontairement opaques -- d'ou le nom "Black Box".
Le modele de donnees est centre sur trois entites :
- **Box** (la boite) -- contient une loot table ponderee
- **Item** (l'objet) -- le resultat d'une ouverture
- **Player** (le joueur) -- son inventaire, ses stats, son etat de progression
---
## 2. Mecanique principale : ouverture de boites
### Boucle de gameplay fondamentale
```
[Joueur] -> Ouvre une boite -> [Systeme de loot] -> Obtient un/des objet(s) -> [Auto-activation] -> Effets en chaine -> [Retour a l'inventaire]
```
### Deroulement d'une ouverture
1. Le joueur selectionne une boite dans son inventaire (ou la boite est auto-selectionnee au debut du jeu).
2. Le systeme de loot tire un ou plusieurs objets selon la loot table de la boite.
3. L'objet est ajoute a l'inventaire du joueur.
4. Le systeme d'auto-activation verifie si des interactions sont possibles avec les objets existants.
5. Si des interactions existent, elles sont executees automatiquement (ouverture de coffre, activation de lieu, etc.).
6. L'affichage est mis a jour en fonction du niveau de progression CLI.
### Premiere ouverture
Le jeu commence avec une unique **Boite de depart**. Cette boite contient toujours (100%) une **Boite a boite**, qui elle-meme contient d'autres boites selon un systeme de poids detaille en section 4.
---
## 3. Systeme de progression CLI (Phases 0-8)
Le coeur de l'originalite d'Open The Box : l'interface elle-meme est un systeme de progression. Le joueur commence avec l'interface la plus minimale possible et debloquer des ameliorations d'interface via les **Boites Meta**.
### Phase 0 -- Console brute
- **Interface** : `Console.ReadLine` / `Console.WriteLine` uniquement
- **Interaction** : le joueur tape du texte pour agir
- **Visuel** : texte blanc sur fond noir, aucune mise en forme
- **Ressenti** : brut, mysterieux, inconfortable volontairement
### Phase 1 -- Couleurs de texte
- **Deblocage** : `UIFeature.TextColors`
- **Ajout** : les objets, noms et quantites s'affichent en couleur
- **Technologie** : codes ANSI basiques (8 couleurs)
- **Impact** : premiere sensation de "progression de l'interface"
### Phase 2 -- Couleurs etendues
- **Deblocage** : `UIFeature.ExtendedColors`
- **Ajout** : palette complete de 256 couleurs, degradees et nuances
- **Technologie** : codes ANSI etendus
- **Impact** : le texte devient visuellement plus riche et lisible
### Phase 3 -- Selection par fleches
- **Deblocage** : `UIFeature.ArrowKeySelection`
- **Ajout** : le joueur peut naviguer dans les menus avec les fleches directionnelles au lieu de taper des commandes
- **Technologie** : `Console.ReadKey` + gestion de curseur
- **Impact** : amelioration majeure de l'ergonomie
### Phase 4 -- Panneau d'inventaire
- **Deblocage** : `UIFeature.InventoryPanel`
- **Ajout** : un panneau lateral affiche l'inventaire du joueur en permanence
- **Technologie** : Spectre.Console `Table` ou `Panel`
- **Impact** : le joueur voit enfin son inventaire sans taper de commande
### Phase 5 -- Panneaux de ressources et stats
- **Deblocage** : `UIFeature.ResourcePanel` + `UIFeature.StatsPanel`
- **Ajout** : barres de ressources (HP, Mana, Food, etc.) et statistiques du joueur
- **Technologie** : Spectre.Console `BarChart` et `Table`
- **Impact** : le jeu ressemble desormais a un vrai RPG en CLI
### Phase 6 -- Portrait et chat
- **Deblocage** : `UIFeature.PortraitPanel` + `UIFeature.ChatPanel`
- **Ajout** : affichage ASCII du portrait du joueur (base sur les cosmetiques equipes) + panneau de chat avec les PNJ
- **Technologie** : Spectre.Console `Canvas` ou `Markup` avance
- **Impact** : dimension sociale et visuelle
### Phase 7 -- Animations et craft
- **Deblocage** : `UIFeature.BoxAnimation` + `UIFeature.CraftingPanel`
- **Ajout** : animations d'ouverture de boites (texte defilant, effets visuels ASCII) + panneau de craft
- **Technologie** : Spectre.Console `Live` et `Status`
- **Impact** : le jeu devient spectaculaire et immersif
### Phase 8 -- Full Layout
- **Deblocage** : `UIFeature.FullLayout` + `UIFeature.KeyboardShortcuts`
- **Ajout** : mise en page complete avec tous les panneaux organises, raccourcis clavier pour toutes les actions
- **Technologie** : Spectre.Console `Layout` complet
- **Impact** : l'interface finale, riche et complete -- la recompense ultime pour la progression
### Tableau recapitulatif
| Phase | UIFeature(s) | Technologie principale |
|-------|-------------------------------------|-----------------------------|
| 0 | *(aucun)* | Console.ReadLine |
| 1 | TextColors | ANSI 8 couleurs |
| 2 | ExtendedColors | ANSI 256 couleurs |
| 3 | ArrowKeySelection | Console.ReadKey |
| 4 | InventoryPanel | Spectre.Console Panel/Table |
| 5 | ResourcePanel, StatsPanel | Spectre.Console BarChart |
| 6 | PortraitPanel, ChatPanel | Spectre.Console Canvas |
| 7 | BoxAnimation, CraftingPanel | Spectre.Console Live/Status |
| 8 | FullLayout, KeyboardShortcuts | Spectre.Console Layout |
---
## 4. Systeme de boites
### Hierarchie complete
Le systeme de boites est le coeur du jeu. Chaque boite contient une **loot table** ponderee. Les boites peuvent contenir d'autres boites, creant un systeme recursif et emergent.
### Boite de depart
> *La toute premiere boite du jeu.*
| Contenu | Poids | Notes |
|----------------------|-------|---------------------------|
| Boite a boite | 100% | Toujours garantie |
La Boite de depart est unique. Elle n'apparait qu'une seule fois au tout debut du jeu.
### Boite a boite
> *La boite qui contient des boites. Le nexus central du jeu.*
La Boite a boite est le hub de distribution principal. Elle contient une selection ponderee parmi toutes les sous-boites disponibles du jeu.
| Contenu | Poids | Conditions |
|------------------------|--------|-----------------------------------------|
| Boite Meta | 20 | *(toujours disponible)* |
| Boite stylee | 15 | *(toujours disponible)* |
| Boite aventure | 15 | *(toujours disponible)* |
| Boite d'amelioration | 12 | Si ressources OU equipement > 0 |
| Boite de fourniture | 12 | Si ressources > 0 |
| Boite noire | 8 | *(toujours disponible)* |
| Boite a histoire | 6 | *(toujours disponible)* |
| Boite a musique | 5 | *(toujours disponible)* |
| Boite a Cookies | 4 | *(toujours disponible)* |
| Boite legendaire | 2 | *(toujours disponible)* |
| Boite legend'hair | 1 | *(toujours disponible)* |
> **Note** : Les poids sont relatifs. Le total n'est pas forcement 100 -- les poids sont normalises au moment du tirage. Les boites conditionnelles sont exclues du tirage si la condition n'est pas remplie.
---
### Boite Meta
> *La boite qui ameliore le jeu lui-meme.*
La Boite Meta contient des deblocages d'interface (UIFeatures) et des ameliorations meta du jeu. Elle est essentielle a la progression du joueur car c'est le seul moyen d'ameliorer l'interface CLI.
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| UIFeature (prochain deblocage) | 60 | Deverrouille la prochaine phase CLI |
| Badge de progression | 15 | Badge commemoratif |
| Font deblocable | 10 | Nouvelle police pour l'interface |
| TextColor (nouveau slot) | 10 | Personnalisation des couleurs de texte |
| Boite Meta (recursion) | 5 | 5% de chance d'obtenir une autre Boite Meta |
Le systeme de deblocage des UIFeatures est **sequentiel** : on debloque toujours la prochaine phase dans l'ordre (Phase 1, puis 2, puis 3, etc.). Si toutes les phases sont deja debloquees, le poids de UIFeature est redistribue aux autres entrees.
---
### Boite stylee
> *La boite de la mode et du style.*
La Boite stylee contient des cosmetiques pour personnaliser l'apparence du joueur.
| Contenu | Poids | Notes |
|--------------------------|-------|--------------------------------------------------------|
| Coiffure (HairStyle) | 25 | Style de cheveux aleatoire |
| Yeux (EyeStyle) | 20 | Style d'yeux aleatoire |
| Corps (BodyStyle) | 15 | Style de corps aleatoire |
| Jambes (LegStyle) | 15 | Style de jambes aleatoire |
| Bras (ArmStyle) | 10 | Style de bras aleatoire |
| Teinture (TintColor) | 10 | Couleur de teinture applicable a n'importe quel slot |
| Nouveau genre | 5 | **ERROR: les boites n'ont pas de genre.** |
> **Note speciale sur "Nouveau genre"** : Cette entree est un easter egg volontaire. Si le joueur tombe sur "Nouveau genre", le systeme affiche un message d'erreur humoristique : `"ERROR: les boites n'ont pas de genre."` et le joueur ne recoit rien -- sauf un Badge special "Genre Error" s'il ne l'a pas deja.
---
### Boite legend'hair
> *La boite legendaire... capillaire.*
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| Coiffure Legendaire (garantie) | 100 | Tire parmi les coiffures de rarete Legendaire ou Mythic |
La Boite legend'hair garantit **toujours** une coiffure de rarete Legendaire ou superieure. C'est la boite la plus rare de la Boite a boite (poids 1).
Coiffures legendaires disponibles :
- **StardustLegendary** -- des cheveux faits de poussiere d'etoile, brillants et changeants
- **Fire** -- des cheveux de flammes vivantes
- **Cyberpunk** -- coiffure high-tech avec effets neon
---
### Boite legendaire
> *La boite du prestige absolu.*
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| Coiffure Legendaire | 30 | Identique a Boite legend'hair |
| Objet Legendaire | 25 | Item de rarete Legendaire (arme, armure) |
| Personnage Legendaire | 20 | Personnage rare et puissant |
| Lieu Legendaire | 15 | Lieu unique et memorable |
| Blueprint Legendaire | 10 | Blueprint de station de craft rare |
Contrairement a la Boite legend'hair (garantie capillaire), la Boite legendaire offre un objet legendaire dans n'importe quelle categorie.
---
### Boite aventure
> *La porte vers neuf mondes differents.*
La Boite aventure contient un **jeton d'aventure** (AdventureToken) qui deverrouille une aventure interactive dans un des 9 themes du jeu.
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| Boite aventure Space | 14 | Aventure spatiale |
| Boite aventure Medieval | 14 | Aventure medievale |
| Boite aventure Pirate | 14 | Aventure pirate |
| Boite aventure Contemporary | 12 | Aventure contemporaine |
| Boite aventure Sentimental | 10 | Aventure sentimentale / romance |
| Boite aventure Prehistoric | 10 | Aventure prehistorique |
| Boite aventure Cosmic | 10 | Aventure cosmique |
| Boite aventure Microscopic | 8 | Aventure microscopique |
| Boite aventure DarkFantasy | 8 | Aventure dark fantasy |
Chaque sous-boite d'aventure contient :
- 1 AdventureToken du theme correspondant
- 1 objet thematique (arme, armure ou consommable lie au theme)
- Chance de : 1 personnage thematique (20%), 1 fragment de lore (30%)
---
### Boite d'amelioration
> *La boite qui rend plus fort.*
**Condition d'apparition** : le joueur doit posseder au moins une ressource OU un equipement dans son inventaire. Si cette condition n'est pas remplie, la Boite d'amelioration est exclue de la loot table de la Boite a boite.
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| Amelioration de ressource max | 35 | +10% au cap d'une ressource aleatoire |
| Amelioration d'equipement | 25 | +1 niveau a un equipement possede |
| Amelioration de stat | 20 | +1 a une statistique aleatoire |
| Blueprint de station | 15 | Plan pour construire une nouvelle station |
| Amelioration de station | 5 | +1 niveau a une station existante |
---
### Boite de fourniture
> *La boite du ravitaillement.*
**Condition d'apparition** : le joueur doit posseder au moins une ressource active (debloquee). Si le joueur n'a encore aucune ressource, cette boite n'apparait pas.
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| Health (soin) | 20 | Restaure des points de vie |
| Mana | 15 | Restaure des points de mana |
| Food (nourriture) | 20 | Restaure de la nourriture |
| Stamina (endurance) | 15 | Restaure de l'endurance |
| Gold (or) | 15 | Ajoute de l'or |
| Energy (energie) | 10 | Restaure de l'energie |
| Blood (sang) | 3 | Restaure du sang (ressource rare) |
| Oxygen (oxygene) | 2 | Restaure de l'oxygene (ressource rare) |
> **Note** : Seules les ressources deja debloquees par le joueur peuvent apparaitre dans le tirage. Les poids sont recalcules dynamiquement.
---
### Boite noire
> *La boite mysterieuse. Personne ne sait ce qu'elle contient avant de l'ouvrir.*
La Boite noire est speciale : son contenu est totalement aleatoire et peut provenir de **n'importe quelle** autre loot table du jeu. Elle peut contenir :
- Un objet de n'importe quelle rarete (Common a Mythic)
- Un cosmetique de n'importe quel slot
- Un materiau de n'importe quel type et forme
- Un personnage
- Un fragment de lore
- Un token d'aventure
- Une autre boite (y compris une autre Boite noire, dans de tres rares cas)
La Boite noire est le symbole meme du principe "Black Box" du jeu : l'opacite totale.
---
### Boite a histoire
> *La boite qui raconte.*
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| Fragment de Lore | 50 | Un morceau d'histoire du monde |
| Story Item | 25 | Un objet lie a la narration |
| Personnage (rencontre) | 15 | Un nouveau PNJ se revele |
| Lieu (decouverte) | 10 | Un nouveau lieu est decouvert |
Les fragments de lore se collectionnent et, une fois assembles, revelent des pans entiers de l'histoire du monde d'Open The Box.
---
### Boite a musique
> *La boite qui chante.*
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| Musique d'ambiance | 40 | Theme musical pour un lieu ou une aventure |
| Jingle d'ouverture | 25 | Son joue a l'ouverture de boite |
| Theme de personnage | 20 | Musique associee a un PNJ |
| Boite a musique rare | 10 | Musique rare ou legendary |
| Easter egg sonore | 5 | Son cache, reference culturelle |
> **Note technique** : les "musiques" sont representees par des descriptions textuelles et des patterns ASCII art dans la console. Si le systeme audio est disponible, elles peuvent aussi etre jouees.
---
### Boite a Cookies
> *La boite des petits plaisirs.*
| Contenu | Poids | Notes |
|----------------------------------|-------|------------------------------------------|
| Cookie de fortune | 40 | Message aleatoire (humoristique ou sage) |
| Cookie de buff | 25 | Buff temporaire (+10% chance de rare) |
| Cookie de craft | 15 | Accelere le prochain craft |
| Cookie dore | 10 | Effet rare et memorable |
| Cookie d'XP | 7 | Bonus d'experience |
| Cookie meta | 3 | Effet meta-game (change une regle) |
---
## 5. Auto-activation (cle + coffre, interactions automatiques)
### Principe
L'auto-activation est un systeme central d'Open The Box. Lorsqu'un objet est ajoute a l'inventaire du joueur, le systeme verifie automatiquement si cet objet peut interagir avec un autre objet deja present.
### Types d'interactions automatiques
| Interaction | Declencheur | Resultat |
|---------------------------|--------------------------------------|-------------------------------------|
| **Cle + Coffre** | Cle et coffre correspondants | Le coffre s'ouvre, revelant son contenu |
| **Carte + Lieu** | Carte d'un lieu + badge d'exploration| Le lieu est deverrouille |
| **Token + Aventure** | AdventureToken + seuil de progression| L'aventure devient accessible |
| **Blueprint + Materiaux** | Blueprint + materiaux requis | La station de craft est construite |
| **Fragment + Fragment** | Fragments complementaires | Un lore complet est revele |
| **Consommable + Ressource** | Consommable + ressource cible | La ressource est restauree |
### Flux d'auto-activation
```
1. Objet ajoute a l'inventaire
2. Pour chaque objet de l'inventaire :
a. Verifier si une interaction est possible avec le nouvel objet
b. Si oui, executer l'interaction
c. L'interaction peut produire de nouveaux objets
d. Si de nouveaux objets sont produits, repeter depuis l'etape 2
3. Fin de la chaine d'auto-activation
```
### Chaines d'activation
Les auto-activations peuvent creer des **chaines** : ouvrir un coffre peut reveler une cle qui ouvre un autre coffre, qui contient un fragment qui complete un lore, qui debloque un personnage. Ces chaines sont l'un des moments les plus satisfaisants du jeu.
### Type de resultat d'interaction (InteractionResultType)
Chaque interaction produit un resultat type parmi :
- **OpenBox** -- ouverture d'une boite ou d'un coffre
- **Craft** -- fabrication d'un objet
- **Transform** -- transformation d'un objet en un autre
- **Consume** -- consommation d'un objet (potion, nourriture)
- **Unlock** -- deblocage d'un contenu (lieu, aventure, personnage)
- **Combine** -- combinaison de plusieurs objets en un seul
- **Teleport** -- deplacement vers un nouveau lieu
---
## 6. Personnalisation
### Slots cosmetiques
Le joueur peut personnaliser l'apparence de son avatar a travers 5 slots cosmetiques :
#### Coiffure (CosmeticSlot.Hair)
| Style | Rarete | Description |
|-------------------|-------------|----------------------------------------------------|
| None | -- | Pas de coiffure (defaut) |
| Short | Common | Cheveux courts classiques |
| Long | Common | Cheveux longs |
| Ponytail | Uncommon | Queue de cheval |
| Braided | Rare | Tresses elaborees |
| Cyberpunk | Epic | Coiffure high-tech avec effets neon |
| Fire | Legendary | Cheveux de flammes vivantes |
| StardustLegendary | Mythic | Cheveux de poussiere d'etoile |
#### Yeux (CosmeticSlot.Eyes)
| Style | Rarete | Description |
|-------------------|-------------|----------------------------------------------------|
| None | -- | Pas de style d'yeux (defaut) |
| Blue | Common | Yeux bleus |
| Green | Common | Yeux verts |
| RedOrange | Uncommon | Yeux rouge-orange |
| Brown | Common | Yeux marron |
| Black | Uncommon | Yeux noirs profonds |
| Sunglasses | Rare | Lunettes de soleil |
| PilotGlasses | Rare | Lunettes d'aviateur |
| AircraftGlasses | Epic | Lunettes de pilote de chasse |
| CyberneticEyes | Legendary | Yeux cybernetiques lumineux |
| MagicianGlasses | Epic | Lunettes de magicien (rondes, dorees) |
#### Corps (CosmeticSlot.Body)
| Style | Rarete | Description |
|-------------------|-------------|----------------------------------------------------|
| Naked | Common | Torse nu (defaut) |
| RegularTShirt | Common | T-shirt classique |
| SexyTShirt | Uncommon | T-shirt echancre |
| Suit | Rare | Costume elegant |
| Armored | Epic | Armure complete |
| Robotic | Legendary | Corps robotique |
#### Jambes (CosmeticSlot.Legs)
| Style | Rarete | Description |
|-------------------|-------------|----------------------------------------------------|
| None | -- | Aucun style (defaut) |
| Naked | Common | Jambes nues |
| Slip | Common | Sous-vetement |
| Short | Uncommon | Short |
| Panty | Uncommon | Collants |
| RocketBoots | Epic | Bottes a reaction |
| PegLeg | Rare | Jambe de bois (pirate) |
| Tentacles | Legendary | Tentacules a la place des jambes |
#### Bras (CosmeticSlot.Arms)
| Style | Rarete | Description |
|-------------------|-------------|----------------------------------------------------|
| None | -- | Aucun style (defaut) |
| Short | Common | Bras courts |
| Regular | Common | Bras normaux |
| Long | Uncommon | Bras longs |
| Mechanical | Epic | Bras mecaniques |
| Wings | Legendary | Ailes a la place des bras |
| ExtraPair | Mythic | Paire de bras supplementaire (4 bras au total) |
### Teintures (TintColor)
Les teintures sont applicables a **n'importe quel** slot cosmetique. Elles modifient la couleur de l'objet cosmetique equipe.
| Teinture | Rarete | Effet visuel |
|-----------|-----------|-------------------------------------------|
| None | -- | Couleur d'origine |
| Cyan | Common | Teinte cyan |
| Orange | Common | Teinte orange |
| Purple | Uncommon | Teinte violette |
| WarmPink | Uncommon | Teinte rose chaud |
| Light | Common | Version eclaircie |
| Dark | Common | Version assombrie |
| Rainbow | Epic | Multicolore arc-en-ciel |
| Neon | Rare | Effet neon brillant |
| Silver | Rare | Teinte argentee metallique |
| Gold | Epic | Teinte doree |
| Void | Legendary | Noir absolu avec reflets d'etoiles |
---
## 7. Materiaux et Craft
### Materiaux (7 types)
Le systeme de craft repose sur 7 types de materiaux, classes par ordre de rarete et de puissance :
| Materiau | Rarete d'apparition | Tier | Description |
|---------------|---------------------|------|--------------------------------------|
| Wood | Common | 1 | Bois, le materiau le plus basique |
| Bronze | Common | 2 | Bronze, alliage simple |
| Iron | Uncommon | 3 | Fer, materiau intermediaire |
| Steel | Rare | 4 | Acier, materiau avance |
| Titanium | Epic | 5 | Titane, materiau de haute technologie|
| Diamond | Legendary | 6 | Diamant, materiau precieux |
| CarbonFiber | Mythic | 7 | Fibre de carbone, materiau ultime |
### Formes de materiaux (9 formes)
Chaque materiau peut exister sous differentes formes, obtenues par transformation dans les stations de craft :
| Forme | Description | Station typique |
|----------|------------------------------------------|--------------------------|
| Raw | Materiau brut, non transforme | *(aucune)* |
| Refined | Materiau raffine, pret a l'emploi | Foundry |
| Nail | Clou, pour la construction | Anvil |
| Plank | Planche (surtout pour le bois) | SawingPost |
| Ingot | Lingot (surtout pour les metaux) | Furnace |
| Sheet | Feuille ou plaque fine | Forge |
| Thread | Fil (pour le tissage et la couture) | Loom |
| Dust | Poudre (pour l'alchimie) | MortarAndPestle |
| Gem | Gemme taillee (pour la joaillerie) | Jewelry |
### Combinatoire
Avec 7 materiaux et 9 formes, le systeme offre **63 combinaisons** possibles de materiaux (7 x 9 = 63). Toutes les combinaisons ne sont pas forcement accessibles immediatement -- certaines necessitent des stations de craft specifiques.
### Stations de craft (30+ stations)
Les stations de craft sont debloquees via des **Blueprints** (WorkstationBlueprint) trouves dans les boites. Chaque station permet de transformer des materiaux et de fabriquer des objets.
| Station | Description | Specialite |
|------------------------|-------------------------------------------------------|-----------------------------|
| Foundry | Fonderie pour raffiner les materiaux bruts | Raw -> Refined |
| Workbench | Etabli polyvalent | Craft general |
| Furnace | Four pour creer des lingots | Refined -> Ingot |
| Loom | Metier a tisser | Refined -> Thread |
| Anvil | Enclume pour forger clous et outils | Ingot -> Nail, outils |
| AlchemyTable | Table d'alchimie | Potions, elixirs |
| Forge | Forge avancee | Ingot -> Sheet, armes |
| SawingPost | Poste de sciage | Wood -> Plank |
| Windmill | Moulin a vent | Transformation de grains |
| Watermill | Moulin a eau | Transformation hydraulique |
| OilPress | Presse a huile | Production d'huiles |
| PotteryWorkshop | Atelier de poterie | Objets en ceramique |
| TailorTable | Table de couturier | Vetements, cosmetiques |
| MortarAndPestle | Mortier et pilon | Refined -> Dust |
| DyeBasin | Bassin de teinture | Teintures |
| Jewelry | Atelier de joaillerie | Refined -> Gem, bijoux |
| Smoker | Fumoir | Conservation de nourriture |
| BrewingVat | Cuve de brassage | Boissons, potions |
| EngineerDesk | Bureau d'ingenieur | Blueprints avances |
| WeldingStation | Poste de soudure | Assemblage metal |
| DrawingTable | Table a dessin | Plans et schemas |
| EngravingBench | Banc de gravure | Gravure et enchantement |
| SewingPost | Poste de couture | Thread -> vetements |
| MagicCauldron | Chaudron magique | Potions magiques puissantes |
| TransformationPentacle | Pentacle de transformation | Transformation d'objets |
| PaintingSpace | Espace de peinture | Art et decoration |
| Distillery | Distillerie | Alcools, essences |
| Printer3D | Imprimante 3D | Objets complexes modernes |
| MatterSynthesizer | Synthetiseur de matiere | Creation de materiaux rares |
| GeneticModStation | Station de modification genetique | Modifications biologiques |
| TemporalBracelet | Bracelet temporel | Manipulation du temps |
| StasisChamber | Chambre de stase | Conservation et evolution |
---
## 8. Ressources
Le joueur gere 8 ressources qui influencent sa capacite a explorer, combattre, crafter et survivre.
| Ressource | Icone | Description | Utilisation principale |
|-----------|-------|--------------------------------------------------|--------------------------------|
| Health | HP | Points de vie | Survie, combat |
| Mana | MP | Points de magie | Sorts, enchantements |
| Food | FD | Nourriture | Exploration, stamina |
| Stamina | ST | Endurance | Actions physiques, craft |
| Blood | BL | Sang | Rituels, magie noire |
| Gold | GD | Or | Commerce, ameliorations |
| Oxygen | O2 | Oxygene | Exploration spatiale/sous-marine|
| Energy | EN | Energie | Machines, stations de craft |
### Mecaniques de ressources
- Chaque ressource a un **maximum** (cap) qui peut etre ameliore via la Boite d'amelioration.
- Les ressources se consomment lors des aventures, du craft et des interactions.
- Les ressources se restaurent via la Boite de fourniture, les consommables et certaines interactions.
- Certaines ressources sont conditionnelles : **Blood** n'apparait qu'avec le theme DarkFantasy, **Oxygen** qu'avec le theme Space ou des aventures sous-marines.
### Deblocage des ressources
Les ressources ne sont pas toutes disponibles des le debut :
| Ressource | Condition de deblocage |
|-----------|-----------------------------------------------------|
| Health | Debloquee des le debut |
| Mana | Premiere rencontre avec un personnage magique |
| Food | Premiere aventure exploree |
| Stamina | Premiere action physique (combat, craft) |
| Blood | Deblocage du theme DarkFantasy |
| Gold | Premier lieu de commerce decouvert |
| Oxygen | Deblocage du theme Space OU aventure sous-marine |
| Energy | Premiere station de craft construite |
---
## 9. Aventures interactives
### Les 9 themes
Les aventures interactives sont des sequences narratives jouables en CLI, chacune avec son propre univers, ses personnages et ses defis.
#### 1. Space (Espace)
- **Ambiance** : science-fiction, exploration spatiale
- **Lieux typiques** : vaisseaux spatiaux, stations orbitales, planetes alien
- **Ressource cle** : Oxygen
- **Personnages associes** : Farah (pilote), Samuel (ingenieur)
- **Mood possible** : Investigation, Spooky
- **Environnement** : Nature (planetes), Town (stations)
#### 2. Medieval (Medieval)
- **Ambiance** : fantasy medievale, chateaux et donjons
- **Lieux typiques** : chateaux, forets enchantees, villages
- **Ressource cle** : Mana, Stamina
- **Personnages associes** : Malkith (chevalier), Linu (magicienne)
- **Mood possible** : Tragedy, Romance
- **Environnement** : Nature, Town
#### 3. Pirate
- **Ambiance** : age d'or de la piraterie
- **Lieux typiques** : navires, iles au tresor, ports
- **Ressource cle** : Gold, Stamina
- **Personnages associes** : Duncan (capitaine), Chenda (navigatrice)
- **Mood possible** : Comedy, Investigation
- **Environnement** : Water, Town
#### 4. Contemporary (Contemporain)
- **Ambiance** : monde moderne, thriller urbain
- **Lieux typiques** : villes, bureaux, appartements
- **Ressource cle** : Energy, Gold
- **Personnages associes** : Sandrea (journaliste), Pierrick (hacker)
- **Mood possible** : Investigation, Comedy
- **Environnement** : Town
#### 5. Sentimental
- **Ambiance** : romance, relations humaines, emotions
- **Lieux typiques** : parcs, cafes, plages au coucher du soleil
- **Ressource cle** : Health (emotionnel), Mana (intuition)
- **Mood possible** : Romance, Tragedy, Comedy
- **Environnement** : Nature, Town
#### 6. Prehistoric (Prehistorique)
- **Ambiance** : ere prehistorique, survie primitive
- **Lieux typiques** : grottes, jungles, volcans
- **Ressource cle** : Food, Stamina
- **Personnages associes** : personnages primitifs
- **Mood possible** : Dark, Comedy
- **Environnement** : Nature
#### 7. Cosmic (Cosmique)
- **Ambiance** : echelle cosmique, entites divines, dimensions paralleles
- **Lieux typiques** : nebuleuses, trous noirs, dimensions alternatives
- **Ressource cle** : Mana, Energy
- **Mood possible** : Spooky, Dark
- **Environnement** : Nature (cosmique)
#### 8. Microscopic (Microscopique)
- **Ambiance** : monde microscopique, cellules, atomes
- **Lieux typiques** : interieur d'un corps humain, molecules, circuits
- **Ressource cle** : Energy, Oxygen
- **Mood possible** : Investigation, Spooky
- **Environnement** : Nature (biologique)
#### 9. DarkFantasy (Dark Fantasy)
- **Ambiance** : fantasy sombre, horreur gothique
- **Lieux typiques** : cryptes, forets maudites, tours sombres
- **Ressource cle** : Blood, Mana
- **Personnages associes** : Malkith (version sombre)
- **Mood possible** : Dark, Spooky, Tragedy
- **Environnement** : Nature, Town
### Structure d'une aventure
Chaque aventure suit cette structure :
1. **Introduction** -- mise en contexte narrative
2. **Exploration** -- le joueur decouvre des lieux et interagit avec l'environnement
3. **Rencontres** -- dialogues avec des PNJ, choix narratifs
4. **Defi** -- combat, enigme ou epreuve
5. **Resolution** -- conclusion de l'aventure avec recompenses
### Recompenses d'aventure
- Objets thematiques (armes, armures, cosmetiques du theme)
- Materiaux rares
- Fragments de lore
- Boites speciales (chance de Boite legendaire)
- Deblocage de personnages
---
## 10. Personnages
### Personnages principaux
Les personnages sont des PNJ que le joueur rencontre et debloque au fil du jeu. Chaque personnage a sa personnalite, son histoire et ses liens avec les themes d'aventure.
#### Farah
- **Role** : Pilote spatiale, exploratrice
- **Theme principal** : Space
- **Personnalite** : courageuse, optimiste, impulsive
- **Lien** : guide le joueur dans les aventures spatiales
- **Quete personnelle** : retrouver son vaisseau perdu
#### Malkith
- **Role** : Chevalier, gardien
- **Theme principal** : Medieval / DarkFantasy
- **Personnalite** : honneur, loyaute, tourmente interieure
- **Lien** : protege le joueur dans les aventures medievales et sombres
- **Quete personnelle** : racheter une faute passee
#### Linu
- **Role** : Magicienne, erudite
- **Theme principal** : Medieval / Cosmic
- **Personnalite** : curieuse, enigmatique, bienveillante
- **Lien** : enseigne la magie et les mysteres cosmiques au joueur
- **Quete personnelle** : dechiffrer un grimoire ancien
#### Chenda
- **Role** : Navigatrice, cartographe
- **Theme principal** : Pirate
- **Personnalite** : pragmatique, debrouillarde, loyale
- **Lien** : guide le joueur sur les mers et dans les explorations
- **Quete personnelle** : cartographier le monde entier
#### Duncan
- **Role** : Capitaine pirate, aventurier
- **Theme principal** : Pirate
- **Personnalite** : charismatique, audacieux, impredictible
- **Lien** : leader naturel, entraine le joueur dans des peripeties
- **Quete personnelle** : trouver le tresor ultime
#### Sandrea
- **Role** : Journaliste d'investigation
- **Theme principal** : Contemporary
- **Personnalite** : tenace, intelligente, ethique
- **Lien** : devoile les mysteres du monde contemporain
- **Quete personnelle** : reveler une conspiration mondiale
#### Samuel
- **Role** : Ingenieur, inventeur
- **Theme principal** : Space / Contemporary
- **Personnalite** : methodique, creatif, reserve
- **Lien** : construit et ameliore les equipements du joueur
- **Quete personnelle** : creer l'invention qui changera le monde
#### Pierrick
- **Role** : Hacker, specialiste techno
- **Theme principal** : Contemporary / Cosmic
- **Personnalite** : cynique, brillant, anti-conformiste
- **Lien** : deverrouille les secrets technologiques et numeriques
- **Quete personnelle** : hacker la realite elle-meme
### Personnages secondaires
Au-dela des 8 personnages principaux, le jeu contient de nombreux PNJ secondaires :
- **Marchands** -- vendent et achetent des objets contre de l'or
- **Gardiens de lieux** -- protegent l'acces aux lieux importants
- **Conteurs** -- revelent des fragments de lore
- **Artisans** -- gerent les stations de craft
- **Guides d'aventure** -- accompagnent le joueur dans les aventures thematiques
- **Enigmatiques** -- personnages mysterieux qui apparaissent aleatoirement
### Relations entre personnages
Les personnages ont des relations entre eux qui influencent les dialogues et les aventures :
- **Farah & Samuel** -- collaboration professionnelle, amitie
- **Malkith & Linu** -- respect mutuel, mentor/eleve
- **Duncan & Chenda** -- capitaine/navigatrice, confiance totale
- **Sandrea & Pierrick** -- journaliste/hacker, alliance tactique
- **Malkith (Medieval) & Malkith (DarkFantasy)** -- deux facettes du meme personnage
---
## 11. Localisation
### Langues supportees
| Code | Langue | Statut |
|------|----------|-------------|
| FR | Francais | Principal |
| EN | Anglais | Secondaire |
### Strategie de localisation
- **Texte de l'interface** : localise via des fichiers de ressources (`.resx` ou JSON)
- **Noms des objets** : localises (ex: "Epee en acier" / "Steel Sword")
- **Noms des personnages** : non localises (les noms propres restent identiques)
- **Fragments de lore** : localises integralement (textes narratifs)
- **Aventures** : dialogues et descriptions localises
- **Enums et cles internes** : en anglais (code source)
### Exemples de localisation
| Cle | FR | EN |
|-------------------------|------------------------------|-----------------------------|
| `ui.open_box` | Ouvrir la boite | Open the box |
| `ui.inventory` | Inventaire | Inventory |
| `item.steel_sword` | Epee en acier | Steel Sword |
| `resource.health` | Points de vie | Health Points |
| `adventure.space.intro` | L'espace infini s'ouvre... | The infinite space opens... |
| `box.starter` | Boite de depart | Starter Box |
| `box.meta` | Boite Meta | Meta Box |
| `box.stylish` | Boite stylee | Stylish Box |
| `error.gender` | ERROR: les boites n'ont pas de genre. | ERROR: boxes don't have a gender. |
---
## Annexe A : Glossaire
| Terme | Definition |
|----------------------|----------------------------------------------------------------------------|
| Black Box Sim | Pattern de design ou les mecaniques internes sont opaques pour le joueur |
| Loot table | Table de probabilites definissant le contenu d'une boite |
| Auto-activation | Interaction automatique entre objets de l'inventaire |
| UIFeature | Fonctionnalite d'interface debloquable |
| Blueprint | Plan permettant de construire une station de craft |
| Fragment de lore | Morceau d'histoire collectible |
| AdventureToken | Jeton d'acces a une aventure thematique |
| Teinture | Couleur applicable a un cosmetique |
| Boite a boite | Boite centrale contenant d'autres boites |
| Boite noire | Boite au contenu totalement aleatoire |
## Annexe B : Conditions de loot (LootConditionType)
Les conditions de loot controlent la disponibilite des objets dans les loot tables :
| Condition | Description |
|--------------------|----------------------------------------------------------|
| HasItem | Le joueur possede un objet specifique |
| HasNotItem | Le joueur ne possede PAS un objet specifique |
| ResourceAbove | Une ressource est au-dessus d'un seuil |
| ResourceBelow | Une ressource est en-dessous d'un seuil |
| BoxesOpenedAbove | Le nombre total de boites ouvertes depasse un seuil |
| HasUIFeature | Le joueur a debloque une fonctionnalite d'interface |
| HasWorkstation | Le joueur possede une station de craft specifique |
| HasAdventure | Le joueur a acces a un theme d'aventure |
| HasCosmetic | Le joueur possede un cosmetique specifique |
## Annexe C : Stats du joueur
| Stat | Description | Impact |
|----------------|------------------------------------------------|--------------------------------------|
| Strength | Force physique | Degats melee, capacite de transport |
| Intelligence | Intelligence | Degats magiques, efficacite de craft |
| Luck | Chance | Probabilites de loot ameliorees |
| Charisma | Charisme | Prix des marchands, dialogues |
| Dexterity | Dexterite | Esquive, vitesse, precision |
| Wisdom | Sagesse | Regeneration de mana, intuition |
---
*Fin du Game Design Document -- Open The Box v0.1.0-alpha*

24
lib/Loreline.deps.json Normal file
View file

@ -0,0 +1,24 @@
{
"runtimeTarget": {
"name": ".NETStandard,Version=v2.1/",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETStandard,Version=v2.1": {},
".NETStandard,Version=v2.1/": {
"Loreline/1.0.0": {
"runtime": {
"Loreline.dll": {}
}
}
}
},
"libraries": {
"Loreline/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

BIN
lib/Loreline.dll Normal file

Binary file not shown.

495
lib/Loreline.xml Normal file
View file

@ -0,0 +1,495 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Loreline</name>
</assembly>
<members>
<member name="T:Loreline.IFields">
<summary>
Base interface to hold loreline values.
This interface allows to map loreline object fields to game-specific objects.
</summary>
</member>
<member name="M:Loreline.IFields.LorelineCreate(Loreline.Interpreter)">
<summary>
Called when the object has been created from an interpreter
</summary>
<param name="interpreter">The interpreter instance</param>
</member>
<member name="M:Loreline.IFields.LorelineGet(Loreline.Interpreter,System.String)">
<summary>
Get the value associated to the given field key
</summary>
<param name="interpreter">The interpreter instance</param>
<param name="key">The field key</param>
<returns>The value associated with the key</returns>
</member>
<member name="M:Loreline.IFields.LorelineSet(Loreline.Interpreter,System.String,System.Object)">
<summary>
Set the value associated to the given field key
</summary>
<param name="interpreter">The interpreter instance</param>
<param name="key">The field key</param>
<param name="value">The value to set</param>
</member>
<member name="M:Loreline.IFields.LorelineRemove(Loreline.Interpreter,System.String)">
<summary>
Remove the field associated to the given key
</summary>
<param name="interpreter">The interpreter instance</param>
<param name="key">The field key to remove</param>
<returns>True if the key was found and removed, false otherwise</returns>
</member>
<member name="M:Loreline.IFields.LorelineExists(Loreline.Interpreter,System.String)">
<summary>
Check if a value exists for the given key
</summary>
<param name="interpreter">The interpreter instance</param>
<param name="key">The field key to check</param>
<returns>True if the key exists, false otherwise</returns>
</member>
<member name="M:Loreline.IFields.LorelineFields(Loreline.Interpreter)">
<summary>
Get all the fields of this object
</summary>
<param name="interpreter">The interpreter instance</param>
<returns>An array of field keys</returns>
</member>
<member name="T:Loreline.Interpreter">
<summary>
Main interpreter class for Loreline scripts.
This class is responsible for executing a parsed Loreline script,
managing the runtime state, and interacting with the host application
through handler functions.
</summary>
</member>
<member name="T:Loreline.Interpreter.TextTag">
<summary>
Represents a tag in text content, which can be used for styling or other purposes.
</summary>
</member>
<member name="F:Loreline.Interpreter.TextTag.Closing">
<summary>
Whether this is a closing tag.
</summary>
</member>
<member name="F:Loreline.Interpreter.TextTag.Value">
<summary>
The value or name of the tag.
</summary>
</member>
<member name="F:Loreline.Interpreter.TextTag.Offset">
<summary>
The offset in the text where this tag appears.
</summary>
</member>
<member name="T:Loreline.Interpreter.ChoiceOption">
<summary>
Represents a choice option presented to the user.
</summary>
</member>
<member name="F:Loreline.Interpreter.ChoiceOption.Text">
<summary>
The text of the choice option.
</summary>
</member>
<member name="F:Loreline.Interpreter.ChoiceOption.Tags">
<summary>
Any tags associated with the choice text.
</summary>
</member>
<member name="F:Loreline.Interpreter.ChoiceOption.Enabled">
<summary>
Whether this choice option is currently enabled.
</summary>
</member>
<member name="T:Loreline.Interpreter.Function">
<summary>
Delegate type for functions that can be called from the script.
</summary>
<param name="interpreter">The interpreter instance</param>
<param name="args">Arguments passed to the function</param>
<returns>The result of the function</returns>
</member>
<member name="T:Loreline.Interpreter.DialogueCallback">
<summary>
Callback type for dialogue continuation.
</summary>
</member>
<member name="T:Loreline.Interpreter.Dialogue">
<summary>
Contains information about a dialogue to be displayed to the user.
</summary>
</member>
<member name="F:Loreline.Interpreter.Dialogue.Interpreter">
<summary>
The interpreter instance.
</summary>
</member>
<member name="F:Loreline.Interpreter.Dialogue.Character">
<summary>
The character speaking (null for narrator text).
</summary>
</member>
<member name="F:Loreline.Interpreter.Dialogue.Text">
<summary>
The text content to display.
</summary>
</member>
<member name="F:Loreline.Interpreter.Dialogue.Tags">
<summary>
Any tags in the text.
</summary>
</member>
<member name="F:Loreline.Interpreter.Dialogue.Callback">
<summary>
Function to call when the text has been displayed.
</summary>
</member>
<member name="T:Loreline.Interpreter.DialogueHandler">
<summary>
Handler type for text output with callback.
This is called when the script needs to display text to the user.
</summary>
<param name="dialogue">A Dialogue structure containing all necessary information</param>
</member>
<member name="T:Loreline.Interpreter.ChoiceCallback">
<summary>
Callback type for choice selection.
</summary>
<param name="index">The index of the selected choice</param>
</member>
<member name="T:Loreline.Interpreter.Choice">
<summary>
Contains information about choices to be presented to the user.
</summary>
</member>
<member name="F:Loreline.Interpreter.Choice.Interpreter">
<summary>
The interpreter instance.
</summary>
</member>
<member name="F:Loreline.Interpreter.Choice.Options">
<summary>
The available choice options.
</summary>
</member>
<member name="F:Loreline.Interpreter.Choice.Callback">
<summary>
Function to call with the index of the selected choice.
</summary>
</member>
<member name="T:Loreline.Interpreter.ChoiceHandler">
<summary>
Handler type for choice presentation with callback.
This is called when the script needs to present choices to the user.
</summary>
<param name="choice">A Choice structure containing all necessary information</param>
</member>
<member name="T:Loreline.Interpreter.CreateFields">
<summary>
A custom instanciator to create fields objects.
</summary>
<param name="interpreter">The interpreter related to this object creation</param>
<param name="type">The expected type of the object, if there is any known</param>
<param name="node">The associated node in the script, if any</param>
<returns></returns>
</member>
<member name="T:Loreline.Interpreter.Finish">
<summary>
Contains information about the script execution completion.
</summary>
</member>
<member name="F:Loreline.Interpreter.Finish.Interpreter">
<summary>
The interpreter instance.
</summary>
</member>
<member name="M:Loreline.Interpreter.InterpreterOptions.Default">
<summary>
Retrieve default interpreter options
</summary>
</member>
<member name="F:Loreline.Interpreter.InterpreterOptions.Functions">
<summary>
Optional map of additional functions to make available to the script
</summary>
</member>
<member name="F:Loreline.Interpreter.InterpreterOptions.StrictAccess">
<summary>
Tells whether access is strict or not. If set to true,
trying to read or write an undefined variable will throw an error.
</summary>
</member>
<member name="F:Loreline.Interpreter.InterpreterOptions.CustomCreateFields">
<summary>
A custom instantiator to create fields objects.
</summary>
</member>
<member name="F:Loreline.Interpreter.InterpreterOptions.Translations">
<summary>
Optional translations map for localization.
Built from a parsed translation file using Engine.ExtractTranslations().
</summary>
</member>
<member name="T:Loreline.Interpreter.FinishHandler">
<summary>
Handler type to be called when the execution finishes.
</summary>
<param name="finish">A Finish structure containing the interpreter instance</param>
</member>
<member name="F:Loreline.Interpreter.RuntimeInterpreter">
<summary>
The underlying runtime interpreter instance.
</summary>
</member>
<member name="M:Loreline.Interpreter.#ctor(Loreline.Script,Loreline.Interpreter.DialogueHandler,Loreline.Interpreter.ChoiceHandler,Loreline.Interpreter.FinishHandler)">
<summary>
Creates a new Loreline script interpreter.
</summary>
<param name="script">The parsed script to execute</param>
<param name="handleDialogue">Function to call when displaying dialogue text</param>
<param name="handleChoice">Function to call when presenting choices</param>
<param name="handleFinish">Function to call when execution finishes</param>
</member>
<member name="M:Loreline.Interpreter.#ctor(Loreline.Script,Loreline.Interpreter.DialogueHandler,Loreline.Interpreter.ChoiceHandler,Loreline.Interpreter.FinishHandler,Loreline.Interpreter.InterpreterOptions)">
<summary>
Creates a new Loreline script interpreter.
</summary>
<param name="script">The parsed script to execute</param>
<param name="handleDialogue">Function to call when displaying dialogue text</param>
<param name="handleChoice">Function to call when presenting choices</param>
<param name="handleFinish">Function to call when execution finishes</param>
<param name="options">Additional options</param>
</member>
<member name="M:Loreline.Interpreter.Start(System.String)">
<summary>
Starts script execution from the beginning or a specific beat.
</summary>
<param name="beatName">Optional name of the beat to start from. If null, execution starts from
the first beat or a beat named "_" if it exists.</param>
<exception cref="T:Loreline.Runtime.RuntimeError">Thrown if the specified beat doesn't exist or if no beats are found in the script</exception>
</member>
<member name="M:Loreline.Interpreter.Save">
<summary>
Saves the current state of the interpreter.
This includes all state variables, character states, and execution stack,
allowing execution to be resumed later from the exact same point.
</summary>
<returns>A JSON string containing the serialized state</returns>
</member>
<member name="M:Loreline.Interpreter.Restore(System.String)">
<summary>
Restores the interpreter state from a previously saved state.
This allows resuming execution from a previously saved state.
</summary>
<param name="savedData">The JSON string containing the serialized state</param>
<exception cref="T:Loreline.Runtime.RuntimeError">Thrown if the save data version is incompatible</exception>
</member>
<member name="M:Loreline.Interpreter.Resume">
<summary>
Resumes execution after restoring state.
This should be called after Restore() to continue execution.
</summary>
</member>
<member name="M:Loreline.Interpreter.GetCharacter(System.String)">
<summary>
Gets a character by name.
</summary>
<param name="name">The name of the character to get</param>
<returns>The character's fields or null if the character doesn't exist</returns>
</member>
<member name="M:Loreline.Interpreter.GetCharacterField(System.String,System.String)">
<summary>
Gets a specific field of a character.
</summary>
<param name="character">The name of the character</param>
<param name="name">The name of the field to get</param>
<returns>The field value or null if the character or field doesn't exist</returns>
</member>
<member name="T:Loreline.Engine">
<summary>
The main public API for Loreline runtime.
Provides easy access to the core functionality for parsing and running Loreline scripts.
</summary>
</member>
<member name="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)">
<summary>
Parses the given text input and creates an executable <see cref="T:Loreline.Script"/> instance from it.
</summary>
<remarks>
This is the first step in working with a Loreline script. The returned
<see cref="T:Loreline.Script"/> object can then be passed to methods Play() or Resume().
</remarks>
<param name="input">The Loreline script content as a string (.lor format)</param>
<param name="filePath">(optional) The file path of the input being parsed. If provided, requires `handleFile` as well.</param>
<param name="handleFile">(optional) A file handler to read imports. If that handler is asynchronous, then `parse()` method will return null and `callback` argument should be used to get the final script</param>
<param name="callback">If provided, will be called with the resulting script as argument. Mostly useful when reading file imports asynchronously</param>
<returns>The parsed script as an AST <see cref="T:Loreline.Script"/> instance (if loaded synchronously)</returns>
<exception cref="T:Loreline.Runtime.Error">Thrown if the script contains syntax errors or other parsing issues</exception>
</member>
<member name="M:Loreline.Engine.Play(Loreline.Script,Loreline.Interpreter.DialogueHandler,Loreline.Interpreter.ChoiceHandler,Loreline.Interpreter.FinishHandler,System.String)">
<summary>
Starts playing a Loreline script from the beginning or a specific beat.
</summary>
<remarks>
This function takes care of initializing the interpreter and starting execution
immediately. You'll need to provide handlers for dialogues, choices, and
script completion.
</remarks>
<param name="script">The parsed script (result from <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/>)</param>
<param name="handleDialogue">Function called when dialogue text should be displayed</param>
<param name="handleChoice">Function called when player needs to make a choice</param>
<param name="handleFinish">Function called when script execution completes</param>
<param name="beatName">Name of a specific beat to start from (defaults to first beat)</param>
<returns>The interpreter instance that is running the script</returns>
</member>
<member name="M:Loreline.Engine.Play(Loreline.Script,Loreline.Interpreter.DialogueHandler,Loreline.Interpreter.ChoiceHandler,Loreline.Interpreter.FinishHandler,Loreline.Interpreter.InterpreterOptions)">
<summary>
Starts playing a Loreline script from the beginning or a specific beat.
</summary>
<remarks>
This function takes care of initializing the interpreter and starting execution
immediately. You'll need to provide handlers for dialogues, choices, and
script completion.
</remarks>
<param name="script">The parsed script (result from <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/>)</param>
<param name="handleDialogue">Function called when dialogue text should be displayed</param>
<param name="handleChoice">Function called when player needs to make a choice</param>
<param name="handleFinish">Function called when script execution completes</param>
<param name="options">Additional options</param>
<returns>The interpreter instance that is running the script</returns>
</member>
<member name="M:Loreline.Engine.Play(Loreline.Script,Loreline.Interpreter.DialogueHandler,Loreline.Interpreter.ChoiceHandler,Loreline.Interpreter.FinishHandler,System.String,Loreline.Interpreter.InterpreterOptions)">
<summary>
Starts playing a Loreline script from the beginning or a specific beat.
</summary>
<remarks>
This function takes care of initializing the interpreter and starting execution
immediately. You'll need to provide handlers for dialogues, choices, and
script completion.
</remarks>
<param name="script">The parsed script (result from <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/>)</param>
<param name="handleDialogue">Function called when dialogue text should be displayed</param>
<param name="handleChoice">Function called when player needs to make a choice</param>
<param name="handleFinish">Function called when script execution completes</param>
<param name="beatName">Name of a specific beat to start from (defaults to first beat)</param>
<param name="options">Additional options</param>
<returns>The interpreter instance that is running the script</returns>
</member>
<member name="M:Loreline.Engine.Resume(Loreline.Script,Loreline.Interpreter.DialogueHandler,Loreline.Interpreter.ChoiceHandler,Loreline.Interpreter.FinishHandler,System.String,System.String)">
<summary>
Resumes a previously saved Loreline script from its saved state.
</summary>
<remarks>
This allows you to continue a story from the exact point where it was saved,
restoring all state variables, choices, and player progress.
</remarks>
<param name="script">The parsed script (result from <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/>)</param>
<param name="handleDialogue">Function called when dialogue text should be displayed</param>
<param name="handleChoice">Function called when player needs to make a choice</param>
<param name="handleFinish">Function called when script execution completes</param>
<param name="saveData">The saved game data (typically from <see cref="M:Loreline.Interpreter.Save"/>)</param>
<param name="beatName">Optional beat name to override where to resume from</param>
<returns>The interpreter instance that is running the script</returns>
</member>
<member name="M:Loreline.Engine.Resume(Loreline.Script,Loreline.Interpreter.DialogueHandler,Loreline.Interpreter.ChoiceHandler,Loreline.Interpreter.FinishHandler,System.String,Loreline.Interpreter.InterpreterOptions)">
<summary>
Resumes a previously saved Loreline script from its saved state.
</summary>
<remarks>
This allows you to continue a story from the exact point where it was saved,
restoring all state variables, choices, and player progress.
</remarks>
<param name="script">The parsed script (result from <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/>)</param>
<param name="handleDialogue">Function called when dialogue text should be displayed</param>
<param name="handleChoice">Function called when player needs to make a choice</param>
<param name="handleFinish">Function called when script execution completes</param>
<param name="saveData">The saved game data (typically from <see cref="M:Loreline.Interpreter.Save"/>)</param>
<param name="options">Additional options</param>
<returns>The interpreter instance that is running the script</returns>
</member>
<member name="M:Loreline.Engine.Resume(Loreline.Script,Loreline.Interpreter.DialogueHandler,Loreline.Interpreter.ChoiceHandler,Loreline.Interpreter.FinishHandler,System.String,System.String,Loreline.Interpreter.InterpreterOptions)">
<summary>
Resumes a previously saved Loreline script from its saved state.
</summary>
<remarks>
This allows you to continue a story from the exact point where it was saved,
restoring all state variables, choices, and player progress.
</remarks>
<param name="script">The parsed script (result from <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/>)</param>
<param name="handleDialogue">Function called when dialogue text should be displayed</param>
<param name="handleChoice">Function called when player needs to make a choice</param>
<param name="handleFinish">Function called when script execution completes</param>
<param name="saveData">The saved game data (typically from <see cref="M:Loreline.Interpreter.Save"/>)</param>
<param name="beatName">Optional beat name to override where to resume from</param>
<param name="options">Additional options</param>
<returns>The interpreter instance that is running the script</returns>
</member>
<member name="M:Loreline.Engine.ExtractTranslations(Loreline.Script)">
<summary>
Extracts translations from a parsed translation script.
</summary>
<remarks>
Given a translation file parsed with <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/>, this returns a translations map
that can be passed as <see cref="F:Loreline.Interpreter.InterpreterOptions.Translations"/> to
Play() or Resume().
</remarks>
<param name="script">The parsed translation script (result from <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/> on a .XX.lor file)</param>
<returns>A translations object to pass as <see cref="F:Loreline.Interpreter.InterpreterOptions.Translations"/></returns>
</member>
<member name="M:Loreline.Engine.Print(Loreline.Script,System.String,System.String)">
<summary>
Prints a parsed script back into Loreline source code.
</summary>
<param name="script">The parsed script (result from <see cref="M:Loreline.Engine.Parse(System.String,System.String,Loreline.Engine.ImportsFileHandler,Loreline.Engine.ParseCallback)"/>)</param>
<param name="indent">The indentation string to use (defaults to two spaces)</param>
<param name="newline">The newline string to use (defaults to "\n")</param>
<returns>The printed source code as a string</returns>
</member>
<member name="T:Loreline.Node">
<summary>
Represents a node in a Loreline AST.
</summary>
</member>
<member name="F:Loreline.Node.RuntimeNode">
<summary>
The underlying runtime node instance.
</summary>
</member>
<member name="F:Loreline.Node.Type">
<summary>
The type of the node as string
</summary>
</member>
<member name="F:Loreline.Node.Id">
<summary>
The id of this node (should be unique within a single script hierarchy)
</summary>
</member>
<member name="M:Loreline.Node.ToJson(System.Boolean)">
<summary>
Converts the node to a JSON representation.
This can be used for debugging or serialization purposes.
</summary>
<param name="pretty">Whether to format the JSON with indentation and line breaks</param>
<returns>A JSON string representation of the node</returns>
</member>
<member name="T:Loreline.Script">
<summary>
Represents the root node of a Loreline script AST.
</summary>
</member>
<member name="F:Loreline.Script.RuntimeScript">
<summary>
The underlying runtime script instance.
</summary>
</member>
<member name="M:Loreline.Script.#ctor(Loreline.Runtime.Script)">
<summary>
Creates a new Script instance with the provided runtime script.
</summary>
<param name="runtimeScript">The parsed runtime script to wrap</param>
</member>
</members>
</doc>

View file

@ -0,0 +1,258 @@
using Loreline;
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
using OpenTheBox.Localization;
using OpenTheBox.Rendering;
namespace OpenTheBox.Adventures;
/// <summary>
/// Represents a single event produced during an adventure, such as granting an item
/// or modifying a resource.
/// </summary>
public sealed record AdventureEvent(GameEventKind Kind, string TargetId, int Amount = 1);
/// <summary>
/// The kind of event that occurred during an adventure.
/// </summary>
public enum GameEventKind
{
ItemGranted,
ItemRemoved,
ResourceAdded
}
/// <summary>
/// Wraps the Loreline API for adventure playback. Loads <c>.lor</c> script files,
/// registers custom game functions, and bridges the callback-based Loreline engine
/// into async/await via <see cref="TaskCompletionSource"/>.
/// </summary>
public sealed class AdventureEngine
{
private static readonly string AdventuresRoot = Path.Combine("content", "adventures");
private readonly IRenderer _renderer;
private readonly LocalizationManager _loc;
public AdventureEngine(IRenderer renderer, LocalizationManager loc)
{
_renderer = renderer;
_loc = loc;
}
/// <summary>
/// Plays an adventure for the given theme, interacting with the player through the
/// renderer and collecting game events (items granted, resources modified, etc.).
/// </summary>
public async Task<List<AdventureEvent>> PlayAdventure(AdventureTheme theme, GameState state)
{
string themeName = theme.ToString().ToLowerInvariant();
string scriptPath = Path.Combine(AdventuresRoot, themeName, "intro.lor");
if (!File.Exists(scriptPath))
{
_renderer.ShowError($"Adventure script not found: {scriptPath}");
return [];
}
string content = await File.ReadAllTextAsync(scriptPath);
var events = new List<AdventureEvent>();
string adventureId = $"{themeName}/intro";
// Parse the script, handling file imports synchronously
Script script = Engine.Parse(
content,
scriptPath,
(path, callback) =>
{
string dir = Path.GetDirectoryName(scriptPath) ?? ".";
string importPath = Path.Combine(dir, path);
if (File.Exists(importPath))
{
callback(File.ReadAllText(importPath));
}
else
{
callback(string.Empty);
}
});
if (script is null)
{
_renderer.ShowError("Failed to parse adventure script.");
return [];
}
// Build interpreter options with custom functions and optional translations
var options = Interpreter.InterpreterOptions.Default();
options.Functions = BuildCustomFunctions(state, events);
// Load translations if the current locale is not English
if (_loc.CurrentLocale != Locale.EN)
{
string localeSuffix = _loc.CurrentLocale.ToString().ToLowerInvariant();
string translationPath = Path.Combine(
AdventuresRoot, themeName, $"intro.{localeSuffix}.lor");
if (File.Exists(translationPath))
{
string translationContent = File.ReadAllText(translationPath);
Script translationScript = Engine.Parse(translationContent);
if (translationScript is not null)
{
options.Translations = Engine.ExtractTranslations(translationScript);
}
}
}
// Use a TaskCompletionSource to bridge the callback-based API into await
var tcs = new TaskCompletionSource<bool>();
// Check for existing save data to resume
bool hasSave = state.AdventureSaveData.TryGetValue(adventureId, out string? saveData)
&& !string.IsNullOrEmpty(saveData);
Loreline.Interpreter interpreter;
if (hasSave)
{
interpreter = Engine.Resume(
script,
dialogue => HandleDialogue(dialogue),
choice => HandleChoice(choice),
finish => HandleFinish(finish, tcs),
saveData!,
options: options);
}
else
{
interpreter = Engine.Play(
script,
dialogue => HandleDialogue(dialogue),
choice => HandleChoice(choice),
finish => HandleFinish(finish, tcs),
options: options);
}
// Wait for the adventure to finish
await tcs.Task;
// Clear the save data for this adventure since it completed
state.AdventureSaveData.Remove(adventureId);
return events;
}
/// <summary>
/// Saves the progress of the currently running adventure into the game state.
/// </summary>
public void SaveProgress(string adventureId, GameState state, Loreline.Interpreter interpreter)
{
string saveJson = interpreter.Save();
state.AdventureSaveData[adventureId] = saveJson;
}
// ── Loreline handlers ───────────────────────────────────────────────
private void HandleDialogue(Loreline.Interpreter.Dialogue dialogue)
{
_renderer.ShowAdventureDialogue(dialogue.Character, dialogue.Text);
_renderer.WaitForKeyPress();
dialogue.Callback();
}
private void HandleChoice(Loreline.Interpreter.Choice choice)
{
var options = new List<string>();
foreach (var opt in choice.Options)
{
options.Add(opt.Enabled ? opt.Text : $"(unavailable) {opt.Text}");
}
int selectedIndex;
while (true)
{
selectedIndex = _renderer.ShowAdventureChoice(options);
// Ensure the selected option is enabled
if (choice.Options[selectedIndex].Enabled)
break;
_renderer.ShowError("That option is not available.");
}
choice.Callback(selectedIndex);
}
private static void HandleFinish(
Loreline.Interpreter.Finish finish,
TaskCompletionSource<bool> tcs)
{
tcs.TrySetResult(true);
}
// ── Custom functions registered into the Loreline interpreter ────────
private Dictionary<string, Loreline.Interpreter.Function> BuildCustomFunctions(
GameState state,
List<AdventureEvent> events)
{
return new Dictionary<string, Loreline.Interpreter.Function>
{
["grantItem"] = (interpreter, args) =>
{
if (args.Length < 1) return null!;
string itemDefId = args[0]?.ToString() ?? string.Empty;
int quantity = args.Length >= 2 && args[1] is double d ? (int)d : 1;
state.AddItem(ItemInstance.Create(itemDefId, quantity));
events.Add(new AdventureEvent(GameEventKind.ItemGranted, itemDefId, quantity));
_renderer.ShowMessage(_loc.Get("adventure.item_granted", itemDefId, quantity));
return null!;
},
["hasItem"] = (interpreter, args) =>
{
if (args.Length < 1) return false;
string itemDefId = args[0]?.ToString() ?? string.Empty;
return state.HasItem(itemDefId);
},
["addResource"] = (interpreter, args) =>
{
if (args.Length < 2) return null!;
string resourceName = args[0]?.ToString() ?? string.Empty;
int amount = args[1] is double d ? (int)d : 0;
events.Add(new AdventureEvent(GameEventKind.ResourceAdded, resourceName, amount));
_renderer.ShowMessage(_loc.Get("adventure.resource_added", resourceName, amount));
return null!;
},
["removeItem"] = (interpreter, args) =>
{
if (args.Length < 1) return null!;
string itemDefId = args[0]?.ToString() ?? string.Empty;
// Find the first matching item and remove it
var item = state.Inventory.FirstOrDefault(i => i.DefinitionId == itemDefId);
if (item is not null)
{
state.RemoveItem(item.Id);
events.Add(new AdventureEvent(GameEventKind.ItemRemoved, itemDefId));
_renderer.ShowMessage(_loc.Get("adventure.item_removed", itemDefId));
}
return null!;
}
};
}
}

View file

@ -0,0 +1,16 @@
using OpenTheBox.Core.Enums;
namespace OpenTheBox.Core.Boxes;
/// <summary>
/// Static definition of a box type, including its loot table and opening requirements.
/// </summary>
public sealed record BoxDefinition(
string Id,
string NameKey,
string DescriptionKey,
ItemRarity Rarity,
LootTable LootTable,
bool IsAutoOpen,
List<string>? RequiredItems = null
);

View file

@ -0,0 +1,13 @@
using OpenTheBox.Core.Enums;
namespace OpenTheBox.Core.Boxes;
/// <summary>
/// Condition that must be met for a loot entry to be eligible for dropping.
/// </summary>
public sealed record LootCondition(
LootConditionType Type,
string? TargetId = null,
string? Comparison = null,
float? Value = null
);

View file

@ -0,0 +1,11 @@
namespace OpenTheBox.Core.Boxes;
/// <summary>
/// A single entry in a loot table, mapping an item definition to a drop weight
/// with an optional eligibility condition.
/// </summary>
public sealed record LootEntry(
string ItemDefinitionId,
float Weight,
LootCondition? Condition = null
);

View file

@ -0,0 +1,10 @@
namespace OpenTheBox.Core.Boxes;
/// <summary>
/// A loot table containing entries that can be rolled when opening a box.
/// </summary>
public sealed record LootTable(
List<LootEntry> Entries,
int GuaranteedRolls,
int RollCount
);

View file

@ -0,0 +1,17 @@
using OpenTheBox.Core.Enums;
namespace OpenTheBox.Core.Characters;
/// <summary>
/// Visual appearance configuration for the player character.
/// </summary>
public sealed record PlayerAppearance
{
public HairStyle HairStyle { get; init; }
public TintColor HairTint { get; init; }
public EyeStyle EyeStyle { get; init; }
public BodyStyle BodyStyle { get; init; }
public LegStyle LegStyle { get; init; }
public ArmStyle ArmStyle { get; init; }
public TintColor BodyTint { get; init; }
}

View file

@ -0,0 +1,36 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// The 9 thematic worlds for interactive adventures.
/// Each theme has its own locations, characters, key resources, moods, and environments.
/// Adventures are accessed via AdventureTokens found in Boite aventure.
/// </summary>
public enum AdventureTheme
{
/// <summary>Science-fiction and space exploration. Key resource: Oxygen.</summary>
Space,
/// <summary>Fantasy medieval setting with castles and dungeons. Key resources: Mana, Stamina.</summary>
Medieval,
/// <summary>Golden age of piracy with ships and treasure islands. Key resources: Gold, Stamina.</summary>
Pirate,
/// <summary>Modern-day urban thriller setting. Key resources: Energy, Gold.</summary>
Contemporary,
/// <summary>Romance and human relationships. Key resources: Health (emotional), Mana (intuition).</summary>
Sentimental,
/// <summary>Prehistoric era with primitive survival. Key resources: Food, Stamina.</summary>
Prehistoric,
/// <summary>Cosmic scale with divine entities and parallel dimensions. Key resources: Mana, Energy.</summary>
Cosmic,
/// <summary>Microscopic world of cells, atoms, and circuits. Key resources: Energy, Oxygen.</summary>
Microscopic,
/// <summary>Dark fantasy with gothic horror elements. Key resources: Blood, Mana. Unlocks Blood resource.</summary>
DarkFantasy
}

View file

@ -0,0 +1,29 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Available arm styles for the <see cref="CosmeticSlot.Arms"/> slot.
/// Includes natural arm variations and fantastical replacements.
/// </summary>
public enum ArmStyle
{
/// <summary>No arm style equipped (default).</summary>
None,
/// <summary>Short arms. Common rarity.</summary>
Short,
/// <summary>Normal-length arms. Common rarity.</summary>
Regular,
/// <summary>Elongated arms. Uncommon rarity.</summary>
Long,
/// <summary>Mechanical prosthetic arms. Epic rarity.</summary>
Mechanical,
/// <summary>Wings replacing arms, enabling flight. Legendary rarity.</summary>
Wings,
/// <summary>An additional pair of arms (4 total). Mythic rarity.</summary>
ExtraPair
}

View file

@ -0,0 +1,26 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Available body styles for the <see cref="CosmeticSlot.Body"/> slot.
/// Represents torso clothing and armor options.
/// </summary>
public enum BodyStyle
{
/// <summary>Bare torso (default). Common rarity.</summary>
Naked,
/// <summary>Standard everyday t-shirt. Common rarity.</summary>
RegularTShirt,
/// <summary>Low-cut fashionable t-shirt. Uncommon rarity.</summary>
SexyTShirt,
/// <summary>Elegant formal suit. Rare rarity.</summary>
Suit,
/// <summary>Full plate armor. Epic rarity.</summary>
Armored,
/// <summary>Robotic mechanical body. Legendary rarity.</summary>
Robotic
}

View file

@ -0,0 +1,23 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// The 5 cosmetic equipment slots available for player customization.
/// Each slot can hold one style item and optionally a <see cref="TintColor"/>.
/// </summary>
public enum CosmeticSlot
{
/// <summary>Head slot for hair styles. See <see cref="HairStyle"/>.</summary>
Hair,
/// <summary>Face slot for eye styles and glasses. See <see cref="EyeStyle"/>.</summary>
Eyes,
/// <summary>Torso slot for shirts and armor. See <see cref="BodyStyle"/>.</summary>
Body,
/// <summary>Lower body slot for pants, boots, and leg cosmetics. See <see cref="LegStyle"/>.</summary>
Legs,
/// <summary>Arm slot for arm styles and appendages. See <see cref="ArmStyle"/>.</summary>
Arms
}

View file

@ -0,0 +1,17 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Broad environment categories for adventure locations.
/// Determines the visual theme and available interactions within a scene.
/// </summary>
public enum EnvironmentType
{
/// <summary>Wilderness, forests, caves, plains, and open landscapes.</summary>
Nature,
/// <summary>Urban areas including villages, cities, stations, and interiors.</summary>
Town,
/// <summary>Aquatic environments: oceans, rivers, underwater, and coastal areas.</summary>
Water
}

View file

@ -0,0 +1,41 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Available eye styles for the <see cref="CosmeticSlot.Eyes"/> slot.
/// Includes natural eye colors and various eyewear options.
/// </summary>
public enum EyeStyle
{
/// <summary>No eye style equipped (default).</summary>
None,
/// <summary>Blue eyes. Common rarity.</summary>
Blue,
/// <summary>Green eyes. Common rarity.</summary>
Green,
/// <summary>Red-orange eyes. Uncommon rarity.</summary>
RedOrange,
/// <summary>Brown eyes. Common rarity.</summary>
Brown,
/// <summary>Deep black eyes. Uncommon rarity.</summary>
Black,
/// <summary>Classic sunglasses. Rare rarity.</summary>
Sunglasses,
/// <summary>Aviator-style pilot glasses. Rare rarity.</summary>
PilotGlasses,
/// <summary>Fighter pilot aircraft glasses. Epic rarity.</summary>
AircraftGlasses,
/// <summary>Glowing cybernetic eye implants. Legendary rarity.</summary>
CyberneticEyes,
/// <summary>Round golden magician spectacles. Epic rarity.</summary>
MagicianGlasses
}

View file

@ -0,0 +1,27 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Unlockable font styles for the CLI interface.
/// Found in Boite Meta. The default console font is always available;
/// additional fonts are unlocked as drops.
/// </summary>
public enum FontStyle
{
/// <summary>The default system console font. Always available.</summary>
Default,
/// <summary>Consolas monospaced font. Clean and technical.</summary>
Consolas,
/// <summary>Firetruc display font. Bold and playful.</summary>
Firetruc,
/// <summary>JetBrains Mono font. Developer-focused with ligatures.</summary>
Jetbrains,
/// <summary>Vercel One font. Modern and sleek.</summary>
VercelOne,
/// <summary>Toto Posted One font. Artistic and distinctive.</summary>
TotoPostedOne
}

View file

@ -0,0 +1,33 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Available hair styles for the <see cref="CosmeticSlot.Hair"/> slot.
/// Styles range from Common to Mythic rarity.
/// Legendary+ styles are guaranteed drops from Boite legend'hair.
/// </summary>
public enum HairStyle
{
/// <summary>No hair style equipped (default).</summary>
None,
/// <summary>Classic short hair. Common rarity.</summary>
Short,
/// <summary>Long flowing hair. Common rarity.</summary>
Long,
/// <summary>Ponytail hairstyle. Uncommon rarity.</summary>
Ponytail,
/// <summary>Elaborate braided hair. Rare rarity.</summary>
Braided,
/// <summary>High-tech hairstyle with neon effects. Epic rarity.</summary>
Cyberpunk,
/// <summary>Living flame hair that flickers and glows. Legendary rarity.</summary>
Fire,
/// <summary>Hair made of shimmering stardust, constantly shifting. Mythic rarity.</summary>
StardustLegendary
}

View file

@ -0,0 +1,29 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Types of results produced by the auto-activation system
/// when items interact with each other in the player's inventory.
/// </summary>
public enum InteractionResultType
{
/// <summary>Opens a box or chest, revealing its contents.</summary>
OpenBox,
/// <summary>Crafts a new item from materials at a workstation.</summary>
Craft,
/// <summary>Transforms one item into another (e.g., via TransformationPentacle).</summary>
Transform,
/// <summary>Consumes an item to restore a resource (potion, food).</summary>
Consume,
/// <summary>Unlocks new content such as a location, adventure, or character.</summary>
Unlock,
/// <summary>Combines multiple items into a single more powerful item.</summary>
Combine,
/// <summary>Teleports the player to a new location.</summary>
Teleport
}

View file

@ -0,0 +1,57 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Categories of items that can be found inside boxes.
/// Each category determines how the item behaves in the inventory
/// and which systems can interact with it.
/// </summary>
public enum ItemCategory
{
/// <summary>A box that can be opened to reveal other items.</summary>
Box,
/// <summary>A key that auto-activates when a matching lock or chest is present.</summary>
Key,
/// <summary>A commemorative badge earned through progression or special events.</summary>
Badge,
/// <summary>A map that can unlock locations when combined with exploration badges.</summary>
Map,
/// <summary>A cosmetic item that changes the player's visual appearance.</summary>
Cosmetic,
/// <summary>A raw, refined, or shaped material used in crafting recipes.</summary>
Material,
/// <summary>A consumable item that restores resources or grants temporary buffs.</summary>
Consumable,
/// <summary>A blueprint that allows construction of a new crafting workstation.</summary>
WorkstationBlueprint,
/// <summary>A collectible fragment of the game's lore and narrative.</summary>
LoreFragment,
/// <summary>A token granting access to a themed interactive adventure.</summary>
AdventureToken,
/// <summary>A meta-upgrade that enhances the CLI interface itself.</summary>
Meta,
/// <summary>A cookie that grants fortune messages, buffs, or meta effects.</summary>
Cookie,
/// <summary>A music item representing ambient themes, jingles, or character themes.</summary>
Music,
/// <summary>An item tied to the narrative that advances the story.</summary>
StoryItem,
/// <summary>An item produced by a crafting workstation.</summary>
CraftedItem,
/// <summary>An item required to complete a character's personal quest.</summary>
QuestItem
}

View file

@ -0,0 +1,26 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Rarity tiers for items, cosmetics, and loot drops.
/// Higher rarities have lower drop weights and stronger effects.
/// </summary>
public enum ItemRarity
{
/// <summary>The most common tier. Frequently found in all box types.</summary>
Common,
/// <summary>Slightly rarer than Common. Minor upgrades over base items.</summary>
Uncommon,
/// <summary>Noticeably rare. Provides meaningful improvements.</summary>
Rare,
/// <summary>Very rare. Significant power or visual distinction.</summary>
Epic,
/// <summary>Extremely rare. Guaranteed from Boite legend'hair and Boite legendaire.</summary>
Legendary,
/// <summary>The rarest tier in the game. Items of mythical power and uniqueness.</summary>
Mythic
}

View file

@ -0,0 +1,32 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Available leg styles for the <see cref="CosmeticSlot.Legs"/> slot.
/// Includes clothing, boots, and fantastical lower-body replacements.
/// </summary>
public enum LegStyle
{
/// <summary>No leg style equipped (default).</summary>
None,
/// <summary>Bare legs. Common rarity.</summary>
Naked,
/// <summary>Simple undergarment. Common rarity.</summary>
Slip,
/// <summary>Casual shorts. Uncommon rarity.</summary>
Short,
/// <summary>Stockings or tights. Uncommon rarity.</summary>
Panty,
/// <summary>Jet-propelled boots for flight. Epic rarity.</summary>
RocketBoots,
/// <summary>Pirate wooden leg. Rare rarity.</summary>
PegLeg,
/// <summary>Tentacles replacing legs entirely. Legendary rarity.</summary>
Tentacles
}

View file

@ -0,0 +1,15 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Supported localization languages.
/// French is the primary language; English is secondary.
/// Character names are not localized; all other text content is.
/// </summary>
public enum Locale
{
/// <summary>English (secondary language).</summary>
EN,
/// <summary>French (primary language).</summary>
FR
}

View file

@ -0,0 +1,36 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Condition types used in loot tables to control item availability.
/// When a condition is not met, the corresponding entry is excluded
/// from the weighted loot roll and its weight is redistributed.
/// </summary>
public enum LootConditionType
{
/// <summary>The player possesses a specific item in their inventory.</summary>
HasItem,
/// <summary>The player does NOT possess a specific item in their inventory.</summary>
HasNotItem,
/// <summary>A specified resource is above a given threshold value.</summary>
ResourceAbove,
/// <summary>A specified resource is below a given threshold value.</summary>
ResourceBelow,
/// <summary>The total number of boxes opened exceeds a threshold.</summary>
BoxesOpenedAbove,
/// <summary>The player has unlocked a specific <see cref="UIFeature"/>.</summary>
HasUIFeature,
/// <summary>The player has built a specific <see cref="WorkstationType"/>.</summary>
HasWorkstation,
/// <summary>The player has access to a specific <see cref="AdventureTheme"/>.</summary>
HasAdventure,
/// <summary>The player owns a specific cosmetic item.</summary>
HasCosmetic
}

View file

@ -0,0 +1,36 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Shapes that a <see cref="MaterialType"/> can take.
/// Materials are transformed between forms at crafting workstations.
/// With 7 material types and 9 forms, the system offers 63 possible combinations.
/// </summary>
public enum MaterialForm
{
/// <summary>Unprocessed material as found in loot drops. No station required.</summary>
Raw,
/// <summary>Processed material ready for further shaping. Produced at Foundry.</summary>
Refined,
/// <summary>Small fastener used in construction. Produced at Anvil.</summary>
Nail,
/// <summary>Flat board, primarily for wood. Produced at SawingPost.</summary>
Plank,
/// <summary>Solid bar, primarily for metals. Produced at Furnace.</summary>
Ingot,
/// <summary>Thin flat piece. Produced at Forge.</summary>
Sheet,
/// <summary>Thin strand for weaving and sewing. Produced at Loom.</summary>
Thread,
/// <summary>Fine powder for alchemy and potions. Produced at MortarAndPestle.</summary>
Dust,
/// <summary>Cut gemstone for jewelry and enchantment. Produced at Jewelry station.</summary>
Gem
}

View file

@ -0,0 +1,30 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// The 7 material types used in crafting, ordered by tier (1-7).
/// Higher-tier materials are rarer and produce more powerful crafted items.
/// Each material can exist in any of the <see cref="MaterialForm"/> shapes.
/// </summary>
public enum MaterialType
{
/// <summary>Tier 1. The most basic material. Common drop.</summary>
Wood,
/// <summary>Tier 2. A simple alloy. Common drop.</summary>
Bronze,
/// <summary>Tier 3. An intermediate metal. Uncommon drop.</summary>
Iron,
/// <summary>Tier 4. An advanced alloy. Rare drop.</summary>
Steel,
/// <summary>Tier 5. High-technology metal. Epic drop.</summary>
Titanium,
/// <summary>Tier 6. Precious crystalline material. Legendary drop.</summary>
Diamond,
/// <summary>Tier 7. The ultimate synthetic material. Mythic drop.</summary>
CarbonFiber
}

View file

@ -0,0 +1,27 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Narrative moods that color the tone of adventures and story events.
/// Each <see cref="AdventureTheme"/> supports a subset of moods
/// that influence dialogue, encounters, and outcomes.
/// </summary>
public enum Mood
{
/// <summary>Grim, ominous, and foreboding atmosphere.</summary>
Dark,
/// <summary>Light-hearted, humorous, and playful tone.</summary>
Comedy,
/// <summary>Sorrowful events with loss and sacrifice.</summary>
Tragedy,
/// <summary>Romantic relationships and emotional connections.</summary>
Romance,
/// <summary>Eerie, unsettling, and frightening ambiance.</summary>
Spooky,
/// <summary>Mystery-driven narrative focused on clues and deduction.</summary>
Investigation
}

View file

@ -0,0 +1,33 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// The 8 player resources that govern survival, exploration, crafting, and combat.
/// Each resource has a maximum cap that can be upgraded via Boite d'amelioration.
/// Resources are unlocked progressively through gameplay.
/// </summary>
public enum ResourceType
{
/// <summary>Hit points. Unlocked from the start. Used for survival and combat.</summary>
Health,
/// <summary>Magic points. Unlocked on first encounter with a magical character.</summary>
Mana,
/// <summary>Nourishment. Unlocked on first adventure explored.</summary>
Food,
/// <summary>Physical endurance. Unlocked on first physical action (combat, craft).</summary>
Stamina,
/// <summary>Blood resource. Unlocked with the DarkFantasy theme. Used for dark rituals.</summary>
Blood,
/// <summary>Currency. Unlocked when a trading location is discovered.</summary>
Gold,
/// <summary>Breathable supply. Unlocked with Space theme or underwater adventures.</summary>
Oxygen,
/// <summary>Power supply. Unlocked when the first crafting workstation is built.</summary>
Energy
}

View file

@ -0,0 +1,26 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Player character statistics that influence gameplay outcomes.
/// Stats can be increased via Boite d'amelioration and leveling.
/// </summary>
public enum StatType
{
/// <summary>Physical power. Affects melee damage and carrying capacity.</summary>
Strength,
/// <summary>Mental acuity. Affects magic damage and crafting efficiency.</summary>
Intelligence,
/// <summary>Fortune factor. Affects loot drop probabilities and critical chances.</summary>
Luck,
/// <summary>Social influence. Affects merchant prices and dialogue options.</summary>
Charisma,
/// <summary>Agility and precision. Affects dodge chance, speed, and accuracy.</summary>
Dexterity,
/// <summary>Insight and intuition. Affects mana regeneration and perception.</summary>
Wisdom
}

View file

@ -0,0 +1,39 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Semantic color slots for text rendering in the CLI interface.
/// Each slot can be customized by the player after unlocking <see cref="UIFeature.TextColors"/>.
/// Additional color slots are unlocked via Boite Meta drops.
/// </summary>
public enum TextColor
{
/// <summary>Primary text color for general interface elements.</summary>
Primary,
/// <summary>Secondary text color for supporting information.</summary>
Secondary,
/// <summary>Tertiary text color for less prominent details.</summary>
Tertiary,
/// <summary>Color used for character and player names.</summary>
Name,
/// <summary>Color used for item names in the inventory and loot displays.</summary>
Item,
/// <summary>Color used for quantity numbers.</summary>
Quantity,
/// <summary>Color used for box names and box-related text.</summary>
Box,
/// <summary>Color used for equipment names (armor, accessories).</summary>
Equipment,
/// <summary>Color used for weapon names.</summary>
Weapon,
/// <summary>Color used for material names and crafting ingredients.</summary>
Material
}

View file

@ -0,0 +1,45 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Tint colors that can be applied to any <see cref="CosmeticSlot"/>.
/// Tints modify the visual color of the equipped cosmetic item.
/// Found in Boite stylee.
/// </summary>
public enum TintColor
{
/// <summary>No tint applied; original color preserved.</summary>
None,
/// <summary>Cyan tint. Common rarity.</summary>
Cyan,
/// <summary>Orange tint. Common rarity.</summary>
Orange,
/// <summary>Purple tint. Uncommon rarity.</summary>
Purple,
/// <summary>Warm pink tint. Uncommon rarity.</summary>
WarmPink,
/// <summary>Lightened version of the base color. Common rarity.</summary>
Light,
/// <summary>Darkened version of the base color. Common rarity.</summary>
Dark,
/// <summary>Multicolored rainbow effect. Epic rarity.</summary>
Rainbow,
/// <summary>Bright neon glow effect. Rare rarity.</summary>
Neon,
/// <summary>Metallic silver tint. Rare rarity.</summary>
Silver,
/// <summary>Metallic gold tint. Epic rarity.</summary>
Gold,
/// <summary>Absolute black with starfield reflections. Legendary rarity.</summary>
Void
}

View file

@ -0,0 +1,45 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// Unlockable CLI interface features that define the visual and interactive
/// progression of the game. Found exclusively in Boite Meta.
/// Phases 0-8 progressively transform the raw console into a full Spectre.Console layout.
/// </summary>
public enum UIFeature
{
/// <summary>Phase 1: Basic 8-color ANSI text coloring for items, names, and quantities.</summary>
TextColors,
/// <summary>Phase 2: Extended 256-color ANSI palette with gradients and shading.</summary>
ExtendedColors,
/// <summary>Phase 3: Navigate menus with arrow keys instead of typing commands.</summary>
ArrowKeySelection,
/// <summary>Phase 4: Persistent side panel displaying the player's inventory.</summary>
InventoryPanel,
/// <summary>Phase 5: Resource bars (HP, Mana, Food, etc.) displayed as bar charts.</summary>
ResourcePanel,
/// <summary>Phase 5: Player statistics panel showing Strength, Intelligence, etc.</summary>
StatsPanel,
/// <summary>Phase 6: ASCII art portrait reflecting equipped cosmetics.</summary>
PortraitPanel,
/// <summary>Phase 6: Chat panel for NPC dialogues and narrative events.</summary>
ChatPanel,
/// <summary>Phase 8: Complete multi-panel layout with all UI elements organized.</summary>
FullLayout,
/// <summary>Phase 8: Keyboard shortcuts for all major actions.</summary>
KeyboardShortcuts,
/// <summary>Phase 7: Animated box-opening sequences with scrolling text and ASCII effects.</summary>
BoxAnimation,
/// <summary>Phase 7: Dedicated crafting panel for material transformation and item creation.</summary>
CraftingPanel
}

View file

@ -0,0 +1,105 @@
namespace OpenTheBox.Core.Enums;
/// <summary>
/// The 32 crafting workstation types that can be built from blueprints.
/// Workstations transform materials between forms and produce crafted items.
/// Blueprints are found in Boite d'amelioration and Boite legendaire.
/// </summary>
public enum WorkstationType
{
/// <summary>Refines raw materials into processed form. (Raw -> Refined)</summary>
Foundry,
/// <summary>General-purpose crafting surface for basic recipes.</summary>
Workbench,
/// <summary>High-heat oven for smelting ingots. (Refined -> Ingot)</summary>
Furnace,
/// <summary>Weaving machine for producing threads. (Refined -> Thread)</summary>
Loom,
/// <summary>Heavy metalworking surface for nails and tools. (Ingot -> Nail)</summary>
Anvil,
/// <summary>Mystical table for brewing potions and elixirs.</summary>
AlchemyTable,
/// <summary>Advanced metalworking station for sheets and weapons. (Ingot -> Sheet)</summary>
Forge,
/// <summary>Lumber processing station. (Wood -> Plank)</summary>
SawingPost,
/// <summary>Wind-powered grain processing station.</summary>
Windmill,
/// <summary>Water-powered hydraulic transformation station.</summary>
Watermill,
/// <summary>Press for extracting oils from raw materials.</summary>
OilPress,
/// <summary>Workshop for creating ceramic objects.</summary>
PotteryWorkshop,
/// <summary>Tailoring surface for creating clothing and cosmetics.</summary>
TailorTable,
/// <summary>Grinding station for producing fine powders. (Refined -> Dust)</summary>
MortarAndPestle,
/// <summary>Basin for applying and creating tint colors for cosmetics.</summary>
DyeBasin,
/// <summary>Precision workstation for cutting gems and crafting jewelry. (Refined -> Gem)</summary>
Jewelry,
/// <summary>Smoking chamber for food preservation.</summary>
Smoker,
/// <summary>Large vat for brewing beverages and potions.</summary>
BrewingVat,
/// <summary>Technical desk for designing advanced blueprints.</summary>
EngineerDesk,
/// <summary>Station for joining metal components together.</summary>
WeldingStation,
/// <summary>Drafting table for creating plans and schematics.</summary>
DrawingTable,
/// <summary>Bench for detailed engraving and enchantment work.</summary>
EngravingBench,
/// <summary>Station for stitching threads into garments. (Thread -> clothing)</summary>
SewingPost,
/// <summary>Enchanted cauldron for brewing powerful magical potions.</summary>
MagicCauldron,
/// <summary>Arcane pentacle for transforming objects into different forms.</summary>
TransformationPentacle,
/// <summary>Creative space for artistic decoration and painting.</summary>
PaintingSpace,
/// <summary>Apparatus for distilling alcohols and essences.</summary>
Distillery,
/// <summary>Modern fabrication device for producing complex objects.</summary>
Printer3D,
/// <summary>Advanced device for synthesizing rare materials from common ones.</summary>
MatterSynthesizer,
/// <summary>High-tech station for biological and genetic modifications.</summary>
GeneticModStation,
/// <summary>Temporal device enabling time manipulation in crafting.</summary>
TemporalBracelet,
/// <summary>Preservation chamber for long-term item evolution and stasis.</summary>
StasisChamber
}

View file

@ -0,0 +1,105 @@
using OpenTheBox.Core.Characters;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
namespace OpenTheBox.Core;
/// <summary>
/// Represents the current/max pair for a resource.
/// </summary>
public sealed record ResourceState(int Current, int Max);
/// <summary>
/// The complete player state, containing all inventory, progression, and configuration data.
/// </summary>
public sealed class GameState
{
public required string PlayerName { get; set; }
public required PlayerAppearance Appearance { get; set; }
public required List<ItemInstance> Inventory { get; set; }
public required Dictionary<ResourceType, ResourceState> Resources { get; set; }
public required Dictionary<StatType, int> Stats { get; set; }
public required HashSet<UIFeature> UnlockedUIFeatures { get; set; }
public required HashSet<WorkstationType> UnlockedWorkstations { get; set; }
public required HashSet<AdventureTheme> UnlockedAdventures { get; set; }
public required HashSet<string> UnlockedCosmetics { get; set; }
public required HashSet<string> CompletedAdventures { get; set; }
public required Dictionary<string, string> AdventureSaveData { get; set; }
public required HashSet<ResourceType> VisibleResources { get; set; }
public required HashSet<StatType> VisibleStats { get; set; }
public required int TotalBoxesOpened { get; set; }
public required Locale CurrentLocale { get; set; }
public required DateTime CreatedAt { get; set; }
public required TimeSpan TotalPlayTime { get; set; }
public required HashSet<FontStyle> AvailableFonts { get; set; }
public required HashSet<TextColor> AvailableTextColors { get; set; }
/// <summary>
/// Returns the current value of a resource, or 0 if the resource is not tracked.
/// </summary>
public int GetResource(ResourceType type)
=> Resources.TryGetValue(type, out var state) ? state.Current : 0;
/// <summary>
/// Returns true if the inventory contains at least one item with the given definition id.
/// </summary>
public bool HasItem(string defId)
=> Inventory.Any(i => i.DefinitionId == defId);
/// <summary>
/// Counts the total quantity of items matching the given definition id.
/// </summary>
public int CountItems(string defId)
=> Inventory.Where(i => i.DefinitionId == defId).Sum(i => i.Quantity);
/// <summary>
/// Returns true if the given UI feature has been unlocked.
/// </summary>
public bool HasUIFeature(UIFeature feature)
=> UnlockedUIFeatures.Contains(feature);
/// <summary>
/// Adds an item instance to the inventory.
/// </summary>
public void AddItem(ItemInstance item)
=> Inventory.Add(item);
/// <summary>
/// Removes an item instance from the inventory by its unique instance id.
/// Returns true if the item was found and removed.
/// </summary>
public bool RemoveItem(Guid id)
{
var item = Inventory.FirstOrDefault(i => i.Id == id);
if (item is null)
return false;
return Inventory.Remove(item);
}
/// <summary>
/// Factory method to create a new GameState with empty collections and sensible defaults.
/// </summary>
public static GameState Create(string name, Locale locale) => new()
{
PlayerName = name,
Appearance = new PlayerAppearance(),
Inventory = [],
Resources = [],
Stats = [],
UnlockedUIFeatures = [],
UnlockedWorkstations = [],
UnlockedAdventures = [],
UnlockedCosmetics = [],
CompletedAdventures = [],
AdventureSaveData = [],
VisibleResources = [],
VisibleStats = [],
TotalBoxesOpened = 0,
CurrentLocale = locale,
CreatedAt = DateTime.UtcNow,
TotalPlayTime = TimeSpan.Zero,
AvailableFonts = [],
AvailableTextColors = []
};
}

View file

@ -0,0 +1,11 @@
namespace OpenTheBox.Core.Interactions;
/// <summary>
/// The result of executing an interaction rule, describing what was consumed and produced.
/// </summary>
public sealed record InteractionResult(
string RuleId,
List<Guid> ConsumedItemIds,
List<string> ProducedItemIds,
string Message
);

View file

@ -0,0 +1,18 @@
using OpenTheBox.Core.Enums;
namespace OpenTheBox.Core.Interactions;
/// <summary>
/// Defines a rule for item interactions, specifying what items are required
/// and what result is produced when the interaction fires.
/// </summary>
public sealed record InteractionRule(
string Id,
List<string> RequiredItemTags,
List<string>? RequiredItemIds,
InteractionResultType ResultType,
string? ResultData,
bool IsAutomatic,
int Priority,
string DescriptionKey
);

View file

@ -0,0 +1,24 @@
using OpenTheBox.Core.Enums;
namespace OpenTheBox.Core.Items;
/// <summary>
/// Static template for an item. Defines the blueprint from which item instances are created.
/// </summary>
public sealed record ItemDefinition(
string Id,
string NameKey,
string DescriptionKey,
ItemCategory Category,
ItemRarity Rarity,
HashSet<string> Tags,
UIFeature? MetaUnlock = null,
CosmeticSlot? CosmeticSlot = null,
string? CosmeticValue = null,
ResourceType? ResourceType = null,
int? ResourceAmount = null,
MaterialType? MaterialType = null,
MaterialForm? MaterialForm = null,
WorkstationType? WorkstationType = null,
AdventureTheme? AdventureTheme = null
);

View file

@ -0,0 +1,18 @@
namespace OpenTheBox.Core.Items;
/// <summary>
/// Runtime instance of an item in inventory, referencing an <see cref="ItemDefinition"/> by its id.
/// </summary>
public sealed record ItemInstance(
Guid Id,
string DefinitionId,
int Quantity = 1,
Dictionary<string, string>? Metadata = null
)
{
/// <summary>
/// Creates a new item instance with an auto-generated id.
/// </summary>
public static ItemInstance Create(string definitionId, int quantity = 1, Dictionary<string, string>? metadata = null)
=> new(Guid.NewGuid(), definitionId, quantity, metadata);
}

View file

@ -0,0 +1,83 @@
using System.Text.Json;
using OpenTheBox.Core.Boxes;
using OpenTheBox.Core.Interactions;
using OpenTheBox.Core.Items;
namespace OpenTheBox.Data;
/// <summary>
/// Central registry for all game content definitions (items, boxes, interaction rules).
/// Content is loaded from data files and registered here for lookup by the simulation engines.
/// </summary>
public class ContentRegistry
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
};
private readonly Dictionary<string, ItemDefinition> _items = [];
private readonly Dictionary<string, BoxDefinition> _boxes = [];
private readonly List<InteractionRule> _interactionRules = [];
public void RegisterItem(ItemDefinition item) => _items[item.Id] = item;
public void RegisterBox(BoxDefinition box) => _boxes[box.Id] = box;
public void RegisterInteractionRule(InteractionRule rule) => _interactionRules.Add(rule);
public ItemDefinition? GetItem(string id) => _items.GetValueOrDefault(id);
public BoxDefinition? GetBox(string id) => _boxes.GetValueOrDefault(id);
/// <summary>
/// Returns true if the given definition id corresponds to a registered box.
/// </summary>
public bool IsBox(string id) => _boxes.ContainsKey(id);
public IReadOnlyDictionary<string, ItemDefinition> Items => _items;
public IReadOnlyDictionary<string, BoxDefinition> Boxes => _boxes;
public IReadOnlyList<InteractionRule> InteractionRules => _interactionRules;
/// <summary>
/// Loads content definitions from JSON files and returns a populated registry.
/// Files that do not exist are silently skipped.
/// </summary>
public static ContentRegistry LoadFromFiles(string itemsPath, string boxesPath, string interactionsPath)
{
var registry = new ContentRegistry();
if (File.Exists(itemsPath))
{
var json = File.ReadAllText(itemsPath);
var items = JsonSerializer.Deserialize<List<ItemDefinition>>(json, JsonOptions);
if (items is not null)
{
foreach (var item in items)
registry.RegisterItem(item);
}
}
if (File.Exists(boxesPath))
{
var json = File.ReadAllText(boxesPath);
var boxes = JsonSerializer.Deserialize<List<BoxDefinition>>(json, JsonOptions);
if (boxes is not null)
{
foreach (var box in boxes)
registry.RegisterBox(box);
}
}
if (File.Exists(interactionsPath))
{
var json = File.ReadAllText(interactionsPath);
var rules = JsonSerializer.Deserialize<List<InteractionRule>>(json, JsonOptions);
if (rules is not null)
{
foreach (var rule in rules)
registry.RegisterInteractionRule(rule);
}
}
return registry;
}
}

View file

@ -0,0 +1,80 @@
using System.Text.Json;
using OpenTheBox.Core.Enums;
namespace OpenTheBox.Localization;
/// <summary>
/// Loads and serves localized strings from JSON files stored at
/// <c>content/strings/{locale}.json</c>. Supports runtime locale switching.
/// </summary>
public sealed class LocalizationManager
{
private static readonly string StringsDirectory = Path.Combine("content", "strings");
private Dictionary<string, string> _strings = [];
/// <summary>
/// The currently loaded locale.
/// </summary>
public Locale CurrentLocale { get; private set; }
/// <summary>
/// Creates a new <see cref="LocalizationManager"/> and immediately loads the
/// specified locale.
/// </summary>
public LocalizationManager(Locale locale)
{
Load(locale);
}
/// <summary>
/// Loads (or reloads) the string table for the given locale.
/// </summary>
public void Load(Locale locale)
{
CurrentLocale = locale;
_strings = [];
string localeName = locale.ToString().ToLowerInvariant();
string path = Path.Combine(StringsDirectory, $"{localeName}.json");
if (!File.Exists(path))
return;
string json = File.ReadAllText(path);
var parsed = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
if (parsed is not null)
{
_strings = parsed;
}
}
/// <summary>
/// Returns the localized string for the given key, formatted with the optional
/// arguments using <see cref="string.Format(string,object[])"/>.
/// If the key is not found, returns <c>"[MISSING:key]"</c>.
/// </summary>
public string Get(string key, params object[] args)
{
if (!_strings.TryGetValue(key, out string? value))
{
return $"[MISSING:{key}]";
}
if (args.Length > 0)
{
return string.Format(value, args);
}
return value;
}
/// <summary>
/// Switches to a different locale, reloading the string table.
/// </summary>
public void Change(Locale locale)
{
Load(locale);
}
}

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>OpenTheBox</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.54.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="Loreline">
<HintPath>..\..\lib\Loreline.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="..\..\content\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>content\%(RecursiveDir)%(Filename)%(Extension)</Link>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
using OpenTheBox.Core;
namespace OpenTheBox.Persistence;
/// <summary>
/// Serializable wrapper around <see cref="GameState"/> that includes save metadata.
/// </summary>
public sealed class SaveData
{
/// <summary>
/// Schema version for forward/backward compatibility checks.
/// </summary>
[JsonPropertyName("version")]
public int Version { get; init; } = 1;
/// <summary>
/// Timestamp of when the save was created.
/// </summary>
[JsonPropertyName("savedAt")]
public DateTime SavedAt { get; init; } = DateTime.UtcNow;
/// <summary>
/// The complete game state snapshot.
/// </summary>
[JsonPropertyName("state")]
public required GameState State { get; init; }
}

View file

@ -0,0 +1,127 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenTheBox.Core;
namespace OpenTheBox.Persistence;
/// <summary>
/// Manages reading and writing save files to disk.
/// Save files are stored as indented JSON in the <c>saves/</c> directory with the
/// <c>.otb</c> extension.
/// </summary>
public sealed class SaveManager
{
private const string SaveDirectory = "saves";
private const string Extension = ".otb";
private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// Ensures the save directory exists.
/// </summary>
private static void EnsureDirectory()
{
if (!Directory.Exists(SaveDirectory))
{
Directory.CreateDirectory(SaveDirectory);
}
}
/// <summary>
/// Builds the full file path for a given slot name.
/// </summary>
private static string SlotPath(string slotName) =>
Path.Combine(SaveDirectory, $"{slotName}{Extension}");
/// <summary>
/// Saves the current game state to the specified slot.
/// </summary>
public void Save(GameState state, string slotName = "autosave")
{
EnsureDirectory();
var data = new SaveData
{
SavedAt = DateTime.UtcNow,
State = state
};
string json = JsonSerializer.Serialize(data, SerializerOptions);
File.WriteAllText(SlotPath(slotName), json);
}
/// <summary>
/// Loads a game state from the specified slot.
/// Returns null if the slot does not exist or cannot be deserialized.
/// </summary>
public GameState? Load(string slotName = "autosave")
{
string path = SlotPath(slotName);
if (!File.Exists(path))
return null;
string json = File.ReadAllText(path);
var data = JsonSerializer.Deserialize<SaveData>(json, SerializerOptions);
return data?.State;
}
/// <summary>
/// Lists all save slots with their names and save timestamps.
/// </summary>
public List<(string Name, DateTime SavedAt)> ListSlots()
{
EnsureDirectory();
var slots = new List<(string Name, DateTime SavedAt)>();
foreach (string file in Directory.GetFiles(SaveDirectory, $"*{Extension}"))
{
string slotName = Path.GetFileNameWithoutExtension(file);
try
{
string json = File.ReadAllText(file);
var data = JsonSerializer.Deserialize<SaveData>(json, SerializerOptions);
if (data is not null)
{
slots.Add((slotName, data.SavedAt));
}
}
catch (JsonException)
{
// Corrupted save file; include it with the file's last write time
slots.Add((slotName, File.GetLastWriteTimeUtc(file)));
}
}
return slots.OrderByDescending(s => s.SavedAt).ToList();
}
/// <summary>
/// Returns true if a save slot with the given name exists on disk.
/// </summary>
public bool SlotExists(string slotName)
{
return File.Exists(SlotPath(slotName));
}
/// <summary>
/// Deletes the save file for the given slot. Does nothing if the slot does not exist.
/// </summary>
public void DeleteSlot(string slotName)
{
string path = SlotPath(slotName);
if (File.Exists(path))
{
File.Delete(path);
}
}
}

422
src/OpenTheBox/Program.cs Normal file
View file

@ -0,0 +1,422 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
using OpenTheBox.Data;
using OpenTheBox.Localization;
using OpenTheBox.Persistence;
using OpenTheBox.Rendering;
using OpenTheBox.Simulation;
using OpenTheBox.Simulation.Actions;
using OpenTheBox.Simulation.Events;
using OpenTheBox.Adventures;
namespace OpenTheBox;
public static class Program
{
private static GameState _state = null!;
private static ContentRegistry _registry = null!;
private static LocalizationManager _loc = null!;
private static SaveManager _saveManager = null!;
private static GameSimulation _simulation = null!;
private static RenderContext _renderContext = null!;
private static IRenderer _renderer = null!;
private static bool _running = true;
public static async Task Main(string[] args)
{
_saveManager = new SaveManager();
_loc = new LocalizationManager(Locale.EN);
_renderContext = new RenderContext();
_renderer = RendererFactory.Create(_renderContext);
await MainMenuLoop();
}
private static async Task MainMenuLoop()
{
while (_running)
{
_renderer.Clear();
_renderer.ShowMessage("========================================");
_renderer.ShowMessage(" OPEN THE BOX");
_renderer.ShowMessage("========================================");
_renderer.ShowMessage("");
_renderer.ShowMessage(_loc.Get("game.subtitle"));
_renderer.ShowMessage("");
var options = new List<string>
{
_loc.Get("menu.new_game"),
_loc.Get("menu.load_game"),
_loc.Get("menu.language"),
_loc.Get("menu.quit")
};
int choice = _renderer.ShowSelection("", options);
switch (choice)
{
case 0: await NewGame(); break;
case 1: await LoadGame(); break;
case 2: ChangeLanguage(); break;
case 3: _running = false; break;
}
}
}
private static async Task NewGame()
{
string name = _renderer.ShowTextInput(_loc.Get("prompt.name"));
if (string.IsNullOrWhiteSpace(name)) name = "BoxOpener";
_state = GameState.Create(name, _loc.CurrentLocale);
InitializeGame();
var starterBox = ItemInstance.Create("box_starter");
_state.AddItem(starterBox);
_renderer.ShowMessage("");
_renderer.ShowMessage($"Welcome, {name}!");
_renderer.ShowMessage(_loc.Get("box.starter.desc"));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
await GameLoop();
}
private static async Task LoadGame()
{
var slots = _saveManager.ListSlots();
if (slots.Count == 0)
{
_renderer.ShowMessage(_loc.Get("save.no_saves"));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
var options = slots.Select(s => $"{s.Name} ({s.SavedAt:yyyy-MM-dd HH:mm})").ToList();
options.Add(_loc.Get("menu.back"));
int choice = _renderer.ShowSelection(_loc.Get("save.choose_slot"), options);
if (choice >= slots.Count) return;
var loaded = _saveManager.Load(slots[choice].Name);
if (loaded == null)
{
_renderer.ShowError("Failed to load save.");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
_state = loaded;
_loc.Change(_state.CurrentLocale);
InitializeGame();
_renderer.ShowMessage(_loc.Get("misc.welcome_back", _state.PlayerName));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
await GameLoop();
}
private static void InitializeGame()
{
_registry = ContentRegistry.LoadFromFiles(
"content/data/items.json",
"content/data/boxes.json",
"content/data/interactions.json"
);
_simulation = new GameSimulation(_registry);
_renderContext = RenderContext.FromGameState(_state);
_renderer = RendererFactory.Create(_renderContext);
}
private static void ChangeLanguage()
{
var options = new List<string> { "English", "Francais" };
int choice = _renderer.ShowSelection(_loc.Get("menu.language"), options);
var newLocale = choice == 0 ? Locale.EN : Locale.FR;
_loc.Change(newLocale);
if (_state != null)
_state.CurrentLocale = newLocale;
_renderer = RendererFactory.Create(_renderContext);
}
private static async Task GameLoop()
{
while (_running)
{
_renderer.Clear();
_renderer.ShowGameState(_state, _renderContext);
var actions = BuildActionList();
if (actions.Count == 0)
{
_renderer.ShowMessage(_loc.Get("error.no_boxes"));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
break;
}
int choice = _renderer.ShowSelection(
_loc.Get("prompt.choose_action"),
actions.Select(a => a.label).ToList());
await ExecuteAction(actions[choice].action);
}
}
private static List<(string label, string action)> BuildActionList()
{
var actions = new List<(string label, string action)>();
var boxes = _state.Inventory.Where(i => _registry.IsBox(i.DefinitionId)).ToList();
if (boxes.Count > 0)
actions.Add((_loc.Get("action.open_box") + $" ({boxes.Count})", "open_box"));
if (_state.Inventory.Count > 0)
actions.Add((_loc.Get("action.inventory"), "inventory"));
if (_state.UnlockedAdventures.Count > 0)
actions.Add((_loc.Get("action.adventure"), "adventure"));
if (_state.UnlockedCosmetics.Count > 0)
actions.Add((_loc.Get("action.appearance"), "appearance"));
actions.Add((_loc.Get("action.save"), "save"));
actions.Add((_loc.Get("action.quit"), "quit"));
return actions;
}
private static async Task ExecuteAction(string action)
{
switch (action)
{
case "open_box": await OpenBoxAction(); break;
case "inventory": ShowInventory(); break;
case "adventure": await StartAdventure(); break;
case "appearance": ChangeAppearance(); break;
case "save": SaveGame(); break;
case "quit": _running = false; break;
}
}
private static async Task OpenBoxAction()
{
var boxes = _state.Inventory.Where(i => _registry.IsBox(i.DefinitionId)).ToList();
if (boxes.Count == 0)
{
_renderer.ShowMessage(_loc.Get("box.no_boxes"));
return;
}
var boxNames = boxes.Select(b =>
_loc.Get(_registry.GetItem(b.DefinitionId)?.NameKey ?? b.DefinitionId)).ToList();
boxNames.Add(_loc.Get("menu.back"));
int choice = _renderer.ShowSelection(_loc.Get("prompt.choose_box"), boxNames);
if (choice >= boxes.Count) return;
var boxInstance = boxes[choice];
var openAction = new OpenBoxAction(boxInstance.Id)
{
BoxDefinitionId = boxInstance.DefinitionId
};
var events = _simulation.ProcessAction(openAction, _state);
await RenderEvents(events);
}
private static async Task RenderEvents(List<GameEvent> events)
{
foreach (var evt in events)
{
switch (evt)
{
case BoxOpenedEvent boxEvt:
var boxDef = _registry.GetBox(boxEvt.BoxId);
_renderer.ShowBoxOpening(
_loc.Get(boxDef?.NameKey ?? boxEvt.BoxId),
boxDef?.Rarity.ToString() ?? "Common");
break;
case ItemReceivedEvent itemEvt:
_state.AddItem(itemEvt.Item);
var itemDef = _registry.GetItem(itemEvt.Item.DefinitionId);
_renderer.ShowLootReveal(
[
(
_loc.Get(itemDef?.NameKey ?? itemEvt.Item.DefinitionId),
(itemDef?.Rarity ?? ItemRarity.Common).ToString(),
(itemDef?.Category ?? ItemCategory.Box).ToString()
)
]);
break;
case UIFeatureUnlockedEvent uiEvt:
_renderContext.Unlock(uiEvt.Feature);
_renderer = RendererFactory.Create(_renderContext);
var featureKey = $"meta.{uiEvt.Feature.ToString().ToLower()}";
_renderer.ShowUIFeatureUnlocked(
_loc.Get("meta.unlocked", _loc.Get(featureKey)));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
break;
case InteractionTriggeredEvent interEvt:
_renderer.ShowInteraction(_loc.Get(interEvt.DescriptionKey));
break;
case ResourceChangedEvent resEvt:
var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} -> {resEvt.NewValue}");
break;
case MessageEvent msgEvt:
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
break;
case ChoiceRequiredEvent choiceEvt:
_renderer.ShowSelection(_loc.Get(choiceEvt.Prompt), choiceEvt.Options);
break;
case LootTableModifiedEvent:
_renderer.ShowMessage(_loc.Get("interaction.key_no_match"));
break;
case AdventureStartedEvent advEvt:
await RunAdventure(advEvt.Theme);
break;
}
}
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
private static void ShowInventory()
{
_renderer.Clear();
if (_state.Inventory.Count == 0)
{
_renderer.ShowMessage("Your inventory is empty. Open more boxes!");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
var grouped = _state.Inventory
.GroupBy(i => i.DefinitionId)
.Select(g =>
{
var def = _registry.GetItem(g.Key);
return (
name: _loc.Get(def?.NameKey ?? g.Key),
rarity: (def?.Rarity ?? ItemRarity.Common).ToString(),
category: (def?.Category ?? ItemCategory.Box).ToString()
);
})
.OrderBy(i => i.category)
.ThenBy(i => i.name)
.ToList();
_renderer.ShowLootReveal(grouped);
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
private static async Task StartAdventure()
{
var available = _state.UnlockedAdventures.ToList();
if (available.Count == 0)
{
_renderer.ShowMessage("No adventures available yet. Keep opening boxes!");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
var options = available.Select(a =>
{
bool completed = _state.CompletedAdventures.Contains(a.ToString());
return (completed ? "[Done] " : "") + a.ToString();
}).ToList();
options.Add(_loc.Get("menu.back"));
int choice = _renderer.ShowSelection(_loc.Get("action.adventure"), options);
if (choice >= available.Count) return;
await RunAdventure(available[choice]);
}
private static async Task RunAdventure(AdventureTheme theme)
{
try
{
var adventureEngine = new AdventureEngine(_renderer, _loc);
var events = await adventureEngine.PlayAdventure(theme, _state);
foreach (var evt in events)
{
if (evt.Kind == GameEventKind.ItemGranted)
_state.AddItem(ItemInstance.Create(evt.TargetId, evt.Amount));
}
_renderer.ShowMessage(_loc.Get("adventure.completed"));
}
catch (FileNotFoundException)
{
_renderer.ShowMessage($"Adventure '{theme}' is coming soon! The boxes are still being assembled.");
}
catch (Exception ex)
{
_renderer.ShowError($"Adventure error: {ex.Message}");
}
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
private static void ChangeAppearance()
{
var cosmeticItems = _state.Inventory
.Where(i =>
{
var def = _registry.GetItem(i.DefinitionId);
return def?.Category == ItemCategory.Cosmetic && def.CosmeticSlot.HasValue;
})
.ToList();
if (cosmeticItems.Count == 0)
{
_renderer.ShowMessage("No cosmetics available yet. Open Style Boxes!");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
var options = cosmeticItems.Select(i =>
{
var def = _registry.GetItem(i.DefinitionId);
return $"[{def?.CosmeticSlot}] {_loc.Get(def?.NameKey ?? i.DefinitionId)}";
}).ToList();
options.Add(_loc.Get("menu.back"));
int choice = _renderer.ShowSelection(_loc.Get("action.appearance"), options);
if (choice >= cosmeticItems.Count) return;
var action = new EquipCosmeticAction(cosmeticItems[choice].Id);
var events = _simulation.ProcessAction(action, _state);
foreach (var evt in events)
{
if (evt is CosmeticEquippedEvent cosEvt)
_renderer.ShowMessage($"Equipped {cosEvt.Slot}: {cosEvt.NewValue}");
else if (evt is MessageEvent msgEvt)
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
}
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
private static void SaveGame()
{
_renderer.ShowMessage(_loc.Get("save.saving"));
_saveManager.Save(_state, _state.PlayerName);
_renderer.ShowMessage(_loc.Get("save.saved", _state.PlayerName));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
}

View file

@ -0,0 +1,124 @@
using OpenTheBox.Core;
namespace OpenTheBox.Rendering;
/// <summary>
/// Phase 0 renderer. Pure Console.WriteLine and Console.ReadLine.
/// No colors, no frames, no fancy stuff. This is the "stone age" of the UI,
/// deliberately ugly and minimal.
/// </summary>
public sealed class BasicRenderer : IRenderer
{
public void ShowMessage(string message)
{
Console.WriteLine(message);
}
public void ShowError(string message)
{
Console.WriteLine($"ERROR: {message}");
}
public void ShowBoxOpening(string boxName, string rarity)
{
Console.WriteLine($"Opening {boxName}...");
Console.WriteLine("...");
Console.WriteLine("......");
Console.WriteLine($"Box opened! (Rarity: {rarity})");
}
public void ShowLootReveal(List<(string name, string rarity, string category)> items)
{
Console.WriteLine("You received:");
for (int i = 0; i < items.Count; i++)
{
var (name, rarity, category) = items[i];
Console.WriteLine($" - {name} [{rarity}] ({category})");
}
}
public int ShowSelection(string prompt, List<string> options)
{
Console.WriteLine(prompt);
for (int i = 0; i < options.Count; i++)
{
Console.WriteLine($" {i + 1}. {options[i]}");
}
while (true)
{
Console.Write("> ");
string? input = Console.ReadLine();
if (int.TryParse(input, out int choice) && choice >= 1 && choice <= options.Count)
{
return choice - 1;
}
Console.WriteLine($"Please enter a number between 1 and {options.Count}.");
}
}
public string ShowTextInput(string prompt)
{
Console.Write($"{prompt}: ");
return Console.ReadLine() ?? string.Empty;
}
public void ShowGameState(GameState state, RenderContext context)
{
// Phase 0: no panels unlocked yet, so nothing to show.
}
public void ShowAdventureDialogue(string? character, string text)
{
if (character is not null)
{
Console.WriteLine($"[{character}]");
}
Console.WriteLine(text);
Console.WriteLine();
}
public int ShowAdventureChoice(List<string> options)
{
Console.WriteLine("What do you do?");
for (int i = 0; i < options.Count; i++)
{
Console.WriteLine($" {i + 1}. {options[i]}");
}
while (true)
{
Console.Write("> ");
string? input = Console.ReadLine();
if (int.TryParse(input, out int choice) && choice >= 1 && choice <= options.Count)
{
return choice - 1;
}
Console.WriteLine($"Please enter a number between 1 and {options.Count}.");
}
}
public void ShowUIFeatureUnlocked(string featureName)
{
Console.WriteLine("========================================");
Console.WriteLine($" NEW FEATURE UNLOCKED: {featureName}");
Console.WriteLine("========================================");
}
public void ShowInteraction(string description)
{
Console.WriteLine($"* {description} *");
}
public void WaitForKeyPress(string? message = null)
{
Console.WriteLine(message ?? "Press any key to continue...");
Console.ReadKey(intercept: true);
Console.WriteLine();
}
public void Clear()
{
Console.Clear();
}
}

View file

@ -0,0 +1,75 @@
using OpenTheBox.Core;
namespace OpenTheBox.Rendering;
/// <summary>
/// Interface for all rendering operations. The game loop calls this to display things.
/// Implementations range from plain-text console output to rich Spectre.Console UI.
/// </summary>
public interface IRenderer
{
/// <summary>
/// Displays a general-purpose message to the player.
/// </summary>
void ShowMessage(string message);
/// <summary>
/// Displays an error message, typically in a distinct style.
/// </summary>
void ShowError(string message);
/// <summary>
/// Shows the box-opening sequence for the given box name and rarity.
/// </summary>
void ShowBoxOpening(string boxName, string rarity);
/// <summary>
/// Reveals the loot obtained from a box, listing each item with its name, rarity, and category.
/// </summary>
void ShowLootReveal(List<(string name, string rarity, string category)> items);
/// <summary>
/// Presents a selection prompt and returns the zero-based index chosen by the player.
/// </summary>
int ShowSelection(string prompt, List<string> options);
/// <summary>
/// Prompts the player for free-form text input and returns the entered string.
/// </summary>
string ShowTextInput(string prompt);
/// <summary>
/// Renders the current game state using the given render context to decide which panels to show.
/// </summary>
void ShowGameState(GameState state, RenderContext context);
/// <summary>
/// Displays a line of adventure dialogue, optionally attributed to a character.
/// </summary>
void ShowAdventureDialogue(string? character, string text);
/// <summary>
/// Presents adventure choices and returns the zero-based index chosen by the player.
/// </summary>
int ShowAdventureChoice(List<string> options);
/// <summary>
/// Announces that a new UI feature has been unlocked.
/// </summary>
void ShowUIFeatureUnlocked(string featureName);
/// <summary>
/// Displays an interaction description to the player.
/// </summary>
void ShowInteraction(string description);
/// <summary>
/// Waits for the player to press any key before continuing.
/// </summary>
void WaitForKeyPress(string? message = null);
/// <summary>
/// Clears the screen.
/// </summary>
void Clear();
}

View file

@ -0,0 +1,61 @@
using Spectre.Console;
using Spectre.Console.Rendering;
namespace OpenTheBox.Rendering.Panels;
/// <summary>
/// Renders recent adventure dialogue messages in a framed panel.
/// </summary>
public static class ChatPanel
{
private const int MaxVisibleMessages = 10;
/// <summary>
/// Builds a renderable chat log from a list of dialogue messages.
/// Each message has an optional character name and text content.
/// </summary>
public static IRenderable Render(List<(string? character, string text)> messages)
{
var rows = new List<IRenderable>();
// Show only the most recent messages
var visible = messages.Count > MaxVisibleMessages
? messages.Skip(messages.Count - MaxVisibleMessages).ToList()
: messages;
if (visible.Count == 0)
{
rows.Add(new Markup("[dim]No dialogue yet.[/]"));
}
else
{
foreach (var (character, text) in visible)
{
if (character is not null)
{
string color = CharacterColor(character);
rows.Add(new Markup($"[bold {color}]{Markup.Escape(character)}:[/] {Markup.Escape(text)}"));
}
else
{
rows.Add(new Markup($"[italic dim]{Markup.Escape(text)}[/]"));
}
}
}
return new Panel(new Rows(rows))
.Header("[bold aqua]Chat[/]")
.Border(BoxBorder.Rounded);
}
/// <summary>
/// Assigns a consistent color to a character name so each speaker is visually distinct.
/// </summary>
private static string CharacterColor(string character)
{
// Simple hash-based color selection for consistent per-character coloring
string[] colors = ["aqua", "yellow", "green", "magenta", "orange1", "cyan1", "deeppink1", "chartreuse1"];
int hash = Math.Abs(character.GetHashCode());
return colors[hash % colors.Length];
}
}

View file

@ -0,0 +1,65 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
using OpenTheBox.Localization;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace OpenTheBox.Rendering.Panels;
/// <summary>
/// Renders the player inventory as a Spectre <see cref="Table"/> grouped by category.
/// </summary>
public static class InventoryPanel
{
/// <summary>
/// Builds a renderable inventory table from the current game state.
/// Uses the localization manager to resolve item name keys.
/// </summary>
public static IRenderable Render(GameState state, LocalizationManager? loc = null)
{
var table = new Table()
.Border(TableBorder.Rounded)
.Title("[bold yellow]Inventory[/]")
.AddColumn(new TableColumn("[bold]Name[/]"))
.AddColumn(new TableColumn("[bold]Category[/]").Centered())
.AddColumn(new TableColumn("[bold]Rarity[/]").Centered())
.AddColumn(new TableColumn("[bold]Qty[/]").RightAligned());
// Group items by their definition id and display grouped
var grouped = state.Inventory
.GroupBy(i => i.DefinitionId)
.OrderBy(g => g.Key);
foreach (var group in grouped)
{
string defId = group.Key;
int totalQty = group.Sum(i => i.Quantity);
// Use localization if available, otherwise fall back to definition id
string name = loc is not null ? loc.Get(defId) : defId;
// We display the definition id as a stand-in for category/rarity since
// we only have ItemInstance at runtime. The full lookup would go through
// an item registry; for now show the raw id.
string category = "-";
string rarity = "-";
string color = "white";
table.AddRow(
$"[{color}]{Markup.Escape(name)}[/]",
Markup.Escape(category),
$"[{color}]{Markup.Escape(rarity)}[/]",
totalQty.ToString());
}
if (!state.Inventory.Any())
{
table.AddRow("[dim]Empty[/]", "", "", "");
}
return new Panel(table)
.Header("[bold yellow]Inventory[/]")
.Border(BoxBorder.Rounded);
}
}

View file

@ -0,0 +1,134 @@
using OpenTheBox.Core.Characters;
using OpenTheBox.Core.Enums;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace OpenTheBox.Rendering.Panels;
/// <summary>
/// Generates simple ASCII art for the player character based on equipped cosmetics.
/// Different styles change individual pieces of the portrait.
/// </summary>
public static class PortraitPanel
{
/// <summary>
/// Builds a renderable ASCII art portrait from the player's appearance settings.
/// </summary>
public static IRenderable Render(PlayerAppearance appearance)
{
string hair = GetHairArt(appearance.HairStyle);
string eyes = GetEyeArt(appearance.EyeStyle);
string body = GetBodyArt(appearance.BodyStyle);
string legs = GetLegArt(appearance.LegStyle);
string arms = GetArmArt(appearance.ArmStyle);
string hairColor = TintToColor(appearance.HairTint);
string bodyColor = TintToColor(appearance.BodyTint);
string portrait = string.Join(Environment.NewLine,
$"[{hairColor}]{Markup.Escape(hair)}[/]",
$"[white]{Markup.Escape(eyes)}[/]",
$"[{bodyColor}]{Markup.Escape(body)}[/]",
$"[{bodyColor}]{Markup.Escape(legs)}[/]",
$"[{bodyColor}]{Markup.Escape(arms)}[/]");
return new Panel(new Markup(portrait))
.Header("[bold green]Portrait[/]")
.Border(BoxBorder.Rounded)
.Padding(1, 0);
}
// ── Hair styles ─────────────────────────────────────────────────────
private static string GetHairArt(HairStyle style) => style switch
{
HairStyle.None => " .-. ",
HairStyle.Short => " ~~~ ",
HairStyle.Long => " ~~~~~ ",
HairStyle.Ponytail => " ~~~\\ ",
HairStyle.Braided => " ///\\\\\\ ",
HairStyle.Cyberpunk => " /\\/\\/\\ ",
HairStyle.Fire => " ||| ",
HairStyle.StardustLegendary => " @@@@@ ",
_ => " ??? "
};
// ── Eye styles ──────────────────────────────────────────────────────
private static string GetEyeArt(EyeStyle style) => style switch
{
EyeStyle.None => " ( o.o ) ",
EyeStyle.Blue => " ( O.O ) ",
EyeStyle.Green => " ( -._ ) ",
EyeStyle.RedOrange => " ( >.< ) ",
EyeStyle.Brown => " ( ^.o ) ",
EyeStyle.Black => " ( -.- ) ",
EyeStyle.Sunglasses => " ( B-) ) ",
EyeStyle.PilotGlasses => " ( B-) ) ",
EyeStyle.AircraftGlasses => " ( B-) ) ",
EyeStyle.CyberneticEyes => " ( *.* ) ",
EyeStyle.MagicianGlasses => " ( o.o ) ",
_ => " ( ?.? ) "
};
// ── Body styles ─────────────────────────────────────────────────────
private static string GetBodyArt(BodyStyle style) => style switch
{
BodyStyle.Naked => " |[---]| ",
BodyStyle.RegularTShirt => " |[===]| ",
BodyStyle.SexyTShirt => " |[~~~]| ",
BodyStyle.Suit => " |[###]| ",
BodyStyle.Armored => " |{===}| ",
BodyStyle.Robotic => " \\(===)/ ",
_ => " |[???]| "
};
// ── Leg styles ──────────────────────────────────────────────────────
private static string GetLegArt(LegStyle style) => style switch
{
LegStyle.None => " | | ",
LegStyle.Naked => " | | ",
LegStyle.Slip => " | | ",
LegStyle.Short => " | | ",
LegStyle.Panty => " | | ",
LegStyle.RocketBoots => " [| |] ",
LegStyle.PegLeg => " |/ ",
LegStyle.Tentacles => " {| |} ",
_ => " | | "
};
// ── Arm styles ──────────────────────────────────────────────────────
private static string GetArmArt(ArmStyle style) => style switch
{
ArmStyle.None => " / \\ ",
ArmStyle.Short => " / \\ ",
ArmStyle.Regular => " _/ \\_ ",
ArmStyle.Long => " X X ",
ArmStyle.Mechanical => " / ~ ",
ArmStyle.Wings => " </ \\> ",
ArmStyle.ExtraPair => " </X X\\> ",
_ => " / \\ "
};
// ── Tint mapping ────────────────────────────────────────────────────
private static string TintToColor(TintColor tint) => tint switch
{
TintColor.None => "white",
TintColor.Cyan => "aqua",
TintColor.Orange => "orange1",
TintColor.Purple => "purple",
TintColor.WarmPink => "deeppink1",
TintColor.Light => "white",
TintColor.Dark => "grey",
TintColor.Rainbow => "gold1",
TintColor.Neon => "green",
TintColor.Silver => "silver",
TintColor.Gold => "gold1",
TintColor.Void => "grey",
_ => "white"
};
}

View file

@ -0,0 +1,60 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace OpenTheBox.Rendering.Panels;
/// <summary>
/// Renders visible player resources as horizontal bars showing current / max values.
/// </summary>
public static class ResourcePanel
{
/// <summary>
/// Builds a renderable resource display from the current game state.
/// Only resources present in <see cref="GameState.VisibleResources"/> are shown.
/// </summary>
public static IRenderable Render(GameState state)
{
var rows = new List<IRenderable>();
foreach (var resourceType in state.VisibleResources.OrderBy(r => r.ToString()))
{
if (!state.Resources.TryGetValue(resourceType, out var resource))
continue;
string label = resourceType.ToString();
int current = resource.Current;
int max = resource.Max;
// Build a text-based bar: [####----] 40/100
int barWidth = 20;
int filled = max > 0 ? (int)Math.Round((double)current / max * barWidth) : 0;
filled = Math.Clamp(filled, 0, barWidth);
int empty = barWidth - filled;
string bar = new string('#', filled) + new string('-', empty);
string color = GetResourceColor(resourceType);
rows.Add(new Markup($" [{color}]{Markup.Escape(label)}[/]: [{color}][{bar}][/] {current}/{max}"));
}
if (rows.Count == 0)
{
rows.Add(new Markup("[dim]No resources visible yet.[/]"));
}
return new Panel(new Rows(rows))
.Header("[bold cyan]Resources[/]")
.Border(BoxBorder.Rounded);
}
private static string GetResourceColor(ResourceType type) => type.ToString().ToLowerInvariant() switch
{
"gold" or "coins" => "gold1",
"energy" or "stamina" => "green",
"mana" or "magic" => "blue",
"health" or "hp" => "red",
_ => "silver"
};
}

View file

@ -0,0 +1,54 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace OpenTheBox.Rendering.Panels;
/// <summary>
/// Renders the player stat values in a framed panel.
/// </summary>
public static class StatsPanel
{
/// <summary>
/// Builds a renderable stats display from the current game state.
/// Only stats present in <see cref="GameState.VisibleStats"/> are shown.
/// </summary>
public static IRenderable Render(GameState state)
{
var rows = new List<IRenderable>();
foreach (var statType in state.VisibleStats.OrderBy(s => s.ToString()))
{
if (!state.Stats.TryGetValue(statType, out int value))
continue;
string label = statType.ToString();
string color = GetStatColor(statType);
rows.Add(new Markup($" [{color}]{Markup.Escape(label)}:[/] [bold]{value}[/]"));
}
if (rows.Count == 0)
{
rows.Add(new Markup("[dim]No stats visible yet.[/]"));
}
// Add total boxes opened as a bonus stat
rows.Add(new Markup($" [silver]Boxes Opened:[/] [bold]{state.TotalBoxesOpened}[/]"));
return new Panel(new Rows(rows))
.Header("[bold magenta]Stats[/]")
.Border(BoxBorder.Rounded);
}
private static string GetStatColor(StatType type) => type.ToString().ToLowerInvariant() switch
{
"strength" or "power" => "red",
"defense" or "armor" => "blue",
"speed" or "agility" => "green",
"luck" => "gold1",
"intelligence" or "wisdom" => "purple",
_ => "silver"
};
}

View file

@ -0,0 +1,53 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
namespace OpenTheBox.Rendering;
/// <summary>
/// Tracks which UI features are unlocked. Used by SpectreRenderer to decide what to show.
/// Built from the current <see cref="GameState"/> so the renderer can progressively
/// enable richer visuals as the player unlocks new features.
/// </summary>
public sealed class RenderContext
{
private readonly HashSet<UIFeature> _unlocked = [];
/// <summary>
/// Returns true if the given UI feature is unlocked in this context.
/// </summary>
public bool Has(UIFeature feature) => _unlocked.Contains(feature);
/// <summary>
/// Unlocks a UI feature so the renderer can start using it.
/// </summary>
public void Unlock(UIFeature feature) => _unlocked.Add(feature);
// ── Convenience properties ──────────────────────────────────────────
public bool HasColors => Has(UIFeature.TextColors);
public bool HasExtendedColors => Has(UIFeature.ExtendedColors);
public bool HasArrowSelection => Has(UIFeature.ArrowKeySelection);
public bool HasInventoryPanel => Has(UIFeature.InventoryPanel);
public bool HasResourcePanel => Has(UIFeature.ResourcePanel);
public bool HasStatsPanel => Has(UIFeature.StatsPanel);
public bool HasPortraitPanel => Has(UIFeature.PortraitPanel);
public bool HasChatPanel => Has(UIFeature.ChatPanel);
public bool HasFullLayout => Has(UIFeature.FullLayout);
public bool HasKeyboardShortcuts => Has(UIFeature.KeyboardShortcuts);
public bool HasBoxAnimation => Has(UIFeature.BoxAnimation);
public bool HasCraftingPanel => Has(UIFeature.CraftingPanel);
/// <summary>
/// Builds a <see cref="RenderContext"/> that mirrors the features already unlocked in a
/// <see cref="GameState"/>.
/// </summary>
public static RenderContext FromGameState(GameState state)
{
var ctx = new RenderContext();
foreach (var feature in state.UnlockedUIFeatures)
{
ctx.Unlock(feature);
}
return ctx;
}
}

View file

@ -0,0 +1,37 @@
namespace OpenTheBox.Rendering;
/// <summary>
/// Static factory that selects the appropriate <see cref="IRenderer"/> implementation
/// based on which UI features the player has unlocked.
/// </summary>
public static class RendererFactory
{
/// <summary>
/// Creates an <see cref="IRenderer"/> suited to the given context.
/// If the context has any Spectre-capable feature unlocked, a <see cref="SpectreRenderer"/>
/// is returned; otherwise the plain <see cref="BasicRenderer"/> is used.
/// </summary>
public static IRenderer Create(RenderContext context)
{
bool hasAnySpectreFeature =
context.HasColors ||
context.HasExtendedColors ||
context.HasArrowSelection ||
context.HasInventoryPanel ||
context.HasResourcePanel ||
context.HasStatsPanel ||
context.HasPortraitPanel ||
context.HasChatPanel ||
context.HasFullLayout ||
context.HasKeyboardShortcuts ||
context.HasBoxAnimation ||
context.HasCraftingPanel;
if (hasAnySpectreFeature)
{
return new SpectreRenderer(context);
}
return new BasicRenderer();
}
}

View file

@ -0,0 +1,411 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Characters;
using OpenTheBox.Core.Enums;
using OpenTheBox.Rendering.Panels;
using Spectre.Console;
namespace OpenTheBox.Rendering;
/// <summary>
/// Progressive renderer using Spectre.Console. Checks <see cref="RenderContext"/> for each
/// feature and falls back gracefully when a capability is not yet unlocked.
/// </summary>
public sealed class SpectreRenderer : IRenderer
{
// ── Message output ──────────────────────────────────────────────────
public void ShowMessage(string message)
{
if (_context.HasColors)
{
AnsiConsole.MarkupLine($"[green]{Markup.Escape(message)}[/]");
}
else
{
Console.WriteLine(message);
}
}
public void ShowError(string message)
{
if (_context.HasColors)
{
AnsiConsole.MarkupLine($"[bold red]ERROR:[/] [red]{Markup.Escape(message)}[/]");
}
else
{
Console.WriteLine($"ERROR: {message}");
}
}
// ── Box opening ─────────────────────────────────────────────────────
public void ShowBoxOpening(string boxName, string rarity)
{
if (_context.HasBoxAnimation)
{
string color = RarityColor(rarity);
AnsiConsole.Status()
.Spinner(Spinner.Known.Star)
.SpinnerStyle(new Style(RarityColorValue(rarity)))
.Start($"Opening [bold {color}]{Markup.Escape(boxName)}[/]...", ctx =>
{
Thread.Sleep(1500);
ctx.Status($"[bold {color}]Something shimmers...[/]");
Thread.Sleep(1000);
});
AnsiConsole.MarkupLine($"[bold {color}]{Markup.Escape(boxName)}[/] opened!");
}
else if (_context.HasColors)
{
string color = RarityColor(rarity);
AnsiConsole.MarkupLine($"Opening [bold {color}]{Markup.Escape(boxName)}[/]...");
Thread.Sleep(800);
AnsiConsole.MarkupLine($"[bold {color}]{Markup.Escape(boxName)}[/] opened!");
}
else
{
Console.WriteLine($"Opening {boxName}...");
Thread.Sleep(500);
Console.WriteLine($"{boxName} opened! (Rarity: {rarity})");
}
}
// ── Loot reveal ─────────────────────────────────────────────────────
public void ShowLootReveal(List<(string name, string rarity, string category)> items)
{
if (_context.HasInventoryPanel)
{
var table = new Table()
.Border(TableBorder.Rounded)
.Title("[bold yellow]Loot![/]")
.AddColumn(new TableColumn("[bold]Name[/]").Centered())
.AddColumn(new TableColumn("[bold]Rarity[/]").Centered())
.AddColumn(new TableColumn("[bold]Category[/]").Centered());
foreach (var (name, rarity, category) in items)
{
string color = RarityColor(rarity);
table.AddRow(
$"[{color}]{Markup.Escape(name)}[/]",
$"[{color}]{Markup.Escape(rarity)}[/]",
Markup.Escape(category));
}
AnsiConsole.Write(table);
}
else if (_context.HasColors)
{
AnsiConsole.MarkupLine("[bold yellow]You received:[/]");
foreach (var (name, rarity, category) in items)
{
string color = RarityColor(rarity);
AnsiConsole.MarkupLine($" - [{color}]{Markup.Escape(name)}[/] [{color}][{Markup.Escape(rarity)}][/] ({Markup.Escape(category)})");
}
}
else
{
Console.WriteLine("You received:");
foreach (var (name, rarity, category) in items)
{
Console.WriteLine($" - {name} [{rarity}] ({category})");
}
}
}
// ── Selection prompts ───────────────────────────────────────────────
public int ShowSelection(string prompt, List<string> options)
{
if (_context.HasArrowSelection)
{
string selected = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title(Markup.Escape(prompt))
.PageSize(10)
.AddChoices(options));
return options.IndexOf(selected);
}
if (_context.HasColors)
{
AnsiConsole.MarkupLine($"[bold]{Markup.Escape(prompt)}[/]");
for (int i = 0; i < options.Count; i++)
{
AnsiConsole.MarkupLine($" [cyan]{i + 1}.[/] {Markup.Escape(options[i])}");
}
}
else
{
Console.WriteLine(prompt);
for (int i = 0; i < options.Count; i++)
{
Console.WriteLine($" {i + 1}. {options[i]}");
}
}
while (true)
{
Console.Write("> ");
string? input = Console.ReadLine();
if (int.TryParse(input, out int choice) && choice >= 1 && choice <= options.Count)
{
return choice - 1;
}
if (_context.HasColors)
{
AnsiConsole.MarkupLine($"[red]Please enter a number between 1 and {options.Count}.[/]");
}
else
{
Console.WriteLine($"Please enter a number between 1 and {options.Count}.");
}
}
}
public string ShowTextInput(string prompt)
{
if (_context.HasColors)
{
return AnsiConsole.Prompt(
new TextPrompt<string>($"[bold]{Markup.Escape(prompt)}[/]:"));
}
Console.Write($"{prompt}: ");
return Console.ReadLine() ?? string.Empty;
}
// ── Game state ──────────────────────────────────────────────────────
public void ShowGameState(GameState state, RenderContext context)
{
if (context.HasFullLayout)
{
RenderFullLayout(state, context);
}
else
{
RenderSequentialPanels(state, context);
}
}
// ── Adventure dialogue ──────────────────────────────────────────────
public void ShowAdventureDialogue(string? character, string text)
{
if (_context.HasColors)
{
if (character is not null)
{
AnsiConsole.MarkupLine($"[bold aqua]{Markup.Escape(character)}[/]");
}
AnsiConsole.MarkupLine($" [italic]{Markup.Escape(text)}[/]");
AnsiConsole.WriteLine();
}
else
{
if (character is not null)
{
Console.WriteLine($"[{character}]");
}
Console.WriteLine(text);
Console.WriteLine();
}
}
public int ShowAdventureChoice(List<string> options)
{
if (_context.HasArrowSelection)
{
string selected = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("[bold yellow]What do you do?[/]")
.PageSize(10)
.AddChoices(options));
return options.IndexOf(selected);
}
return ShowSelection("What do you do?", options);
}
// ── UI feature unlock announcement ──────────────────────────────────
public void ShowUIFeatureUnlocked(string featureName)
{
if (_context.HasColors)
{
AnsiConsole.Write(new Rule($"[bold yellow]NEW FEATURE UNLOCKED[/]").RuleStyle("yellow"));
AnsiConsole.Write(new FigletText(featureName).Color(Color.Yellow).Centered());
AnsiConsole.Write(new Rule().RuleStyle("yellow"));
}
else
{
Console.WriteLine("========================================");
Console.WriteLine($" NEW FEATURE UNLOCKED: {featureName}");
Console.WriteLine("========================================");
}
}
// ── Interaction ─────────────────────────────────────────────────────
public void ShowInteraction(string description)
{
if (_context.HasColors)
{
AnsiConsole.MarkupLine($"[italic silver]* {Markup.Escape(description)} *[/]");
}
else
{
Console.WriteLine($"* {description} *");
}
}
// ── Utility ─────────────────────────────────────────────────────────
public void WaitForKeyPress(string? message = null)
{
if (_context.HasColors)
{
AnsiConsole.MarkupLine($"[dim]{Markup.Escape(message ?? "Press any key to continue...")}[/]");
}
else
{
Console.WriteLine(message ?? "Press any key to continue...");
}
Console.ReadKey(intercept: true);
Console.WriteLine();
}
public void Clear()
{
AnsiConsole.Clear();
}
// ── Construction ────────────────────────────────────────────────────
private RenderContext _context;
public SpectreRenderer(RenderContext context)
{
_context = context;
}
/// <summary>
/// Allows updating the context when new features are unlocked mid-session.
/// </summary>
public void UpdateContext(RenderContext context)
{
_context = context;
}
// ── Private helpers ─────────────────────────────────────────────────
private static string RarityColor(string rarity) => rarity.ToLowerInvariant() switch
{
"common" => "white",
"uncommon" => "green",
"rare" => "blue",
"epic" => "purple",
"legendary" => "gold1",
"mythic" => "red",
_ => "white"
};
private static Color RarityColorValue(string rarity) => rarity.ToLowerInvariant() switch
{
"common" => Color.White,
"uncommon" => Color.Green,
"rare" => Color.Blue,
"epic" => Color.Purple,
"legendary" => Color.Gold1,
"mythic" => Color.Red,
_ => Color.White
};
/// <summary>
/// Renders all panels in a full Spectre Layout grid.
/// </summary>
private void RenderFullLayout(GameState state, RenderContext context)
{
var layout = new Layout("Root")
.SplitRows(
new Layout("Top")
.SplitColumns(
new Layout("Portrait"),
new Layout("Stats"),
new Layout("Resources")),
new Layout("Middle")
.SplitColumns(
new Layout("Inventory").Ratio(2),
new Layout("Chat")),
new Layout("Bottom"));
if (context.HasPortraitPanel)
layout["Portrait"].Update(PortraitPanel.Render(state.Appearance));
else
layout["Portrait"].Update(new Panel("[dim]???[/]").Header("Portrait"));
if (context.HasStatsPanel)
layout["Stats"].Update(StatsPanel.Render(state));
else
layout["Stats"].Update(new Panel("[dim]???[/]").Header("Stats"));
if (context.HasResourcePanel)
layout["Resources"].Update(ResourcePanel.Render(state));
else
layout["Resources"].Update(new Panel("[dim]???[/]").Header("Resources"));
if (context.HasInventoryPanel)
layout["Inventory"].Update(InventoryPanel.Render(state));
else
layout["Inventory"].Update(new Panel("[dim]???[/]").Header("Inventory"));
if (context.HasChatPanel)
layout["Chat"].Update(ChatPanel.Render([]));
else
layout["Chat"].Update(new Panel("[dim]???[/]").Header("Chat"));
if (context.HasCraftingPanel)
layout["Bottom"].Update(new Panel("[dim]Crafting area[/]").Header("Crafting"));
else
layout["Bottom"].Update(new Panel("[dim]???[/]").Header("???"));
AnsiConsole.Write(layout);
}
/// <summary>
/// Renders only the panels the player has unlocked, stacked vertically.
/// </summary>
private void RenderSequentialPanels(GameState state, RenderContext context)
{
if (context.HasPortraitPanel)
{
AnsiConsole.Write(PortraitPanel.Render(state.Appearance));
}
if (context.HasStatsPanel)
{
AnsiConsole.Write(StatsPanel.Render(state));
}
if (context.HasResourcePanel)
{
AnsiConsole.Write(ResourcePanel.Render(state));
}
if (context.HasInventoryPanel)
{
AnsiConsole.Write(InventoryPanel.Render(state));
}
if (context.HasChatPanel)
{
AnsiConsole.Write(ChatPanel.Render([]));
}
}
}

View file

@ -0,0 +1,52 @@
using OpenTheBox.Core.Enums;
namespace OpenTheBox.Simulation.Actions;
/// <summary>
/// Base type for all player-initiated actions in the game.
/// </summary>
public abstract record GameAction
{
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}
/// <summary>
/// Player opens a box from their inventory.
/// </summary>
public sealed record OpenBoxAction(Guid BoxInstanceId) : GameAction
{
/// <summary>
/// The definition id of the box being opened.
/// </summary>
public string? BoxDefinitionId { get; init; }
}
/// <summary>
/// Player uses an item from their inventory.
/// </summary>
public sealed record UseItemAction(Guid ItemInstanceId) : GameAction;
/// <summary>
/// Player crafts at a workstation using material items.
/// </summary>
public sealed record CraftAction(WorkstationType Station, List<Guid> MaterialIds) : GameAction;
/// <summary>
/// Player starts an adventure.
/// </summary>
public sealed record StartAdventureAction(AdventureTheme Theme) : GameAction;
/// <summary>
/// Player changes the game locale.
/// </summary>
public sealed record ChangeLocaleAction(Locale NewLocale) : GameAction;
/// <summary>
/// Player equips a cosmetic item.
/// </summary>
public sealed record EquipCosmeticAction(Guid ItemInstanceId) : GameAction;
/// <summary>
/// Player saves the game to a named slot.
/// </summary>
public sealed record SaveGameAction(string SlotName) : GameAction;

View file

@ -0,0 +1,154 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Boxes;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
using OpenTheBox.Data;
using OpenTheBox.Simulation.Events;
namespace OpenTheBox.Simulation;
/// <summary>
/// Handles box opening logic: evaluates loot table conditions, performs weighted random rolls,
/// and recursively opens auto-open boxes.
/// </summary>
public class BoxEngine(ContentRegistry registry)
{
/// <summary>
/// Opens a box, evaluating its loot table against the current game state and returning
/// all resulting events (items received, nested box opens, etc.).
/// </summary>
public List<GameEvent> Open(string boxDefId, GameState state, Random rng)
{
var events = new List<GameEvent>();
var boxDef = registry.GetBox(boxDefId);
if (boxDef is null)
return events;
var eligibleEntries = FilterEligibleEntries(boxDef.LootTable, state);
if (eligibleEntries.Count == 0)
return events;
var droppedItemDefIds = new List<string>();
// Handle guaranteed rolls: take the top entries by weight up to GuaranteedRolls count
var guaranteedCount = Math.Min(boxDef.LootTable.GuaranteedRolls, eligibleEntries.Count);
var guaranteedEntries = eligibleEntries
.OrderByDescending(e => e.Weight)
.Take(guaranteedCount)
.ToList();
foreach (var entry in guaranteedEntries)
{
droppedItemDefIds.Add(entry.ItemDefinitionId);
}
// Handle weighted random rolls
if (boxDef.LootTable.RollCount > 0 && eligibleEntries.Count > 0)
{
var weightedEntries = eligibleEntries
.Select(e => ((LootEntry)e, e.Weight))
.ToList();
var picks = WeightedRandom.PickMultiple(weightedEntries, rng, boxDef.LootTable.RollCount);
foreach (var pick in picks)
{
droppedItemDefIds.Add(pick.ItemDefinitionId);
}
}
events.Add(new BoxOpenedEvent(boxDefId, droppedItemDefIds));
// Create item instances for each dropped item
foreach (var itemDefId in droppedItemDefIds)
{
var itemDef = registry.GetItem(itemDefId);
if (itemDef is null)
continue;
var instance = ItemInstance.Create(itemDefId);
state.AddItem(instance);
events.Add(new ItemReceivedEvent(instance));
// Recursively open auto-open boxes
if (itemDef.Category == ItemCategory.Box)
{
var nestedBoxDef = registry.GetBox(itemDefId);
if (nestedBoxDef is not null && nestedBoxDef.IsAutoOpen)
{
state.RemoveItem(instance.Id);
events.Add(new ItemConsumedEvent(instance.Id));
events.AddRange(Open(itemDefId, state, rng));
}
}
}
return events;
}
/// <summary>
/// Filters loot entries to only those whose conditions are satisfied by the current game state.
/// </summary>
private static List<LootEntry> FilterEligibleEntries(LootTable lootTable, GameState state)
{
var eligible = new List<LootEntry>();
foreach (var entry in lootTable.Entries)
{
if (entry.Condition is null || EvaluateCondition(entry.Condition, state))
{
eligible.Add(entry);
}
}
return eligible;
}
/// <summary>
/// Evaluates a single loot condition against the game state.
/// </summary>
private static bool EvaluateCondition(LootCondition condition, GameState state)
{
return condition.Type switch
{
LootConditionType.HasItem => condition.TargetId is not null && state.HasItem(condition.TargetId),
LootConditionType.HasNotItem => condition.TargetId is not null && !state.HasItem(condition.TargetId),
LootConditionType.ResourceAbove => condition.TargetId is not null
&& condition.Value.HasValue
&& Enum.TryParse<ResourceType>(condition.TargetId, out var resAbove)
&& state.GetResource(resAbove) > condition.Value.Value,
LootConditionType.ResourceBelow => condition.TargetId is not null
&& condition.Value.HasValue
&& Enum.TryParse<ResourceType>(condition.TargetId, out var resBelow)
&& state.GetResource(resBelow) < condition.Value.Value,
LootConditionType.HasUIFeature => condition.TargetId is not null
&& Enum.TryParse<UIFeature>(condition.TargetId, out var feature)
&& state.HasUIFeature(feature),
LootConditionType.BoxesOpenedAbove => condition.Value.HasValue
&& state.TotalBoxesOpened > condition.Value.Value,
LootConditionType.HasWorkstation => condition.TargetId is not null
&& Enum.TryParse<WorkstationType>(condition.TargetId, out var ws)
&& state.UnlockedWorkstations.Contains(ws),
LootConditionType.HasAdventure => condition.TargetId is not null
&& Enum.TryParse<AdventureTheme>(condition.TargetId, out var adv)
&& state.UnlockedAdventures.Contains(adv),
LootConditionType.HasCosmetic => condition.TargetId is not null
&& state.UnlockedCosmetics.Contains(condition.TargetId),
_ => true
};
}
/// <summary>
/// Performs a comparison operation between an actual value and a target value.
/// </summary>
private static bool CompareValue(float actual, string? comparison, float target)
{
return comparison switch
{
">=" => actual >= target,
"<=" => actual <= target,
">" => actual > target,
"<" => actual < target,
"==" => Math.Abs(actual - target) < 0.001f,
"!=" => Math.Abs(actual - target) >= 0.001f,
_ => false
};
}
}

View file

@ -0,0 +1,72 @@
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
namespace OpenTheBox.Simulation.Events;
/// <summary>
/// Base type for all events produced by the simulation in response to actions.
/// </summary>
public abstract record GameEvent
{
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}
/// <summary>
/// A box was opened, producing dropped items.
/// </summary>
public sealed record BoxOpenedEvent(string BoxId, List<string> DroppedItemDefIds) : GameEvent;
/// <summary>
/// An item was received and added to inventory.
/// </summary>
public sealed record ItemReceivedEvent(ItemInstance Item) : GameEvent;
/// <summary>
/// An item was consumed and removed from inventory.
/// </summary>
public sealed record ItemConsumedEvent(Guid InstanceId) : GameEvent;
/// <summary>
/// An interaction rule was triggered.
/// </summary>
public sealed record InteractionTriggeredEvent(string RuleId, string DescriptionKey) : GameEvent;
/// <summary>
/// A UI feature was unlocked via a meta item.
/// </summary>
public sealed record UIFeatureUnlockedEvent(UIFeature Feature) : GameEvent;
/// <summary>
/// A resource value changed.
/// </summary>
public sealed record ResourceChangedEvent(ResourceType Type, int OldValue, int NewValue) : GameEvent;
/// <summary>
/// A cosmetic was equipped in a slot.
/// </summary>
public sealed record CosmeticEquippedEvent(CosmeticSlot Slot, string NewValue) : GameEvent;
/// <summary>
/// An adventure was started.
/// </summary>
public sealed record AdventureStartedEvent(AdventureTheme Theme) : GameEvent;
/// <summary>
/// An adventure was completed.
/// </summary>
public sealed record AdventureCompletedEvent(AdventureTheme Theme) : GameEvent;
/// <summary>
/// A loot table was modified (e.g., a key injected a matching box).
/// </summary>
public sealed record LootTableModifiedEvent(string BoxId, string AddedEntryId, string Reason) : GameEvent;
/// <summary>
/// The simulation requires the player to make a choice between options.
/// </summary>
public sealed record ChoiceRequiredEvent(string Prompt, List<string> Options) : GameEvent;
/// <summary>
/// A generic message to be displayed to the player.
/// </summary>
public sealed record MessageEvent(string MessageKey, string[]? Args = null) : GameEvent;

View file

@ -0,0 +1,221 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
using OpenTheBox.Data;
using OpenTheBox.Simulation.Actions;
using OpenTheBox.Simulation.Events;
namespace OpenTheBox.Simulation;
/// <summary>
/// The BLACK BOX. Zero I/O. Central orchestrator for all game logic.
/// All game actions are processed through this class, which dispatches to the appropriate
/// engine and runs post-processing passes (auto-activation, meta unlocks).
/// </summary>
public class GameSimulation
{
private readonly ContentRegistry _registry;
private readonly Random _rng;
private readonly BoxEngine _boxEngine;
private readonly InteractionEngine _interactionEngine;
private readonly MetaEngine _metaEngine;
private readonly ResourceEngine _resourceEngine;
public GameSimulation(ContentRegistry registry, Random? rng = null)
{
_registry = registry;
_rng = rng ?? new Random();
_boxEngine = new BoxEngine(registry);
_interactionEngine = new InteractionEngine(registry);
_metaEngine = new MetaEngine();
_resourceEngine = new ResourceEngine();
}
/// <summary>
/// Processes a game action against the current state, returning all resulting events in order.
/// This is the single entry point for all game logic.
/// </summary>
public List<GameEvent> ProcessAction(GameAction action, GameState state)
{
var events = new List<GameEvent>();
switch (action)
{
case OpenBoxAction openBox:
events.AddRange(HandleOpenBox(openBox, state));
break;
case UseItemAction useItem:
events.AddRange(HandleUseItem(useItem, state));
break;
case CraftAction craft:
events.AddRange(HandleCraft(craft, state));
break;
case StartAdventureAction startAdventure:
events.AddRange(HandleStartAdventure(startAdventure, state));
break;
case ChangeLocaleAction changeLocale:
events.AddRange(HandleChangeLocale(changeLocale, state));
break;
case EquipCosmeticAction equipCosmetic:
events.AddRange(HandleEquipCosmetic(equipCosmetic, state));
break;
case SaveGameAction:
// Save is handled externally; simulation just acknowledges
events.Add(new MessageEvent("save.acknowledged"));
break;
}
return events;
}
private List<GameEvent> HandleOpenBox(OpenBoxAction action, GameState state)
{
var events = new List<GameEvent>();
// Find the box item in inventory
var boxItem = state.Inventory.FirstOrDefault(i => i.Id == action.BoxInstanceId);
if (boxItem is null)
{
events.Add(new MessageEvent("error.item_not_found"));
return events;
}
// Consume the box item
state.RemoveItem(boxItem.Id);
events.Add(new ItemConsumedEvent(boxItem.Id));
// Open the box
var boxEvents = _boxEngine.Open(boxItem.DefinitionId, state, _rng);
events.AddRange(boxEvents);
state.TotalBoxesOpened++;
// Collect newly received items for post-processing
var newItems = boxEvents.OfType<ItemReceivedEvent>().Select(e => e.Item).ToList();
// Run auto-activation pass
events.AddRange(_interactionEngine.CheckAutoActivations(newItems, state));
// Run meta pass
events.AddRange(_metaEngine.ProcessNewItems(newItems, state, _registry));
return events;
}
private List<GameEvent> HandleUseItem(UseItemAction action, GameState state)
{
var events = new List<GameEvent>();
var item = state.Inventory.FirstOrDefault(i => i.Id == action.ItemInstanceId);
if (item is null)
{
events.Add(new MessageEvent("error.item_not_found"));
return events;
}
var itemDef = _registry.GetItem(item.DefinitionId);
if (itemDef is null)
{
events.Add(new MessageEvent("error.definition_not_found"));
return events;
}
// Check if it's a consumable resource item
if (itemDef.ResourceType.HasValue)
{
events.AddRange(_resourceEngine.ProcessConsumable(item, state, _registry));
return events;
}
// Otherwise, check interaction rules
var interactionEvents = _interactionEngine.CheckAutoActivations([item], state);
events.AddRange(interactionEvents);
return events;
}
private List<GameEvent> HandleCraft(CraftAction action, GameState state)
{
var events = new List<GameEvent>();
if (!state.UnlockedWorkstations.Contains(action.Station))
{
events.Add(new MessageEvent("error.workstation_locked"));
return events;
}
// Consume all material items
foreach (var materialId in action.MaterialIds)
{
var material = state.Inventory.FirstOrDefault(i => i.Id == materialId);
if (material is null)
{
events.Add(new MessageEvent("error.material_not_found", [materialId.ToString()]));
return events;
}
state.RemoveItem(materialId);
events.Add(new ItemConsumedEvent(materialId));
}
// Crafting result is determined by interaction rules matching the materials
// The interaction engine will handle rule matching and result production
events.Add(new MessageEvent("craft.materials_consumed"));
return events;
}
private List<GameEvent> HandleStartAdventure(StartAdventureAction action, GameState state)
{
var events = new List<GameEvent>();
if (!state.UnlockedAdventures.Contains(action.Theme))
{
events.Add(new MessageEvent("error.adventure_locked"));
return events;
}
events.Add(new AdventureStartedEvent(action.Theme));
return events;
}
private static List<GameEvent> HandleChangeLocale(ChangeLocaleAction action, GameState state)
{
var events = new List<GameEvent>();
state.CurrentLocale = action.NewLocale;
events.Add(new MessageEvent("locale.changed", [action.NewLocale.ToString()]));
return events;
}
private List<GameEvent> HandleEquipCosmetic(EquipCosmeticAction action, GameState state)
{
var events = new List<GameEvent>();
var item = state.Inventory.FirstOrDefault(i => i.Id == action.ItemInstanceId);
if (item is null)
{
events.Add(new MessageEvent("error.item_not_found"));
return events;
}
var itemDef = _registry.GetItem(item.DefinitionId);
if (itemDef?.CosmeticSlot is null || itemDef.CosmeticValue is null)
{
events.Add(new MessageEvent("error.not_a_cosmetic"));
return events;
}
events.Add(new CosmeticEquippedEvent(itemDef.CosmeticSlot.Value, itemDef.CosmeticValue));
return events;
}
}

View file

@ -0,0 +1,139 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Interactions;
using OpenTheBox.Core.Items;
using OpenTheBox.Data;
using OpenTheBox.Simulation.Events;
namespace OpenTheBox.Simulation;
/// <summary>
/// Evaluates interaction rules against newly received items and the current game state,
/// triggering automatic interactions or requesting player choices when multiple apply.
/// </summary>
public class InteractionEngine(ContentRegistry registry)
{
/// <summary>
/// Checks all interaction rules against newly received items and returns events for
/// any auto-activations or choice prompts.
/// </summary>
public List<GameEvent> CheckAutoActivations(List<ItemInstance> newItems, GameState state)
{
var events = new List<GameEvent>();
foreach (var newItem in newItems)
{
var itemDef = registry.GetItem(newItem.DefinitionId);
if (itemDef is null)
continue;
var matchingRules = FindMatchingRules(itemDef, state);
if (matchingRules.Count == 0)
{
// Special case: key without a matching openable item injects a box into future loot tables
if (itemDef.Tags.Contains("Key"))
{
var hasMatchingOpenable = state.Inventory.Any(i =>
{
var def = registry.GetItem(i.DefinitionId);
return def is not null && def.Tags.Contains("Openable");
});
if (!hasMatchingOpenable)
{
events.Add(new LootTableModifiedEvent(
BoxId: "starter_box",
AddedEntryId: newItem.DefinitionId,
Reason: $"Key '{newItem.DefinitionId}' has no matching Openable item; injecting into future loot tables"
));
}
}
continue;
}
var automaticRules = matchingRules
.Where(r => r.IsAutomatic)
.OrderByDescending(r => r.Priority)
.ToList();
if (automaticRules.Count == 1)
{
// Single automatic match: auto-execute
var rule = automaticRules[0];
events.AddRange(ExecuteRule(rule, newItem, state));
}
else if (automaticRules.Count > 1)
{
// Multiple automatic matches: let the player choose
events.Add(new ChoiceRequiredEvent(
Prompt: $"Multiple interactions available for '{newItem.DefinitionId}'",
Options: automaticRules.Select(r => r.Id).ToList()
));
}
else
{
// Non-automatic rules found but none are automatic -- no auto-activation
}
}
return events;
}
/// <summary>
/// Finds all interaction rules that match the given item definition and game state.
/// </summary>
private List<InteractionRule> FindMatchingRules(ItemDefinition itemDef, GameState state)
{
var matching = new List<InteractionRule>();
foreach (var rule in registry.InteractionRules)
{
// Check required tags
var tagsMatch = rule.RequiredItemTags.All(tag => itemDef.Tags.Contains(tag));
if (!tagsMatch)
continue;
// Check required item ids (if specified, at least one must be in inventory)
if (rule.RequiredItemIds is not null && rule.RequiredItemIds.Count > 0)
{
var hasRequiredItem = rule.RequiredItemIds.All(id => state.HasItem(id));
if (!hasRequiredItem)
continue;
}
matching.Add(rule);
}
return matching;
}
/// <summary>
/// Executes a single interaction rule, consuming the trigger item and producing results.
/// </summary>
private List<GameEvent> ExecuteRule(InteractionRule rule, ItemInstance triggerItem, GameState state)
{
var events = new List<GameEvent>();
// Consume the trigger item
state.RemoveItem(triggerItem.Id);
events.Add(new ItemConsumedEvent(triggerItem.Id));
// Produce result items if ResultData specifies item definition ids (comma-separated)
if (rule.ResultData is not null)
{
var resultItemIds = rule.ResultData.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var resultItemId in resultItemIds)
{
var resultInstance = ItemInstance.Create(resultItemId);
state.AddItem(resultInstance);
events.Add(new ItemReceivedEvent(resultInstance));
}
}
events.Add(new InteractionTriggeredEvent(rule.Id, rule.DescriptionKey));
return events;
}
}

View file

@ -0,0 +1,61 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
using OpenTheBox.Data;
using OpenTheBox.Simulation.Events;
namespace OpenTheBox.Simulation;
/// <summary>
/// Processes meta-tagged items to unlock UI features, make resources/stats visible,
/// and apply cosmetic or other permanent progression changes.
/// </summary>
public class MetaEngine
{
/// <summary>
/// Processes newly received items and applies any meta-unlock effects to the game state.
/// </summary>
public List<GameEvent> ProcessNewItems(List<ItemInstance> newItems, GameState state, ContentRegistry registry)
{
var events = new List<GameEvent>();
foreach (var item in newItems)
{
var itemDef = registry.GetItem(item.DefinitionId);
if (itemDef is null)
continue;
// Unlock UI feature if this item has a MetaUnlock
if (itemDef.MetaUnlock.HasValue && state.UnlockedUIFeatures.Add(itemDef.MetaUnlock.Value))
{
events.Add(new UIFeatureUnlockedEvent(itemDef.MetaUnlock.Value));
}
// Make resource type visible if this item references a resource
if (itemDef.ResourceType.HasValue)
{
state.VisibleResources.Add(itemDef.ResourceType.Value);
}
// Unlock workstation if this item references one
if (itemDef.WorkstationType.HasValue)
{
state.UnlockedWorkstations.Add(itemDef.WorkstationType.Value);
}
// Unlock adventure if this item references a theme
if (itemDef.AdventureTheme.HasValue)
{
state.UnlockedAdventures.Add(itemDef.AdventureTheme.Value);
}
// Track cosmetic unlocks
if (itemDef.CosmeticSlot.HasValue && itemDef.CosmeticValue is not null)
{
state.UnlockedCosmetics.Add(item.DefinitionId);
}
}
return events;
}
}

View file

@ -0,0 +1,51 @@
using OpenTheBox.Core;
using OpenTheBox.Core.Items;
using OpenTheBox.Data;
using OpenTheBox.Simulation.Events;
namespace OpenTheBox.Simulation;
/// <summary>
/// Processes consumable items that modify resource values, clamping results
/// within valid bounds.
/// </summary>
public class ResourceEngine
{
/// <summary>
/// Applies the resource effect of a consumable item to the game state.
/// Returns a <see cref="ResourceChangedEvent"/> if a resource was modified, or an empty list otherwise.
/// </summary>
public List<GameEvent> ProcessConsumable(ItemInstance item, GameState state, ContentRegistry registry)
{
var events = new List<GameEvent>();
var itemDef = registry.GetItem(item.DefinitionId);
if (itemDef?.ResourceType is null || itemDef.ResourceAmount is null)
return events;
var resourceType = itemDef.ResourceType.Value;
var amount = itemDef.ResourceAmount.Value;
if (!state.Resources.TryGetValue(resourceType, out var resourceState))
{
// Resource not yet tracked; initialize with a default max
resourceState = new ResourceState(0, 100);
state.Resources[resourceType] = resourceState;
}
var oldValue = resourceState.Current;
var newValue = Math.Clamp(oldValue + amount, 0, resourceState.Max);
if (newValue != oldValue)
{
state.Resources[resourceType] = resourceState with { Current = newValue };
events.Add(new ResourceChangedEvent(resourceType, oldValue, newValue));
}
// Consume the item
state.RemoveItem(item.Id);
events.Add(new ItemConsumedEvent(item.Id));
return events;
}
}

View file

@ -0,0 +1,46 @@
namespace OpenTheBox.Simulation;
/// <summary>
/// Utility class for performing weighted random selections.
/// </summary>
public static class WeightedRandom
{
/// <summary>
/// Picks a single item from a weighted list using the provided random source.
/// </summary>
/// <exception cref="ArgumentException">Thrown when the entries list is empty.</exception>
public static T Pick<T>(IReadOnlyList<(T item, float weight)> entries, Random rng)
{
if (entries.Count == 0)
throw new ArgumentException("Cannot pick from an empty list.", nameof(entries));
var totalWeight = 0f;
for (var i = 0; i < entries.Count; i++)
totalWeight += entries[i].weight;
var roll = (float)(rng.NextDouble() * totalWeight);
var cumulative = 0f;
for (var i = 0; i < entries.Count; i++)
{
cumulative += entries[i].weight;
if (roll < cumulative)
return entries[i].item;
}
// Fallback to last entry (handles floating-point edge cases)
return entries[^1].item;
}
/// <summary>
/// Picks multiple items from a weighted list (with replacement) using the provided random source.
/// </summary>
public static List<T> PickMultiple<T>(IReadOnlyList<(T item, float weight)> entries, Random rng, int count)
{
var results = new List<T>(count);
for (var i = 0; i < count; i++)
results.Add(Pick(entries, rng));
return results;
}
}