Compare commits
2 Commits
mimo/code/
...
feat/mnemo
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d2dbabd1 | |||
| efda34c8a9 |
3
app.js
3
app.js
@@ -5,6 +5,7 @@ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
|
||||
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
||||
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
||||
import { SessionRooms } from './nexus/components/session-rooms.js';
|
||||
import { TimelineScrubber } from './nexus/components/timeline-scrubber.js';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1.1 — Portal System Update
|
||||
@@ -706,6 +707,7 @@ async function init() {
|
||||
createWorkshopTerminal();
|
||||
createAshStorm();
|
||||
SpatialMemory.init(scene);
|
||||
TimelineScrubber.init(SpatialMemory);
|
||||
SessionRooms.init(scene, camera, null);
|
||||
updateLoad(90);
|
||||
|
||||
@@ -2831,6 +2833,7 @@ function gameLoop() {
|
||||
// Project Mnemosyne - Memory Orb Animation
|
||||
if (typeof animateMemoryOrbs === 'function') {
|
||||
SpatialMemory.update(delta);
|
||||
TimelineScrubber.update();
|
||||
animateMemoryOrbs(delta);
|
||||
}
|
||||
|
||||
|
||||
205
nexus/components/timeline-scrubber.js
Normal file
205
nexus/components/timeline-scrubber.js
Normal file
@@ -0,0 +1,205 @@
|
||||
// ═══════════════════════════════════════════
|
||||
// PROJECT MNEMOSYNE — TIMELINE SCRUBBER
|
||||
// ═══════════════════════════════════════════
|
||||
//
|
||||
// Horizontal timeline bar overlay for scrolling through fact history.
|
||||
// Crystals outside the visible time window fade out.
|
||||
//
|
||||
// Issue: #1169
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
const TimelineScrubber = (() => {
|
||||
let _container = null;
|
||||
let _bar = null;
|
||||
let _handle = null;
|
||||
let _labels = null;
|
||||
let _spatialMemory = null;
|
||||
let _rangeStart = 0; // 0-1 normalized
|
||||
let _rangeEnd = 1; // 0-1 normalized
|
||||
let _minTimestamp = null;
|
||||
let _maxTimestamp = null;
|
||||
let _active = false;
|
||||
|
||||
const PRESETS = {
|
||||
'hour': { label: 'Last Hour', ms: 3600000 },
|
||||
'day': { label: 'Last Day', ms: 86400000 },
|
||||
'week': { label: 'Last Week', ms: 604800000 },
|
||||
'all': { label: 'All Time', ms: Infinity }
|
||||
};
|
||||
|
||||
// ─── INIT ──────────────────────────────────────────
|
||||
function init(spatialMemory) {
|
||||
_spatialMemory = spatialMemory;
|
||||
_buildDOM();
|
||||
_computeTimeRange();
|
||||
console.info('[Mnemosyne] Timeline scrubber initialized');
|
||||
}
|
||||
|
||||
function _buildDOM() {
|
||||
_container = document.createElement('div');
|
||||
_container.id = 'mnemosyne-timeline';
|
||||
_container.style.cssText = `
|
||||
position: fixed; bottom: 0; left: 0; right: 0; height: 48px;
|
||||
background: rgba(5, 5, 16, 0.85); border-top: 1px solid #1a2a4a;
|
||||
z-index: 1000; display: flex; align-items: center; padding: 0 16px;
|
||||
font-family: monospace; font-size: 12px; color: #8899aa;
|
||||
backdrop-filter: blur(8px); transition: opacity 0.3s;
|
||||
`;
|
||||
|
||||
// Preset buttons
|
||||
const presetDiv = document.createElement('div');
|
||||
presetDiv.style.cssText = 'display: flex; gap: 8px; margin-right: 16px;';
|
||||
Object.entries(PRESETS).forEach(([key, preset]) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = preset.label;
|
||||
btn.style.cssText = `
|
||||
background: #0a0f28; border: 1px solid #1a2a4a; color: #4af0c0;
|
||||
padding: 4px 8px; cursor: pointer; font-family: monospace; font-size: 11px;
|
||||
border-radius: 3px; transition: background 0.2s;
|
||||
`;
|
||||
btn.onmouseenter = () => btn.style.background = '#1a2a4a';
|
||||
btn.onmouseleave = () => btn.style.background = '#0a0f28';
|
||||
btn.onclick = () => _applyPreset(key);
|
||||
presetDiv.appendChild(btn);
|
||||
});
|
||||
_container.appendChild(presetDiv);
|
||||
|
||||
// Timeline bar
|
||||
_bar = document.createElement('div');
|
||||
_bar.style.cssText = `
|
||||
flex: 1; height: 20px; background: #0a0f28; border: 1px solid #1a2a4a;
|
||||
border-radius: 3px; position: relative; cursor: pointer; margin: 0 8px;
|
||||
`;
|
||||
|
||||
// Handle (draggable range selector)
|
||||
_handle = document.createElement('div');
|
||||
_handle.style.cssText = `
|
||||
position: absolute; top: 0; left: 0%; width: 100%; height: 100%;
|
||||
background: rgba(74, 240, 192, 0.15); border-left: 2px solid #4af0c0;
|
||||
border-right: 2px solid #4af0c0; cursor: ew-resize;
|
||||
`;
|
||||
_bar.appendChild(_handle);
|
||||
_container.appendChild(_bar);
|
||||
|
||||
// Labels
|
||||
_labels = document.createElement('div');
|
||||
_labels.style.cssText = 'min-width: 200px; text-align: right; font-size: 11px;';
|
||||
_labels.textContent = 'All Time';
|
||||
_container.appendChild(_labels);
|
||||
|
||||
// Drag handling
|
||||
let dragging = null;
|
||||
_handle.addEventListener('mousedown', (e) => {
|
||||
dragging = { startX: e.clientX, startLeft: parseFloat(_handle.style.left) || 0, startWidth: parseFloat(_handle.style.width) || 100 };
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!dragging) return;
|
||||
const barRect = _bar.getBoundingClientRect();
|
||||
const dx = (e.clientX - dragging.startX) / barRect.width * 100;
|
||||
let newLeft = Math.max(0, Math.min(100 - dragging.startWidth, dragging.startLeft + dx));
|
||||
_handle.style.left = newLeft + '%';
|
||||
_rangeStart = newLeft / 100;
|
||||
_rangeEnd = (newLeft + dragging.startWidth) / 100;
|
||||
_applyFilter();
|
||||
});
|
||||
document.addEventListener('mouseup', () => { dragging = null; });
|
||||
|
||||
document.body.appendChild(_container);
|
||||
}
|
||||
|
||||
function _computeTimeRange() {
|
||||
if (!_spatialMemory) return;
|
||||
const memories = _spatialMemory.getAllMemories();
|
||||
if (memories.length === 0) return;
|
||||
|
||||
let min = Infinity, max = -Infinity;
|
||||
memories.forEach(m => {
|
||||
const t = new Date(m.timestamp || 0).getTime();
|
||||
if (t < min) min = t;
|
||||
if (t > max) max = t;
|
||||
});
|
||||
_minTimestamp = min;
|
||||
_maxTimestamp = max;
|
||||
}
|
||||
|
||||
function _applyPreset(key) {
|
||||
const preset = PRESETS[key];
|
||||
if (!preset) return;
|
||||
|
||||
if (preset.ms === Infinity) {
|
||||
_rangeStart = 0;
|
||||
_rangeEnd = 1;
|
||||
} else {
|
||||
const now = Date.now();
|
||||
const range = _maxTimestamp - _minTimestamp;
|
||||
if (range <= 0) return;
|
||||
const cutoff = now - preset.ms;
|
||||
_rangeStart = Math.max(0, (cutoff - _minTimestamp) / range);
|
||||
_rangeEnd = 1;
|
||||
}
|
||||
|
||||
_handle.style.left = (_rangeStart * 100) + '%';
|
||||
_handle.style.width = ((_rangeEnd - _rangeStart) * 100) + '%';
|
||||
_labels.textContent = preset.label;
|
||||
_applyFilter();
|
||||
}
|
||||
|
||||
function _applyFilter() {
|
||||
if (!_spatialMemory) return;
|
||||
const range = _maxTimestamp - _minTimestamp;
|
||||
if (range <= 0) return;
|
||||
|
||||
const startMs = _minTimestamp + range * _rangeStart;
|
||||
const endMs = _minTimestamp + range * _rangeEnd;
|
||||
|
||||
_spatialMemory.getCrystalMeshes().forEach(mesh => {
|
||||
const ts = new Date(mesh.userData.createdAt || 0).getTime();
|
||||
if (ts >= startMs && ts <= endMs) {
|
||||
mesh.visible = true;
|
||||
// Smooth restore
|
||||
if (mesh.material) mesh.material.opacity = mesh.userData._savedOpacity || mesh.material.opacity;
|
||||
} else {
|
||||
// Fade out
|
||||
if (mesh.material) {
|
||||
mesh.userData._savedOpacity = mesh.userData._savedOpacity || mesh.material.opacity;
|
||||
mesh.material.opacity = 0.02;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update label with date range
|
||||
const startStr = new Date(startMs).toLocaleDateString();
|
||||
const endStr = new Date(endMs).toLocaleDateString();
|
||||
_labels.textContent = startStr + ' — ' + endStr;
|
||||
}
|
||||
|
||||
function update() {
|
||||
_computeTimeRange();
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (_container) _container.style.display = 'flex';
|
||||
_active = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (_container) _container.style.display = 'none';
|
||||
_active = false;
|
||||
// Restore all crystals
|
||||
if (_spatialMemory) {
|
||||
_spatialMemory.getCrystalMeshes().forEach(mesh => {
|
||||
mesh.visible = true;
|
||||
if (mesh.material && mesh.userData._savedOpacity) {
|
||||
mesh.material.opacity = mesh.userData._savedOpacity;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isActive() { return _active; }
|
||||
|
||||
return { init, update, show, hide, isActive };
|
||||
})();
|
||||
|
||||
export { TimelineScrubber };
|
||||
@@ -1,112 +0,0 @@
|
||||
# Bannerlord Local Install Guide (macOS / Apple Silicon)
|
||||
|
||||
## Goal
|
||||
Run the GOG Mount & Blade II: Bannerlord build natively on Alexander's Mac (arm64, macOS Sequoia+).
|
||||
|
||||
## Prerequisites
|
||||
- macOS 14+ on Apple Silicon (arm64)
|
||||
- ~60 GB free disk space (game + Wine prefix)
|
||||
- GOG installer files in `~/Downloads/`:
|
||||
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
|
||||
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124)-1.bin` through `-13.bin`
|
||||
|
||||
## Step 1: Install Porting Kit
|
||||
|
||||
Porting Kit (free) wraps Wine/GPTK for macOS. It has a GUI but we automate what we can.
|
||||
|
||||
```bash
|
||||
brew install --cask porting-kit
|
||||
```
|
||||
|
||||
Launch it once to complete first-run setup:
|
||||
```bash
|
||||
open -a "Porting Kit"
|
||||
```
|
||||
|
||||
## Step 2: Create Wine Prefix + Install Game
|
||||
|
||||
**Option A: Via Porting Kit GUI (recommended)**
|
||||
|
||||
1. Open Porting Kit
|
||||
2. Click "Install Game" → "Custom Port" or search for Bannerlord
|
||||
3. Point it at: `~/Downloads/setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
|
||||
4. Follow the GOG installer wizard
|
||||
5. Install to default path inside the Wine prefix
|
||||
6. When done, note the prefix path (usually `~/Library/Application Support/PortingKit/...`)
|
||||
|
||||
**Option B: Manual Wine prefix (advanced)**
|
||||
|
||||
If you have Homebrew Wine (or GPTK) installed:
|
||||
|
||||
```bash
|
||||
# Create prefix
|
||||
export WINEPREFIX="$HOME/Games/Bannerlord"
|
||||
wine64 boot /init
|
||||
|
||||
# Run the GOG installer (it auto-chains the .bin files)
|
||||
cd ~/Downloads
|
||||
wine64 setup_mount__blade_ii_bannerlord_1.3.15.109797_\(64bit\)_\(89124\).exe
|
||||
```
|
||||
|
||||
Follow the GOG installer wizard. Default install path is fine.
|
||||
|
||||
## Step 3: Locate the Game Binary
|
||||
|
||||
After installation, the game executable is at:
|
||||
```
|
||||
$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
||||
```
|
||||
|
||||
Or inside Porting Kit's prefix at:
|
||||
```
|
||||
~/Library/Application Support/PortingKit/<prefix-name>/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
||||
```
|
||||
|
||||
## Step 4: First Launch
|
||||
|
||||
```bash
|
||||
# Find the actual path first, then:
|
||||
cd "$HOME/Games/Bannerlord/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client"
|
||||
wine64 Bannerlord.exe
|
||||
```
|
||||
|
||||
Or use the launcher script:
|
||||
```bash
|
||||
./portal/bannerlord/launch.sh
|
||||
```
|
||||
|
||||
## Step 5: Proof (Operator Checklist)
|
||||
|
||||
- [ ] Game window opens and is visible on screen
|
||||
- [ ] At least the main menu renders (TaleWorlds logo, "Campaign", "Custom Battle", etc.)
|
||||
- [ ] Screenshot taken: save to `portal/bannerlord/proof/`
|
||||
- [ ] Launch command recorded below for repeatability
|
||||
|
||||
**Launch command (fill in after install):**
|
||||
```
|
||||
# Repeatable launch:
|
||||
./portal/bannerlord/launch.sh
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Black screen on launch:**
|
||||
- Try: `wine64 Bannerlord.exe -force-d3d11` or `-force-vulkan`
|
||||
- Set Windows version: `winecfg` → set to Windows 10
|
||||
|
||||
**Missing DLLs:**
|
||||
- Install DirectX runtime: `winetricks d3dx9 d3dx10 d3dx11 vcrun2019`
|
||||
|
||||
**Performance:**
|
||||
- GPTK/Rosetta overhead is expected; 30-60 FPS is normal on M1/M2
|
||||
- Lower in-game graphics settings to "Medium" for first run
|
||||
|
||||
**Installer won't chain .bin files:**
|
||||
- Make sure all .bin files are in the same directory as the .exe
|
||||
- Verify with: `ls -la ~/Downloads/setup_mount__blade_ii_bannerlord_*`
|
||||
|
||||
## References
|
||||
- GamePortal Protocol: `GAMEPORTAL_PROTOCOL.md`
|
||||
- Portal config: `portals.json` (entry: "bannerlord")
|
||||
- GOG App ID: Mount & Blade II: Bannerlord
|
||||
- Steam App ID: 261550 (for Steam stats integration)
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bannerlord Launcher for macOS (Apple Silicon via Wine/GPTK)
|
||||
# Usage: ./portal/bannerlord/launch.sh [--wine-prefix PATH] [--exe PATH]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# Defaults — override with flags or environment
|
||||
WINEPREFIX="${WINEPREFIX:-$HOME/Games/Bannerlord}"
|
||||
BANNERLORD_EXE=""
|
||||
WINE_CMD=""
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--wine-prefix) WINEPREFIX="$2"; shift 2 ;;
|
||||
--exe) BANNERLORD_EXE="$2"; shift 2 ;;
|
||||
--help)
|
||||
echo "Usage: $0 [--wine-prefix PATH] [--exe PATH]"
|
||||
echo ""
|
||||
echo "Defaults:"
|
||||
echo " Wine prefix: $WINEPREFIX"
|
||||
echo " Auto-discovers Bannerlord.exe in the prefix"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Find wine command
|
||||
find_wine() {
|
||||
if command -v wine64 &>/dev/null; then
|
||||
echo "wine64"
|
||||
elif command -v wine &>/dev/null; then
|
||||
echo "wine"
|
||||
elif [ -f "/Applications/Whisky.app/Contents/Resources/WhiskyCmd" ]; then
|
||||
echo "/Applications/Whisky.app/Contents/Resources/WhiskyCmd"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
WINE_CMD="$(find_wine)"
|
||||
if [ -z "$WINE_CMD" ]; then
|
||||
echo "ERROR: No Wine runtime found."
|
||||
echo "Install one of:"
|
||||
echo " brew install --cask porting-kit"
|
||||
echo " brew install --cask crossover"
|
||||
echo " brew tap apple/apple && brew install game-porting-toolkit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Wine runtime: $WINE_CMD"
|
||||
echo "Wine prefix: $WINEPREFIX"
|
||||
|
||||
# Find Bannerlord.exe if not specified
|
||||
if [ -z "$BANNERLORD_EXE" ]; then
|
||||
# Search common GOG install paths
|
||||
SEARCH_PATHS=(
|
||||
"$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||
"$WINEPREFIX/drive_c/GOG Games/Mount Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||
"$WINEPREFIX/drive_c/Program Files/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||
)
|
||||
|
||||
# Also search PortingKit prefixes
|
||||
while IFS= read -r -d '' exe; do
|
||||
SEARCH_PATHS+=("$exe")
|
||||
done < <(find "$HOME/Library/Application Support/PortingKit" -name "Bannerlord.exe" -print0 2>/dev/null || true)
|
||||
|
||||
for path in "${SEARCH_PATHS[@]}"; do
|
||||
if [ -f "$path" ]; then
|
||||
BANNERLORD_EXE="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$BANNERLORD_EXE" ] || [ ! -f "$BANNERLORD_EXE" ]; then
|
||||
echo "ERROR: Bannerlord.exe not found."
|
||||
echo "Searched:"
|
||||
echo " $WINEPREFIX/drive_c/GOG Games/"
|
||||
echo " ~/Library/Application Support/PortingKit/"
|
||||
echo ""
|
||||
echo "Run the install first. See: portal/bannerlord/INSTALL.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Game binary: $BANNERLORD_EXE"
|
||||
echo "Launching..."
|
||||
echo ""
|
||||
|
||||
# Log the launch for proof
|
||||
LAUNCH_LOG="$SCRIPT_DIR/proof/launch_$(date +%Y%m%d_%H%M%S).log"
|
||||
mkdir -p "$SCRIPT_DIR/proof"
|
||||
{
|
||||
echo "=== Bannerlord Launch ==="
|
||||
echo "Date: $(date -Iseconds)"
|
||||
echo "Wine: $WINE_CMD"
|
||||
echo "Prefix: $WINEPREFIX"
|
||||
echo "Binary: $BANNERLORD_EXE"
|
||||
echo "User: $(whoami)"
|
||||
echo "macOS: $(sw_vers -productVersion)"
|
||||
echo "Arch: $(uname -m)"
|
||||
echo "========================="
|
||||
} > "$LAUNCH_LOG"
|
||||
echo "Launch log: $LAUNCH_LOG"
|
||||
echo ""
|
||||
|
||||
# Set the prefix and launch
|
||||
export WINEPREFIX
|
||||
EXE_DIR="$(dirname "$BANNERLORD_EXE")"
|
||||
cd "$EXE_DIR"
|
||||
exec "$WINE_CMD" "Bannerlord.exe" "$@"
|
||||
@@ -1,16 +0,0 @@
|
||||
# Bannerlord Proof
|
||||
|
||||
Screenshots and launch logs proving the game runs locally on the Mac.
|
||||
|
||||
## How to capture proof
|
||||
|
||||
1. Launch the game: `./portal/bannerlord/launch.sh`
|
||||
2. Wait for main menu to render
|
||||
3. Take screenshot: `screencapture -x portal/bannerlord/proof/main_menu_$(date +%Y%m%d).png`
|
||||
4. Save launch log (auto-generated by launch.sh)
|
||||
|
||||
## Expected proof files
|
||||
|
||||
- `main_menu_*.png` — screenshot of game main menu
|
||||
- `launch_*.log` — launch command + environment details
|
||||
- `ingame_*.png` — optional in-game screenshots
|
||||
13
portals.json
13
portals.json
@@ -23,21 +23,18 @@
|
||||
"rotation": { "y": 0.5 },
|
||||
"portal_type": "game-world",
|
||||
"world_category": "strategy-rpg",
|
||||
"environment": "local",
|
||||
"environment": "production",
|
||||
"access_mode": "operator",
|
||||
"readiness_state": "active",
|
||||
"telemetry_source": "local-desktop:bannerlord",
|
||||
"telemetry_source": "hermes-harness:bannerlord",
|
||||
"owner": "Timmy",
|
||||
"app_id": 261550,
|
||||
"window_title": "Mount & Blade II: Bannerlord",
|
||||
"install_source": "gog",
|
||||
"gog_version": "1.3.15.109797",
|
||||
"launcher_script": "portal/bannerlord/launch.sh",
|
||||
"install_guide": "portal/bannerlord/INSTALL.md",
|
||||
"destination": {
|
||||
"type": "local-launch",
|
||||
"url": "https://bannerlord.timmy.foundation",
|
||||
"type": "harness",
|
||||
"action_label": "Enter Calradia",
|
||||
"params": { "world": "calradia", "runtime": "wine/gptk" }
|
||||
"params": { "world": "calradia" }
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user