first commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[{compose,docker-compose}.{yml,yaml}]
|
||||||
|
indent_size = 4
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=thecrimesyndicate
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.codex
|
||||||
|
/.cursor/
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/fonts-manifest.dev.json
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
_ide_helper.php
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
+937
@@ -0,0 +1,937 @@
|
|||||||
|
# 🎮 Text Mafia Browser Game - Project Plan
|
||||||
|
|
||||||
|
**Inspired by Omerta & Torn City**
|
||||||
|
**Built with Laravel**
|
||||||
|
**Last Updated:** 2026-06-26
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
A persistent browser-based text MMORPG where players build criminal empires, join factions, commit crimes, trade items, and engage in PvP/PvE combat. The game features deep economic systems, territory control, and social interactions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Core Concept
|
||||||
|
|
||||||
|
- **Persistent World**: Game state continues even when players are offline
|
||||||
|
- **Text-Based**: Primary interface is text with minimal graphics
|
||||||
|
- **Turn-Based Actions**: Most actions cost energy and have cooldowns
|
||||||
|
- **Player-Driven Economy**: Players trade, control markets, and manage resources
|
||||||
|
- **Faction Warfare**: Competing criminal organizations battle for control
|
||||||
|
- **Progression System**: Level up characters through experience and training
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Phase Breakdown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Phase 1: Foundation (2-3 weeks)
|
||||||
|
*Core infrastructure, user management, and basic game loop*
|
||||||
|
|
||||||
|
| Priority | Feature | Description | Laravel Components |
|
||||||
|
|----------|---------|-------------|-------------------|
|
||||||
|
| ⭐⭐⭐ | **User Authentication** | Registration, login, password reset, email verification | `Auth`, `Breeze`, `Sanctum` |
|
||||||
|
| ⭐⭐⭐ | **Character Creation** | Name, gender, starting stats, backstory | `Character` model, migration, factory |
|
||||||
|
| ⭐⭐⭐ | **Basic Game UI** | Layout, navigation, responsive design | Blade templates, Tailwind CSS |
|
||||||
|
| ⭐⭐⭐ | **Energy System** | Actions cost energy, regenerates over time | `energy` column, scheduler |
|
||||||
|
| ⭐⭐⭐ | **Health System** | HP, hospital on death, regen timer | `health`, `in_hospital` columns |
|
||||||
|
| ⭐⭐ | **Basic Crimes** | Simple crimes (pickpocket, mugging) | `Crime`, `CrimeAttempt` models |
|
||||||
|
| ⭐⭐ | **Inventory System** | Items, equipment, consumables | `Item`, `Inventory` pivot table |
|
||||||
|
| ⭐⭐ | **Money System** | Cash, bank, interest | `money`, `bank` columns |
|
||||||
|
| ⭐ | **User Profile** | Stats display, avatar, status | `Profile` component |
|
||||||
|
| ⭐ | **News/Feed System** | Global and user-specific events | `ActivityLog`, `Notification` |
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Working Laravel application with authentication
|
||||||
|
- Character creation flow
|
||||||
|
- Energy and health systems with regeneration
|
||||||
|
- 5-10 basic crimes to commit
|
||||||
|
- Inventory with item management
|
||||||
|
- Basic UI with navigation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Phase 2: Core Gameplay (3-4 weeks)
|
||||||
|
*Economy, combat, and faction basics*
|
||||||
|
|
||||||
|
| Priority | Feature | Description | Laravel Components |
|
||||||
|
|----------|---------|-------------|-------------------|
|
||||||
|
| ⭐⭐⭐ | **Attack System** | Attack other players, hit lists | `Attack`, `Hit` models, cooldowns |
|
||||||
|
| ⭐⭐⭐ | **Faction System** | Create/join factions, ranks, chat | `Faction`, `FactionMember`, `FactionChat` |
|
||||||
|
| ⭐⭐⭐ | **Skills System** | Strength, defense, speed, dexterity | `skill_*` columns, training |
|
||||||
|
| ⭐⭐⭐ | **Item Market** | Buy/sell items from other players | `MarketListing` model |
|
||||||
|
| ⭐⭐ | **Shops** | NPC shops for weapons, armor, items | `Shop`, `ShopItem` models |
|
||||||
|
| ⭐⭐ | **Jail System** | Sentenced after losing attacks | `jail_time` column, release scheduler |
|
||||||
|
| ⭐⭐ | **Hospital System** | Revive after death, medical items | `hospital_time`, `revive_cost` |
|
||||||
|
| ⭐⭐ | **Travel System** | Travel between cities/countries | `Location` model, travel costs |
|
||||||
|
| ⭐ | **Bank System** | Deposit/withdraw, interest | `Bank` model, daily interest job |
|
||||||
|
| ⭐ | **Mail System** | Send/receive messages | `Mail` model, `read_at` timestamp |
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Full PvP combat system
|
||||||
|
- Faction creation and management
|
||||||
|
- Skill training and progression
|
||||||
|
- Player-to-player marketplace
|
||||||
|
- Jail and hospital mechanics
|
||||||
|
- Travel between multiple locations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Phase 3: Advanced Features (4-5 weeks)
|
||||||
|
*Depth, strategy, and social systems*
|
||||||
|
|
||||||
|
| Priority | Feature | Description | Laravel Components |
|
||||||
|
|----------|---------|-------------|-------------------|
|
||||||
|
| ⭐⭐⭐ | **Faction Wars** | Faction vs faction warfare | `FactionWar`, `WarAttack` models |
|
||||||
|
| ⭐⭐⭐ | **Territory System** | Control areas for bonuses | `Territory`, `TerritoryControl` |
|
||||||
|
| ⭐⭐⭐ | **Missions/Quests** | Storyline and daily missions | `Mission`, `MissionStep` models |
|
||||||
|
| ⭐⭐ | **Casino** | Gambling games (roulette, blackjack) | `CasinoGame`, `Bet` models |
|
||||||
|
| ⭐⭐ | **Stock Market** | Invest in game companies | `Stock`, `Portfolio` models |
|
||||||
|
| ⭐⭐ | **Properties** | Buy houses, businesses | `Property`, `PropertyUpgrade` |
|
||||||
|
| ⭐⭐ | **Vehicles** | Faster travel, combat bonuses | `Vehicle` model |
|
||||||
|
| ⭐⭐ | **Awards/Medals** | Achievements system | `Award`, `UserAward` |
|
||||||
|
| ⭐ | **Bounties** | Place bounties on players | `Bounty` model |
|
||||||
|
| ⭐ | **Alliances** | Faction alliances | `Alliance` pivot table |
|
||||||
|
| ⭐ | **Spies** | Infiltrate other factions | `SpyReport` model |
|
||||||
|
| ⭐ | **Merits** | Premium currency system | `merits` column |
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Faction warfare and territory control
|
||||||
|
- Mission/quest system with rewards
|
||||||
|
- Casino with multiple games
|
||||||
|
- Stock market with fluctuations
|
||||||
|
- Property ownership system
|
||||||
|
- Advanced social features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 Phase 4: Polish & Scale (2-3 weeks)
|
||||||
|
*Performance, moderation, and extras*
|
||||||
|
|
||||||
|
| Priority | Feature | Description | Laravel Components |
|
||||||
|
|----------|---------|-------------|-------------------|
|
||||||
|
| ⭐⭐⭐ | **Admin Panel** | Manage users, items, economy | `Nova` or custom admin |
|
||||||
|
| ⭐⭐⭐ | **Moderation Tools** | Ban, mute, freeze users | `ModerationLog` model |
|
||||||
|
| ⭐⭐ | **API for Mobile** | REST/GraphQL endpoints | `Sanctum`, API resources |
|
||||||
|
| ⭐⭐ | **Real-time Chat** | WebSocket-based chat | `Laravel Echo`, `Pusher` |
|
||||||
|
| ⭐⭐ | **Leaderboards** | Top players, factions, wealth | Cached queries, weekly resets |
|
||||||
|
| ⭐⭐ | **Event System** | Holiday events, double XP | `GameEvent` model |
|
||||||
|
| ⭐ | **Referral System** | Invite friends for rewards | `Referral` model |
|
||||||
|
| ⭐ | **Two-Factor Auth** | Security for accounts | `Jetstream` 2FA |
|
||||||
|
| ⭐ | **Backup System** | Database backups | Scheduled commands |
|
||||||
|
| ⭐ | **Caching** | Optimize performance | Redis, cache helpers |
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Full admin dashboard
|
||||||
|
- Moderation capabilities
|
||||||
|
- Mobile API (if needed)
|
||||||
|
- Real-time features
|
||||||
|
- Performance optimizations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Database Schema
|
||||||
|
|
||||||
|
### Core Tables (Phase 1-2)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Users & Characters
|
||||||
|
users (id, name, email, password, email_verified_at, last_active_at, ip_address, user_agent, created_at, updated_at)
|
||||||
|
characters (id, user_id, name, gender, level, experience, money, bank, energy, max_energy,
|
||||||
|
health, max_health, strength, defense, speed, dexterity, location_id,
|
||||||
|
faction_id, in_hospital_until, in_jail_until, last_action_at, status, created_at, updated_at)
|
||||||
|
|
||||||
|
-- Game World
|
||||||
|
locations (id, name, slug, description, travel_cost, requires_level, faction_control, created_at)
|
||||||
|
|
||||||
|
-- Economy
|
||||||
|
items (id, name, slug, description, type, value, buy_price, sell_price, requires_level,
|
||||||
|
attack_bonus, defense_bonus, health_bonus, energy_bonus, is_tradeable, is_usable, created_at)
|
||||||
|
inventories (id, character_id, item_id, quantity, equipped, created_at, updated_at)
|
||||||
|
shops (id, name, slug, location_id, restock_interval, restock_amount, created_at)
|
||||||
|
shop_items (id, shop_id, item_id, stock, price, created_at, updated_at)
|
||||||
|
|
||||||
|
-- Actions
|
||||||
|
crimes (id, name, slug, description, difficulty, success_rate, reward_min, reward_max,
|
||||||
|
experience, energy_cost, cooldown, requires_level, location_id, created_at)
|
||||||
|
crime_attempts (id, character_id, crime_id, success, reward, experience_gained, attempted_at)
|
||||||
|
|
||||||
|
attacks (id, attacker_id, defender_id, attacker_health_after, defender_health_after,
|
||||||
|
attacker_damage, defender_damage, weapon_id, armor_id, result, attacked_at)
|
||||||
|
hits (id, target_id, bounty, placed_by_id, expires_at, claimed_by_id, claimed_at, created_at)
|
||||||
|
|
||||||
|
-- Factions
|
||||||
|
factions (id, name, tag, slug, description, leader_id, bank, respect, founded_at, created_at)
|
||||||
|
faction_members (id, faction_id, character_id, rank, joined_at, created_at)
|
||||||
|
faction_ranks (id, faction_id, name, level_requirement, permissions, created_at)
|
||||||
|
faction_chat (id, faction_id, character_id, message, created_at)
|
||||||
|
|
||||||
|
-- Communication
|
||||||
|
mail (id, sender_id, recipient_id, subject, body, read_at, is_system, created_at)
|
||||||
|
notifications (id, user_id, type, data, read_at, created_at)
|
||||||
|
|
||||||
|
-- System
|
||||||
|
activity_logs (id, character_id, action, description, ip_address, user_agent, created_at)
|
||||||
|
game_events (id, name, slug, description, start_at, end_at, is_active, created_at)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Tables (Phase 3-4)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Phase 3
|
||||||
|
missions (id, name, slug, description, type, difficulty, reward_money, reward_exp, reward_item_id, cooldown, created_at)
|
||||||
|
mission_steps (id, mission_id, step_number, description, requirements, created_at)
|
||||||
|
character_missions (id, character_id, mission_id, step_completed, completed_at, created_at)
|
||||||
|
|
||||||
|
faction_wars (id, attacker_faction_id, defender_faction_id, started_by_id, start_reason,
|
||||||
|
start_at, end_at, status, winner_faction_id, created_at)
|
||||||
|
war_attacks (id, war_id, attacking_faction_id, defending_faction_id, attacker_id, defender_id,
|
||||||
|
attacker_damage, defender_damage, result, attacked_at)
|
||||||
|
territories (id, name, slug, location_id, controlling_faction_id, income_bonus, defense_bonus, created_at)
|
||||||
|
|
||||||
|
stocks (id, company_name, symbol, current_price, min_price, max_price, volatility, created_at)
|
||||||
|
portfolios (id, character_id, stock_id, quantity, average_price, created_at, updated_at)
|
||||||
|
stock_history (id, stock_id, price, volume, recorded_at)
|
||||||
|
|
||||||
|
properties (id, name, slug, location_id, owner_id, type, level, purchase_price, sell_price,
|
||||||
|
income_per_hour, defense_bonus, storage_capacity, created_at)
|
||||||
|
vehicles (id, name, slug, type, speed_bonus, attack_bonus, defense_bonus, purchase_price,
|
||||||
|
sell_price, fuel_capacity, fuel_consumption, created_at)
|
||||||
|
character_vehicles (id, character_id, vehicle_id, fuel, created_at)
|
||||||
|
|
||||||
|
-- Phase 4
|
||||||
|
bounties (id, target_id, amount, placed_by_id, reason, expires_at, claimed_by_id, claimed_at, created_at)
|
||||||
|
alliances (id, faction_id_1, faction_id_2, initiated_by_id, status, created_at, accepted_at, ended_at)
|
||||||
|
spy_reports (id, faction_id, target_faction_id, reported_by_id, report_type, data, created_at)
|
||||||
|
awards (id, name, slug, description, icon, created_at)
|
||||||
|
character_awards (id, character_id, award_id, awarded_at, created_at)
|
||||||
|
|
||||||
|
referrals (id, referrer_id, referred_id, code, reward_claimed, created_at)
|
||||||
|
moderation_logs (id, moderator_id, target_id, action, reason, duration, expires_at, created_at)
|
||||||
|
|
||||||
|
-- Admin
|
||||||
|
admin_settings (id, key, value, description, created_at, updated_at)
|
||||||
|
reports (id, reporter_id, reported_id, type, reason, status, handled_by_id, handled_at, created_at)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Laravel Architecture
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── Console/
|
||||||
|
│ └── Commands/
|
||||||
|
│ ├── Game/
|
||||||
|
│ │ ├── ProcessEnergyRegen.php
|
||||||
|
│ │ ├── ProcessHealthRegen.php
|
||||||
|
│ │ ├── ReleaseFromJail.php
|
||||||
|
│ │ ├── ReleaseFromHospital.php
|
||||||
|
│ │ ├── DailyInterest.php
|
||||||
|
│ │ └── RestockShops.php
|
||||||
|
│ └── ...
|
||||||
|
├── Http/
|
||||||
|
│ ├── Controllers/
|
||||||
|
│ │ ├── Auth/
|
||||||
|
│ │ │ └── (Laravel default)
|
||||||
|
│ │ ├── Game/
|
||||||
|
│ │ │ ├── CharacterController.php
|
||||||
|
│ │ │ ├── CrimeController.php
|
||||||
|
│ │ │ ├── AttackController.php
|
||||||
|
│ │ │ ├── FactionController.php
|
||||||
|
│ │ │ ├── InventoryController.php
|
||||||
|
│ │ │ ├── ShopController.php
|
||||||
|
│ │ │ ├── MarketController.php
|
||||||
|
│ │ │ ├── MailController.php
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── Admin/
|
||||||
|
│ │ ├── DashboardController.php
|
||||||
|
│ │ ├── UserController.php
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── Middleware/
|
||||||
|
│ │ ├── CheckEnergy.php
|
||||||
|
│ │ ├── CheckInJail.php
|
||||||
|
│ │ ├── CheckInHospital.php
|
||||||
|
│ │ ├── VerifyCharacter.php
|
||||||
|
│ │ ├── CheckLevel.php
|
||||||
|
│ │ ├── RateLimitActions.php
|
||||||
|
│ │ └── LogActivity.php
|
||||||
|
│ └── Requests/
|
||||||
|
│ ├── Game/
|
||||||
|
│ │ ├── AttemptCrimeRequest.php
|
||||||
|
│ │ ├── AttackRequest.php
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── Admin/
|
||||||
|
├── Models/
|
||||||
|
│ ├── Character.php
|
||||||
|
│ ├── Crime.php
|
||||||
|
│ ├── Faction.php
|
||||||
|
│ ├── Item.php
|
||||||
|
│ ├── Attack.php
|
||||||
|
│ ├── Location.php
|
||||||
|
│ ├── ActivityLog.php
|
||||||
|
│ ├── Notification.php
|
||||||
|
│ └── ...
|
||||||
|
├── Policies/
|
||||||
|
│ ├── CharacterPolicy.php
|
||||||
|
│ ├── FactionPolicy.php
|
||||||
|
│ └── ...
|
||||||
|
├── Services/
|
||||||
|
│ ├── Game/
|
||||||
|
│ │ ├── CombatService.php
|
||||||
|
│ │ ├── EconomyService.php
|
||||||
|
│ │ ├── EnergyService.php
|
||||||
|
│ │ ├── CrimeService.php
|
||||||
|
│ │ ├── FactionService.php
|
||||||
|
│ │ ├── InventoryService.php
|
||||||
|
│ │ └── NotificationService.php
|
||||||
|
│ └── ...
|
||||||
|
├── Traits/
|
||||||
|
│ ├── HasCharacter.php
|
||||||
|
│ ├── HasInventory.php
|
||||||
|
│ ├── HasExperience.php
|
||||||
|
│ └── ...
|
||||||
|
├── Enums/
|
||||||
|
│ ├── ActionResult.php
|
||||||
|
│ ├── AttackResult.php
|
||||||
|
│ ├── CharacterStatus.php
|
||||||
|
│ ├── CrimeType.php
|
||||||
|
│ ├── ItemType.php
|
||||||
|
│ └── ...
|
||||||
|
└── Providers/
|
||||||
|
└── GameServiceProvider.php
|
||||||
|
|
||||||
|
routes/
|
||||||
|
├── web.php
|
||||||
|
├── api.php
|
||||||
|
└── channels.php
|
||||||
|
|
||||||
|
resources/
|
||||||
|
├── views/
|
||||||
|
│ ├── layouts/
|
||||||
|
│ │ ├── app.blade.php
|
||||||
|
│ │ ├── game.blade.php
|
||||||
|
│ │ └── auth.blade.php
|
||||||
|
│ ├── game/
|
||||||
|
│ │ ├── partials/
|
||||||
|
│ │ │ ├── energy-bar.blade.php
|
||||||
|
│ │ │ ├── health-bar.blade.php
|
||||||
|
│ │ │ ├── nav.blade.php
|
||||||
|
│ │ │ ├── notifications.blade.php
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ ├── dashboard.blade.php
|
||||||
|
│ │ ├── character/
|
||||||
|
│ │ │ ├── show.blade.php
|
||||||
|
│ │ │ ├── create.blade.php
|
||||||
|
│ │ │ └── edit.blade.php
|
||||||
|
│ │ ├── crimes/
|
||||||
|
│ │ │ ├── index.blade.php
|
||||||
|
│ │ │ └── attempt.blade.php
|
||||||
|
│ │ ├── attack/
|
||||||
|
│ │ │ ├── index.blade.php
|
||||||
|
│ │ │ └── hit-list.blade.php
|
||||||
|
│ │ ├── factions/
|
||||||
|
│ │ │ ├── index.blade.php
|
||||||
|
│ │ │ ├── show.blade.php
|
||||||
|
│ │ │ └── manage.blade.php
|
||||||
|
│ │ ├── inventory.blade.php
|
||||||
|
│ │ ├── shops.blade.php
|
||||||
|
│ │ ├── market.blade.php
|
||||||
|
│ │ ├── mail/
|
||||||
|
│ │ │ ├── inbox.blade.php
|
||||||
|
│ │ │ └── compose.blade.php
|
||||||
|
│ │ └── leaderboards.blade.php
|
||||||
|
│ └── auth/
|
||||||
|
│ └── (Laravel default)
|
||||||
|
└── lang/
|
||||||
|
└── en/
|
||||||
|
├── game.php
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
database/
|
||||||
|
├── migrations/
|
||||||
|
├── seeders/
|
||||||
|
└── factories/
|
||||||
|
|
||||||
|
public/
|
||||||
|
├── css/
|
||||||
|
├── js/
|
||||||
|
└── images/
|
||||||
|
|
||||||
|
config/
|
||||||
|
├── game.php
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
storage/
|
||||||
|
logs/
|
||||||
|
|
||||||
|
tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Key Services (Business Logic Layer)
|
||||||
|
|
||||||
|
| Service | Purpose | Key Methods |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| **CombatService** | Handles all attack calculations | `calculateDamage()`, `processAttack()`, `determineWinner()`, `applyCrit()` |
|
||||||
|
| **EconomyService** | Money, bank, transactions | `transfer()`, `deposit()`, `withdraw()`, `applyInterest()`, `addMoney()`, `removeMoney()` |
|
||||||
|
| **EnergyService** | Energy management | `consumeEnergy()`, `regenEnergy()`, `getMaxEnergy()`, `getRegenRate()` |
|
||||||
|
| **HealthService** | Health management | `takeDamage()`, `heal()`, `revive()`, `isDead()`, `isInHospital()` |
|
||||||
|
| **CrimeService** | Crime attempts | `attemptCrime()`, `calculateSuccess()`, `applyRewards()`, `applyPenalties()` |
|
||||||
|
| **FactionService** | Faction operations | `createFaction()`, `joinFaction()`, `leaveFaction()`, `promoteMember()`, `demoteMember()` |
|
||||||
|
| **InventoryService** | Item management | `addItem()`, `removeItem()`, `useItem()`, `equipItem()`, `unequipItem()` |
|
||||||
|
| **NotificationService** | User notifications | `sendNotification()`, `broadcastEvent()`, `factionAnnouncement()`, `systemMessage()` |
|
||||||
|
| **TravelService** | Location travel | `travelTo()`, `calculateCost()`, `isUnlocked()`, `getLocations()` |
|
||||||
|
| **MarketService** | Player marketplace | `createListing()`, `purchaseListing()`, `cancelListing()`, `getListings()` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Scheduled Commands
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In app/Console/Kernel.php
|
||||||
|
|
||||||
|
protected function schedule(Schedule $schedule)
|
||||||
|
{
|
||||||
|
// Energy regen (every 5 minutes)
|
||||||
|
$schedule->command('game:regen-energy')->everyFiveMinutes()->name('energy-regen');
|
||||||
|
|
||||||
|
// Health regen (every 10 minutes)
|
||||||
|
$schedule->command('game:regen-health')->everyTenMinutes()->name('health-regen');
|
||||||
|
|
||||||
|
// Release from hospital (every minute)
|
||||||
|
$schedule->command('game:release-hospital')->everyMinute()->name('hospital-release');
|
||||||
|
|
||||||
|
// Release from jail (every minute)
|
||||||
|
$schedule->command('game:release-jail')->everyMinute()->name('jail-release');
|
||||||
|
|
||||||
|
// Daily bank interest (midnight)
|
||||||
|
$schedule->command('game:daily-interest')->dailyAt('00:00')->name('daily-interest');
|
||||||
|
|
||||||
|
// Restock shops (every 6 hours)
|
||||||
|
$schedule->command('game:restock-shops')->everySixHours()->name('restock-shops');
|
||||||
|
|
||||||
|
// Cleanup old activity (daily)
|
||||||
|
$schedule->command('game:cleanup-logs')->dailyAt('01:00')->name('cleanup-logs');
|
||||||
|
|
||||||
|
// Faction wars reset (weekly)
|
||||||
|
$schedule->command('game:reset-wars')->weekly()->mondays()->at('00:00')->name('reset-wars');
|
||||||
|
|
||||||
|
// Process cooldowns (every minute)
|
||||||
|
$schedule->command('game:process-cooldowns')->everyMinute()->name('process-cooldowns');
|
||||||
|
|
||||||
|
// Update leaderboards (hourly)
|
||||||
|
$schedule->command('game:update-leaderboards')->hourly()->name('update-leaderboards');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Middleware
|
||||||
|
|
||||||
|
| Middleware | Purpose | Logic |
|
||||||
|
|------------|---------|-------|
|
||||||
|
| **CheckEnergy** | Ensure user has enough energy | Redirect if energy < action cost |
|
||||||
|
| **CheckInJail** | Block actions if in jail | Redirect to jail page with release time |
|
||||||
|
| **CheckInHospital** | Block actions if in hospital | Redirect to hospital page with release time |
|
||||||
|
| **VerifyCharacter** | Ensure user has a character | Redirect to character creation |
|
||||||
|
| **CheckLevel** | Verify level requirements | Abort 403 if level too low |
|
||||||
|
| **RateLimitActions** | Prevent action spamming | Throttle by action type (configurable) |
|
||||||
|
| **LogActivity** | Track user actions | Create activity log entries automatically |
|
||||||
|
| **CheckCooldown** | Prevent action spamming | Check if action is on cooldown |
|
||||||
|
| **CheckAlive** | Ensure character is alive | Redirect if health <= 0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Frontend Approach
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- **CSS Framework**: Tailwind CSS (via Laravel Breeze)
|
||||||
|
- **JavaScript**: Alpine.js (lightweight reactivity)
|
||||||
|
- **Real-time**: Laravel Echo + Pusher (for chat, notifications)
|
||||||
|
- **Charts**: Chart.js (for stats, leaderboards)
|
||||||
|
- **Icons**: Heroicons or Font Awesome
|
||||||
|
|
||||||
|
### Key Pages
|
||||||
|
|
||||||
|
#### Phase 1 Pages
|
||||||
|
1. **Home/Dashboard** - Overview, stats, recent activity, quick actions
|
||||||
|
2. **Character Create** - Name, gender, starting stats selection
|
||||||
|
3. **Character Profile** - Stats, equipment, achievements
|
||||||
|
4. **Crimes** - Available crimes list, attempt history
|
||||||
|
5. **Inventory** - Items, equipment, use/equip actions
|
||||||
|
6. **Shops** - Browse and purchase items
|
||||||
|
7. **City/Locations** - Current location info, available actions
|
||||||
|
8. **Leaderboards** - Top players by level, money, etc.
|
||||||
|
|
||||||
|
#### Phase 2 Pages
|
||||||
|
9. **Attack** - Find targets, view hit lists, attack history
|
||||||
|
10. **Factions** - Faction list, create/join, member list
|
||||||
|
11. **Faction Management** - Ranks, members, bank, chat
|
||||||
|
12. **Market** - Player-to-player item trading
|
||||||
|
13. **Mail** - Inbox, compose, read messages
|
||||||
|
14. **Bank** - Deposit, withdraw, transfer
|
||||||
|
15. **Travel** - Map, travel between locations
|
||||||
|
16. **Jail** - Current sentence, breakout attempt
|
||||||
|
17. **Hospital** - Revive options, healing items
|
||||||
|
|
||||||
|
#### Phase 3+ Pages
|
||||||
|
18. **Missions** - Available missions, progress tracking
|
||||||
|
19. **Faction Wars** - Active wars, attack history
|
||||||
|
20. **Territory** - Controlled areas, income bonuses
|
||||||
|
21. **Casino** - Gambling games
|
||||||
|
22. **Stock Market** - Company listings, portfolio
|
||||||
|
23. **Properties** - Owned properties, upgrades
|
||||||
|
24. **Admin Dashboard** - User management, game settings
|
||||||
|
|
||||||
|
### UI Components (Blade + Alpine)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Energy Bar Component -->
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-4">
|
||||||
|
<div
|
||||||
|
class="bg-blue-600 h-4 rounded-full transition-all duration-500"
|
||||||
|
style="width: {{ $character->energy_percentage }}%"
|
||||||
|
x-data="{ energy: {{ $character->energy }}, max: {{ $character->max_energy }} }"
|
||||||
|
x-text="energy + '/' + max"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cooldown Timer -->
|
||||||
|
<span
|
||||||
|
x-data="{
|
||||||
|
cooldown: {{ $cooldownSeconds }},
|
||||||
|
formatTime(seconds) {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
x-text="formatTime(cooldown)"
|
||||||
|
x-init="setInterval(() => { if(cooldown > 0) cooldown--; }, 1000)"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<!-- Action Button with Energy Check -->
|
||||||
|
<button
|
||||||
|
@click="attemptCrime({{ $crime->id }})"
|
||||||
|
:disabled="energy < {{ $crime->energy_cost }}"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Attempt ({{ $crime->energy_cost }} energy)
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Game Configuration
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/game.php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'energy' => [
|
||||||
|
'start_amount' => 100,
|
||||||
|
'max' => 100,
|
||||||
|
'regen_rate' => 5, // Energy regenerated per interval
|
||||||
|
'regen_interval' => 300, // Seconds between regen (5 minutes)
|
||||||
|
'action_costs' => [
|
||||||
|
'crime' => 10,
|
||||||
|
'attack' => 15,
|
||||||
|
'travel' => 5,
|
||||||
|
'train' => 20,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'health' => [
|
||||||
|
'start_amount' => 100,
|
||||||
|
'max' => 100,
|
||||||
|
'regen_rate' => 2, // Health regenerated per interval
|
||||||
|
'regen_interval' => 600, // Seconds between regen (10 minutes)
|
||||||
|
'hospital_cost' => 1000, // Cost to revive from hospital
|
||||||
|
'death_threshold' => 0, // Health <= 0 = dead
|
||||||
|
],
|
||||||
|
|
||||||
|
'combat' => [
|
||||||
|
'base_damage' => 5,
|
||||||
|
'damage_variation' => 0.20, // +/- 20% random variation
|
||||||
|
'crit_chance' => 0.10, // 10% chance for critical hit
|
||||||
|
'crit_multiplier' => 2.0, // Critical hits do 2x damage
|
||||||
|
'defense_reduction' => 0.5, // Each defense point reduces damage by 0.5
|
||||||
|
'min_damage' => 1, // Minimum damage per hit
|
||||||
|
'attack_cooldown' => 300, // 5 minutes between attacks
|
||||||
|
'hit_list_cost' => 5000, // Cost to place a hit
|
||||||
|
],
|
||||||
|
|
||||||
|
'crime' => [
|
||||||
|
'success_rate' => 0.70, // 70% base success rate
|
||||||
|
'failure_penalty' => 0.10, // 10% health loss on failure
|
||||||
|
'cooldown_reduction' => 0.10, // 10% cooldown reduction per level
|
||||||
|
'reward_multiplier' => 1.10, // 10% more reward per level
|
||||||
|
],
|
||||||
|
|
||||||
|
'jail' => [
|
||||||
|
'sentence_length' => 3600, // 1 hour base sentence
|
||||||
|
'breakout_chance' => 0.05, // 5% chance to break out per attempt
|
||||||
|
'breakout_cost' => 1000, // Cost to attempt breakout
|
||||||
|
'sentence_multiplier' => 1.5, // Sentence increases by 50% for repeat offenses
|
||||||
|
],
|
||||||
|
|
||||||
|
'economy' => [
|
||||||
|
'bank_interest' => 0.05, // 5% daily interest
|
||||||
|
'tax_rate' => 0.10, // 10% tax on faction bank withdrawals
|
||||||
|
'start_money' => 5000, // Starting cash
|
||||||
|
'start_bank' => 0, // Starting bank balance
|
||||||
|
'max_bank' => 10000000, // Maximum bank balance
|
||||||
|
],
|
||||||
|
|
||||||
|
'leveling' => [
|
||||||
|
'exp_curve' => 'exponential', // 'linear' or 'exponential'
|
||||||
|
'exp_per_level' => 1000, // Base EXP per level
|
||||||
|
'exp_multiplier' => 1.20, // 20% more EXP needed per level
|
||||||
|
'stat_points_per_level' => 2, // Points to allocate per level
|
||||||
|
'max_level' => 100, // Maximum character level
|
||||||
|
],
|
||||||
|
|
||||||
|
'skills' => [
|
||||||
|
'strength' => [
|
||||||
|
'attack_bonus' => 0.5, // +0.5 damage per point
|
||||||
|
'max' => 100,
|
||||||
|
],
|
||||||
|
'defense' => [
|
||||||
|
'damage_reduction' => 0.5, // -0.5 damage per point
|
||||||
|
'max' => 100,
|
||||||
|
],
|
||||||
|
'speed' => [
|
||||||
|
'attack_first_chance' => 0.02, // +2% chance to attack first per point
|
||||||
|
'max' => 100,
|
||||||
|
],
|
||||||
|
'dexterity' => [
|
||||||
|
'crit_chance' => 0.005, // +0.5% crit chance per point
|
||||||
|
'max' => 100,
|
||||||
|
],
|
||||||
|
'training_cost' => 100, // Money cost to train a stat
|
||||||
|
'training_limit' => 5, // Max stat points per training session
|
||||||
|
],
|
||||||
|
|
||||||
|
'factions' => [
|
||||||
|
'create_cost' => 10000, // Cost to create a faction
|
||||||
|
'create_level' => 10, // Minimum level to create a faction
|
||||||
|
'max_members' => 50, // Maximum faction members
|
||||||
|
'respect_per_win' => 10, // Respect gained per war win
|
||||||
|
'respect_per_loss' => -5, // Respect lost per war loss
|
||||||
|
'bank_tax' => 0.05, // 5% tax on faction bank deposits
|
||||||
|
],
|
||||||
|
|
||||||
|
'locations' => [
|
||||||
|
'start_location' => 1, // ID of starting location
|
||||||
|
'travel_cost_base' => 100, // Base travel cost
|
||||||
|
'travel_cost_per_level' => 10, // Additional cost per location level
|
||||||
|
],
|
||||||
|
|
||||||
|
'limits' => [
|
||||||
|
'max_inventory_items' => 100,
|
||||||
|
'max_equipped_items' => 10,
|
||||||
|
'max_mail_messages' => 50,
|
||||||
|
'max_market_listings' => 20,
|
||||||
|
'max_faction_chat_history' => 100,
|
||||||
|
],
|
||||||
|
|
||||||
|
'cooldowns' => [
|
||||||
|
'default' => 60, // 1 minute default cooldown
|
||||||
|
'attack' => 300, // 5 minutes
|
||||||
|
'crime' => 120, // 2 minutes
|
||||||
|
'travel' => 60, // 1 minute
|
||||||
|
'train' => 300, // 5 minutes
|
||||||
|
'market_listing' => 60, // 1 minute
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Character Progression
|
||||||
|
|
||||||
|
### Starting Stats (Level 1)
|
||||||
|
- **Health**: 100/100
|
||||||
|
- **Energy**: 100/100
|
||||||
|
- **Money**: $5,000
|
||||||
|
- **Bank**: $0
|
||||||
|
- **Strength**: 5
|
||||||
|
- **Defense**: 5
|
||||||
|
- **Speed**: 5
|
||||||
|
- **Dexterity**: 5
|
||||||
|
- **Experience**: 0/1000
|
||||||
|
- **Level**: 1
|
||||||
|
|
||||||
|
### Level Up Rewards
|
||||||
|
- **Experience**: Reset to 0, next level requires more EXP
|
||||||
|
- **Stat Points**: +2 points to distribute among stats
|
||||||
|
- **Health**: +5 max health
|
||||||
|
- **Energy**: +5 max energy
|
||||||
|
- **Money**: +$1,000 bonus
|
||||||
|
|
||||||
|
### Experience Sources
|
||||||
|
| Action | EXP Reward |
|
||||||
|
|--------|------------|
|
||||||
|
| Successful Crime | 50-200 (scaled by difficulty) |
|
||||||
|
| Failed Crime | 10-50 |
|
||||||
|
| Winning Attack | 100-300 |
|
||||||
|
| Losing Attack | 25-100 |
|
||||||
|
| Killing Player | 500 |
|
||||||
|
| Training Stat | 10 |
|
||||||
|
| Completing Mission | 100-1000 |
|
||||||
|
| Faction War Win | 500 |
|
||||||
|
| Daily Login | 50 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 Economy Balance
|
||||||
|
|
||||||
|
### Money Sources
|
||||||
|
| Source | Amount | Frequency |
|
||||||
|
|--------|--------|-----------|
|
||||||
|
| Starting | $5,000 | Once |
|
||||||
|
| Crime Rewards | $50-$500 | Per crime |
|
||||||
|
| Attack Wins | $100-$1,000 | Per win |
|
||||||
|
| Selling Items | Variable | Per sale |
|
||||||
|
| Bank Interest | 5% daily | Daily |
|
||||||
|
| Faction Bank | Variable | Per deposit |
|
||||||
|
| Property Income | $100-$10,000 | Per hour |
|
||||||
|
| Stock Market | Variable | Per trade |
|
||||||
|
|
||||||
|
### Money Sinks
|
||||||
|
| Sink | Amount | Frequency |
|
||||||
|
|------|--------|-----------|
|
||||||
|
| Attack Loses | $50-$500 | Per loss |
|
||||||
|
| Crime Failures | $10-$100 | Per failure |
|
||||||
|
| Hospital Revival | $1,000 | Per death |
|
||||||
|
| Jail Breakout | $1,000 | Per attempt |
|
||||||
|
| Training Stats | $100 | Per point |
|
||||||
|
| Travel | $100-$1,000 | Per trip |
|
||||||
|
| Buying Items | Variable | Per purchase |
|
||||||
|
| Faction Creation | $10,000 | Once |
|
||||||
|
| Hit List Placement | $5,000 | Per hit |
|
||||||
|
| Property Purchase | Variable | Per property |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎲 Game Mechanics Details
|
||||||
|
|
||||||
|
### Combat System
|
||||||
|
|
||||||
|
```
|
||||||
|
Attacker Rolls Initiative:
|
||||||
|
- Base: 50% chance to attack first
|
||||||
|
- Speed bonus: +2% per speed point
|
||||||
|
- Random: +/- 10% variation
|
||||||
|
|
||||||
|
If Attacker goes first:
|
||||||
|
1. Attacker calculates damage
|
||||||
|
2. Defender calculates damage (if still alive)
|
||||||
|
|
||||||
|
Damage Calculation:
|
||||||
|
Base Damage = attacker_strength * 0.5 + weapon_attack_bonus
|
||||||
|
Damage Variation = Base Damage * (0.8 + random(0, 0.4))
|
||||||
|
Critical Check = if random(0, 1) < (0.1 + attacker_dexterity * 0.005)
|
||||||
|
Damage *= 2
|
||||||
|
|
||||||
|
Defender Reduction:
|
||||||
|
Defense Reduction = defender_defense * 0.5
|
||||||
|
Actual Damage = max(1, Damage - Defense Reduction)
|
||||||
|
|
||||||
|
Defender Health -= Actual Damage
|
||||||
|
|
||||||
|
If Defender Health <= 0:
|
||||||
|
Attacker wins
|
||||||
|
Defender goes to hospital
|
||||||
|
Attacker gets EXP and money rewards
|
||||||
|
```
|
||||||
|
|
||||||
|
### Crime System
|
||||||
|
|
||||||
|
```
|
||||||
|
Success Check:
|
||||||
|
Base Success Rate = crime_success_rate
|
||||||
|
Modified Rate = Base Rate + (player_level * 0.01) - (crime_difficulty * 0.05)
|
||||||
|
Final Rate = clamp(Modified Rate, 0.1, 0.95)
|
||||||
|
|
||||||
|
Roll = random(0, 1)
|
||||||
|
If Roll <= Final Rate:
|
||||||
|
Success!
|
||||||
|
Reward = random(crime_reward_min, crime_reward_max)
|
||||||
|
EXP = crime_experience * (1 + player_level * 0.01)
|
||||||
|
Else:
|
||||||
|
Failure
|
||||||
|
Health Loss = max(1, player_max_health * crime_failure_penalty)
|
||||||
|
No reward
|
||||||
|
```
|
||||||
|
|
||||||
|
### Faction Wars
|
||||||
|
|
||||||
|
```
|
||||||
|
War Declaration:
|
||||||
|
- Requires: 5,000 respect
|
||||||
|
- Cooldown: 24 hours between declarations
|
||||||
|
- Duration: 7 days (configurable)
|
||||||
|
|
||||||
|
War Actions:
|
||||||
|
- Members can attack enemy faction members
|
||||||
|
- Each kill: +10 respect for faction
|
||||||
|
- Each death: -5 respect for faction
|
||||||
|
|
||||||
|
War Victory:
|
||||||
|
- Faction with most kills wins
|
||||||
|
- Winner gets: 50% of loser's faction bank
|
||||||
|
- Loser gets: Respect penalty, temporary debuffs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Development Timeline
|
||||||
|
|
||||||
|
| Phase | Duration | Deliverables | Team Size | Status |
|
||||||
|
|-------|----------|--------------|-----------|--------|
|
||||||
|
| **Phase 1: Foundation** | 2-3 weeks | Auth, Character, Energy, Crimes, Inventory, UI | 1-2 devs | ⬜ Not Started |
|
||||||
|
| **Phase 2: Core Gameplay** | 3-4 weeks | Combat, Factions, Skills, Market, Jail/Hospital | 2 devs | ⬜ Not Started |
|
||||||
|
| **Phase 3: Advanced Features** | 4-5 weeks | Wars, Territory, Missions, Casino, Stock Market | 2-3 devs | ⬜ Not Started |
|
||||||
|
| **Phase 4: Polish** | 2-3 weeks | Admin, Moderation, API, Real-time | 1-2 devs | ⬜ Not Started |
|
||||||
|
| **Testing** | 2 weeks | Bug fixes, balance, stress testing | All | ⬜ Not Started |
|
||||||
|
| **Launch** | 1 week | Deployment, monitoring | All | ⬜ Not Started |
|
||||||
|
|
||||||
|
**Total Estimated Time: 14-17 weeks (3.5-4 months)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Deployment Strategy
|
||||||
|
|
||||||
|
### Environment Requirements
|
||||||
|
- **PHP**: 8.2+
|
||||||
|
- **Laravel**: 11.x
|
||||||
|
- **Database**: MySQL 8.0+ or PostgreSQL 15+
|
||||||
|
- **Cache**: Redis 7.0+
|
||||||
|
- **Queue**: Redis + Supervisor
|
||||||
|
- **Web Server**: Nginx or Apache
|
||||||
|
- **OS**: Ubuntu 22.04 LTS
|
||||||
|
|
||||||
|
### Hosting Options
|
||||||
|
1. **Laravel Forge + DigitalOcean** (Recommended for simplicity)
|
||||||
|
2. **Laravel Vapor** (Serverless, scales automatically)
|
||||||
|
3. **AWS EC2 + RDS** (For enterprise scale)
|
||||||
|
4. **Self-hosted VPS** (For full control)
|
||||||
|
|
||||||
|
### Deployment Steps
|
||||||
|
1. Set up server with PHP, MySQL, Redis, Nginx
|
||||||
|
2. Configure Laravel environment variables
|
||||||
|
3. Run migrations and seeders
|
||||||
|
4. Set up cron jobs for scheduled commands
|
||||||
|
5. Configure queue workers
|
||||||
|
6. Set up SSL certificate (Let's Encrypt)
|
||||||
|
7. Configure monitoring (Sentry, Laravel Horizon)
|
||||||
|
8. Set up backups (daily database backups)
|
||||||
|
|
||||||
|
### Scaling Considerations
|
||||||
|
- **Database**: Use read replicas for leaderboards and reports
|
||||||
|
- **Queue**: Separate queue workers for different job types
|
||||||
|
- **Caching**: Aggressive caching for:
|
||||||
|
- Leaderboards (cache for 5-10 minutes)
|
||||||
|
- Shop items (cache for 1 hour)
|
||||||
|
- Character stats (cache for 1 minute)
|
||||||
|
- **Rate Limiting**: Per-action rate limits to prevent abuse
|
||||||
|
- **Horizontal Scaling**: Stateless app servers behind load balancer
|
||||||
|
|
||||||
|
### Security Measures
|
||||||
|
- HTTPS everywhere (301 redirect from HTTP)
|
||||||
|
- CSRF protection (Laravel default)
|
||||||
|
- Input validation (Form Requests)
|
||||||
|
- Rate limiting (Laravel ThrottleRequests)
|
||||||
|
- SQL injection prevention (Eloquent ORM)
|
||||||
|
- XSS protection (Blade {{ }} escaping)
|
||||||
|
- Regular backups (daily, encrypted)
|
||||||
|
- Security headers (CSP, X-Frame-Options, etc.)
|
||||||
|
- Fail2Ban for brute force protection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Implementation Checklist
|
||||||
|
|
||||||
|
### Phase 1 Checklist
|
||||||
|
|
||||||
|
- [ ] Set up Laravel project with Breeze
|
||||||
|
- [ ] Configure database connection
|
||||||
|
- [ ] Create User model (extend Laravel default)
|
||||||
|
- [ ] Create Character model and migration
|
||||||
|
- [ ] Create Character factory and seeder
|
||||||
|
- [ ] Create Location model and migration
|
||||||
|
- [ ] Create Item model and migration
|
||||||
|
- [ ] Create Inventory pivot table
|
||||||
|
- [ ] Create Crime model and migration
|
||||||
|
- [ ] Create ActivityLog model and migration
|
||||||
|
- [ ] Create Notification model and migration
|
||||||
|
- [ ] Set up relationships between models
|
||||||
|
- [ ] Create CharacterController
|
||||||
|
- [ ] Create CrimeController
|
||||||
|
- [ ] Create InventoryController
|
||||||
|
- [ ] Create EnergyService
|
||||||
|
- [ ] Create HealthService
|
||||||
|
- [ ] Create EconomyService
|
||||||
|
- [ ] Create CrimeService
|
||||||
|
- [ ] Create NotificationService
|
||||||
|
- [ ] Create middleware: CheckEnergy, CheckInJail, CheckInHospital, VerifyCharacter
|
||||||
|
- [ ] Create scheduled commands for energy/health regen
|
||||||
|
- [ ] Create game config file
|
||||||
|
- [ ] Set up Tailwind CSS
|
||||||
|
- [ ] Create base layout (app.blade.php)
|
||||||
|
- [ ] Create game layout (game.blade.php)
|
||||||
|
- [ ] Create character creation pages
|
||||||
|
- [ ] Create dashboard page
|
||||||
|
- [ ] Create crimes page
|
||||||
|
- [ ] Create inventory page
|
||||||
|
- [ ] Create partials: energy-bar, health-bar, nav, notifications
|
||||||
|
- [ ] Add Alpine.js for interactivity
|
||||||
|
- [ ] Test character creation flow
|
||||||
|
- [ ] Test energy consumption and regen
|
||||||
|
- [ ] Test crime attempts
|
||||||
|
- [ ] Test inventory management
|
||||||
|
- [ ] Deploy to staging environment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes & Assumptions
|
||||||
|
|
||||||
|
1. **Game Name**: "The Crime Syndicate" (can be changed)
|
||||||
|
2. **Single Character per User**: Initially, one character per user account
|
||||||
|
3. **Turn-Based**: Actions are not real-time; players take turns
|
||||||
|
4. **Persistent World**: Game state updates even when players are offline
|
||||||
|
5. **Browser-Based**: No downloads required; works in any modern browser
|
||||||
|
6. **Mobile-Friendly**: Responsive design for mobile devices
|
||||||
|
7. **Free to Play**: Core gameplay is free; premium features optional
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Resources & References
|
||||||
|
|
||||||
|
- **Omerta Game**: https://www.omertagame.com/
|
||||||
|
- **Torn City**: https://www.torn.com/
|
||||||
|
- **Laravel Documentation**: https://laravel.com/docs
|
||||||
|
- **Tailwind CSS**: https://tailwindcss.com/docs
|
||||||
|
- **Alpine.js**: https://alpinejs.dev/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Contacts & Contributors
|
||||||
|
|
||||||
|
- **Project Lead**: [Your Name]
|
||||||
|
- **Developers**: [Team Members]
|
||||||
|
- **Designers**: [Team Members]
|
||||||
|
- **Testers**: [Team Members]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Last Updated**: 2026-06-26
|
||||||
|
**Next Review**: After Phase 1 completion
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Laravel
|
||||||
|
|
||||||
|
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||||
|
|
||||||
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||||
|
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||||
|
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||||
|
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||||
|
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||||
|
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||||
|
|
||||||
|
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||||
|
|
||||||
|
## Learning Laravel
|
||||||
|
|
||||||
|
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||||
|
|
||||||
|
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||||
|
|
||||||
|
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
|
||||||
|
|
||||||
|
## Agentic Development
|
||||||
|
|
||||||
|
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require laravel/boost --dev
|
||||||
|
|
||||||
|
php artisan boost:install
|
||||||
|
```
|
||||||
|
|
||||||
|
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Game;
|
||||||
|
|
||||||
|
use App\Models\Character;
|
||||||
|
use App\Services\Game\EnergyService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ProcessEnergyRegen extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'game:regen-energy';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Regenerate energy for all characters';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(EnergyService $energyService): void
|
||||||
|
{
|
||||||
|
$this->info('Starting energy regeneration...');
|
||||||
|
|
||||||
|
$characters = Character::where('energy', '<', \DB::raw('max_energy'))
|
||||||
|
->where('status', 'active')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$count = $characters->count();
|
||||||
|
|
||||||
|
$this->info("Regenerating energy for {$count} characters...");
|
||||||
|
|
||||||
|
foreach ($characters as $character) {
|
||||||
|
$energyService->regenerate($character);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Energy regeneration completed!');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Game;
|
||||||
|
|
||||||
|
use App\Models\Character;
|
||||||
|
use App\Services\Game\HealthService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ProcessHealthRegen extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'game:regen-health';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Regenerate health for all characters';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(HealthService $healthService): void
|
||||||
|
{
|
||||||
|
$this->info('Starting health regeneration...');
|
||||||
|
|
||||||
|
$characters = Character::where('health', '<', \DB::raw('max_health'))
|
||||||
|
->where('status', 'active')
|
||||||
|
->where('health', '>', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$count = $characters->count();
|
||||||
|
|
||||||
|
$this->info("Regenerating health for {$count} characters...");
|
||||||
|
|
||||||
|
foreach ($characters as $character) {
|
||||||
|
$healthService->regenerate($character);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Health regeneration completed!');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum AttackResult: string
|
||||||
|
{
|
||||||
|
case WIN = 'win';
|
||||||
|
case LOSE = 'lose';
|
||||||
|
case DRAW = 'draw';
|
||||||
|
case ESCAPE = 'escape';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all result values as an array
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(self::cases(), 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for a result
|
||||||
|
*/
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::WIN => 'Victory',
|
||||||
|
self::LOSE => 'Defeat',
|
||||||
|
self::DRAW => 'Draw',
|
||||||
|
self::ESCAPE => 'Escaped',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the attacker won
|
||||||
|
*/
|
||||||
|
public function attackerWon(): bool
|
||||||
|
{
|
||||||
|
return $this === self::WIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the defender won (attacker lost)
|
||||||
|
*/
|
||||||
|
public function defenderWon(): bool
|
||||||
|
{
|
||||||
|
return $this === self::LOSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the experience reward for this result (for attacker)
|
||||||
|
*/
|
||||||
|
public function experienceReward(): int
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::WIN => 200,
|
||||||
|
self::LOSE => 50,
|
||||||
|
self::DRAW => 100,
|
||||||
|
self::ESCAPE => 25,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the money reward for this result (for attacker)
|
||||||
|
*/
|
||||||
|
public function moneyReward(): int
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::WIN => 500,
|
||||||
|
self::LOSE => 0,
|
||||||
|
self::DRAW => 100,
|
||||||
|
self::ESCAPE => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum CharacterStatus: string
|
||||||
|
{
|
||||||
|
case ACTIVE = 'active';
|
||||||
|
case IN_HOSPITAL = 'in_hospital';
|
||||||
|
case IN_JAIL = 'in_jail';
|
||||||
|
case DEAD = 'dead';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all status values as an array
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(self::cases(), 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for a status
|
||||||
|
*/
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::ACTIVE => 'Active',
|
||||||
|
self::IN_HOSPITAL => 'In Hospital',
|
||||||
|
self::IN_JAIL => 'In Jail',
|
||||||
|
self::DEAD => 'Dead',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character can perform actions
|
||||||
|
*/
|
||||||
|
public function canAct(): bool
|
||||||
|
{
|
||||||
|
return $this === self::ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color class for the status badge
|
||||||
|
*/
|
||||||
|
public function color(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::ACTIVE => 'bg-green-500',
|
||||||
|
self::IN_HOSPITAL => 'bg-red-500',
|
||||||
|
self::IN_JAIL => 'bg-yellow-500',
|
||||||
|
self::DEAD => 'bg-gray-500',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum CrimeDifficulty: string
|
||||||
|
{
|
||||||
|
case EASY = 'easy';
|
||||||
|
case MEDIUM = 'medium';
|
||||||
|
case HARD = 'hard';
|
||||||
|
case VERY_HARD = 'very_hard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all difficulty values as an array
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(self::cases(), 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for a difficulty
|
||||||
|
*/
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::EASY => 'Easy',
|
||||||
|
self::MEDIUM => 'Medium',
|
||||||
|
self::HARD => 'Hard',
|
||||||
|
self::VERY_HARD => 'Very Hard',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the success rate modifier for this difficulty
|
||||||
|
*/
|
||||||
|
public function successModifier(): float
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::EASY => 0.20, // +20% success rate
|
||||||
|
self::MEDIUM => 0.00, // Base success rate
|
||||||
|
self::HARD => -0.20, // -20% success rate
|
||||||
|
self::VERY_HARD => -0.40,// -40% success rate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the reward multiplier for this difficulty
|
||||||
|
*/
|
||||||
|
public function rewardMultiplier(): float
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::EASY => 0.5,
|
||||||
|
self::MEDIUM => 1.0,
|
||||||
|
self::HARD => 2.0,
|
||||||
|
self::VERY_HARD => 3.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color class for the difficulty badge
|
||||||
|
*/
|
||||||
|
public function color(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::EASY => 'bg-green-500',
|
||||||
|
self::MEDIUM => 'bg-yellow-500',
|
||||||
|
self::HARD => 'bg-orange-500',
|
||||||
|
self::VERY_HARD => 'bg-red-500',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum ItemType: string
|
||||||
|
{
|
||||||
|
case WEAPON = 'weapon';
|
||||||
|
case ARMOR = 'armor';
|
||||||
|
case CONSUMABLE = 'consumable';
|
||||||
|
case MISC = 'misc';
|
||||||
|
case CURRENCY = 'currency';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all item type values as an array
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(self::cases(), 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for an item type
|
||||||
|
*/
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::WEAPON => 'Weapon',
|
||||||
|
self::ARMOR => 'Armor',
|
||||||
|
self::CONSUMABLE => 'Consumable',
|
||||||
|
self::MISC => 'Miscellaneous',
|
||||||
|
self::CURRENCY => 'Currency',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this item type can be equipped
|
||||||
|
*/
|
||||||
|
public function canEquip(): bool
|
||||||
|
{
|
||||||
|
return in_array($this, [self::WEAPON, self::ARMOR]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this item type can be used/consumed
|
||||||
|
*/
|
||||||
|
public function canUse(): bool
|
||||||
|
{
|
||||||
|
return $this === self::CONSUMABLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Auth\LoginRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AuthenticatedSessionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the login view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming authentication request.
|
||||||
|
*/
|
||||||
|
public function store(LoginRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->authenticate();
|
||||||
|
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy an authenticated session.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ConfirmablePasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the confirm password view.
|
||||||
|
*/
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
return view('auth.confirm-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm the user's password.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if (! Auth::guard('web')->validate([
|
||||||
|
'email' => $request->user()->email,
|
||||||
|
'password' => $request->password,
|
||||||
|
])) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => __('auth.password'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->put('auth.password_confirmed_at', time());
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EmailVerificationNotificationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send a new email verification notification.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->sendEmailVerificationNotification();
|
||||||
|
|
||||||
|
return back()->with('status', 'verification-link-sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class EmailVerificationPromptController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the email verification prompt.
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request): RedirectResponse|View
|
||||||
|
{
|
||||||
|
return $request->user()->hasVerifiedEmail()
|
||||||
|
? redirect()->intended(route('dashboard', absolute: false))
|
||||||
|
: view('auth.verify-email');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class NewPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset view.
|
||||||
|
*/
|
||||||
|
public function create(Request $request): View
|
||||||
|
{
|
||||||
|
return view('auth.reset-password', ['request' => $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming new password request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'token' => ['required'],
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Here we will attempt to reset the user's password. If it is successful we
|
||||||
|
// will update the password on an actual user model and persist it to the
|
||||||
|
// database. Otherwise we will parse the error and return the response.
|
||||||
|
$status = Password::reset(
|
||||||
|
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||||
|
function (User $user) use ($request) {
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'remember_token' => Str::random(60),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
event(new PasswordReset($user));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the password was successfully reset, we will redirect the user back to
|
||||||
|
// the application's home authenticated view. If there is an error we can
|
||||||
|
// redirect them back to where they came from with their error message.
|
||||||
|
return $status == Password::PASSWORD_RESET
|
||||||
|
? redirect()->route('login')->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class PasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the user's password.
|
||||||
|
*/
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validateWithBag('updatePassword', [
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('status', 'password-updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class PasswordResetLinkController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset link request view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.forgot-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming password reset link request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// We will send the password reset link to this user. Once we have attempted
|
||||||
|
// to send the link, we will examine the response then see the message we
|
||||||
|
// need to show to the user. Finally, we'll send out a proper response.
|
||||||
|
$status = Password::sendResetLink(
|
||||||
|
$request->only('email')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $status == Password::RESET_LINK_SENT
|
||||||
|
? back()->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\Registered;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class RegisteredUserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the registration view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.register');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming registration request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new Registered($user));
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
|
||||||
|
return redirect(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Auth\Events\Verified;
|
||||||
|
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class VerifyEmailController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mark the authenticated user's email address as verified.
|
||||||
|
*/
|
||||||
|
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
|
event(new Verified($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Game;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Character;
|
||||||
|
use App\Models\Location;
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use App\Services\Game\EconomyService;
|
||||||
|
use App\Services\Game\EnergyService;
|
||||||
|
use App\Services\Game\HealthService;
|
||||||
|
use App\Services\Game\NotificationService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class CharacterController extends Controller
|
||||||
|
{
|
||||||
|
protected EconomyService $economyService;
|
||||||
|
protected EnergyService $energyService;
|
||||||
|
protected HealthService $healthService;
|
||||||
|
protected NotificationService $notificationService;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->economyService = new EconomyService();
|
||||||
|
$this->energyService = new EnergyService();
|
||||||
|
$this->healthService = new HealthService();
|
||||||
|
$this->notificationService = new NotificationService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the character creation form
|
||||||
|
*/
|
||||||
|
public function create(Request $request): View
|
||||||
|
{
|
||||||
|
$genders = ['male', 'female', 'other'];
|
||||||
|
$locations = Location::orderBy('name')->get();
|
||||||
|
|
||||||
|
return view('game.character.create', compact('genders', 'locations'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created character
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:50', 'unique:characters,name'],
|
||||||
|
'gender' => ['required', 'string', 'in:male,female,other'],
|
||||||
|
'location_id' => ['required', 'exists:locations,id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// Check if user already has a character
|
||||||
|
if ($user->character) {
|
||||||
|
return redirect()->route('game.dashboard')
|
||||||
|
->with('error', 'You already have a character!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the character
|
||||||
|
$character = Character::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'gender' => $validated['gender'],
|
||||||
|
'location_id' => $validated['location_id'],
|
||||||
|
'level' => 1,
|
||||||
|
'experience' => 0,
|
||||||
|
'money' => $this->economyService->getStartingMoney(),
|
||||||
|
'bank' => $this->economyService->getStartingBank(),
|
||||||
|
'energy' => config('game.energy.start_amount', 100),
|
||||||
|
'max_energy' => config('game.energy.max', 100),
|
||||||
|
'health' => config('game.health.start_amount', 100),
|
||||||
|
'max_health' => config('game.health.max', 100),
|
||||||
|
'strength' => 5,
|
||||||
|
'defense' => 5,
|
||||||
|
'speed' => 5,
|
||||||
|
'dexterity' => 5,
|
||||||
|
'status' => 'active',
|
||||||
|
'last_action_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Log character creation
|
||||||
|
ActivityLog::logCharacterCreated($character);
|
||||||
|
|
||||||
|
return redirect()->route('game.dashboard')
|
||||||
|
->with('success', 'Character created successfully! Welcome to the game.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the character profile
|
||||||
|
*/
|
||||||
|
public function show(Request $request): View
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('game.character.show', compact('character'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the character edit form
|
||||||
|
*/
|
||||||
|
public function edit(Request $request): View
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('game.character.edit', compact('character'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the character
|
||||||
|
*/
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['sometimes', 'string', 'max:50', 'unique:characters,name,'.$character->id],
|
||||||
|
'gender' => ['sometimes', 'string', 'in:male,female,other'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$character->update($validated);
|
||||||
|
|
||||||
|
return redirect()->route('game.character.show')
|
||||||
|
->with('success', 'Character updated successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposit money to bank
|
||||||
|
*/
|
||||||
|
public function deposit(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'amount' => ['required', 'integer', 'min:1'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->economyService->depositToBank($character, $validated['amount'])) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('success', 'Money deposited to bank successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('error', 'Failed to deposit money. You may not have enough money or reached the bank limit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw money from bank
|
||||||
|
*/
|
||||||
|
public function withdraw(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'amount' => ['required', 'integer', 'min:1'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->economyService->withdrawFromBank($character, $validated['amount'])) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('success', 'Money withdrawn from bank successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('error', 'Failed to withdraw money. You may not have enough in your bank.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revive character from hospital
|
||||||
|
*/
|
||||||
|
public function revive(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character->isDead() && !$character->isInHospital()) {
|
||||||
|
return redirect()->route('game.dashboard')
|
||||||
|
->with('error', 'Your character is not in hospital.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->healthService->revive($character)) {
|
||||||
|
$this->notificationService->sendCharacterRevived($request->user(), $character);
|
||||||
|
|
||||||
|
return redirect()->route('game.dashboard')
|
||||||
|
->with('success', 'Character revived successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('game.dashboard')
|
||||||
|
->with('error', 'Failed to revive character. You may not have enough money.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Game;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Crime;
|
||||||
|
use App\Models\CrimeAttempt;
|
||||||
|
use App\Services\Game\CrimeService;
|
||||||
|
use App\Services\Game\EnergyService;
|
||||||
|
use App\Services\Game\NotificationService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class CrimeController extends Controller
|
||||||
|
{
|
||||||
|
protected CrimeService $crimeService;
|
||||||
|
protected EnergyService $energyService;
|
||||||
|
protected NotificationService $notificationService;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->crimeService = new CrimeService();
|
||||||
|
$this->energyService = new EnergyService();
|
||||||
|
$this->notificationService = new NotificationService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of available crimes
|
||||||
|
*/
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
$availableCrimes = $this->crimeService->getAvailableCrimes($character);
|
||||||
|
$recentAttempts = $this->crimeService->getRecentAttempts($character, 5);
|
||||||
|
|
||||||
|
return view('game.crimes.index', compact('character', 'availableCrimes', 'recentAttempts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to commit a crime
|
||||||
|
*/
|
||||||
|
public function attempt(Request $request, Crime $crime): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$attempt = $this->crimeService->attemptCrime($character, $crime);
|
||||||
|
|
||||||
|
if ($attempt->isSuccessful()) {
|
||||||
|
$this->notificationService->sendCrimeSuccess($request->user(), $crime, $attempt->reward);
|
||||||
|
return redirect()->route('game.crimes.index')
|
||||||
|
->with('success', 'Crime successful! You earned $'.$attempt->reward);
|
||||||
|
} else {
|
||||||
|
$this->notificationService->sendCrimeFailure($request->user(), $crime);
|
||||||
|
return redirect()->route('game.crimes.index')
|
||||||
|
->with('error', 'Crime failed! You took damage.');
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return redirect()->route('game.crimes.index')
|
||||||
|
->with('error', $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show crime details
|
||||||
|
*/
|
||||||
|
public function show(Request $request, Crime $crime): View
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if crime is on cooldown
|
||||||
|
$onCooldown = $this->crimeService->isOnCooldown($character, $crime);
|
||||||
|
$cooldownRemaining = $onCooldown ? $this->crimeService->getCooldownRemaining($character, $crime) : 0;
|
||||||
|
|
||||||
|
// Calculate success rate for this character
|
||||||
|
$successRate = $crime->calculateSuccessRate($character);
|
||||||
|
|
||||||
|
return view('game.crimes.show', compact('character', 'crime', 'onCooldown', 'cooldownRemaining', 'successRate'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Game;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use App\Models\Notification;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the game dashboard
|
||||||
|
*/
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recent activity logs
|
||||||
|
$recentActivity = ActivityLog::where('character_id', $character->id)
|
||||||
|
->latest()
|
||||||
|
->take(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Get unread notifications
|
||||||
|
$unreadNotifications = $request->user()->notifications()
|
||||||
|
->whereNull('read_at')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('game.dashboard', compact('character', 'recentActivity', 'unreadNotifications'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Game;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Inventory;
|
||||||
|
use App\Models\Item;
|
||||||
|
use App\Services\Game\EconomyService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class InventoryController extends Controller
|
||||||
|
{
|
||||||
|
protected EconomyService $economyService;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->economyService = new EconomyService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the character's inventory
|
||||||
|
*/
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventory = $character->inventory()->with('item')->get();
|
||||||
|
|
||||||
|
return view('game.inventory.index', compact('character', 'inventory'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equip an item
|
||||||
|
*/
|
||||||
|
public function equip(Request $request, Inventory $inventory): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character || $inventory->character_id !== $character->id) {
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('error', 'Unauthorized action.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inventory->equip()) {
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('success', 'Item equipped successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('error', 'Cannot equip this item.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unequip an item
|
||||||
|
*/
|
||||||
|
public function unequip(Request $request, Inventory $inventory): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character || $inventory->character_id !== $character->id) {
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('error', 'Unauthorized action.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventory->unequip();
|
||||||
|
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('success', 'Item unequipped successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use an item
|
||||||
|
*/
|
||||||
|
public function use(Request $request, Inventory $inventory): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character || $inventory->character_id !== $character->id) {
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('error', 'Unauthorized action.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inventory->use($character)) {
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('success', 'Item used successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('error', 'Cannot use this item.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop an item
|
||||||
|
*/
|
||||||
|
public function drop(Request $request, Inventory $inventory): RedirectResponse
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character || $inventory->character_id !== $character->id) {
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('error', 'Unauthorized action.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'quantity' => ['required', 'integer', 'min:1', 'max:'.$inventory->quantity],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($inventory->remove($validated['quantity'])) {
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('success', 'Item dropped successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('game.inventory.index')
|
||||||
|
->with('error', 'Failed to drop item.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\ProfileUpdateRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Redirect;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the user's profile form.
|
||||||
|
*/
|
||||||
|
public function edit(Request $request): View
|
||||||
|
{
|
||||||
|
return view('profile.edit', [
|
||||||
|
'user' => $request->user(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile information.
|
||||||
|
*/
|
||||||
|
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->user()->fill($request->validated());
|
||||||
|
|
||||||
|
if ($request->user()->isDirty('email')) {
|
||||||
|
$request->user()->email_verified_at = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->save();
|
||||||
|
|
||||||
|
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the user's account.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validateWithBag('userDeletion', [
|
||||||
|
'password' => ['required', 'current_password'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return Redirect::to('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware\Game;
|
||||||
|
|
||||||
|
use App\Services\Game\EnergyService;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CheckEnergy
|
||||||
|
{
|
||||||
|
protected EnergyService $energyService;
|
||||||
|
|
||||||
|
public function __construct(EnergyService $energyService)
|
||||||
|
{
|
||||||
|
$this->energyService = $energyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, string $action = 'default'): Response
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
// Check if character can act
|
||||||
|
if (!$character->canAct()) {
|
||||||
|
return redirect()->route('game.dashboard')
|
||||||
|
->with('error', 'You cannot perform actions right now.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get energy cost for this action type
|
||||||
|
$cost = $this->energyService->getCostFor($action);
|
||||||
|
|
||||||
|
// Check if character has enough energy
|
||||||
|
if (!$character->hasEnergy($cost)) {
|
||||||
|
return redirect()->route('game.dashboard')
|
||||||
|
->with('error', "You don't have enough energy ({$cost} required) for this action.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware\Game;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CheckInHospital
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if ($character->isInHospital() || $character->isDead()) {
|
||||||
|
$releaseTime = $character->in_hospital_until ? $character->in_hospital_until->diffForHumans() : 'unknown';
|
||||||
|
$reviveCost = config('game.health.hospital_cost', 1000);
|
||||||
|
|
||||||
|
return redirect()->route('game.hospital')
|
||||||
|
->with('error', "You are in hospital and cannot perform actions. You will be released {$releaseTime} or can revive for \${$reviveCost}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware\Game;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CheckInJail
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if ($character->isInJail()) {
|
||||||
|
$releaseTime = $character->in_jail_until ? $character->in_jail_until->diffForHumans() : 'unknown';
|
||||||
|
|
||||||
|
return redirect()->route('game.jail')
|
||||||
|
->with('error', "You are in jail and cannot perform actions. You will be released {$releaseTime}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware\Game;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class VerifyCharacter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
// Check if user is authenticated
|
||||||
|
if (!$request->user()) {
|
||||||
|
return redirect()->route('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has a character
|
||||||
|
$character = $request->user()->character;
|
||||||
|
|
||||||
|
if (!$character) {
|
||||||
|
// No character exists, redirect to character creation
|
||||||
|
return redirect()->route('game.character.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character exists, proceed with request
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class LoginRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => ['required', 'string', 'email'],
|
||||||
|
'password' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate the request's credentials.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function authenticate(): void
|
||||||
|
{
|
||||||
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
|
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||||
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => trans('auth.failed'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RateLimiter::clear($this->throttleKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the login request is not rate limited.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function ensureIsNotRateLimited(): void
|
||||||
|
{
|
||||||
|
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new Lockout($this));
|
||||||
|
|
||||||
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => trans('auth.throttle', [
|
||||||
|
'seconds' => $seconds,
|
||||||
|
'minutes' => ceil($seconds / 60),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limiting throttle key for the request.
|
||||||
|
*/
|
||||||
|
public function throttleKey(): string
|
||||||
|
{
|
||||||
|
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ProfileUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'lowercase',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class)->ignore($this->user()->id),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $character_id
|
||||||
|
* @property string $action
|
||||||
|
* @property string|null $description
|
||||||
|
* @property string|null $ip_address
|
||||||
|
* @property string|null $user_agent
|
||||||
|
* @property \Illuminate\Support\Carbon $created_at
|
||||||
|
*/
|
||||||
|
class ActivityLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'character_id',
|
||||||
|
'action',
|
||||||
|
'description',
|
||||||
|
'ip_address',
|
||||||
|
'user_agent',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the character associated with this activity log.
|
||||||
|
*/
|
||||||
|
public function character(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Character::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new activity log entry
|
||||||
|
*/
|
||||||
|
public static function log(Character $character, string $action, string $description = null, string $ipAddress = null, string $userAgent = null): self
|
||||||
|
{
|
||||||
|
return self::create([
|
||||||
|
'character_id' => $character->id,
|
||||||
|
'action' => $action,
|
||||||
|
'description' => $description,
|
||||||
|
'ip_address' => $ipAddress,
|
||||||
|
'user_agent' => $userAgent,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a crime attempt
|
||||||
|
*/
|
||||||
|
public static function logCrimeAttempt(Character $character, Crime $crime, bool $success, int $reward = 0): self
|
||||||
|
{
|
||||||
|
$result = $success ? 'success' : 'failure';
|
||||||
|
$description = "Attempted crime: {$crime->name} - {$result}";
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$description .= " (Reward: \${$reward})";
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::log($character, 'crime_attempt', $description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log money transaction
|
||||||
|
*/
|
||||||
|
public static function logMoneyTransaction(Character $character, string $type, int $amount, string $reason): self
|
||||||
|
{
|
||||||
|
$action = match($type) {
|
||||||
|
'earned' => 'money_earned',
|
||||||
|
'spent' => 'money_spent',
|
||||||
|
'deposited' => 'money_deposited',
|
||||||
|
'withdrew' => 'money_withdrew',
|
||||||
|
default => 'money_transaction',
|
||||||
|
};
|
||||||
|
|
||||||
|
$description = "\${$amount} {$type} - {$reason}";
|
||||||
|
|
||||||
|
return self::log($character, $action, $description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log level up
|
||||||
|
*/
|
||||||
|
public static function logLevelUp(Character $character, int $newLevel): self
|
||||||
|
{
|
||||||
|
return self::log(
|
||||||
|
$character,
|
||||||
|
'level_up',
|
||||||
|
"Reached level {$newLevel}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log character creation
|
||||||
|
*/
|
||||||
|
public static function logCharacterCreated(Character $character): self
|
||||||
|
{
|
||||||
|
return self::log(
|
||||||
|
$character,
|
||||||
|
'character_created',
|
||||||
|
"New character created: {$character->name}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\CharacterStatus;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $gender
|
||||||
|
* @property int $level
|
||||||
|
* @property int $experience
|
||||||
|
* @property int $money
|
||||||
|
* @property int $bank
|
||||||
|
* @property int $energy
|
||||||
|
* @property int $max_energy
|
||||||
|
* @property int $health
|
||||||
|
* @property int $max_health
|
||||||
|
* @property int $strength
|
||||||
|
* @property int $defense
|
||||||
|
* @property int $speed
|
||||||
|
* @property int $dexterity
|
||||||
|
* @property int $location_id
|
||||||
|
* @property int|null $faction_id
|
||||||
|
* @property \Illuminate\Support\Carbon|null $in_hospital_until
|
||||||
|
* @property \Illuminate\Support\Carbon|null $in_jail_until
|
||||||
|
* @property \Illuminate\Support\Carbon $last_action_at
|
||||||
|
* @property CharacterStatus $status
|
||||||
|
* @property \Illuminate\Support\Carbon $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class Character extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'name',
|
||||||
|
'gender',
|
||||||
|
'level',
|
||||||
|
'experience',
|
||||||
|
'money',
|
||||||
|
'bank',
|
||||||
|
'energy',
|
||||||
|
'max_energy',
|
||||||
|
'health',
|
||||||
|
'max_health',
|
||||||
|
'strength',
|
||||||
|
'defense',
|
||||||
|
'speed',
|
||||||
|
'dexterity',
|
||||||
|
'location_id',
|
||||||
|
'faction_id',
|
||||||
|
'in_hospital_until',
|
||||||
|
'in_jail_until',
|
||||||
|
'last_action_at',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'in_hospital_until' => 'datetime',
|
||||||
|
'in_jail_until' => 'datetime',
|
||||||
|
'last_action_at' => 'datetime',
|
||||||
|
'status' => CharacterStatus::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that owns the character.
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the location where the character is.
|
||||||
|
*/
|
||||||
|
public function location(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Location::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the faction the character belongs to.
|
||||||
|
* Phase 2: Faction System
|
||||||
|
*/
|
||||||
|
// public function faction(): BelongsTo
|
||||||
|
// {
|
||||||
|
// return $this->belongsTo(Faction::class)->withDefault();
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the inventory items for the character.
|
||||||
|
*/
|
||||||
|
public function inventory(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Inventory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the crime attempts for the character.
|
||||||
|
*/
|
||||||
|
public function crimeAttempts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CrimeAttempt::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the activity logs for the character.
|
||||||
|
*/
|
||||||
|
public function activityLogs(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ActivityLog::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character can perform actions
|
||||||
|
*/
|
||||||
|
public function canAct(): bool
|
||||||
|
{
|
||||||
|
return $this->status === CharacterStatus::ACTIVE && $this->health > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is in hospital
|
||||||
|
*/
|
||||||
|
public function isInHospital(): bool
|
||||||
|
{
|
||||||
|
return $this->status === CharacterStatus::IN_HOSPITAL
|
||||||
|
|| ($this->in_hospital_until && $this->in_hospital_until->isFuture());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is in jail
|
||||||
|
*/
|
||||||
|
public function isInJail(): bool
|
||||||
|
{
|
||||||
|
return $this->status === CharacterStatus::IN_JAIL
|
||||||
|
|| ($this->in_jail_until && $this->in_jail_until->isFuture());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is alive
|
||||||
|
*/
|
||||||
|
public function isAlive(): bool
|
||||||
|
{
|
||||||
|
return $this->health > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get energy percentage
|
||||||
|
*/
|
||||||
|
public function getEnergyPercentageAttribute(): float
|
||||||
|
{
|
||||||
|
if ($this->max_energy <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return min(100, ($this->energy / $this->max_energy) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get health percentage
|
||||||
|
*/
|
||||||
|
public function getHealthPercentageAttribute(): float
|
||||||
|
{
|
||||||
|
if ($this->max_health <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return min(100, ($this->health / $this->max_health) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total attack power
|
||||||
|
*/
|
||||||
|
public function getAttackPowerAttribute(): int
|
||||||
|
{
|
||||||
|
$base = $this->strength * config('game.skills.strength.attack_bonus', 0.5);
|
||||||
|
$weapon = $this->inventory()->where('equipped', true)->whereHas('item', function($q) {
|
||||||
|
$q->where('type', 'weapon');
|
||||||
|
})->first();
|
||||||
|
|
||||||
|
$weaponBonus = $weapon ? $weapon->item->attack_bonus : 0;
|
||||||
|
|
||||||
|
return (int) ($base + $weaponBonus + config('game.combat.base_damage', 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total defense
|
||||||
|
*/
|
||||||
|
public function getDefenseAttribute(): int
|
||||||
|
{
|
||||||
|
$base = $this->defense;
|
||||||
|
$armor = $this->inventory()->where('equipped', true)->whereHas('item', function($q) {
|
||||||
|
$q->where('type', 'armor');
|
||||||
|
})->first();
|
||||||
|
|
||||||
|
$armorBonus = $armor ? $armor->item->defense_bonus : 0;
|
||||||
|
|
||||||
|
return $base + $armorBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get experience needed for next level
|
||||||
|
*/
|
||||||
|
public function getExperienceForNextLevelAttribute(): int
|
||||||
|
{
|
||||||
|
$base = config('game.leveling.exp_per_level', 1000);
|
||||||
|
$multiplier = config('game.leveling.exp_multiplier', 1.20);
|
||||||
|
|
||||||
|
if (config('game.leveling.exp_curve') === 'exponential') {
|
||||||
|
return (int) ($base * pow($multiplier, $this->level - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character has enough energy for an action
|
||||||
|
*/
|
||||||
|
public function hasEnergy(int $amount): bool
|
||||||
|
{
|
||||||
|
return $this->energy >= $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume energy
|
||||||
|
*/
|
||||||
|
public function consumeEnergy(int $amount): bool
|
||||||
|
{
|
||||||
|
if (!$this->hasEnergy($amount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->energy -= $amount;
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add energy (up to max)
|
||||||
|
*/
|
||||||
|
public function addEnergy(int $amount): void
|
||||||
|
{
|
||||||
|
$this->energy = min($this->max_energy, $this->energy + $amount);
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set energy to max
|
||||||
|
*/
|
||||||
|
public function restoreEnergy(): void
|
||||||
|
{
|
||||||
|
$this->energy = $this->max_energy;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take damage
|
||||||
|
*/
|
||||||
|
public function takeDamage(int $amount): int
|
||||||
|
{
|
||||||
|
$defenseReduction = $this->defense * config('game.combat.defense_reduction', 0.5);
|
||||||
|
$actualDamage = max(1, $amount - $defenseReduction);
|
||||||
|
|
||||||
|
$this->health -= $actualDamage;
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
if ($this->health <= 0) {
|
||||||
|
$this->health = 0;
|
||||||
|
$this->status = CharacterStatus::DEAD;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actualDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heal health
|
||||||
|
*/
|
||||||
|
public function heal(int $amount): void
|
||||||
|
{
|
||||||
|
$this->health = min($this->max_health, $this->health + $amount);
|
||||||
|
|
||||||
|
if ($this->status === CharacterStatus::DEAD && $this->health > 0) {
|
||||||
|
$this->status = CharacterStatus::ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add experience
|
||||||
|
*/
|
||||||
|
public function addExperience(int $amount): void
|
||||||
|
{
|
||||||
|
$this->experience += $amount;
|
||||||
|
|
||||||
|
while ($this->experience >= $this->experience_for_next_level && $this->level < config('game.leveling.max_level', 100)) {
|
||||||
|
$this->levelUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Level up the character
|
||||||
|
*/
|
||||||
|
public function levelUp(): void
|
||||||
|
{
|
||||||
|
$this->level++;
|
||||||
|
$this->experience -= $this->experience_for_next_level;
|
||||||
|
$this->max_health += 5;
|
||||||
|
$this->max_energy += 5;
|
||||||
|
$this->health = $this->max_health;
|
||||||
|
$this->energy = $this->max_energy;
|
||||||
|
$this->money += 1000;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add money
|
||||||
|
*/
|
||||||
|
public function addMoney(int $amount): void
|
||||||
|
{
|
||||||
|
$this->money += $amount;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove money
|
||||||
|
*/
|
||||||
|
public function removeMoney(int $amount): bool
|
||||||
|
{
|
||||||
|
if ($this->money < $amount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->money -= $amount;
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character has enough money
|
||||||
|
*/
|
||||||
|
public function hasMoney(int $amount): bool
|
||||||
|
{
|
||||||
|
return $this->money >= $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposit money to bank
|
||||||
|
*/
|
||||||
|
public function depositToBank(int $amount): bool
|
||||||
|
{
|
||||||
|
if (!$this->hasMoney($amount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxBank = config('game.economy.max_bank', 10000000);
|
||||||
|
|
||||||
|
if ($this->bank + $amount > $maxBank) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->money -= $amount;
|
||||||
|
$this->bank += $amount;
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw money from bank
|
||||||
|
*/
|
||||||
|
public function withdrawFromBank(int $amount): bool
|
||||||
|
{
|
||||||
|
if ($this->bank < $amount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bank -= $amount;
|
||||||
|
$this->money += $amount;
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply daily bank interest
|
||||||
|
*/
|
||||||
|
public function applyBankInterest(): void
|
||||||
|
{
|
||||||
|
$interestRate = config('game.economy.bank_interest', 0.05);
|
||||||
|
$interest = (int) ($this->bank * $interestRate);
|
||||||
|
|
||||||
|
$maxBank = config('game.economy.max_bank', 10000000);
|
||||||
|
|
||||||
|
if ($this->bank + $interest > $maxBank) {
|
||||||
|
$interest = $maxBank - $this->bank;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bank += $interest;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the character's full name with level
|
||||||
|
*/
|
||||||
|
public function getFullNameAttribute(): string
|
||||||
|
{
|
||||||
|
return "{$this->name} (Lv. {$this->level})";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\CrimeDifficulty;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $slug
|
||||||
|
* @property string|null $description
|
||||||
|
* @property CrimeDifficulty $difficulty
|
||||||
|
* @property float $success_rate
|
||||||
|
* @property int $reward_min
|
||||||
|
* @property int $reward_max
|
||||||
|
* @property int $experience
|
||||||
|
* @property int $energy_cost
|
||||||
|
* @property int $cooldown
|
||||||
|
* @property int|null $requires_level
|
||||||
|
* @property int|null $location_id
|
||||||
|
* @property \Illuminate\Support\Carbon $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class Crime extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'difficulty',
|
||||||
|
'success_rate',
|
||||||
|
'reward_min',
|
||||||
|
'reward_max',
|
||||||
|
'experience',
|
||||||
|
'energy_cost',
|
||||||
|
'cooldown',
|
||||||
|
'requires_level',
|
||||||
|
'location_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'difficulty' => CrimeDifficulty::class,
|
||||||
|
'success_rate' => 'float',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the location where this crime can be committed.
|
||||||
|
*/
|
||||||
|
public function location(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Location::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the crime attempts for this crime.
|
||||||
|
*/
|
||||||
|
public function attempts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CrimeAttempt::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a character can attempt this crime
|
||||||
|
*/
|
||||||
|
public function canBeAttemptedBy(Character $character): bool
|
||||||
|
{
|
||||||
|
// Check level requirement
|
||||||
|
if ($this->requires_level && $character->level < $this->requires_level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check energy
|
||||||
|
if ($character->energy < $this->energy_cost) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check location (if crime is location-specific)
|
||||||
|
if ($this->location_id && $character->location_id !== $this->location_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the success rate for a specific character
|
||||||
|
*/
|
||||||
|
public function calculateSuccessRate(Character $character): float
|
||||||
|
{
|
||||||
|
$baseRate = $this->success_rate;
|
||||||
|
$difficultyModifier = $this->difficulty->successModifier();
|
||||||
|
$levelModifier = $character->level * 0.01; // +1% per level
|
||||||
|
|
||||||
|
$rate = $baseRate + $difficultyModifier + $levelModifier;
|
||||||
|
|
||||||
|
// Clamp between 10% and 95%
|
||||||
|
return max(0.10, min(0.95, $rate));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the reward for a successful crime
|
||||||
|
*/
|
||||||
|
public function calculateReward(Character $character): int
|
||||||
|
{
|
||||||
|
$baseReward = random_int($this->reward_min, $this->reward_max);
|
||||||
|
$difficultyMultiplier = $this->difficulty->rewardMultiplier();
|
||||||
|
$levelMultiplier = 1 + ($character->level * 0.01); // +1% per level
|
||||||
|
|
||||||
|
return (int) ($baseReward * $difficultyMultiplier * $levelMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the experience reward
|
||||||
|
*/
|
||||||
|
public function calculateExperience(Character $character): int
|
||||||
|
{
|
||||||
|
$baseExp = $this->experience;
|
||||||
|
$levelMultiplier = 1 + ($character->level * 0.01);
|
||||||
|
|
||||||
|
return (int) ($baseExp * $levelMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for the difficulty
|
||||||
|
*/
|
||||||
|
public function getDifficultyLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->difficulty->label();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color class for the difficulty badge
|
||||||
|
*/
|
||||||
|
public function getDifficultyColorAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->difficulty->color();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $character_id
|
||||||
|
* @property int $crime_id
|
||||||
|
* @property bool $success
|
||||||
|
* @property int $reward
|
||||||
|
* @property int $experience_gained
|
||||||
|
* @property \Illuminate\Support\Carbon $attempted_at
|
||||||
|
*/
|
||||||
|
class CrimeAttempt extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'character_id',
|
||||||
|
'crime_id',
|
||||||
|
'success',
|
||||||
|
'reward',
|
||||||
|
'experience_gained',
|
||||||
|
'attempted_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'success' => 'boolean',
|
||||||
|
'attempted_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the character who attempted this crime.
|
||||||
|
*/
|
||||||
|
public function character(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Character::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the crime that was attempted.
|
||||||
|
*/
|
||||||
|
public function crime(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Crime::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the crime was successful
|
||||||
|
*/
|
||||||
|
public function isSuccessful(): bool
|
||||||
|
{
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the crime failed
|
||||||
|
*/
|
||||||
|
public function isFailed(): bool
|
||||||
|
{
|
||||||
|
return !$this->success;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $character_id
|
||||||
|
* @property int $item_id
|
||||||
|
* @property int $quantity
|
||||||
|
* @property bool $equipped
|
||||||
|
* @property \Illuminate\Support\Carbon $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class Inventory extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'character_id',
|
||||||
|
'item_id',
|
||||||
|
'quantity',
|
||||||
|
'equipped',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'equipped' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the character that owns this inventory item.
|
||||||
|
*/
|
||||||
|
public function character(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Character::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item.
|
||||||
|
*/
|
||||||
|
public function item(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Item::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add items to inventory
|
||||||
|
*/
|
||||||
|
public static function addItem(Character $character, Item $item, int $quantity = 1): self
|
||||||
|
{
|
||||||
|
$inventory = self::firstOrNew([
|
||||||
|
'character_id' => $character->id,
|
||||||
|
'item_id' => $item->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$inventory->quantity += $quantity;
|
||||||
|
$inventory->save();
|
||||||
|
|
||||||
|
return $inventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove items from inventory
|
||||||
|
*/
|
||||||
|
public function remove(int $quantity = 1): bool
|
||||||
|
{
|
||||||
|
if ($this->quantity < $quantity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->quantity -= $quantity;
|
||||||
|
|
||||||
|
if ($this->quantity <= 0) {
|
||||||
|
$this->delete();
|
||||||
|
} else {
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equip the item
|
||||||
|
*/
|
||||||
|
public function equip(): bool
|
||||||
|
{
|
||||||
|
if (!$this->item->canEquip()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unequip any other item of the same type
|
||||||
|
$this->character->inventory()
|
||||||
|
->where('equipped', true)
|
||||||
|
->whereHas('item', function($q) {
|
||||||
|
$q->where('type', $this->item->type);
|
||||||
|
})
|
||||||
|
->update(['equipped' => false]);
|
||||||
|
|
||||||
|
$this->equipped = true;
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unequip the item
|
||||||
|
*/
|
||||||
|
public function unequip(): void
|
||||||
|
{
|
||||||
|
$this->equipped = false;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the item (consume it)
|
||||||
|
*/
|
||||||
|
public function use(Character $character): bool
|
||||||
|
{
|
||||||
|
if (!$this->item->canUse()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->item->canBeUsedBy($character)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply item effects
|
||||||
|
$this->applyEffects($character);
|
||||||
|
|
||||||
|
// Remove one from inventory
|
||||||
|
return $this->remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the item's effects to the character
|
||||||
|
*/
|
||||||
|
protected function applyEffects(Character $character): void
|
||||||
|
{
|
||||||
|
$item = $this->item;
|
||||||
|
|
||||||
|
if ($item->health_bonus > 0) {
|
||||||
|
$character->heal($item->health_bonus);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($item->energy_bonus > 0) {
|
||||||
|
$character->addEnergy($item->energy_bonus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add money if it's a currency item
|
||||||
|
if ($item->type === ItemType::CURRENCY && $item->value > 0) {
|
||||||
|
$character->addMoney($item->value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total value of this inventory item
|
||||||
|
*/
|
||||||
|
public function getTotalValueAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->quantity * $this->item->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\ItemType;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $slug
|
||||||
|
* @property string|null $description
|
||||||
|
* @property ItemType $type
|
||||||
|
* @property int $value
|
||||||
|
* @property int|null $buy_price
|
||||||
|
* @property int|null $sell_price
|
||||||
|
* @property int|null $requires_level
|
||||||
|
* @property int $attack_bonus
|
||||||
|
* @property int $defense_bonus
|
||||||
|
* @property int $health_bonus
|
||||||
|
* @property int $energy_bonus
|
||||||
|
* @property bool $is_tradeable
|
||||||
|
* @property bool $is_usable
|
||||||
|
* @property \Illuminate\Support\Carbon $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class Item extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'type',
|
||||||
|
'value',
|
||||||
|
'buy_price',
|
||||||
|
'sell_price',
|
||||||
|
'requires_level',
|
||||||
|
'attack_bonus',
|
||||||
|
'defense_bonus',
|
||||||
|
'health_bonus',
|
||||||
|
'energy_bonus',
|
||||||
|
'is_tradeable',
|
||||||
|
'is_usable',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'type' => ItemType::class,
|
||||||
|
'is_tradeable' => 'boolean',
|
||||||
|
'is_usable' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the characters that have this item in their inventory.
|
||||||
|
*/
|
||||||
|
public function characters(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Character::class, 'inventories')
|
||||||
|
->withPivot('quantity', 'equipped')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the shops that sell this item.
|
||||||
|
*/
|
||||||
|
public function shops(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Shop::class, 'shop_items')
|
||||||
|
->withPivot('stock', 'price')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this item can be equipped
|
||||||
|
*/
|
||||||
|
public function canEquip(): bool
|
||||||
|
{
|
||||||
|
return $this->type->canEquip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this item can be used/consumed
|
||||||
|
*/
|
||||||
|
public function canUse(): bool
|
||||||
|
{
|
||||||
|
return $this->is_usable && $this->type->canUse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a character can use this item (level requirement)
|
||||||
|
*/
|
||||||
|
public function canBeUsedBy(Character $character): bool
|
||||||
|
{
|
||||||
|
if ($this->requires_level && $character->level < $this->requires_level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sell price or default to half of buy price
|
||||||
|
*/
|
||||||
|
public function getSellPriceAttribute(): int
|
||||||
|
{
|
||||||
|
if ($this->sell_price > 0) {
|
||||||
|
return $this->sell_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) ($this->buy_price * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a weapon
|
||||||
|
*/
|
||||||
|
public function isWeapon(): bool
|
||||||
|
{
|
||||||
|
return $this->type === ItemType::WEAPON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is armor
|
||||||
|
*/
|
||||||
|
public function isArmor(): bool
|
||||||
|
{
|
||||||
|
return $this->type === ItemType::ARMOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a consumable
|
||||||
|
*/
|
||||||
|
public function isConsumable(): bool
|
||||||
|
{
|
||||||
|
return $this->type === ItemType::CONSUMABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for the item type
|
||||||
|
*/
|
||||||
|
public function getTypeLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->type->label();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $slug
|
||||||
|
* @property string|null $description
|
||||||
|
* @property int $travel_cost
|
||||||
|
* @property int|null $requires_level
|
||||||
|
* @property int|null $faction_control
|
||||||
|
* @property \Illuminate\Support\Carbon $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class Location extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'travel_cost',
|
||||||
|
'requires_level',
|
||||||
|
'faction_control',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the characters at this location.
|
||||||
|
*/
|
||||||
|
public function characters(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Character::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the shops at this location.
|
||||||
|
*/
|
||||||
|
public function shops(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Shop::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the crimes available at this location.
|
||||||
|
*/
|
||||||
|
public function crimes(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Crime::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a character can travel to this location
|
||||||
|
*/
|
||||||
|
public function canTravel(Character $character): bool
|
||||||
|
{
|
||||||
|
// Check level requirement
|
||||||
|
if ($this->requires_level && $character->level < $this->requires_level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if character has enough money
|
||||||
|
if ($character->money < $this->travel_cost) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the travel cost from another location to this one
|
||||||
|
*/
|
||||||
|
public function getTravelCostFrom(Location $from): int
|
||||||
|
{
|
||||||
|
// For now, just use this location's travel cost
|
||||||
|
// In a more advanced system, we'd calculate based on distance
|
||||||
|
return $this->travel_cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property string $type
|
||||||
|
* @property array|null $data
|
||||||
|
* @property \Illuminate\Support\Carbon|null $read_at
|
||||||
|
* @property \Illuminate\Support\Carbon $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class Notification extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'type',
|
||||||
|
'data',
|
||||||
|
'read_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'data' => 'array',
|
||||||
|
'read_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who received this notification.
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the notification has been read
|
||||||
|
*/
|
||||||
|
public function isRead(): bool
|
||||||
|
{
|
||||||
|
return $this->read_at !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the notification is unread
|
||||||
|
*/
|
||||||
|
public function isUnread(): bool
|
||||||
|
{
|
||||||
|
return $this->read_at === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the notification as read
|
||||||
|
*/
|
||||||
|
public function markAsRead(): void
|
||||||
|
{
|
||||||
|
$this->read_at = now();
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the notification as unread
|
||||||
|
*/
|
||||||
|
public function markAsUnread(): void
|
||||||
|
{
|
||||||
|
$this->read_at = null;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification
|
||||||
|
*/
|
||||||
|
public static function createForUser(User $user, string $type, array $data = []): self
|
||||||
|
{
|
||||||
|
return self::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'type' => $type,
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a system notification
|
||||||
|
*/
|
||||||
|
public static function sendSystem(User $user, string $message): self
|
||||||
|
{
|
||||||
|
return self::createForUser($user, 'system', [
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a crime success notification
|
||||||
|
*/
|
||||||
|
public static function sendCrimeSuccess(User $user, Crime $crime, int $reward): self
|
||||||
|
{
|
||||||
|
return self::createForUser($user, 'crime_success', [
|
||||||
|
'crime_name' => $crime->name,
|
||||||
|
'reward' => $reward,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a crime failure notification
|
||||||
|
*/
|
||||||
|
public static function sendCrimeFailure(User $user, Crime $crime): self
|
||||||
|
{
|
||||||
|
return self::createForUser($user, 'crime_failure', [
|
||||||
|
'crime_name' => $crime->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification message
|
||||||
|
*/
|
||||||
|
public function getMessageAttribute(): string
|
||||||
|
{
|
||||||
|
return match($this->type) {
|
||||||
|
'system' => $this->data['message'] ?? 'System notification',
|
||||||
|
'crime_success' => "Crime successful: {$this->data['crime_name']}! You earned \${$this->data['reward']}",
|
||||||
|
'crime_failure' => "Crime failed: {$this->data['crime_name']}.",
|
||||||
|
default => $this->type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification icon
|
||||||
|
*/
|
||||||
|
public function getIconAttribute(): string
|
||||||
|
{
|
||||||
|
return match($this->type) {
|
||||||
|
'system' => 'information-circle',
|
||||||
|
'crime_success' => 'check-circle',
|
||||||
|
'crime_failure' => 'x-circle',
|
||||||
|
'money' => 'currency-dollar',
|
||||||
|
'level_up' => 'arrow-up-circle',
|
||||||
|
default => 'bell',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification color
|
||||||
|
*/
|
||||||
|
public function getColorAttribute(): string
|
||||||
|
{
|
||||||
|
return match($this->type) {
|
||||||
|
'system' => 'blue',
|
||||||
|
'crime_success' => 'green',
|
||||||
|
'crime_failure' => 'red',
|
||||||
|
'money' => 'yellow',
|
||||||
|
'level_up' => 'purple',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Database\Factories\UserFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
#[Fillable(['name', 'email', 'password'])]
|
||||||
|
#[Hidden(['password', 'remember_token'])]
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the character for this user.
|
||||||
|
*/
|
||||||
|
public function character(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(Character::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all characters for this user (for future multi-character support).
|
||||||
|
*/
|
||||||
|
public function characters(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Character::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notifications for this user.
|
||||||
|
*/
|
||||||
|
public function notifications(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Notification::class)->orderBy('created_at', 'desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class GameServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
// Register game services as singletons
|
||||||
|
$this->app->singleton(\App\Services\Game\EnergyService::class, function ($app) {
|
||||||
|
return new \App\Services\Game\EnergyService();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(\App\Services\Game\HealthService::class, function ($app) {
|
||||||
|
return new \App\Services\Game\HealthService();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(\App\Services\Game\EconomyService::class, function ($app) {
|
||||||
|
return new \App\Services\Game\EconomyService();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(\App\Services\Game\CrimeService::class, function ($app) {
|
||||||
|
return new \App\Services\Game\CrimeService();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(\App\Services\Game\NotificationService::class, function ($app) {
|
||||||
|
return new \App\Services\Game\NotificationService();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
// Boot logic if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class ScheduleServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->commands([
|
||||||
|
\App\Console\Commands\Game\ProcessEnergyRegen::class,
|
||||||
|
\App\Console\Commands\Game\ProcessHealthRegen::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Game;
|
||||||
|
|
||||||
|
use App\Models\Character;
|
||||||
|
use App\Models\Crime;
|
||||||
|
use App\Models\CrimeAttempt;
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use App\Enums\CharacterStatus;
|
||||||
|
|
||||||
|
class CrimeService
|
||||||
|
{
|
||||||
|
protected EnergyService $energyService;
|
||||||
|
protected EconomyService $economyService;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->energyService = new EnergyService();
|
||||||
|
$this->economyService = new EconomyService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt a crime
|
||||||
|
*/
|
||||||
|
public function attemptCrime(Character $character, Crime $crime): CrimeAttempt
|
||||||
|
{
|
||||||
|
// Check if character can attempt this crime
|
||||||
|
if (!$crime->canBeAttemptedBy($character)) {
|
||||||
|
throw new \Exception('Character cannot attempt this crime.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if character can act
|
||||||
|
if (!$character->canAct()) {
|
||||||
|
throw new \Exception('Character cannot perform actions right now.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume energy
|
||||||
|
if (!$this->energyService->consume($character, $crime->energy_cost, 'crime')) {
|
||||||
|
throw new \Exception('Not enough energy to attempt this crime.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate success rate
|
||||||
|
$successRate = $crime->calculateSuccessRate($character);
|
||||||
|
$isSuccess = random_int(0, 9999) / 10000 <= $successRate;
|
||||||
|
|
||||||
|
$reward = 0;
|
||||||
|
$experience = 0;
|
||||||
|
|
||||||
|
if ($isSuccess) {
|
||||||
|
// Successful crime
|
||||||
|
$reward = $crime->calculateReward($character);
|
||||||
|
$experience = $crime->calculateExperience($character);
|
||||||
|
|
||||||
|
$this->economyService->addMoney($character, $reward, "Crime: {$crime->name}");
|
||||||
|
$character->addExperience($experience);
|
||||||
|
|
||||||
|
// Log success
|
||||||
|
ActivityLog::logCrimeAttempt($character, $crime, true, $reward);
|
||||||
|
} else {
|
||||||
|
// Failed crime - take health penalty
|
||||||
|
$healthPenalty = (int) ($character->max_health * config('game.crime.failure_penalty', 0.10));
|
||||||
|
$character->takeDamage($healthPenalty);
|
||||||
|
|
||||||
|
// If dead, send to hospital
|
||||||
|
if ($character->isDead()) {
|
||||||
|
$character->sendToHospital();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log failure
|
||||||
|
ActivityLog::logCrimeAttempt($character, $crime, false, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create crime attempt record
|
||||||
|
$attempt = CrimeAttempt::create([
|
||||||
|
'character_id' => $character->id,
|
||||||
|
'crime_id' => $crime->id,
|
||||||
|
'success' => $isSuccess,
|
||||||
|
'reward' => $reward,
|
||||||
|
'experience_gained' => $experience,
|
||||||
|
'attempted_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $attempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available crimes for a character
|
||||||
|
*/
|
||||||
|
public function getAvailableCrimes(Character $character): \Illuminate\Database\Eloquent\Collection
|
||||||
|
{
|
||||||
|
return Crime::query()
|
||||||
|
->where(function($query) use ($character) {
|
||||||
|
$query->whereNull('requires_level')
|
||||||
|
->orWhere('requires_level', '<=', $character->level);
|
||||||
|
})
|
||||||
|
->where(function($query) use ($character) {
|
||||||
|
$query->whereNull('location_id')
|
||||||
|
->orWhere('location_id', $character->location_id);
|
||||||
|
})
|
||||||
|
->where('energy_cost', '<=', $character->energy)
|
||||||
|
->orderBy('difficulty')
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent crime attempts for a character
|
||||||
|
*/
|
||||||
|
public function getRecentAttempts(Character $character, int $limit = 10): \Illuminate\Database\Eloquent\Collection
|
||||||
|
{
|
||||||
|
return $character->crimeAttempts()
|
||||||
|
->with('crime')
|
||||||
|
->latest()
|
||||||
|
->take($limit)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a crime is on cooldown for a character
|
||||||
|
*/
|
||||||
|
public function isOnCooldown(Character $character, Crime $crime): bool
|
||||||
|
{
|
||||||
|
$lastAttempt = $character->crimeAttempts()
|
||||||
|
->where('crime_id', $crime->id)
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$lastAttempt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cooldown = $crime->cooldown;
|
||||||
|
$timeSinceAttempt = now()->diffInSeconds($lastAttempt->attempted_at);
|
||||||
|
|
||||||
|
return $timeSinceAttempt < $cooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remaining cooldown time for a crime
|
||||||
|
*/
|
||||||
|
public function getCooldownRemaining(Character $character, Crime $crime): int
|
||||||
|
{
|
||||||
|
if (!$this->isOnCooldown($character, $crime)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastAttempt = $character->crimeAttempts()
|
||||||
|
->where('crime_id', $crime->id)
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$cooldown = $crime->cooldown;
|
||||||
|
$timeSinceAttempt = now()->diffInSeconds($lastAttempt->attempted_at);
|
||||||
|
|
||||||
|
return max(0, $cooldown - $timeSinceAttempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new crime
|
||||||
|
*/
|
||||||
|
public function createCrime(array $data): Crime
|
||||||
|
{
|
||||||
|
return Crime::create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a crime
|
||||||
|
*/
|
||||||
|
public function updateCrime(Crime $crime, array $data): Crime
|
||||||
|
{
|
||||||
|
$crime->update($data);
|
||||||
|
return $crime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a crime
|
||||||
|
*/
|
||||||
|
public function deleteCrime(Crime $crime): bool
|
||||||
|
{
|
||||||
|
return $crime->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Game;
|
||||||
|
|
||||||
|
use App\Models\Character;
|
||||||
|
|
||||||
|
class EconomyService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transfer money between characters
|
||||||
|
*/
|
||||||
|
public function transfer(Character $from, Character $to, int $amount, string $reason = ''): bool
|
||||||
|
{
|
||||||
|
if (!$from->hasMoney($amount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($from->id === $to->id) {
|
||||||
|
return false; // Can't transfer to self
|
||||||
|
}
|
||||||
|
|
||||||
|
$from->removeMoney($amount);
|
||||||
|
$to->addMoney($amount);
|
||||||
|
|
||||||
|
// Log the transaction
|
||||||
|
ActivityLog::logMoneyTransaction($from, 'spent', $amount, "Transfer to {$to->name}: {$reason}");
|
||||||
|
ActivityLog::logMoneyTransaction($to, 'earned', $amount, "Transfer from {$from->name}: {$reason}");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add money to a character
|
||||||
|
*/
|
||||||
|
public function addMoney(Character $character, int $amount, string $reason = ''): void
|
||||||
|
{
|
||||||
|
$character->addMoney($amount);
|
||||||
|
|
||||||
|
if ($amount > 0 && $reason) {
|
||||||
|
ActivityLog::logMoneyTransaction($character, 'earned', $amount, $reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove money from a character
|
||||||
|
*/
|
||||||
|
public function removeMoney(Character $character, int $amount, string $reason = ''): bool
|
||||||
|
{
|
||||||
|
if (!$character->hasMoney($amount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$character->removeMoney($amount);
|
||||||
|
|
||||||
|
if ($amount > 0 && $reason) {
|
||||||
|
ActivityLog::logMoneyTransaction($character, 'spent', $amount, $reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposit money to bank
|
||||||
|
*/
|
||||||
|
public function depositToBank(Character $character, int $amount): bool
|
||||||
|
{
|
||||||
|
return $character->depositToBank($amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw money from bank
|
||||||
|
*/
|
||||||
|
public function withdrawFromBank(Character $character, int $amount): bool
|
||||||
|
{
|
||||||
|
return $character->withdrawFromBank($amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply daily interest to all characters
|
||||||
|
*/
|
||||||
|
public function applyDailyInterest(): void
|
||||||
|
{
|
||||||
|
$characters = Character::all();
|
||||||
|
|
||||||
|
foreach ($characters as $character) {
|
||||||
|
$character->applyBankInterest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the starting money for new characters
|
||||||
|
*/
|
||||||
|
public function getStartingMoney(): int
|
||||||
|
{
|
||||||
|
return config('game.economy.start_money', 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the starting bank balance
|
||||||
|
*/
|
||||||
|
public function getStartingBank(): int
|
||||||
|
{
|
||||||
|
return config('game.economy.start_bank', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum bank balance
|
||||||
|
*/
|
||||||
|
public function getMaxBank(): int
|
||||||
|
{
|
||||||
|
return config('game.economy.max_bank', 10000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the bank interest rate
|
||||||
|
*/
|
||||||
|
public function getBankInterestRate(): float
|
||||||
|
{
|
||||||
|
return config('game.economy.bank_interest', 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Game;
|
||||||
|
|
||||||
|
use App\Models\Character;
|
||||||
|
|
||||||
|
class EnergyService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Consume energy for an action
|
||||||
|
*/
|
||||||
|
public function consume(Character $character, int $amount, string $action): bool
|
||||||
|
{
|
||||||
|
if (!$character->canAct()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$character->hasEnergy($amount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $character->consumeEnergy($amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate energy for a character
|
||||||
|
*/
|
||||||
|
public function regenerate(Character $character): void
|
||||||
|
{
|
||||||
|
$regenRate = config('game.energy.regen_rate', 5);
|
||||||
|
$maxEnergy = config('game.energy.max', 100);
|
||||||
|
|
||||||
|
// Ensure character has max_energy set
|
||||||
|
if ($character->max_energy <= 0) {
|
||||||
|
$character->max_energy = $maxEnergy;
|
||||||
|
}
|
||||||
|
|
||||||
|
$character->addEnergy($regenRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the energy regen rate
|
||||||
|
*/
|
||||||
|
public function getRegenRate(): int
|
||||||
|
{
|
||||||
|
return config('game.energy.regen_rate', 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the energy regen interval in seconds
|
||||||
|
*/
|
||||||
|
public function getRegenInterval(): int
|
||||||
|
{
|
||||||
|
return config('game.energy.regen_interval', 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character has enough energy for an action type
|
||||||
|
*/
|
||||||
|
public function hasEnoughFor(Character $character, string $actionType): bool
|
||||||
|
{
|
||||||
|
$cost = config("game.energy.action_costs.{$actionType}", config('game.energy.action_costs.default', 10));
|
||||||
|
|
||||||
|
return $character->hasEnergy($cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the energy cost for an action type
|
||||||
|
*/
|
||||||
|
public function getCostFor(string $actionType): int
|
||||||
|
{
|
||||||
|
return config("game.energy.action_costs.{$actionType}", config('game.energy.action_costs.default', 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore all energy for a character
|
||||||
|
*/
|
||||||
|
public function restoreAll(Character $character): void
|
||||||
|
{
|
||||||
|
$maxEnergy = config('game.energy.max', 100);
|
||||||
|
|
||||||
|
if ($character->max_energy <= 0) {
|
||||||
|
$character->max_energy = $maxEnergy;
|
||||||
|
}
|
||||||
|
|
||||||
|
$character->energy = $character->max_energy;
|
||||||
|
$character->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Game;
|
||||||
|
|
||||||
|
use App\Models\Character;
|
||||||
|
|
||||||
|
class HealthService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Regenerate health for a character
|
||||||
|
*/
|
||||||
|
public function regenerate(Character $character): void
|
||||||
|
{
|
||||||
|
if ($character->isDead()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$regenRate = config('game.health.regen_rate', 2);
|
||||||
|
$character->heal($regenRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the health regen rate
|
||||||
|
*/
|
||||||
|
public function getRegenRate(): int
|
||||||
|
{
|
||||||
|
return config('game.health.regen_rate', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the health regen interval in seconds
|
||||||
|
*/
|
||||||
|
public function getRegenInterval(): int
|
||||||
|
{
|
||||||
|
return config('game.health.regen_interval', 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heal a character
|
||||||
|
*/
|
||||||
|
public function heal(Character $character, int $amount): void
|
||||||
|
{
|
||||||
|
$character->heal($amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deal damage to a character
|
||||||
|
*/
|
||||||
|
public function damage(Character $character, int $amount): int
|
||||||
|
{
|
||||||
|
return $character->takeDamage($amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revive a character from hospital
|
||||||
|
*/
|
||||||
|
public function revive(Character $character): bool
|
||||||
|
{
|
||||||
|
if (!$character->isDead() && !$character->isInHospital()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cost = config('game.health.hospital_cost', 1000);
|
||||||
|
|
||||||
|
if ($character->money < $cost) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$character->money -= $cost;
|
||||||
|
$character->health = $character->max_health;
|
||||||
|
$character->status = 'active';
|
||||||
|
$character->in_hospital_until = null;
|
||||||
|
$character->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send character to hospital
|
||||||
|
*/
|
||||||
|
public function sendToHospital(Character $character): void
|
||||||
|
{
|
||||||
|
$character->health = 0;
|
||||||
|
$character->status = 'in_hospital';
|
||||||
|
$character->in_hospital_until = now()->addSeconds(config('game.health.regen_interval', 600) * 5); // 5 intervals
|
||||||
|
$character->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore all health for a character
|
||||||
|
*/
|
||||||
|
public function restoreAll(Character $character): void
|
||||||
|
{
|
||||||
|
$character->health = $character->max_health;
|
||||||
|
|
||||||
|
if ($character->status !== 'active') {
|
||||||
|
$character->status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
$character->in_hospital_until = null;
|
||||||
|
$character->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character needs hospital
|
||||||
|
*/
|
||||||
|
public function needsHospital(Character $character): bool
|
||||||
|
{
|
||||||
|
return $character->isDead() || $character->isInHospital();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Game;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Notification;
|
||||||
|
use App\Models\Character;
|
||||||
|
use App\Models\Crime;
|
||||||
|
|
||||||
|
class NotificationService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send a notification to a user
|
||||||
|
*/
|
||||||
|
public function send(User $user, string $type, array $data = [], bool $saveActivity = true): Notification
|
||||||
|
{
|
||||||
|
$notification = Notification::createForUser($user, $type, $data);
|
||||||
|
|
||||||
|
return $notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a system notification
|
||||||
|
*/
|
||||||
|
public function sendSystem(User $user, string $message): Notification
|
||||||
|
{
|
||||||
|
return Notification::sendSystem($user, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a crime success notification
|
||||||
|
*/
|
||||||
|
public function sendCrimeSuccess(User $user, Crime $crime, int $reward): Notification
|
||||||
|
{
|
||||||
|
return Notification::sendCrimeSuccess($user, $crime, $reward);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a crime failure notification
|
||||||
|
*/
|
||||||
|
public function sendCrimeFailure(User $user, Crime $crime): Notification
|
||||||
|
{
|
||||||
|
return Notification::sendCrimeFailure($user, $crime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a level up notification
|
||||||
|
*/
|
||||||
|
public function sendLevelUp(User $user, Character $character, int $newLevel): Notification
|
||||||
|
{
|
||||||
|
return $this->send($user, 'level_up', [
|
||||||
|
'character_name' => $character->name,
|
||||||
|
'level' => $newLevel,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a money earned notification
|
||||||
|
*/
|
||||||
|
public function sendMoneyEarned(User $user, int $amount, string $reason): Notification
|
||||||
|
{
|
||||||
|
return $this->send($user, 'money_earned', [
|
||||||
|
'amount' => $amount,
|
||||||
|
'reason' => $reason,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a money spent notification
|
||||||
|
*/
|
||||||
|
public function sendMoneySpent(User $user, int $amount, string $reason): Notification
|
||||||
|
{
|
||||||
|
return $this->send($user, 'money_spent', [
|
||||||
|
'amount' => $amount,
|
||||||
|
'reason' => $reason,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an energy low notification
|
||||||
|
*/
|
||||||
|
public function sendEnergyLow(User $user, Character $character): Notification
|
||||||
|
{
|
||||||
|
return $this->send($user, 'energy_low', [
|
||||||
|
'character_name' => $character->name,
|
||||||
|
'energy' => $character->energy,
|
||||||
|
'max_energy' => $character->max_energy,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a health low notification
|
||||||
|
*/
|
||||||
|
public function sendHealthLow(User $user, Character $character): Notification
|
||||||
|
{
|
||||||
|
return $this->send($user, 'health_low', [
|
||||||
|
'character_name' => $character->name,
|
||||||
|
'health' => $character->health,
|
||||||
|
'max_health' => $character->max_health,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a character dead notification
|
||||||
|
*/
|
||||||
|
public function sendCharacterDead(User $user, Character $character): Notification
|
||||||
|
{
|
||||||
|
return $this->send($user, 'character_dead', [
|
||||||
|
'character_name' => $character->name,
|
||||||
|
'revive_cost' => config('game.health.hospital_cost', 1000),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a character revived notification
|
||||||
|
*/
|
||||||
|
public function sendCharacterRevived(User $user, Character $character): Notification
|
||||||
|
{
|
||||||
|
return $this->send($user, 'character_revived', [
|
||||||
|
'character_name' => $character->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unread notifications for a user
|
||||||
|
*/
|
||||||
|
public function getUnread(User $user): \Illuminate\Database\Eloquent\Collection
|
||||||
|
{
|
||||||
|
return $user->notifications()
|
||||||
|
->whereNull('read_at')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all notifications for a user
|
||||||
|
*/
|
||||||
|
public function getAll(User $user, int $limit = 20): \Illuminate\Database\Eloquent\Collection
|
||||||
|
{
|
||||||
|
return $user->notifications()
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->take($limit)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a notification as read
|
||||||
|
*/
|
||||||
|
public function markAsRead(Notification $notification): void
|
||||||
|
{
|
||||||
|
$notification->markAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all notifications as read for a user
|
||||||
|
*/
|
||||||
|
public function markAllAsRead(User $user): int
|
||||||
|
{
|
||||||
|
return $user->notifications()
|
||||||
|
->whereNull('read_at')
|
||||||
|
->update(['read_at' => now()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a notification
|
||||||
|
*/
|
||||||
|
public function delete(Notification $notification): bool
|
||||||
|
{
|
||||||
|
return $notification->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all read notifications for a user
|
||||||
|
*/
|
||||||
|
public function deleteAllRead(User $user): int
|
||||||
|
{
|
||||||
|
return $user->notifications()
|
||||||
|
->whereNotNull('read_at')
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the count of unread notifications for a user
|
||||||
|
*/
|
||||||
|
public function getUnreadCount(User $user): int
|
||||||
|
{
|
||||||
|
return $user->notifications()
|
||||||
|
->whereNull('read_at')
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AppLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.app');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class GuestLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.guest');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
// Game middleware
|
||||||
|
$middleware->alias([
|
||||||
|
'verify.character' => \App\Http\Middleware\Game\VerifyCharacter::class,
|
||||||
|
'check.energy' => \App\Http\Middleware\Game\CheckEnergy::class,
|
||||||
|
'check.jail' => \App\Http\Middleware\Game\CheckInJail::class,
|
||||||
|
'check.hospital' => \App\Http\Middleware\Game\CheckInHospital::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Game middleware group
|
||||||
|
$middleware->group('game', [
|
||||||
|
'auth',
|
||||||
|
'verify.character',
|
||||||
|
'check.jail',
|
||||||
|
'check.hospital',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
$exceptions->shouldRenderJsonWhen(
|
||||||
|
fn (Request $request) => $request->is('api/*'),
|
||||||
|
);
|
||||||
|
})->create();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
use App\Providers\GameServiceProvider;
|
||||||
|
use App\Providers\ScheduleServiceProvider;
|
||||||
|
|
||||||
|
return [
|
||||||
|
AppServiceProvider::class,
|
||||||
|
GameServiceProvider::class,
|
||||||
|
ScheduleServiceProvider::class,
|
||||||
|
];
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
services:
|
||||||
|
laravel.test:
|
||||||
|
build:
|
||||||
|
context: './vendor/laravel/sail/runtimes/8.5'
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
WWWGROUP: '${WWWGROUP}'
|
||||||
|
image: 'sail-8.5/app'
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
ports:
|
||||||
|
- '${APP_PORT:-80}:80'
|
||||||
|
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||||
|
environment:
|
||||||
|
WWWUSER: '${WWWUSER}'
|
||||||
|
LARAVEL_SAIL: 1
|
||||||
|
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||||
|
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||||
|
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||||
|
volumes:
|
||||||
|
- '.:/var/www/html'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
mysql:
|
||||||
|
image: 'mysql:8.4'
|
||||||
|
ports:
|
||||||
|
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
MYSQL_ROOT_HOST: '%'
|
||||||
|
MYSQL_DATABASE: '${DB_DATABASE}'
|
||||||
|
MYSQL_USER: '${DB_USERNAME}'
|
||||||
|
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||||
|
MYSQL_EXTRA_OPTIONS: '${MYSQL_EXTRA_OPTIONS:-}'
|
||||||
|
volumes:
|
||||||
|
- 'sail-mysql:/var/lib/mysql'
|
||||||
|
- './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- mysqladmin
|
||||||
|
- ping
|
||||||
|
- '-p${DB_PASSWORD}'
|
||||||
|
retries: 3
|
||||||
|
timeout: 5s
|
||||||
|
networks:
|
||||||
|
sail:
|
||||||
|
driver: bridge
|
||||||
|
volumes:
|
||||||
|
sail-mysql:
|
||||||
|
driver: local
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/laravel",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"framework"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/tinker": "^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/breeze": "^2.0",
|
||||||
|
"laravel/pint": "^1.27",
|
||||||
|
"laravel/sail": "^1.63",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"sebastian/environment": "^6.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install --ignore-scripts",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"npm run dev\" --names=server,queue,vite --kill-others"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi @no_additional_args",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
Generated
+6729
File diff suppressed because it is too large
Load Diff
+126
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the number of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "storage", "octane",
|
||||||
|
| "session", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'storage' => [
|
||||||
|
'driver' => 'storage',
|
||||||
|
'disk' => env('CACHE_STORAGE_DISK'),
|
||||||
|
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'stores' => [
|
||||||
|
'database',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Serializable Classes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the classes that can be unserialized from cache
|
||||||
|
| storage. By default, no PHP classes will be unserialized from your
|
||||||
|
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'serializable_classes' => false,
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pdo\Mysql;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Game Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file contains all the game-specific configuration values.
|
||||||
|
| These settings control the game mechanics, balance, and behavior.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('GAME_NAME', 'The Crime Syndicate'),
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'maintenance' => env('GAME_MAINTENANCE', false),
|
||||||
|
|
||||||
|
// Energy system configuration
|
||||||
|
'energy' => [
|
||||||
|
'start_amount' => 100,
|
||||||
|
'max' => 100,
|
||||||
|
'regen_rate' => 5, // Energy regenerated per interval
|
||||||
|
'regen_interval' => 300, // Seconds between regen (5 minutes)
|
||||||
|
'action_costs' => [
|
||||||
|
'crime' => 10,
|
||||||
|
'attack' => 15,
|
||||||
|
'travel' => 5,
|
||||||
|
'train' => 20,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
// Health system configuration
|
||||||
|
'health' => [
|
||||||
|
'start_amount' => 100,
|
||||||
|
'max' => 100,
|
||||||
|
'regen_rate' => 2, // Health regenerated per interval
|
||||||
|
'regen_interval' => 600, // Seconds between regen (10 minutes)
|
||||||
|
'hospital_cost' => 1000, // Cost to revive from hospital
|
||||||
|
'death_threshold' => 0, // Health <= 0 = dead
|
||||||
|
],
|
||||||
|
|
||||||
|
// Combat system configuration
|
||||||
|
'combat' => [
|
||||||
|
'base_damage' => 5,
|
||||||
|
'damage_variation' => 0.20, // +/- 20% random variation
|
||||||
|
'crit_chance' => 0.10, // 10% chance for critical hit
|
||||||
|
'crit_multiplier' => 2.0, // Critical hits do 2x damage
|
||||||
|
'defense_reduction' => 0.5, // Each defense point reduces damage by 0.5
|
||||||
|
'min_damage' => 1, // Minimum damage per hit
|
||||||
|
'attack_cooldown' => 300, // 5 minutes between attacks
|
||||||
|
'hit_list_cost' => 5000, // Cost to place a hit
|
||||||
|
],
|
||||||
|
|
||||||
|
// Crime system configuration
|
||||||
|
'crime' => [
|
||||||
|
'success_rate' => 0.70, // 70% base success rate
|
||||||
|
'failure_penalty' => 0.10, // 10% health loss on failure
|
||||||
|
'cooldown_reduction' => 0.10, // 10% cooldown reduction per level
|
||||||
|
'reward_multiplier' => 1.10, // 10% more reward per level
|
||||||
|
],
|
||||||
|
|
||||||
|
// Jail system configuration
|
||||||
|
'jail' => [
|
||||||
|
'sentence_length' => 3600, // 1 hour base sentence
|
||||||
|
'breakout_chance' => 0.05, // 5% chance to break out per attempt
|
||||||
|
'breakout_cost' => 1000, // Cost to attempt breakout
|
||||||
|
'sentence_multiplier' => 1.5, // Sentence increases by 50% for repeat offenses
|
||||||
|
],
|
||||||
|
|
||||||
|
// Economy configuration
|
||||||
|
'economy' => [
|
||||||
|
'bank_interest' => 0.05, // 5% daily interest
|
||||||
|
'tax_rate' => 0.10, // 10% tax on faction bank withdrawals
|
||||||
|
'start_money' => 5000, // Starting cash
|
||||||
|
'start_bank' => 0, // Starting bank balance
|
||||||
|
'max_bank' => 10000000, // Maximum bank balance
|
||||||
|
],
|
||||||
|
|
||||||
|
// Leveling system configuration
|
||||||
|
'leveling' => [
|
||||||
|
'exp_curve' => 'exponential', // 'linear' or 'exponential'
|
||||||
|
'exp_per_level' => 1000, // Base EXP per level
|
||||||
|
'exp_multiplier' => 1.20, // 20% more EXP needed per level
|
||||||
|
'stat_points_per_level' => 2, // Points to allocate per level
|
||||||
|
'max_level' => 100, // Maximum character level
|
||||||
|
],
|
||||||
|
|
||||||
|
// Skills configuration
|
||||||
|
'skills' => [
|
||||||
|
'strength' => [
|
||||||
|
'attack_bonus' => 0.5, // +0.5 damage per point
|
||||||
|
'max' => 100,
|
||||||
|
],
|
||||||
|
'defense' => [
|
||||||
|
'damage_reduction' => 0.5, // -0.5 damage per point
|
||||||
|
'max' => 100,
|
||||||
|
],
|
||||||
|
'speed' => [
|
||||||
|
'attack_first_chance' => 0.02, // +2% chance to attack first per point
|
||||||
|
'max' => 100,
|
||||||
|
],
|
||||||
|
'dexterity' => [
|
||||||
|
'crit_chance' => 0.005, // +0.5% crit chance per point
|
||||||
|
'max' => 100,
|
||||||
|
],
|
||||||
|
'training_cost' => 100, // Money cost to train a stat
|
||||||
|
'training_limit' => 5, // Max stat points per training session
|
||||||
|
],
|
||||||
|
|
||||||
|
// Faction configuration
|
||||||
|
'factions' => [
|
||||||
|
'create_cost' => 10000, // Cost to create a faction
|
||||||
|
'create_level' => 10, // Minimum level to create a faction
|
||||||
|
'max_members' => 50, // Maximum faction members
|
||||||
|
'respect_per_win' => 10, // Respect gained per war win
|
||||||
|
'respect_per_loss' => -5, // Respect lost per war loss
|
||||||
|
'bank_tax' => 0.05, // 5% tax on faction bank deposits
|
||||||
|
],
|
||||||
|
|
||||||
|
// Location configuration
|
||||||
|
'locations' => [
|
||||||
|
'start_location' => 1, // ID of starting location
|
||||||
|
'travel_cost_base' => 100, // Base travel cost
|
||||||
|
'travel_cost_per_level' => 10, // Additional cost per location level
|
||||||
|
],
|
||||||
|
|
||||||
|
// Game limits
|
||||||
|
'limits' => [
|
||||||
|
'max_inventory_items' => 100,
|
||||||
|
'max_equipped_items' => 10,
|
||||||
|
'max_mail_messages' => 50,
|
||||||
|
'max_market_listings' => 20,
|
||||||
|
'max_faction_chat_history' => 100,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Cooldowns (in seconds)
|
||||||
|
'cooldowns' => [
|
||||||
|
'default' => 60, // 1 minute default cooldown
|
||||||
|
'attack' => 300, // 5 minutes
|
||||||
|
'crime' => 120, // 2 minutes
|
||||||
|
'travel' => 60, // 1 minute
|
||||||
|
'train' => 300, // 5 minutes
|
||||||
|
'market_listing' => 60, // 1 minute
|
||||||
|
],
|
||||||
|
|
||||||
|
// Character statuses
|
||||||
|
'statuses' => [
|
||||||
|
'active' => 'active',
|
||||||
|
'in_hospital' => 'in_hospital',
|
||||||
|
'in_jail' => 'in_jail',
|
||||||
|
'dead' => 'dead',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Item types
|
||||||
|
'item_types' => [
|
||||||
|
'weapon' => 'weapon',
|
||||||
|
'armor' => 'armor',
|
||||||
|
'consumable' => 'consumable',
|
||||||
|
'misc' => 'misc',
|
||||||
|
'currency' => 'currency',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Crime difficulties
|
||||||
|
'crime_difficulties' => [
|
||||||
|
'easy' => 'easy',
|
||||||
|
'medium' => 'medium',
|
||||||
|
'hard' => 'hard',
|
||||||
|
'very_hard' => 'very_hard',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Attack results
|
||||||
|
'attack_results' => [
|
||||||
|
'win' => 'win',
|
||||||
|
'lose' => 'lose',
|
||||||
|
'draw' => 'draw',
|
||||||
|
'escape' => 'escape',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the connection options for every queue backend
|
||||||
|
| used by your application. An example configuration is provided for
|
||||||
|
| each backend supported by Laravel. You're also free to add more.
|
||||||
|
|
|
||||||
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||||
|
| "deferred", "background", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sync' => [
|
||||||
|
'driver' => 'sync',
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||||
|
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||||
|
'queue' => env('DB_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'beanstalkd' => [
|
||||||
|
'driver' => 'beanstalkd',
|
||||||
|
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||||
|
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => 0,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqs' => [
|
||||||
|
'driver' => 'sqs',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||||
|
'queue' => env('SQS_QUEUE', 'default'),
|
||||||
|
'suffix' => env('SQS_SUFFIX'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => null,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'deferred' => [
|
||||||
|
'driver' => 'deferred',
|
||||||
|
],
|
||||||
|
|
||||||
|
'background' => [
|
||||||
|
'driver' => 'background',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'connections' => [
|
||||||
|
'database',
|
||||||
|
'deferred',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Batching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following options configure the database and table that store job
|
||||||
|
| batching information. These options can be updated to any database
|
||||||
|
| connection and table which has been defined by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'batching' => [
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'job_batches',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Failed Queue Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure the behavior of failed queue job logging so you
|
||||||
|
| can control how and where failed jobs are stored. Laravel ships with
|
||||||
|
| support for storing failed jobs in a simple file or in a database.
|
||||||
|
|
|
||||||
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'failed' => [
|
||||||
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'failed_jobs',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'key' => env('RESEND_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'notifications' => [
|
||||||
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Lifetime
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the number of minutes that you wish the session
|
||||||
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
|
| to expire immediately when the browser is closed then you may
|
||||||
|
| indicate that via the expire_on_close configuration option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Encryption
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to easily specify that all of your session data
|
||||||
|
| should be encrypted before it's stored. All encryption is performed
|
||||||
|
| automatically by Laravel and you may use the session like normal.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session File Location
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the "file" session driver, the session files are placed
|
||||||
|
| on disk. The default storage location is defined here; however, you
|
||||||
|
| are free to provide another location where they should be stored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" or "redis" session drivers, you may specify a
|
||||||
|
| connection that should be used to manage these sessions. This should
|
||||||
|
| correspond to a connection in your database configuration options.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connection' => env('SESSION_CONNECTION'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" session driver, you may specify the table to
|
||||||
|
| be used to store sessions. Of course, a sensible default is defined
|
||||||
|
| for you; however, you're welcome to change this to another table.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using one of the framework's cache driven session backends, you may
|
||||||
|
| define the cache store which should be used to store the session data
|
||||||
|
| between requests. This must match one of your defined cache stores.
|
||||||
|
|
|
||||||
|
| Affects: "dynamodb", "memcached", "redis"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Sweeping Lottery
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some session drivers must manually sweep their storage location to get
|
||||||
|
| rid of old sessions from storage. Here are the chances that it will
|
||||||
|
| happen on a given request. By default, the odds are 2 out of 100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may change the name of the session cookie that is created by
|
||||||
|
| the framework. Typically, you should not need to change this value
|
||||||
|
| since doing so does not grant a meaningful security improvement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cookie' => env(
|
||||||
|
'SESSION_COOKIE',
|
||||||
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The session cookie path determines the path for which the cookie will
|
||||||
|
| be regarded as available. Typically, this will be the root path of
|
||||||
|
| your application, but you're free to change this when necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the domain and subdomains the session cookie is
|
||||||
|
| available to. By default, the cookie will be available to the root
|
||||||
|
| domain without subdomains. Typically, this shouldn't be changed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTPS Only Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By setting this option to true, session cookies will only be sent back
|
||||||
|
| to the server if the browser has a HTTPS connection. This will keep
|
||||||
|
| the cookie from being sent to you when it can't be done securely.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTP Access Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
|
| value of the cookie and the cookie will only be accessible through
|
||||||
|
| the HTTP protocol. It's unlikely you should disable this option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Same-Site Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines how your cookies behave when cross-site requests
|
||||||
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
|
| will set this value to "lax" to permit secure cross-site requests.
|
||||||
|
|
|
||||||
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||||
|
|
|
||||||
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Partitioned Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will tie the cookie to the top-level site for
|
||||||
|
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||||
|
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Serialization
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the serialization strategy for session data, which
|
||||||
|
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||||
|
| objects in the session but can make an application vulnerable to
|
||||||
|
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||||
|
|
|
||||||
|
| Supported: "json", "php"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'serialization' => 'json',
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Enums\CharacterStatus;
|
||||||
|
use App\Models\Character;
|
||||||
|
use App\Models\Location;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<Character>
|
||||||
|
*/
|
||||||
|
class CharacterFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$genders = ['male', 'female', 'other'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->unique()->firstName() . ' ' . $this->faker->lastName(),
|
||||||
|
'gender' => $genders[array_rand($genders)],
|
||||||
|
'level' => $this->faker->numberBetween(1, 10),
|
||||||
|
'experience' => $this->faker->numberBetween(0, 5000),
|
||||||
|
'money' => $this->faker->numberBetween(1000, 50000),
|
||||||
|
'bank' => $this->faker->numberBetween(0, 100000),
|
||||||
|
'energy' => $this->faker->numberBetween(50, 100),
|
||||||
|
'max_energy' => 100,
|
||||||
|
'health' => $this->faker->numberBetween(50, 100),
|
||||||
|
'max_health' => 100,
|
||||||
|
'strength' => $this->faker->numberBetween(5, 20),
|
||||||
|
'defense' => $this->faker->numberBetween(5, 20),
|
||||||
|
'speed' => $this->faker->numberBetween(5, 20),
|
||||||
|
'dexterity' => $this->faker->numberBetween(5, 20),
|
||||||
|
'location_id' => Location::factory(),
|
||||||
|
'faction_id' => null,
|
||||||
|
'in_hospital_until' => null,
|
||||||
|
'in_jail_until' => null,
|
||||||
|
'last_action_at' => now(),
|
||||||
|
'status' => CharacterStatus::ACTIVE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a character with specific stats.
|
||||||
|
*/
|
||||||
|
public function withStats(int $strength, int $defense, int $speed, int $dexterity): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'strength' => $strength,
|
||||||
|
'defense' => $defense,
|
||||||
|
'speed' => $speed,
|
||||||
|
'dexterity' => $dexterity,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a character at a specific level.
|
||||||
|
*/
|
||||||
|
public function atLevel(int $level): static
|
||||||
|
{
|
||||||
|
$experience = 0;
|
||||||
|
if ($level > 1) {
|
||||||
|
$base = config('game.leveling.exp_per_level', 1000);
|
||||||
|
$multiplier = config('game.leveling.exp_multiplier', 1.20);
|
||||||
|
for ($i = 2; $i <= $level; $i++) {
|
||||||
|
$experience += (int) ($base * pow($multiplier, $i - 2));
|
||||||
|
}
|
||||||
|
// Add some extra to be in the middle of the level
|
||||||
|
$experience += (int) ($base * pow($multiplier, $level - 1) * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'level' => $level,
|
||||||
|
'experience' => $experience,
|
||||||
|
'max_health' => 100 + (($level - 1) * 5),
|
||||||
|
'max_energy' => 100 + (($level - 1) * 5),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a character with full health and energy.
|
||||||
|
*/
|
||||||
|
public function fresh(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'health' => $attributes['max_health'] ?? 100,
|
||||||
|
'energy' => $attributes['max_energy'] ?? 100,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a character in hospital.
|
||||||
|
*/
|
||||||
|
public function inHospital(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'health' => 1,
|
||||||
|
'status' => CharacterStatus::IN_HOSPITAL,
|
||||||
|
'in_hospital_until' => now()->addHours(1),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a dead character.
|
||||||
|
*/
|
||||||
|
public function dead(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'health' => 0,
|
||||||
|
'status' => CharacterStatus::DEAD,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a character in jail.
|
||||||
|
*/
|
||||||
|
public function inJail(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'status' => CharacterStatus::IN_JAIL,
|
||||||
|
'in_jail_until' => now()->addHours(1),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Character;
|
||||||
|
use App\Models\Inventory;
|
||||||
|
use App\Models\Item;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<Inventory>
|
||||||
|
*/
|
||||||
|
class InventoryFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'character_id' => Character::factory(),
|
||||||
|
'item_id' => Item::factory(),
|
||||||
|
'quantity' => $this->faker->numberBetween(1, 10),
|
||||||
|
'equipped' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an equipped item.
|
||||||
|
*/
|
||||||
|
public function equipped(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'equipped' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create with a specific quantity.
|
||||||
|
*/
|
||||||
|
public function withQuantity(int $quantity): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'quantity' => $quantity,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Location;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<Location>
|
||||||
|
*/
|
||||||
|
class LocationFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->unique()->city() . ' ' . $this->faker->randomElement(['District', 'Area', 'Sector', 'Zone']),
|
||||||
|
'slug' => $this->faker->unique()->slug(2),
|
||||||
|
'description' => $this->faker->sentence(),
|
||||||
|
'travel_cost' => $this->faker->numberBetween(100, 5000),
|
||||||
|
'requires_level' => $this->faker->randomElement([null, 1, 3, 5, 10, 15]),
|
||||||
|
'faction_control' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a starting location.
|
||||||
|
*/
|
||||||
|
public function starting(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'name' => 'Downtown',
|
||||||
|
'slug' => 'downtown',
|
||||||
|
'description' => 'The bustling city center with plenty of opportunities.',
|
||||||
|
'travel_cost' => 0,
|
||||||
|
'requires_level' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<User>
|
||||||
|
*/
|
||||||
|
class UserFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current password being used by the factory.
|
||||||
|
*/
|
||||||
|
protected static ?string $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->name(),
|
||||||
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model's email address should be unverified.
|
||||||
|
*/
|
||||||
|
public function unverified(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'email_verified_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('password');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->bigInteger('expiration')->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->bigInteger('expiration')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedSmallInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('name');
|
||||||
|
$table->integer('total_jobs');
|
||||||
|
$table->integer('pending_jobs');
|
||||||
|
$table->integer('failed_jobs');
|
||||||
|
$table->longText('failed_job_ids');
|
||||||
|
$table->mediumText('options')->nullable();
|
||||||
|
$table->integer('cancelled_at')->nullable();
|
||||||
|
$table->integer('created_at');
|
||||||
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uuid')->unique();
|
||||||
|
$table->string('connection');
|
||||||
|
$table->string('queue');
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->longText('exception');
|
||||||
|
$table->timestamp('failed_at')->useCurrent();
|
||||||
|
|
||||||
|
$table->index(['connection', 'queue', 'failed_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('jobs');
|
||||||
|
Schema::dropIfExists('job_batches');
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('locations', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name', 100);
|
||||||
|
$table->string('slug', 100)->unique();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->unsignedInteger('travel_cost')->default(0);
|
||||||
|
$table->unsignedInteger('requires_level')->nullable()->default(null);
|
||||||
|
$table->unsignedBigInteger('faction_control')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('slug');
|
||||||
|
$table->index('requires_level');
|
||||||
|
$table->index('faction_control');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('locations');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\CharacterStatus;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('characters', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('name', 50);
|
||||||
|
$table->enum('gender', ['male', 'female', 'other']);
|
||||||
|
$table->unsignedInteger('level')->default(1);
|
||||||
|
$table->unsignedBigInteger('experience')->default(0);
|
||||||
|
$table->unsignedBigInteger('money')->default(5000);
|
||||||
|
$table->unsignedBigInteger('bank')->default(0);
|
||||||
|
$table->unsignedInteger('energy')->default(100);
|
||||||
|
$table->unsignedInteger('max_energy')->default(100);
|
||||||
|
$table->unsignedInteger('health')->default(100);
|
||||||
|
$table->unsignedInteger('max_health')->default(100);
|
||||||
|
$table->unsignedInteger('strength')->default(5);
|
||||||
|
$table->unsignedInteger('defense')->default(5);
|
||||||
|
$table->unsignedInteger('speed')->default(5);
|
||||||
|
$table->unsignedInteger('dexterity')->default(5);
|
||||||
|
$table->foreignId('location_id')->constrained();
|
||||||
|
$table->unsignedBigInteger('faction_id')->nullable();
|
||||||
|
$table->timestamp('in_hospital_until')->nullable();
|
||||||
|
$table->timestamp('in_jail_until')->nullable();
|
||||||
|
$table->timestamp('last_action_at')->nullable();
|
||||||
|
$table->enum('status', CharacterStatus::values())->default(CharacterStatus::ACTIVE->value);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('user_id');
|
||||||
|
$table->index('name');
|
||||||
|
$table->index('level');
|
||||||
|
$table->index('location_id');
|
||||||
|
$table->index('faction_id');
|
||||||
|
$table->index('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('characters');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\ItemType;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name', 100);
|
||||||
|
$table->string('slug', 100)->unique();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->enum('type', ItemType::values());
|
||||||
|
$table->unsignedInteger('value')->default(0);
|
||||||
|
$table->unsignedInteger('buy_price')->nullable()->default(null);
|
||||||
|
$table->unsignedInteger('sell_price')->nullable()->default(null);
|
||||||
|
$table->unsignedInteger('requires_level')->nullable()->default(null);
|
||||||
|
$table->integer('attack_bonus')->default(0);
|
||||||
|
$table->integer('defense_bonus')->default(0);
|
||||||
|
$table->integer('health_bonus')->default(0);
|
||||||
|
$table->integer('energy_bonus')->default(0);
|
||||||
|
$table->boolean('is_tradeable')->default(true);
|
||||||
|
$table->boolean('is_usable')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('slug');
|
||||||
|
$table->index('type');
|
||||||
|
$table->index('requires_level');
|
||||||
|
$table->index('buy_price');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('items');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\CrimeDifficulty;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('crimes', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name', 100);
|
||||||
|
$table->string('slug', 100)->unique();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->enum('difficulty', CrimeDifficulty::values())->default(CrimeDifficulty::EASY->value);
|
||||||
|
$table->float('success_rate')->default(0.70);
|
||||||
|
$table->unsignedInteger('reward_min')->default(0);
|
||||||
|
$table->unsignedInteger('reward_max')->default(0);
|
||||||
|
$table->unsignedInteger('experience')->default(0);
|
||||||
|
$table->unsignedInteger('energy_cost')->default(10);
|
||||||
|
$table->unsignedInteger('cooldown')->default(120); // seconds
|
||||||
|
$table->unsignedInteger('requires_level')->nullable()->default(null);
|
||||||
|
$table->foreignId('location_id')->nullable()->constrained();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('slug');
|
||||||
|
$table->index('difficulty');
|
||||||
|
$table->index('requires_level');
|
||||||
|
$table->index('location_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('crimes');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('crime_attempts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('character_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('crime_id')->constrained();
|
||||||
|
$table->boolean('success')->default(false);
|
||||||
|
$table->unsignedBigInteger('reward')->default(0);
|
||||||
|
$table->unsignedBigInteger('experience_gained')->default(0);
|
||||||
|
$table->timestamp('attempted_at')->useCurrent();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('character_id');
|
||||||
|
$table->index('crime_id');
|
||||||
|
$table->index('success');
|
||||||
|
$table->index('attempted_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('crime_attempts');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('activity_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('character_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('action', 100);
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->string('user_agent')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('character_id');
|
||||||
|
$table->index('action');
|
||||||
|
$table->index('created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('activity_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('notifications', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('type', 100);
|
||||||
|
$table->json('data')->nullable();
|
||||||
|
$table->timestamp('read_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('user_id');
|
||||||
|
$table->index('type');
|
||||||
|
$table->index('read_at');
|
||||||
|
$table->index('created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('notifications');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('inventories', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('character_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('item_id')->constrained();
|
||||||
|
$table->unsignedInteger('quantity')->default(1);
|
||||||
|
$table->boolean('equipped')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('character_id');
|
||||||
|
$table->index('item_id');
|
||||||
|
$table->index('equipped');
|
||||||
|
|
||||||
|
// Unique constraint: one inventory entry per character per item
|
||||||
|
$table->unique(['character_id', 'item_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('inventories');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Crime;
|
||||||
|
use App\Models\Location;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class CrimeSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$downtown = Location::where('slug', 'downtown')->first();
|
||||||
|
$industrial = Location::where('slug', 'industrial-district')->first();
|
||||||
|
$waterfront = Location::where('slug', 'waterfront')->first();
|
||||||
|
|
||||||
|
$crimes = [
|
||||||
|
// Downtown Crimes
|
||||||
|
[
|
||||||
|
'name' => 'Pickpocket',
|
||||||
|
'slug' => 'pickpocket',
|
||||||
|
'description' => 'Steal money from an unsuspecting pedestrian.',
|
||||||
|
'difficulty' => 'easy',
|
||||||
|
'success_rate' => 0.80,
|
||||||
|
'reward_min' => 100,
|
||||||
|
'reward_max' => 500,
|
||||||
|
'experience' => 50,
|
||||||
|
'energy_cost' => 10,
|
||||||
|
'cooldown' => 60,
|
||||||
|
'requires_level' => 1,
|
||||||
|
'location_id' => $downtown?->id ?? null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Mugging',
|
||||||
|
'slug' => 'mugging',
|
||||||
|
'description' => 'Rob someone at knife-point.',
|
||||||
|
'difficulty' => 'medium',
|
||||||
|
'success_rate' => 0.70,
|
||||||
|
'reward_min' => 500,
|
||||||
|
'reward_max' => 1500,
|
||||||
|
'experience' => 100,
|
||||||
|
'energy_cost' => 15,
|
||||||
|
'cooldown' => 120,
|
||||||
|
'requires_level' => 3,
|
||||||
|
'location_id' => $downtown?->id ?? null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Shoplifting',
|
||||||
|
'slug' => 'shoplifting',
|
||||||
|
'description' => 'Steal items from a store.',
|
||||||
|
'difficulty' => 'medium',
|
||||||
|
'success_rate' => 0.65,
|
||||||
|
'reward_min' => 200,
|
||||||
|
'reward_max' => 1000,
|
||||||
|
'experience' => 75,
|
||||||
|
'energy_cost' => 12,
|
||||||
|
'cooldown' => 120,
|
||||||
|
'requires_level' => 1,
|
||||||
|
'location_id' => $downtown?->id ?? null,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Industrial District Crimes
|
||||||
|
[
|
||||||
|
'name' => 'Factory Theft',
|
||||||
|
'slug' => 'factory-theft',
|
||||||
|
'description' => 'Steal goods from a factory warehouse.',
|
||||||
|
'difficulty' => 'medium',
|
||||||
|
'success_rate' => 0.60,
|
||||||
|
'reward_min' => 1000,
|
||||||
|
'reward_max' => 3000,
|
||||||
|
'experience' => 150,
|
||||||
|
'energy_cost' => 20,
|
||||||
|
'cooldown' => 180,
|
||||||
|
'requires_level' => 5,
|
||||||
|
'location_id' => $industrial?->id ?? null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Equipment Sabotage',
|
||||||
|
'slug' => 'equipment-sabotage',
|
||||||
|
'description' => 'Sabotage factory equipment for a fee.',
|
||||||
|
'difficulty' => 'hard',
|
||||||
|
'success_rate' => 0.50,
|
||||||
|
'reward_min' => 2000,
|
||||||
|
'reward_max' => 5000,
|
||||||
|
'experience' => 200,
|
||||||
|
'energy_cost' => 25,
|
||||||
|
'cooldown' => 300,
|
||||||
|
'requires_level' => 8,
|
||||||
|
'location_id' => $industrial?->id ?? null,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Waterfront Crimes
|
||||||
|
[
|
||||||
|
'name' => 'Smuggling',
|
||||||
|
'slug' => 'smuggling',
|
||||||
|
'description' => 'Smuggle illegal goods past customs.',
|
||||||
|
'difficulty' => 'hard',
|
||||||
|
'success_rate' => 0.55,
|
||||||
|
'reward_min' => 3000,
|
||||||
|
'reward_max' => 8000,
|
||||||
|
'experience' => 250,
|
||||||
|
'energy_cost' => 30,
|
||||||
|
'cooldown' => 300,
|
||||||
|
'requires_level' => 10,
|
||||||
|
'location_id' => $waterfront?->id ?? null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Piracy',
|
||||||
|
'slug' => 'piracy',
|
||||||
|
'description' => 'Hijack a shipment at sea.',
|
||||||
|
'difficulty' => 'very_hard',
|
||||||
|
'success_rate' => 0.40,
|
||||||
|
'reward_min' => 5000,
|
||||||
|
'reward_max' => 15000,
|
||||||
|
'experience' => 400,
|
||||||
|
'energy_cost' => 40,
|
||||||
|
'cooldown' => 600,
|
||||||
|
'requires_level' => 15,
|
||||||
|
'location_id' => $waterfront?->id ?? null,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Generic Crimes (available everywhere)
|
||||||
|
[
|
||||||
|
'name' => 'Car Theft',
|
||||||
|
'slug' => 'car-theft',
|
||||||
|
'description' => 'Steal a car and sell it for parts.',
|
||||||
|
'difficulty' => 'medium',
|
||||||
|
'success_rate' => 0.75,
|
||||||
|
'reward_min' => 2000,
|
||||||
|
'reward_max' => 4000,
|
||||||
|
'experience' => 120,
|
||||||
|
'energy_cost' => 20,
|
||||||
|
'cooldown' => 180,
|
||||||
|
'requires_level' => 5,
|
||||||
|
'location_id' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Burglary',
|
||||||
|
'slug' => 'burglary',
|
||||||
|
'description' => 'Break into a house and steal valuables.',
|
||||||
|
'difficulty' => 'hard',
|
||||||
|
'success_rate' => 0.60,
|
||||||
|
'reward_min' => 3000,
|
||||||
|
'reward_max' => 6000,
|
||||||
|
'experience' => 200,
|
||||||
|
'energy_cost' => 25,
|
||||||
|
'cooldown' => 300,
|
||||||
|
'requires_level' => 7,
|
||||||
|
'location_id' => null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($crimes as $crime) {
|
||||||
|
Crime::updateOrCreate(
|
||||||
|
['slug' => $crime['slug']],
|
||||||
|
$crime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class DatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the application's database.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Seed locations first (required by other seeders)
|
||||||
|
$this->call([
|
||||||
|
LocationSeeder::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Then seed items and crimes
|
||||||
|
$this->call([
|
||||||
|
ItemSeeder::class,
|
||||||
|
CrimeSeeder::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create test user
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => 'Test User',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Optionally create a test character for the user
|
||||||
|
// This can be done manually through the UI or via a separate seeder
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Item;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class ItemSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$items = [
|
||||||
|
// Weapons
|
||||||
|
[
|
||||||
|
'name' => 'Knife',
|
||||||
|
'slug' => 'knife',
|
||||||
|
'description' => 'A basic knife for self-defense.',
|
||||||
|
'type' => 'weapon',
|
||||||
|
'value' => 100,
|
||||||
|
'buy_price' => 500,
|
||||||
|
'sell_price' => 250,
|
||||||
|
'requires_level' => 1,
|
||||||
|
'attack_bonus' => 5,
|
||||||
|
'defense_bonus' => 0,
|
||||||
|
'health_bonus' => 0,
|
||||||
|
'energy_bonus' => 0,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Baseball Bat',
|
||||||
|
'slug' => 'baseball-bat',
|
||||||
|
'description' => 'A sturdy bat that can deal serious damage.',
|
||||||
|
'type' => 'weapon',
|
||||||
|
'value' => 500,
|
||||||
|
'buy_price' => 2500,
|
||||||
|
'sell_price' => 1250,
|
||||||
|
'requires_level' => 3,
|
||||||
|
'attack_bonus' => 15,
|
||||||
|
'defense_bonus' => 0,
|
||||||
|
'health_bonus' => 0,
|
||||||
|
'energy_bonus' => 0,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Pistol',
|
||||||
|
'slug' => 'pistol',
|
||||||
|
'description' => 'A handgun for more serious encounters.',
|
||||||
|
'type' => 'weapon',
|
||||||
|
'value' => 2000,
|
||||||
|
'buy_price' => 10000,
|
||||||
|
'sell_price' => 5000,
|
||||||
|
'requires_level' => 10,
|
||||||
|
'attack_bonus' => 30,
|
||||||
|
'defense_bonus' => 0,
|
||||||
|
'health_bonus' => 0,
|
||||||
|
'energy_bonus' => 0,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Armor
|
||||||
|
[
|
||||||
|
'name' => 'Leather Jacket',
|
||||||
|
'slug' => 'leather-jacket',
|
||||||
|
'description' => 'Basic protection from attacks.',
|
||||||
|
'type' => 'armor',
|
||||||
|
'value' => 200,
|
||||||
|
'buy_price' => 1000,
|
||||||
|
'sell_price' => 500,
|
||||||
|
'requires_level' => 1,
|
||||||
|
'attack_bonus' => 0,
|
||||||
|
'defense_bonus' => 5,
|
||||||
|
'health_bonus' => 0,
|
||||||
|
'energy_bonus' => 0,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Kevlar Vest',
|
||||||
|
'slug' => 'kevlar-vest',
|
||||||
|
'description' => 'Heavy body armor for serious protection.',
|
||||||
|
'type' => 'armor',
|
||||||
|
'value' => 1000,
|
||||||
|
'buy_price' => 5000,
|
||||||
|
'sell_price' => 2500,
|
||||||
|
'requires_level' => 8,
|
||||||
|
'attack_bonus' => 0,
|
||||||
|
'defense_bonus' => 25,
|
||||||
|
'health_bonus' => 0,
|
||||||
|
'energy_bonus' => 0,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Consumables
|
||||||
|
[
|
||||||
|
'name' => 'First Aid Kit',
|
||||||
|
'slug' => 'first-aid-kit',
|
||||||
|
'description' => 'Restores 50 health when used.',
|
||||||
|
'type' => 'consumable',
|
||||||
|
'value' => 500,
|
||||||
|
'buy_price' => 2500,
|
||||||
|
'sell_price' => 1000,
|
||||||
|
'requires_level' => 1,
|
||||||
|
'attack_bonus' => 0,
|
||||||
|
'defense_bonus' => 0,
|
||||||
|
'health_bonus' => 50,
|
||||||
|
'energy_bonus' => 0,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Energy Drink',
|
||||||
|
'slug' => 'energy-drink',
|
||||||
|
'description' => 'Restores 30 energy when used.',
|
||||||
|
'type' => 'consumable',
|
||||||
|
'value' => 300,
|
||||||
|
'buy_price' => 1500,
|
||||||
|
'sell_price' => 750,
|
||||||
|
'requires_level' => 1,
|
||||||
|
'attack_bonus' => 0,
|
||||||
|
'defense_bonus' => 0,
|
||||||
|
'health_bonus' => 0,
|
||||||
|
'energy_bonus' => 30,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Mega Health Pack',
|
||||||
|
'slug' => 'mega-health-pack',
|
||||||
|
'description' => 'Restores all health when used.',
|
||||||
|
'type' => 'consumable',
|
||||||
|
'value' => 1500,
|
||||||
|
'buy_price' => 7500,
|
||||||
|
'sell_price' => 3000,
|
||||||
|
'requires_level' => 15,
|
||||||
|
'attack_bonus' => 0,
|
||||||
|
'defense_bonus' => 0,
|
||||||
|
'health_bonus' => 100,
|
||||||
|
'energy_bonus' => 0,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
[
|
||||||
|
'name' => 'Lockpick Set',
|
||||||
|
'slug' => 'lockpick-set',
|
||||||
|
'description' => 'Useful for picking locks.',
|
||||||
|
'type' => 'misc',
|
||||||
|
'value' => 200,
|
||||||
|
'buy_price' => 1000,
|
||||||
|
'sell_price' => 500,
|
||||||
|
'requires_level' => 5,
|
||||||
|
'attack_bonus' => 0,
|
||||||
|
'defense_bonus' => 0,
|
||||||
|
'health_bonus' => 0,
|
||||||
|
'energy_bonus' => 0,
|
||||||
|
'is_tradeable' => true,
|
||||||
|
'is_usable' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
Item::updateOrCreate(
|
||||||
|
['slug' => $item['slug']],
|
||||||
|
$item
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Location;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class LocationSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$locations = [
|
||||||
|
[
|
||||||
|
'name' => 'Downtown',
|
||||||
|
'slug' => 'downtown',
|
||||||
|
'description' => 'The bustling city center with plenty of opportunities for crime.',
|
||||||
|
'travel_cost' => 0,
|
||||||
|
'requires_level' => null,
|
||||||
|
'faction_control' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Industrial District',
|
||||||
|
'slug' => 'industrial-district',
|
||||||
|
'description' => 'Factories and warehouses provide cover for illegal activities.',
|
||||||
|
'travel_cost' => 500,
|
||||||
|
'requires_level' => 5,
|
||||||
|
'faction_control' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Waterfront',
|
||||||
|
'slug' => 'waterfront',
|
||||||
|
'description' => 'The docks are perfect for smuggling operations.',
|
||||||
|
'travel_cost' => 1000,
|
||||||
|
'requires_level' => 10,
|
||||||
|
'faction_control' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Uptown',
|
||||||
|
'slug' => 'uptown',
|
||||||
|
'description' => 'The wealthy district. High rewards but high risks.',
|
||||||
|
'travel_cost' => 1500,
|
||||||
|
'requires_level' => 15,
|
||||||
|
'faction_control' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Suburbs',
|
||||||
|
'slug' => 'suburbs',
|
||||||
|
'description' => 'Quiet residential area. Less police presence but fewer opportunities.',
|
||||||
|
'travel_cost' => 300,
|
||||||
|
'requires_level' => 3,
|
||||||
|
'faction_control' => null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($locations as $location) {
|
||||||
|
Location::updateOrCreate(
|
||||||
|
['slug' => $location['slug']],
|
||||||
|
$location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2989
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"alpinejs": "^3.4.2",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"laravel-vite-plugin": "^3.1",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"tailwindcss": "^3.1.0",
|
||||||
|
"vite": "^8.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.18.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory>tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>app</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
<env name="DB_DATABASE" value="testing"/>
|
||||||
|
<env name="DB_URL" value=""/>
|
||||||
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
|
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews -Indexes
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Handle Authorization Header
|
||||||
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
# Handle X-XSRF-Token Header
|
||||||
|
RewriteCond %{HTTP:x-xsrf-token} .
|
||||||
|
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||||
|
|
||||||
|
# Redirect Trailing Slashes If Not A Folder...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [L,R=301]
|
||||||
|
|
||||||
|
# Send Requests To Front Controller...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
</IfModule>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Determine if the application is in maintenance mode...
|
||||||
|
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||||
|
require $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the request...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
|
$app->handleRequest(Request::capture());
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user