stage 0 database schema ready

This commit is contained in:
portakal 2026-05-25 20:07:14 +03:00
commit 7720522a32
18 changed files with 5577 additions and 4 deletions

6
.gitignore vendored
View file

@ -48,6 +48,6 @@ go.work.sum
.env
# Editor/IDE
# .idea/
# .vscode/
.idea/
.vscode/
gitpushlink

View file

@ -1 +1,62 @@
package backend
package main
import (
"context"
"database/sql"
"fmt"
"os"
"skyrama_clone_backend/backend/db"
_ "github.com/go-sql-driver/mysql" // MySQL Driver
"golang.org/x/crypto/bcrypt"
)
func create_user(queries *db.Queries, ctx context.Context, username string, password string) {
hashedPass, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
resultt, err := queries.CreateUser(ctx, db.CreateUserParams{
Username: username,
PasswordHash: string(hashedPass),
})
if err != nil {
fmt.Printf("Error at operation : %v\n", err)
os.Exit(1)
}
fmt.Println(resultt)
fmt.Printf("added user : %s ", username)
}
func update_user(queries *db.Queries, ctx context.Context, username string, password string) {
hashed_pass, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
err := queries.UpdateUserPasswordByUserName(ctx, db.UpdateUserPasswordByUserNameParams{
PasswordHash: string(hashed_pass),
Username: username,
})
if err != nil {
fmt.Printf("Error at operation : %v\n", err)
os.Exit(1)
}
fmt.Printf("updated user : %s with %s \n", username, string(hashed_pass))
fmt.Println("successfully updated user")
}
func main() {
dsn := "skyramaUser:Skyrama1234.@tcp(127.0.0.1:3306)/skyrama_clone?parseTime=true"
dbConn, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Printf("Error opening database pool: %v\n", err)
os.Exit(1)
}
defer dbConn.Close()
if err := dbConn.Ping(); err != nil {
fmt.Printf("Database unreachable: %v\n", err)
os.Exit(1)
}
ctx := context.Background()
queries := db.New(dbConn)
create_user(queries, ctx, "tester_adnan", "a")
//update_user(queries, ctx, "tester_adnan", "a")
}

View file

@ -0,0 +1,336 @@
-- ============================================================================
-- 1. AUTHENTICATION & USERS
-- ============================================================================
-- name: CreateUser :execresult
INSERT INTO users (username, password_hash)
VALUES (?, ?);
-- name: UpdateUserPasswordByUserName :exec
UPDATE users
SET password_hash = ?
WHERE username = ?;
-- name: GetUserByUsername :one
SELECT id, username, password_hash
FROM users
WHERE username = ? LIMIT 1;
-- name: GetUserIdByUsername :one
SELECT id
FROM users
WHERE username = ? LIMIT 1;
-- name: DeleteUserByUsername :exec
UPDATE users
SET deleted_at = CURRENT_TIMESTAMP
WHERE username = ?;
-- name: DeleteAirportByUserName :exec
UPDATE airports
SET is_deleted = TRUE
WHERE user_id = (SELECT id FROM users WHERE username = ?);
-- ============================================================================
-- 2. AIRPORT CORE MANAGEMENT
-- ============================================================================
-- airport row management
-- name: CreateAirportByUserName :execresult
INSERT INTO airports (name, user_id, origin_country_id)
VALUES (?, (SELECT id FROM users WHERE username = ?), ?);
-- name: GetAirportByUserId :one
SELECT id, name, user_id, origin_country_id, cash, current_passengers, max_passenger_capacity, current_fuel, max_fuel_capacity, is_available, player_level, experience_points, passenger_generation_rate, fuel_generation_rate, resources_last_updated_at, is_deleted
FROM airports
WHERE user_id = ? LIMIT 1;
-- name: GetAirportByUserName :one
SELECT id, name, user_id, origin_country_id, cash, current_passengers, max_passenger_capacity, current_fuel, max_fuel_capacity, is_available, player_level, experience_points ,passenger_generation_rate, fuel_generation_rate, resources_last_updated_at, is_deleted
FROM airports
WHERE user_id = (SELECT id FROM users WHERE username = ?) LIMIT 1;
-- airport resource management
-- name: UpdateAirportCurrencies :exec
UPDATE airports
SET cash = ?,
current_passengers = ?,
current_fuel = ?
WHERE id = ?;
-- name: UpdateAirportResources :exec
UPDATE airports
SET
current_fuel = LEAST(
max_fuel_capacity,
current_fuel + (fuel_generation_rate * TIMESTAMPDIFF(SECOND, resources_last_updated_at, NOW()))
),
current_passengers = LEAST(
max_passenger_capacity,
current_passengers + (passenger_generation_rate * TIMESTAMPDIFF(SECOND, resources_last_updated_at, NOW()))
),
resources_last_updated_at = NOW()
WHERE id = ?;
-- name: UpdateAirportCash :exec
UPDATE airports
SET cash = ?
WHERE id = ?;
-- name: UpdateAirportpassengers :exec
UPDATE airports
SET current_passengers = ?
WHERE id = ?;
-- name: UpdateAirportFuel :exec
UPDATE airports
SET current_fuel = ?
WHERE id = ?;
-- name: UpdateAirportmaxPassengers :exec
UPDATE airports
SET max_passenger_capacity = ?
WHERE id = ?;
-- name: UpdateAirportMaxFuel :exec
UPDATE airports
SET max_fuel_capacity = ?
WHERE id = ?;
-- name: UpdateAirportFuelRate :exec
UPDATE airports
SET fuel_generation_rate = ?
WHERE id = ?;
-- name: UpdateAirportPassengerRate :exec
UPDATE airports
SET passenger_generation_rate = ?
WHERE id = ?;
-- name: UpdateAirportResourcesUpdated :exec
update airports
set resources_last_updated_at = ?
where id = ?;
-- name: updatePlayerExperience :exec
UPDATE airports
SET experience_points = ?
WHERE id = ?;
-- name: updatePlayerLevel :exec
UPDATE airports
SET player_level = ?
WHERE id = ?;
-- ============================================================================
-- 3. WAREHOUSE & INVENTORY SYSTEM
-- ============================================================================
-- ============================================================================
-- 4. BUILDINGS & PRODUCTION ENGINE
-- ============================================================================
-- name: addStorageBuilding :execresult
INSERT INTO buildings (name,min_level,construction_cost_cash,capacity,storage_type,storage_subtype)
VALUES (?, ?, ?, ?, ?, ?);
-- name: addOperationalBuilding :execresult
INSERT INTO buildings (name,min_level,construction_cost_cash,operation_type,operation_size)
VALUES (?, ?, ?, ?, ?);
-- name: addPassiveProductionBuilding :execresult
INSERT INTO buildings (name,min_level,construction_cost_cash,passive_type,passive_rate)
VALUES (?, ?, ?, ?, ?);
-- name: addBuilding :execresult
INSERT INTO buildings (name,min_level,construction_cost_cash)
VALUES (?, ?, ?);
-- name: AddBuildingToAirportByUsername :execresult
INSERT INTO airport_buildings (airport_id, building_id)
VALUES ((select id from airports where user_id = (select id from users where username = ?)), ?);
-- name: GetAirportBuildingsByUsername :many
SELECT ab.id AS airport_building_id,b.building_type , b.*
FROM airport_buildings ab
JOIN buildings b ON ab.building_id = b.id
WHERE ab.airport_id = (select id from airports where user_id = (select id from users where username = ?));
-- planes & fleet management
-- name: addPlane :execresult
INSERT INTO planes (name, aircraft_size,operation_type,aircraft_type ,min_level, operation_cost_fuel, operation_cost_cash, max_passengers, travel_time_inSeconds,buying_cost_cash, completed_flight_experience, completed_flight_cash_reward, completed_flight_cargo_reward)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-- name: GetPlaneById :one
SELECT * FROM planes WHERE id = ?;
-- name: GetAllPlanes :many
SELECT * FROM planes;
-- name: GetPlanesByOperationType :many
SELECT * FROM planes WHERE operation_type = ?;
-- name: GetPlanesByMinLevel :many
SELECT * FROM planes WHERE min_level <= ?;
-- name: GetPlanesByAircraftSize :many
SELECT * FROM planes WHERE aircraft_size = ?;
-- name: GetPlanesByAircraftType :many
SELECT * FROM planes WHERE aircraft_type = ?;
-- name: addPlaneToAirportByUsername :execresult
INSERT INTO player_planes (airport_id, plane_id, assigned_hangar_id, current_building_id, status)
VALUES ((select id from airports where user_id = (select id from users where username = ?)), ?, ?, ?, 'idle');
-- name: GetPlayerPlanesByUsername :many
SELECT pp.id AS player_plane_id, pp.*
FROM player_planes pp
JOIN planes p ON pp.plane_id = p.id
WHERE pp.airport_id = (select id from airports where user_id = (select id from users where username = ?));
-- name: FindAvailableInfrastructureForPlane :one
SELECT ab.id
FROM airport_buildings ab
JOIN buildings b ON ab.building_id = b.id
LEFT JOIN player_planes pp ON ab.id = pp.current_building_id
WHERE ab.airport_id = (select id from airports where user_id = (select id from users where username = ?))
AND b.operation_type = ? -- 'runway' or 'service' (service bay)
AND b.operation_size = ? -- matches plane's aircraft_size requirement
AND pp.id IS NULL -- Ensures no other plane is physically here right now
LIMIT 1;
-- name: UpdatePlayerPlaneState :exec
UPDATE player_planes
SET current_building_id = ?,
status = ?,
flight_start_time = ?
WHERE id = ?;
-- name: CheckHangarOccupancyByUsername :one
SELECT COUNT(*) AS total_parked
FROM player_planes pp
JOIN airport_buildings ab ON pp.current_building_id = ab.id
WHERE ab.airport_id = (select id from airports where user_id = (select id from users where username = ?))
AND ab.building_id = ?; -- specific hangar
--
-- name: CheckRunwayOccupancyByUsername :one
SELECT COUNT(*) AS total_occupied
FROM player_planes pp
JOIN airport_buildings ab ON pp.current_building_id = ab.id
WHERE ab.airport_id = (select id from airports where user_id = (select id from users where username = ?))
AND ab.building_id = ?; -- specific runway
-- name: GetPlanesReadyToLandByUsername :many
SELECT pp.id AS player_plane_id, pp.airport_id, pp.assigned_hangar_id,
p.completed_flight_experience, p.completed_flight_cash_reward, p.completed_flight_cargo_reward, p.operation_type
FROM player_planes pp
JOIN planes p ON pp.plane_id = p.id
WHERE pp.status = 'flying'
AND ADDTIME(pp.flight_start_time, p.travel_time) <= NOW()
AND pp.airport_id = (select id from airports where user_id = (select id from users where username = ?));
-- ============================================================================
-- 5. FLEET & DYNAMIC INFRASTRUCTURE OPERATIONS
-- ============================================================================
-- name: BuyPlaneForPlayer :execresult
INSERT INTO player_planes (airport_id, plane_id, assigned_hangar_id, current_building_id, status)
VALUES (?, ?, ?, ?, 'idle');
-- name: GetPlayerFleet :many
SELECT pp.id AS player_plane_id, pp.status, pp.updated_at, pp.flight_start_time, pp.current_building_id, pp.assigned_hangar_id,p.*
FROM player_planes pp
JOIN planes p ON pp.plane_id = p.id
WHERE pp.airport_id = ?;
-- name: FindAvailableInfrastructure :one
SELECT ab.id
FROM airport_buildings ab
JOIN buildings b ON ab.building_id = b.id
LEFT JOIN player_planes pp ON ab.id = pp.current_building_id
WHERE ab.airport_id = ?
AND b.operation_type = ? -- 'runway' or 'service' (service bay)
AND b.operation_size = ? -- matches plane's aircraft_size requirement
AND pp.id IS NULL -- Ensures no other plane is physically here right now
LIMIT 1;
-- name: UpdatePlaneState :exec
UPDATE player_planes
SET current_building_id = ?,
status = ?,
flight_start_time = ?
WHERE id = ?;
-- name: CheckHangarOccupancy :one
SELECT COUNT(*) AS total_parked
FROM player_planes
WHERE current_building_id = ?;
-- ============================================================================
-- 6. TIME-BASED FLIGHT ENGINE
-- ============================================================================
-- name: GetPlanesReadyToLand :many
SELECT pp.id AS player_plane_id, pp.airport_id, pp.assigned_hangar_id,
p.completed_flight_experience, p.completed_flight_cash_reward, p.completed_flight_cargo_reward, p.operation_type
FROM player_planes pp
JOIN planes p ON pp.plane_id = p.id
WHERE pp.status = 'flying'
-- Uses your logic: flight_start_time + plane travel duration <= current clock
AND ADDTIME(pp.flight_start_time, p.travel_time) <= NOW();
-- ============================================================================
-- 7. PROGRESSION CONTRACTS & ECONOMY LOGS
-- ============================================================================
-- name: GetActiveContracts :many
SELECT id, title, required_product_id, required_amount, current_amount_delivered, is_completed, expires_at
FROM airport_contracts
WHERE airport_id = ? AND is_completed = FALSE AND expires_at > NOW();
-- name: UpdateContractDelivery :exec
UPDATE airport_contracts
SET current_amount_delivered = current_amount_delivered + ?
WHERE id = ? AND is_completed = FALSE;
-- name: CompleteContract :exec
UPDATE airport_contracts
SET is_completed = TRUE
WHERE id = ?;
-- name: LogCurrencyTransaction :exec
INSERT INTO currency_transaction_logs (airport_id, amount_changed, currency_type, reason)
VALUES (?, ?, ?, ?);
-- name: LogCompletedFlightRoute :exec
INSERT INTO completed_flight_routes_log (origin_airport_id, destination_airport_id, start_time, end_time)
VALUES (?, ?, ?, NOW());

View file

@ -0,0 +1,218 @@
-- ============================================================================
-- 1. AUTHENTICATION & USERS
-- ============================================================================
-- name: CreateUser :execresult
INSERT INTO users (username, password_hash)
VALUES (?, ?);
-- name: UpdateUserPasswordByUserName :exec
UPDATE users
SET password_hash = ?
WHERE username = ?;
-- name: GetUserByUsername :one
SELECT id, username, password_hash, created_at
FROM users
WHERE username = ? LIMIT 1;
-- name: GetUserIdByUsername :one
SELECT id
FROM users
WHERE username = ? LIMIT 1;
-- ============================================================================
-- 2. AIRPORT CORE MANAGEMENT
-- ============================================================================
-- airport row management
-- name: CreateAirportByUserName :execresult
INSERT INTO airports (name, user_id, origin_country_id)
VALUES (?, (SELECT id FROM users WHERE username = ?), ?);
-- name: GetAirportByUserId :one
SELECT id, name, user_id, origin_country_id, cash, current_passengers, max_passenger_capacity, current_fuel, max_fuel_capacity, is_available, player_level, experience_points, passenger_generation_rate, fuel_generation_rate, resources_last_updated_at
FROM airports
WHERE user_id = ? LIMIT 1;
-- name: GetAirportByUserName :one
SELECT id, name, user_id, origin_country_id, cash, current_passengers, max_passenger_capacity, current_fuel, max_fuel_capacity, is_available, player_level, experience_points ,passenger_generation_rate, fuel_generation_rate, resources_last_updated_at
FROM airports
WHERE user_id = (SELECT id FROM users WHERE username = ?) LIMIT 1;
-- airport resource management
-- name: UpdateAirportCurrencies :exec
UPDATE airports
SET cash = ?,
current_passengers = ?,
current_fuel = ?
WHERE id = ?;
-- name: UpdateAirportCash :exec
UPDATE airports
SET cash = ?
WHERE id = ?;
-- name: UpdateAirportpassengers :exec
UPDATE airports
SET current_passengers = ?
WHERE id = ?;
-- name: UpdateAirportFuel :exec
UPDATE airports
SET current_fuel = ?
WHERE id = ?;
-- name: UpdateAirportmaxPassengers :exec
UPDATE airports
SET max_passenger_capacity = ?
WHERE id = ?;
-- name: UpdateAirportMaxFuel :exec
UPDATE airports
SET max_fuel_capacity = ?
WHERE id = ?;
-- name: UpdateAirportFuelRate :exec
UPDATE airports
SET fuel_generation_rate = ?
WHERE id = ?;
-- name: UpdateAirportPassengerRate :exec
UPDATE airports
SET passenger_generation_rate = ?
WHERE id = ?;
-- name: UpdateAirportResourcesUpdated :exec
update airports
set resources_last_updated_at = ?
where id = ?;
-- name: updatePlayerExperience :exec
UPDATE airports
SET experience_points = ?,
WHERE id = ?;
-- name: updatePlayerLevel :exec
UPDATE airports
SET player_level = ?
WHERE id = ?;
-- ============================================================================
-- 3. WAREHOUSE & INVENTORY SYSTEM
-- ============================================================================
-- ============================================================================
-- 4. BUILDINGS & PRODUCTION ENGINE
-- ============================================================================
-- name: addStorageBuilding:execresult
INSERT INTO buildings (name,min_level,construction_cost_cash,capacity,storage_type,storage_subtype)
VALUES (?, ?, ?, ?, ?, ?);
-- name: addOperationalBuilding:execresult
INSERT INTO buildings (name,min_level,construction_cost_cash,operation_type,operation_size)
VALUES (?, ?, ?, ?, ?);
-- name: addPassiveProductionBuilding:execresult
INSERT INTO buildings (name,min_level,construction_cost_cash,currency_type,income_rate)
VALUES (?, ?, ?, ?, ?);
-- name: addProductionBuilding:execresult
INSERT INTO buildings (name,min_level,construction_cost_cash,recipe_id,storage_1,storage_2,storage_3)
VALUES (?, ?, ?, ?, ?, ?, ?);
-- name: AddBuildingToAirport :execresult
INSERT INTO airport_buildings (airport_id, building_id, level)
VALUES ((select id from airports where user_id = (select id from users where username = ?)), ?, 1);
-- name: GetAirportBuildings :many
SELECT ab.id AS airport_building_id, ab.level, b.*
FROM airport_buildings ab
JOIN buildings b ON ab.building_id = b.id
WHERE ab.airport_id = (select id from airports where user_id = (select id from users where username = ?));
-- ============================================================================
-- 5. FLEET & DYNAMIC INFRASTRUCTURE OPERATIONS
-- ============================================================================
-- name: BuyPlaneForPlayer :execresult
INSERT INTO player_planes (airport_id, plane_id, assigned_hangar_id, current_building_id, status)
VALUES (?, ?, ?, ?, 'idle');
-- name: GetPlayerFleet :many
SELECT pp.id AS player_plane_id, pp.status, pp.updated_at, pp.flight_start_time, pp.current_building_id, pp.assigned_hangar_id, pp.assigned_cargo_id,
p.*
FROM player_planes pp
JOIN planes p ON pp.plane_id = p.id
WHERE pp.airport_id = ?;
-- name: FindAvailableInfrastructure :one
SELECT ab.id
FROM airport_buildings ab
JOIN buildings b ON ab.building_id = b.id
LEFT JOIN player_planes pp ON ab.id = pp.current_building_id
WHERE ab.airport_id = ?
AND b.operation_type = ? -- 'runway' or 'service' (service bay)
AND b.operation_size = ? -- matches plane's aircraft_size requirement
AND pp.id IS NULL -- Ensures no other plane is physically here right now
LIMIT 1;
-- name: UpdatePlaneState :exec
UPDATE player_planes
SET current_building_id = ?,
status = ?,
assigned_cargo_id = ?,
flight_start_time = ?
WHERE id = ?;
-- name: CheckHangarOccupancy :one
SELECT COUNT(*) AS total_parked
FROM player_planes
WHERE current_building_id = ?;
-- ============================================================================
-- 6. TIME-BASED FLIGHT ENGINE
-- ============================================================================
-- name: GetPlanesReadyToLand :many
SELECT pp.id AS player_plane_id, pp.airport_id, pp.assigned_hangar_id,
p.completed_flight_experience, p.completed_flight_cash_reward, p.completed_flight_cargo_reward, p.operation_type, pp.assigned_cargo_id
FROM player_planes pp
JOIN planes p ON pp.plane_id = p.id
WHERE pp.status = 'flying'
-- Uses your logic: flight_start_time + plane travel duration <= current clock
AND ADDTIME(pp.flight_start_time, p.travel_time) <= NOW();
-- ============================================================================
-- 7. PROGRESSION CONTRACTS & ECONOMY LOGS
-- ============================================================================
-- name: GetActiveContracts :many
SELECT id, title, required_product_id, required_amount, current_amount_delivered, is_completed, expires_at
FROM airport_contracts
WHERE airport_id = ? AND is_completed = FALSE AND expires_at > NOW();
-- name: UpdateContractDelivery :exec
UPDATE airport_contracts
SET current_amount_delivered = current_amount_delivered + ?
WHERE id = ? AND is_completed = FALSE;
-- name: CompleteContract :exec
UPDATE airport_contracts
SET is_completed = TRUE
WHERE id = ?;
-- name: LogCurrencyTransaction :exec
INSERT INTO currency_transaction_logs (airport_id, amount_changed, currency_type, reason)
VALUES (?, ?, ?, ?);
-- name: LogCompletedFlightRoute :exec
INSERT INTO completed_flight_routes_log (origin_airport_id, destination_country_id, start_time, end_time)
VALUES (?, ?, ?, NOW());

View file

@ -0,0 +1,260 @@
-- stages
-- stage 0 : launch planes to country's base airport (accepts everyhing sendds nothing back)
-- stage 1 : add NPCs and cargo planes and shop buildings.
-- stage 2 : add player progression, building upgrades, effects and more complex recipes.
-- stage 3 : TBD
-- good
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
username varchar(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP DEFAULT NULL
);
-- product table act as a product identifier ie egg, milk, flour etc
create table if not exists products (
id smallint unsigned primary key auto_increment,
name varchar(255) not null
);
-- for stage 2 will not be used
-- increasing speed (crafting,currency type generation etc)),
CREATE table if not exists effects (
id smallint unsigned primary key auto_increment,
name varchar(255) not null
);
-- used airport grouping and for contracts, each country has 3 produce they can export there will not be cap
create table if not exists countries (
id smallint unsigned primary key auto_increment,
name varchar(255) not null,
continent ENUM('void','Africa', 'Antarctica', 'Asia', 'Europe', 'North America', 'Oceania', 'South America') NOT NULL, -- universal enum
produce_1_id smallint unsigned not null,
produce_2_id smallint unsigned not null,
produce_3_id smallint unsigned not null,
foreign key (produce_1_id) references products(id),
foreign key (produce_2_id) references products(id),
foreign key (produce_3_id) references products(id)
);
-- main table
-- primary access table act as a player identifier, each player can only have one airport, but they can have multiple planes and buildings
create table if not exists airports (
id integer unsigned primary key auto_increment,
name varchar(255) not null,
user_id integer unique not null,
origin_country_id smallint unsigned not null,
-- resources
cash bigint unsigned not null default 100,
current_passengers INTEGER UNSIGNED NOT NULL DEFAULT 100,
max_passenger_capacity INTEGER UNSIGNED NOT NULL DEFAULT 100,
current_fuel INTEGER UNSIGNED NOT NULL DEFAULT 500,
max_fuel_capacity INTEGER UNSIGNED NOT NULL DEFAULT 1000,
-- resource generation
fuel_generation_rate INT UNSIGNED NOT NULL DEFAULT 20,
passenger_generation_rate INT UNSIGNED NOT NULL DEFAULT 5,
resources_last_updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- progression
is_available boolean not null default true, -- if true can be sent planes from anyone
player_level integer unsigned not null default 1,
experience_points bigint unsigned not null default 0,
is_deleted BOOLEAN DEFAULT FALSE,
foreign key (origin_country_id) references countries(id),
foreign key (user_id) references users(id),
constraint chk_fuel_capacity CHECK (current_fuel <= max_fuel_capacity),
constraint chk_positive_fuel CHECK (current_fuel >= 0),
CONSTRAINT chk_passenger_capacity CHECK (current_passengers <= max_passenger_capacity),
CONSTRAINT chk_positive_passenger CHECK (current_passengers >= 0)
);
-- for stage 1, used as template for recipes.
create table if not exists recipes (
id integer unsigned primary key auto_increment,
name varchar(255) not null,
ingredient_1_id smallint unsigned ,
ingredient_1_amount integer unsigned ,
ingredient_2_id smallint unsigned,
ingredient_2_amount integer unsigned ,
ingredient_3_id smallint unsigned ,
ingredient_3_amount integer unsigned ,
product_id smallint unsigned not null,
yield_amount integer unsigned not null,
foreign key (ingredient_1_id) references products(id),
foreign key (ingredient_2_id) references products(id),
foreign key (ingredient_3_id) references products(id),
foreign key (product_id) references products(id)
);
-- BUILDNGS
-- hangars fuel tanks etc
CREATE table if not exists buildings (
-- shared
id integer unsigned primary key auto_increment,
name varchar(255) not null,
min_level integer unsigned not null DEfAULT 1,
construction_cost_cash integer unsigned not null default 100,
upgrade_tier TINYINT UNSIGNED DEFAULT 0,
upgrade_cost_cash_to_uptier integer unsigned default null,
building_type ENUM('none','storage', 'support', 'operation', 'passive', 'shop', 'production') not null,
-- production specific
-- storage
capacity integer unsigned ,
storage_type ENUM( 'fuel' , 'plane', 'product'),
storage_subtype ENUM('none', 'airplane', 'helicopter', 'seaplane','spaceship'),
-- support
effect_id smallint unsigned,
effect_value integer,
-- operation
operation_size enum('small','medium' ,'large', 'huge'),
operation_type enum('runway', 'service', 'production'),
-- passive income
passive_type enum('passengers', 'fuel'),
passive_rate integer ,
foreign key (effect_id) references effects(id)
);
-- stage 1
create table if not exists building_products (
id integer unsigned primary key auto_increment,
building_id integer unsigned not null,
recipe_id integer unsigned,
storage_product integer unsigned ,
storage_1 integer unsigned ,
storage_2 integer unsigned ,
storage_3 integer unsigned ,
foreign key (building_id) references buildings(id),
foreign key (recipe_id) references recipes(id)
);
-- many to many relation between airport and buildings
create table if not exists airport_buildings (
id integer unsigned primary key auto_increment,
airport_id integer unsigned not null,
building_id integer unsigned not null,
-- shop specific
recipe_id integer unsigned ,
storage_product integer unsigned ,
storage_1 integer unsigned ,
storage_2 integer unsigned ,
storage_3 integer unsigned ,
level integer unsigned not null default 1,
foreign key (airport_id) references airports(id) ,
foreign key (building_id) references buildings(id)
);
-- planes
-- template for planes
create table if not exists planes (
id integer unsigned primary key auto_increment,
name varchar(255) not null,
min_level integer unsigned not null default 1,
operation_cost_fuel integer unsigned not null,
operation_cost_cash integer unsigned not null DEFAULT 10,
max_passengers integer unsigned,
travel_time_inSeconds integer unsigned not null,
operation_type enum('passenger', 'cargo') not null,
aircraft_size enum('small', 'medium', 'large', 'huge') not null,
aircraft_type enum('plane', 'helicopter', 'seaplane','spaceship') not null,
buying_cost_cash integer unsigned not null DEFAULT 100,
completed_flight_experience integer unsigned not null DEFAULT 20,
completed_flight_cash_reward integer unsigned DEFAULT 50,
completed_flight_cargo_reward integer unsigned DEFAULT null
);
-- actual planes owned by players, tracks location, status, assigned hangar etc
CREATE TABLE IF NOT EXISTS player_planes (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
airport_id INT UNSIGNED NOT NULL,
plane_id INTEGER UNSIGNED NOT NULL,
-- 1. assignment (Must always point to a hangar they own they move between suitable hangars)
assigned_hangar_id INT UNSIGNED NOT NULL,
-- 2. Real-time physical location (stored, Service Bay, Runway, or NULL if flying)
current_building_id INT UNSIGNED DEFAULT NULL,
-- assigned_cargo_id INTEGER UNSIGNED DEFAULT NULL, -- stage 1 For cargo planes, track what they're carrying
status ENUM('idle', 'maintenance', 'boarding', 'taxiing', 'flying') NOT NULL DEFAULT 'idle',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
flight_start_time TIMESTAMP NULL DEFAULT NULL,
FOREIGN KEY (airport_id) REFERENCES airports(id),
FOREIGN KEY (plane_id) REFERENCES planes(id),
FOREIGN KEY (assigned_hangar_id) REFERENCES airport_buildings(id),
FOREIGN KEY (current_building_id) REFERENCES airport_buildings(id)
);
-- stage 1
CREATE TABLE IF NOT EXISTS airport_inventory (
airport_id INTEGER unsigned NOT NULL,
product_id SMALLINT UNSIGNED NOT NULL,
quantity INTEGER UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (airport_id, product_id),
FOREIGN KEY (airport_id) REFERENCES airports(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- stage ? tbd
CREATE TABLE IF NOT EXISTS airport_contracts (
id INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
airport_id INTEGER unsigned NOT NULL,
title VARCHAR(255) NOT NULL,
-- What they need to deliver
required_product_id SMALLINT UNSIGNED NOT NULL,
required_amount INTEGER UNSIGNED NOT NULL,
current_amount_delivered INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Status
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
expires_at TIMESTAMP NOT NULL,
FOREIGN KEY (airport_id) REFERENCES airports(id),
FOREIGN KEY (required_product_id) REFERENCES products(id)
);
-- logs
create table if not exists completed_flight_routes_log (
id integer unsigned primary key auto_increment,
origin_airport_id integer unsigned not null,
destination_airport_id integer unsigned not null,
start_time timestamp not null default current_timestamp,
end_time timestamp default null,
foreign key (origin_airport_id) references airports(id),
foreign key (destination_airport_id) references airports(id)
);
CREATE TABLE IF NOT EXISTS currency_transaction_logs (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
airport_id INT UNSIGNED NOT NULL,
amount_changed INT NOT NULL, -- Negative for buying, positive for earning
currency_type ENUM('cash', 'fuel', 'passengers') NOT NULL,
reason VARCHAR(255) NOT NULL, -- e.g., 'bought_plane_id_12', 'completed_flight_34'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (airport_id) REFERENCES airports(id)
);

View file

@ -0,0 +1,254 @@
-- stages
-- stage 0 : launch planes to country's base airport (accepts everyhing sendds nothing back)
-- stage 1 : add NPCs and cargo planes and shop buildings.
-- stage 2 : add player progression, building upgrades, effects and more complex recipes.
-- stage 3 : TBD
-- good
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
username varchar(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- product table act as a product identifier ie egg, milk, flour etc
create table if not exists products (
id smallint unsigned primary key auto_increment,
name varchar(255) not null
);
-- for stage 2 will not be used
-- increasing speed (crafting,currency type generation etc)),
CREATE table if not exists effects (
id smallint unsigned primary key auto_increment,
name varchar(255) not null
);
-- used airport grouping and for contracts, each country has 3 produce they can export there will not be cap
create table if not exists countries (
id smallint unsigned primary key auto_increment,
name varchar(255) not null,
continent ENUM('void','Africa', 'Antarctica', 'Asia', 'Europe', 'North America', 'Oceania', 'South America') NOT NULL, -- universal enum
produce_1_id smallint unsigned not null,
produce_2_id smallint unsigned not null,
produce_3_id smallint unsigned not null,
foreign key (produce_1_id) references products(id),
foreign key (produce_2_id) references products(id),
foreign key (produce_3_id) references products(id)
);
-- main table
-- primary access table act as a player identifier, each player can only have one airport, but they can have multiple planes and buildings
create table if not exists airports (
id integer unsigned primary key auto_increment,
name varchar(255) not null,
user_id integer unique not null,
origin_country_id smallint unsigned not null,
-- resources
cash bigint unsigned not null default 100,
current_passengers INTEGER UNSIGNED NOT NULL DEFAULT 100,
max_passenger_capacity INTEGER UNSIGNED NOT NULL DEFAULT 100,
current_fuel INTEGER UNSIGNED NOT NULL DEFAULT 500,
max_fuel_capacity INTEGER UNSIGNED NOT NULL DEFAULT 1000,
-- resource generation
fuel_generation_rate INT UNSIGNED NOT NULL DEFAULT 20,
passenger_generation_rate INT UNSIGNED NOT NULL DEFAULT 5,
resources_last_updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- progression
is_available boolean not null default true, -- if true can be sent planes from anyone
player_level integer unsigned not null default 1,
experience_points bigint unsigned not null default 0,
foreign key (origin_country_id) references countries(id),
foreign key (user_id) references users(id),
constraint chk_fuel_capacity CHECK (current_fuel <= max_fuel_capacity),
constraint chk_positive_fuel CHECK (current_fuel >= 0)
);
-- for stage 1, used as template for recipes.
create table if not exists recipes (
id integer unsigned primary key auto_increment,
name varchar(255) not null,
ingredient_1_id smallint unsigned ,
ingredient_1_amount integer unsigned ,
ingredient_2_id smallint unsigned,
ingredient_2_amount integer unsigned ,
ingredient_3_id smallint unsigned ,
ingredient_3_amount integer unsigned ,
product_id smallint unsigned not null,
yield_amount integer unsigned not null,
foreign key (ingredient_1_id) references products(id),
foreign key (ingredient_2_id) references products(id),
foreign key (ingredient_3_id) references products(id),
foreign key (product_id) references products(id)
);
-- BUILDNGS
-- hangars fuel tanks etc
CREATE table if not exists buildings (
-- shared
id integer unsigned primary key auto_increment,
name varchar(255) not null,
min_level integer unsigned not null DEfAULT 1,
construction_cost_cash integer unsigned not null default 100,
upgrade_tier TINYINT UNSIGNED DEFAULT 0,
upgrade_cost_cash_to_uptier integer unsigned default null,
-- storage
capacity integer unsigned ,
storage_type ENUM( 'fuel' , 'plane', 'product'),
storage_subtype ENUM('none', 'airplane', 'helicopter', 'seaplane','spaceship'),
-- support
effect_id smallint unsigned,
effect_value integer,
-- operation
operation_size enum('small','medium' ,'large', 'huge'),
operation_type enum('runway', 'service', 'production'),
-- passive income
passive_type enum('passengers', 'fuel'),
passive_rate integer ,
foreign key (effect_id) references effects(id)
);
--stage 1
create table if not exists building_products (
id integer unsigned primary key auto_increment,
building_id integer unsigned not null,
recipe_id integer unsigned,
storage_product integer unsigned ,
storage_1 integer unsigned ,
storage_2 integer unsigned ,
storage_3 integer unsigned ,
foreign key (building_id) references buildings(id),
foreign key (recipe_id) references recipes(id)
);
-- many to many relation between airport and buildings
create table if not exists airport_buildings (
id integer unsigned primary key auto_increment,
airport_id integer unsigned not null,
building_id integer unsigned not null,
-- shop specific
recipe_id integer unsigned ,
storage_product integer unsigned ,
storage_1 integer unsigned ,
storage_2 integer unsigned ,
storage_3 integer unsigned ,
level integer unsigned not null default 1,
foreign key (airport_id) references airports(id) ,
foreign key (building_id) references buildings(id)
);
-- planes
-- template for planes
create table if not exists planes (
id integer unsigned primary key auto_increment,
name varchar(255) not null,
min_level integer unsigned not null default 1,
operation_cost_fuel integer unsigned not null,
operation_cost_cash integer unsigned not null DEFAULT 10,
max_passengers integer unsigned,
travel_time_inSeconds integer unsigned not null,
operation_type enum('passenger', 'cargo') not null,
aircraft_size enum('small', 'medium', 'large', 'huge') not null,
aircraft_type enum('plane', 'helicopter', 'seaplane','spaceship') not null,
buying_cost_cash integer unsigned not null DEFAULT 100,
completed_flight_experience integer unsigned not null DEFAULT 20,
completed_flight_cash_reward integer unsigned DEFAULT 50,
completed_flight_cargo_reward integer unsigned DEFAULT 10
);
-- actual planes owned by players, tracks location, status, assigned hangar etc
CREATE TABLE IF NOT EXISTS player_planes (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
airport_id INT UNSIGNED NOT NULL,
plane_id INTEGER UNSIGNED NOT NULL,
-- 1. assignment (Must always point to a hangar they own they move between suitable hangars)
assigned_hangar_id INT UNSIGNED NOT NULL,
-- 2. Real-time physical location (stored, Service Bay, Runway, or NULL if flying)
current_building_id INT UNSIGNED DEFAULT NULL,
-- assigned_cargo_id INTEGER UNSIGNED DEFAULT NULL, -- stage 1 For cargo planes, track what they're carrying
status ENUM('idle', 'maintenance', 'boarding', 'taxiing', 'flying') NOT NULL DEFAULT 'idle',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
flight_start_time TIMESTAMP NULL DEFAULT NULL,
FOREIGN KEY (airport_id) REFERENCES airports(id),
FOREIGN KEY (plane_id) REFERENCES planes(id),
FOREIGN KEY (assigned_hangar_id) REFERENCES airport_buildings(id),
FOREIGN KEY (current_building_id) REFERENCES airport_buildings(id)
--foreign key (assigned_cargo_id) references products(id) -- stage 1
);
-- stage 1
CREATE TABLE IF NOT EXISTS airport_inventory (
airport_id INTEGER unsigned NOT NULL,
product_id SMALLINT UNSIGNED NOT NULL,
quantity INTEGER UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (airport_id, product_id),
FOREIGN KEY (airport_id) REFERENCES airports(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- stage ? tbd
CREATE TABLE IF NOT EXISTS airport_contracts (
id INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
airport_id INTEGER unsigned NOT NULL,
title VARCHAR(255) NOT NULL,
-- What they need to deliver
required_product_id SMALLINT UNSIGNED NOT NULL,
required_amount INTEGER UNSIGNED NOT NULL,
current_amount_delivered INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Status
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
expires_at TIMESTAMP NOT NULL,
FOREIGN KEY (airport_id) REFERENCES airports(id),
FOREIGN KEY (required_product_id) REFERENCES products(id)
);
-- logs
create table if not exists completed_flight_routes_log (
id integer unsigned primary key auto_increment,
origin_airport_id integer unsigned not null,
destination_airport_id integer unsigned not null,
start_time timestamp not null default current_timestamp,
end_time timestamp default null,
foreign key (origin_airport_id) references airports(id),
foreign key (destination_airport_id) references airports(id)
);
CREATE TABLE IF NOT EXISTS currency_transaction_logs (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
airport_id INT UNSIGNED NOT NULL,
amount_changed INT NOT NULL, -- Negative for buying, positive for earning
currency_type ENUM('cash', 'fuel', 'passengers') NOT NULL,
reason VARCHAR(255) NOT NULL, -- e.g., 'bought_plane_id_12', 'completed_flight_34'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (airport_id) REFERENCES airports(id)
);

View file

@ -0,0 +1,9 @@
version: "2"
sql:
- engine: "mysql"
queries: "queries.sql"
schema: "schema.sql"
gen:
go:
package: "db"
out: "../"

31
backend/db/db.go Normal file
View file

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

704
backend/db/models.go Normal file
View file

@ -0,0 +1,704 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import (
"database/sql"
"database/sql/driver"
"fmt"
"time"
)
type BuildingsBuildingType string
const (
BuildingsBuildingTypeNone BuildingsBuildingType = "none"
BuildingsBuildingTypeStorage BuildingsBuildingType = "storage"
BuildingsBuildingTypeSupport BuildingsBuildingType = "support"
BuildingsBuildingTypeOperation BuildingsBuildingType = "operation"
BuildingsBuildingTypePassive BuildingsBuildingType = "passive"
BuildingsBuildingTypeShop BuildingsBuildingType = "shop"
BuildingsBuildingTypeProduction BuildingsBuildingType = "production"
)
func (e *BuildingsBuildingType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = BuildingsBuildingType(s)
case string:
*e = BuildingsBuildingType(s)
default:
return fmt.Errorf("unsupported scan type for BuildingsBuildingType: %T", src)
}
return nil
}
type NullBuildingsBuildingType struct {
BuildingsBuildingType BuildingsBuildingType
Valid bool // Valid is true if BuildingsBuildingType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullBuildingsBuildingType) Scan(value interface{}) error {
if value == nil {
ns.BuildingsBuildingType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.BuildingsBuildingType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullBuildingsBuildingType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.BuildingsBuildingType), nil
}
type BuildingsOperationSize string
const (
BuildingsOperationSizeSmall BuildingsOperationSize = "small"
BuildingsOperationSizeMedium BuildingsOperationSize = "medium"
BuildingsOperationSizeLarge BuildingsOperationSize = "large"
BuildingsOperationSizeHuge BuildingsOperationSize = "huge"
)
func (e *BuildingsOperationSize) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = BuildingsOperationSize(s)
case string:
*e = BuildingsOperationSize(s)
default:
return fmt.Errorf("unsupported scan type for BuildingsOperationSize: %T", src)
}
return nil
}
type NullBuildingsOperationSize struct {
BuildingsOperationSize BuildingsOperationSize
Valid bool // Valid is true if BuildingsOperationSize is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullBuildingsOperationSize) Scan(value interface{}) error {
if value == nil {
ns.BuildingsOperationSize, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.BuildingsOperationSize.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullBuildingsOperationSize) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.BuildingsOperationSize), nil
}
type BuildingsOperationType string
const (
BuildingsOperationTypeRunway BuildingsOperationType = "runway"
BuildingsOperationTypeService BuildingsOperationType = "service"
BuildingsOperationTypeProduction BuildingsOperationType = "production"
)
func (e *BuildingsOperationType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = BuildingsOperationType(s)
case string:
*e = BuildingsOperationType(s)
default:
return fmt.Errorf("unsupported scan type for BuildingsOperationType: %T", src)
}
return nil
}
type NullBuildingsOperationType struct {
BuildingsOperationType BuildingsOperationType
Valid bool // Valid is true if BuildingsOperationType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullBuildingsOperationType) Scan(value interface{}) error {
if value == nil {
ns.BuildingsOperationType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.BuildingsOperationType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullBuildingsOperationType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.BuildingsOperationType), nil
}
type BuildingsPassiveType string
const (
BuildingsPassiveTypePassengers BuildingsPassiveType = "passengers"
BuildingsPassiveTypeFuel BuildingsPassiveType = "fuel"
)
func (e *BuildingsPassiveType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = BuildingsPassiveType(s)
case string:
*e = BuildingsPassiveType(s)
default:
return fmt.Errorf("unsupported scan type for BuildingsPassiveType: %T", src)
}
return nil
}
type NullBuildingsPassiveType struct {
BuildingsPassiveType BuildingsPassiveType
Valid bool // Valid is true if BuildingsPassiveType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullBuildingsPassiveType) Scan(value interface{}) error {
if value == nil {
ns.BuildingsPassiveType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.BuildingsPassiveType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullBuildingsPassiveType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.BuildingsPassiveType), nil
}
type BuildingsStorageSubtype string
const (
BuildingsStorageSubtypeNone BuildingsStorageSubtype = "none"
BuildingsStorageSubtypeAirplane BuildingsStorageSubtype = "airplane"
BuildingsStorageSubtypeHelicopter BuildingsStorageSubtype = "helicopter"
BuildingsStorageSubtypeSeaplane BuildingsStorageSubtype = "seaplane"
BuildingsStorageSubtypeSpaceship BuildingsStorageSubtype = "spaceship"
)
func (e *BuildingsStorageSubtype) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = BuildingsStorageSubtype(s)
case string:
*e = BuildingsStorageSubtype(s)
default:
return fmt.Errorf("unsupported scan type for BuildingsStorageSubtype: %T", src)
}
return nil
}
type NullBuildingsStorageSubtype struct {
BuildingsStorageSubtype BuildingsStorageSubtype
Valid bool // Valid is true if BuildingsStorageSubtype is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullBuildingsStorageSubtype) Scan(value interface{}) error {
if value == nil {
ns.BuildingsStorageSubtype, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.BuildingsStorageSubtype.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullBuildingsStorageSubtype) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.BuildingsStorageSubtype), nil
}
type BuildingsStorageType string
const (
BuildingsStorageTypeFuel BuildingsStorageType = "fuel"
BuildingsStorageTypePlane BuildingsStorageType = "plane"
BuildingsStorageTypeProduct BuildingsStorageType = "product"
)
func (e *BuildingsStorageType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = BuildingsStorageType(s)
case string:
*e = BuildingsStorageType(s)
default:
return fmt.Errorf("unsupported scan type for BuildingsStorageType: %T", src)
}
return nil
}
type NullBuildingsStorageType struct {
BuildingsStorageType BuildingsStorageType
Valid bool // Valid is true if BuildingsStorageType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullBuildingsStorageType) Scan(value interface{}) error {
if value == nil {
ns.BuildingsStorageType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.BuildingsStorageType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullBuildingsStorageType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.BuildingsStorageType), nil
}
type CountriesContinent string
const (
CountriesContinentVoid CountriesContinent = "void"
CountriesContinentAfrica CountriesContinent = "Africa"
CountriesContinentAntarctica CountriesContinent = "Antarctica"
CountriesContinentAsia CountriesContinent = "Asia"
CountriesContinentEurope CountriesContinent = "Europe"
CountriesContinentNorthAmerica CountriesContinent = "North America"
CountriesContinentOceania CountriesContinent = "Oceania"
CountriesContinentSouthAmerica CountriesContinent = "South America"
)
func (e *CountriesContinent) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = CountriesContinent(s)
case string:
*e = CountriesContinent(s)
default:
return fmt.Errorf("unsupported scan type for CountriesContinent: %T", src)
}
return nil
}
type NullCountriesContinent struct {
CountriesContinent CountriesContinent
Valid bool // Valid is true if CountriesContinent is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullCountriesContinent) Scan(value interface{}) error {
if value == nil {
ns.CountriesContinent, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.CountriesContinent.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullCountriesContinent) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.CountriesContinent), nil
}
type CurrencyTransactionLogsCurrencyType string
const (
CurrencyTransactionLogsCurrencyTypeCash CurrencyTransactionLogsCurrencyType = "cash"
CurrencyTransactionLogsCurrencyTypeFuel CurrencyTransactionLogsCurrencyType = "fuel"
CurrencyTransactionLogsCurrencyTypePassengers CurrencyTransactionLogsCurrencyType = "passengers"
)
func (e *CurrencyTransactionLogsCurrencyType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = CurrencyTransactionLogsCurrencyType(s)
case string:
*e = CurrencyTransactionLogsCurrencyType(s)
default:
return fmt.Errorf("unsupported scan type for CurrencyTransactionLogsCurrencyType: %T", src)
}
return nil
}
type NullCurrencyTransactionLogsCurrencyType struct {
CurrencyTransactionLogsCurrencyType CurrencyTransactionLogsCurrencyType
Valid bool // Valid is true if CurrencyTransactionLogsCurrencyType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullCurrencyTransactionLogsCurrencyType) Scan(value interface{}) error {
if value == nil {
ns.CurrencyTransactionLogsCurrencyType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.CurrencyTransactionLogsCurrencyType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullCurrencyTransactionLogsCurrencyType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.CurrencyTransactionLogsCurrencyType), nil
}
type PlanesAircraftSize string
const (
PlanesAircraftSizeSmall PlanesAircraftSize = "small"
PlanesAircraftSizeMedium PlanesAircraftSize = "medium"
PlanesAircraftSizeLarge PlanesAircraftSize = "large"
PlanesAircraftSizeHuge PlanesAircraftSize = "huge"
)
func (e *PlanesAircraftSize) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = PlanesAircraftSize(s)
case string:
*e = PlanesAircraftSize(s)
default:
return fmt.Errorf("unsupported scan type for PlanesAircraftSize: %T", src)
}
return nil
}
type NullPlanesAircraftSize struct {
PlanesAircraftSize PlanesAircraftSize
Valid bool // Valid is true if PlanesAircraftSize is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullPlanesAircraftSize) Scan(value interface{}) error {
if value == nil {
ns.PlanesAircraftSize, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.PlanesAircraftSize.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullPlanesAircraftSize) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.PlanesAircraftSize), nil
}
type PlanesAircraftType string
const (
PlanesAircraftTypePlane PlanesAircraftType = "plane"
PlanesAircraftTypeHelicopter PlanesAircraftType = "helicopter"
PlanesAircraftTypeSeaplane PlanesAircraftType = "seaplane"
PlanesAircraftTypeSpaceship PlanesAircraftType = "spaceship"
)
func (e *PlanesAircraftType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = PlanesAircraftType(s)
case string:
*e = PlanesAircraftType(s)
default:
return fmt.Errorf("unsupported scan type for PlanesAircraftType: %T", src)
}
return nil
}
type NullPlanesAircraftType struct {
PlanesAircraftType PlanesAircraftType
Valid bool // Valid is true if PlanesAircraftType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullPlanesAircraftType) Scan(value interface{}) error {
if value == nil {
ns.PlanesAircraftType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.PlanesAircraftType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullPlanesAircraftType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.PlanesAircraftType), nil
}
type PlanesOperationType string
const (
PlanesOperationTypePassenger PlanesOperationType = "passenger"
PlanesOperationTypeCargo PlanesOperationType = "cargo"
)
func (e *PlanesOperationType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = PlanesOperationType(s)
case string:
*e = PlanesOperationType(s)
default:
return fmt.Errorf("unsupported scan type for PlanesOperationType: %T", src)
}
return nil
}
type NullPlanesOperationType struct {
PlanesOperationType PlanesOperationType
Valid bool // Valid is true if PlanesOperationType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullPlanesOperationType) Scan(value interface{}) error {
if value == nil {
ns.PlanesOperationType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.PlanesOperationType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullPlanesOperationType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.PlanesOperationType), nil
}
type PlayerPlanesStatus string
const (
PlayerPlanesStatusIdle PlayerPlanesStatus = "idle"
PlayerPlanesStatusMaintenance PlayerPlanesStatus = "maintenance"
PlayerPlanesStatusBoarding PlayerPlanesStatus = "boarding"
PlayerPlanesStatusTaxiing PlayerPlanesStatus = "taxiing"
PlayerPlanesStatusFlying PlayerPlanesStatus = "flying"
)
func (e *PlayerPlanesStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = PlayerPlanesStatus(s)
case string:
*e = PlayerPlanesStatus(s)
default:
return fmt.Errorf("unsupported scan type for PlayerPlanesStatus: %T", src)
}
return nil
}
type NullPlayerPlanesStatus struct {
PlayerPlanesStatus PlayerPlanesStatus
Valid bool // Valid is true if PlayerPlanesStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullPlayerPlanesStatus) Scan(value interface{}) error {
if value == nil {
ns.PlayerPlanesStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.PlayerPlanesStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullPlayerPlanesStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.PlayerPlanesStatus), nil
}
type Airport struct {
ID uint32
Name string
UserID int32
OriginCountryID uint16
Cash uint64
CurrentPassengers uint32
MaxPassengerCapacity uint32
CurrentFuel uint32
MaxFuelCapacity uint32
FuelGenerationRate uint32
PassengerGenerationRate uint32
ResourcesLastUpdatedAt time.Time
IsAvailable bool
PlayerLevel uint32
ExperiencePoints uint64
IsDeleted sql.NullBool
}
type AirportBuilding struct {
ID uint32
AirportID uint32
BuildingID uint32
RecipeID sql.NullInt32
StorageProduct sql.NullInt32
Storage1 sql.NullInt32
Storage2 sql.NullInt32
Storage3 sql.NullInt32
Level uint32
}
type AirportContract struct {
ID uint32
AirportID uint32
Title string
RequiredProductID uint16
RequiredAmount uint32
CurrentAmountDelivered uint32
IsCompleted bool
ExpiresAt time.Time
}
type AirportInventory struct {
AirportID uint32
ProductID uint16
Quantity uint32
}
type Building struct {
ID uint32
Name string
MinLevel uint32
ConstructionCostCash uint32
UpgradeTier sql.NullInt16
UpgradeCostCashToUptier sql.NullInt32
BuildingType BuildingsBuildingType
Capacity sql.NullInt32
StorageType NullBuildingsStorageType
StorageSubtype NullBuildingsStorageSubtype
EffectID sql.NullInt16
EffectValue sql.NullInt32
OperationSize NullBuildingsOperationSize
OperationType NullBuildingsOperationType
PassiveType NullBuildingsPassiveType
PassiveRate sql.NullInt32
}
type BuildingProduct struct {
ID uint32
BuildingID uint32
RecipeID sql.NullInt32
StorageProduct sql.NullInt32
Storage1 sql.NullInt32
Storage2 sql.NullInt32
Storage3 sql.NullInt32
}
type CompletedFlightRoutesLog struct {
ID uint32
OriginAirportID uint32
DestinationAirportID uint32
StartTime time.Time
EndTime sql.NullTime
}
type Country struct {
ID uint16
Name string
Continent CountriesContinent
Produce1ID uint16
Produce2ID uint16
Produce3ID uint16
}
type CurrencyTransactionLog struct {
ID uint64
AirportID uint32
AmountChanged int32
CurrencyType CurrencyTransactionLogsCurrencyType
Reason string
CreatedAt sql.NullTime
}
type Effect struct {
ID uint16
Name string
}
type Plane struct {
ID uint32
Name string
MinLevel uint32
OperationCostFuel uint32
OperationCostCash uint32
MaxPassengers sql.NullInt32
TravelTimeInseconds uint32
OperationType PlanesOperationType
AircraftSize PlanesAircraftSize
AircraftType PlanesAircraftType
BuyingCostCash uint32
CompletedFlightExperience uint32
CompletedFlightCashReward sql.NullInt32
CompletedFlightCargoReward sql.NullInt32
}
type PlayerPlane struct {
ID uint32
AirportID uint32
PlaneID uint32
AssignedHangarID uint32
CurrentBuildingID sql.NullInt32
Status PlayerPlanesStatus
UpdatedAt sql.NullTime
FlightStartTime sql.NullTime
}
type Product struct {
ID uint16
Name string
}
type Recipe struct {
ID uint32
Name string
Ingredient1ID sql.NullInt16
Ingredient1Amount sql.NullInt32
Ingredient2ID sql.NullInt16
Ingredient2Amount sql.NullInt32
Ingredient3ID sql.NullInt16
Ingredient3Amount sql.NullInt32
ProductID uint16
YieldAmount uint32
}
type User struct {
ID int32
Username string
PasswordHash string
CreatedAt sql.NullTime
DeletedAt sql.NullTime
}

1436
backend/db/queries.sql.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -1 +1,205 @@
package main
import (
"fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// ============================================================================
// STYLING DEFINITIONS (Lipgloss)
// ============================================================================
var (
// Colors
skyBlue = lipgloss.Color("117")
neonPink = lipgloss.Color("205")
moneyGreen = lipgloss.Color("85")
gray = lipgloss.Color("240")
// Component Styles
titleStyle = lipgloss.NewStyle().
Foreground(skyBlue).
Bold(true).
MarginTop(1).
MarginBottom(1)
headerBoxStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(skyBlue).
Padding(0, 2).
MarginBottom(1)
listStyle = lipgloss.NewStyle().MarginLeft(2)
itemStyle = lipgloss.NewStyle().
PaddingLeft(2).
Foreground(lipgloss.Color("252"))
selectedItemStyle = lipgloss.NewStyle().
PaddingLeft(2).
Foreground(neonPink).
Bold(true).
Background(lipgloss.Color("236")) // Slight background highlight
footerStyle = lipgloss.NewStyle().
Foreground(gray).
MarginTop(2)
)
// ============================================================================
// 1. THE MODEL
// ============================================================================
type homepageModel struct {
cursor int
choices []string
selected string
// Mock Player Data (In a real app, pass this in via initialModel)
playerName string
cash int
fleetCount int
}
func initialHomepageModel() homepageModel {
return homepageModel{
// Our interactive menu options
choices: []string{
"🗄️ Manage Hangar Fleet",
"🏢 View Airport Infrastructure",
"🏁 Active Runways & Service Loop",
"🚪 Logout / Quit",
},
playerName: "CaptainGo",
cash: 14500,
fleetCount: 12,
}
}
// ============================================================================
// 2. THE INIT FUNCTION
// ============================================================================
func (m homepageModel) Init() tea.Cmd {
return nil // No background tasks to start immediately
}
// ============================================================================
// 3. THE UPDATE FUNCTION (Handles inputs)
// ============================================================================
func (m homepageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// Handle keyboard events
case tea.KeyMsg:
switch msg.String() {
// Quit commands
case "ctrl+c", "q", "esc":
m.selected = "quit"
return m, tea.Quit
// Move up the menu
case "up", "k":
if m.cursor > 0 {
m.cursor--
} else {
m.cursor = len(m.choices) - 1 // Wrap around to bottom
}
// Move down the menu
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
} else {
m.cursor = 0 // Wrap around to top
}
// Select an option
case "enter", " ":
m.selected = m.choices[m.cursor]
return m, tea.Quit
}
}
return m, nil
}
// ============================================================================
// 4. THE VIEW FUNCTION (Renders the UI)
// ============================================================================
func (m homepageModel) View() string {
var b strings.Builder
// --- 1. TITLE ---
b.WriteString(titleStyle.Render("✈️ SKYRAMA COMMAND CENTER"))
b.WriteString("\n")
// --- 2. HEADER BAR (Player Stats) ---
statsLine := fmt.Sprintf("[ 🔑 Player: %s ] [ 💰 Cash: %s ] [ 🛩️ Fleet: %d ]",
lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Bold(true).Render(m.playerName),
lipgloss.NewStyle().Foreground(moneyGreen).Render(fmt.Sprintf("%d €", m.cash)),
m.fleetCount,
)
b.WriteString(headerBoxStyle.Render(statsLine))
b.WriteString("\n\n")
// --- 3. INTERACTIVE MENU ---
b.WriteString(" Main Operations Menu:\n\n")
for i, choice := range m.choices {
// Is the cursor pointing at this choice?
cursorStr := " " // no cursor
if m.cursor == i {
cursorStr = lipgloss.NewStyle().Foreground(neonPink).Render(">")
b.WriteString(fmt.Sprintf("%s %s\n", cursorStr, selectedItemStyle.Render(choice)))
} else {
b.WriteString(fmt.Sprintf("%s %s\n", cursorStr, itemStyle.Render(choice)))
}
}
// --- 4. FOOTER ---
b.WriteString(footerStyle.Render("\n ↑/↓: Navigate • Enter: Select • q/esc: Quit"))
// Return the final drawn screen with a little left margin padding
return lipgloss.NewStyle().MarginLeft(2).Render(b.String()) + "\n"
}
// ============================================================================
// THE WRAPPER FUNCTION
// ============================================================================
// RunHomepageScreen starts the TUI and returns the menu string the user selected.
func RunHomepageScreen() string {
p := tea.NewProgram(initialHomepageModel(), tea.WithAltScreen()) // AltScreen creates a full-screen view
finalModel, err := p.Run()
if err != nil {
fmt.Printf("Error running dashboard: %v", err)
os.Exit(1)
}
if m, ok := finalModel.(homepageModel); ok {
return m.selected
}
return "quit"
}
/*
// Optional Main function to test it directly
func main() {
selection := RunHomepageScreen()
// Clear the screen on exit to print standard output
fmt.Print("\033[H\033[2J")
if selection == "quit" || selection == "🚪 Logout / Quit" {
fmt.Println("Gracefully disconnected from SkyRama servers. Goodbye!")
os.Exit(0)
}
fmt.Printf("You selected: %s\n", selection)
fmt.Println("Here is where you would load the next Bubble Tea model for that screen!")
}
*/

View file

@ -1 +1,206 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// ============================================================================
// STYLING DEFINITIONS (Lipgloss)
// ============================================================================
var (
focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
cursorStyle = focusedStyle.Copy()
noStyle = lipgloss.NewStyle()
// A nice box around the login screen
dialogBoxStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("87")).
Padding(1, 4).
MarginTop(1).
MarginLeft(2)
)
// ============================================================================
// 1. THE MODEL
// ============================================================================
type loginModel struct {
focusIndex int
inputs []textinput.Model
cursorMode textinput.CursorMode
// Flags to return back to the main application
isSubmitted bool
isCanceled bool
errorMessage string
}
func initialModell() loginModel {
m := loginModel{
inputs: make([]textinput.Model, 2),
}
var t textinput.Model
for i := range m.inputs {
t = textinput.New()
t.Cursor.Style = cursorStyle
t.CharLimit = 32
switch i {
case 0:
t.Placeholder = "Username"
t.Focus()
t.PromptStyle = focusedStyle
t.TextStyle = focusedStyle
case 1:
t.Placeholder = "Password"
t.EchoMode = textinput.EchoPassword // Masks the password with *
t.EchoCharacter = '•'
}
m.inputs[i] = t
}
return m
}
// ============================================================================
// 2. THE INIT FUNCTION (Runs once on startup)
// ============================================================================
func (m loginModel) Init() tea.Cmd {
return textinput.Blink
}
// ============================================================================
// 3. THE UPDATE FUNCTION (Handles keypresses)
// ============================================================================
func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
m.isCanceled = true
return m, tea.Quit
// Handle switching between username and password fields
case "tab", "shift+tab", "enter", "up", "down":
s := msg.String()
// If they press Enter on the password field, submit the form
if s == "enter" && m.focusIndex == len(m.inputs)-1 {
m.isSubmitted = true
return m, tea.Quit
}
// Cycle through inputs
if s == "up" || s == "shift+tab" {
m.focusIndex--
} else {
m.focusIndex++
}
if m.focusIndex > len(m.inputs)-1 {
m.focusIndex = 0
} else if m.focusIndex < 0 {
m.focusIndex = len(m.inputs) - 1
}
// Apply focus states
cmds := make([]tea.Cmd, len(m.inputs))
for i := 0; i <= len(m.inputs)-1; i++ {
if i == m.focusIndex {
cmds[i] = m.inputs[i].Focus()
m.inputs[i].PromptStyle = focusedStyle
m.inputs[i].TextStyle = focusedStyle
continue
}
m.inputs[i].Blur()
m.inputs[i].PromptStyle = noStyle
m.inputs[i].TextStyle = noStyle
}
return m, tea.Batch(cmds...)
}
}
// Handle character inputs and blinking
cmd := m.updateInputs(msg)
return m, cmd
}
func (m *loginModel) updateInputs(msg tea.Msg) tea.Cmd {
cmds := make([]tea.Cmd, len(m.inputs))
for i := range m.inputs {
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
}
return tea.Batch(cmds...)
}
// ============================================================================
// 4. THE VIEW FUNCTION (Renders the UI)
// ============================================================================
func (m loginModel) View() string {
// Build the UI string
var b strings.Builder
b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("78")).Render("✈️ SKYRAMA TERMINAL LOGIN\n\n"))
for i := range m.inputs {
b.WriteString(m.inputs[i].View())
if i < len(m.inputs)-1 {
b.WriteRune('\n')
}
}
button := &lipgloss.Style{}
if m.focusIndex == len(m.inputs) {
button = &focusedStyle
} else {
button = &blurredStyle
}
b.WriteString("\n")
if m.errorMessage != "" {
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Render("⚠️ " + m.errorMessage + "\n"))
} else {
b.WriteString("\n")
}
b.WriteString(button.Render("[ Submit ]"))
b.WriteString(blurredStyle.Render("\n\n(esc to quit)"))
// Wrap the whole thing in our dialog box
return dialogBoxStyle.Render(b.String()) + "\n"
}
// ============================================================================
// THE WRAPPER FUNCTION (Call this from your app)
// ============================================================================
// RunLoginScreen starts the TUI and returns the username, password, and a boolean
// indicating if the user completed the form (true) or pressed escape (false).
func RunLoginScreen() (username string, password string, submitted bool) {
p := tea.NewProgram(initialModell())
// Run the Bubble Tea program synchronously
finalModel, err := p.Run()
if err != nil {
fmt.Printf("Error running login screen: %v", err)
os.Exit(1)
}
// Cast the returned tea.Model back into our specific loginModel
if m, ok := finalModel.(loginModel); ok {
if m.isSubmitted {
return m.inputs[0].Value(), m.inputs[1].Value(), true
}
}
return "", "", false
}

182
backend/server/main.go Normal file
View file

@ -0,0 +1,182 @@
package main
import (
"context"
"database/sql"
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
_ "github.com/go-sql-driver/mysql" // MySQL Driver
"golang.org/x/crypto/bcrypt"
// CHANGE THIS to match your go.mod name + /dba
"skyrama_clone_backend/backend/db"
)
// ============================================================================
// 1. STATE DEFINITIONS
// ============================================================================
type sessionState int
const (
stateLogin sessionState = iota
stateHomepage
)
// ============================================================================
// 2. THE MASTER MODEL (The Router)
// ============================================================================
type mainModel struct {
state sessionState
queries *db.Queries // <-- NEW: Holds our database queries
login loginModel
homepage homepageModel
}
func initialModel(q *db.Queries) mainModel {
return mainModel{
state: stateLogin,
queries: q,
login: initialModell(),
homepage: initialHomepageModel(),
}
}
func (m mainModel) Init() tea.Cmd {
return m.login.Init()
}
// ============================================================================
// 3. THE MASTER UPDATE (Traffic Cop & Authentication)
// ============================================================================
func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == "ctrl+c" {
return m, tea.Quit
}
//var cmd tea.Cmd
var cmds []tea.Cmd
ctx := context.Background()
switch m.state {
case stateLogin:
newLoginModel, loginCmd := m.login.Update(msg)
m.login = newLoginModel.(loginModel)
cmds = append(cmds, loginCmd)
// Did the user press Enter on the Submit button?
if m.login.isSubmitted {
username := m.login.inputs[0].Value()
password := m.login.inputs[1].Value()
// 1. Fetch the user from the database
user, err := m.queries.GetUserByUsername(ctx, username)
if err != nil {
m.login.errorMessage = "User not found."
m.login.isSubmitted = false // Reset so they can try again
return m, nil
}
// 2. Verify the password hash
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
m.login.errorMessage = "Invalid password."
m.login.isSubmitted = false // Reset so they can try again
return m, nil
}
// 3. Fetch their Airport Data
// Note: user.ID is int32 in sqlc (based on schema)
airport, err := m.queries.GetAirportByUserId(ctx, user.ID)
if err != nil {
m.login.errorMessage = "No airport found for this user."
m.login.isSubmitted = false
return m, nil
}
// 4. Fetch their Fleet Count
// Note: airport.ID is uint32, GetPlayerFleet expects the correct type
fleet, _ := m.queries.GetPlayerFleet(ctx, airport.ID)
// 5. SUCCESS! Push real data to the homepage and switch screens
m.homepage.playerName = user.Username
m.homepage.cash = int(airport.Cash)
m.homepage.fleetCount = len(fleet)
m.state = stateHomepage
cmds = append(cmds, m.homepage.Init())
} else if m.login.isCanceled {
return m, tea.Quit
}
case stateHomepage:
newHomepageModel, homeCmd := m.homepage.Update(msg)
m.homepage = newHomepageModel.(homepageModel)
cmds = append(cmds, homeCmd)
if m.homepage.selected != "" {
if m.homepage.selected == "🚪 Logout / Quit" {
return m, tea.Quit
}
// Placeholder for other menus
return m, tea.Quit
}
}
return m, tea.Batch(cmds...)
}
func (m mainModel) View() string {
switch m.state {
case stateLogin:
return m.login.View()
case stateHomepage:
return m.homepage.View()
default:
return "Error: Unknown state"
}
}
// ============================================================================
// MAIN EXECUTION WITH DATABASE POOL
// ============================================================================
func main() {
// 1. Connect to MySQL Database
// Ensure ?parseTime=true is present!
dsn := "skyramaUser:Skyrama1234.@tcp(127.0.0.1:3306)/skyrama_clone?parseTime=true"
dbConn, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Printf("Error opening database pool: %v\n", err)
os.Exit(1)
}
defer dbConn.Close()
if err := dbConn.Ping(); err != nil {
fmt.Printf("Database unreachable: %v\n", err)
os.Exit(1)
}
// 2. Initialize sqlc queries
queries := db.New(dbConn)
// 3. Start the Bubble Tea UI, passing the database into the model
p := tea.NewProgram(initialModel(queries), tea.WithAltScreen())
finalModel, err := p.Run()
if err != nil {
fmt.Printf("Fatal error starting SkyRama: %v", err)
os.Exit(1)
}
fmt.Print("\033[H\033[2J") // Clear terminal on exit
if m, ok := finalModel.(mainModel); ok {
if m.state == stateHomepage && m.homepage.selected != "" && m.homepage.selected != "🚪 Logout / Quit" {
fmt.Printf("You clicked '%s', which we will build next!\n", m.homepage.selected)
} else {
fmt.Println("Gracefully disconnected from SkyRama servers. Goodbye!")
}
}
}

View file

@ -1 +1,78 @@
package utils
import "skyrama_clone_backend/backend/objects"
type Msg = isPacket_Msg
func NewChat(msg string) Msg {
return &Packet_Chat{
Chat: &ChatMessage{
Msg: msg,
},
}
}
func NewId(id uint64) Msg {
return &Packet_Id{
Id: &IdMessage{
Id: id,
},
}
}
func NewOkResponse() Msg {
return &Packet_OkResponse{
OkResponse: &OkResponseMessage{},
}
}
func NewDenyResponse(reason string) Msg {
return &Packet_DenyResponse{
DenyResponse: &DenyResponseMessage{
Reason: reason,
},
}
}
func NewPlayer(id uint64, player *objects.Player) Msg {
return &Packet_Player{
Player: &PlayerMessage{
Id: id,
Name: player.Name,
X: player.X,
Y: player.Y,
Radius: player.Radius,
Direction: player.Direction,
Speed: player.Speed,
},
}
}
func newSporeMessage(id uint64, spore *objects.Spore) *SporeMessage {
return &SporeMessage{
Id: id,
X: spore.X,
Y: spore.Y,
Radius: spore.Radius,
}
}
func NewSpore(id uint64, spore *objects.Spore) Msg {
return &Packet_Spore{
newSporeMessage(id, spore),
}
}
func NewSporeBatches(spores map[uint64]*objects.Spore) Msg {
SporeMessages := make([]*SporeMessage, len(spores))
for id, spore := range spores {
SporeMessages = append(SporeMessages, newSporeMessage(id, spore))
}
return &Packet_SporeBatch{
SporeBatch: &SporesBatchMessage{
Spores: SporeMessages,
},
}
}

1441
backend/utils/packets.pb.go Normal file

File diff suppressed because it is too large Load diff

33
go.mod Normal file
View file

@ -0,0 +1,33 @@
module skyrama_clone_backend
go 1.26.3
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v1.0.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
)

75
go.sum Normal file
View file

@ -0,0 +1,75 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=

47
shared/packets.proto Normal file
View file

@ -0,0 +1,47 @@
syntax = "proto3";
// protoc -I=shared --go_out=backend .\shared\packets.proto
package packets;
option go_package = "utils/";
message ChatMessage { string msg = 1; }
message IdMessage { uint64 id = 1; }
message LoginRequestMessage { string username = 1; string password = 2; }
message RegisterRequestMessage { string username = 1; string password = 2; int32 color = 3; }
message OkResponseMessage { }
message DenyResponseMessage { string reason = 1; }
message PlayerMessage { uint64 id = 1; string name = 2; double x = 3; double y = 4; double radius = 5; double direction = 6; double speed = 7; int32 color = 8; }
message PlayerDirectionMessage { double direction = 1; }
message SporeMessage { uint64 id = 1; double x = 2; double y = 3; double radius = 4; }
message SporeConsumedMessage { uint64 spore_id = 1; }
message SporesBatchMessage { repeated SporeMessage spores = 1; }
message PlayerConsumedMessage { uint64 player_id = 1; }
message HiscoreBoardRequestMessage { }
message HiscoreMessage { uint64 rank = 1; string name = 2; uint64 score = 3; }
message HiscoreBoardMessage { repeated HiscoreMessage hiscores = 1; }
message FinishedBrowsingHiscoresMessage { }
message SearchHiscoreMessage { string name = 1; }
message DisconnectMessage { string reason = 1; }
message Packet {
uint64 sender_id = 1;
oneof msg {
ChatMessage chat = 2;
IdMessage id = 3;
LoginRequestMessage login_request = 4;
RegisterRequestMessage register_request = 5;
OkResponseMessage ok_response = 6;
DenyResponseMessage deny_response = 7;
PlayerMessage player = 8;
PlayerDirectionMessage player_direction = 9;
SporeMessage spore = 10;
SporeConsumedMessage spore_consumed = 11;
SporesBatchMessage spores_batch = 12;
PlayerConsumedMessage player_consumed = 13;
HiscoreBoardRequestMessage hiscore_board_request = 14;
HiscoreMessage hiscore = 15;
HiscoreBoardMessage hiscore_board = 16;
FinishedBrowsingHiscoresMessage finished_browsing_hiscores = 17;
SearchHiscoreMessage search_hiscore = 18;
DisconnectMessage disconnect = 19;
}
}