tiny-essentials
Version:
Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.
174 lines (126 loc) • 6.79 kB
Markdown
# 🛠️ TinyClassManager
Welcome to **TinyClassManager**! This is a lightweight, linear mixin manager designed to compose base classes with optional modules (like features or "DLCs") in a safe, predictable, and sequential way. It handles dependencies and prevents multiple class reuses automatically.
---
## 🚀 Key Features
* **🔒 Immutable Chain State:** Each `.insert()` call consumes the previous manager, preventing unwanted side effects or weird mutations.
* **🧩 Dependency Verification:** Automatically ensures that a required plugin is loaded before the dependent plugin is applied.
* **🛑 Conflict Protection:** Prevents you from installing the exact same plugin twice on the same class chain.
* **⚡ Clean Architecture:** Uses static class properties to manage metadata, keeping your plugin functions pure and direct.
---
## 📐 API Overview
### `TinyClassManager<T>`
#### Properties
* **`appliedPlugins`** (`string[]`): Returns an array of the names of all successfully installed plugins so far.
* **`size`** (`number`): The total count of applied plugins plus the core base class.
* **`currentClass`** (`T`): Accesses the current state of the class chain inside the manager.
* **`used`** (`boolean`): Indicates whether this specific manager instance has already been transitioned or finalized.
#### Methods
* **`constructor(coreClass)`**: Initializes the manager with your foundational core class.
* **`insert(applyFn)`**: Applies a new plugin to the chain by passing a function that returns the extended class. The returned class **must** define a `static _tinyDepName` string, and can optionally define a `static _tinyDeps` array of strings. Returns a *new* `TinyClassManager` instance.
* **`use(plugin)`**: *[Deprecated]* Legacy method for applying plugins using a definition object. Use `insert` instead.
* **`build()`**: Finalizes the composition chain, marks the instance as consumed, and returns the fully compiled class ready for instantiation.
---
## 📖 Detailed Usage Guide & Step-by-Step Example
Let's break down exactly how to design, extend, and instantiate a class structure using `TinyClassManager`. You can mirror this step-by-step blueprint for your own features!
### Step 1: Establish the Core Base 🧱
Every structure needs a solid foundation. The core class is the absolute minimum requirement and contains properties that every subsequent plugin might rely on.
```javascript
import TinyClassManager from '../TinyClassManager.mjs';
// The foundational class
class EntityCore {
/**
* @param {string} id
*/
constructor(id) {
/** @type {string} */
this.id = id;
}
}
```
### Step 2: Create an Independent Module 🏷️
Now, let's create our first module. A plugin is a function that receives the base class and returns an extended version of it. We use static properties (`_tinyDepName` and `_tinyDeps`) to tell the manager what this plugin is and what it needs. This module doesn't depend on anything else, so its dependencies array is empty.
```javascript
/**
* @param {typeof EntityCore} Base - The incoming base class to extend.
*/
const applyHealth = (Base) =>
class HealthModule extends Base {
static _tinyDepName = 'Health';
/** @type {string[]} */
static _tinyDeps = []; // Works fine straight out of the box with the Core
/**
* @param {string} id
*/
constructor(id) {
super(id); // Always remember to pass arguments down!
/** @type {number} */
this.hp = 100;
}
/**
* Deducts health from the entity.
* @param {number} amount - The amount of raw damage to deal.
*/
takeDamage(amount) {
this.hp -= amount;
console.log(`Took ${amount} damage. HP left: ${this.hp}`);
}
};
```
### Step 3: Create a Dependent Module 🛡️
Here is where the magic happens! The `Armor` module strictly requires the `Health` module to be loaded first because it relies on overriding the `takeDamage` logic. We enforce this relationship inside the `static _tinyDeps` array.
```javascript
/**
* @param {ReturnType<typeof applyHealth>} Base - The class containing Health features.
*/
const applyArmor = (Base) =>
class ArmorModule extends Base {
static _tinyDepName = 'Armor';
static _tinyDeps = ['Health']; // Strictly requires the "Health" module!
/**
* @param {string} id
*/
constructor(id) {
super(id);
/** @type {number} */
this.armor = 50;
}
/**
* Deducts health, factoring in damage mitigation provided by armor.
* @param {number} amount - The initial damage before armor reduction.
*/
takeDamage(amount) {
// Mitigates damage using the armor stat
const reducedDamage = Math.max(0, amount - this.armor * 0.1);
// Pass the mitigated damage up to the Health layer
super.takeDamage(reducedDamage);
}
};
```
### Step 4: Chain and Build Your Custom Class 🏗️
Finally, use the manager to safely piece your modules together. Remember that `TinyClassManager` uses an explosive state pattern: once you run `.insert()` or `.build()`, that specific instance is closed, forcing you to use the new returned chain instance.
```javascript
try {
// 1. Wrap your core class
const CoreTest = new TinyClassManager(EntityCore);
// 2. Chain your modules and compile!
// If you try to swap the order (.insert(applyArmor).insert(applyHealth)), it will throw an explicit dependency error.
const FullyArmoredEntity = CoreTest.insert(applyHealth).insert(applyArmor).build();
// 3. Create your custom instance!
const myEntity = new FullyArmoredEntity('Player_1');
// 4. Test out the behaviors
myEntity.takeDamage(30);
// Output: "Took 25 damage. HP left: 75" (30 damage - 5 armor reduction = 25 damage)
console.log(`Armor value: ${myEntity.armor}`); // Output: Armor value: 50
console.log(`Current HP: ${myEntity.hp}`); // Output: Current HP: 75
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
```
---
## 💡 How to Mimic This Pattern
When you want to build a new set of mechanics for your game or application using this architecture, always remember to follow this loop:
1. **Define your Base State**: Keep it clean and focused on global data configurations.
2. **Declare Plugins as Functions**: Create a function that accepts a `Base` class and returns an extended class. Equip it with `static _tinyDepName` (string) and `static _tinyDeps` (array of strings).
3. **Use `super` Inheritances**: Inside your plugin classes, always call `super(...)` in constructors and use `super.methodName(...)` if you are overriding an existing skill or function to allow all mixed behaviors to fire smoothly down the pipeline!