From 9876743bcff5ae5fd7a807510bc7936bb397163e Mon Sep 17 00:00:00 2001 From: Docker VM Date: Sun, 28 Jul 2024 18:06:05 -0400 Subject: [PATCH] Working palette editor --- app/Helpers/TwitchHelper.php | 3 +- .../Controllers/AdministrationController.php | 61 ++++ app/Http/Controllers/CommandController.php | 53 ++++ .../Controllers/CurrentStateController.php | 21 ++ .../Controllers/GameSessionController.php | 48 ++- app/Http/Controllers/PaletteController.php | 81 ++++- app/Http/Controllers/TwitchController.php | 60 ++-- app/Models/CommandHistory.php | 19 +- app/Models/CurrentState.php | 17 +- app/Models/GameSession.php | 22 +- app/Models/TwitchUser.php | 7 +- app/Models/WinningState.php | 23 ++ ...28_add_ended_at_to_game_sessions_table.php | 36 --- ...7_28_031242_create_twitch_users_table.php} | 2 +- ...24_07_28_031243_create_palettes_table.php} | 8 +- ...28_031244_create_palette_colors_table.php} | 19 +- ..._28_031245_create_game_sessions_table.php} | 3 + ...8_031246_create_command_history_table.php} | 19 +- ..._28_031247_create_current_state_table.php} | 13 +- ...7_28_031248_create_winning_state_table.php | 25 ++ resources/css/administration.css | 0 resources/css/palette-editor.css | 13 +- resources/js/administration.js | 0 resources/js/grid.js | 41 +-- resources/js/palette-editor.js | 296 ++++++++++++++++-- resources/js/twitch.js | 7 +- .../views/administration/index.blade.php | 87 +++++ .../layouts/administration-layout.blade.php | 24 ++ .../views/layouts/palette-layout.blade.php | 3 +- resources/views/palette-editor.blade.php | 99 ++++-- routes/web.php | 35 ++- vite.config.js | 2 +- 32 files changed, 920 insertions(+), 227 deletions(-) create mode 100644 app/Http/Controllers/AdministrationController.php create mode 100644 app/Http/Controllers/CommandController.php create mode 100644 app/Http/Controllers/CurrentStateController.php create mode 100644 app/Models/WinningState.php delete mode 100644 database/migrations/2024_07_20_214228_add_ended_at_to_game_sessions_table.php rename database/migrations/{2024_07_20_201600_create_twitch_users_table.php => 2024_07_28_031242_create_twitch_users_table.php} (88%) mode change 100755 => 100644 rename database/migrations/{2024_07_20_201604_create_source_images_table.php => 2024_07_28_031243_create_palettes_table.php} (57%) mode change 100755 => 100644 rename database/migrations/{2024_07_21_033036_create_palettes_table.php => 2024_07_28_031244_create_palette_colors_table.php} (80%) rename database/migrations/{2024_07_20_201601_create_game_sessions_table.php => 2024_07_28_031245_create_game_sessions_table.php} (70%) mode change 100755 => 100644 rename database/migrations/{2024_07_20_201603_create_command_history_table.php => 2024_07_28_031246_create_command_history_table.php} (57%) mode change 100755 => 100644 rename database/migrations/{2024_07_20_201602_create_current_state_table.php => 2024_07_28_031247_create_current_state_table.php} (75%) mode change 100755 => 100644 create mode 100644 database/migrations/2024_07_28_031248_create_winning_state_table.php create mode 100644 resources/css/administration.css create mode 100644 resources/js/administration.js create mode 100644 resources/views/administration/index.blade.php create mode 100644 resources/views/layouts/administration-layout.blade.php diff --git a/app/Helpers/TwitchHelper.php b/app/Helpers/TwitchHelper.php index 3b4ba30..ce109b9 100755 --- a/app/Helpers/TwitchHelper.php +++ b/app/Helpers/TwitchHelper.php @@ -11,7 +11,8 @@ public static function parseMessage($message) Log::debug('TwitchHelper::parseMessage', ['message' => $message]); // Match commands like !place A 1 red, !p A 1 red, !paint A 1 red - $commandPattern = '/^(?:!place|!p|!paint)\s([A-P])\s(\d{1,2})\s(\w+)$/i'; + // Updated regex to handle hyphenated color names + $commandPattern = '/^(?:!place|!p|!paint)\s([A-P])\s(\d{1,2})\s([\w-]+)$/i'; if (preg_match($commandPattern, $message, $matches)) { Log::debug('TwitchHelper::parseMessage - Command matched', ['matches' => $matches]); diff --git a/app/Http/Controllers/AdministrationController.php b/app/Http/Controllers/AdministrationController.php new file mode 100644 index 0000000..8a88959 --- /dev/null +++ b/app/Http/Controllers/AdministrationController.php @@ -0,0 +1,61 @@ +with('palette')->latest()->first(); + $currentState = $currentGame ? CurrentState::where('game_session_id', $currentGame->id)->with('color')->get() : []; + + return view('administration.index', compact('palettes', 'currentGame', 'currentState')); + } + + public function changePalette(Request $request) + { + $request->validate([ + 'palette_id' => 'required|exists:palettes,id', + ]); + + $currentGame = GameSession::whereNull('ended_at')->latest()->first(); + if ($currentGame) { + $currentGame->palette_id = $request->palette_id; + $currentGame->save(); + return redirect()->route('administration.index')->with('success', 'Palette changed successfully.'); + } + + return redirect()->route('administration.index')->with('error', 'No active game session.'); + } + + public function createGame(Request $request) + { + $request->validate([ + 'session_name' => 'required|string|max:255', + 'palette_id' => 'required|exists:palettes,id', + ]); + + // End the current game session if one is active + $currentGame = GameSession::whereNull('ended_at')->latest()->first(); + if ($currentGame) { + $currentGame->update(['ended_at' => now()]); + } + + // Create the new game session + $newGame = GameSession::create([ + 'session_name' => $request->session_name, + 'palette_id' => $request->palette_id, + ]); + + // Reset the board for the new game session + CurrentState::where('game_session_id', $newGame->id)->delete(); + + return redirect()->route('administration.index')->with('success', 'New game session created and board reset.'); + } +} diff --git a/app/Http/Controllers/CommandController.php b/app/Http/Controllers/CommandController.php new file mode 100644 index 0000000..85ed7c9 --- /dev/null +++ b/app/Http/Controllers/CommandController.php @@ -0,0 +1,53 @@ +validate([ + 'username' => 'required|string', + 'x' => 'required|string', + 'y' => 'required|string', + 'color' => 'required|string', + ]); + + $user = TwitchUser::firstOrCreate(['username' => $request->username]); + $currentGame = GameSession::whereNull('ended_at')->latest()->first(); + if (!$currentGame) { + return response()->json(['error' => 'No active game session'], 404); + } + + $color = PaletteColor::where('hex_value', $request->color)->first(); + if (!$color) { + return response()->json(['error' => 'Invalid color'], 400); + } + + $command = CommandHistory::create([ + 'twitch_user_id' => $user->id, + 'game_session_id' => $currentGame->id, + 'x' => $request->x, + 'y' => $request->y, + 'palette_color_id' => $color->id, + 'timestamp' => now(), + ]); + + CurrentState::updateOrCreate( + ['game_session_id' => $currentGame->id, 'x' => $request->x, 'y' => $request->y], + ['palette_color_id' => $color->id, 'updated_by' => $user->id, 'timestamp' => now()] + ); + + // Logic to check if the current state matches the winning state + // ... + + return response()->json($command); + } +} diff --git a/app/Http/Controllers/CurrentStateController.php b/app/Http/Controllers/CurrentStateController.php new file mode 100644 index 0000000..0052316 --- /dev/null +++ b/app/Http/Controllers/CurrentStateController.php @@ -0,0 +1,21 @@ +latest()->first(); + if (!$currentGame) { + return response()->json(['error' => 'No active game session'], 404); + } + + $currentState = CurrentState::where('game_session_id', $currentGame->id)->with('color')->get(); + return response()->json($currentState); + } +} diff --git a/app/Http/Controllers/GameSessionController.php b/app/Http/Controllers/GameSessionController.php index 535a3c2..fbf637c 100644 --- a/app/Http/Controllers/GameSessionController.php +++ b/app/Http/Controllers/GameSessionController.php @@ -4,33 +4,53 @@ use Illuminate\Http\Request; use App\Models\GameSession; +use App\Models\Palette; +use App\Models\WinningState; class GameSessionController extends Controller { public function start(Request $request) { - $sessionName = $request->input('session_name'); - $session = GameSession::create(['session_name' => $sessionName]); + $request->validate([ + 'session_name' => 'required|string|max:255', + 'palette_id' => 'required|exists:palettes,id', + 'image_path' => 'required|string' + ]); - return response()->json($session); + $gameSession = GameSession::create([ + 'session_name' => $request->session_name, + 'palette_id' => $request->palette_id, + 'image_path' => $request->image_path, + ]); + + // Logic to initialize the winning state based on the provided image + // ... + + return response()->json($gameSession); } - public function end(Request $request) + public function end(Request $request, $id) { - $sessionId = $request->input('session_id'); - $session = GameSession::find($sessionId); + $gameSession = GameSession::findOrFail($id); + $gameSession->update(['ended_at' => now()]); - if ($session) { - $session->update(['ended_at' => now()]); - return response()->json($session); - } - - return response()->json(['error' => 'Session not found'], 404); + return response()->json($gameSession); } public function current() { - $session = GameSession::whereNull('ended_at')->latest()->first(); - return response()->json($session); + $currentGame = GameSession::whereNull('ended_at')->latest()->first(); + return response()->json($currentGame); + } + + public function getCurrentGameSession() + { + $gameSession = GameSession::whereNull('ended_at')->latest()->first(); + + if (!$gameSession) { + $gameSession = GameSession::create(['session_name' => 'Session ' . now(), 'palette_id' => 1, 'image_path' => '']); + } + + return $gameSession; } } diff --git a/app/Http/Controllers/PaletteController.php b/app/Http/Controllers/PaletteController.php index 2a023e2..eca91c1 100644 --- a/app/Http/Controllers/PaletteController.php +++ b/app/Http/Controllers/PaletteController.php @@ -5,12 +5,19 @@ use Illuminate\Http\Request; use App\Models\Palette; use App\Models\PaletteColor; +use Illuminate\Support\Facades\Storage; class PaletteController extends Controller { - public function index() + public function index(Request $request) { $palettes = Palette::all(); + + // Check if it's an AJAX request + if ($request->ajax()) { + return response()->json($palettes); + } + return view('palette-editor', compact('palettes')); } @@ -57,6 +64,10 @@ public function updateColor(Request $request, Palette $palette, PaletteColor $co public function deleteColor(Palette $palette, PaletteColor $color) { + if ($palette->name === 'default-colors') { + return response()->json(['message' => 'Cannot delete colors from the default palette'], 403); + } + $color->delete(); return response()->json(['message' => 'Color deleted']); } @@ -65,4 +76,72 @@ public function getColors(Palette $palette) { return response()->json($palette->colors); } + + public function export(Palette $palette) + { + $palette->load('colors'); // Load the colors relationship + $fileName = $palette->name . '.pal'; + $content = $palette->colors->map(function($color) { + return [ + 'name' => $color->name, + 'hex_value' => $color->hex_value + ]; + })->toArray(); + $jsonContent = json_encode($content, JSON_PRETTY_PRINT); // Format JSON for readability + + return response($jsonContent, 200) + ->header('Content-Type', 'application/json') + ->header('Content-Disposition', 'attachment; filename="'.$fileName.'"'); + } + + public function import(Request $request) + { + $request->validate([ + 'file' => 'required|file|mimes:json,txt' + ]); + + $file = $request->file('file'); + $content = json_decode(file_get_contents($file), true); + + $paletteName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); + $palette = Palette::firstOrCreate(['name' => $paletteName]); + + foreach ($content as $color) { + PaletteColor::updateOrCreate( + ['palette_id' => $palette->id, 'name' => $color['name']], + ['hex_value' => $color['hex_value']] + ); + } + + return response()->json(['message' => 'Palette imported successfully', 'palette' => $palette]); + } + + public function rename(Request $request, Palette $palette) + { + if ($palette->name === 'default-colors') { + return response()->json(['message' => 'Cannot rename the default palette'], 403); + } + + $request->validate([ + 'name' => 'required|unique:palettes,name', + ]); + + $palette->name = $request->name; + $palette->save(); + + return response()->json($palette); + } + + + public function delete(Palette $palette) + { + if ($palette->name === 'default-colors') { + return response()->json(['message' => 'Cannot delete the default palette'], 403); + } + + $palette->colors()->delete(); + $palette->delete(); + + return response()->json(['message' => 'Palette deleted']); + } } diff --git a/app/Http/Controllers/TwitchController.php b/app/Http/Controllers/TwitchController.php index d0cbc60..3a9ad36 100755 --- a/app/Http/Controllers/TwitchController.php +++ b/app/Http/Controllers/TwitchController.php @@ -7,7 +7,8 @@ use App\Models\TwitchUser; use App\Models\CommandHistory; use App\Models\CurrentState; -use App\Models\GameSession; +use App\Models\PaletteColor; +use App\Http\Controllers\GameSessionController; use Illuminate\Support\Facades\Log; class TwitchController extends Controller @@ -17,7 +18,7 @@ public function parseChatMessage(Request $request) Log::info("Entered parseChatMessage"); $message = $request->input('message'); $username = $request->input('username'); - $gameSession = $this->getCurrentGameSession(); + $gameSession = app(GameSessionController::class)->getCurrentGameSession(); Log::debug('TwitchController::parseChatMessage', [ 'message' => $message, @@ -34,67 +35,74 @@ public function parseChatMessage(Request $request) return response()->json(['error' => 'No message, username, or valid game session provided'], 400); } - $user = TwitchUser::firstOrCreate(['twitch_username' => $username]); + $user = TwitchUser::firstOrCreate(['username' => $username]); $parsedCommand = TwitchHelper::parseMessage($message); Log::debug('TwitchController::parseChatMessage - Parsed Command', ['parsedCommand' => $parsedCommand]); if ($parsedCommand) { + $color = $this->getColorFromName($parsedCommand['color'], $gameSession->palette_id); + if (!$color) { + return response()->json(['error' => 'Invalid color'], 400); + } + CommandHistory::create([ - 'user_id' => $user->id, + 'twitch_user_id' => $user->id, 'command' => $parsedCommand['command'], 'x' => $parsedCommand['x'] ?? null, 'y' => $parsedCommand['y'] ?? null, - 'color' => $parsedCommand['color'] ?? null, + 'palette_color_id' => $color->id, 'game_session_id' => $gameSession->id, ]); switch ($parsedCommand['command']) { case 'place': - $this->handlePlaceCommand($parsedCommand, $user->id); + $this->handlePlaceCommand($parsedCommand, $user->id, $gameSession->id, $color->id); break; // Add cases for other commands like 'row', 'column', 'fill' } - return response()->json($parsedCommand); + return response()->json([ + 'command' => $parsedCommand, + 'color' => [ + 'hex_value' => $color->hex_value + ] + ]); } Log::debug('TwitchController::parseChatMessage - Invalid command', ['message' => $message]); return response()->json(['error' => 'Invalid command'], 400); } - private function handlePlaceCommand($parsedCommand, $userId) + private function handlePlaceCommand($parsedCommand, $userId, $gameSessionId, $colorId) { Log::debug('TwitchController::handlePlaceCommand', ['command' => $parsedCommand]); $x = $parsedCommand['x']; $y = $parsedCommand['y']; - $color = $parsedCommand['color']; - $currentState = CurrentState::where('x', $x)->where('y', $y)->first(); + $currentState = CurrentState::where('x', $x) + ->where('y', $y) + ->where('game_session_id', $gameSessionId) + ->first(); Log::debug('TwitchController::handlePlaceCommand - Retrieved Current State', ['currentState' => $currentState]); if ($currentState) { - $currentState->update(['color' => $color, 'updated_by' => $userId]); + $currentState->update(['palette_color_id' => $colorId, 'updated_by' => $userId, 'updated_at' => now()]); } else { - CurrentState::create(['x' => $x, 'y' => $y, 'color' => $color, 'updated_by' => $userId]); + CurrentState::create([ + 'x' => $x, + 'y' => $y, + 'palette_color_id' => $colorId, + 'updated_by' => $userId, + 'updated_at' => now(), + 'game_session_id' => $gameSessionId, + ]); } } - private function getCurrentGameSession() + private function getColorFromName($colorName, $paletteId) { - $gameSession = GameSession::whereNull('ended_at')->latest()->first(); - - if (!$gameSession) { - $gameSession = GameSession::create(['session_name' => 'Session ' . now()]); - } - - return $gameSession; - } - - public function getCurrentState() - { - $currentState = CurrentState::all(); - return response()->json($currentState); + return PaletteColor::where('palette_id', $paletteId)->where('name', $colorName)->first(); } } diff --git a/app/Models/CommandHistory.php b/app/Models/CommandHistory.php index f8eeb58..602e187 100755 --- a/app/Models/CommandHistory.php +++ b/app/Models/CommandHistory.php @@ -9,7 +9,22 @@ class CommandHistory extends Model { use HasFactory; - protected $table = 'command_history'; + protected $table = 'command_history'; // Specify the correct table name - protected $fillable = ['user_id', 'command', 'x', 'y', 'color', 'game_session_id']; + protected $fillable = ['twitch_user_id', 'game_session_id', 'x', 'y', 'palette_color_id', 'timestamp']; + + public function user() + { + return $this->belongsTo(TwitchUser::class); + } + + public function gameSession() + { + return $this->belongsTo(GameSession::class); + } + + public function color() + { + return $this->belongsTo(PaletteColor::class); + } } diff --git a/app/Models/CurrentState.php b/app/Models/CurrentState.php index d9ed5b3..afd0437 100755 --- a/app/Models/CurrentState.php +++ b/app/Models/CurrentState.php @@ -11,5 +11,20 @@ class CurrentState extends Model protected $table = 'current_state'; - protected $fillable = ['x', 'y', 'color', 'updated_by']; + protected $fillable = ['game_session_id', 'x', 'y', 'palette_color_id', 'updated_by', 'updated_at']; + + public function gameSession() + { + return $this->belongsTo(GameSession::class); + } + + public function color() + { + return $this->belongsTo(PaletteColor::class, 'palette_color_id'); + } + + public function updatedBy() + { + return $this->belongsTo(TwitchUser::class, 'updated_by'); + } } diff --git a/app/Models/GameSession.php b/app/Models/GameSession.php index da29b44..a6d21d2 100755 --- a/app/Models/GameSession.php +++ b/app/Models/GameSession.php @@ -9,7 +9,25 @@ class GameSession extends Model { use HasFactory; - protected $fillable = ['session_name', 'ended_at']; + protected $fillable = ['session_name', 'palette_id', 'image_path', 'ended_at']; - protected $dates = ['ended_at']; + public function palette() + { + return $this->belongsTo(Palette::class); + } + + public function commands() + { + return $this->hasMany(CommandHistory::class); + } + + public function currentState() + { + return $this->hasMany(CurrentState::class); + } + + public function winningState() + { + return $this->hasMany(WinningState::class); + } } diff --git a/app/Models/TwitchUser.php b/app/Models/TwitchUser.php index bb232cf..e5b1ca2 100755 --- a/app/Models/TwitchUser.php +++ b/app/Models/TwitchUser.php @@ -9,5 +9,10 @@ class TwitchUser extends Model { use HasFactory; - protected $fillable = ['twitch_username']; + protected $fillable = ['username']; + + public function commands() + { + return $this->hasMany(CommandHistory::class); + } } diff --git a/app/Models/WinningState.php b/app/Models/WinningState.php new file mode 100644 index 0000000..57a76be --- /dev/null +++ b/app/Models/WinningState.php @@ -0,0 +1,23 @@ +belongsTo(GameSession::class); + } + + public function color() + { + return $this->belongsTo(PaletteColor::class); + } +} diff --git a/database/migrations/2024_07_20_214228_add_ended_at_to_game_sessions_table.php b/database/migrations/2024_07_20_214228_add_ended_at_to_game_sessions_table.php deleted file mode 100644 index 9ebb6ef..0000000 --- a/database/migrations/2024_07_20_214228_add_ended_at_to_game_sessions_table.php +++ /dev/null @@ -1,36 +0,0 @@ -timestamp('ended_at')->nullable(); - } - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('game_sessions', function (Blueprint $table) { - if (Schema::hasColumn('game_sessions', 'ended_at')) { - $table->dropColumn('ended_at'); - } - }); - } -} diff --git a/database/migrations/2024_07_20_201600_create_twitch_users_table.php b/database/migrations/2024_07_28_031242_create_twitch_users_table.php old mode 100755 new mode 100644 similarity index 88% rename from database/migrations/2024_07_20_201600_create_twitch_users_table.php rename to database/migrations/2024_07_28_031242_create_twitch_users_table.php index 6a2c9d4..8d20941 --- a/database/migrations/2024_07_20_201600_create_twitch_users_table.php +++ b/database/migrations/2024_07_28_031242_create_twitch_users_table.php @@ -10,7 +10,7 @@ public function up() { Schema::create('twitch_users', function (Blueprint $table) { $table->id(); - $table->string('twitch_username')->unique(); + $table->string('username')->unique(); $table->timestamps(); }); } diff --git a/database/migrations/2024_07_20_201604_create_source_images_table.php b/database/migrations/2024_07_28_031243_create_palettes_table.php old mode 100755 new mode 100644 similarity index 57% rename from database/migrations/2024_07_20_201604_create_source_images_table.php rename to database/migrations/2024_07_28_031243_create_palettes_table.php index ac562e3..a74ac1a --- a/database/migrations/2024_07_20_201604_create_source_images_table.php +++ b/database/migrations/2024_07_28_031243_create_palettes_table.php @@ -4,21 +4,19 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateSourceImagesTable extends Migration +class CreatePalettesTable extends Migration { public function up() { - Schema::create('source_images', function (Blueprint $table) { + Schema::create('palettes', function (Blueprint $table) { $table->id(); $table->string('name'); - $table->json('pixel_state'); - $table->json('palette'); $table->timestamps(); }); } public function down() { - Schema::dropIfExists('source_images'); + Schema::dropIfExists('palettes'); } } diff --git a/database/migrations/2024_07_21_033036_create_palettes_table.php b/database/migrations/2024_07_28_031244_create_palette_colors_table.php similarity index 80% rename from database/migrations/2024_07_21_033036_create_palettes_table.php rename to database/migrations/2024_07_28_031244_create_palette_colors_table.php index 93cf4c8..7abe844 100644 --- a/database/migrations/2024_07_21_033036_create_palettes_table.php +++ b/database/migrations/2024_07_28_031244_create_palette_colors_table.php @@ -4,21 +4,10 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreatePalettesTable extends Migration +class CreatePaletteColorsTable extends Migration { - /** - * Run the migrations. - * - * @return void - */ public function up() { - Schema::create('palettes', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->timestamps(); - }); - Schema::create('palette_colors', function (Blueprint $table) { $table->id(); $table->foreignId('palette_id')->constrained()->onDelete('cascade'); @@ -65,14 +54,8 @@ public function up() } } - /** - * Reverse the migrations. - * - * @return void - */ public function down() { Schema::dropIfExists('palette_colors'); - Schema::dropIfExists('palettes'); } } diff --git a/database/migrations/2024_07_20_201601_create_game_sessions_table.php b/database/migrations/2024_07_28_031245_create_game_sessions_table.php old mode 100755 new mode 100644 similarity index 70% rename from database/migrations/2024_07_20_201601_create_game_sessions_table.php rename to database/migrations/2024_07_28_031245_create_game_sessions_table.php index 3956163..2c6b317 --- a/database/migrations/2024_07_20_201601_create_game_sessions_table.php +++ b/database/migrations/2024_07_28_031245_create_game_sessions_table.php @@ -11,6 +11,9 @@ public function up() Schema::create('game_sessions', function (Blueprint $table) { $table->id(); $table->string('session_name'); + $table->foreignId('palette_id')->constrained('palettes')->onDelete('cascade')->default(1); + $table->string('image_path')->nullable(); + $table->timestamp('ended_at')->nullable(); $table->timestamps(); }); } diff --git a/database/migrations/2024_07_20_201603_create_command_history_table.php b/database/migrations/2024_07_28_031246_create_command_history_table.php old mode 100755 new mode 100644 similarity index 57% rename from database/migrations/2024_07_20_201603_create_command_history_table.php rename to database/migrations/2024_07_28_031246_create_command_history_table.php index 2bd5db7..a983826 --- a/database/migrations/2024_07_20_201603_create_command_history_table.php +++ b/database/migrations/2024_07_28_031246_create_command_history_table.php @@ -6,30 +6,19 @@ class CreateCommandHistoryTable extends Migration { - /** - * Run the migrations. - * - * @return void - */ public function up() { Schema::create('command_history', function (Blueprint $table) { $table->id(); - $table->foreignId('user_id')->constrained('twitch_users')->onDelete('cascade'); - $table->string('command'); - $table->string('x')->nullable(); - $table->string('y')->nullable(); - $table->string('color')->nullable(); + $table->foreignId('twitch_user_id')->constrained('twitch_users')->onDelete('cascade'); $table->foreignId('game_session_id')->constrained('game_sessions')->onDelete('cascade'); + $table->string('x'); + $table->string('y'); + $table->foreignId('palette_color_id')->constrained('palette_colors')->onDelete('cascade'); $table->timestamps(); }); } - /** - * Reverse the migrations. - * - * @return void - */ public function down() { Schema::dropIfExists('command_history'); diff --git a/database/migrations/2024_07_20_201602_create_current_state_table.php b/database/migrations/2024_07_28_031247_create_current_state_table.php old mode 100755 new mode 100644 similarity index 75% rename from database/migrations/2024_07_20_201602_create_current_state_table.php rename to database/migrations/2024_07_28_031247_create_current_state_table.php index 60b438c..552c4e8 --- a/database/migrations/2024_07_20_201602_create_current_state_table.php +++ b/database/migrations/2024_07_28_031247_create_current_state_table.php @@ -6,28 +6,19 @@ class CreateCurrentStateTable extends Migration { - /** - * Run the migrations. - * - * @return void - */ public function up() { Schema::create('current_state', function (Blueprint $table) { $table->id(); + $table->foreignId('game_session_id')->constrained('game_sessions')->onDelete('cascade'); $table->string('x'); $table->string('y'); - $table->string('color'); + $table->foreignId('palette_color_id')->constrained('palette_colors')->onDelete('cascade'); $table->foreignId('updated_by')->constrained('twitch_users')->onDelete('cascade'); $table->timestamps(); }); } - /** - * Reverse the migrations. - * - * @return void - */ public function down() { Schema::dropIfExists('current_state'); diff --git a/database/migrations/2024_07_28_031248_create_winning_state_table.php b/database/migrations/2024_07_28_031248_create_winning_state_table.php new file mode 100644 index 0000000..11684d8 --- /dev/null +++ b/database/migrations/2024_07_28_031248_create_winning_state_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('game_session_id')->constrained('game_sessions')->onDelete('cascade'); + $table->string('x'); + $table->string('y'); + $table->foreignId('palette_color_id')->constrained('palette_colors')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('winning_state'); + } +} diff --git a/resources/css/administration.css b/resources/css/administration.css new file mode 100644 index 0000000..e69de29 diff --git a/resources/css/palette-editor.css b/resources/css/palette-editor.css index e4a8042..d572843 100644 --- a/resources/css/palette-editor.css +++ b/resources/css/palette-editor.css @@ -1,6 +1,17 @@ -.color-picker { +.pickr { + width: 100%; + height: 36px; +} + +.pickr .pcr-button { border: 1px solid black; border-radius: 4px; width: 100%; height: 36px; } + +#rename-palette-name { + border-radius: 25px; + text-align: center; + vertical-align: middle; +} diff --git a/resources/js/administration.js b/resources/js/administration.js new file mode 100644 index 0000000..e69de29 diff --git a/resources/js/grid.js b/resources/js/grid.js index 2958ba3..00b3576 100755 --- a/resources/js/grid.js +++ b/resources/js/grid.js @@ -1,39 +1,12 @@ -function colorCell(cellName, colorName) { - console.log(`colorCell called with: ${cellName}, ${colorName}`); +function colorCell(cellName, hexValue) { + console.log(`colorCell called with: ${cellName}, ${hexValue}`); - // Map of valid named colors to their hex values - const validColors = { - "white": "#FFFFFF", - "lightGray": "#E4E4E4", - "mediumGray": "#888888", - "darkGray": "#222222", - "pink": "#FFA7D1", - "red": "#E50000", - "orange": "#E59500", - "brown": "#A06A42", - "yellow": "#E5D900", - "lightGreen": "#94E044", - "green": "#02BE01", - "cyan": "#00D3DD", - "blue": "#0083C7", - "darkBlue": "#0000EA", - "purple": "#CF6EE4", - "darkPurple": "#820080", - "black": "#000000" - }; - - // Check if the provided color name is valid - if (!validColors[colorName]) { - console.warn(`Invalid color name: ${colorName}. Please use a valid named color.`); - return; - } - // Find the cell using its class name const cell = document.querySelector(`.cell.${cellName}`); - // If the cell exists, change its background color to the corresponding hex value + // If the cell exists, change its background color to the provided hex value if (cell) { - cell.style.backgroundColor = validColors[colorName]; + cell.style.backgroundColor = hexValue; } else { console.warn(`Cell ${cellName} not found.`); } @@ -84,7 +57,11 @@ document.addEventListener('DOMContentLoaded', function() { .then(response => response.json()) .then(data => { data.forEach(pixel => { - colorCell(`${pixel.x}${pixel.y}`, pixel.color); + if (pixel.color && pixel.color.hex_value) { + colorCell(`${pixel.x}${pixel.y}`, pixel.color.hex_value); + } else { + console.warn(`No color found for pixel at ${pixel.x}${pixel.y}`); + } }); }) .catch(error => { diff --git a/resources/js/palette-editor.js b/resources/js/palette-editor.js index 43b4cb2..5c07cf2 100644 --- a/resources/js/palette-editor.js +++ b/resources/js/palette-editor.js @@ -1,14 +1,45 @@ document.addEventListener('DOMContentLoaded', function() { + console.log('DOM fully loaded and parsed.'); + const paletteSelector = document.getElementById('palette-selector'); const newPaletteButton = document.getElementById('new-palette'); const newPaletteForm = document.getElementById('new-palette-form'); const createPaletteForm = document.getElementById('create-palette-form'); const addColorForm = document.getElementById('add-color-form'); const createColorForm = document.getElementById('create-color-form'); - const paletteColorsDiv = document.getElementById('palette-colors'); const colorEditorsDiv = document.getElementById('color-editors'); const addColorButton = document.getElementById('add-color'); const savePaletteButton = document.getElementById('save-palette'); + const exportPaletteButton = document.getElementById('export-palette'); + const importPaletteButton = document.getElementById('import-palette'); + const importFileInput = document.getElementById('import-file'); + const renamePaletteButton = document.getElementById('rename-palette'); + const renamePaletteNameInput = document.getElementById('rename-palette-name'); + const deletePaletteButton = document.getElementById('delete-palette'); + const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'), {}); + const confirmDeleteCheckbox = document.getElementById('confirmDeleteCheckbox'); + const confirmDeleteButton = document.getElementById('confirmDeleteButton'); + + // CSRF token + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + + // Logging element states + console.log('paletteSelector:', paletteSelector); + console.log('newPaletteButton:', newPaletteButton); + console.log('newPaletteForm:', newPaletteForm); + console.log('createPaletteForm:', createPaletteForm); + console.log('addColorForm:', addColorForm); + console.log('createColorForm:', createColorForm); + console.log('colorEditorsDiv:', colorEditorsDiv); + console.log('addColorButton:', addColorButton); + console.log('savePaletteButton:', savePaletteButton); + console.log('deletePaletteButton:', deletePaletteButton); + console.log('csrfToken:', csrfToken); + + if (!paletteSelector || !newPaletteButton || !newPaletteForm || !createPaletteForm || !addColorForm || !createColorForm || !colorEditorsDiv || !addColorButton || !savePaletteButton || !exportPaletteButton || !importPaletteButton || !importFileInput || !renamePaletteButton || !renamePaletteNameInput || !deletePaletteButton) { + console.error('Missing required elements on the page.'); + return; + } newPaletteButton.addEventListener('click', function() { newPaletteForm.style.display = 'block'; @@ -17,28 +48,40 @@ document.addEventListener('DOMContentLoaded', function() { createPaletteForm.addEventListener('submit', function(event) { event.preventDefault(); const formData = new FormData(createPaletteForm); + console.log('Creating new palette with data:', formData); fetch('/palette-editor', { method: 'POST', - body: formData + body: formData, + headers: { + 'X-CSRF-TOKEN': csrfToken + } }) .then(response => response.json()) .then(data => { - alert('Palette created: ' + data.name); - paletteSelector.innerHTML += ``; + console.log('Palette created:', data); + fetchPalettes().then(() => { + const newOption = document.createElement('option'); + newOption.value = data.id; + newOption.text = data.name; + paletteSelector.add(newOption); + paletteSelector.value = data.id; + loadPaletteColors(data.id); + }); newPaletteForm.style.display = 'none'; createPaletteForm.reset(); }) .catch(error => { - console.error('Error:', error); + console.error('Error creating palette:', error); }); }); paletteSelector.addEventListener('change', function() { const paletteId = paletteSelector.value; + console.log('Palette selected:', paletteId); if (paletteId) { loadPaletteColors(paletteId); + checkPaletteRestrictions(); } else { - paletteColorsDiv.innerHTML = ''; addColorForm.style.display = 'none'; colorEditorsDiv.innerHTML = ''; } @@ -51,26 +94,38 @@ document.addEventListener('DOMContentLoaded', function() { savePaletteButton.addEventListener('click', function() { const paletteId = createColorForm.getAttribute('data-palette-id'); const colorEditors = document.querySelectorAll('.color-editor'); + const colorUpdates = []; + colorEditors.forEach(editor => { const colorId = editor.getAttribute('data-color-id'); const colorName = editor.querySelector('.color-name').value; const colorHex = editor.querySelector('.color-hex-input').value; + colorUpdates.push({ + colorId, + colorName, + colorHex + }); + }); + + console.log('Saving palette with ID:', paletteId); + colorUpdates.forEach(({colorId, colorName, colorHex}) => { + console.log(`Saving color: ${colorName} (${colorHex}) with ID: ${colorId}`); if (colorId) { // Update existing color fetch(`/palette-editor/${paletteId}/colors/${colorId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify({ name: colorName, hex_value: colorHex }) }) .then(response => response.json()) .then(data => { - alert('Color updated: ' + data.name); + console.log('Color updated:', data); }) .catch(error => { - console.error('Error:', error); + console.error('Error updating color:', error); }); } else { // Add new color @@ -78,27 +133,185 @@ document.addEventListener('DOMContentLoaded', function() { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify({ name: colorName, hex_value: colorHex }) }) .then(response => response.json()) .then(data => { - alert('Color added: ' + data.name); + console.log('Color added:', data); editor.setAttribute('data-color-id', data.id); }) .catch(error => { - console.error('Error:', error); + console.error('Error adding color:', error); }); } }); }); + exportPaletteButton.addEventListener('click', function() { + const paletteId = paletteSelector.value; + console.log('Exporting palette with ID:', paletteId); + if (paletteId) { + fetch(`/palette-editor/${paletteId}/export`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.blob(); + }) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `${paletteSelector.options[paletteSelector.selectedIndex].text}.pal`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }) + .catch(error => { + console.error('Error exporting palette:', error); + }); + } else { + alert('Please select a palette to export.'); + } + }); + + + importPaletteButton.addEventListener('click', function() { + importFileInput.click(); + }); + + importFileInput.addEventListener('change', function(event) { + const file = event.target.files[0]; + if (file) { + const formData = new FormData(); + formData.append('file', file); + + console.log('Importing palette from file:', file.name); + fetch('/palette-editor/import', { + method: 'POST', + body: formData, + headers: { + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + console.log('Palette imported successfully', data); + const newOption = document.createElement('option'); + newOption.value = data.palette.id; + newOption.text = data.palette.name; + paletteSelector.add(newOption); + paletteSelector.value = data.palette.id; + loadPaletteColors(data.palette.id); + }) + .catch(error => { + console.error('Error importing palette:', error); + }); + } + }); + + renamePaletteButton.addEventListener('click', function() { + const paletteId = paletteSelector.value; + const newName = renamePaletteNameInput.value.trim(); + console.log('Renaming palette ID:', paletteId, 'to:', newName); + if (!paletteId) { + alert('Please select a palette to rename.'); + return; + } + if (!newName) { + alert('Please enter a new name for the palette.'); + return; + } + + fetch(`/palette-editor/${paletteId}/rename`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ name: newName }) + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + console.log('Palette renamed:', data); + fetchPalettes().then(() => { + const option = Array.from(paletteSelector.options).find(option => option.value == paletteId); + if (option) { + option.text = newName; + paletteSelector.value = paletteId; + } + loadPaletteColors(paletteId); + }); + }) + .catch(error => { + console.error('Error renaming palette:', error); + }); + }); + + deletePaletteButton.addEventListener('click', function() { + const paletteId = paletteSelector.value; + console.log('Preparing to delete palette ID:', paletteId); + if (!paletteId) { + alert('Please select a palette to delete.'); + return; + } + // Show modal + deleteModal.show(); + }); + + confirmDeleteCheckbox.addEventListener('change', function() { + confirmDeleteButton.disabled = !confirmDeleteCheckbox.checked; + }); + + confirmDeleteButton.addEventListener('click', function() { + const paletteId = paletteSelector.value; + console.log('Deleting palette with ID:', paletteId); + if (paletteId && confirmDeleteCheckbox.checked) { + fetch(`/palette-editor/${paletteId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + console.log('Palette deleted:', data); + fetchPalettes().then(() => { + const option = Array.from(paletteSelector.options).find(option => option.value == paletteId); + if (option) { + paletteSelector.removeChild(option); + } + const defaultPaletteOption = Array.from(paletteSelector.options).find(option => option.text === 'default-colors'); + if (defaultPaletteOption) { + defaultPaletteOption.selected = true; + loadPaletteColors(defaultPaletteOption.value); + } else { + paletteSelector.value = ''; + colorEditorsDiv.innerHTML = ''; + } + }); + deleteModal.hide(); + }) + .catch(error => { + console.error('Error deleting palette:', error); + }); + } + }); + function loadPaletteColors(paletteId) { + console.log('Loading colors for palette ID:', paletteId); fetch(`/palette-editor/${paletteId}/colors`) .then(response => response.json()) .then(data => { - paletteColorsDiv.innerHTML = '

Colors in Palette

'; + console.log('Loaded palette colors:', data); colorEditorsDiv.innerHTML = ''; data.forEach(color => { addColorEditor(color.id, color.name, color.hex_value); @@ -107,11 +320,12 @@ document.addEventListener('DOMContentLoaded', function() { createColorForm.setAttribute('data-palette-id', paletteId); }) .catch(error => { - console.error('Error:', error); + console.error('Error loading palette colors:', error); }); } function addColorEditor(colorId = '', colorName = '', colorHex = '#ffffff') { + console.log(`Adding color editor for color: ${colorName} (${colorHex}) with ID: ${colorId}`); const editor = document.createElement('div'); editor.className = 'color-editor col-md-2 mb-3'; editor.setAttribute('data-color-id', colorId); @@ -135,21 +349,26 @@ document.addEventListener('DOMContentLoaded', function() { const deleteButton = editor.querySelector('.delete-color'); deleteButton.addEventListener('click', function() { + const paletteId = createColorForm.getAttribute('data-palette-id'); + if (paletteId === '1') { + alert('Cannot delete colors from the default-colors palette.'); + return; + } + console.log('Deleting color ID:', colorId, 'from palette ID:', paletteId); if (colorId) { - const paletteId = createColorForm.getAttribute('data-palette-id'); fetch(`/palette-editor/${paletteId}/colors/${colorId}`, { method: 'DELETE', headers: { - 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + 'X-CSRF-TOKEN': csrfToken } }) .then(response => response.json()) .then(data => { - alert('Color deleted: ' + data.message); + console.log('Color deleted:', data.message); colorEditorsDiv.removeChild(editor); }) .catch(error => { - console.error('Error:', error); + console.error('Error deleting color:', error); }); } else { colorEditorsDiv.removeChild(editor); @@ -184,8 +403,49 @@ document.addEventListener('DOMContentLoaded', function() { }); } + function fetchPalettes() { + console.log('Fetching palettes from the server'); + return fetch('/palette-editor') + .then(response => response.json()) + .then(data => { + console.log('Fetched palettes:', data); + paletteSelector.innerHTML = ''; + data.forEach(palette => { + const option = document.createElement('option'); + option.value = palette.id; + option.text = palette.name; + paletteSelector.add(option); + }); + }) + .catch(error => { + console.error('Error fetching palettes:', error); + }); + } + + function checkPaletteRestrictions() { + const paletteId = paletteSelector.value; + const isDefaultPalette = paletteId === '1'; + + console.log('Checking restrictions for palette ID:', paletteId, 'isDefaultPalette:', isDefaultPalette); + + deletePaletteButton.disabled = isDefaultPalette; + addColorButton.disabled = isDefaultPalette; + savePaletteButton.disabled = isDefaultPalette; + + if (isDefaultPalette) { + document.querySelectorAll('.delete-color').forEach(button => { + button.disabled = true; + }); + } else { + document.querySelectorAll('.delete-color').forEach(button => { + button.disabled = false; + }); + } + } + // Load default colors on page load window.onload = function() { + console.log('Page loaded, selecting default palette if available'); const defaultPaletteOption = Array.from(paletteSelector.options).find(option => option.text === 'default-colors'); if (defaultPaletteOption) { defaultPaletteOption.selected = true; diff --git a/resources/js/twitch.js b/resources/js/twitch.js index 7bb08dc..176b22d 100644 --- a/resources/js/twitch.js +++ b/resources/js/twitch.js @@ -46,7 +46,8 @@ function connectWebSocket() { console.log('Chat Message:', chatMessage); console.log('Username:', username); - const commandPattern = /^(?:!place|!p|!paint)\s+([A-P])\s*(\d{1,2})\s+(\w+)$/i; + // Update the regular expression to handle color names with hyphens + const commandPattern = /^(?:!place|!p|!paint)\s+([A-P])\s*(\d{1,2})\s+([\w-]+)$/i; const commandMatch = chatMessage.match(commandPattern); console.log('Command Match:', commandMatch); if (commandMatch) { @@ -114,9 +115,9 @@ function sendPlaceCommand(chatMessage, username, x, y, color) { .then(response => response.json()) .then(data => { console.log('Server Response:', data); - if (data && data.command === 'place') { + if (data && data.command.command === 'place') { console.log('Parsed Place Command from server:', data); - colorCell(`${data.x}${data.y}`, data.color); + colorCell(`${data.command.x}${data.command.y}`, data.color.hex_value); } else { console.error('Invalid command in server response.'); } diff --git a/resources/views/administration/index.blade.php b/resources/views/administration/index.blade.php new file mode 100644 index 0000000..aaa9e98 --- /dev/null +++ b/resources/views/administration/index.blade.php @@ -0,0 +1,87 @@ +@extends('layouts.administration-layout') + +@section('title', 'Administration') + +@section('content') +
+

Administration Dashboard

+ + @if(session('success')) +
{{ session('success') }}
+ @endif + + @if(session('error')) +
{{ session('error') }}
+ @endif + +
+
+

Current Game

+
+
+ @if($currentGame) +

Session Name: {{ $currentGame->session_name }}

+

Current Palette: {{ $currentGame->palette ? $currentGame->palette->name : 'None' }}

+
+

Current State

+
+ @foreach($currentState as $state) +
+ ({{ $state->x }}, {{ $state->y }}) +
+ @endforeach +
+
+ @else +

No active game session.

+ @endif +
+
+ +
+
+

Change Palette

+
+
+
+ @csrf +
+ + +
+ +
+
+
+ +
+
+

Create New Game

+
+
+
+ @csrf +
+ + +
+
+ + +
+ +
+
+
+
+@endsection diff --git a/resources/views/layouts/administration-layout.blade.php b/resources/views/layouts/administration-layout.blade.php new file mode 100644 index 0000000..69c5365 --- /dev/null +++ b/resources/views/layouts/administration-layout.blade.php @@ -0,0 +1,24 @@ + + + + + + @yield('title', 'Administration') + + + + + @vite(['resources/css/app.css', 'resources/css/administration.css', 'resources/js/administration.js']) + + +
+ @yield('content') +
+ + + + + + + + diff --git a/resources/views/layouts/palette-layout.blade.php b/resources/views/layouts/palette-layout.blade.php index f288cc5..c53b3ce 100644 --- a/resources/views/layouts/palette-layout.blade.php +++ b/resources/views/layouts/palette-layout.blade.php @@ -10,6 +10,7 @@ + @vite(['resources/css/app.css', 'resources/css/palette-editor.css', 'resources/js/palette-editor.js']) @@ -17,7 +18,7 @@ @yield('content') - + diff --git a/resources/views/palette-editor.blade.php b/resources/views/palette-editor.blade.php index 666eda1..f98e100 100644 --- a/resources/views/palette-editor.blade.php +++ b/resources/views/palette-editor.blade.php @@ -3,39 +3,78 @@ @section('title', 'Palette Editor') @section('content') -
-

Palette Editor

-
- - -
- -
-
- -