mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
WiP implementing VM integration
This commit is contained in:
@@ -2,7 +2,69 @@ require 'open3'
|
||||
|
||||
module BreakEscape
|
||||
class GamesController < ApplicationController
|
||||
before_action :set_game, only: [:show, :scenario, :scenario_map, :ink, :room, :container, :sync_state, :unlock, :inventory, :objectives, :complete_task, :update_task_progress]
|
||||
before_action :set_game, only: [:show, :scenario, :scenario_map, :ink, :room, :container, :sync_state, :unlock, :inventory, :objectives, :complete_task, :update_task_progress, :submit_flag]
|
||||
|
||||
# GET /games/new?mission_id=:id
|
||||
# Show VM set selection page for VM-required missions
|
||||
def new
|
||||
@mission = Mission.find(params[:mission_id])
|
||||
authorize @mission, :create_game? if defined?(Pundit)
|
||||
|
||||
if @mission.requires_vms?
|
||||
@available_vm_sets = @mission.valid_vm_sets_for_user(current_user)
|
||||
@existing_games = Game.where(player: current_player, mission: @mission)
|
||||
end
|
||||
end
|
||||
|
||||
# POST /games
|
||||
# Create a new game instance for a mission
|
||||
def create
|
||||
@mission = Mission.find(params[:mission_id])
|
||||
authorize @mission, :create_game? if defined?(Pundit)
|
||||
|
||||
# Build initial player_state with VM/flag context
|
||||
initial_player_state = {}
|
||||
|
||||
# Hacktivity mode with VM set
|
||||
if params[:vm_set_id].present? && defined?(::VmSet)
|
||||
vm_set = ::VmSet.find_by(id: params[:vm_set_id])
|
||||
return render json: { error: 'VM set not found' }, status: :not_found unless vm_set
|
||||
|
||||
# Validate VM set belongs to user and matches mission
|
||||
if BreakEscape::Mission.hacktivity_mode?
|
||||
unless @mission.valid_vm_sets_for_user(current_user).include?(vm_set)
|
||||
return render json: { error: 'Invalid VM set for this mission' }, status: :forbidden
|
||||
end
|
||||
initial_player_state['vm_set_id'] = vm_set.id
|
||||
else
|
||||
# Standalone mode - vm_set_id shouldn't be used
|
||||
Rails.logger.warn "[BreakEscape] vm_set_id provided but not in Hacktivity mode, ignoring"
|
||||
end
|
||||
end
|
||||
|
||||
# Standalone mode with manual flags
|
||||
if params[:standalone_flags].present?
|
||||
flags = if params[:standalone_flags].is_a?(Array)
|
||||
params[:standalone_flags]
|
||||
else
|
||||
params[:standalone_flags].split(',').map(&:strip).reject(&:blank?)
|
||||
end
|
||||
initial_player_state['standalone_flags'] = flags
|
||||
end
|
||||
|
||||
# CRITICAL: Set player_state BEFORE save! so callbacks can read vm_set_id
|
||||
# Callback order is:
|
||||
# 1. before_create :generate_scenario_data_with_context (reads player_state['vm_set_id'])
|
||||
# 2. before_create :initialize_player_state (adds default fields)
|
||||
@game = Game.new(
|
||||
player: current_player,
|
||||
mission: @mission
|
||||
)
|
||||
@game.player_state = initial_player_state
|
||||
@game.save!
|
||||
|
||||
redirect_to game_path(@game)
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @game if defined?(Pundit)
|
||||
@@ -28,6 +90,11 @@ module BreakEscape
|
||||
filtered['objectivesState'] = @game.player_state['objectivesState']
|
||||
end
|
||||
|
||||
# Include submitted flags for flag station minigame
|
||||
if @game.player_state['submitted_flags'].present?
|
||||
filtered['submittedFlags'] = @game.player_state['submitted_flags']
|
||||
end
|
||||
|
||||
render json: filtered
|
||||
rescue => e
|
||||
Rails.logger.error "[BreakEscape] scenario error: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
||||
@@ -375,6 +442,43 @@ module BreakEscape
|
||||
render json: result
|
||||
end
|
||||
|
||||
# ==========================================
|
||||
# VM/Flag Integration
|
||||
# ==========================================
|
||||
|
||||
# POST /games/:id/flags
|
||||
# Submit a CTF flag for validation
|
||||
def submit_flag
|
||||
authorize @game if defined?(Pundit)
|
||||
|
||||
flag_key = params[:flag]
|
||||
|
||||
unless flag_key.present?
|
||||
return render json: { success: false, message: 'No flag provided' }, status: :bad_request
|
||||
end
|
||||
|
||||
result = @game.submit_flag(flag_key)
|
||||
|
||||
if result[:success]
|
||||
# Find rewards for this flag in scenario
|
||||
rewards = find_flag_rewards(flag_key)
|
||||
|
||||
# Process rewards
|
||||
reward_results = process_flag_rewards(flag_key, rewards)
|
||||
|
||||
Rails.logger.info "[BreakEscape] Flag submitted: #{flag_key}, rewards: #{reward_results.length}"
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
message: result[:message],
|
||||
flag: flag_key,
|
||||
rewards: reward_results
|
||||
}
|
||||
else
|
||||
render json: result, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_game
|
||||
@@ -776,5 +880,134 @@ module BreakEscape
|
||||
def render_error(message, status)
|
||||
render json: { error: message }, status: status
|
||||
end
|
||||
|
||||
# ==========================================
|
||||
# Flag Reward Helpers
|
||||
# ==========================================
|
||||
|
||||
def find_flag_rewards(flag_key)
|
||||
rewards = []
|
||||
|
||||
# Search scenario for flag-station with this flag
|
||||
@game.scenario_data['rooms']&.each do |room_id, room|
|
||||
room['objects']&.each do |obj|
|
||||
next unless obj['type'] == 'flag-station'
|
||||
next unless obj['flags']&.any? { |f| f.downcase == flag_key.downcase }
|
||||
|
||||
flag_station_id = obj['id'] || obj['name']
|
||||
|
||||
# Support both hash structure (preferred) and array structure (legacy)
|
||||
if obj['flagRewards'].is_a?(Hash)
|
||||
# Hash structure: { "flag{key}": { "type": "unlock_door", ... } }
|
||||
# Case-insensitive lookup
|
||||
reward_key = obj['flagRewards'].keys.find { |k| k.downcase == flag_key.downcase }
|
||||
reward = obj['flagRewards'][reward_key] if reward_key
|
||||
if reward
|
||||
rewards << reward.merge(
|
||||
'flag_station_id' => flag_station_id,
|
||||
'room_id' => room_id
|
||||
)
|
||||
end
|
||||
elsif obj['flagRewards'].is_a?(Array)
|
||||
# Array structure (legacy): rewards[i] corresponds to flags[i]
|
||||
flag_index = obj['flags'].find_index { |f| f.downcase == flag_key.downcase }
|
||||
if flag_index && obj['flagRewards'][flag_index]
|
||||
rewards << obj['flagRewards'][flag_index].merge(
|
||||
'flag_station_id' => flag_station_id,
|
||||
'room_id' => room_id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
rewards
|
||||
end
|
||||
|
||||
def process_flag_rewards(flag_key, rewards)
|
||||
results = []
|
||||
|
||||
rewards.each do |reward|
|
||||
# Skip if already claimed
|
||||
if @game.player_state['flag_rewards_claimed']&.include?(flag_key)
|
||||
results << { type: 'skipped', reason: 'Already claimed' }
|
||||
next
|
||||
end
|
||||
|
||||
# Process each reward type
|
||||
case reward['type']
|
||||
when 'give_item'
|
||||
results << process_item_reward(reward, flag_key)
|
||||
|
||||
when 'unlock_door'
|
||||
results << process_door_unlock_reward(reward, flag_key)
|
||||
|
||||
when 'emit_event'
|
||||
results << process_event_reward(reward, flag_key)
|
||||
|
||||
else
|
||||
results << { type: 'unknown', data: reward }
|
||||
end
|
||||
end
|
||||
|
||||
# Mark rewards as claimed
|
||||
@game.player_state['flag_rewards_claimed'] ||= []
|
||||
@game.player_state['flag_rewards_claimed'] << flag_key
|
||||
@game.save!
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
def process_item_reward(reward, flag_key)
|
||||
# Find the flag-station object to pull item from its itemsHeld
|
||||
flag_station = find_flag_station_by_id(reward['flag_station_id'])
|
||||
|
||||
return { type: 'error', message: 'Flag station not found' } unless flag_station
|
||||
|
||||
# Get item from itemsHeld (similar to NPC item giving)
|
||||
item = flag_station['itemsHeld']&.find { |i| i['type'] == reward['item_type'] || i['name'] == reward['item_name'] }
|
||||
|
||||
return { type: 'error', message: 'Item not found in flag station' } unless item
|
||||
|
||||
# Add to player inventory
|
||||
@game.add_inventory_item!(item)
|
||||
|
||||
{ type: 'give_item', item: item, success: true }
|
||||
end
|
||||
|
||||
def process_door_unlock_reward(reward, flag_key)
|
||||
room_id = reward['room_id'] || reward['target_room']
|
||||
|
||||
return { type: 'error', message: 'No room_id specified' } unless room_id
|
||||
|
||||
# Unlock the door (same as NPC door unlock)
|
||||
@game.unlock_room!(room_id)
|
||||
|
||||
{ type: 'unlock_door', room_id: room_id, success: true }
|
||||
end
|
||||
|
||||
def process_event_reward(reward, flag_key)
|
||||
# Emit event (NPC can listen and trigger conversations)
|
||||
event_name = reward['event_name'] || "flag_submitted:#{flag_key}"
|
||||
|
||||
# Store event in player_state for client to emit
|
||||
@game.player_state['pending_events'] ||= []
|
||||
@game.player_state['pending_events'] << {
|
||||
'name' => event_name,
|
||||
'data' => { 'flag' => flag_key, 'timestamp' => Time.current.to_i }
|
||||
}
|
||||
@game.save!
|
||||
|
||||
{ type: 'emit_event', event_name: event_name, success: true }
|
||||
end
|
||||
|
||||
def find_flag_station_by_id(flag_station_id)
|
||||
@game.scenario_data['rooms']&.each do |_room_id, room|
|
||||
room['objects']&.each do |obj|
|
||||
return obj if (obj['id'] || obj['name']) == flag_station_id && obj['type'] == 'flag-station'
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,13 +20,18 @@ module BreakEscape
|
||||
@mission = Mission.find(params[:id])
|
||||
authorize @mission if defined?(Pundit)
|
||||
|
||||
# Create or find game instance for current player
|
||||
@game = Game.find_or_create_by!(
|
||||
player: current_player,
|
||||
mission: @mission
|
||||
)
|
||||
|
||||
redirect_to game_path(@game)
|
||||
if @mission.requires_vms? && BreakEscape::Mission.hacktivity_mode?
|
||||
# VM missions need explicit game creation with VM set selection
|
||||
# Redirect to games#new which shows VM set selection UI
|
||||
redirect_to new_game_path(mission_id: @mission.id)
|
||||
else
|
||||
# Legacy behavior for non-VM missions - auto-create game
|
||||
@game = Game.find_or_create_by!(
|
||||
player: current_player,
|
||||
mission: @mission
|
||||
)
|
||||
redirect_to game_path(@game)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user