A game by eXtIO.de · on Steam Spacecraft Tactics

A 2D turn-based tactical space game. Save the last rebel outpost: develop a strategy, execute it in tactical battles and micromanage your ship.

Spacecraft Tactics blends strategic campaign play, ship management and turn-based tactical combat into one deep, moddable package. Play it solo, in online co-op, PvP or PvE — and once you're done playing, the built-in editors and mod tools let you build the game yourself.

What's in the game

Campaign

Fight through 5 chapters, collect loot, craft items and upgrade your ship against increasingly difficult challenges — with 3 difficulty levels, a dedicated speedrun mode with splits, and global highscores.

Micromanage your ship

Command your crew across energy, shield, O2 and network grids: distribute power, fight damage, heal and revive the crew, keep shields, air and healing fields up — with automations when things get busy.

Tactical battles

Manage your capacitor, shield buffer and EM emissions. Pick the right loadout, position your ship, watch your opponent — develop a strategy and execute it.

5 game modes

The Last Outpost, Battles (PvE with progression), Arena (PvP), Race and Custom Game — all single-player and multiplayer, with online co-op, a game browser, Steam invites and quick-join codes.

Editors & sharing

Design your own ships in the ship editor and new scenarios in the scene editor — then share them with the integrated sharing functionality or on Steam Workshop.

Modding

Modular from the ground up: rebalance the game, add tilesets, sounds and translations, or go full conversion with the Groovy scripting API. Workshop-ready.

Screenshots

The campaign: relocate the outpost before the main fleet arrives.
Ship view: crew, systems, grids and damage control.
Tactical battles: positioning, emissions and loadout choices.

One person, seven years, one game

Spacecraft Tactics is a one-man project, developed in my spare time over seven years between 2016 and 2023 — engine, tools, art direction, servers, everything. I hope you like it.

Visit spacecraft-tactics.com Wishlist on Steam

A game by eXtIO.de · Early Access on Steam

An LLM-driven choose-your-own-adventure that adapts to you. You describe your next move — the game responds with new narration.

Guide a party through dangerous places, talk to characters, inspect the world, collect gear, craft solutions and make choices that reshape the narrative. There is no script to stay on: your freeform instructions are interpreted into real world actions. Behind the chat flow, a structured simulation — locations, entities, items, quests — grounds every action and remembers what you did.

How it plays

Say what you want to do

Move, attack, talk, inspect, trade, craft, sneak, manage your party — your instruction is classified into structured actions, and multiple actions can chain in one turn. Or just pick one from the menu.

Party & inventory

Run a party of characters, each with attributes, skills, health and a personal backpack. Pick up, use and transfer items, or trade them with NPCs.

Quests & a connected world

Main and side quests with ordered objectives that update automatically as the story unfolds — across a graph of locations with NPCs, ambient items and danger levels.

Game Master

An autonomous LLM-driven game master resolves world-side consequences after your turn: NPC reactions, pursuit, missed fallout, quest developments. Toggle it on or off as you like.

Three-tier memory

A sliding chat window, importance-filtered summaries and a tag-based archive keep the story coherent over long sessions — while keeping VRAM and token costs low.

Fully local LLMs

Ships with an embedded llama-server and runs on edge-device hardware from about 8 GB VRAM. Also connects to Ollama and OpenAI-compatible endpoints. Your story stays on your machine.

Screenshots

Your turn, your style — freeform input or menu, with Game Master, party and quests always in view.
The world at a glance — locations, characters and live quest state.
New adventures in minutes: the assistant interviews you and drafts the world.

Local-first, by design

Exo's CYOA only connects to the bundled runtime or LLM software on your own machine — cloud providers are not supported. Guardrails with eight protected categories keep the sandbox safe without becoming moral censorship. Adventures can be exported as PDF, chat-style or as a book. It launched in Early Access on Steam: fully playable today, with roughly a year of polish, deeper world reactivity and more adventure styles ahead.

Visit exos-cyoa.com Get it on Steam

A Java library by eXtIO.de · Apache 2.0

lm-lib

A modular Java library for LLM-powered applications: an agent runtime for agentic orchestration plus a client layer for six LLM providers — published on Maven Central.

In practice — a real agent from Exo's CYOA

ActionClassifierAgent is one of dozens of lm-lib agents shipping in Exo's CYOA: it has the model classify the player's instruction as JSON, JsonAgentResponseHandler parses the answer into the agent context, and post processing filters the parsed fields down to usable actions. Condensed for brevity.

ActionClassifierAgent.java — exos-cyoa
1// A real lm-lib agent shipping in Exo's CYOA (condensed):
2// classifies the player's instruction by having the model
3// answer in JSON, then works on the parsed result below.
4@Component
5class ActionClassifierAgent implements BaseAgent {
6 
7 public AgentType agentType(final AgentContext ctx) {
8 return AgentType.COMPLETION;
9 }
10 
11 public String textTemplate() {
12 return """
13 PLAYER INSTRUCTION: {{{instruction}}}
14 Classify the instruction into the action taxonomy.
15 OUTPUT FORMAT: {"actions":[{"name":"...",
16 "confidence":"HIGH|MEDIUM"}]}. Return only the json.
17 """;
18 }
19 
20 public AgentResponseHandler responseHandler(final AgentContext ctx) {
21 return new JsonAgentResponseHandler("_classifier_",
22 (context, chunk) -> Thinking.modelReasoning(
23 context, chunk.reasoningContent()));
24 }
25 
26 public void postProcess(final AgentContext context) {
27 final var names = context.getStringValues("_classifier_name");
28 final var confidences = context.getStringValues("_classifier_confidence");
29 final var actions = new ArrayList<ClassifiedAction>();
30 for (int i = 0; i < names.size(); i++) {
31 if (!List.of("HIGH", "MEDIUM").contains(confidences.get(i))) {
32 continue; // drop LOW confidence entries
33 }
34 actions.add(new ClassifiedAction(names.get(i), confidences.get(i)));
35 }
36 context.getContext().put("classifiedActions", actions);
37 }
38 
39 public AgentNext chooseNextAgent(final AgentContext context) {
40 return new AgentNext("OrchestrationAgent", null);
41 }
42}
main · UTF-8 · Java exos-cyoa · de.extio:lm-lib 2.11.3
A Java game engine by eXtIO.de · Apache 2.0

game-engine

A modular game engine framework for building complex 2D games in Java: lifecycle-managed modules, a 2D renderer, a typed event bus, storage, audio, localization and Steam integration — built on Spring Boot.

In practice — a real module from Exo's CYOA

DisclaimerModule is one of dozens of game-engine modules shipping in Exo's CYOA: it owns a draggable Window, builds its UI from pooled ControlRenderingBo objects, reacts to typed events on the event bus and switches the displayed language through the localization service. Condensed for brevity.

DisclaimerModule.java — exos-cyoa-gui
1// A real game-engine module shipping in Exo's CYOA: a disclaimer
2// window with localized texts and buttons, driven by the engine's
3// typed event bus and built from pooled rendering objects.
4@Component
5public class DisclaimerModule extends AbstractClientModule {
6 
7 public record ShowDisclaimerRequest(String reference) implements Event { }
8 
9 public record DisclaimerResponse(String reference, boolean agreed) implements Event { }
10 
11 @Autowired EventService eventService;
12 @Autowired LocalizationService localizationService;
13 @Autowired RenderingBoPool renderingBoPool; // + Window, StaticResourceService, ...
14 
15 private Window disclaimerWindow;
16 
17 public void onLoad() {
18 this.disclaimerWindow = this.applicationContext.getBean(Window.class);
19 this.disclaimerWindow.setNormalizedDimension(
20 ImmutableCoordI2.create(WINDOW_WIDTH, WINDOW_HEIGHT));
21 this.disclaimerWindow.setDraggable(true);
22 this.disclaimerWindow.setCloseButton(false);
23 this.getModuleService().changeActiveState(this.getId(), true);
24 }
25 
26 public void onActivate() {
27 this.eventService.register(ShowDisclaimerRequest.class, this.getId(),
28 this::onShowDisclaimerRequest); // typed record events
29 this.getModuleService().changeDisplayState(this.disclaimerWindow.getId(), true);
30 }
31 
32 public void onShow() {
33 this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent);
34 this.getModuleService().hideExcept(this.getId(), this.disclaimerWindow.getId());
35 }
36 
37 public void onHide() {
38 this.eventService.unregister(UiControlEvent.class, this.getId());
39 this.getModuleService().restoreVisibility();
40 }
41 
42 private void onShowDisclaimerRequest(final ShowDisclaimerRequest request) {
43 this.currentReference = request.reference();
44 this.buildGui();
45 this.getModuleService().changeDisplayState(this.getId(), true);
46 }
47 
48 private void onUiControlEvent(final UiControlEvent event) {
49 switch (event.getId()) {
50 case "Disclaimer_Button_LangEn" -> {
51 this.localizationService.setLanguage("en");
52 this.buildGui();
53 }
54 case "Disclaimer_Button_LangDe" -> {
55 this.localizationService.setLanguage("de");
56 this.buildGui();
57 }
58 case "Disclaimer_Button_Agree" -> this.onAgree(true);
59 default -> { }
60 }
61 }
62 
63 private void buildGui() {
64 this.disclaimerWindow.clearRenderingBos();
65 
66 final var titleBo = this.renderingBoPool
67 .acquire("Disclaimer_Label_Title", ControlRenderingBo.class)
68 .setCaption(this.localizationService.translate("ecyoa-565"))
69 .setFontSize(48)
70 .setType(LabelControl.class)
71 .withDimensionAbsolute(WINDOW_WIDTH - CONTENT_PADDING * 2, 65)
72 .withPositionRelative(CONTENT_PADDING, Window.MARGIN_TOP);
73 this.disclaimerWindow.putRenderingBo(titleBo);
74 
75 final var textBo = this.renderingBoPool
76 .acquire("Disclaimer_TextArea_Legal", ControlRenderingBo.class)
77 .setCaption(this.loadLegalText()) // legal text, per language
78 .setType(MultiLineTextAreaControl.class) // scrollable content area
79 .withDimensionAbsolute(contentWidth, viewportHeight);
80 this.disclaimerWindow.putRenderingBo(textBo);
81 
82 // language + agree/disagree buttons, then:
83 this.disclaimerWindow.draw();
84 }
85 
86 private void onAgree(final boolean agreed) {
87 this.gameSettingsClientService.setDisclaimerAgreed(agreed);
88 this.eventService.fire(new DisclaimerResponse(this.currentReference, agreed));
89 this.getModuleService().changeDisplayState(this.getId(), false);
90 }
91}
main · UTF-8 · Java exos-cyoa-gui · de.extio:game-engine 1.0.40