Skip to content

NiceVibrations Haptics — Design Spec

Date: 2026-07-09 · Status: approved scope: core tier, static in-code map

Goal

Wire the newly imported NiceVibrations module (Assets/ExternalAssets/Feel/NiceVibrations/, Lofelt 4.x, self-contained — no other Feel content required) into Shadow Raiders so that key player-facing beats fire mobile haptic feedback, gated by the existing settings toggle.

Components

1. Assets/Scripts/Presentation/Haptics.cs (new)

Static helper mirroring the Floaters / UiFeedback seam pattern:

public enum HapticEvent
{
    UiTap, UiConfirmArm,                                  // UI
    PlayerHit, LowHp, Death,                              // survival
    MeleeHit, Crit, Kill, BackstabKill, BossDown,         // combat
    ChestOpen, ChestLegendary, GateOpen,                  // interaction
    PortalCalled, PortalOpen, Extracted, KeyPickup,       // extraction
    LootCommon, LootRare,                                 // loot
    Dodge, HealDone,                                      // movement
    StormWarn, StormBreak,                                // world alerts
}

public static class Haptics
{
    public static void Play(HapticEvent e);
}
  • Play maps the event to a HapticPatterns.PresetType via one static switch/table — the single retuning point.
  • Gates: GameSettings.Vibration must be true (read live per call — no subscription needed); global minimum gap of 60 ms between fires (Time.unscaledTime) so simultaneous beats don't mush into one long buzz. Exception: UiTap/Selection presets never block a bigger event; ordering is fire-and-forget, first-come.
  • No Configure() needed — the class is stateless apart from the last-fire timestamp.
  • Calls HapticPatterns.PlayPreset(...) (fire-and-forget; safe no-op in editor without a gamepad).

2. Scene wiring

  • Add one HapticReceiver component (from Lofelt.NiceVibrations) to the persistent bootstrap GameObject in the boot scene (the object carrying MetaLoopController, or the UIRoot — whichever survives scene loads). It handles app-focus interruption. Leave its serialized hapticsEnabled/outputLevel at defaults; our gate is upstream in Haptics.Play.

3. Settings integration (already exists — no new work)

  • SaveService.Data.vibration (persisted), GameSettings.Vibration + SetVibration() (runtime seam), SettingsView/SettingsController (UI toggle) are all wired end-to-end today. Haptics.Play reading GameSettings.Vibration completes the loop. GameSettings.OnVibration hook stays unused for now.

Event → preset map (core tier)

HapticEvent Preset Call site
UiTap Selection UIButton.cs:91 beside UiFeedback.PlayClick (universal button funnel)
UiConfirmArm MediumImpact UIButton.cs:95 (ArmConfirm branch)
PlayerHit MediumImpact PlayerController.cs:621 OnDamaged, beside shake.playerHit
LowHp Warning same handler — edge-detect Health.Fraction crossing below 0.3, latch; reset when healed back above
Death Failure PlayerController.cs:747 OnDied
MeleeHit LightImpact PlayerController.cs:373 (melee strike lands)
Crit RigidImpact PlayerController.cs:394 (backstab crit, target survives)
BackstabKill HeavyImpact PlayerController.cs:386 (silent-kill beat, pairs with hitstop)
Kill RigidImpact EnemyController.cs:942 (player-attributed kill; LastAttacker == player guard)
BossDown Success EnemyController.cs:964 (floor guard slain)
ChestOpen MediumImpact Chest.cs:165 Open() — rarity already branched here
ChestLegendary HeavyImpact same branch, legendary case
GateOpen HeavyImpact Gate.cs:90 Open() (player-channel only; rival path bypasses)
PortalCalled LightImpact Portal.cs Call(), only when by != "rival"
PortalOpen MediumImpact Portal.cs:143 (board window opens)
Extracted Success Portal.cs:177 (extraction/descend completes)
KeyPickup Success KeyPickup.cs:85 (player acquires vault key)
LootCommon Selection LootService.cs:135 Collect switch — currency/blueprints
LootRare LightImpact / MediumImpact same switch — gear/consumables; epic+legendary → MediumImpact
Dodge LightImpact PlayerController.cs:594 (successful dodge only)
HealDone Success PlayerController.cs:268 (heal channel completes)
StormWarn Warning RaidController.cs:79 (storm2 "make for an exit")
StormBreak HeavyImpact RaidController.cs:80 beside shake.storm
Purchase Success InventoryController.cs:375 (level-up spend succeeds)
Equip LightImpact InventoryController.cs:321 (gear equipped)

(Purchase and Equip are also in the enum — the code snippet above omits them for brevity.) Win/lose result cards get no haptic of their own: Extracted fired seconds earlier in-raid and Death fired at OnDied — a card-time repeat would double-fire.

Explicitly excluded (this iteration)

  • Per-shot ranged-fire haptics (auto-fire cadence = constant buzz on mobile).
  • Optional alert tier: spotted, afflicted (burn/web), stamina empty, heal interrupted, "need a key" denial, dodge-fail.
  • Continuous/parametric haptics (PlayConstant ramp on extraction boarding) — phase 2.
  • Anything rival/AI-initiated — structurally unreachable from the chosen call sites.

Build / project settings

No changes required (verified): Android .aar self-declares VIBRATE permission and minSdk 17 (project minSdk 25, IL2CPP, ARM64); iOS LofeltHaptics.framework needs iOS 13+ (project targets 15.0); Input System 1.19 present → asmdef version-define compiles the editor/gamepad path; game code lives in the default assembly which auto-references Lofelt.NiceVibrations; the three JSON templates in Scripts/Components/Resources/ are present.

Testing

  • Editor: verify compile via Unity MCP console logs; play-mode smoke: trigger each beat, confirm no exceptions from HapticPatterns (editor playback is a no-op without a gamepad — correctness = no errors + gate logic).
  • Gate check: toggle vibration off in Settings → Haptics.Play early-outs (add a temporary debug assertion or log during verification only).
  • On-device (Android) verification is the user's playtest.