Files
kondiplo_front/app/views/games/show.html.erb
kontei bb9ec2df1d
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
掲示板実装
2026-02-19 22:30:59 +09:00

1193 lines
63 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<% content_for :title, @game.title %>
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900"><%= h(@game.title) %></h1>
<% if @game.memo.present? %>
<p class="mt-1 text-sm text-gray-500"><%= h(@game.memo) %></p>
<% end %>
<div class="mt-2 flex items-center space-x-4 text-sm text-gray-600">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
<%= @game.solo_mode? ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800' %>">
<%= @game.solo_mode? ? 'ソロモード' : 'マルチプレイヤーモード' %>
</span>
<% if @game.administrator %>
<span>管理者: <%= h(@game.administrator.username) %></span>
<% end %>
<span>状態: <%= @game.status %></span>
</div>
<% if @game.year_limit.present? || @game.victory_sc_count != 18 || @game.scoring_system != "none" || @game.auto_turn? %>
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs">
<span class="font-medium text-gray-500">ハウスルール:</span>
<% if @game.auto_turn? %>
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-blue-100 text-blue-800">
<i class="fa-solid fa-clock mr-1"></i> <%= @game.schedule_display %>
</span>
<% end %>
<% if @game.year_limit.present? %>
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-amber-100 text-amber-800">
<i class="fa-solid fa-hourglass-half mr-1"></i> <%= @game.year_limit %>年制限
</span>
<% end %>
<% if @game.victory_sc_count != 18 %>
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-green-100 text-green-800">
<i class="fa-solid fa-crown mr-1"></i> 目標SC: <%= @game.victory_sc_count %>
</span>
<% end %>
<% if @game.scoring_system != "none" %>
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-800">
<i class="fa-solid fa-chart-bar mr-1"></i> <%= @game.scoring_system_name %>
</span>
<% end %>
</div>
<% end %>
<% if @game.auto_turn? && @game.next_deadline_at.present? && @game.status == "in_progress" %>
<div class="mt-2 px-3 py-1.5 rounded-md bg-blue-50 border border-blue-200 text-sm text-blue-800 flex items-center gap-2">
<i class="fa-solid fa-clock"></i>
<span>次のターン締切: <strong><%= @game.next_deadline_at.in_time_zone("Asia/Tokyo").strftime("%Y-%m-%d %H:%M") %></strong></span>
<span class="text-blue-600" id="deadline-countdown"></span>
</div>
<script>
(function() {
const deadline = new Date("<%= @game.next_deadline_at.iso8601 %>");
const countdownEl = document.getElementById('deadline-countdown');
function updateCountdown() {
const now = new Date();
const diff = deadline - now;
if (diff <= 0) {
countdownEl.textContent = '(締切済み — まもなく処理されます)';
return;
}
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
countdownEl.textContent = `(残り ${hours}時間 ${minutes}分)`;
}
updateCountdown();
setInterval(updateCountdown, 60000);
})();
</script>
<% end %>
</div>
<div class="flex space-x-3">
<% if current_user&.admin? || @game.administrator == current_user %>
<%= link_to "設定", edit_game_path(@game), class: "inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
<%= button_to "削除", @game, method: :delete, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500", data: { turbo_confirm: "本当に削除しますか?" } %>
<% end %>
</div>
</div>
<!-- ゲーム参加セクション -->
<% if !@game.solo_mode? && @game.status == 'recruiting' %>
<div class="bg-white shadow overflow-hidden sm:rounded-lg border border-gray-200 mb-6">
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">ゲーム参加</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="mb-4">
<p class="text-sm text-gray-600">
現在の参加者: <span class="font-semibold"><%= @game.participants.count %></span> /
<span class="font-semibold"><%= @game.participants_count %></span>人
</p>
<% if @game.password_protected? %>
<p class="text-sm text-amber-600 mt-1">※このゲームはパスワード保護されています</p>
<% end %>
</div>
<!-- 参加者リスト -->
<div class="mb-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">参加者一覧:</h4>
<div class="space-y-1">
<% @game.participants.includes(:user).each do |participant| %>
<div class="flex items-center justify-between text-sm">
<span><%= participant.user.username %></span>
<span class="text-xs text-gray-500">
<%= participant.power.present? ? "(#{participant.power})" : "(未選択)" %>
</span>
</div>
<% end %>
</div>
</div>
<!-- 参加ボタン -->
<% current_participant = @game.participants.find_by(user: current_user) %>
<% if current_user && !current_participant && @game.participants.count < @game.participants_count %>
<%= form_with url: join_game_game_path(@game), method: :post, class: "space-y-3" do |f| %>
<% if @game.password_protected? %>
<div>
<%= f.label :password, "参加パスワード", class: "block text-sm font-medium text-gray-700" %>
<%= f.password_field :password, class: "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" %>
</div>
<% end %>
<div>
<%= f.submit "ゲームに参加", class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
</div>
<% end %>
<% elsif current_participant %>
<p class="text-sm text-green-600">あなたはこのゲームに参加しています</p>
<% elsif @game.participants.count >= @game.participants_count %>
<p class="text-sm text-red-600">このゲームは満員です</p>
<% elsif !current_user %>
<p class="text-sm text-gray-600">
<%= link_to "ログイン", login_path %>してゲームに参加
</p>
<% end %>
<!-- ゲーム管理者用コントロール -->
<% if current_user&.admin? || @game.administrator == current_user %>
<div class="mt-6 pt-6 border-t border-gray-200">
<h4 class="text-sm font-medium text-gray-700 mb-3">ゲーム管理者コントロール</h4>
<div class="space-y-2">
<% if @game.participants.count >= 2 && @game.status == 'recruiting' %>
<%= button_to "国選択フェーズ開始", start_power_selection_game_path(@game),
method: :post,
class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<!-- 国選択フェーズ -->
<% if @game.status == 'power_selection' %>
<div class="bg-white shadow overflow-hidden sm:rounded-lg border border-gray-200 mb-6">
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">国選択フェーズ</h3>
<p class="mt-1 text-sm text-gray-500">全参加者が国を選択してください</p>
</div>
<div class="px-4 py-5 sm:p-6">
<!-- 参加者と国選択状況 -->
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-3">参加者の国選択状況:</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<% @game.participants.includes(:user).each do |participant| %>
<div class="flex items-center justify-between p-3 border border-gray-200 rounded-md <%= participant.power.present? ? 'bg-green-50' : 'bg-yellow-50' %>">
<div>
<span class="font-medium"><%= participant.user.username %></span>
<% if participant.power.present? %>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<%= participant.power %>
</span>
<% else %>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
未選択
</span>
<% end %>
</div>
<% if participant.user == current_user && !participant.power.present? %>
<%= form_with url: select_power_game_participant_path(@game, participant), method: :patch, class: "flex items-center space-x-2" do |f| %>
<%= f.select :power,
options_for_select(@game.available_powers.map { |p| [p, p] }),
{ prompt: "国を選択" },
{ class: "block pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" } %>
<%= f.submit "選択", class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
<!-- ゲーム管理者用コントロール -->
<% if (current_user&.admin? || @game.administrator == current_user) && @game.all_powers_assigned? %>
<div class="mt-6 pt-6 border-t border-gray-200">
<div class="bg-green-50 border border-green-200 rounded-md p-4">
<h4 class="text-sm font-medium text-green-800 mb-2">全員の国選択が完了しました!</h4>
<%= button_to "命令入力フェーズ開始", start_order_input_game_path(@game),
method: :post,
class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<!-- ゲームターン表示 -->
<% if @display_turn && (@game.solo_mode? || @game.status == 'in_progress' || @game_finished) %>
<% if @game_finished && @winner_info %>
<!-- 最終結果表示セクション -->
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6 shadow-sm">
<div class="flex">
<div class="flex-shrink-0">
<i class="fa-solid fa-trophy text-yellow-400 text-2xl"></i>
</div>
<div class="ml-3 w-full">
<h3 class="text-lg leading-6 font-medium text-yellow-800">
Game Over - Final Result
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p class="font-bold mb-1">
結果: <%= @winner_info[:type] == "Solo Victory" ? "ソロ勝利" : "引き分け (Draw)" %>
</p>
<p>
<%= @winner_info[:type] == "Solo Victory" ? "勝者:" : "生存国:" %>
<% @winner_info[:winners].each do |winner| %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 border border-yellow-200 mr-2">
<%= winner %>
</span>
<% end %>
</p>
<% if @winner_info[:scores].present? %>
<div class="mt-3 border-t border-yellow-200 pt-3">
<p class="font-bold mb-2">スコア(<%= @game.scoring_system_name %>:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
<% @winner_info[:scores].sort_by { |_, v| -v }.each do |power, score| %>
<div class="flex justify-between items-center bg-white/60 rounded px-3 py-1.5">
<span class="font-medium text-xs"><%= power %></span>
<span class="text-sm font-bold text-yellow-900"><%= score %></span>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
<!-- ヘッダーエリア -->
<div id="game-turn-section" class="bg-white shadow overflow-hidden sm:rounded-lg border border-gray-200 mb-6">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gray-50">
<div>
<% if @game_finished %>
<div class="flex items-center space-x-2 mb-1">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
履歴モード
</span>
<h2 class="text-2xl font-bold text-gray-900"><%= @current_season_year %></h2>
</div>
<% else %>
<h2 class="text-2xl font-bold text-gray-900"><%= @current_season_year %></h2>
<% end %>
<div class="mt-1 flex items-center space-x-4 text-sm text-gray-600">
<span>ターン: <%= @display_turn.number %></span>
<span class="text-gray-400">|</span>
<span>フェーズ: <%= @display_turn.phase %></span>
</div>
</div>
<% if @game_finished %>
<!-- ターンナビゲーション (終了済みゲーム) -->
<div class="flex items-center space-x-2">
<% if @display_turn.number > 1 %>
<%= link_to game_path(@game, turn_number: @display_turn.number - 1, anchor: "game-turn-section"), class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", data: { turbo: false } do %>
<i class="fa-solid fa-chevron-left mr-1"></i> 前のターン
<% end %>
<% else %>
<button disabled class="inline-flex items-center px-3 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-400 bg-gray-50 cursor-not-allowed">
<i class="fa-solid fa-chevron-left mr-1"></i> 前のターン
</button>
<% end %>
<% if @display_turn.number < @latest_turn.number %>
<%= link_to game_path(@game, turn_number: @display_turn.number + 1, anchor: "game-turn-section"), class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", data: { turbo: false } do %>
次のターン <i class="fa-solid fa-chevron-right ml-1"></i>
<% end %>
<% else %>
<button disabled class="inline-flex items-center px-3 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-400 bg-gray-50 cursor-not-allowed">
次のターン <i class="fa-solid fa-chevron-right ml-1"></i>
</button>
<% end %>
</div>
<% else %>
<!-- 進行中ゲーム: 担当国表示 + ターンナビゲーション -->
<div class="flex items-center space-x-4">
<% current_participant = @game.participants.find_by(user: current_user) %>
<% if current_participant&.power.present? %>
<div class="flex items-center bg-white px-4 py-2 rounded border border-gray-300 shadow-sm">
<span class="mr-2 text-sm text-gray-500">あなたの担当国:</span>
<span class="text-lg font-bold text-indigo-700"><%= current_participant.power %></span>
<%= hidden_field_tag :current_user_power, current_participant.power, id: "current-user-power" %>
</div>
<% else %>
<div class="flex items-center">
<label for="power-select" class="mr-2 text-sm font-medium text-gray-700">視点切り替え:</label>
<% powers = %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY] %>
<% powers << "ALL" if current_user&.admin? || @game.administrator == current_user %>
<%= select_tag :power, options_for_select(powers), id: "power-select", prompt: "国を選択", class: "block w-40 pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" %>
</div>
<% end %>
<% if @latest_turn.number > 1 %>
<div class="flex items-center space-x-2">
<% if @display_turn.number > 1 %>
<%= link_to game_path(@game, turn_number: @display_turn.number - 1), class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", data: { turbo: false } do %>
<i class="fa-solid fa-chevron-left mr-1"></i> 前
<% end %>
<% else %>
<button disabled class="inline-flex items-center px-3 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-400 bg-gray-50 cursor-not-allowed">
<i class="fa-solid fa-chevron-left mr-1"></i> 前
</button>
<% end %>
<% if @display_turn.number < @latest_turn.number %>
<%= link_to game_path(@game, turn_number: @display_turn.number + 1), class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", data: { turbo: false } do %>
次 <i class="fa-solid fa-chevron-right ml-1"></i>
<% end %>
<% else %>
<button disabled class="inline-flex items-center px-3 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-400 bg-gray-50 cursor-not-allowed">
次 <i class="fa-solid fa-chevron-right ml-1"></i>
</button>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左カラム: マップ (2/3) -->
<div class="lg:col-span-2 space-y-6">
<div class="bg-white shadow rounded-lg overflow-hidden border border-gray-200">
<div class="p-4 bg-gray-100 flex flex-col items-center justify-center min-h-[500px]">
<div class="game-map w-full overflow-hidden flex justify-center">
<% if @display_turn %>
<% if @game_finished %>
<% initial_svg = @display_turn.svg_orders&.dig("ALL") || @display_turn.svg_orders&.dig("NONE") || @display_turn.svg_date %>
<% else %>
<% initial_svg = @display_turn.svg_orders&.dig("NONE") || @display_turn.svg_date %>
<% end %>
<% if initial_svg.present? %>
<%= raw initial_svg %>
<% else %>
<p class="text-gray-500">マップデータがありません</p>
<% end %>
<% end %>
</div>
</div>
</div>
<!-- 命令入力セクション (スマホなど狭い画面では下に配置) -->
<% unless @game_finished || (@display_turn && @display_turn.number < @latest_turn.number) %>
<div class="bg-white shadow rounded-lg border border-gray-200 p-6">
<h3 class="text-lg font-bold text-gray-900 mb-4 border-b pb-2">命令入力</h3>
<%= form_with url: submit_orders_turn_path(@latest_turn), method: :patch, id: "orders-form", class: "space-y-4" do |f| %>
<%= f.hidden_field :power, id: "hidden-power-field" %>
<div id="orders-dropdowns" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<p class="text-sm text-gray-500 col-span-2">国を選択(または担当国でログイン)してください</p>
</div>
<div id="submit-button-container" class="hidden pt-4 border-t mt-4 flex justify-between items-center">
<%= f.submit "命令を送信", class: "w-full md:w-auto inline-flex justify-center items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 shadow-sm" %>
</div>
<% end %>
</div>
<% end %>
<!-- 引き分け投票セクション (独立したカード) - 最新ターン閲覧時かつゲーム進行中のみ表示 -->
<% unless @game_finished || (@display_turn && @display_turn.number < @latest_turn.number) %>
<% current_participant = @game.participants.find_by(user: current_user) %>
<% if current_participant&.power.present? %>
<div class="bg-white shadow rounded-lg border border-gray-200 p-6">
<% if @latest_turn.draw_voted?(current_participant.power) %>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-purple-700">引き分けに投票済みです</span>
<form action="<%= vote_draw_game_path(@game) %>" method="post">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
<button type="submit" class="inline-flex items-center px-3 py-1.5 border border-purple-300 shadow-sm text-sm font-medium rounded text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
投票を取り消す
</button>
</form>
</div>
<% else %>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500">ゲームを引き分けで終了しますか?</span>
<form action="<%= vote_draw_game_path(@game) %>" method="post" id="draw-vote-form">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
<button type="submit" id="draw-vote-btn" class="inline-flex items-center px-3 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
引き分けに投票
</button>
</form>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<!-- 右カラム: 情報パネル (1/3) -->
<div class="space-y-6">
<!-- 掲示板リンク -->
<div class="bg-white shadow rounded-lg border border-gray-200 overflow-hidden">
<div class="p-4 flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0 bg-indigo-100 rounded-md p-3">
<i class="fa-solid fa-comments text-indigo-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">外交・交渉掲示板</h3>
<p class="text-sm text-gray-500">
秘密交渉や全体アナウンスはこちら
</p>
</div>
</div>
<div>
<%= link_to game_boards_path(@game), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %>
掲示板へ
<%
# 未読件数取得(参加中の全掲示板の合計)
current_participant = @game.participants.find_by(user: current_user)
if current_participant
total_unread = 0
current_participant.boards.each do |board|
total_unread += board.unread_count_for(current_participant)
end
if total_unread > 0
%>
<span class="ml-2 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-100 bg-red-600 rounded-full">
<%= total_unread %>
</span>
<%
end
end
%>
<% end %>
</div>
</div>
</div>
<!-- 国ステータス表 -->
<div class="bg-white shadow rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-3 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">国別情報</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200" id="country-status-table">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">国名</th>
<th scope="col" class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">SC</th>
<th scope="col" class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Unit</th>
<th scope="col" class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">命令</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 text-sm">
<% @country_statuses.each do |status| %>
<tr class="<%= 'bg-indigo-50' if status[:is_user] %> hover:bg-gray-50 transition-colors" data-power="<%= status[:power] %>">
<td class="px-3 py-2 whitespace-nowrap font-medium flex items-center">
<span class="inline-flex items-center px-2 py-0.5 rounded text-sm font-bold border border-gray-200 shadow-sm <%= power_color_class(status[:power]) %>">
<%= status[:power] %>
</span>
<% if status[:is_user] %>
<span class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-800">YOU</span>
<% elsif status[:participant].nil? %>
<span class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800" title="担当者なし(自動処理)">
<i class="fa-solid fa-robot mr-1"></i> NPC
</span>
<% end %>
<% if @latest_turn.draw_voted?(status[:power]) %>
<span class="ml-2 text-purple-600" title="引き分け投票中">
<i class="fa-solid fa-handshake"></i>
</span>
<% end %>
</td>
<td class="px-3 py-2 whitespace-nowrap text-center text-gray-500"><%= status[:sc_count] %></td>
<td class="px-3 py-2 whitespace-nowrap text-center text-gray-500"><%= status[:unit_count] %></td>
<td class="px-3 py-2 whitespace-nowrap text-center status-cell">
<% if status[:submitted] %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
完了
</span>
<% else %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
未完了
</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<!-- 確定した命令ログ -->
<div class="bg-white shadow rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-3 bg-gray-50 border-b border-gray-200">
<h3 class="text-sm font-bold text-gray-700 uppercase tracking-wider">確定した命令</h3>
</div>
<div class="p-4 max-h-60 overflow-y-auto">
<div id="decided-orders-content" class="text-xs text-gray-600 space-y-1">
<p class="text-center text-gray-500">-</p>
</div>
</div>
</div>
<!-- 管理者コントロール -->
<% unless @game_finished %>
<% if current_user&.admin? || @game.administrator == current_user %>
<div class="bg-white shadow rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-3 bg-gray-50 border-b border-gray-200">
<h3 class="text-sm font-bold text-gray-700 uppercase tracking-wider">管理者メニュー</h3>
</div>
<div class="p-4" id="process-turn-container">
<% if !@game.solo_mode? && !@game.all_orders_submitted? %>
<div class="mb-3 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p class="text-xs text-yellow-800">
⚠️ 全プレイヤーの命令入力が完了していません。<br>
未完了: <%= @game.participants.where(orders_submitted: false).pluck(:power).compact.join(', ') %>
</p>
</div>
<div class="space-y-2">
<%= button_to "ターン終了(全員完了待ち)", process_turn_turn_path(@display_turn), method: :post, form_class: "w-full", class: "w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500", data: { turbo_confirm: "全員の命令入力が完了していません。本当に実行しますか?" } %>
<%= button_to "強制ターン終了", process_turn_turn_path(@display_turn, force: true), method: :post, form_class: "w-full", class: "w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500", data: { turbo_confirm: "⚠️ 未完了のプレイヤーがいますが、強制的にターンを終了します。よろしいですか?" } %>
<div class="pt-2 border-t border-gray-200 mt-2">
<%= button_to "強制引き分け (Draw)", force_draw_game_path(@game), method: :post, form_class: "w-full", class: "w-full inline-flex justify-center items-center px-4 py-2 border border-purple-300 text-sm font-medium rounded-md text-purple-700 bg-purple-50 hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500", data: { turbo_confirm: "⚠️ ゲームを強制的に引き分けとして終了させます。よろしいですか?" } %>
</div>
</div>
<% else %>
<%= button_to "ターン終了", process_turn_turn_path(@display_turn), method: :post, form_class: "w-full", class: "w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500", data: { turbo_confirm: "本当に実行しますか?これにより結果が計算され、次のフェーズに進みます。" } %>
<% end %>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
<script>
console.log('Script loaded');
// --- 翻訳用辞書定義 ---
const PROVINCE_MAP = {
// 海域
"ADR": "アドリア海", "AEG": "エーゲ海", "BAL": "バルト海", "BAR": "バレンツ海", "BLA": "黒海",
"EAS": "東地中海", "ENG": "イギリス海峡", "BOT": "ボスニア湾", "GOL": "リオン湾", "LYO": "リオン湾",
"HEL": "ヘルゴラント湾", "ION": "イオニア海", "IRI": "アイルランド海", "MID": "中部大西洋", "MAO": "中部大西洋",
"NAO": "北大西洋", "NTH": "北海", "NWG": "ノルウェー海", "SKA": "スカゲラク海峡", "TYS": "ティレニア海",
"WES": "西地中海",
// 内陸・沿岸
"ALB": "アルバニア", "ANK": "アンカラ", "APU": "アプリア", "ARM": "アルメニア",
"BEL": "ベルギー", "BER": "ベルリン", "BOH": "ボヘミア", "BRE": "ブレスト",
"BUD": "ブダペスト", "BUL": "ブルガリア", "BUR": "ブルゴーニュ", "CLY": "クライド",
"CON": "コンスタンティノープル", "DEN": "デンマーク", "EDI": "エディンバラ", "FIN": "フィンランド",
"GAL": "ガリシア", "GAS": "ガスコニュ", "GRE": "ギリシャ", "HOL": "オランダ",
"KIE": "キール", "LON": "ロンドン", "LVN": "リヴォニア", "LIV": "リヴォニア",
"MAR": "マルセイユ", "MOS": "モスクワ", "MUN": "ミュンヘン", "NAP": "ナポリ",
"NAF": "北アフリカ", "NWY": "ノルウェー", "PAR": "パリ", "PIC": "ピカルディ",
"PIE": "ピエモンテ", "POR": "ポルトガル", "PRU": "プロシア", "ROM": "ローマ",
"RUH": "ルール", "RUM": "ルーマニア", "SER": "セルビア", "SEV": "セヴァストポリ",
"SIL": "シレジア", "SMY": "スミルナ", "SPA": "スペイン", "STP": "ペテルブルク",
"SWE": "スウェーデン", "SYR": "シリア", "TRI": "トリエステ", "TUN": "チュニジア",
"TUS": "トスカーナ", "TYR": "チロル", "UKR": "ウクライナ", "VEN": "ヴェネチア",
"VIE": "ウィーン", "WAL": "ウェールズ", "WAR": "ワルシャワ", "YOR": "ヨークシャー"
};
const UNIT_TYPE_MAP = {
"A": "陸軍",
"F": "海軍"
};
// 命令翻訳関数
function localizeOrder(orderStr) {
if (!orderStr) return "";
// 正規表現で分解
// 例: "F VEN H" -> type="F", loc="VEN", rest="H"
// 例: "A PAR - BUR" -> type="A", loc="PAR", rest="- BUR"
const match = orderStr.match(/^([AF])\s+([A-Z/]{3,6})\s+(.*)$/);
if (!match) {
// WAIVE増設放棄
if (orderStr === "WAIVE") {
return "増設放棄 (WAIVE)";
}
// Build: "A BRE B" or "F BRE B"
const buildMatch = orderStr.match(/^([AF])\s+([A-Z/]{3,6})\s+B$/);
if (buildMatch) {
const bType = buildMatch[1];
const bLoc = buildMatch[2];
const unitLabel = UNIT_TYPE_MAP[bType] === '陸軍' ? '陸軍' : '海軍';
return `${PROVINCE_MAP[bLoc] || bLoc}に${unitLabel}を増設 (${orderStr})`;
}
// Disband: "A BRE D" (already handled below but just in case)
const disbandMatch = orderStr.match(/^([AF])\s+([A-Z/]{3,6})\s+D$/);
if (disbandMatch) {
const dType = disbandMatch[1];
const dLoc = disbandMatch[2];
const dUnitName = (PROVINCE_MAP[dLoc] || dLoc) + UNIT_TYPE_MAP[dType];
return `${dUnitName}を撤去 (${orderStr})`;
}
// マッチしない場合はフォールバック
return basicTranslate(orderStr);
}
const type = match[1];
const loc = match[2];
const rest = match[3];
const unitName = (PROVINCE_MAP[loc] || loc) + UNIT_TYPE_MAP[type];
// 命令内容の解析
// 1. Hold
if (rest === "H" || rest === "Hold") {
return `${unitName}:維持 (${orderStr})`;
}
// 2. Move ( - DST )
const moveMatch = rest.match(/^-\s+([A-Z]{3})$/);
if (moveMatch) {
const dest = moveMatch[1];
return `${unitName}${PROVINCE_MAP[dest] || dest}へ移動 (${orderStr})`;
}
// 3. Support ( S ... )
if (rest.startsWith("S ")) {
const targetOrder = rest.substring(2).trim(); // "A MAR - BUR" or "A MAR"
// targetOrder も再帰的あるいは簡易的に翻訳したいが、"A MAR" だけだと "マルセイユ陸軍" になる
// "A MAR - BUR" だと "マルセイユ陸軍:ブルゴーニュへ移動"
// ターゲット解析
const targetMatch = targetOrder.match(/^([AF])\s+([A-Z]{3})(.*)$/);
if (targetMatch) {
const tType = targetMatch[1];
const tLoc = targetMatch[2];
const tRest = targetMatch[3].trim();
const tUnitName = (PROVINCE_MAP[tLoc] || tLoc) + UNIT_TYPE_MAP[tType];
if (tRest === "" || tRest === "H") {
return `${unitName}${tUnitName}の維持を支援 (${orderStr})`;
} else if (tRest.startsWith("- ")) {
const tDest = tRest.substring(2).trim();
return `${unitName}${tUnitName}の${PROVINCE_MAP[tDest] || tDest}への移動を支援 (${orderStr})`;
}
}
return `${unitName}${basicTranslate(targetOrder)}を支援 (${orderStr})`;
}
// 4. Convoy ( C ... )
if (rest.startsWith("C ")) {
// C A LON - BEL
const targetOrder = rest.substring(2).trim();
const targetMatch = targetOrder.match(/^([AF])\s+([A-Z]{3})\s+-\s+([A-Z]{3})$/);
if (targetMatch) {
const tType = targetMatch[1];
const tLoc = targetMatch[2];
const tDest = targetMatch[3];
const tUnitName = (PROVINCE_MAP[tLoc] || tLoc) + UNIT_TYPE_MAP[tType];
return `${unitName}${tUnitName}の${PROVINCE_MAP[tDest] || tDest}への移動を輸送 (${orderStr})`;
}
}
// 5. Retreat ( R DST or - DST )
// 通常のMoveと同じフォーマットで来ることが多いが、文脈による
// 6. Build / Disband (これらは "Build A VEN" のような形式で来る場合と、"A VEN D" のような場合がある)
// ここでは "A VEN D" (Disband) を想定
if (rest === "D" || rest === "Disband") {
return `${unitName}:解隊 (${orderStr})`;
}
// フォールバック
return `${unitName}${basicTranslate(rest)} (${orderStr})`;
}
// Helper to wrap translation with original
function localizeOrderWithOriginal(orderStr) {
const translated = localizeOrder(orderStr);
// localizeOrder already adds original if matched, but if it returned via other paths we need to check
// Above implementation modifies all return paths? No, wait.
// Let's modify localizeOrder to be pure translation, then append.
// OR, just modify localizeOrder to always append.
return translated;
}
function basicTranslate(str) {
let res = str;
// 単純置換
for (const [code, name] of Object.entries(PROVINCE_MAP)) {
res = res.replace(new RegExp("\\b" + code + "\\b", "g"), name);
}
res = res.replace(/\bA\b/g, "陸軍");
res = res.replace(/\bF\b/g, "海軍");
res = res.replace(/\bH\b/g, "維持");
res = res.replace(/\bHold\b/g, "維持");
return res;
}
let autoRefreshInterval = null;
let currentTurnNumber = <%= @latest_turn.number %>;
let currentPhase = "<%= @latest_turn.phase %>";
const isGameFinished = <%= @game_finished.to_json %>;
const isViewingHistory = <%= (@display_turn && @display_turn.number < @latest_turn.number).to_json %>;
function refreshGameData() {
if (isGameFinished || isViewingHistory) {
console.log('Skipping auto-refresh:', isGameFinished ? 'game finished' : 'viewing history');
return;
}
fetch('<%= turn_data_game_path(@game, format: :json) %>')
.then(response => response.json())
.then(data => {
// console.log('Auto-refresh: Game data updated', data);
// 1. フェーズやターンが進んでいたらリロード
if (data.turn_number !== currentTurnNumber || data.phase !== currentPhase) {
console.log('Turn or Phase changed. Reloading page...');
window.location.reload();
return;
}
const decidedOrdersData = data.decided_orders || {};
const decidedContent = document.getElementById('decided-orders-content');
const mapContainer = document.querySelector('.game-map');
const svgOrdersData = data.svg_orders || {};
const defaultSvg = data.default_svg;
// 2. マップの更新 (現在の選択状態に合わせて)
if (mapContainer) {
const powerSelect = document.getElementById('power-select');
const currentUserPowerElement = document.getElementById('current-user-power');
let selectedPower = null;
if (currentUserPowerElement) {
selectedPower = currentUserPowerElement.value;
} else if (powerSelect) {
selectedPower = powerSelect.value;
}
// 選択された国のSVG、なければデフォルト、なければNO_ORDERS
let newSvgContent = defaultSvg;
if (selectedPower) {
// 大文字小文字の揺れを吸収してキーを探す
const normalizedPower = selectedPower.toUpperCase();
const powerKey = Object.keys(svgOrdersData).find(key => key.toUpperCase() === normalizedPower);
if (powerKey && svgOrdersData[powerKey]) {
newSvgContent = svgOrdersData[powerKey];
}
} else if (svgOrdersData["NONE"]) {
newSvgContent = svgOrdersData["NONE"];
}
// 中身が違えば更新 (ちらつき防止のため単純比較でチェック)
// SVGは大きいのでハッシュ比較などが望ましいが、簡易的に
if (mapContainer.innerHTML !== newSvgContent) {
mapContainer.innerHTML = newSvgContent;
// SVGレスポンシブ対応: 幅を100%に、高さは自動に
const svg = mapContainer.querySelector('svg');
if (svg) {
svg.removeAttribute('width');
svg.removeAttribute('height');
svg.style.width = '100%';
svg.style.height = 'auto';
svg.style.maxHeight = '70vh'; // 縦長になりすぎないように制限
}
console.log('Map updated via auto-refresh');
}
}
// 3. 国別ステータス表の更新
if (data.country_statuses) {
const table = document.getElementById('country-status-table');
if (table) {
data.country_statuses.forEach(status => {
const row = table.querySelector(`tr[data-power="${status.power}"]`);
if (row) {
// SC数、Unit数の更新 (必要なら)
// row.children[1].textContent = status.sc_count;
// row.children[2].textContent = status.unit_count;
// 提出状況の更新
const statusCell = row.querySelector('.status-cell');
if (statusCell) {
if (status.submitted) {
if (!statusCell.innerHTML.includes('bg-green-100')) {
statusCell.innerHTML = '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">完了</span>';
}
} else {
if (!statusCell.innerHTML.includes('bg-gray-100')) {
statusCell.innerHTML = '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">未完了</span>';
}
}
}
}
});
}
}
if (!decidedContent) return;
// 4. Decided Orders の更新 (既存ロジック)
// プレイヤーモードで一般ユーザーの場合
const currentUserPowerElement = document.getElementById('current-user-power');
if (currentUserPowerElement) {
const userPower = currentUserPowerElement.value;
const normalizedPower = userPower.toUpperCase();
const possibleOrdersData = data.possible_orders.possible_orders || {};
const powerKey = Object.keys(possibleOrdersData).find(key => key.toUpperCase() === normalizedPower);
if (decidedOrdersData[powerKey]) {
const powerDecidedOrders = decidedOrdersData[powerKey];
const decidedLocations = Object.keys(powerDecidedOrders);
let newHtml = '';
if (decidedLocations.length > 0) {
decidedLocations.forEach(loc => {
newHtml += `<p class="mb-1"><span class="font-semibold">・</span> ${localizeOrder(powerDecidedOrders[loc])}</p>`;
});
} else {
newHtml = '<p class="text-center text-gray-500">-</p>';
}
if (decidedContent.innerHTML !== newHtml) decidedContent.innerHTML = newHtml;
} else {
if (decidedContent.innerHTML !== '<p class="text-center text-gray-500">-</p>') {
decidedContent.innerHTML = '<p class="text-center text-gray-500">-</p>';
}
}
} else {
// 管理者モードの場合は全体の命令数を表示
const totalOrders = Object.keys(decidedOrdersData).length;
let newHtml = '';
if (totalOrders > 0) {
newHtml += `<p class="text-center text-gray-600">命令提出済み: ${totalOrders}カ国</p>`;
Object.keys(decidedOrdersData).forEach(power => {
newHtml += `<p class="mb-1 text-xs"><span class="font-semibold">${power}:</span> 提出済み</p>`;
});
} else {
newHtml = '<p class="text-center text-gray-500">-</p>';
}
if (decidedContent.innerHTML !== newHtml) decidedContent.innerHTML = newHtml;
}
// 5. 管理者用コントロール (未提出状況など) の更新
const processTurnContainer = document.getElementById('process-turn-container');
if (processTurnContainer) {
// 警告メッセージの表示制御などをここで行うには、DOM要素の再構築が必要になるため、
// 今回は簡易的に「全員提出済み」かどうかでリロードをかける、あるいは
// 警告表示エリアだけを更新するアプローチが考えられます。
// ここでは「全員提出済み」状態が変わった場合false -> trueにリロードすることで
// ボタンの活性化(警告消去)を反映させます。
// サーバー側で計算した all_orders_submitted を利用
// 直前の状態(画面ロード時)と異なればリロード
// ただし、自分が提出しただけでリロードされるとウザいので、
// 「全員提出完了」になった瞬間だけリロードするのが良いかも
// ここではシンプルに、未提出国のリストを表示する要素があれば更新する実装を追加します
const warningBox = processTurnContainer.querySelector('.bg-yellow-50');
if (data.all_orders_submitted) {
if (warningBox) {
// 警告が出ているのに全員提出済みになった -> リロードしてボタンを緑にする
console.log('All orders submitted. Reloading to update admin UI...');
window.location.reload();
}
} else {
// まだ未提出がいる場合、メッセージを更新
if (data.missing_orders_powers && data.missing_orders_powers.length > 0) {
const missingText = data.missing_orders_powers.join(', ');
// 警告ボックスがあるなら中身を更新
if (warningBox) {
const p = warningBox.querySelector('p');
if (p) {
p.innerHTML = `⚠️ 全プレイヤーの命令入力が完了していません。<br>未完了: ${missingText}`;
}
}
}
}
}
})
.catch(error => {
console.error('Auto-refresh error:', error);
});
}
function initializeGameMap() {
const powerSelect = document.getElementById('power-select');
const mapContainer = document.querySelector('.game-map');
const ordersDropdowns = document.getElementById('orders-dropdowns');
const decidedContent = document.getElementById('decided-orders-content');
const submitButtonContainer = document.getElementById('submit-button-container');
const hiddenPowerField = document.getElementById('hidden-power-field');
if (!mapContainer) {
console.log('Map container not found');
return;
}
// 既存の自動更新タイマーをクリア
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
<% if !@game_finished && !(@display_turn && @display_turn.number < @latest_turn.number) %>
// 5秒ごとに自動更新を開始 (最新ターン閲覧時のみ)
autoRefreshInterval = setInterval(refreshGameData, 5000);
console.log('Auto-refresh started (every 5 seconds)');
<% else %>
console.log('Auto-refresh disabled:', <%= @game_finished.to_json %> ? 'game finished' : 'viewing history');
<% end %>
// 初回ロード時のデータ取得と表示
const fetchUrl = '<%= raw turn_data_game_path(@game, format: :json, turn_number: @display_turn&.number, t: Time.current.to_i) %>';
console.log('Fetching game data from:', fetchUrl);
fetch(fetchUrl)
.then(response => response.json())
.then(data => {
console.log('Game data loaded for Turn:', data.turn_number);
console.log('Phase:', data.phase);
const possibleOrdersData = data.possible_orders.possible_orders || {};
const decidedOrdersData = data.decided_orders || {};
const svgOrdersData = data.svg_orders || {};
const defaultSvg = data.default_svg;
// プレイヤーモードで一般ユーザーの場合は自動的に自分の国を設定
const currentUserPowerElement = document.getElementById('current-user-power');
if (currentUserPowerElement) {
const userPower = currentUserPowerElement.value;
const normalizedPower = userPower.toUpperCase();
const powerKey = Object.keys(possibleOrdersData).find(key => key.toUpperCase() === normalizedPower);
// 自分の国のSVGを表示 (履歴モードではサーバー描画済みのSVGを保持)
if (!isViewingHistory) {
if (powerKey && svgOrdersData[powerKey]) {
mapContainer.innerHTML = svgOrdersData[powerKey];
} else {
mapContainer.innerHTML = defaultSvg;
}
}
// 自分の国の命令入力を表示
if (powerKey && possibleOrdersData[powerKey]) {
hiddenPowerField.value = powerKey;
// プルダウンを生成
const powerOrders = possibleOrdersData[powerKey];
const locations = Object.keys(powerOrders);
ordersDropdowns.innerHTML = '';
// 調整フェーズの判定と増設/撤去サマリ表示
const isAdjustmentPhase = data.phase && data.phase.endsWith('A');
if (isAdjustmentPhase && data.country_statuses) {
const myStatus = data.country_statuses.find(s => s.power.toUpperCase() === powerKey.toUpperCase());
if (myStatus) {
const diff = myStatus.sc_count - myStatus.unit_count;
const summaryDiv = document.createElement('div');
summaryDiv.className = 'col-span-2 mb-2 p-3 rounded-md text-sm';
if (diff > 0) {
summaryDiv.className += ' bg-green-50 border border-green-200 text-green-800';
summaryDiv.innerHTML = `<i class="fa-solid fa-plus-circle mr-1"></i> <span class="font-bold">${diff} ユニット増設可能</span><span class="ml-2 text-xs text-green-600">SC: ${myStatus.sc_count} / ユニット: ${myStatus.unit_count}</span>`;
} else if (diff < 0) {
summaryDiv.className += ' bg-red-50 border border-red-200 text-red-800';
summaryDiv.innerHTML = `<i class="fa-solid fa-minus-circle mr-1"></i> <span class="font-bold">${Math.abs(diff)} ユニット撤去必要</span><span class="ml-2 text-xs text-red-600">SC: ${myStatus.sc_count} / ユニット: ${myStatus.unit_count}</span>`;
} else {
summaryDiv.className += ' bg-gray-50 border border-gray-200 text-gray-600';
summaryDiv.innerHTML = `<i class="fa-solid fa-check-circle mr-1"></i> <span class="font-bold">調整不要</span><span class="ml-2 text-xs">SC: ${myStatus.sc_count} / ユニット: ${myStatus.unit_count}</span>`;
}
ordersDropdowns.appendChild(summaryDiv);
}
}
// 命令可能な地域のみフィルタリング(空配列の地域はスキップ)
const actionableLocations = locations.filter(loc => powerOrders[loc].length > 0);
actionableLocations.forEach(location => {
const orders = powerOrders[location];
const currentOrder = decidedOrdersData[powerKey] && decidedOrdersData[powerKey][location];
// location (例: "VEN") を翻訳
let locLabel = PROVINCE_MAP[location] || location;
let originalUnitCode = location;
if (orders.length > 0) {
const typeMatch = orders[0].match(/^([AF])\s+/);
if (typeMatch) {
const unitType = typeMatch[1];
locLabel += UNIT_TYPE_MAP[unitType];
originalUnitCode = unitType + " " + location;
}
}
const div = document.createElement('div');
div.className = 'flex flex-col';
const label = document.createElement('label');
label.className = 'text-xs font-medium text-gray-700 mb-1';
label.textContent = `${locLabel} (${originalUnitCode}):`;
const select = document.createElement('select');
select.name = `orders[${location}]`;
select.className = 'block w-full pl-3 pr-10 py-1.5 text-xs border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md';
// 空のオプションを追加
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = '-- 命令を選択 --';
select.appendChild(emptyOption);
// 各命令をオプションとして追加
orders.forEach(order => {
const option = document.createElement('option');
option.value = order;
// 日本語訳 + (原文)
option.textContent = localizeOrder(order);
if (currentOrder === order) {
option.selected = true;
}
select.appendChild(option);
});
div.appendChild(label);
div.appendChild(select);
ordersDropdowns.appendChild(div);
});
// 送信ボタンを表示
submitButtonContainer.classList.remove('hidden');
// Decided Ordersを表示
if (decidedOrdersData[powerKey]) {
const powerDecidedOrders = decidedOrdersData[powerKey];
const decidedLocations = Object.keys(powerDecidedOrders);
if (decidedLocations.length > 0) {
decidedContent.innerHTML = '';
decidedLocations.forEach(loc => {
const p = document.createElement('p');
p.className = 'mb-1';
// 日本語訳 + (原文)
const orderText = localizeOrder(powerDecidedOrders[loc]);
p.innerHTML = `<span class="font-semibold">・</span> ${orderText}`;
decidedContent.appendChild(p);
});
} else {
decidedContent.innerHTML = '<p class="text-center text-gray-500">-</p>';
}
} else {
decidedContent.innerHTML = '<p class="text-center text-gray-500">-</p>';
}
} else {
ordersDropdowns.innerHTML = '<p class="text-xs text-gray-500 text-center">命令がありません</p>';
decidedContent.innerHTML = '<p class="text-center text-gray-500">-</p>';
submitButtonContainer.classList.add('hidden');
}
return;
}
// 通常のプルダウン動作(管理者モードなど)
// 初期表示: 命令なしのSVGを表示 (履歴モードではサーバー描画済みのSVGを保持)
if (!isViewingHistory) {
if (svgOrdersData["NONE"]) {
mapContainer.innerHTML = svgOrdersData["NONE"];
} else {
mapContainer.innerHTML = defaultSvg;
}
}
if (powerSelect) {
powerSelect.addEventListener('change', function() {
const power = this.value;
// 画像の切り替え
if (power && svgOrdersData[power]) {
mapContainer.innerHTML = svgOrdersData[power];
} else if (svgOrdersData["NONE"]) {
mapContainer.innerHTML = svgOrdersData["NONE"];
} else {
mapContainer.innerHTML = defaultSvg;
}
const svg = mapContainer.querySelector('svg');
if (svg) {
svg.removeAttribute('width');
svg.removeAttribute('height');
svg.style.width = '100%';
svg.style.height = 'auto';
svg.style.maxHeight = '70vh';
}
});
}
// 初期表示後もスタイル適用
const svg = mapContainer.querySelector('svg');
if (svg) {
svg.removeAttribute('width');
svg.removeAttribute('height');
svg.style.width = '100%';
svg.style.height = 'auto';
svg.style.maxHeight = '70vh';
}
})
.catch(error => {
console.error('Error loading game data:', error);
});
}
// Turbo環境下のスクリプト実行:
// 1. 直接呼び出し: 初回ロードやTurboがbodyを完全に置換した場合に動作
// 2. turbo:load: Turboフォーム送信後のリダイレクトなど、スクリプトが再実行されない場合のフォールバック
//
// 重複防止フラグ: 同一ページで2回実行されるのを防ぐ
const pageLoadId = Date.now();
window._lastGameMapInitId = pageLoadId;
// 直接実行
initializeGameMap();
// turbo:load フォールバック(リスナー重複防止)
if (window._gameMapTurboListener) {
document.removeEventListener('turbo:load', window._gameMapTurboListener);
}
window._gameMapTurboListener = function() {
// 直接実行で既に初期化済みなら(同一ページ遷移でない)スキップ
if (window._lastGameMapInitId === pageLoadId) {
// 同一ページIDなら重複 → フラグを変えて次回に備える
window._lastGameMapInitId = null;
return;
}
initializeGameMap();
};
document.addEventListener('turbo:load', window._gameMapTurboListener);
// クリーンアップ処理の登録(重複防止)
if (window.gameAutoRefreshCleanup) {
document.removeEventListener('turbo:before-visit', window.gameAutoRefreshCleanup);
document.removeEventListener('beforeunload', window.gameAutoRefreshCleanup);
}
// 新しいクリーンアップ関数を定義
window.gameAutoRefreshCleanup = function() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
console.log('Auto-refresh stopped');
}
};
// リスナー登録
document.addEventListener('turbo:before-visit', window.gameAutoRefreshCleanup);
window.addEventListener('beforeunload', window.gameAutoRefreshCleanup);
</script>
<% else %>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div class="flex">
<div class="ml-3">
<p class="text-sm text-yellow-700">
<% if @game.solo_mode? %>
ソロモードのゲーム準備中...
<% elsif @game.status == 'recruiting' %>
プレイヤーの参加を待っています...
<% else %>
ゲーム準備中...
<% end %>
</p>
</div>
</div>
</div>
<% end %>