2025-11-21 15:27:54 +00:00
|
|
|
require 'open3'
|
|
|
|
|
|
|
|
|
|
module BreakEscape
|
|
|
|
|
class GamesController < ApplicationController
|
2025-11-21 15:27:54 +00:00
|
|
|
before_action :set_game, only: [:show, :scenario, :ink, :room]
|
2025-11-21 15:27:54 +00:00
|
|
|
|
|
|
|
|
def show
|
|
|
|
|
authorize @game if defined?(Pundit)
|
|
|
|
|
@mission = @game.mission
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# GET /games/:id/scenario
|
|
|
|
|
# Returns scenario JSON for this game instance
|
2025-11-21 15:27:54 +00:00
|
|
|
# Filtered for lazy-loading: only metadata and connections, no room contents
|
2025-11-21 15:27:54 +00:00
|
|
|
def scenario
|
|
|
|
|
authorize @game if defined?(Pundit)
|
2025-11-21 15:27:54 +00:00
|
|
|
render json: @game.filtered_scenario_for_bootstrap
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# GET /games/:id/room/:room_id
|
|
|
|
|
# Returns room data for a specific room (lazy-loading support)
|
|
|
|
|
def room
|
|
|
|
|
authorize @game if defined?(Pundit)
|
|
|
|
|
|
|
|
|
|
room_id = params[:room_id]
|
|
|
|
|
return render_error('Missing room_id parameter', :bad_request) unless room_id.present?
|
|
|
|
|
|
|
|
|
|
# Get room data from scenario
|
|
|
|
|
room_data = @game.scenario_data['rooms']&.[](room_id)
|
|
|
|
|
return render_error("Room not found: #{room_id}", :not_found) unless room_data
|
|
|
|
|
|
|
|
|
|
Rails.logger.debug "[BreakEscape] Serving room data for: #{room_id}"
|
|
|
|
|
|
|
|
|
|
# Return room data with the room_id for client reference
|
|
|
|
|
render json: { room_id: room_id, room: room_data }
|
2025-11-21 15:27:54 +00:00
|
|
|
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?
|
|
|
|
|
|
2025-11-21 15:27:54 +00:00
|
|
|
Rails.logger.debug "[BreakEscape] Loading ink for NPC: #{npc_id}"
|
|
|
|
|
|
2025-11-21 15:27:54 +00:00
|
|
|
# Find NPC in scenario data
|
|
|
|
|
npc = find_npc_in_scenario(npc_id)
|
2025-11-21 15:27:54 +00:00
|
|
|
return render_error("NPC not found in scenario: #{npc_id}", :not_found) unless npc
|
|
|
|
|
|
|
|
|
|
Rails.logger.debug "[BreakEscape] Found NPC: #{npc['id']} with storyPath: #{npc['storyPath']}"
|
|
|
|
|
|
|
|
|
|
# Check if storyPath is set
|
|
|
|
|
unless npc['storyPath'].present?
|
|
|
|
|
Rails.logger.warn "[BreakEscape] NPC #{npc['id']} has no storyPath defined"
|
|
|
|
|
return render_error("NPC #{npc['id']} has no storyPath defined", :bad_request)
|
|
|
|
|
end
|
2025-11-21 15:27:54 +00:00
|
|
|
|
|
|
|
|
# Resolve ink file path and compile if needed
|
|
|
|
|
ink_json_path = resolve_and_compile_ink(npc['storyPath'])
|
2025-11-21 15:27:54 +00:00
|
|
|
unless ink_json_path && File.exist?(ink_json_path)
|
|
|
|
|
Rails.logger.error "[BreakEscape] Ink file not found for #{npc['storyPath']} (resolved to #{ink_json_path})"
|
|
|
|
|
return render_error("Ink script not found for #{npc['storyPath']}", :not_found)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
Rails.logger.debug "[BreakEscape] Serving ink from: #{ink_json_path}"
|
2025-11-21 15:27:54 +00:00
|
|
|
|
|
|
|
|
# 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)
|
2025-11-21 15:27:54 +00:00
|
|
|
available_npcs = []
|
|
|
|
|
@game.scenario_data['rooms']&.each do |room_id, room_data|
|
|
|
|
|
room_data['npcs']&.each do |npc|
|
|
|
|
|
available_npcs << "#{npc['id']} (#{room_id})"
|
|
|
|
|
return npc if npc['id'] == npc_id
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Log available NPCs for debugging
|
|
|
|
|
if available_npcs.any?
|
|
|
|
|
Rails.logger.debug "[BreakEscape] Available NPCs: #{available_npcs.join(', ')}"
|
|
|
|
|
else
|
|
|
|
|
Rails.logger.warn "[BreakEscape] No NPCs found in scenario data"
|
2025-11-21 15:27:54 +00:00
|
|
|
end
|
2025-11-21 15:27:54 +00:00
|
|
|
|
2025-11-21 15:27:54 +00:00
|
|
|
nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Resolve ink path and compile if necessary
|
|
|
|
|
def resolve_and_compile_ink(story_path)
|
2025-11-21 15:27:54 +00:00
|
|
|
# Use Engine root for Rails Engine context
|
|
|
|
|
engine_root = BreakEscape::Engine.root
|
|
|
|
|
base_path = engine_root.join(story_path)
|
2025-11-21 15:27:54 +00:00
|
|
|
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
|