This project uses HarmonyOS Flutter to build a text-based Loot-Shoot-Extract game. Its core idea is to isolate in-raid loot from out-of-raid permanent assets through pure in-memory snapshots, solving state consistency and failure-penalty problems without relying on a backend. Keywords: Flutter, in-memory snapshots, state isolation.
Technical specification snapshot
| Parameter | Description |
|---|---|
| Programming language | Dart |
| Runtime framework | Flutter |
| Target platform | HarmonyOS / OpenHarmony adaptation scenarios |
| Architecture pattern | Single-file prototype + StatefulWidget-driven state |
| State protocol | Dual-state switching between Raid and Hideout |
| Data storage | Pure memory, no backend database |
| Core dependencies | Flutter SDK, dart:math |
| Reference engagement | The original article shows 7 likes and 8 saves |
This project proves that pure memory can still build a trustworthy Loot-Shoot-Extract state system
The project compresses the traditional Loot-Shoot-Extract loop into a “micro immune battlefield.” After entering a raid, the player collects samples, encounters combat, and either extracts successfully or dies. The entire process depends not on a database, but on lifecycle management of in-memory objects.
The real challenge is not the UI, but asset boundaries. In-raid materials must be losable, out-of-raid storage must persist, and the secure container must survive across scopes for the long term. This requires the state model to support isolation, mergeability, and explainability at the same time.
The example screenshots show dual-state interaction between raid and hideout
AI Visual Insight: This image shows a text-based main screen or storage interface. It emphasizes list-based information layout, resource slots, and lightweight interaction entry points, indicating that the project intentionally minimizes heavy graphics rendering and instead highlights state flow and inventory decisions.
AI Visual Insight: This image reflects an in-raid exploration or battle report interface. It may include an event log, HP values, and turn-based feedback, emphasizing high-frequency updates driven by text and making it well suited to direct rendering-tree control through simple state variables.
AI Visual Insight: This image shows loot or container interaction results. It illustrates that once resources are acquired, the system must immediately decide where they go: into the temporary tactical backpack or into the permanent cross-raid secure container. That choice is the interaction core of the risk-reward system.
The inventory model must complete domain abstraction first
The project begins by defining Item and Inventory. Here, Item is not just a plain data structure. It is a domain object with a unique id, value, size, and color-like attributes, so items can still be identified reliably when moving across containers.
class Item {
final String id;
final String name;
final int value; // Sale value
final int size; // Space consumed
Item(this.name, this.value, this.size)
: id = DateTime.now().microsecondsSinceEpoch.toString();
@override
bool operator ==(Object other) =>
other is Item && other.id == id; // Use a unique ID to preserve consistency across containers
@override
int get hashCode => id.hashCode;
}
This code establishes comparable and transferable item entities, providing the foundation for later snapshot isolation and incremental merging.
Inventory capacity validation is the mathematical foundation of in-raid pickup decisions
The tension in Loot-Shoot-Extract gameplay fundamentally comes from capacity constraints. Without a size system, players could pick up items indefinitely and the risk model would collapse. That is why Inventory must explicitly maintain both maxSize and currentSize.
The capacity formula can be summarized as follows: the current occupied space plus the target item size must not exceed the inventory limit. This constraint is both an algorithmic boundary and an interaction boundary. Any “pickup failed” or “inventory full” message should originate here, not from scattered UI checks.
class Inventory {
final int maxSize;
final List
<Item> items = [];
Inventory(this.maxSize);
int get currentSize =>
items.fold(0, (sum, item) => sum + item.size); // Calculate used capacity
bool canAdd(Item item) =>
currentSize + item.size <= maxSize; // Pre-check capacity
bool add(Item item) {
if (!canAdd(item)) return false; // Reject immediately if over capacity
items.add(item);
return true;
}
void clear() => items.clear();
}
This code converges all capacity validation into a single guard function, ensuring that every pickup action follows the same rule set.
In-raid and out-of-raid state isolation depends on three containers, not three pages
The most valuable part of this design is that it implements state partitioning as container partitioning. _stash is the permanent out-of-raid storage, _tacticalRig is the temporary in-raid snapshot, and _safeBox is the shared cross-raid container. Because each has a clear responsibility, failure penalties can map directly to object-handling strategies.
The secure container is the most critical part. It is not a copy, but a global reference shared across raid and hideout. In other words, when the player dies, only the temporary snapshot is destroyed. Items in the secure container are not cleared along with in-raid objects.
bool _inRaid = false;
final Inventory _stash = Inventory(100); // Permanent out-of-raid storage
final Inventory _safeBox = Inventory(4); // Shared cross-raid container
Inventory _tacticalRig = Inventory(20); // Temporary in-raid snapshot
void _extractSuccess() {
_inRaid = false;
for (final item in _tacticalRig.items) {
_stash.add(item); // Merge into permanent storage after successful extraction
}
_tacticalRig.clear(); // Destroy the in-raid snapshot
}
void _miaDead() {
_inRaid = false;
_tacticalRig.clear(); // On death, only clear temporary loot
}
This code turns the three rules—“extract to keep,” “die to lose,” and “secure container is protected”—into directly executable memory semantics.
A boolean state machine is enough to support high-frequency rendering switches between raid and hideout
The project does not use a complex navigation stack. Instead, it uses _inRaid to control the flow of the main render tree. For a text-driven interface with frequent event updates, this approach is lighter, keeps the state source singular, and is easier to maintain.
When _inRaid == true, the UI renders in-raid battle reports and events. Otherwise, it renders the out-of-raid preparation and storage view. Entering a raid also resets HP, steps, and the temporary inventory. In essence, this is a lightweight state-machine initialization process.
The auto-combat system shifts interaction pressure to loot decisions
Combat does not aim for mechanical complexity. Instead, it uses automatic resolution. The benefit is direct: it shifts player attention away from skill execution and toward risk management. In a Loot-Shoot-Extract structure, what matters most is whether the player can bring the rewards home afterward.
void handleCombatEvent() {
int enemyHp = 30;
int enemyAtk = 12;
int enemyDef = 2;
while (_currentHp > 0 && enemyHp > 0) {
enemyHp -= (_atk - enemyDef).clamp(1, 999); // Player damage is at least 1
if (enemyHp <= 0) break;
_currentHp -= (enemyAtk - _def).clamp(1, 999); // Enemy damage is at least 1
}
if (_currentHp <= 0) {
_miaDead(); // If HP reaches zero, the in-raid snapshot becomes invalid
} else {
// After victory, proceed to drop and storage decisions
}
}
This code closes the combat loop through the shortest possible path, keeping the system focused on post-battle drops and container allocation.
This architecture works well for prototype validation and instructional project breakdowns
From an engineering perspective, this is not a complete commercial solution, but it is highly effective for validating state isolation, object references, and in-raid versus out-of-raid lifecycle management. During HarmonyOS adaptation exploration in particular, this type of pure in-memory prototype can quickly prove the feasibility of Flutter cross-platform rendering and interaction logic.
If the project evolves further, it could add local persistence, serialized snapshots, equipment durability, randomized map events, and more fine-grained state management solutions such as Riverpod or Bloc. But the core principle remains unchanged: in-raid data is a snapshot, out-of-raid data is the source of truth for permanent assets, and cross-domain containers must have independent semantics.
FAQ
1. Why doesn’t this project directly use a database to store in-raid state?
Because the goal is to validate state isolation under pure in-memory conditions. A database can solve persistence, but it cannot directly demonstrate the core design idea that object lifecycle determines asset fate.
2. Why must the secure container use a global singleton or shared reference?
Because the value of the secure container lies in cross-raid persistence. If the system copied it every time a new raid started, items inside it could no longer be guaranteed to survive independently after death. Only a shared reference matches the intended gameplay semantics.
3. Is this approach suitable for large game projects?
Not as a direct solution for complex commercial games, but it is well suited for gameplay prototypes, instructional demos, and state management experiments. To scale toward larger projects, you would need persistence, an event bus, save recovery, and stricter data consistency controls.
[AI Readability Summary]
This article reconstructs and analyzes a micro-scale Loot-Shoot-Extract project built with HarmonyOS Flutter. It focuses on in-raid versus out-of-raid state isolation in a pure in-memory architecture, inventory capacity validation, and auto-combat resolution. It shows how snapshot containers, a shared secure container, and a lightweight state machine can implement high-risk asset management without a backend.