mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
feat: Add controllers and routes
- Add MissionsController for scenario selection - Add GamesController with scenario/ink endpoints - Add JIT Ink compilation logic - Add API::GamesController for game state management - Configure routes with REST + API endpoints - Add authorization hooks (Pundit) - Add polymorphic player support (current_player helper)
This commit is contained in:
114
app/controllers/break_escape/api/games_controller.rb
Normal file
114
app/controllers/break_escape/api/games_controller.rb
Normal file
@@ -0,0 +1,114 @@
|
||||
module BreakEscape
|
||||
module Api
|
||||
class GamesController < ApplicationController
|
||||
before_action :set_game
|
||||
|
||||
# GET /games/:id/bootstrap
|
||||
# Initial game data for client
|
||||
def bootstrap
|
||||
authorize @game if defined?(Pundit)
|
||||
|
||||
render json: {
|
||||
gameId: @game.id,
|
||||
missionName: @game.mission.display_name,
|
||||
startRoom: @game.scenario_data['startRoom'],
|
||||
playerState: @game.player_state,
|
||||
roomLayout: build_room_layout
|
||||
}
|
||||
end
|
||||
|
||||
# PUT /games/:id/sync_state
|
||||
# Periodic state sync from client
|
||||
def sync_state
|
||||
authorize @game if defined?(Pundit)
|
||||
|
||||
# Update allowed fields
|
||||
if params[:currentRoom]
|
||||
@game.player_state['currentRoom'] = params[:currentRoom]
|
||||
end
|
||||
|
||||
if params[:globalVariables]
|
||||
@game.update_global_variables!(params[:globalVariables].to_unsafe_h)
|
||||
end
|
||||
|
||||
@game.save!
|
||||
|
||||
render json: { success: true }
|
||||
end
|
||||
|
||||
# POST /games/:id/unlock
|
||||
# Validate unlock attempt
|
||||
def unlock
|
||||
authorize @game if defined?(Pundit)
|
||||
|
||||
target_type = params[:targetType]
|
||||
target_id = params[:targetId]
|
||||
attempt = params[:attempt]
|
||||
method = params[:method]
|
||||
|
||||
is_valid = @game.validate_unlock(target_type, target_id, attempt, method)
|
||||
|
||||
if is_valid
|
||||
if target_type == 'door'
|
||||
@game.unlock_room!(target_id)
|
||||
room_data = @game.filtered_room_data(target_id)
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
type: 'door',
|
||||
roomData: room_data
|
||||
}
|
||||
else
|
||||
@game.unlock_object!(target_id)
|
||||
render json: {
|
||||
success: true,
|
||||
type: 'object'
|
||||
}
|
||||
end
|
||||
else
|
||||
render json: {
|
||||
success: false,
|
||||
message: 'Invalid attempt'
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# POST /games/:id/inventory
|
||||
# Update inventory
|
||||
def inventory
|
||||
authorize @game if defined?(Pundit)
|
||||
|
||||
action = params[:action]
|
||||
item = params[:item]
|
||||
|
||||
case action
|
||||
when 'add'
|
||||
@game.add_inventory_item!(item.to_unsafe_h)
|
||||
render json: { success: true, inventory: @game.player_state['inventory'] }
|
||||
when 'remove'
|
||||
@game.remove_inventory_item!(item['id'])
|
||||
render json: { success: true, inventory: @game.player_state['inventory'] }
|
||||
else
|
||||
render json: { success: false, message: 'Invalid action' }, status: :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_game
|
||||
@game = Game.find(params[:id])
|
||||
end
|
||||
|
||||
def build_room_layout
|
||||
layout = {}
|
||||
@game.scenario_data['rooms'].each do |room_id, room_data|
|
||||
layout[room_id] = {
|
||||
connections: room_data['connections'],
|
||||
locked: room_data['locked'] || false
|
||||
}
|
||||
end
|
||||
layout
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,30 @@
|
||||
module BreakEscape
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
# Include Pundit if available
|
||||
include Pundit::Authorization if defined?(Pundit)
|
||||
|
||||
# Helper method to get current player (polymorphic)
|
||||
def current_player
|
||||
if BreakEscape.standalone_mode?
|
||||
# Standalone mode - get/create demo user
|
||||
@current_player ||= DemoUser.first_or_create!(handle: 'demo_player')
|
||||
else
|
||||
# Mounted mode - use Hacktivity's current_user
|
||||
current_user
|
||||
end
|
||||
end
|
||||
helper_method :current_player
|
||||
|
||||
# Handle authorization errors
|
||||
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||
|
||||
private
|
||||
|
||||
def user_not_authorized
|
||||
flash[:alert] = "You are not authorized to perform this action."
|
||||
redirect_to(request.referrer || root_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
119
app/controllers/break_escape/games_controller.rb
Normal file
119
app/controllers/break_escape/games_controller.rb
Normal file
@@ -0,0 +1,119 @@
|
||||
require 'open3'
|
||||
|
||||
module BreakEscape
|
||||
class GamesController < ApplicationController
|
||||
before_action :set_game, only: [:show, :scenario, :ink]
|
||||
|
||||
def show
|
||||
authorize @game if defined?(Pundit)
|
||||
@mission = @game.mission
|
||||
end
|
||||
|
||||
# GET /games/:id/scenario
|
||||
# Returns scenario JSON for this game instance
|
||||
def scenario
|
||||
authorize @game if defined?(Pundit)
|
||||
render json: @game.scenario_data
|
||||
end
|
||||
|
||||
# GET /games/:id/ink?npc=helper1
|
||||
# Returns NPC script (JIT compiled if needed)
|
||||
def ink
|
||||
authorize @game if defined?(Pundit)
|
||||
|
||||
npc_id = params[:npc]
|
||||
return render_error('Missing npc parameter', :bad_request) unless npc_id.present?
|
||||
|
||||
# Find NPC in scenario data
|
||||
npc = find_npc_in_scenario(npc_id)
|
||||
return render_error('NPC not found in scenario', :not_found) unless npc
|
||||
|
||||
# Resolve ink file path and compile if needed
|
||||
ink_json_path = resolve_and_compile_ink(npc['storyPath'])
|
||||
return render_error('Ink script not found', :not_found) unless ink_json_path && File.exist?(ink_json_path)
|
||||
|
||||
# Serve compiled JSON
|
||||
render json: JSON.parse(File.read(ink_json_path))
|
||||
rescue JSON::ParserError => e
|
||||
render_error("Invalid JSON in compiled ink: #{e.message}", :internal_server_error)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_game
|
||||
@game = Game.find(params[:id])
|
||||
end
|
||||
|
||||
def find_npc_in_scenario(npc_id)
|
||||
@game.scenario_data['rooms']&.each do |_room_id, room_data|
|
||||
npc = room_data['npcs']&.find { |n| n['id'] == npc_id }
|
||||
return npc if npc
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# Resolve ink path and compile if necessary
|
||||
def resolve_and_compile_ink(story_path)
|
||||
base_path = Rails.root.join(story_path)
|
||||
json_path = find_compiled_json(base_path)
|
||||
ink_path = find_ink_source(base_path)
|
||||
|
||||
if ink_path && needs_compilation?(ink_path, json_path)
|
||||
Rails.logger.info "[BreakEscape] Compiling #{File.basename(ink_path)}..."
|
||||
json_path = compile_ink(ink_path)
|
||||
end
|
||||
|
||||
json_path
|
||||
end
|
||||
|
||||
def find_compiled_json(base_path)
|
||||
return base_path if File.exist?(base_path)
|
||||
|
||||
ink_json_path = base_path.to_s.gsub(/\.json$/, '.ink.json')
|
||||
return Pathname.new(ink_json_path) if File.exist?(ink_json_path)
|
||||
|
||||
json_path = base_path.to_s.gsub(/\.ink\.json$/, '.json')
|
||||
return Pathname.new(json_path) if File.exist?(json_path)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def find_ink_source(base_path)
|
||||
ink_path = base_path.to_s.gsub(/\.(ink\.)?json$/, '.ink')
|
||||
File.exist?(ink_path) ? Pathname.new(ink_path) : nil
|
||||
end
|
||||
|
||||
def needs_compilation?(ink_path, json_path)
|
||||
return true unless json_path && File.exist?(json_path)
|
||||
File.mtime(ink_path) > File.mtime(json_path)
|
||||
end
|
||||
|
||||
def compile_ink(ink_path)
|
||||
output_path = ink_path.to_s.gsub(/\.ink$/, '.json')
|
||||
inklecate_path = Rails.root.join('bin', 'inklecate')
|
||||
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
inklecate_path.to_s,
|
||||
'-o', output_path,
|
||||
ink_path.to_s
|
||||
)
|
||||
|
||||
unless status.success?
|
||||
Rails.logger.error "[BreakEscape] Ink compilation failed: #{stderr}"
|
||||
raise "Ink compilation failed for #{File.basename(ink_path)}: #{stderr}"
|
||||
end
|
||||
|
||||
if stderr.present?
|
||||
Rails.logger.warn "[BreakEscape] Ink compilation warnings: #{stderr}"
|
||||
end
|
||||
|
||||
Rails.logger.info "[BreakEscape] Compiled #{File.basename(ink_path)} (#{(File.size(output_path) / 1024.0).round(2)} KB)"
|
||||
|
||||
Pathname.new(output_path)
|
||||
end
|
||||
|
||||
def render_error(message, status)
|
||||
render json: { error: message }, status: status
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/controllers/break_escape/missions_controller.rb
Normal file
24
app/controllers/break_escape/missions_controller.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module BreakEscape
|
||||
class MissionsController < ApplicationController
|
||||
def index
|
||||
@missions = if defined?(Pundit)
|
||||
policy_scope(Mission)
|
||||
else
|
||||
Mission.published
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@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)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user