WIP
This commit is contained in:
parent
91c37fca0a
commit
0cbd691cc7
|
@ -119,3 +119,7 @@ ## Contributing
|
|||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
|
||||
## TODO
|
||||
Explain setting up oauth
|
||||
https://twitchapps.com/tmi/
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class TwitchHelper
|
||||
{
|
||||
public static function parseMessage($message)
|
||||
{
|
||||
$commandPattern = '/^!place\s([A-P])\s(\d{1,2})\s(\w+)$/i';
|
||||
$shortCommandPattern = '/^!p\s([A-P])(\d{1,2})\s(\w+)$/i';
|
||||
|
||||
if (preg_match($commandPattern, $message, $matches)) {
|
||||
return [
|
||||
'command' => 'place',
|
||||
'x' => $matches[1],
|
||||
'y' => $matches[2],
|
||||
'color' => $matches[3]
|
||||
];
|
||||
}
|
||||
|
||||
if (preg_match($shortCommandPattern, $message, $matches)) {
|
||||
return [
|
||||
'command' => 'place',
|
||||
'x' => $matches[1],
|
||||
'y' => $matches[2],
|
||||
'color' => $matches[3]
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TwitchHelper
|
||||
{
|
||||
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';
|
||||
|
||||
if (preg_match($commandPattern, $message, $matches)) {
|
||||
Log::debug('TwitchHelper::parseMessage - Command matched', ['matches' => $matches]);
|
||||
return [
|
||||
'command' => 'place',
|
||||
'x' => $matches[1],
|
||||
'y' => $matches[2],
|
||||
'color' => $matches[3]
|
||||
];
|
||||
}
|
||||
|
||||
Log::debug('TwitchHelper::parseMessage - No match found');
|
||||
return null;
|
||||
}
|
||||
}
|
0
app/Http/Controllers/Auth/EmailVerificationNotificationController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/EmailVerificationNotificationController.php
Normal file → Executable file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\GameSession;
|
||||
|
||||
class GameSessionController extends Controller
|
||||
{
|
||||
public function start(Request $request)
|
||||
{
|
||||
$sessionName = $request->input('session_name');
|
||||
$session = GameSession::create(['session_name' => $sessionName]);
|
||||
|
||||
return response()->json($session);
|
||||
}
|
||||
|
||||
public function end(Request $request)
|
||||
{
|
||||
$sessionId = $request->input('session_id');
|
||||
$session = GameSession::find($sessionId);
|
||||
|
||||
if ($session) {
|
||||
$session->update(['ended_at' => now()]);
|
||||
return response()->json($session);
|
||||
}
|
||||
|
||||
return response()->json(['error' => 'Session not found'], 404);
|
||||
}
|
||||
|
||||
public function current()
|
||||
{
|
||||
$session = GameSession::whereNull('ended_at')->latest()->first();
|
||||
return response()->json($session);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Palette;
|
||||
use App\Models\PaletteColor;
|
||||
|
||||
class PaletteController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$palettes = Palette::all();
|
||||
return view('palette-editor', compact('palettes'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|unique:palettes,name',
|
||||
]);
|
||||
|
||||
$palette = Palette::create(['name' => $request->name]);
|
||||
return response()->json($palette);
|
||||
}
|
||||
|
||||
public function addColor(Request $request, Palette $palette)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|regex:/^[a-z-]+$/',
|
||||
'hex_value' => 'required|regex:/^#[a-fA-F0-9]{6}$/'
|
||||
]);
|
||||
|
||||
$color = PaletteColor::create([
|
||||
'palette_id' => $palette->id,
|
||||
'name' => $request->name,
|
||||
'hex_value' => $request->hex_value
|
||||
]);
|
||||
|
||||
return response()->json($color);
|
||||
}
|
||||
|
||||
public function updateColor(Request $request, Palette $palette, PaletteColor $color)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|regex:/^[a-z-]+$/',
|
||||
'hex_value' => 'required|regex:/^#[a-fA-F0-9]{6}$/'
|
||||
]);
|
||||
|
||||
$color->update([
|
||||
'name' => $request->name,
|
||||
'hex_value' => $request->hex_value
|
||||
]);
|
||||
|
||||
return response()->json($color);
|
||||
}
|
||||
|
||||
public function deleteColor(Palette $palette, PaletteColor $color)
|
||||
{
|
||||
$color->delete();
|
||||
return response()->json(['message' => 'Color deleted']);
|
||||
}
|
||||
|
||||
public function getColors(Palette $palette)
|
||||
{
|
||||
return response()->json($palette->colors);
|
||||
}
|
||||
}
|
|
@ -4,23 +4,97 @@
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Helpers\TwitchHelper;
|
||||
use App\Models\TwitchUser;
|
||||
use App\Models\CommandHistory;
|
||||
use App\Models\CurrentState;
|
||||
use App\Models\GameSession;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TwitchController extends Controller
|
||||
{
|
||||
public function parseChatMessage(Request $request)
|
||||
{
|
||||
Log::info("Entered parseChatMessage");
|
||||
$message = $request->input('message');
|
||||
$username = $request->input('username');
|
||||
$gameSession = $this->getCurrentGameSession();
|
||||
|
||||
if (!$message) {
|
||||
return response()->json(['error' => 'No message provided'], 400);
|
||||
Log::debug('TwitchController::parseChatMessage', [
|
||||
'message' => $message,
|
||||
'username' => $username,
|
||||
'gameSessionId' => $gameSession->id
|
||||
]);
|
||||
|
||||
if (!$message || !$username || !$gameSession) {
|
||||
Log::debug('TwitchController::parseChatMessage - Missing input or invalid game session', [
|
||||
'message' => $message,
|
||||
'username' => $username,
|
||||
'gameSession' => $gameSession
|
||||
]);
|
||||
return response()->json(['error' => 'No message, username, or valid game session provided'], 400);
|
||||
}
|
||||
|
||||
$user = TwitchUser::firstOrCreate(['twitch_username' => $username]);
|
||||
|
||||
$parsedCommand = TwitchHelper::parseMessage($message);
|
||||
Log::debug('TwitchController::parseChatMessage - Parsed Command', ['parsedCommand' => $parsedCommand]);
|
||||
|
||||
if ($parsedCommand) {
|
||||
CommandHistory::create([
|
||||
'user_id' => $user->id,
|
||||
'command' => $parsedCommand['command'],
|
||||
'x' => $parsedCommand['x'] ?? null,
|
||||
'y' => $parsedCommand['y'] ?? null,
|
||||
'color' => $parsedCommand['color'] ?? null,
|
||||
'game_session_id' => $gameSession->id,
|
||||
]);
|
||||
|
||||
switch ($parsedCommand['command']) {
|
||||
case 'place':
|
||||
$this->handlePlaceCommand($parsedCommand, $user->id);
|
||||
break;
|
||||
// Add cases for other commands like 'row', 'column', 'fill'
|
||||
}
|
||||
|
||||
return response()->json($parsedCommand);
|
||||
}
|
||||
|
||||
Log::debug('TwitchController::parseChatMessage - Invalid command', ['message' => $message]);
|
||||
return response()->json(['error' => 'Invalid command'], 400);
|
||||
}
|
||||
|
||||
private function handlePlaceCommand($parsedCommand, $userId)
|
||||
{
|
||||
Log::debug('TwitchController::handlePlaceCommand', ['command' => $parsedCommand]);
|
||||
|
||||
$x = $parsedCommand['x'];
|
||||
$y = $parsedCommand['y'];
|
||||
$color = $parsedCommand['color'];
|
||||
|
||||
$currentState = CurrentState::where('x', $x)->where('y', $y)->first();
|
||||
Log::debug('TwitchController::handlePlaceCommand - Retrieved Current State', ['currentState' => $currentState]);
|
||||
|
||||
if ($currentState) {
|
||||
$currentState->update(['color' => $color, 'updated_by' => $userId]);
|
||||
} else {
|
||||
CurrentState::create(['x' => $x, 'y' => $y, 'color' => $color, 'updated_by' => $userId]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getCurrentGameSession()
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CommandHistory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'command_history';
|
||||
|
||||
protected $fillable = ['user_id', 'command', 'x', 'y', 'color', 'game_session_id'];
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CurrentState extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'current_state';
|
||||
|
||||
protected $fillable = ['x', 'y', 'color', 'updated_by'];
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GameSession extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['session_name', 'ended_at'];
|
||||
|
||||
protected $dates = ['ended_at'];
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Palette extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function colors()
|
||||
{
|
||||
return $this->hasMany(PaletteColor::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PaletteColor extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['palette_id', 'name', 'hex_value'];
|
||||
|
||||
public function palette()
|
||||
{
|
||||
return $this->belongsTo(Palette::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SourceImage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name', 'pixel_state', 'palette'];
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TwitchUser extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['twitch_username'];
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateTwitchUsersTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('twitch_users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('twitch_username')->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('twitch_users');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGameSessionsTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('game_sessions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('session_name');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('game_sessions');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateCurrentStateTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('current_state', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('x');
|
||||
$table->string('y');
|
||||
$table->string('color');
|
||||
$table->foreignId('updated_by')->constrained('twitch_users')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('current_state');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
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('game_session_id')->constrained('game_sessions')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('command_history');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateSourceImagesTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('source_images', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->json('pixel_state');
|
||||
$table->json('palette');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('source_images');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddEndedAtToGameSessionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('game_sessions', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('game_sessions', 'ended_at')) {
|
||||
$table->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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePalettesTable 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');
|
||||
$table->string('name');
|
||||
$table->string('hex_value');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Insert default palette
|
||||
DB::table('palettes')->insert([
|
||||
'name' => 'default-colors',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$defaultColors = [
|
||||
["white", "#FFFFFF"],
|
||||
["light-gray", "#E4E4E4"],
|
||||
["medium-gray", "#888888"],
|
||||
["dark-gray", "#222222"],
|
||||
["pink", "#FFA7D1"],
|
||||
["red", "#E50000"],
|
||||
["orange", "#E59500"],
|
||||
["brown", "#A06A42"],
|
||||
["yellow", "#E5D900"],
|
||||
["light-green", "#94E044"],
|
||||
["green", "#02BE01"],
|
||||
["cyan", "#00D3DD"],
|
||||
["blue", "#0083C7"],
|
||||
["dark-blue", "#0000EA"],
|
||||
["purple", "#CF6EE4"],
|
||||
["dark-purple", "#820080"],
|
||||
["black", "#000000"]
|
||||
];
|
||||
|
||||
foreach ($defaultColors as $color) {
|
||||
DB::table('palette_colors')->insert([
|
||||
'palette_id' => 1,
|
||||
'name' => $color[0],
|
||||
'hex_value' => $color[1],
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('palette_colors');
|
||||
Schema::dropIfExists('palettes');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--grid-size: 16; /* Adjust this value for grid size (e.g., 3 for 3x3) */
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
height: 100vh;
|
||||
width: 100vh; /* Keep the grid square */
|
||||
max-width: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(calc(var(--grid-size) + 2), 1fr);
|
||||
grid-template-rows: repeat(calc(var(--grid-size) + 2), 1fr);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
font-size: calc(0.5vw + 0.5vh); /* Adjust font size for better visibility */
|
||||
}
|
||||
|
||||
.cell {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.color-picker {
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
}
|
|
@ -30,34 +30,26 @@ ws.onmessage = (event) => {
|
|||
if (parsedMessage && parsedMessage.command === 'PRIVMSG') {
|
||||
const chatMessage = parsedMessage.params.slice(1).join(' ').trim();
|
||||
const username = parsedMessage.tags['display-name'] || parsedMessage.prefix.split('!')[0];
|
||||
const gameSessionId = 1; // Replace with logic to get the current game session ID
|
||||
|
||||
console.log('Chat Message:', chatMessage);
|
||||
console.log('Username:', username);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('chat-message', { detail: { user: username, message: chatMessage } }));
|
||||
|
||||
// Process !place command
|
||||
fetch('/twitch/parse', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ message: chatMessage })
|
||||
body: JSON.stringify({ message: chatMessage, username: username, game_session_id: gameSessionId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data && data.command === 'place') {
|
||||
console.log('Parsed Command:', data);
|
||||
// Call the existing API with parsed coordinates and color
|
||||
// Example API call: /api/place/{x}/{y}/{color}
|
||||
fetch(`/api/place/${data.x}/${data.y}/${data.color}`, { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
console.log('API Result:', result);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('API Error:', error);
|
||||
});
|
||||
console.log('Parsed Place Command:', data);
|
||||
colorCell(`${data.x}${data.y}`, data.color);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -66,6 +58,7 @@ ws.onmessage = (event) => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket Error:', error);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
function colorCell(cellName, colorName) {
|
||||
console.log(`colorCell called with: ${cellName}, ${colorName}`);
|
||||
|
||||
// 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 (cell) {
|
||||
cell.style.backgroundColor = validColors[colorName];
|
||||
} else {
|
||||
console.warn(`Cell ${cellName} not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach colorCell function to the window object to make it globally accessible
|
||||
window.colorCell = colorCell;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const gridSize = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--grid-size').trim());
|
||||
const grid = document.querySelector('.grid-container');
|
||||
|
||||
function getLabel(index) {
|
||||
let label = '';
|
||||
while (index >= 0) {
|
||||
label = String.fromCharCode((index % 26) + 65) + label;
|
||||
index = Math.floor(index / 26) - 1;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
// Create top labels
|
||||
grid.innerHTML += '<div class="label"></div>'; // Empty top-left corner
|
||||
for (let i = 0; i < gridSize; i++) {
|
||||
grid.innerHTML += `<div class="label">${getLabel(i)}</div>`;
|
||||
}
|
||||
grid.innerHTML += '<div class="label"></div>'; // Empty top-right corner
|
||||
|
||||
// Create rows with side labels and cells
|
||||
for (let i = 1; i <= gridSize; i++) {
|
||||
grid.innerHTML += `<div class="label">${i}</div>`;
|
||||
for (let j = 0; j < gridSize; j++) {
|
||||
let cellName = `${getLabel(j)}${i}`; // This will generate names like A1, A2, B1, etc.
|
||||
grid.innerHTML += `<div class="cell ${cellName}"></div>`;
|
||||
}
|
||||
grid.innerHTML += `<div class="label">${i}</div>`;
|
||||
}
|
||||
|
||||
// Create bottom labels
|
||||
grid.innerHTML += '<div class="label"></div>'; // Empty bottom-left corner
|
||||
for (let i = 0; i < gridSize; i++) {
|
||||
grid.innerHTML += `<div class="label">${getLabel(i)}</div>`;
|
||||
}
|
||||
grid.innerHTML += '<div class="label"></div>'; // Empty bottom-right corner
|
||||
|
||||
// Fetch and apply the current state
|
||||
fetch('/api/current-state')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.forEach(pixel => {
|
||||
colorCell(`${pixel.x}${pixel.y}`, pixel.color);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching current state:', error);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,195 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
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');
|
||||
|
||||
newPaletteButton.addEventListener('click', function() {
|
||||
newPaletteForm.style.display = 'block';
|
||||
});
|
||||
|
||||
createPaletteForm.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(createPaletteForm);
|
||||
fetch('/palette-editor', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert('Palette created: ' + data.name);
|
||||
paletteSelector.innerHTML += `<option value="${data.id}">${data.name}</option>`;
|
||||
newPaletteForm.style.display = 'none';
|
||||
createPaletteForm.reset();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
paletteSelector.addEventListener('change', function() {
|
||||
const paletteId = paletteSelector.value;
|
||||
if (paletteId) {
|
||||
loadPaletteColors(paletteId);
|
||||
} else {
|
||||
paletteColorsDiv.innerHTML = '';
|
||||
addColorForm.style.display = 'none';
|
||||
colorEditorsDiv.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
addColorButton.addEventListener('click', function() {
|
||||
addColorEditor();
|
||||
});
|
||||
|
||||
savePaletteButton.addEventListener('click', function() {
|
||||
const paletteId = createColorForm.getAttribute('data-palette-id');
|
||||
const colorEditors = document.querySelectorAll('.color-editor');
|
||||
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;
|
||||
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')
|
||||
},
|
||||
body: JSON.stringify({ name: colorName, hex_value: colorHex })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert('Color updated: ' + data.name);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
} else {
|
||||
// Add new color
|
||||
fetch(`/palette-editor/${paletteId}/colors`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ name: colorName, hex_value: colorHex })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert('Color added: ' + data.name);
|
||||
editor.setAttribute('data-color-id', data.id);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadPaletteColors(paletteId) {
|
||||
fetch(`/palette-editor/${paletteId}/colors`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
paletteColorsDiv.innerHTML = '<h2>Colors in Palette</h2>';
|
||||
colorEditorsDiv.innerHTML = '';
|
||||
data.forEach(color => {
|
||||
addColorEditor(color.id, color.name, color.hex_value);
|
||||
});
|
||||
addColorForm.style.display = 'block';
|
||||
createColorForm.setAttribute('data-palette-id', paletteId);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function addColorEditor(colorId = '', colorName = '', colorHex = '#ffffff') {
|
||||
const editor = document.createElement('div');
|
||||
editor.className = 'color-editor col-md-2 mb-3';
|
||||
editor.setAttribute('data-color-id', colorId);
|
||||
editor.innerHTML = `
|
||||
<div class="form-row align-items-end">
|
||||
<div class="col-12">
|
||||
<label>Color Name:</label>
|
||||
<input type="text" class="form-control color-name" value="${colorName}" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label>Color Hex:</label>
|
||||
<div class="color-picker"></div>
|
||||
<input type="hidden" class="color-hex-input" value="${colorHex}" required>
|
||||
</div>
|
||||
<div class="col-12 text-right mt-2">
|
||||
<button type="button" class="btn btn-danger btn-sm delete-color"><i class="fas fa-trash-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
colorEditorsDiv.appendChild(editor);
|
||||
|
||||
const deleteButton = editor.querySelector('.delete-color');
|
||||
deleteButton.addEventListener('click', function() {
|
||||
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')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert('Color deleted: ' + data.message);
|
||||
colorEditorsDiv.removeChild(editor);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
} else {
|
||||
colorEditorsDiv.removeChild(editor);
|
||||
}
|
||||
});
|
||||
|
||||
const pickr = Pickr.create({
|
||||
el: editor.querySelector('.color-picker'),
|
||||
theme: 'classic', // or 'monolith', or 'nano'
|
||||
default: colorHex,
|
||||
components: {
|
||||
preview: true,
|
||||
opacity: false,
|
||||
hue: true,
|
||||
|
||||
interaction: {
|
||||
hex: true,
|
||||
input: true,
|
||||
clear: true,
|
||||
save: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pickr.on('save', (color, instance) => {
|
||||
editor.querySelector('.color-hex-input').value = color.toHEXA().toString();
|
||||
pickr.hide();
|
||||
});
|
||||
|
||||
pickr.on('clear', instance => {
|
||||
editor.querySelector('.color-hex-input').value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Load default colors on page load
|
||||
window.onload = function() {
|
||||
const defaultPaletteOption = Array.from(paletteSelector.options).find(option => option.text === 'default-colors');
|
||||
if (defaultPaletteOption) {
|
||||
defaultPaletteOption.selected = true;
|
||||
loadPaletteColors(defaultPaletteOption.value);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
let ws;
|
||||
|
||||
function connectWebSocket() {
|
||||
console.log("Attempting to connect to Twitch chat with websocket...");
|
||||
ws = new WebSocket('wss://irc-ws.chat.twitch.tv:443');
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('Connected to Twitch chat');
|
||||
|
||||
const oauthToken = import.meta.env.VITE_TWITCH_BOT_OAUTH_TOKEN;
|
||||
const username = import.meta.env.VITE_TWITCH_BOT_USERNAME;
|
||||
const channel = import.meta.env.VITE_TWITCH_CHANNEL;
|
||||
|
||||
console.log('OAuth Token:', oauthToken ? 'Loaded' : 'Missing');
|
||||
console.log('Username:', username ? username : 'Missing');
|
||||
console.log('Channel:', channel ? channel : 'Missing');
|
||||
|
||||
if (!oauthToken || !username || !channel) {
|
||||
console.error('Missing required environment variables');
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send(`PASS ${oauthToken}`);
|
||||
ws.send(`NICK ${username}`);
|
||||
ws.send(`JOIN #${channel}`);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
console.log('Received:', event.data);
|
||||
|
||||
const parsedMessage = parseTwitchMessage(event.data);
|
||||
console.log('Parsed Message:', parsedMessage);
|
||||
|
||||
if (parsedMessage.command === '376') {
|
||||
console.log('End of MOTD received. Ready to process messages.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage.command === 'JOIN') {
|
||||
console.log(`Joined channel: ${parsedMessage.params[0]}`);
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage.command === 'PRIVMSG') {
|
||||
const chatMessage = parsedMessage.params.slice(1).join(' ').trim();
|
||||
const username = parsedMessage.prefix.split('!')[0];
|
||||
console.log('Chat Message:', chatMessage);
|
||||
console.log('Username:', username);
|
||||
|
||||
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) {
|
||||
const [, x, y, color] = commandMatch;
|
||||
console.log(`Parsed Command: !place ${x}${y} ${color}`);
|
||||
sendPlaceCommand(chatMessage, username, x, y, color);
|
||||
} else {
|
||||
console.log('No matching command found in the message.');
|
||||
}
|
||||
} else {
|
||||
console.log('Message is not a PRIVMSG.');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket Error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
console.log('WebSocket attempting to reconnect');
|
||||
setTimeout(connectWebSocket, 5000); // Reconnect after 5 seconds
|
||||
};
|
||||
}
|
||||
|
||||
function parseTwitchMessage(message) {
|
||||
console.log('Raw Message:', message);
|
||||
|
||||
const prefixEnd = message.indexOf(' ');
|
||||
const prefix = message.substring(1, prefixEnd);
|
||||
|
||||
const commandEnd = message.indexOf(' ', prefixEnd + 1);
|
||||
const command = message.substring(prefixEnd + 1, commandEnd);
|
||||
|
||||
const params = message.substring(commandEnd + 1).split(' :');
|
||||
const tagsStart = message.indexOf('@');
|
||||
const tags = {};
|
||||
|
||||
if (tagsStart !== -1) {
|
||||
const tagsEnd = message.indexOf(' ', tagsStart + 1);
|
||||
const tagsRaw = message.substring(tagsStart + 1, tagsEnd);
|
||||
tagsRaw.split(';').forEach(tag => {
|
||||
const [key, value] = tag.split('=');
|
||||
tags[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Parsed Tags:', tags);
|
||||
console.log('Parsed Command:', command);
|
||||
console.log('Parsed Params:', params);
|
||||
|
||||
return { prefix, command, params, tags };
|
||||
}
|
||||
|
||||
function sendPlaceCommand(chatMessage, username, x, y, color) {
|
||||
console.log(`Sending command to server: !place ${x}${y} ${color}`);
|
||||
fetch('/twitch/parse', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ message: chatMessage, username: username, game_session_id: 1 }) // Adjust game_session_id as needed
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Server Response:', data);
|
||||
if (data && data.command === 'place') {
|
||||
console.log('Parsed Place Command from server:', data);
|
||||
colorCell(`${data.x}${data.y}`, data.color);
|
||||
} else {
|
||||
console.error('Invalid command in server response.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Parsing Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Connect to the WebSocket when the script loads
|
||||
connectWebSocket();
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
@ -1,43 +1,13 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('Dashboard') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900">
|
||||
<div id="tmi-status" class="mt-4"></div>
|
||||
<div id="chat-messages" class="mt-4"></div>
|
||||
<div id="dice-result" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tmiStatus = document.getElementById('tmi-status');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
const diceResult = document.getElementById('dice-result');
|
||||
|
||||
window.addEventListener('tmi-connected', function() {
|
||||
tmiStatus.innerText = 'Connected to Twitch chat!';
|
||||
});
|
||||
|
||||
window.addEventListener('chat-message', function(event) {
|
||||
console.log('Chat Message Event:', event.detail);
|
||||
const message = document.createElement('div');
|
||||
message.innerText = `${event.detail.user} : ${event.detail.message}`;
|
||||
chatMessages.appendChild(message);
|
||||
});
|
||||
|
||||
window.addEventListener('dice-rolled', function(event) {
|
||||
console.log('Dice Rolled Event:', event.detail);
|
||||
diceResult.innerText = 'Dice roll result: ' + event.detail;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dynamic Grid</title>
|
||||
@vite(['resources/css/grid.css', 'resources/js/grid.js', 'resources/js/twitch.js'])
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="grid-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -12,11 +12,10 @@
|
|||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/client.js' ])
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/twitch.js' ])
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
@include('layouts.navigation')
|
||||
|
||||
<!-- Page Heading -->
|
||||
@isset($header)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user