# BTRW 職業作成ガイドライン
このフォルダ(`btrw/classes`)には、JavaScript で記述された職業スクリプトを配置します。
## 📚 技術的な実装方法
API リファレンスやイベントハンドラの詳細については **[ClassEventReference.md](../docs/ClassEventReference.md)** を参照してください。
## 🎯 職業設計の基本方針
### ❌ 避けるべき設計
#### 1. 戦闘特化職(数値調整だけの職業)
- **NG例**: 攻撃力+20%、防御力+30%、移動速度+15% など
- **理由**: 数値を弄るだけの職業は戦闘の優劣が明確すぎて駆け引きが生まれない
- **代替案**: 特定の条件下で発動する能力や、戦術的な選択肢を増やす能力にする
#### 2. エクゾディア系(勝ち確・負け確が極端)
- **NG例**: 発動できたら即勝利、発動できなければ無力
- **理由**: 運ゲーになりゲーム性が損なわれる
- **代替案**: 強力だが発動条件が明確で対策可能な能力にする
#### 3. 過度なストレス要因
- **慎重に扱うべき要素**:
- 行動阻害(移動不可、攻撃不可など) - 強力なデバフ(盲目、鈍化など) - 一方的な妨害(アイテム破壊、経験値剥奪など)
- **対策**: クールダウンを長くする、発動条件を厳しくする、カウンタープレイを用意する
#### 4. ログ通知の欠如
- **問題**: 他人に影響を与える能力で通知がないと、相手は何が起きたか理解できない
- **結果**: 理不尽感、ストレス、対策を学ぶ機会の喪失
- **必須**: 他人に影響を与える能力には**必ずログ通知**を実装する
#### 5. チーム戦への配慮不足
- **NG例**: 個人戦でしか機能しない能力(例: 全員を敵とみなす)
- **必須**: `btrw.isSameTeam(a, b)` や `btrw.teamId(player)` でチーム判定を行う
```javascript
onEntityDamageByEntity(event, attackerCtx, targetCtx) {
// 味方も攻撃してしまう this._applyEffect(targetCtx.player());
}
onEntityDamageByEntity(event, attackerCtx, targetCtx) {
const attacker = attackerCtx.player(); const target = targetCtx.player(); // 味方には効果を適用しない if (btrw.isSameTeam(attacker, target)) return; this._applyEffect(target);
}
```
### 4. ログ通知の実装(他人に影響を与える能力)
他人に影響を与える能力は、**必ず被影響者にログ通知**を出してください。
#### 通知が必須の効果
| 効果の種類 | 必要性 | 例 |
| ----------- | ------- | ----- |
| **移動阻害** | 🔴 必須 | 引き寄せ、ノックバック、移動不可、スワップ |
| **状態異常** | 🔴 必須 | 盲目、鈍化、毒、衰弱 |
| **アイテム操作** | 🔴 必須 | インベントリ変更、装備破壊、アイテム奪取 |
| **位置変更** | 🔴 必須 | テレポート、座標変更 |
| **視界妨害** | 🟡 推奨 | パーティクル大量生成、ブロック設置 |
| **バフ/デバフ** | 🟡 推奨 | エフェクトアイコンが出るが通知があると親切 |
| **ダメージ** | 🟢 任意 | 攻撃はダメージログで明確なため省略可 |
#### 通知の実装例
```javascript
onEntityDamageByEntity(event, attackerCtx, targetCtx) {
const attacker = attackerCtx.player();
const target = targetCtx.player();
// 味方は除外
if (btrw.isSameTeam(attacker, target)) return;
// 引き寄せ効果
this._pullTowards(target, attacker);
// 通知(必須)
const ChatColor = Java.type('org.bukkit.ChatColor');
btrw.actionBar(target,
ChatColor.DARK_PURPLE + '⚠ グラビティウェル ' +
ChatColor.YELLOW + '(' + attacker.getName() + ')');
}
```
#### 通知文のベストプラクティス
```javascript
target.sendMessage('引き寄せられた!');
target.sendMessage('[グラビティウェル] 引き寄せられた!');
target.sendMessage(ChatColor.DARK_PURPLE + '[グラビティウェル] ' +
ChatColor.GRAY + attacker.getName() + ' に引き寄せられた!');
btrw.actionBar(target,
ChatColor.DARK_PURPLE + '⚠ グラビティウェル ' +
ChatColor.YELLOW + '(' + attacker.getName() + ')');
```
通知を出すべき理由:**
- ✅ 相手が何の効果を受けたか理解できる
- ✅ 次回からの対策が立てられる
- ✅ 理不尽感を減らし、納得感を高める
- ✅ 職業の存在をアピールできる(認知度向上)
## ✅ 推奨される設計
### 1. 多様なプレイスタイルに対応
戦闘が苦手なプレイヤーでも活躍できる職業を含めることで、ゲーム全体のバランスが良くなります。
#### 非戦闘系の役割例
| 役割 | 説明 | 実装例 |
| ------ | ------ | -------- |
| **偵察** | 敵の位置や情報を味方に伝える | コンパス追跡、透視、マップ共有 |
| **搬送** | アイテムや味方を素早く移動させる | テレポート、高速移動、エンダーチェスト |
| **撹乱** | 敵の戦術を混乱させる | 罠設置、フェイク情報、変装 |
| **蘇生** | 死亡した味方を復活させる | リスポーン地点設定、復活儀式 |
| **支援** | 味方を強化・回復する | バフ付与、回復アイテム生成 |
| **建築** | 防衛拠点や移動経路を構築 | 資材配給、高速建築 |
#### 実装例:偵察職
```javascript
onItemUse(event, ctx) {
if (!btrw.isRightClick(event, 'scout_compass')) return;
const player = ctx.player();
const nearestEnemy = this._findNearestEnemy(player);
if (nearestEnemy) {
player.setCompassTarget(nearestEnemy.getLocation());
btrw.actionBar(player, '§e最も近い敵: ' + nearestEnemy.getName());
} else {
btrw.actionBar(player, '§7敵が見つかりません');
}
}
_findNearestEnemy(player) {
const Bukkit = Java.type('org.bukkit.Bukkit');
const players = Bukkit.getOnlinePlayers().toArray();
let nearest = null;
let minDist = Infinity;
for (let i = 0; i < players.length; i++) {
const other = players[i];
// 自分、味方、死亡者は除外
if (other.equals(player)) continue;
if (btrw.isSameTeam(player, other)) continue;
if (!btrw.isAlive(other)) continue;
const dist = player.getLocation().distance(other.getLocation());
if (dist < minDist) {
minDist = dist;
nearest = other;
}
}
return nearest;
}
```
### 2. ジョーカー要素(一発逆転の可能性)
劣勢でも希望を持てる能力は、ゲームを最後まで楽しくします。ただし、**相手にストレスを与えないこと**が重要です。
#### ジョーカー系の設計指針
- **タイミング**: 戦闘中ではなく、戦闘前や逃走時に発動する能力
- **相手への配慮**: 直接戦闘を無効化するのではなく、戦術的選択肢を増やす
- **演出**: 発動時に派手なエフェクトで盛り上げる
- **リスク**: 失敗時のデメリットや使用回数制限
#### 実装例:ジョーカー職(ラストスタンド)
```javascript
onDeath(ctx) {
const player = ctx.player();
const loc = player.getLocation();
const world = player.getWorld();
// 1. 派手な爆発演出(ダメージなし)
world.createExplosion(loc, 4.0, false, false);
const Sound = Java.type('org.bukkit.Sound');
world.playSound(loc, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 2.0, 0.8);
// 2. 周囲10ブロック以内の死亡した味方を1人蘇生
const Bukkit = Java.type('org.bukkit.Bukkit');
const players = Bukkit.getOnlinePlayers().toArray();
const deadTeammates = [];
for (let i = 0; i < players.length; i++) {
const other = players[i];
// 自分、生存者、他チームは除外
if (other.equals(player)) continue;
if (!btrw.isSameTeam(player, other)) continue;
if (btrw.isAlive(other)) continue;
deadTeammates.push(other);
}
if (deadTeammates.length > 0) {
// ランダムに1人選んで蘇生
const target = deadTeammates[Math.floor(Math.random() * deadTeammates.length)];
const resurrected = btrw.resurrect(target, loc, 3000); // 3秒無敵
if (resurrected) {
const ChatColor = Java.type('org.bukkit.ChatColor');
const message = ChatColor.GOLD + '[ラストスタンド] ' +
ChatColor.AQUA + target.getName() +
ChatColor.GRAY + ' が ' +
ChatColor.YELLOW + player.getName() +
ChatColor.GRAY + ' の犠牲により蘇生された';
// チーム全員に通知
for (let i = 0; i < players.length; i++) {
const p = players[i];
if (btrw.isSameTeam(player, p)) {
p.sendMessage(message);
}
}
}
}
}
```
このデザインが良い理由:**
- ✅ 死亡後の発動なので、戦闘中の相手にストレスを与えない
- ✅ チームプレイを重視(味方の蘇生)
- ✅ 派手な演出で盛り上がる
- ✅ HeavensFeelとの差別化(確実に1人蘇生、条件なし)
### 3. 駆け引きが生まれる能力
単純な数値強化ではなく、**読み合い**や**タイミング**が重要な能力が面白さを生みます。
#### 駆け引き要素の例
- **カウンター系**: 攻撃を受けた瞬間に反撃
- **フェイク系**: 偽情報やダミーで相手を誤認させる
- **タイミング系**: 特定のタイミングでのみ強力(満月、昼夜、ボス出現時など)
- **リスク&リターン**: 強力だが失敗時のデメリットがある
```javascript
onEntityDamage(event, ctx) {
const player = ctx.player();
// 2秒間のカウンターチャンスを付与
if (!this._counterWindow) this._counterWindow = {};
this._counterWindow[player.getUniqueId().toString()] = Date.now();
btrw.actionBar(player, '§eカウンターチャンス!(2秒)');
}
onEntityDamageByEntity(event, attackerCtx, targetCtx) {
const attacker = attackerCtx.player();
const uuid = attacker.getUniqueId().toString();
// カウンター窓が開いているか確認
if (!this._counterWindow || !this._counterWindow[uuid]) return;
const elapsed = Date.now() - this._counterWindow[uuid];
if (elapsed > 2000) {
delete this._counterWindow[uuid];
return;
}
// カウンター成功:ダメージ2倍
event.setDamage(event.getDamage() * 2.0);
delete this._counterWindow[uuid];
btrw.actionBar(attacker, '§6§lカウンター成功!');
}
```
## 🔍 チェックリスト
新しい職業を作成する際は、以下を確認してください:
- [ ] チーム戦でも正しく動作するか(`btrw.isSameTeam()` の使用)
- [ ] 単なる数値強化ではなく、戦術的な選択肢があるか
- [ ] 発動すれば勝ち確定、という極端な能力ではないか
- [ ] 相手にカウンタープレイの余地があるか
- [ ] クールダウンや使用制限は適切か
- [ ] 他プレイヤーに過度なストレスを与えないか
- [ ] **他人に影響を与える能力にログ通知を実装しているか** 🔴
- [ ] `onGameStart` / `onGameEnd` / `onDeselect` で状態をクリーンアップしているか
- [ ] グローバル変数(`this._xxx`)の初期化を忘れていないか
## 📖 参考資料
- **API リファレンス**: [ClassEventReference.md](../docs/ClassEventReference.md)
- **実装テンプレート**: [職業実装テンプレート.md](../職業実装テンプレート.md)
- **既存職業**: このフォルダ内の `.js` ファイルを参考にしてください
## 💡 迷ったら
1. **既存の職業を参考にする**: 似た能力を持つ職業の実装を確認
2. **シンプルから始める**: 複雑な能力より、わかりやすい能力が良い
3. **テストプレイ**: 実際にゲームで使ってバランスを確認
4. **フィードバック**: プレイヤーの意見を聞いて調整
Good Luck! 🎮**
## 最小実装サンプル
```js
btrw.registerClass({
id: 'example', displayName: 'Example', icon: 'PAPER', configNamespace: 'classes.example', lore: ['§7右クリックで動作'], enabled: true,
onGameStartPlayer(ctx) { this._flag(ctx.player(), true); },
onGameEndPlayer(ctx) { this._flag(ctx.player(), false); },
onDeselect(ctx) { this._flag(ctx.player(), false); },
onItemUse(event, ctx) {
if (!this._isActive(ctx.player())) return;
if (!btrw.isRightClick(event, 'example_token')) return;
event.setCancelled(true);
btrw.msg(ctx.player(), 'Example skill!');
},
_flag(p, on) {
if (!this._active) this._active = {};
const id = p.getUniqueId();
if (on) this._active[id] = true; else delete this._active[id];
},
_isActive(p) { return !!(this._active && this._active[p.getUniqueId()]); },
});
```
### トークン配布
```js
const ChatColor = Java.type('org.bukkit.ChatColor');
function ensureToken(player) {
const token = btrw.makeToken(
'ECHO_SHARD', 'example_token',
ChatColor.AQUA + 'Example Token',
[ChatColor.GRAY + '右クリックで発動']
);
const inv = player.getInventory();
let has = false;
for (const item of inv.getContents()) {
if (item && item.isSimilar && item.isSimilar(token)) { has = true; break; }
}
if (!has) inv.addItem(token);
}
```
## 既存職業の参照例
実装に迷ったら、以下の既存職業を参考にしてください:
| ファイル | 主な機能 | 参考ポイント |
| ---------- | ---------- | -------------- |
| `astral.js` | 10秒スペクテイター化 | `makeToken` / `startCooldownLabel` / `beginTempSpectate` の組み合わせ |
| `camera_jack.js` | 目標憑依 | `MannequinService` のダミー化 + ボスバー更新 |
| `builder.js` | 定期バンドル支給 | タスクの cancel・インベントリ残余処理 |
| `mimic.js` | 変身+元に戻す | `_ensureToken`/再配布タスクパターン |
| `heavensfeel.js` | キル時ヘッドドロップ+蘇生 | グローバル死亡イベント(`btrw.onPlayerDeath`)の使用例 |
| `bomb_disposal.js` | クリーパー討伐でTNT入手 | シンプルなパッシブ能力の実装例 |

コメント
最新を表示する
NG表示方式
NGID一覧