460 lines
20 KiB
JavaScript
460 lines
20 KiB
JavaScript
/*:
|
||
* @target MZ
|
||
* @plugindesc フロアマネージャー ※マップ名を参照してるので取扱注意。詳細はヘルプ
|
||
* @author kurogoma
|
||
* @base PluginCommonBase
|
||
* @help 名称一致で検索してるため、マップ名の変更には要注意(ハジメイセキなど) floorTable参照
|
||
*
|
||
* @command Initialize
|
||
* @desc 初期化
|
||
*
|
||
* @command CheckNextFloor
|
||
* @text 次の階層確認
|
||
* @desc 仮決定したマップ名を変数に格納(未訪問)
|
||
* @arg variableId @type variable @default 1
|
||
*
|
||
* @command MoveToNextFloor
|
||
* @text 次の階層移動
|
||
* @desc 仮決定済みマップへ移動(訪問済み)
|
||
*
|
||
* @command CheckNextRareFloor
|
||
* @text 次のレア階層確認
|
||
* @desc 仮決定したレアマップ名を変数に格納
|
||
* @arg variableId @type variable @default 1
|
||
*
|
||
* @command MoveToNextRareFloor
|
||
* @text 次のレア階層移動
|
||
* @desc 仮決定済みレアマップへ移動(階層は加算されない)
|
||
*
|
||
* @command StartTimer
|
||
* @text タイマー開始
|
||
* @desc ランタイマーを開始(0からスタート)
|
||
*
|
||
* @command StopTimer
|
||
* @text タイマー停止
|
||
* @desc ランタイマーを停止(クリア時に呼び出す)
|
||
*/
|
||
|
||
(() => {
|
||
'use strict';
|
||
const script = document.currentScript;
|
||
const param = PluginManagerEx.createParameter(script);
|
||
|
||
//
|
||
// FloorManager
|
||
//
|
||
class FloorManager {
|
||
constructor() {
|
||
this.currentFloor = 0; // 現在の階層
|
||
this.visitedFloorIds = [];
|
||
this.tempCheckFloorIds = {};
|
||
this.mapQueues = {}; // レベルごとのマップキュー(シャッフル済み)
|
||
this.rareMapQueue = []; // レアマップキュー(シャッフル済み)
|
||
this.runStartPlayTime = 0; // 開始時のPlayTime(秒) 0は未設定または開始直後とみなす
|
||
this.runEndPlayTime = 0; // 終了時のPlayTime(秒)
|
||
|
||
this.floorTable = [ // フロアテーブル
|
||
{ mapLevel: 1, min: 1, max: 1, type: "fixed", name: "Ruins of the Beginning" }, // 固定マップ(難易度1)
|
||
{ mapLevel: 1, min: 2, max: 3, type: "level", level: "Level 1" }, // 難易度1
|
||
{ mapLevel: 2, min: 4, max: 6, type: "level", level: "Level 2" }, // 難易度2
|
||
{ mapLevel: 3, min: 7, max: 9, type: "level", level: "Level 3" }, // 難易度3
|
||
{ mapLevel: 4, min: 10, max: 10, type: "fixed", name: "Hall of Giants" }, // ボス固定(難易度4)
|
||
{ mapLevel: 4, min: 11, max: 13, type: "level", level: "Level 4" }, // 難易度4
|
||
{ mapLevel: 5, min: 14, max: 16, type: "level", level: "Level 5" }, // 難易度5
|
||
{ mapLevel: 6, min: 17, max: 19, type: "level", level: "Level 6" }, // 難易度6
|
||
{ mapLevel: 7, min: 20, max: 20, type: "fixed", name: "Garden of Rot" }, // ボス固定(難易度7)
|
||
{ mapLevel: 7, min: 21, max: 23, type: "level", level: "Level 7" }, // 難易度7
|
||
{ mapLevel: 8, min: 24, max: 26, type: "level", level: "Level 8" }, // 難易度8
|
||
{ mapLevel: 9, min: 27, max: 29, type: "level", level: "Level 9" }, // 難易度9
|
||
{ mapLevel: 10, min: 30, max: 30, type: "fixed", name: "Altar of Dominion" }, // ボス固定(難易度10)
|
||
{ mapLevel: 10, min: 31, max: 31, type: "fixed", name: "Ruins of the End" }, // 固定マップ(難易度10)
|
||
{ mapLevel: 11, min: 32, max: 32, type: "fixed", name: "Demon of Greed" } // 難易度10
|
||
];
|
||
}
|
||
|
||
initialize() {
|
||
console.log("[FloorManager] initialize");
|
||
this.currentFloor = 0;
|
||
this.visitedFloorIds = [];
|
||
this.tempCheckFloorIds = {};
|
||
this.mapQueues = {};
|
||
this.rareMapQueue = [];
|
||
this.runStartPlayTime = 0;
|
||
this.runEndPlayTime = 0;
|
||
if (!$dataMapInfos) {
|
||
console.warn("[FloorManager] $dataMapInfos not loaded");
|
||
return;
|
||
}
|
||
this.loadAvailableMaps();
|
||
}
|
||
|
||
loadAvailableMaps() {
|
||
// --- 親「通常マップ」を検索 ---
|
||
const mainParentEntry = Object.entries($dataMapInfos)
|
||
.find(([id, info]) => info && info.name === "Normal Map");
|
||
if (!mainParentEntry) {
|
||
console.warn("[FloorManager] 親 'Normal Map' が見つかりません");
|
||
return;
|
||
}
|
||
|
||
const mainParentId = Number(mainParentEntry[0]);
|
||
|
||
// --- 「通常マップ」配下の子フォルダ(レベルや固定)を抽出 ---
|
||
const levelParents = Object.entries($dataMapInfos)
|
||
.filter(([id, info]) =>
|
||
info &&
|
||
info.parentId === mainParentId &&
|
||
(/^Level \d+$/.test(info.name) || /^ボスマップ\d+$/.test(info.name))
|
||
);
|
||
|
||
// --- レベルごとにマップを分類 ---
|
||
const parentIds = levelParents.map(([id]) => Number(id));
|
||
const allMaps = Object.entries($dataMapInfos)
|
||
.filter(([id, info]) => info && parentIds.includes(info.parentId))
|
||
.map(([id, info]) => ({
|
||
id: Number(id),
|
||
name: info.name,
|
||
order: info.order || 0,
|
||
parentId: info.parentId,
|
||
parentName: ($dataMapInfos[info.parentId] && $dataMapInfos[info.parentId].name) || ""
|
||
}));
|
||
|
||
// --- レベルごとにグループ化してシャッフル ---
|
||
this.mapQueues = {};
|
||
for (const map of allMaps) {
|
||
if (!this.mapQueues[map.parentName]) {
|
||
this.mapQueues[map.parentName] = [];
|
||
}
|
||
this.mapQueues[map.parentName].push(map);
|
||
}
|
||
|
||
// 各レベルのキューをソート(order順)してからシャッフル
|
||
for (const level of Object.keys(this.mapQueues)) {
|
||
// まずorder順にソート(ツクールのツリー表示順)
|
||
this.mapQueues[level].sort((a, b) => a.order - b.order);
|
||
// シャッフルONならランダム化
|
||
if (this.isShuffle()) {
|
||
this.shuffleArray(this.mapQueues[level]);
|
||
}
|
||
}
|
||
|
||
// --- レアマップ(親「レアマップ」)も取得 ---
|
||
const rareParentEntry = Object.entries($dataMapInfos)
|
||
.find(([id, info]) => info && info.name === "Rare Map");
|
||
if (rareParentEntry) {
|
||
const rareParentId = Number(rareParentEntry[0]);
|
||
this.rareMapQueue = Object.entries($dataMapInfos)
|
||
.filter(([id, info]) => info && info.parentId === rareParentId)
|
||
.map(([id, info]) => ({
|
||
id: Number(id),
|
||
name: info.name,
|
||
order: info.order || 0
|
||
}));
|
||
|
||
// まずorder順にソート(ツクールのツリー表示順)
|
||
this.rareMapQueue.sort((a, b) => a.order - b.order);
|
||
// シャッフルONならランダム化
|
||
if (this.isShuffle()) {
|
||
this.shuffleArray(this.rareMapQueue);
|
||
}
|
||
} else {
|
||
console.warn("[FloorManager] 親 'Rare Map' が見つかりません");
|
||
}
|
||
|
||
// 初期化完了ログ(共通メソッドで出力)
|
||
this.logQueueState("初期化完了");
|
||
}
|
||
|
||
isShuffle() { return true; }
|
||
|
||
shuffleArray(array) {
|
||
for (let i = array.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[array[i], array[j]] = [array[j], array[i]];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* キューの状態をログ出力(初期化・ロード時共通)
|
||
* @param {string} context - 呼び出し元の識別用文字列
|
||
*/
|
||
logQueueState(context) {
|
||
const sortedLevels = Object.keys(this.mapQueues || {}).sort((a, b) => {
|
||
const numA = parseInt(a.replace(/\D/g, '')) || 0;
|
||
const numB = parseInt(b.replace(/\D/g, '')) || 0;
|
||
return numA - numB;
|
||
});
|
||
console.log(`[FloorManager] ${context}`);
|
||
for (const level of sortedLevels) {
|
||
const queue = this.mapQueues[level];
|
||
console.log(` ${level}: [${queue.map(m => m.name).join(', ')}]`);
|
||
}
|
||
if (this.rareMapQueue && this.rareMapQueue.length > 0) {
|
||
console.log(` レアマップ: [${this.rareMapQueue.map(m => m.name).join(', ')}]`);
|
||
}
|
||
}
|
||
|
||
findFixedMapByName(bossName) {
|
||
const entry = Object.entries($dataMapInfos).find(
|
||
([, info]) => info && info.name === bossName
|
||
);
|
||
if (entry) {
|
||
return { id: Number(entry[0]), name: entry[1].name, order: entry[1].order || 0 };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
getFloorInfo(floorNumber) {
|
||
const entry = this.floorTable.find(e => floorNumber >= e.min && floorNumber <= e.max);
|
||
return entry || { type: "level", level: "レベル1" };
|
||
}
|
||
|
||
checkFloor(eventId) {
|
||
if (this.tempCheckFloorIds[eventId] != null)
|
||
return this.tempCheckFloorIds[eventId];
|
||
|
||
const nextFloor = this.currentFloor + 1;
|
||
const info = this.getFloorInfo(nextFloor);
|
||
|
||
// --- 固定マップならここで確定 ---
|
||
if (info.type === "fixed") {
|
||
const map = this.findFixedMapByName(info.name);
|
||
if (map) {
|
||
this.tempCheckFloorIds[eventId] = map.id;
|
||
console.log(`[FloorManager] 固定階層 ${nextFloor} -> ${map.name}`);
|
||
return map.id;
|
||
}
|
||
}
|
||
|
||
// --- 通常レベルマップの抽選(キューから順番に取得) ---
|
||
const queue = this.mapQueues[info.level];
|
||
if (!queue || queue.length === 0) {
|
||
console.warn(`[FloorManager] ${info.level} のキューが空です`);
|
||
return null;
|
||
}
|
||
|
||
// キューの先頭から未使用のマップを探す
|
||
const usedFloors = Object.values(this.tempCheckFloorIds);
|
||
let chosenIndex = queue.findIndex(map =>
|
||
!this.visitedFloorIds.includes(map.id) &&
|
||
!usedFloors.includes(map.id)
|
||
);
|
||
|
||
// 全て使用済みの場合は先頭を使う(フォールバック)
|
||
if (chosenIndex === -1) {
|
||
console.warn(`[FloorManager] ${info.level} の未使用マップがありません。先頭を使用`);
|
||
chosenIndex = 0;
|
||
}
|
||
|
||
const chosen = queue[chosenIndex];
|
||
this.tempCheckFloorIds[eventId] = chosen.id;
|
||
console.log(`[FloorManager] ${info.level} -> ${chosen.name} (queue index: ${chosenIndex})`);
|
||
return chosen.id;
|
||
}
|
||
|
||
checkRareFloor(eventId) {
|
||
if (this.tempCheckFloorIds[eventId] != null)
|
||
return this.tempCheckFloorIds[eventId];
|
||
|
||
if (!this.rareMapQueue || this.rareMapQueue.length === 0) {
|
||
console.warn("[FloorManager] レアマップキューが空です");
|
||
return null;
|
||
}
|
||
|
||
// キューの先頭から未使用のマップを探す
|
||
const usedFloors = Object.values(this.tempCheckFloorIds);
|
||
let chosenIndex = this.rareMapQueue.findIndex(map =>
|
||
!this.visitedFloorIds.includes(map.id) &&
|
||
!usedFloors.includes(map.id)
|
||
);
|
||
|
||
// 全て使用済みの場合は先頭を使う(フォールバック)
|
||
if (chosenIndex === -1) {
|
||
console.warn("[FloorManager] 未使用レアマップがありません。先頭を使用");
|
||
chosenIndex = 0;
|
||
}
|
||
|
||
const chosen = this.rareMapQueue[chosenIndex];
|
||
this.tempCheckFloorIds[eventId] = chosen.id;
|
||
console.log(`[FloorManager] eventId:${eventId} -> rare map: ${chosen.name} (queue index: ${chosenIndex})`);
|
||
return chosen.id;
|
||
}
|
||
|
||
moveToNextFloor(eventId) {
|
||
const nextFloorId = this.tempCheckFloorIds[eventId];
|
||
if (!nextFloorId) return null;
|
||
|
||
this.currentFloor++;
|
||
this.visitedFloorIds.push(nextFloorId);
|
||
this.tempCheckFloorIds = {};
|
||
console.log(`[FloorManager] eventId:${eventId} -> floorId:${nextFloorId}(${$dataMapInfos[nextFloorId]?.name})`);
|
||
return nextFloorId;
|
||
}
|
||
|
||
moveToNextRareFloor(eventId) {
|
||
const nextFloorId = this.tempCheckFloorIds[eventId];
|
||
if (!nextFloorId) {
|
||
console.warn(`[FloorManager] eventId:${eventId} has no assigned rare floor.`);
|
||
return null;
|
||
}
|
||
// currentFloorは増やさない(寄り道)
|
||
this.visitedFloorIds.push(nextFloorId); // ← 重要:再抽選防止
|
||
this.tempCheckFloorIds = {};
|
||
console.log(`[FloorManager] eventId:${eventId} -> rareFloorId:${nextFloorId}(${$dataMapInfos[nextFloorId]?.name})`);
|
||
return nextFloorId;
|
||
}
|
||
|
||
getNextMapName(floorId) {
|
||
// 全キューから検索
|
||
for (const level of Object.keys(this.mapQueues)) {
|
||
const mapData = this.mapQueues[level].find(m => m.id === floorId);
|
||
if (mapData) return mapData.name;
|
||
}
|
||
// レアマップキューから検索
|
||
const rareData = this.rareMapQueue.find(m => m.id === floorId);
|
||
if (rareData) return rareData.name;
|
||
// フォールバック
|
||
if ($dataMapInfos[floorId]) return $dataMapInfos[floorId].name;
|
||
return "???";
|
||
}
|
||
|
||
//=====================================================================
|
||
// タイマー機能($gameSystem.playtime() ベース)
|
||
//=====================================================================
|
||
|
||
// タイマー開始
|
||
startTimer() {
|
||
// 現在のプレイ時間(秒)を記録
|
||
this.runStartPlayTime = $gameSystem.playtime();
|
||
this.runEndPlayTime = 0; // 終了時間はクリア
|
||
console.log(`[FloorManager] タイマー開始 (Start: ${this.runStartPlayTime}s)`);
|
||
}
|
||
|
||
// タイマー停止(完了時刻を記録)
|
||
stopTimer() {
|
||
// 開始していなければ無視 (0の場合は開始直後と区別つかないが、実害少ないので許容)
|
||
this.runEndPlayTime = $gameSystem.playtime();
|
||
console.log(`[FloorManager] タイマー停止: ${this.getFormattedTime()}`);
|
||
}
|
||
|
||
// フォーマット済み時間文字列を取得(H:MM:SS)
|
||
getFormattedTime() {
|
||
const elapsedSeconds = Math.max(0, this.runEndPlayTime - this.runStartPlayTime);
|
||
const hours = Math.floor(elapsedSeconds / 3600);
|
||
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
|
||
const seconds = elapsedSeconds % 60;
|
||
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||
}
|
||
}
|
||
|
||
window.Kurogoma = window.Kurogoma || {};
|
||
window.Kurogoma.floorManager = new FloorManager();
|
||
const floorManager = window.Kurogoma.floorManager;
|
||
|
||
// --- セーブにfloorManagerの状態を含める ---
|
||
const _DataManager_makeSaveContents = DataManager.makeSaveContents;
|
||
DataManager.makeSaveContents = function() {
|
||
const contents = _DataManager_makeSaveContents.call(this);
|
||
if (floorManager) {
|
||
contents.floorManager = {
|
||
currentFloor: floorManager.currentFloor,
|
||
visitedFloorIds: floorManager.visitedFloorIds,
|
||
tempCheckFloorIds: floorManager.tempCheckFloorIds,
|
||
mapQueues: floorManager.mapQueues,
|
||
rareMapQueue: floorManager.rareMapQueue,
|
||
runStartPlayTime: floorManager.runStartPlayTime,
|
||
runEndPlayTime: floorManager.runEndPlayTime
|
||
};
|
||
}
|
||
return contents;
|
||
};
|
||
|
||
// --- ロード時に復元 ---
|
||
const _DataManager_extractSaveContents = DataManager.extractSaveContents;
|
||
DataManager.extractSaveContents = function(contents) {
|
||
_DataManager_extractSaveContents.call(this, contents);
|
||
if (floorManager && contents.floorManager) {
|
||
Object.assign(floorManager, contents.floorManager);
|
||
floorManager.logQueueState(`ロード完了 (階層: ${floorManager.currentFloor}, 訪問済み: ${floorManager.visitedFloorIds.length}, 経過: ${floorManager.getFormattedTime()})`);
|
||
} else {
|
||
console.warn("[FloorManager] セーブデータにfloorManager情報がない → 再初期化");
|
||
floorManager?.initialize();
|
||
}
|
||
};
|
||
|
||
//
|
||
// プラグインコマンド
|
||
//
|
||
PluginManagerEx.registerCommand(script, "Initialize", () => {
|
||
floorManager.initialize();
|
||
});
|
||
|
||
PluginManagerEx.registerCommand(script, "CheckNextFloor", function(args) {
|
||
const eventId = $gameMap._interpreter._eventId || 0;
|
||
const floorId = floorManager.checkFloor(eventId);
|
||
const mapName = floorManager.getNextMapName(floorId);
|
||
$gameVariables.setValue(args.variableId, mapName);
|
||
});
|
||
|
||
PluginManagerEx.registerCommand(script, "MoveToNextFloor", function() {
|
||
const eventId = $gameMap._interpreter._eventId || 0;
|
||
const floorId = floorManager.moveToNextFloor(eventId);
|
||
if (!floorId) return;
|
||
$gamePlayer.reserveTransferToEvent({ mapId: floorId, eventId: 2, direction: 0, fadeType: 0 });
|
||
this.setWaitMode("transfer");
|
||
});
|
||
|
||
PluginManagerEx.registerCommand(script, "CheckNextRareFloor", function(args) {
|
||
const eventId = $gameMap._interpreter._eventId || 0;
|
||
const floorId = floorManager.checkRareFloor(eventId);
|
||
const mapName = floorManager.getNextMapName(floorId);
|
||
$gameVariables.setValue(args.variableId, mapName);
|
||
});
|
||
|
||
PluginManagerEx.registerCommand(script, "MoveToNextRareFloor", function() {
|
||
const eventId = $gameMap._interpreter._eventId || 0;
|
||
const floorId = floorManager.moveToNextRareFloor(eventId);
|
||
if (!floorId) return;
|
||
$gamePlayer.reserveTransferToEvent({ mapId: floorId, eventId: 2, direction: 0, fadeType: 0 });
|
||
this.setWaitMode("transfer");
|
||
});
|
||
|
||
PluginManagerEx.registerCommand(script, "StartTimer", () => {
|
||
floorManager.startTimer();
|
||
});
|
||
|
||
PluginManagerEx.registerCommand(script, "StopTimer", () => {
|
||
floorManager.stopTimer();
|
||
});
|
||
|
||
//
|
||
// Game_Player 拡張
|
||
//
|
||
Game_Player.prototype.reserveTransferToEvent = function({ mapId, eventId, direction, fadeType }) {
|
||
this.reserveTransfer(mapId, 0, 0, direction, fadeType);
|
||
this._newEventId = eventId;
|
||
};
|
||
|
||
const _Game_Player_performTransfer = Game_Player.prototype.performTransfer;
|
||
Game_Player.prototype.performTransfer = function() {
|
||
if (this.isTransferring()) {
|
||
if (this._newMapId !== $gameMap.mapId() || this._needsMapReload) {
|
||
$gameMap.setup(this._newMapId);
|
||
this._needsMapReload = false;
|
||
}
|
||
if (this._newEventId > 0) {
|
||
const event = $gameMap.event(this._newEventId);
|
||
if (event) {
|
||
this._newX = event.x;
|
||
this._newY = event.y;
|
||
this._newDirection = event.event().pages[0].image.direction;
|
||
}
|
||
this._newEventId = 0;
|
||
}
|
||
this.setDirection(this._newDirection);
|
||
this.locate(this._newX, this._newY);
|
||
this.refresh();
|
||
this.clearTransferInfo();
|
||
}
|
||
};
|
||
})();
|