{"id":"hakochanjp/rain-particle","name":"rain-particle","scope":"hakochanjp","platform":"roblox","description":"A camera/character-follow rain particle module for Roblox","version":"0.3.0","latest":"0.3.0","versions":["0.1.0","0.2.0","0.3.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"9d54e85b8f1cbb38bd0a2ff309e45f2273c4f7bd4c1f43ac4d36c41db80bc998","likes":0,"downloads":0,"install":"forest install hakochanjp/rain-particle","url":"https://forest.dev/p/roblox/hakochanjp/rain-particle","files":"https://api.forest.dev/ai/package/roblox/hakochanjp/rain-particle/files","readme":"# RainParticle\n\nカメラまたは自キャラの周辺にパーティクルを降らせる Roblox 向け **クライアント専用** モジュール。\n\n> ⚠️ クライアント専用：`require` と `RainParticle.new` は `LocalScript` / `RunContext = Client` のスクリプトからのみ呼び出してください。サーバー側で `new` を呼ぶと error します。\n\n## 特徴\n\n- **カメラ / キャラ追従** — `FollowTarget` で基準点を切り替え可能（キャラがリスポーン中は自動でカメラにフォールバック）\n- **Template ベースの見た目** — Studio Explorer で作り込んだ `ParticleEmitter` を渡すだけでその見た目を再現\n- **Rain / Snow プリセット同梱** — `RainParticle.Preset.Rain(template)` / `RainParticle.Preset.Snow(template)` でいきなり使える\n- **複数インスタンス同時稼働** — クラス設計のため「豪雨＋霧＋桜吹雪」などのレイヤー演出が可能\n- **落下制御ロック** — `FallSpeed` / `FallDistance` から Lifetime を自動計算し、実際の落下距離が保証される\n- **軽量実装** — パーティクルごとに Part を作らず Attachment を使用\n- **Strict モード** — `--!strict`、selene / stylua で品質チェック済み\n\n## インストール\n\n利用側プロジェクトの `wally.toml`:\n\n```toml\n[dependencies]\nRainParticle = \"hakochanjp/rain-particle@0.2.0\"\n```\n\nそして `wally install` で `Packages/RainParticle` が生成される。\n\n## 使い方\n\n### 最小サンプル\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Packages = ReplicatedStorage:WaitForChild(\"Packages\")\n\nlocal RainParticle = require(Packages.RainParticle)\n\n-- Studio Explorer で作った ParticleEmitter を Template として用意\nlocal template = ReplicatedStorage:WaitForChild(\"MyRainTemplate\") :: ParticleEmitter\n\nlocal rain = RainParticle.new({\n    FollowTarget = RainParticle.Enum.FollowTarget.Character,\n    Template     = template,\n    Rate         = 100,\n    AreaSize     = Vector3.new(60, 1, 60),\n    Height       = 25,\n    ForwardOffset = 20,\n    FallSpeed    = 30,\n    FallDistance = 50,\n})\nrain:Start()\n```\n\n### 動作中の設定更新\n\n```lua\n-- Rate だけ変える（ループは止まらない）\nrain:Update({ Rate = 500 })\n\n-- 一時停止（破棄せず生成だけ止める）\nrain:Update({ Rate = 0 })\n\n-- 見た目だけ差し替え\nrain:Update({\n    EmitterProperties = {\n        Color         = ColorSequence.new(Color3.fromRGB(180, 220, 255)),\n        LightEmission = 0.3,\n    },\n})\n```\n\n### EmitterProperties のリセット\n\n差分マージ仕様のため、一度設定した値は同じキーを再指定するまで残ります。「未指定状態に戻す」場合は `RainParticle.None` sentinel を使ってください。\n\n```lua\n-- 全クリア（Template 由来の見た目に戻す）\nrain:Update({ EmitterProperties = RainParticle.None })\n\n-- 特定キーだけ取り消し\nrain:Update({ EmitterProperties = { LightEmission = RainParticle.None } })\n```\n\nなお `Update({ EmitterProperties = {} })` は「変更なし」として扱われます。\n\n### 粒単位オクルージョン（Occlusion）\n\n`Occlusion = true` を指定すると、生成点の直上 Raycast で屋内判定を行い、屋内なら該当粒をスポーンしません。さらに落下経路上の下向き Raycast で屋根・庇等の遮蔽面に当たった場合は `Lifetime` をその距離にクランプし、粒が遮蔽面でちょうど消えるようにします（既定 false・完全後方互換）。\n\n```lua\n-- 粒単位オクルージョン: 屋内にスポーンせず、屋根で粒が止まる\nlocal rain = RainParticle.new(RainParticle.Preset.Rain(nil, {\n    Occlusion = true,\n}))\nrain:Start()\n```\n\n`OcclusionParams: RaycastParams?` で Raycast のフィルタを上書きできます（省略時の既定: `Exclude` 空 / `IgnoreWater = true` / `RespectCanCollide = false`）。`RainParticle.None` を渡すと未指定状態に戻せます。\n\n> 💡 **近似・制約**：Lifetime クランプは鉛直落下前提のため、`SpreadAngle` 等で粒を斜めに飛ばすと消える位置は近似になります（既定テンプレートは真下落下）。透明パーツ（ガラス等）も遮蔽として扱われます。下向き Raycast はキャラクターにもヒットします（雨が人に当たって消える見た目）。コスト目安は 1 粒あたり最大 2 Raycast（Rate 600/s で毎フレーム約 20 本）。\n\n### プリセット（Rain / Snow）\n\n主要パターン用の工場関数を同梱しています。**Template は省略可能**で、省略するとリポジトリ同梱のテクスチャ（hakochanjp アカウントにアップロード済み）を使います。\n\n```lua\n-- もっとも短い書き方：同梱テクスチャでそのまま動く\nlocal rain = RainParticle.new(RainParticle.Preset.Rain())\nrain:Start()\n\n-- 同梱テクスチャ + override\nlocal snow = RainParticle.new(RainParticle.Preset.Snow(nil, {\n    Rate = 40,\n    FallSpeed = 8,\n}))\nsnow:Start()\n\n-- 自前テクスチャを使いたい場合\nlocal myTemplate = Instance.new(\"ParticleEmitter\")\nmyTemplate.Texture = \"rbxassetid://<your_image_id>\"\nlocal custom = RainParticle.new(RainParticle.Preset.Rain(myTemplate))\ncustom:Start()\n```\n\n`override` の `EmitterProperties` はプリセットの `EmitterProperties` と per-key マージされます（`RainParticle.None` で削除も可）。`override.Template` は無視され、常に第 1 引数の Template（または同梱）が採用されます。\n\n#### 同梱テクスチャ\n\n| プリセット | テクスチャ | サイズ | Image ID | 用途 |\n|---|---|---|---|---|\n| `Preset.Rain` | `assets/rain_streak.png` | 512 × 512 | `97568157179715` | square 中央に 1 本の雨粒 streak（**水平方向**に描画 → VelocityParallel で 90° 回転して縦の雨に）|\n| `Preset.Snow` | `assets/snow_dot.png` | 128 × 128 | `127825484280482` | radial フェードの柔らかい白丸 |\n\nImage ID は `RainParticle.Preset.RainTextureId` / `RainParticle.Preset.SnowTextureId` でも参照できます（自作 ParticleEmitter で同じテクスチャを使いたいときに便利）。\n\n> ## ⚠️ 本番運用時の注意：同梱テクスチャは「お試し用」です\n>\n> `Preset.Rain()` / `Preset.Snow()` の **引数なし形式が参照するテクスチャは hakochanjp 個人アカウントにホスト**されています。Roblox のモデレーション・アカウント凍結・将来的な削除などが起きると、**downstream の本番ゲームの雨/雪が黙って消えたり透明な四角になる**可能性があります。\n>\n> **本番プロジェクトでは必ず以下のいずれかにしてください**：\n>\n> 1. **推奨**: `assets/rain_streak.png` / `assets/snow_dot.png` を自分のアカウントに再アップロードし、`Preset.Rain(myTemplate)` の形で自前 Template を渡す\n> 2. または `Preset.RainTextureId` / `Preset.SnowTextureId` を参照しつつ、リスクを自分で受け入れる\n>\n> ```lua\n> -- 推奨パターン\n> local myTemplate = Instance.new(\"ParticleEmitter\")\n> myTemplate.Texture = \"rbxassetid://<自分でアップロードした image id>\"\n> local rain = RainParticle.new(RainParticle.Preset.Rain(myTemplate))\n> rain:Start()\n> ```\n\n> 💡 **Decal ID と Image ID の違い**：Asset Manager 経由でアップロードすると Decal（AssetTypeId=13）の ID が表示されますが、`ParticleEmitter.Texture` が要求するのは内側の **Image（AssetTypeId=1）の ID** です。Studio 上で Asset Manager から Template にドラッグ＆ドロップすると自動的に Image ID が入りますが、ID を手で貼り付ける場合は次のように内側 ID を抽出してください。\n>\n> ```lua\n> -- 編集モードのコマンドバーで実行\n> local model = game:GetService(\"InsertService\"):LoadAsset(<DECAL_ID>)\n> print(model:FindFirstChildOfClass(\"Decal\").Texture) -- → rbxassetid://<IMAGE_ID>\n> ```\n\n### 複数レイヤー演出\n\n```lua\nlocal heavyRain = RainParticle.new(RainParticle.Preset.Rain(rainTemplate))\nlocal snow      = RainParticle.new(RainParticle.Preset.Snow(snowTemplate))\nheavyRain:Start()\nsnow:Start()\n```\n\nすべてのインスタンスは `workspace.RainParticleContainers` 配下の子 Folder として整理されます。\n\n### 停止・破棄\n\n```lua\nrain:Stop()    -- スポーン停止（既存粒は自然消滅）。_accumulator もリセット\nrain:Destroy() -- 完全破棄（既存粒も即座削除）\n```\n\n> 💡 **一時停止と「停止」の使い分け**：\n> - **短時間で再開する**用途には `rain:Update({ Rate = 0 })` を使う。PreRender 接続は維持され、Rate を戻すだけで即時再開できる。\n> - **本当に止める**（しばらく使わない / リソースを解放したい）場合は `rain:Stop()` を使う。PreRender 接続が切断され、内部の `_accumulator` も 0 にリセットされる。次の `:Start()` までスポーン処理は一切走らない。\n> - 完全に捨てる場合は `rain:Destroy()`。インスタンスを再利用しないなら必ずこれを呼ぶ。\n\n### 動作中の Template 差し替え\n\n`Update({ Template = newTemplate })` で **動作中に Template を差し替え可能**です。差し替え後にスポーンされる粒から新しい Template が使われます（既に飛んでいる粒は寿命まで旧 Template の見た目のまま）。\n\n```lua\n-- 雨 → 桜吹雪に切り替え（既存の雨粒はそのまま落ち切る）\nrain:Update({ Template = sakuraTemplate })\n```\n\nなお Template は `RainParticle.None` での「未指定に戻す」操作はサポートしません（必ず ParticleEmitter インスタンスを渡してください）。\n\n## 公開API\n\n### インスタンス生成\n\n| 関数 | 説明 |\n|---|---|\n| `RainParticle.new(config: RainInitConfig): RainParticle` | 新しいインスタンスを生成。`Template` は型レベルで必須 |\n\n### インスタンスメソッド\n\n| メソッド | 説明 |\n|---|---|\n| `rain:Start(override: RainConfig?)` | 生成開始。override を渡すと内部で Update してから起動 |\n| `rain:Update(override: RainConfig)` | 設定を差分マージ（起動状態は変えない）。値に `RainParticle.None` を渡すと未指定状態に戻る |\n| `rain:Stop()` | スポーン停止（container 保持、既存粒は自然消滅、_accumulator リセット）|\n| `rain:Destroy()` | リソース完全破棄 |\n\n### Enum / sentinel\n\n- `RainParticle.Enum.FollowTarget.Camera` — カメラ基準\n- `RainParticle.Enum.FollowTarget.Character` — 自キャラ HumanoidRootPart 基準\n- `RainParticle.None` — `Update` / Preset override で「未指定状態に戻す」ことを表す sentinel（[EmitterProperties のリセット](#emitterproperties-のリセット)参照）\n\n### プリセット工場関数\n\n| 関数 | シグネチャ | 戻り値 |\n|---|---|---|\n| `RainParticle.Preset.Rain` | `(template: ParticleEmitter?, override: RainConfig?) -> RainInitConfig` | マルチドロップ雨用の `RainInitConfig` |\n| `RainParticle.Preset.Snow` | `(template: ParticleEmitter?, override: RainConfig?) -> RainInitConfig` | ふんわり雪用の `RainInitConfig` |\n| `RainParticle.Preset.RainTextureId` | `number` 定数 | 同梱 rain_streak.png の Image asset ID |\n| `RainParticle.Preset.SnowTextureId` | `number` 定数 | 同梱 snow_dot.png の Image asset ID |\n\n**第 1 引数 `template`（任意）**: ベースとなる `ParticleEmitter`。**省略または `nil` を渡すと同梱テクスチャを Texture にした ParticleEmitter を自動生成して使う**。\n\n**第 2 引数 `override`（任意）**: プリセットのデフォルト値を per-key で上書きする差分。\n- top-level（`Rate` / `Height` 等）は `nil` 安全に上書き（`Rate = 0` も尊重される）\n- `EmitterProperties` は base と incoming を per-key マージ\n- `EmitterProperties = RainParticle.None` で base の emitter props を完全クリア\n- `EmitterProperties = { Color = RainParticle.None }` で個別キーだけ削除\n- `override.Template` は無視（常に第 1 引数の template が採用される）\n\n**デフォルト値**\n\n| プロパティ | `Preset.Rain` | `Preset.Snow` |\n|---|---|---|\n| `FollowTarget` | `\"Camera\"` | `\"Camera\"` |\n| `Rate` | `400` | `80` |\n| `AreaSize` | `Vector3.new(80, 1, 80)` | `Vector3.new(100, 1, 100)` |\n| `Height` | `35` | `50` |\n| `ForwardOffset` | `15` | `20` |\n| `FallSpeed` | `120` | `12` |\n| `FallDistance` | `80` | `80` |\n| `EmitterProperties.Color` | `ColorSequence.new(Color3.fromRGB(220, 235, 255))` | `ColorSequence.new(Color3.fromRGB(255, 255, 255))` |\n| `EmitterProperties.Size` | `NumberSequence.new(2)` | `NumberSequence.new(0.5)` |\n| `EmitterProperties.Transparency` | `NumberSequence.new(0.2)` | `NumberSequence.new(0.2)` |\n| `EmitterProperties.LightEmission` | `0.1` | `0.2` |\n| `EmitterProperties.LightInfluence` | `0.5` | `0.3` |\n| `EmitterProperties.Rotation` | — | `NumberRange.new(0, 360)` |\n| `EmitterProperties.RotSpeed` | — | `NumberRange.new(-30, 30)` |\n| `EmitterProperties.SpreadAngle` | — | `Vector2.new(15, 15)` |\n| `EmitterProperties.Orientation` | `Enum.ParticleOrientation.VelocityParallel` | `Enum.ParticleOrientation.FacingCamera` |\n\n> 💡 `Preset.Rain` の `VelocityParallel` は **テクスチャの U 軸を velocity 方向に揃える** 仕様です。`assets/rain_streak.png` は streak を **水平方向に描画している** ため、落下時に 90° 回転して画面上は縦の雨として描画されます。キャラ移動時に少し傾く motion-aligned な見え方が得られます（縦長 streak テクスチャを使う場合は `FacingCamera` を選んでください）。\n\n> 💡 Preset 専用のモジュールが `RainParticle.Preset`（= `src/Preset.luau`）に分離されているため、`local Preset = require(RainParticle.Preset)` のように直接 require して `Preset.Rain(...)` / `Preset.Snow(...)` を呼ぶこともできます。\n\n### 型エクスポート\n\n| 型 | 用途 |\n|---|---|\n| `RainParticle.RainInitConfig` | `new()` 用。`Template` 必須 |\n| `RainParticle.RainConfig` | `Update` / `Start` 用。全項目任意 |\n| `RainParticle.RainParticle` | インスタンス型 |\n| `RainParticle.FollowTarget` | `\"Camera\" \\| \"Character\"` |\n\n## RainConfig プロパティ一覧\n\n| プロパティ | 型 | 説明 |\n|---|---|---|\n| `FollowTarget` | `\"Camera\" \\| \"Character\"` | 追従対象（既定: `Camera`）。それ以外の値は error |\n| `Rate` | `number` | 毎秒の生成数（既定: 300）。**0 で一時停止**、負数は error |\n| `AreaSize` | `Vector3` | 水平生成範囲（X/Z のみ使用、Y は無視）|\n| `Height` | `number` | 基準点からの上方オフセット（負値で下方）|\n| `ForwardOffset` | `number` | 基準点からの前方オフセット（負値で後方）|\n| `FallSpeed` | `number` | 落下速度（スタッド/秒）。正の数 |\n| `FallDistance` | `number` | 落下距離（スタッド）。正の数。Lifetime を自動計算 |\n| `EmitterProperties` | `{[string]: any}?` | ParticleEmitter の任意プロパティ辞書上書き。`RainParticle.None` でリセット |\n| `Occlusion` | `boolean?` | 粒単位オクルージョン（既定 false）。屋内ならスポーンせず、落下経路上の遮蔽面で `Lifetime` をクランプして粒を消す |\n| `OcclusionParams` | `RaycastParams?` | オクルージョン Raycast のフィルタ上書き。既定: `Exclude` 空 / `IgnoreWater=true` / `RespectCanCollide=false`。`RainParticle.None` でリセット |\n| `Template` | `ParticleEmitter` | **`new()` で必須** ベース ParticleEmitter |\n\n## 自動上書き（ロック）されるプロパティ\n\nRainParticle の落下制御に必須のため、以下は Template / EmitterProperties で指定しても無視されます（警告が出ます）。\n\n| プロパティ | 固定値 | 理由 |\n|---|---|---|\n| `Rate` | `0` | 単発 `Emit(1)` で生成するため |\n| `Lifetime` | `FallDistance / FallSpeed` | 落下挙動と一致させる |\n| `Speed` | `FallSpeed` | 放出速度 = 落下速度 |\n| `LockedToPart` | `false` | 自然落下 |\n| `VelocityInheritance` | `0` | 基準点の移動を継承しない |\n| `Drag` | `0` | FallDistance 精度維持 |\n| `Acceleration` | `Vector3.zero` | 等速落下（`workspace.Gravity` の影響なし）|\n| `WindAffectsDrag` | `false` | GlobalWind の影響なし |\n| `EmissionDirection` | `Bottom` | 下向き放出（RainParticle 仕様）|\n| `Enabled` | `true` | `Emit(1)` のため。停止は `Stop()` または `Rate = 0` を使う |\n\n挙動を変えたい場合は `FallSpeed` / `FallDistance` で調整してください。\n\n## 開発\n\n```bash\nrokit install    # ツールチェーン（rojo / wally / stylua / selene）を導入\nwally install    # 依存パッケージを取得\nrojo serve dev.project.json   # Studio と同期\n```\n\n### ディレクトリ構成\n\n```\nRobloxRainParticle/\n├── src/                 # ライブラリ本体（wally で配布）\n│   └── init.luau\n├── tests/               # 動作確認スクリプト（wally 配布対象外）\n│   └── RainParticleTest/\n├── assets/              # 推奨テクスチャ PNG（wally 配布対象外、要手動アップロード）\n│   ├── rain_streak.png  # Preset.Rain 用\n│   └── snow_dot.png     # Preset.Snow 用\n├── default.project.json # リリース用 Rojo 設定（src のみ）\n├── dev.project.json     # 開発用 Rojo 設定（src + tests + Packages）\n├── wally.toml\n├── rokit.toml\n├── stylua.toml\n├── selene.toml\n├── LICENSE\n└── README.md\n```\n\n## ライセンス\n\nMIT License © 2026 hakochanjp\n","readmeTruncated":false}